From 29b620806b77e9673c5c28bfdbd2413c6714d365 Mon Sep 17 00:00:00 2001 From: Guntbert Reiter Date: Mon, 7 Sep 2020 19:10:32 +0200 Subject: [PATCH 01/92] Correct sentence in README (#1933) questions (not answers) get answers :-) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 38e298282..33be1f5e9 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ The nightly build from master is [available on Google Play](https://play.google. ### Support -Check out our [FAQs](https://github.com/tuskyapp/faq), your answer may already be answered. +Check out our [FAQs](https://github.com/tuskyapp/faq), your question may already be answered. If you have any bug reports, feature requests or questions please open an issue or send us a toot at [Tusky@mastodon.social](https://mastodon.social/@Tusky)! For translating Tusky into your language, visit https://weblate.tusky.app/ From 003263b267ae0a3fd0e15314271cfeaf7cbae564 Mon Sep 17 00:00:00 2001 From: Vancha Date: Mon, 3 May 2021 01:48:11 +0000 Subject: [PATCH 02/92] Translated using Weblate (Dutch) Currently translated at 98.0% (451 of 460 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/nl/ Translated using Weblate (Dutch) Currently translated at 97.8% (450 of 460 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/nl/ --- app/src/main/res/values-nl/strings.xml | 51 ++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index cfa8e7f08..7ddfca08c 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -216,6 +216,7 @@ %1$s, %2$s en %3$s %1$s en %2$s + %d nieuwe interactie %d nieuwe interacties Besloten account @@ -321,6 +322,7 @@ %1$s en %2$s %1$s, %2$s en %3$d meer + maximum van %1$d tab bereikt maximum van %1$d tabs bereikt Media: %s @@ -459,4 +461,53 @@ Om in te plannen moet je in Mastodon een minimum interval van 5 minuten gebruiken. Volgverzoeken Hashtags + Bijlage + volgverzoek verstuurd + Afmelden + Abonneren + De toot waarvoor jij een antwoord had opgesteld, is verwijderd + Het is versturen van deze toot is mislukt! + Weet je zeker dat je de lijst %s wil verwijderen\? + + Je kan niet meer dan %1$d media bijlage toevoegen. + Je kan niet meer dan %1$d media bijlagen toevoegen. + + Notificaties op tijdlijn beperken + Opgeslagen! + Jou eigen opmerkingen over dit account + Welzijn + De titel van de bovenste statusbalk verbergen + Bevestigingsdialoogvenster weergeven voor boosten + Link previews in tijdlijnen weergeven + Er zijn geen aankondigingen. + Oneindig + Looptijd + Swipe gebaar inschakelen tussen tabs + + %s persoon + %s personen + + Hashtag toevoegen + Geluid + Notificaties wanneer iemand waar je op geabonneerd bent een nieuwe toot plaatst + Nieuwe toots + Notificaties over volgverzoeken + Onder + Boven + Niet standaard emojis animeren + Kleurverloop weergeven voor verborgen media + iemand waar ik op geabonneerd ben heeft een nieuwe toot geplaatst + Notificaties verbergen + \@%s dempen\? + \@%s blokkeren\? + Gesprek dempen opheffen + Gesprek dempen + %s niet meer dempen + Notificaties van %s dempen + Notificaties van %s niet meer dempen + %s niet meer dempen + %s heeft net een bericht geplaatst + %s verzoekt u te volgen + Aankondigingen + Notificaties bekijken \ No newline at end of file From f8e9bf575ddd5a6a36ac9c005acec54bd3e95efb Mon Sep 17 00:00:00 2001 From: SPevSand Date: Mon, 3 May 2021 01:48:11 +0000 Subject: [PATCH 03/92] Translated using Weblate (Russian) Currently translated at 95.4% (439 of 460 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/ru/ --- app/src/main/res/values-ru/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 1b43d7236..bf1eec125 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -523,4 +523,5 @@ Продолжительность Вложения Аудио + %s только что опубликовал(а) \ No newline at end of file From bf306767458ab92de2193368179a74c445200afa Mon Sep 17 00:00:00 2001 From: joenepraat Date: Mon, 3 May 2021 01:48:11 +0000 Subject: [PATCH 04/92] Translated using Weblate (Dutch) Currently translated at 99.3% (457 of 460 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/nl/ --- app/src/main/res/values-nl/strings.xml | 76 ++++++++++++++------------ 1 file changed, 41 insertions(+), 35 deletions(-) diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 7ddfca08c..4ff5a90e0 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -18,7 +18,7 @@ Er is toestemming nodig om media op te slaan. Afbeeldingen en video\'s kunnen niet allebei aan dezelfde toot worden toegevoegd. Uploaden mislukt. - Fout tijdens verzenden toot, + Fout tijdens verzenden toot. Start Meldingen Lokaal @@ -53,7 +53,7 @@ %s markeerde jouw toot als favoriet %s volgt jou Rapporteer @%s - Extra opmerkingen + Extra opmerkingen\? Snelle reactie Reageren Boosten @@ -398,16 +398,16 @@ Poll met keuzes: %s, %s, %s, %s; %s Acties voor afbeelding %s - %d dag over - %d dagen over + %d dag te gaan + %d dagen te gaan - %d uur over - %d uur over + %d uur te gaan + %d uur te gaan - %d minuut over - %d minuten over + %d minuut te gaan + %d minuten te gaan %d seconde over @@ -456,58 +456,64 @@ Zoeken mislukt Poll Fout tijdens opzoeken toot %s - Je hebt nog geen concepten - Je hebt nog geen ingeplande toots + Je hebt nog geen concepten. + Je hebt nog geen ingeplande toots. Om in te plannen moet je in Mastodon een minimum interval van 5 minuten gebruiken. Volgverzoeken Hashtags - Bijlage + Bijlagen volgverzoek verstuurd Afmelden Abonneren - De toot waarvoor jij een antwoord had opgesteld, is verwijderd - Het is versturen van deze toot is mislukt! - Weet je zeker dat je de lijst %s wil verwijderen\? + De toot waarvoor jij een reactie had opgesteld, is verwijderd + Het versturen van deze toot is mislukt! + Weet je zeker dat je de lijst %s wilt verwijderen\? - Je kan niet meer dan %1$d media bijlage toevoegen. - Je kan niet meer dan %1$d media bijlagen toevoegen. + Je kan niet meer dan %1$d mediabijlage uploaden. + Je kan niet meer dan %1$d mediabijlagen uploaden. - Notificaties op tijdlijn beperken + Meldingen op tijdlijn beperken Opgeslagen! - Jou eigen opmerkingen over dit account + Jouw eigen opmerking over dit account Welzijn De titel van de bovenste statusbalk verbergen - Bevestigingsdialoogvenster weergeven voor boosten - Link previews in tijdlijnen weergeven + Vraag voor het boosten van een toot een bevestiging + Linkpreviews in tijdlijnen weergeven Er zijn geen aankondigingen. Oneindig Looptijd - Swipe gebaar inschakelen tussen tabs + Swipebewegingen om tussen tabs te schakelen inschakelen %s persoon %s personen Hashtag toevoegen Geluid - Notificaties wanneer iemand waar je op geabonneerd bent een nieuwe toot plaatst + Meldingen wanneer iemand waar je op bent geabonneerd een nieuwe toot plaatst Nieuwe toots - Notificaties over volgverzoeken + Meldingen over volgverzoeken Onder Boven - Niet standaard emojis animeren + Lokale emojis animeren Kleurverloop weergeven voor verborgen media - iemand waar ik op geabonneerd ben heeft een nieuwe toot geplaatst - Notificaties verbergen - \@%s dempen\? + iemand waar ik op ben geabonneerd heeft een nieuwe toot geplaatst + Meldingen verbergen + \@%s negeren\? \@%s blokkeren\? - Gesprek dempen opheffen - Gesprek dempen - %s niet meer dempen - Notificaties van %s dempen - Notificaties van %s niet meer dempen - %s niet meer dempen - %s heeft net een bericht geplaatst + Gesprek niet meer negeren + Gesprek negeren + %s niet meer negeren + Meldingen van %s negeren + Meldingen van %s niet meer negeren + %s niet meer negeren + %s heeft net een toot geplaatst %s verzoekt u te volgen Aankondigingen - Notificaties bekijken + Meldingen beoordelen + Concept verwijderd + Kwantitatieve statistieken voor toots verbergen + Laden van reactie-informatie mislukt + Oude concepten + Kwantitatieve statistieken in profielen verbergen + Hoofd navigatiepositie \ No newline at end of file From 593234e763a4279079e4df4f8c111b2aada6723c Mon Sep 17 00:00:00 2001 From: Ho Nhat Duy Date: Mon, 3 May 2021 01:48:12 +0000 Subject: [PATCH 05/92] Translated using Weblate (Vietnamese) Currently translated at 100.0% (460 of 460 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/vi/ --- app/src/main/res/values-vi/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 7f8bfd51e..b45a4b826 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -291,7 +291,7 @@ Cộng đồng xem thêm Trả lời @%s - Album + Media Luôn hiện nội dung bị ẩn Luôn hiện nội dung nhạy cảm Đang theo dõi bạn From 6dda3c35d51e29071a0efdde7b0b71a303f38f3d Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Mon, 3 May 2021 01:48:12 +0000 Subject: [PATCH 06/92] Translated using Weblate (Ukrainian) Currently translated at 100.0% (460 of 460 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/uk/ --- app/src/main/res/values-uk/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 73882f1ea..6144773a8 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -532,4 +532,5 @@ Не вдалося завантажити Поточний набір емодзі Google Стандартний набір емодзі Mastodon + Навіть попри те, що ваш обліковий запис загальнодоступний, співробітники %1$s вважають, що ви, можливо, захочете переглянути запити від цих облікових записів власноруч. \ No newline at end of file From 8eeb6b5f2a92c7daa32e53a2174fbec1c365a02d Mon Sep 17 00:00:00 2001 From: Conny Duck Date: Tue, 4 May 2021 19:36:51 +0200 Subject: [PATCH 07/92] Release 81 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 08055058b..50ada95d8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -20,8 +20,8 @@ android { applicationId APP_ID minSdkVersion 21 targetSdkVersion 29 - versionCode 80 - versionName "14.0" + versionCode 81 + versionName "15.0 beta 1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true From 6f53883d72985ed548478e97ec90d71a1866576e Mon Sep 17 00:00:00 2001 From: Ho Nhat Duy Date: Tue, 4 May 2021 17:36:48 +0000 Subject: [PATCH 08/92] Translated using Weblate (Vietnamese) Currently translated at 100.0% (460 of 460 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/vi/ --- app/src/main/res/values-vi/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index b45a4b826..d8921b580 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -247,7 +247,7 @@ Yêu cầu theo dõi Thông báo về người theo dõi mới Người theo dõi mới - Thông báo về lược nhắc tới + Thông báo về lượt nhắc tới To nhất To Trung bình @@ -269,7 +269,7 @@ Bật proxy Dùng proxy Vượt tường lửa - Thông báo khi của bạn được chia sẻ + Thông báo khi tút của bạn được chia sẻ Chia sẻ Thông báo về lượt yêu cầu theo dõi Báo lỗi và đề xuất tính năng @@ -383,7 +383,7 @@ Đã lưu Đã thích Đã chia sẻ - Âm thanh + Không có mô tả Nội dung nhạy cảm: %s tối đa %1$d tab From 9baab34582d631a488fa3c613ebf58fe4f488326 Mon Sep 17 00:00:00 2001 From: Conny Duck Date: Tue, 4 May 2021 21:23:13 +0200 Subject: [PATCH 09/92] prepare changelog for Tusky 15 --- fastlane/metadata/android/en-US/changelogs/82.txt | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 fastlane/metadata/android/en-US/changelogs/82.txt diff --git a/fastlane/metadata/android/en-US/changelogs/82.txt b/fastlane/metadata/android/en-US/changelogs/82.txt new file mode 100644 index 000000000..6e6afdd43 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/82.txt @@ -0,0 +1,5 @@ +Tusky v15.0 + +- Follow requests are now always shown in the main menu. +- The time picker for scheduling a post has a design consistent with the rest of the app now +Full changelog: https://github.com/tuskyapp/Tusky/releases From 076a2b248abcc355369725d2c2d23f6c9bd6bb75 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Thu, 6 May 2021 07:33:15 +0200 Subject: [PATCH 10/92] fix crash when captioning large images (#2149) --- .../tusky/components/compose/dialog/CaptionDialog.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt index 50d5c23b6..e356cd3fb 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt @@ -101,8 +101,10 @@ fun T.makeCaptionDialog(existingDescription: String?, // size. Maybe we should limit the size of CustomTarget Glide.with(this) .load(previewUri) - .into(object : CustomTarget() { - override fun onLoadCleared(placeholder: Drawable?) {} + .into(object : CustomTarget(4096, 4096) { + override fun onLoadCleared(placeholder: Drawable?) { + imageView.setImageDrawable(placeholder) + } override fun onResourceReady(resource: Drawable, transition: Transition?) { imageView.setImageDrawable(resource) From ddba46da16f0a25a943180e876f40cd14adaf300 Mon Sep 17 00:00:00 2001 From: Vegard Skjefstad Date: Thu, 6 May 2021 05:04:53 +0000 Subject: [PATCH 11/92] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegian?= =?UTF-8?q?=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (13 of 13 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/nb_NO/ --- fastlane/metadata/android/nb-NO/changelogs/82.txt | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 fastlane/metadata/android/nb-NO/changelogs/82.txt diff --git a/fastlane/metadata/android/nb-NO/changelogs/82.txt b/fastlane/metadata/android/nb-NO/changelogs/82.txt new file mode 100644 index 000000000..91c664660 --- /dev/null +++ b/fastlane/metadata/android/nb-NO/changelogs/82.txt @@ -0,0 +1,5 @@ +Tusky v15.0 + +- Følgeforespørsler vises nå alltid i hovedmenyen +- Designet på tidsvelgeren som brukes for planlegge toots er likt som resten av appen +Full liste med endringer: https://github.com/tuskyapp/Tusky/releases From 0bdbde2ac2d38719167ba486b3583ae1a9d3f6d3 Mon Sep 17 00:00:00 2001 From: Danial Behzadi Date: Thu, 6 May 2021 05:04:53 +0000 Subject: [PATCH 12/92] Translated using Weblate (Persian) Currently translated at 100.0% (13 of 13 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/fa/ --- fastlane/metadata/android/fa/changelogs/82.txt | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 fastlane/metadata/android/fa/changelogs/82.txt diff --git a/fastlane/metadata/android/fa/changelogs/82.txt b/fastlane/metadata/android/fa/changelogs/82.txt new file mode 100644 index 000000000..c1ba7e8e9 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/82.txt @@ -0,0 +1,5 @@ +تاسکی نگارش ۱۵٫۰ + +- درخواست‌های پیگیری اکنون همواره در فهرست اصلی نشان داده می‌شوند. +- گزینشگر زمان برای زمان‌بندی یک فرسته، اکنون ظاهری هماهنگ با باقی کاره دارد. +گزارش تغییر کامل: https://github.com/tuskyapp/Tusky/releases From 8ab003de7f86a8ac28a280159b2928a391a24e07 Mon Sep 17 00:00:00 2001 From: Ho Nhat Duy Date: Thu, 6 May 2021 05:04:54 +0000 Subject: [PATCH 13/92] Translated using Weblate (Vietnamese) Currently translated at 100.0% (13 of 13 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/vi/ --- fastlane/metadata/android/vi/changelogs/82.txt | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 fastlane/metadata/android/vi/changelogs/82.txt diff --git a/fastlane/metadata/android/vi/changelogs/82.txt b/fastlane/metadata/android/vi/changelogs/82.txt new file mode 100644 index 000000000..3aa0b8814 --- /dev/null +++ b/fastlane/metadata/android/vi/changelogs/82.txt @@ -0,0 +1,5 @@ +Tusky v15.0 + +- Hiện yêu cầu theo dõi trên menu chính. +- Cải thiện thời gian lên lịch tút với thiết kế thuận tiện hơn. +Full changelog: https://github.com/tuskyapp/Tusky/releases From 49a297b1d1c829ef632f20a92be7390b563d1db0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sveinn=20=C3=AD=20Felli?= Date: Thu, 6 May 2021 05:04:54 +0000 Subject: [PATCH 14/92] Translated using Weblate (Icelandic) Currently translated at 100.0% (13 of 13 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/is/ --- fastlane/metadata/android/is/changelogs/82.txt | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 fastlane/metadata/android/is/changelogs/82.txt diff --git a/fastlane/metadata/android/is/changelogs/82.txt b/fastlane/metadata/android/is/changelogs/82.txt new file mode 100644 index 000000000..de9857ad8 --- /dev/null +++ b/fastlane/metadata/android/is/changelogs/82.txt @@ -0,0 +1,5 @@ +Tusky útg. 15.0 + +- Fylgjendabeiðnir eru núna alltaf birtar í aðalvalmyndinni. +- Val tíma áætlaðra færslna hefur verið endurhönnuð til samræmis við aðra hluta forritsins. +Full breytingaskrá: https://github.com/tuskyapp/Tusky/releases From e22cce88e6978b1d7d85c1c8aa0bce5d019bb78d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Gera?= Date: Thu, 6 May 2021 05:04:54 +0000 Subject: [PATCH 15/92] Translated using Weblate (Hungarian) Currently translated at 100.0% (13 of 13 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/hu/ --- fastlane/metadata/android/hu/changelogs/82.txt | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 fastlane/metadata/android/hu/changelogs/82.txt diff --git a/fastlane/metadata/android/hu/changelogs/82.txt b/fastlane/metadata/android/hu/changelogs/82.txt new file mode 100644 index 000000000..1a5aeb88d --- /dev/null +++ b/fastlane/metadata/android/hu/changelogs/82.txt @@ -0,0 +1,5 @@ +Tusky v15.0 + +- A követési kérelmeket mindig mutatjuk a főmenüben. +- A posztok időzítésére használt időválasztó kinézete illeszkedik az app többi részéhez. +Összes változtatás: https://github.com/tuskyapp/Tusky/releases From f235a947e6abd42776d03e7a6d3b99365511baf8 Mon Sep 17 00:00:00 2001 From: XoseM Date: Thu, 6 May 2021 05:04:54 +0000 Subject: [PATCH 16/92] Translated using Weblate (Galician) Currently translated at 100.0% (13 of 13 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/gl/ --- fastlane/metadata/android/gl/changelogs/82.txt | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 fastlane/metadata/android/gl/changelogs/82.txt diff --git a/fastlane/metadata/android/gl/changelogs/82.txt b/fastlane/metadata/android/gl/changelogs/82.txt new file mode 100644 index 000000000..99482d83f --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/82.txt @@ -0,0 +1,5 @@ +Tusky v15.0 + +- As solicitudes de seguimento móstranse sempre no menú principal. +- O selector para programar unha publicación ten un deseño consistente co resto da aplicación +Rexistro completo dos cambios: https://github.com/tuskyapp/Tusky/releases From c742f7dbb01a0d4f109aa9d137076cf1ef760dda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sveinn=20=C3=AD=20Felli?= Date: Wed, 5 May 2021 19:53:57 +0000 Subject: [PATCH 17/92] Translated using Weblate (Icelandic) Currently translated at 100.0% (460 of 460 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/is/ --- app/src/main/res/values-is/strings.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index d73446e2f..1a23be14f 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -244,6 +244,7 @@ %1$s, %2$s og %3$s %1$s og %2$s + %d ný aðgerð %d nýjar aðgerðir Læstur notandaaðgangur @@ -354,6 +355,7 @@ %1$s og %2$s %1$s, %2$s og %3$d til viðbótar + hámarksfjölda %1$d flipa náð hámarksfjölda %1$d flipa náð Myndefni: %s @@ -496,6 +498,7 @@ Ótiltekið Tímalengd + Þú getur ekki sent inn fleiri en %1$d myndefnisviðhengi. Þú getur ekki sent inn fleiri en %1$d myndefnisviðhengi. Fela magntölfræði notendasniða @@ -507,4 +510,5 @@ Ný tíst einhver sem ég er áskrifandi að birti nýtt tíst %s sendi inn rétt í þessu + Jafnvel þótt aðgangurinn þinn sé ekki læstur, fannst starfsfólki %1$s að þú gætir viljað yfirfara handvirkt fylgjendabeiðnir frá þessum aðgöngum. \ No newline at end of file From 176365d8ab30f68f749c687ed92f312dbeb5bf04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Gera?= Date: Wed, 5 May 2021 19:53:58 +0000 Subject: [PATCH 18/92] Translated using Weblate (Hungarian) Currently translated at 100.0% (460 of 460 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/hu/ --- app/src/main/res/values-hu/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 175724baa..de1ff410a 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -519,4 +519,5 @@ Egyedi emojik animálása Leiratkozás Feliratkozás + Bár a fiókod nincs zárolva, a %1$s csapata úgy gondolta, hogy ezen fiókok követési kérelmeit átnéznéd. \ No newline at end of file From b156be6ded3a79d4448706aeb60adbe5d5569cef Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sun, 9 May 2021 18:37:41 +0200 Subject: [PATCH 19/92] work around mastodon mute bug (#2150) --- .../tusky/components/search/SearchViewModel.kt | 2 +- .../keylesspalace/tusky/network/TimelineCases.kt | 4 ++-- .../keylesspalace/tusky/view/MuteAccountDialog.kt | 13 +++++++++++-- .../tusky/viewmodel/AccountViewModel.kt | 2 +- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt index 08afe44de..57c78ef8e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt @@ -193,7 +193,7 @@ class SearchViewModel @Inject constructor( return accountManager.getAllAccountsOrderedByActive() } - fun muteAccount(accountId: String, notifications: Boolean, duration: Int) { + fun muteAccount(accountId: String, notifications: Boolean, duration: Int?) { timelineCases.mute(accountId, notifications, duration) } diff --git a/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt b/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt index 8cf2b688d..6e79f0755 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt @@ -33,7 +33,7 @@ interface TimelineCases { fun reblog(status: Status, reblog: Boolean): Single fun favourite(status: Status, favourite: Boolean): Single fun bookmark(status: Status, bookmark: Boolean): Single - fun mute(id: String, notifications: Boolean, duration: Int) + fun mute(id: String, notifications: Boolean, duration: Int?) fun block(id: String) fun delete(id: String): Single fun pin(status: Status, pin: Boolean) @@ -104,7 +104,7 @@ class TimelineCasesImpl( } } - override fun mute(id: String, notifications: Boolean, duration: Int) { + override fun mute(id: String, notifications: Boolean, duration: Int?) { mastodonApi.muteAccount(id, notifications, duration) .subscribe({ eventHub.dispatch(MuteEvent(id)) diff --git a/app/src/main/java/com/keylesspalace/tusky/view/MuteAccountDialog.kt b/app/src/main/java/com/keylesspalace/tusky/view/MuteAccountDialog.kt index da5e79874..022927e61 100644 --- a/app/src/main/java/com/keylesspalace/tusky/view/MuteAccountDialog.kt +++ b/app/src/main/java/com/keylesspalace/tusky/view/MuteAccountDialog.kt @@ -10,7 +10,7 @@ import com.keylesspalace.tusky.databinding.DialogMuteAccountBinding fun showMuteAccountDialog( activity: Activity, accountUsername: String, - onOk: (notifications: Boolean, duration: Int) -> Unit + onOk: (notifications: Boolean, duration: Int?) -> Unit ) { val binding = DialogMuteAccountBinding.inflate(activity.layoutInflater) binding.warning.text = activity.getString(R.string.dialog_mute_warning, accountUsername) @@ -20,7 +20,16 @@ fun showMuteAccountDialog( .setView(binding.root) .setPositiveButton(android.R.string.ok) { _, _ -> val durationValues = activity.resources.getIntArray(R.array.mute_duration_values) - onOk(binding.checkbox.isChecked, durationValues[binding.duration.selectedItemPosition]) + + // workaround to make indefinite muting work with Mastodon 3.3.0 + // https://github.com/tuskyapp/Tusky/issues/2107 + val duration = if(binding.duration.selectedItemPosition == 0) { + null + } else { + durationValues[binding.duration.selectedItemPosition] + } + + onOk(binding.checkbox.isChecked, duration) } .setNegativeButton(android.R.string.cancel, null) .show() diff --git a/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountViewModel.kt index a0f0ed680..1837652e6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountViewModel.kt @@ -119,7 +119,7 @@ class AccountViewModel @Inject constructor( } } - fun muteAccount(notifications: Boolean, duration: Int) { + fun muteAccount(notifications: Boolean, duration: Int?) { changeRelationship(RelationShipAction.MUTE, notifications, duration) } From 4fb22062619e8df2492ed2c18af8f59e8c8b9021 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sun, 16 May 2021 18:24:30 +0200 Subject: [PATCH 20/92] fix crash because of shrinked resources (#2156) --- app/src/main/res/raw/keep.xml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 app/src/main/res/raw/keep.xml diff --git a/app/src/main/res/raw/keep.xml b/app/src/main/res/raw/keep.xml new file mode 100644 index 000000000..c9c47c15e --- /dev/null +++ b/app/src/main/res/raw/keep.xml @@ -0,0 +1,3 @@ + + From dedacae0c691776692d442bac43425a686cc320d Mon Sep 17 00:00:00 2001 From: ashed Date: Sun, 16 May 2021 16:34:13 +0000 Subject: [PATCH 21/92] Translated using Weblate (Russian) Currently translated at 100.0% (13 of 13 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/ru/ --- fastlane/metadata/android/ru/changelogs/58.txt | 6 +++--- fastlane/metadata/android/ru/changelogs/77.txt | 10 ++++++++++ fastlane/metadata/android/ru/changelogs/80.txt | 7 +++++++ fastlane/metadata/android/ru/changelogs/82.txt | 5 +++++ 4 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 fastlane/metadata/android/ru/changelogs/77.txt create mode 100644 fastlane/metadata/android/ru/changelogs/80.txt create mode 100644 fastlane/metadata/android/ru/changelogs/82.txt diff --git a/fastlane/metadata/android/ru/changelogs/58.txt b/fastlane/metadata/android/ru/changelogs/58.txt index 008507490..fbce6db11 100644 --- a/fastlane/metadata/android/ru/changelogs/58.txt +++ b/fastlane/metadata/android/ru/changelogs/58.txt @@ -2,9 +2,9 @@ Tusky v6.0 - Фильтры ленты перенесены в настройки учетной записи и будут синхронизироваться с сервером. - Теперь вы можете иметь собственный хэштег-вкладку -- Списки теперь можно редактировать -- Безопасность: удалена поддержка TLS 1.0 и TLS 1.1, а также добавлена поддержка TLS 1.3 на Android 6+. -- При составлении окна теперь будут предлагаться пользовательские смайлики. +- Списки можно редактировать +- Безопасность: удалена поддержка TLS 1.0 и TLS 1.1, добавлена поддержка TLS 1.3 на Android 6+. +- При составлении сообщения теперь предлагаются пользовательские эмодзи. - Новая настройка темы "следовать теме системы" - Улучшена доступность ленты - Tusky теперь игнорирует неизвестные уведомления diff --git a/fastlane/metadata/android/ru/changelogs/77.txt b/fastlane/metadata/android/ru/changelogs/77.txt new file mode 100644 index 000000000..e9e81c91a --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/77.txt @@ -0,0 +1,10 @@ +Tusky v13.0 + +- поддержка заметок профиля (функция Mastodon 3.2.0) +- поддержка объявлений администратора (функция Mastodon 3.1.0) + +- аватар выбранной вами учётной записи теперь будет отображаться на главной панели инструментов +- щелчок по отображаемому имени на временной шкале теперь откроет страницу профиля этого пользователя + +- множество исправлений ошибок и небольших улучшений +- улучшенные переводы diff --git a/fastlane/metadata/android/ru/changelogs/80.txt b/fastlane/metadata/android/ru/changelogs/80.txt new file mode 100644 index 000000000..698ebc10e --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/80.txt @@ -0,0 +1,7 @@ +Tusky v14.0 + +- Показ уведомлений о публикации сообщений подписанных пользователей - щёлкните значок колокольчика в их профиле! (Особенность Mastodon 3.3.0) +- Функция черновика в Tusky была полностью переработана для скорости, удобства и стабильности. +- Новый режим благополучия, ограничивающий определённые функции Tusky. +- Анимация пользовательских смайлов. +Полный список изменений: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/ru/changelogs/82.txt b/fastlane/metadata/android/ru/changelogs/82.txt new file mode 100644 index 000000000..c16d990ec --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/82.txt @@ -0,0 +1,5 @@ +Tusky v15.0 + +- Запросы на подписку теперь всегда отображаются в главном меню. +- Средство выбора времени для планирования публикации теперь имеет дизайн, совместимый с остальной частью приложения. +Полный список изменений: https://github.com/tuskyapp/Tusky/releases From f0098d6e8203c9bf77afd499a979f64399ea5f4e Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Sun, 16 May 2021 16:34:13 +0000 Subject: [PATCH 22/92] Translated using Weblate (Ukrainian) Currently translated at 100.0% (13 of 13 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/uk/ --- fastlane/metadata/android/uk/changelogs/82.txt | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 fastlane/metadata/android/uk/changelogs/82.txt diff --git a/fastlane/metadata/android/uk/changelogs/82.txt b/fastlane/metadata/android/uk/changelogs/82.txt new file mode 100644 index 000000000..7e9a5be2f --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/82.txt @@ -0,0 +1,5 @@ +Tusky v15.0 + +- Запити на підписку тепер завжди показано в головному меню. +- Тепер вибір часу для планування публікації має дизайн, який відповідає решті програми +Журнал усіх змін: https://github.com/tuskyapp/Tusky/releases From 760715c4d04f5cfb8ee1232da7c6ad5e87d5b5b0 Mon Sep 17 00:00:00 2001 From: Bifo Ho Date: Sun, 16 May 2021 16:34:13 +0000 Subject: [PATCH 23/92] Translated using Weblate (Bengali (Bangladesh)) Currently translated at 23.0% (3 of 13 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/bn_BD/ Translated using Weblate (Bengali (India)) Currently translated at 30.7% (4 of 13 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/bn_IN/ --- .../metadata/android/bn-IN/changelogs/67.txt | 2 +- .../android/bn-IN/full_description.txt | 19 ++++++++++--------- .../android/bn-IN/short_description.txt | 2 +- .../android/bn_BD/full_description.txt | 12 ++++++++++++ .../android/bn_BD/short_description.txt | 1 + fastlane/metadata/android/bn_BD/title.txt | 1 + 6 files changed, 26 insertions(+), 11 deletions(-) create mode 100644 fastlane/metadata/android/bn_BD/full_description.txt create mode 100644 fastlane/metadata/android/bn_BD/short_description.txt create mode 100644 fastlane/metadata/android/bn_BD/title.txt diff --git a/fastlane/metadata/android/bn-IN/changelogs/67.txt b/fastlane/metadata/android/bn-IN/changelogs/67.txt index 249433061..d76c7e2b7 100644 --- a/fastlane/metadata/android/bn-IN/changelogs/67.txt +++ b/fastlane/metadata/android/bn-IN/changelogs/67.txt @@ -1,4 +1,4 @@ -টাস্কি v9.0 +টাস্কি সং-৯.০ - আপনি এখন টাস্কি থেকে পোল তৈরি করতে পারেন - উন্নত অনুসন্ধান diff --git a/fastlane/metadata/android/bn-IN/full_description.txt b/fastlane/metadata/android/bn-IN/full_description.txt index 75b32cebc..0584bbf7a 100644 --- a/fastlane/metadata/android/bn-IN/full_description.txt +++ b/fastlane/metadata/android/bn-IN/full_description.txt @@ -1,12 +1,13 @@ -টাস্কি একটি ফ্রি এবং ওপেন সোর্স সোশ্যাল নেটওয়ার্ক সার্ভার মস্তোদনের হালকা ওজনের ক্লায়েন্ট। +টাস্কি একটি ফ্রি এবং মুক্ত উৎসবিশিষ্ট সামাজিক নেটওয়ার্ক সার্ভার মাস্টোডনের জন্য একটি হালকা ক্লায়েন্ট। -• মেটেরিয়াল ডিসাইন +• মেটেরিয়াল নকশা • বেশিরভাগ মাস্টডন এপিআই প্রয়োগ করা হয়েছে -• বহু অ্যাকাউন্ট সমর্থন -• দিনের সময় ভিত্তিতে অটো-স্যুইচ করার সম্ভাবনা সহ অন্ধকার এবং হালকা থিম -• খসড়া - টুটগুলি রচনা করুন এবং সেগুলি পরে সংরক্ষণ করুন -• বিভিন্ন ইমোজি শৈলীর মধ্যে চয়ন করুন -• সমস্ত পর্দার আকারের জন্য অনুকূলিত -• সম্পূর্ণ ওপেন সোর্স - গুগল পরিষেবাগুলির মতো কোনও অ-নিরপেক্ষ নির্ভরতা নেই +• বহু অ্যাকাউন্ট সমর্থন করে +• দিনের সময় ভিত্তিতে অন্ধকার এবং হালকা রঙে পাল্টানো যায় +• খসড়া - টুট রচনা করে পরে ছাপানোর জন্য সংরক্ষণ করো +• বিভিন্ন আবেগ শৈলীর বিদ্যমান +• সমস্ত পর্দার আকারের জন্য অনূকুল +• সম্পূর্ণ মুক্ত উৎসবিশিষ্ট- গুগল পরিষেবাগুলোর মতো কোনও অ-মুক্ত নির্ভরতা নেই -মাস্টডন সম্পর্কে আরও জানতে, https://joinmastodon.org/ দেখুন +মাস্টডন সম্পর্কে আরও জানতে, দেখো +https://joinmastodon.org/ diff --git a/fastlane/metadata/android/bn-IN/short_description.txt b/fastlane/metadata/android/bn-IN/short_description.txt index c67d3f371..a8ae79c4d 100644 --- a/fastlane/metadata/android/bn-IN/short_description.txt +++ b/fastlane/metadata/android/bn-IN/short_description.txt @@ -1 +1 @@ -সামাজিক নেটওয়ার্ক মাস্টডনের জন্য একাধিক অ্যাকাউন্ট ক্লায়েন্ট +মাস্টডন নামক সামাজিক নেটওয়ার্কের জন্য একাধিক অ্যাকাউন্ট সমর্থনকারী ক্লায়েন্ট diff --git a/fastlane/metadata/android/bn_BD/full_description.txt b/fastlane/metadata/android/bn_BD/full_description.txt new file mode 100644 index 000000000..d33c469c9 --- /dev/null +++ b/fastlane/metadata/android/bn_BD/full_description.txt @@ -0,0 +1,12 @@ +টাস্কি একটি ফ্রি এবং মুক্ত উৎসবিশিষ্ট সামাজিক নেটওয়ার্ক সার্ভার মাস্টোডনের জন্য একটি হালকা ক্লায়েন্ট। + +• মেটেরিয়াল নকশা +• বেশিরভাগ মাস্টডন এপিআই প্রয়োগ করা হয়েছে +• বহু অ্যাকাউন্ট সমর্থন করে +• দিনের সময় ভিত্তিতে অন্ধকার এবং হালকা রঙে পাল্টানো যায় +• খসড়া - টুট রচনা করে পরে ছাপানোর জন্য সংরক্ষণ করো +• বিভিন্ন আবেগ শৈলীর বিদ্যমান +• সমস্ত পর্দার আকারের জন্য অনূকুল +• সম্পূর্ণ মুক্ত উৎসবিশিষ্ট- গুগল পরিষেবাগুলোর মতো কোনও অ-মুক্ত নির্ভরতা নেই + +মাস্টডন সম্পর্কে আরও জানতে, দেখো https://joinmastodon.org/ diff --git a/fastlane/metadata/android/bn_BD/short_description.txt b/fastlane/metadata/android/bn_BD/short_description.txt new file mode 100644 index 000000000..a8ae79c4d --- /dev/null +++ b/fastlane/metadata/android/bn_BD/short_description.txt @@ -0,0 +1 @@ +মাস্টডন নামক সামাজিক নেটওয়ার্কের জন্য একাধিক অ্যাকাউন্ট সমর্থনকারী ক্লায়েন্ট diff --git a/fastlane/metadata/android/bn_BD/title.txt b/fastlane/metadata/android/bn_BD/title.txt new file mode 100644 index 000000000..40477bfe8 --- /dev/null +++ b/fastlane/metadata/android/bn_BD/title.txt @@ -0,0 +1 @@ +টাস্কি From f0a53e4e9fc856cc3db355ad7dc1ddb1f2e935e1 Mon Sep 17 00:00:00 2001 From: test8421 Date: Sun, 16 May 2021 16:34:14 +0000 Subject: [PATCH 24/92] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (13 of 13 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/zh_Hans/ --- fastlane/metadata/android/zh-Hans/changelogs/82.txt | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 fastlane/metadata/android/zh-Hans/changelogs/82.txt diff --git a/fastlane/metadata/android/zh-Hans/changelogs/82.txt b/fastlane/metadata/android/zh-Hans/changelogs/82.txt new file mode 100644 index 000000000..3decf1f15 --- /dev/null +++ b/fastlane/metadata/android/zh-Hans/changelogs/82.txt @@ -0,0 +1,5 @@ +Tusky v15.0 + +- 关注请求将总在首页显示。 +- 预订嘟文时间选取器的设计风格现与 App 一致。 +完整更新日志:https://github.com/tuskyapp/Tusky/releases From d34002c082bb76046c08cf9cf916205c14c8ba44 Mon Sep 17 00:00:00 2001 From: Connyduck Date: Sun, 16 May 2021 16:34:14 +0000 Subject: [PATCH 25/92] Translated using Weblate (German) Currently translated at 69.2% (9 of 13 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/de/ --- fastlane/metadata/android/de/changelogs/82.txt | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 fastlane/metadata/android/de/changelogs/82.txt diff --git a/fastlane/metadata/android/de/changelogs/82.txt b/fastlane/metadata/android/de/changelogs/82.txt new file mode 100644 index 000000000..9240dea53 --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/82.txt @@ -0,0 +1,5 @@ +Tusky v15.0 + +- Folgeanfragen werden jetzt immer im Menü angezeigt +- Der Zeitauswahldialog beim planen eines Beitrags hat jetzt ein besseres Design +Alle Änderungen: https://github.com/tuskyapp/Tusky/releases From f6d81b41c02db7ff56d90ef010faf3f42e3ab219 Mon Sep 17 00:00:00 2001 From: ashed Date: Sun, 16 May 2021 13:22:54 +0000 Subject: [PATCH 26/92] Translated using Weblate (Russian) Currently translated at 100.0% (460 of 460 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/ru/ --- app/src/main/res/values-ru/strings.xml | 117 ++++++++++++++++--------- 1 file changed, 75 insertions(+), 42 deletions(-) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index bf1eec125..dd90ed48b 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -16,7 +16,7 @@ Файл не может быть открыт. Необходимо разрешение на чтение медиаконтента. Необходимо разрешение для хранения медиаконтента. - Изображения и видео не могут быть прикрекплены к статусу одновременно. + Изображения и видео не могут быть прикреплены к статусу одновременно. Загрузка не удалась. Ошибка при отправке поста. Главная @@ -43,14 +43,14 @@ Чувствительный контент Медиа скрыто Нажмите для просмотра - Развернуть - Свернуть + Показать больше + Показать меньше Развернуть Свернуть Ничего нет. Ничего нет. Потяните вниз, чтобы обновить! - %s продвинул(а) ваш статус - %s понравился ваш статус + %s продвинул(а) вашу запись + %s понравился ваша запись %s подписался(-лась) на вас Пожаловаться на @%s Дополнительные комментарии? @@ -59,8 +59,8 @@ Продвинуть Убрать продвижение Нравится - Не нравится - Развернуть + Убрать из избранного + Больше Написать Войти Выйти @@ -70,7 +70,7 @@ Заблокировать Разблокировать Скрыть продвижения - Показывать продвижения + Показать продвижения Пожаловаться Удалить Удалить и исправить @@ -94,10 +94,10 @@ Отменить глушение Упомянуть Скрыть медиаконтент - Нарисовать + Открыть drawer Сохранить Редактировать профиль - Изменить + Редактировать Отменить Принять Отклонить @@ -112,7 +112,7 @@ Хэштеги Перейти к автору Показывать продвижения - Показать, кому нравится + Показать избранное Хэштеги Упоминания Ссылки @@ -123,12 +123,12 @@ Поделиться как … Скачать медиафайл Скачивание медиафайла - Поделиться ссылкой на статус… - Поделиться статусом… + Поделиться ссылкой на запись… + Поделиться записью… Поделиться медифайлом… Отправить! Пользователь разблокирован - Глушение снято + Пользователь разглушен Отправлено! Ответ успешно отправлен. Какой узел? @@ -143,18 +143,20 @@ Заголовок Что такое узел? Соединение… - Здесь можно ввести адрес или домен любого узла, например, mastodon.social, icosahedron.website, social.tchncs.de и других!\n\nЕсли у вас еще нет аккаунта, введите адрес узла, на котором хотите зарегистрироваться, и создайте аккаунт.\n\n - Узел - это то место, где размещен ваш аккаунт, но вы можете взаимодействовать с пользователями других узлов, как будто вы находитесь на одном сайте.\n - \n - Чтобы получить больше информации посетите joinmastodon.org. - + Здесь можно ввести адрес или домен любого узла, например, mastodon.social, icosahedron.website, social.tchncs.de и других! +\n +\nЕсли у вас ещё нет аккаунта, введите адрес узла, на котором хотите зарегистрироваться, и создайте аккаунт. +\n +\n Узел - это то место, где размещён ваш аккаунт, но вы можете взаимодействовать с пользователями других узлов, как будто вы находитесь на одном сайте. +\n +\n Чтобы получить больше информации посетите joinmastodon.org. Завершается загрузка медиаконтента Загружается… Скачать Отменить запрос на подписку? Отписаться от этого аккаунта? - Удалить статус? - Удалить статус и превратить его в черновик? + Удалить запись\? + Удалить запись и превратить её в черновик\? Публичный: Показать в публичных лентах Скрытый: Не показывать в лентах Приватный: Показать только подписчикам @@ -162,10 +164,10 @@ Push-уведомления Push-уведомления Предупреждения - Звуковые уведомления - Использовать вибрацию - Световые уведомления - Уведомлять когда… + Уведомлять звуком + Уведомлять вибрацией + Уведомлять светом + Уведомлять когда упомянули подписались мои посты продвинули @@ -176,7 +178,7 @@ Фильтры Тёмная Светлая - Черная + Чёрная Автоматическая (по времени) Как в системе Браузер @@ -221,6 +223,9 @@ %1$s, %2$s, и %3$s %1$s и %2$s + Новое событие: %d + Новые события: %d + Новых событий: %d Новых событий: %d Закрытый аккаунт @@ -354,7 +359,7 @@ Открепить Закрепить - %1$s Понравилось + %1$s Понравился %1$s Понравилось %1$s Понравилось %1$s Понравилось @@ -371,6 +376,9 @@ %1$s и %2$s %1$s, %2$s и ещё %3$d + достигнут лимит в %1$d вкладку + достигнут лимит в %1$d вкладок + достигнут лимит в %1$d вкладок достигнут лимит в %1$d вкладок @@ -429,9 +437,9 @@ Скрытые домены Заглушить %s %s показывается - Вы уверены, что хотите заблокировать %s целиком\? Вы перестанете видеть посты с того узла во всех публичных лентах и уведомлениях. Все ваши подписчики с того домена будут удалены. + Вы уверены, что хотите заблокировать %s целиком\? Вы перестанете видеть посты из того домена во всех публичных лентах и уведомлениях. Все ваши подписчики из того домена будут удалены. Скрыть узел целиком - завершившиеся опросы + опросы завершились Анимировать GIF-аватары Слово целиком Если слово или фраза состоит только из букв и цифр, будет учитываться полное совпадение @@ -461,10 +469,10 @@ Множественный выбор Вариант %d Изменить - Отложенные записи + Запланированные записи Редактировать - Отложенные записи - Отложить запись + Запланированные записи + Запланировать запись Сброс Закладки Добавить в закладки @@ -476,10 +484,10 @@ Аудиофайлы должны быть меньше 40МБ. Ошибка поиска поста %s У вас нет черновиков. - У вас нет запланированный постов. + У вас нет запланированных постов. Минимальный интервал планирования в Mastodon составляет 5 минут. - Показвать диалог подтверждения перед продвижением - Показывать предосмотр ссылок в лентах + Показывать диалог подтверждения перед продвижением + Показывать предпросмотр ссылок в лентах Включить переключение между вкладками смахиванием %s человек @@ -493,7 +501,7 @@ Заглушить @%s\? Заблокировать @%s\? Показать обсуждение - Скрыть обсуждение + Заглушить обсуждение запрос на подписку от %s Тэги Добавить тэг @@ -503,20 +511,20 @@ Расположение панели навигации Отменить глушение %s Скрыть уведомления - Заблокировать уведомления от %s + Заглушить уведомления от %s Получать уведомления от %s Разблокировать %s Сохранено! Ваша личная заметка об этой учётной записи - Скрыть заголовок в верхней панели + Скрыть заголовок верхней панели Объявлений нет. Объявления - "Некоторая информация, которая может повлиять на ваше психическое благополучие, будет скрыта. Это включает в себя: + "Информация, могущая повлиять на ваше психическое благополучие, будет скрыта. Она включает: \n \n - Избранное/Продвижение/Уведомления подписок -\n - Избранное/Продвижение счета на тутах -\n - Статистика подписчиков/публикаций в профилях -\n +\n - Избранное/Счётчики продвижения постов +\n - Статистика подписчиков/постов в профилях +\n \n На push-уведомления это не повлияет, но вы можете просмотреть настройки уведомлений вручную." Благосостояние Неопределённая @@ -524,4 +532,29 @@ Вложения Аудио %s только что опубликовал(а) + Ваша учётная запись не заблокирована, но персонал %1$s подумал, что вы можете захотеть вручную просмотреть запросы на отслеживание от этих учётных записей. + + Вы не можете загрузить более %1$d мультимедийного вложения. + Вы не можете загрузить более %1$d мультимедийных вложений. + Вы не можете загрузить более %1$d мультимедийных вложений. + Вы не можете загрузить более %1$d мультимедийных вложений. + + Скрыть количественную статистику по сообщениям + Отписаться + Подписаться + Пост, на который вы написали ответ, был удалён + Не удалось загрузить информацию об ответе + Функция черновика в Tusky была полностью переработана, чтобы сделать её более быстрой, удобной и стабильной. +\nВы по-прежнему можете получить доступ к своим старым черновикам с помощью кнопки на экране новых черновиков, но они будут удалены в будущем обновлении! + Старые черновики + Черновик удалён + Этот пост не удалось отправить! + Вы действительно хотите удалить список %s\? + Скрыть количественную статистику по сообщениям + Ограничить уведомления на временной шкале + Просмотр уведомлений + Уведомления, когда кто-то, на кого вы подписаны, опубликовал новую запись + Новые записи + Анимировать собственные эмодзи + кто-то, на кого я подписан, опубликовал новую запись \ No newline at end of file From b5ce8ac2ee1ef1a64bfae039940379112da16b7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Gera?= Date: Sun, 16 May 2021 13:22:54 +0000 Subject: [PATCH 27/92] Translated using Weblate (Hungarian) Currently translated at 100.0% (460 of 460 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/hu/ --- app/src/main/res/values-hu/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index de1ff410a..c76bc0650 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -182,7 +182,7 @@ HTTP proxy szerver HTTP Proxy port Tülkök alapértelmezett láthatósága - Minden média szenzitívnek jelölése + Minden média kényesnek jelölése A beállítások szinkronizálása nem sikerült Nyilvános Listázatlan @@ -231,7 +231,7 @@ Követés kérelmezve Követ téged - Mindig mutassa a szenzitív tartalmat + Mindig mutassa a kényes tartalmat Média több betöltése Fiók hozzáadása From 6b07d34759797125d6715e7a5d23f5670e84065e Mon Sep 17 00:00:00 2001 From: Connyduck Date: Sun, 16 May 2021 13:22:54 +0000 Subject: [PATCH 28/92] Added translation using Weblate (Greek) --- app/src/main/res/values-el/strings.xml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 app/src/main/res/values-el/strings.xml diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml new file mode 100644 index 000000000..a6b3daec9 --- /dev/null +++ b/app/src/main/res/values-el/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file From 3c40dd1cf5b55b4a39c6d5313bb0fb566db87657 Mon Sep 17 00:00:00 2001 From: Bifo Ho Date: Sun, 16 May 2021 13:22:54 +0000 Subject: [PATCH 29/92] Translated using Weblate (Bengali (Bangladesh)) Currently translated at 100.0% (460 of 460 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/bn_BD/ --- app/src/main/res/values-bn-rBD/strings.xml | 83 +++++++++++++++++++++- 1 file changed, 81 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-bn-rBD/strings.xml b/app/src/main/res/values-bn-rBD/strings.xml index c547ee143..aa285d3d1 100644 --- a/app/src/main/res/values-bn-rBD/strings.xml +++ b/app/src/main/res/values-bn-rBD/strings.xml @@ -13,7 +13,8 @@ সতর্কবার্তা: %s মিডিয়া: %s - সর্বাধিক %1$d টি ট্যাব পৌঁছেছে + সর্বোচ্চ %1$dটি ট্যাব পৌঁছেছে + সর্বোচ্চ %1$dটি ট্যাব পৌঁছেছে দ্বারা পছন্দ দ্বারা সর্মথন @@ -296,7 +297,7 @@ মিডিয়া লুকানো সংবেদনশীল কন্টেন্ট লাইসেন্সগুলি - খসড়াগুলো + খসড়া আপনার প্রোফাইল সম্পাদনা করুন অনুরোধ অনুসরণ করুন অবরুদ্ধ ব্যবহারকারী @@ -439,4 +440,82 @@ %s তোমার টুট বুস্ট করেছে %s তোমার টুট বুস্ট করেছে ঘোষণা + %1$s,%2$s,%3$s এবং %4$d অন্যরা + যখন আমার সদস্যতা নেওয়া কেউ টুট দেয় তখন বিজ্ঞপ্তি দিবে + নতুন টুট + বিশেষ আবেগ বানাও + সদস্যতা আছে এমন একজন টুট দিয়েছে + কোনো ঘোষণা নেই। + যদিও তোমার অ্যাকাউন্ট রুদ্ধকৃত না, %1$s রা ভেবেছে এই অ্যাকাউন্টগুলোর অনুসরণ অনুরোধ তোমার পরীক্ষা করা উচিত। + নতুন খসড়া বৈশিষ্ট দ্রুততর হওয়ার জন্য নতুনভাবে নকশা করা হয়েছে, যা সহজে ব্যবহারযোগ্য ও কম সমস্যাপূর্ণ। +\n আগের খসড়াগুলো খসড়া পাতার বোতাম দিয়ে যেতে পারো, কিন্তু ভবিষ্যত হালনাগাদে তা সরিয়ে ফেলা হবে! + যে টুটের উত্তর খসড়া করেছিলে তা মুছে ফেলা হয়েছে + এই তালিকাটা আসলেই মুছতে চাও\? + + %1$d টার বেশি সংযুক্তি পাঠানো যাবে না। + %1$d টার বেশি সংযুক্তি পাঠানো যাবে না। + + টুট পাঠাতে ব্যর্থ! + উত্তরের তথ্য আনতে ব্যর্থ + সংরক্ষিত! + অবতারে পরিসংখ্যান লুকাও + ছাপার পরিসংখ্যান লুকাও + সময়কাল বিজ্ঞপ্তি সীমাবদ্ধ করো + তোমার মানসিক স্বাস্থে নেতিবাচক প্রভাব ফেলতে পারে এমন জিনিসগুলো লুকানো আছে। যেমন: +\n +\n - পছন্দ/বুস্ট/অনুসরণ বিজ্ঞপ্তি +\n - টুটে পছন্দ/বুস্ট সংখ্যা +\n - অবতারে অনুসরণকারী/পরিসংখ্যান +\n +\nপুশ-বিজ্ঞপ্তিতে প্রভাব পরবে না, কিন্তু বিজ্ঞপ্তি পছন্দ পাল্টাতে পারবে। + এই অ্যাকাউন্ট নিয়ে তোমার ব্যক্তিগত লেখা + শীর্ষস্থানীয় সরঞ্জামের শিরোনামটি লুকাও + খসড়া মুছো হয়েছে + পুরোনো খসড়া + বিজ্ঞপ্তি + সুস্থতা + সময়হীন + সময়কাল + + %d সেকেন্ড বাকি + %d সেকেন্ড বাকি + + + %d মিনিট বাকি + %d মিনিট বাকি + + + %d ঘন্টা বাকি + %d ঘন্টা বাকি + + + %d দিন বাকি + %d দিন বাকি + + + %s জন + %s জন + + + %sটি ভোট + %sটি ভোট + + %1$s, %2$s এবং %3$d আরো অন্য জন + %1$s এবং %2$s + সদস্যতা + সদস্যতা বাতিল + + %s বুস্ট + %s বুস্ট + + %1$s স্থানান্তরিত হয়েছে এখানে: + সংযুক্তি + শব্দ + + %dটি নতুন ক্রিয়া + %dটি নতুন ক্রিয়া + + %1$s আর %2$s + %1$s, %2$s, আর %3$s + %s তোমাকে উল্লেখ করেছে \ No newline at end of file From 89ec1db47df323646c7832103d29ac5fe1bc1cb1 Mon Sep 17 00:00:00 2001 From: GunChleoc Date: Sun, 16 May 2021 13:22:55 +0000 Subject: [PATCH 30/92] Translated using Weblate (Gaelic) Currently translated at 100.0% (460 of 460 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/gd/ Translated using Weblate (Gaelic) Currently translated at 99.7% (459 of 460 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/gd/ --- app/src/main/res/values-gd/strings.xml | 65 +++++++++++++------------- 1 file changed, 33 insertions(+), 32 deletions(-) diff --git a/app/src/main/res/values-gd/strings.xml b/app/src/main/res/values-gd/strings.xml index c9d0d5fc0..13d53a29b 100644 --- a/app/src/main/res/values-gd/strings.xml +++ b/app/src/main/res/values-gd/strings.xml @@ -26,14 +26,14 @@ Freagair… Lorg… Clàraich a-steach le Mastodon - DÙD! + POSTAICH! Meur-chlàr Emoji Na lean tuilleadh Lean Barrachd Feuch ris a-rithist Dùin - DÙD + POSTAICH Sguab às Sguab às is dèan dreachd ùr air Dèan gearan @@ -55,13 +55,13 @@ Cuir crìoch air an fho-sgrìobhadh Fo-sgrìobh Beòthaich na h-Emojis gnàthaichte - Bha againn ris an dùd a bha thu airson freagairt dha a thoirt air falbh + Bha againn ris a’ phost a bha thu airson freagairt dha a thoirt air falbh Chaidh an dreach a sguabadh às Cha deach leinn fiosrachadh na freagairte a luchdadh Seann-dreachdan Chaidh dealbhadh gu tur ùr a chur air gleus nan dreachdan aig Tusky ach am biodh e nas luaithe, nas fhasa cleachdadh is nas lugha de bhugaichean ann. \n Gheibh thu grèim air na seann-dreachdan agad fhathast le putan air sgrìn ùr nan dreachdan ach thèid an toirt air falbh le ùrachadh ri teachd! - Cha b’ urrainn dhuinn an dùd a chur! + Cha b’ urrainn dhuinn am post a chur! Ceanglachain Fuaim A bheil thu cinnteach gu bheil thu airson an liosta %s a sguabadh às\? @@ -87,14 +87,14 @@ Thèid cuid a dh’fhiosrachadh a dh’fhaodadh droch-bhuaidh a thoirt air d’ shlàinte-inntinn fhalach. Tha seo a’ gabhail a-staigh: \n \n - Brathan air annsachdan/brosnachaidhean/leantainn -\n - Cunntas nan annsachdan/brosnachaidhean air dùdan +\n - Cunntas nan annsachdan/brosnachaidhean air postaichean \n - Stadastaireachd an luchd-leantainn/nam postaichean air pròifilean \n \n Cha doir seo buaidh air na brathan-putaidh ach ’s urrainn dhut roghainnean nam brathan agad atharrachadh a làimh. Slàinte-inntinn - Brathan nuair a dh’fhoillsich cuideigin air a rinn mi fo-sgrìobhadh dùd ùr - Dùdan ùra - dh’fhoillsich cuideigin air a rinn mi fo-sgrìobhadh dùd ùr + Brathan nuair a dh’fhoillsich cuideigin air a rinn mi fo-sgrìobhadh post ùr + Postaichean ùra + dh’fhoillsich cuideigin air a rinn mi fo-sgrìobhadh post ùr Tha %s air rud a phostadh Chan eil brath-fios ann. Brathan-fios @@ -172,7 +172,7 @@ %1$s • %2$s Gnìomhan dhan dealbh %s A bheil thu cinnteach gu bheil thu airson na brathan uile agad fhalamhachadh gu buan\? - Sgrìobh dùd + Sgrìobh post Cuir an sàs Criathraich Falamhaich @@ -236,7 +236,7 @@ Uaireigin eile Feumaidh tu Tusky ath-thòiseachadh gus na roghainnean seo a chur an sàs Feumaidh tu an aplacaid ath-thòiseachadh - Fosgail an dùd + Fosgail am post Leudaich/Co-theannaich gach staid ’Ga lorg… Feumaidh tu na seataichean seo de dh’Emojis a luchdadh a-nuas an toiseach @@ -244,11 +244,11 @@ Stoidhle nan Emojis Chaidh lethbhreac dheth a chur air an stòr-bhòrd Chan eil Emojis gnàthaichte aig an ionstans %s agad - Chaidh lethbhreac dhen dùd agad a shàbhaladh ’na dhreachd + Chaidh lethbhreac dhen phost agad a shàbhaladh ’na dhreachd Chaidh sgur dhen chur - A’ cur nan dùd - Mearachd a’ cur an dùid - A’ cur an dùid… + A’ cur nam post + Mearachd a’ cur a’ phuist + A’ cur a’ phuist… A bheil thu airson a shàbhaladh ’na dhreachd\? Feumaidh tu gabhail ri luchd-leantainn ùr a làimh Glais an cunntas @@ -273,14 +273,14 @@ Cuir cunntas ris An abairt ri chriathradh Mur eil ach litrichean is àireamhan san fhacal-luirg, cha dèid a chur an sàs ach ma bhios e a’ maidseadh an fhacail shlàin - Leudaich dùdan ris a bheil rabhadh susbainte an-còmhnaidh - Co-roinn ceangal dhan dùd - Co-roinn susbaint an dùid + Leudaich postaichean ris a bheil rabhadh susbainte an-còmhnaidh + Co-roinn ceangal dhan phost + Co-roinn susbaint a’ phuist ’S e bathar-bog saor le bun-tùs fosgailte a th’ ann an Tusky. Tha e fo cheadachas GNU General Public License tionndadh 3. Chì thu an ceadachas an-seo: https://www.gnu.org/licenses/gpl-3.0.en.html - Brathan nuair a thèid dùd agad a chomharrachadh ’na annsachd - Brathan nuair a thèid dùd agad brosnachadh - A bheil thu airson an dùd seo a sguabadh às is dreachd ùr a dhèanamh air\? - A bheil thu airson an dùd seo a sguabadh às\? + Brathan nuair a thèid post agad a chomharrachadh ’na annsachd + Brathan nuair a thèid post agad brosnachadh + A bheil thu airson am post seo a sguabadh às is dreachd ùr a dhèanamh air\? + A bheil thu airson am post seo a sguabadh às\? ’S urrainn dhut seòladh no àrainn-lìn aig ionstans sam bith a chur a-steach an-seo, can mastodon.social, icosahedron.website, social.tchncs.de agus a bharrachd! \n \nMur eil cunntas agad fhathast, cuir a-steach ainm an ionstans sa bheil thu airson ballrachd fhaighinn airson cunntas a chruthachadh ann. @@ -288,16 +288,16 @@ \n’S e an t-aon àite far an cruthaich thu cunntas a th’ ann an ionstans ud ’s a nì an t-òstadh dhan chunntas agad. Gidheadh, ’s urrainn dhut conaltradh le daoine a tha air ionstans eile agus leantainn orra mar gun robh sibh air an aon làrach. \n \nGheibh thu barrachd fiosrachaidh air joinmastodon.org. - Co-roinn an dùd le… - Co-roinn URL an dùid le… - Cuir dùd air an sgeideal - Faicsinneachd an dùid - Dùdan air an sgeideal - Chuir %s an dùd agad ris na h-annsachdan - Bhrosnaich %s an dùd agad - Dùdan air an sgeideal - Dùd - Mearachd a’ cur an dùd. + Co-roinn am post le… + Co-roinn URL a’ phuist le… + Cuir post air an sgeideal + Faicsinneachd a’ phuist + Postaichean air an sgeideal + Chuir %s am post agad ris na h-annsachdan + Bhrosnaich %s am post agad + Postaichean air an sgeideal + Post + Mearachd a’ cur a’ phuist. Dì-mhùch %s Tagaichean hais Luchd-leantainn @@ -495,7 +495,7 @@ Briog air gus a shealltainn Meadhanan falaichte Susbaint fhrionasach - Bhrosnaich %s + ’Ga bhrosnachadh le %s \@%s Ceadachasan Iarrtasan leantainn @@ -532,4 +532,5 @@ Chan fhaod seo a bhith falamh. Thachair mearachd leis an lìonra! Thoir sùil air a’ cheangal agad is feuch ris a-rithist! Thachair mearachd. + Ged nach eil an cunntas agad glaiste, tha sgioba %1$s dhen bheachd gum b’ fheàirrde thu lèirmheas a dhèanamh air na h-iarrtasan leantainn o na cunntasan seo a làimh. \ No newline at end of file From c823a1cb85390197449b5738f5bad3373fd509a6 Mon Sep 17 00:00:00 2001 From: Iris S Date: Sun, 16 May 2021 13:22:55 +0000 Subject: [PATCH 31/92] Translated using Weblate (Greek) Currently translated at 0.6% (3 of 460 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/el/ --- app/src/main/res/values-el/strings.xml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index a6b3daec9..4734fea97 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -1,2 +1,6 @@ - \ No newline at end of file + + Αυτό δεν μπορεί να είναι κενό. + Προέκυψε σφάλμα δικτύου! Παρακαλώ ελέγξτε τη σύνδεσή σας και προσπαθήστε ξανά! + Προέκυψε ένα σφάλμα. + \ No newline at end of file From c44ba1ccb34fd61e0fd2b43a4349ee9481d4213b Mon Sep 17 00:00:00 2001 From: test8421 Date: Sun, 16 May 2021 13:22:55 +0000 Subject: [PATCH 32/92] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (460 of 460 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/zh_Hans/ --- app/src/main/res/values-zh-rCN/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 7884d24d5..b1bd8df67 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -524,4 +524,5 @@ 显示动态自定义Emoji 关注的人发送了新嘟文 %s 发送了新嘟文 + 即使您的账号未上锁,管理员 %1$s 认为您可能需要手动处理来自这些账号的关注请求。 \ No newline at end of file From 54e5807a5ffa4751a077348e94f1c46974ebcae7 Mon Sep 17 00:00:00 2001 From: Fjuro Date: Sun, 16 May 2021 13:22:55 +0000 Subject: [PATCH 33/92] Translated using Weblate (Czech) Currently translated at 92.6% (426 of 460 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/cs/ --- app/src/main/res/values-cs/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 558db2ecb..83cb9751d 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -481,7 +481,8 @@ Odkrýt oznámení od %s Odkrýt %s Ztišit @%s\? - %s požádal/a aby vás mohl/a sledovat Zobrazit dialogové okno s potvrzením při boostování + %s právě vydal + Oznámení \ No newline at end of file From d52b0a67eb935c93c0a070bde538db812549fe5f Mon Sep 17 00:00:00 2001 From: Ho Nhat Duy Date: Sun, 16 May 2021 13:22:55 +0000 Subject: [PATCH 34/92] Translated using Weblate (Vietnamese) Currently translated at 100.0% (460 of 460 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/vi/ --- app/src/main/res/values-vi/strings.xml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index d8921b580..23e9fd170 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -296,15 +296,15 @@ Luôn hiện nội dung nhạy cảm Đang theo dõi bạn %ds - %dm - %dh - %d ngày - %d năm + %d phút trước + %d giờ trước + %d ngày trước + %d năm trước %ds - %dm - %dh - %dd - %dy + %d phút + %d giờ + %d ngày + %d năm Yêu cầu theo dõi Video Hình ảnh From cb3840c699a49946c3f6b7a0ddfbf798396bb238 Mon Sep 17 00:00:00 2001 From: Conny Duck Date: Sun, 16 May 2021 18:53:34 +0200 Subject: [PATCH 35/92] Release 82 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 50ada95d8..06b6a3ea2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -20,8 +20,8 @@ android { applicationId APP_ID minSdkVersion 21 targetSdkVersion 29 - versionCode 81 - versionName "15.0 beta 1" + versionCode 82 + versionName "15.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true From 6c37cc770c8fcf253f00d2c64f26b98a31d3adf0 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sun, 16 May 2021 19:17:56 +0200 Subject: [PATCH 36/92] remove SavedToots (#2141) * remove SavedToots * fix tests --- .../26.json | 747 ++++++++++++++++++ .../com/keylesspalace/tusky/MainActivity.kt | 27 +- .../tusky/SavedTootActivity.java | 209 ----- .../tusky/adapter/SavedTootAdapter.java | 122 --- .../components/compose/ComposeActivity.kt | 1 - .../components/compose/ComposeViewModel.kt | 21 +- .../tusky/components/drafts/DraftsActivity.kt | 37 +- .../components/drafts/DraftsViewModel.kt | 6 - .../keylesspalace/tusky/db/AppDatabase.java | 34 +- .../com/keylesspalace/tusky/db/TootDao.java | 45 -- .../keylesspalace/tusky/db/TootEntity.java | 151 ---- .../tusky/di/ActivitiesModule.kt | 3 - .../com/keylesspalace/tusky/di/AppModule.kt | 4 +- .../receiver/SendStatusBroadcastReceiver.kt | 1 - .../tusky/service/SendTootService.kt | 7 - .../tusky/util/SaveTootHelper.java | 57 -- .../main/res/layout/activity_saved_toot.xml | 34 - app/src/main/res/layout/item_saved_toot.xml | 28 - app/src/main/res/menu/drafts.xml | 10 - app/src/main/res/values-ar/strings.xml | 4 +- app/src/main/res/values-ber/strings.xml | 2 +- app/src/main/res/values-bg/strings.xml | 7 +- app/src/main/res/values-bn-rBD/strings.xml | 4 +- app/src/main/res/values-bn-rIN/strings.xml | 4 +- app/src/main/res/values-ca/strings.xml | 5 +- app/src/main/res/values-ckb/strings.xml | 4 +- app/src/main/res/values-cs/strings.xml | 4 +- app/src/main/res/values-cy/strings.xml | 2 +- app/src/main/res/values-de/strings.xml | 7 +- app/src/main/res/values-eo/strings.xml | 4 +- app/src/main/res/values-es/strings.xml | 7 +- app/src/main/res/values-eu/strings.xml | 4 +- app/src/main/res/values-fa/strings.xml | 7 +- app/src/main/res/values-fr/strings.xml | 4 +- app/src/main/res/values-ga/strings.xml | 4 +- app/src/main/res/values-gd/strings.xml | 9 +- app/src/main/res/values-gl/strings.xml | 7 +- app/src/main/res/values-hi/strings.xml | 4 +- app/src/main/res/values-hu/strings.xml | 7 +- app/src/main/res/values-is/strings.xml | 7 +- app/src/main/res/values-it/strings.xml | 4 +- app/src/main/res/values-ja/strings.xml | 4 +- app/src/main/res/values-kab/strings.xml | 4 +- app/src/main/res/values-ko/strings.xml | 2 +- app/src/main/res/values-ml/strings.xml | 2 +- app/src/main/res/values-nl/strings.xml | 6 +- app/src/main/res/values-no-rNB/strings.xml | 7 +- app/src/main/res/values-oc/strings.xml | 4 +- app/src/main/res/values-pl/strings.xml | 4 +- app/src/main/res/values-pt-rBR/strings.xml | 7 +- app/src/main/res/values-ru/strings.xml | 6 +- app/src/main/res/values-sa/strings.xml | 4 +- app/src/main/res/values-sk/strings.xml | 2 +- app/src/main/res/values-sl/strings.xml | 2 +- app/src/main/res/values-sv/strings.xml | 4 +- app/src/main/res/values-ta/strings.xml | 2 +- app/src/main/res/values-th/strings.xml | 7 +- app/src/main/res/values-tr/strings.xml | 4 +- app/src/main/res/values-uk/strings.xml | 7 +- app/src/main/res/values-vi/strings.xml | 7 +- app/src/main/res/values-zh-rCN/strings.xml | 7 +- app/src/main/res/values-zh-rHK/strings.xml | 5 +- app/src/main/res/values-zh-rMO/strings.xml | 2 +- app/src/main/res/values-zh-rSG/strings.xml | 2 +- app/src/main/res/values-zh-rTW/strings.xml | 7 +- app/src/main/res/values/strings.xml | 11 +- .../tusky/ComposeActivityTest.kt | 2 - 67 files changed, 872 insertions(+), 904 deletions(-) create mode 100644 app/schemas/com.keylesspalace.tusky.db.AppDatabase/26.json delete mode 100644 app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java delete mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/SavedTootAdapter.java delete mode 100644 app/src/main/java/com/keylesspalace/tusky/db/TootDao.java delete mode 100644 app/src/main/java/com/keylesspalace/tusky/db/TootEntity.java delete mode 100644 app/src/main/java/com/keylesspalace/tusky/util/SaveTootHelper.java delete mode 100644 app/src/main/res/layout/activity_saved_toot.xml delete mode 100644 app/src/main/res/layout/item_saved_toot.xml delete mode 100644 app/src/main/res/menu/drafts.xml diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/26.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/26.json new file mode 100644 index 000000000..bd82fd4fd --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/26.json @@ -0,0 +1,747 @@ +{ + "formatVersion": 1, + "database": { + "version": 26, + "identityHash": "14fb3d5743b7a89e8e62463e05f086ab", + "entities": [ + { + "tableName": "DraftEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '14fb3d5743b7a89e8e62463e05f086ab')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt index 1628d41d9..b7763bcd5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt @@ -31,7 +31,6 @@ import android.widget.ImageView import androidx.appcompat.app.AlertDialog import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.content.ContextCompat -import androidx.core.content.edit import androidx.core.content.pm.ShortcutManagerCompat import androidx.emoji.text.EmojiCompat import androidx.emoji.text.EmojiCompat.InitCallback @@ -243,7 +242,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje // Flush old media that was cached for sharing deleteStaleCachedMedia(applicationContext.getExternalFilesDir("Tusky")) } - draftWarning() } override fun onResume() { @@ -417,7 +415,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje } }, primaryDrawerItem { - nameRes = R.string.action_access_saved_toot + nameRes = R.string.action_access_drafts iconRes = R.drawable.ic_notebook onClick = { val intent = DraftsActivity.newIntent(context) @@ -755,29 +753,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje header.setActiveProfile(accountManager.activeAccount!!.id) } - private fun draftWarning() { - val sharedPrefsKey = "show_draft_warning" - appDb.tootDao().savedTootCount() - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .autoDispose(this, Lifecycle.Event.ON_DESTROY) - .subscribe { draftCount -> - val showDraftWarning = preferences.getBoolean(sharedPrefsKey, true) - if (draftCount > 0 && showDraftWarning) { - AlertDialog.Builder(this) - .setMessage(R.string.new_drafts_warning) - .setNegativeButton("Don't show again") { _, _ -> - preferences.edit(commit = true) { - putBoolean(sharedPrefsKey, false) - } - } - .setPositiveButton(android.R.string.ok, null) - .show() - } - } - - } - override fun getActionButton(): FloatingActionButton? = binding.composeButton override fun androidInjector() = androidInjector diff --git a/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java b/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java deleted file mode 100644 index 63a32b174..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java +++ /dev/null @@ -1,209 +0,0 @@ -/* Copyright 2017 Andrew Dawson - * - * This file is a part of Tusky. - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky; if not, - * see . */ - -package com.keylesspalace.tusky; - -import android.content.Intent; -import android.os.AsyncTask; -import android.os.Bundle; -import android.view.View; - -import androidx.annotation.Nullable; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.widget.Toolbar; -import androidx.lifecycle.Lifecycle; -import androidx.recyclerview.widget.DividerItemDecoration; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -import com.google.gson.Gson; -import com.google.gson.reflect.TypeToken; -import com.keylesspalace.tusky.adapter.SavedTootAdapter; -import com.keylesspalace.tusky.appstore.EventHub; -import com.keylesspalace.tusky.appstore.StatusComposedEvent; -import com.keylesspalace.tusky.components.compose.ComposeActivity; -import com.keylesspalace.tusky.db.AppDatabase; -import com.keylesspalace.tusky.db.TootDao; -import com.keylesspalace.tusky.db.TootEntity; -import com.keylesspalace.tusky.di.Injectable; -import com.keylesspalace.tusky.util.SaveTootHelper; -import com.keylesspalace.tusky.view.BackgroundMessageView; - -import java.lang.ref.WeakReference; -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.List; - -import javax.inject.Inject; - -import io.reactivex.android.schedulers.AndroidSchedulers; - -import static com.keylesspalace.tusky.components.compose.ComposeActivity.ComposeOptions; -import static com.uber.autodispose.AutoDispose.autoDisposable; -import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from; - -public final class SavedTootActivity extends BaseActivity implements SavedTootAdapter.SavedTootAction, - Injectable { - - // ui - private SavedTootAdapter adapter; - private BackgroundMessageView errorMessageView; - - private List toots = new ArrayList<>(); - @Nullable - private AsyncTask asyncTask; - - @Inject - EventHub eventHub; - @Inject - AppDatabase database; - @Inject - SaveTootHelper saveTootHelper; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - eventHub.getEvents() - .observeOn(AndroidSchedulers.mainThread()) - .ofType(StatusComposedEvent.class) - .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) - .subscribe((__) -> this.fetchToots()); - - setContentView(R.layout.activity_saved_toot); - - Toolbar toolbar = findViewById(R.id.toolbar); - setSupportActionBar(toolbar); - ActionBar bar = getSupportActionBar(); - if (bar != null) { - bar.setTitle(getString(R.string.title_drafts)); - bar.setDisplayHomeAsUpEnabled(true); - bar.setDisplayShowHomeEnabled(true); - } - - RecyclerView recyclerView = findViewById(R.id.recyclerView); - errorMessageView = findViewById(R.id.errorMessageView); - recyclerView.setHasFixedSize(true); - LinearLayoutManager layoutManager = new LinearLayoutManager(this); - recyclerView.setLayoutManager(layoutManager); - DividerItemDecoration divider = new DividerItemDecoration( - this, layoutManager.getOrientation()); - recyclerView.addItemDecoration(divider); - adapter = new SavedTootAdapter(this); - recyclerView.setAdapter(adapter); - } - - @Override - protected void onResume() { - super.onResume(); - fetchToots(); - } - - @Override - protected void onPause() { - super.onPause(); - if (asyncTask != null) asyncTask.cancel(true); - } - - private void fetchToots() { - asyncTask = new FetchPojosTask(this, database.tootDao()) - .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - - private void setNoContent(int size) { - if (size == 0) { - errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_saved_status, null); - errorMessageView.setVisibility(View.VISIBLE); - } else { - errorMessageView.setVisibility(View.GONE); - } - } - - @Override - public void delete(int position, TootEntity item) { - - saveTootHelper.deleteDraft(item); - - toots.remove(position); - // update adapter - if (adapter != null) { - adapter.removeItem(position); - setNoContent(toots.size()); - } - } - - @Override - public void click(int position, TootEntity item) { - Gson gson = new Gson(); - Type stringListType = new TypeToken>() {}.getType(); - List jsonUrls = gson.fromJson(item.getUrls(), stringListType); - List descriptions = gson.fromJson(item.getDescriptions(), stringListType); - - ComposeOptions composeOptions = new ComposeOptions( - /*scheduledTootUid*/null, - item.getUid(), - /*drafId*/null, - item.getText(), - jsonUrls, - descriptions, - /*mentionedUsernames*/null, - item.getInReplyToId(), - /*replyVisibility*/null, - item.getVisibility(), - item.getContentWarning(), - item.getInReplyToUsername(), - item.getInReplyToText(), - /*mediaAttachments*/null, - /*draftAttachments*/null, - /*scheduledAt*/null, - /*sensitive*/null, - /*poll*/null, - /* modifiedInitialState */ true - ); - Intent intent = ComposeActivity.startIntent(this, composeOptions); - startActivity(intent); - } - - static final class FetchPojosTask extends AsyncTask> { - - private final WeakReference activityRef; - private final TootDao tootDao; - - FetchPojosTask(SavedTootActivity activity, TootDao tootDao) { - this.activityRef = new WeakReference<>(activity); - this.tootDao = tootDao; - } - - @Override - protected List doInBackground(Void... voids) { - return tootDao.loadAll(); - } - - @Override - protected void onPostExecute(List pojos) { - super.onPostExecute(pojos); - SavedTootActivity activity = activityRef.get(); - if (activity == null) return; - - activity.toots.clear(); - activity.toots.addAll(pojos); - - // set ui - activity.setNoContent(pojos.size()); - activity.adapter.setItems(activity.toots); - activity.adapter.notifyDataSetChanged(); - } - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/SavedTootAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/SavedTootAdapter.java deleted file mode 100644 index af9c31d5d..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/SavedTootAdapter.java +++ /dev/null @@ -1,122 +0,0 @@ -/* Copyright 2017 Andrew Dawson - * - * This file is a part of Tusky. - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky; if not, - * see . */ - -package com.keylesspalace.tusky.adapter; - -import android.content.Context; -import androidx.annotation.Nullable; -import androidx.recyclerview.widget.RecyclerView; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageButton; -import android.widget.TextView; - -import com.keylesspalace.tusky.R; -import com.keylesspalace.tusky.db.TootEntity; - -import java.util.ArrayList; -import java.util.List; - -public class SavedTootAdapter extends RecyclerView.Adapter { - private List list; - private SavedTootAction handler; - - public SavedTootAdapter(Context context) { - super(); - list = new ArrayList<>(); - handler = (SavedTootAction) context; - } - - @Override - public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.item_saved_toot, parent, false); - return new TootViewHolder(view); - } - - @Override - public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) { - TootViewHolder holder = (TootViewHolder) viewHolder; - holder.bind(getItem(position)); - } - - @Override - public int getItemCount() { - return list.size(); - } - - public void setItems(List newToot) { - list = new ArrayList<>(); - list.addAll(newToot); - } - - public void addItems(List newToot) { - int end = list.size(); - list.addAll(newToot); - notifyItemRangeInserted(end, newToot.size()); - } - - @Nullable - public TootEntity removeItem(int position) { - if (position < 0 || position >= list.size()) { - return null; - } - TootEntity toot = list.remove(position); - notifyItemRemoved(position); - return toot; - } - - private TootEntity getItem(int position) { - if (position >= 0 && position < list.size()) { - return list.get(position); - } - return null; - } - - // handler saved toot - public interface SavedTootAction { - void delete(int position, TootEntity item); - - void click(int position, TootEntity item); - } - - private class TootViewHolder extends RecyclerView.ViewHolder { - View view; - TextView content; - ImageButton suppr; - - TootViewHolder(View view) { - super(view); - this.view = view; - this.content = view.findViewById(R.id.content); - this.suppr = view.findViewById(R.id.suppr); - } - - void bind(final TootEntity item) { - suppr.setEnabled(true); - - if (item != null) { - content.setText(item.getText()); - - suppr.setOnClickListener(v -> { - v.setEnabled(false); - handler.delete(getBindingAdapterPosition(), item); - }); - view.setOnClickListener(v -> handler.click(getBindingAdapterPosition(), item)); - } - } - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt index 649ed1420..f71f26536 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt @@ -1013,7 +1013,6 @@ class ComposeActivity : BaseActivity(), data class ComposeOptions( // Let's keep fields var until all consumers are Kotlin var scheduledTootId: String? = null, - var savedTootUid: Int? = null, var draftId: Int? = null, var tootText: String? = null, var mediaUrls: List? = null, diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt index 71293511d..28019b1ae 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt @@ -45,14 +45,12 @@ class ComposeViewModel @Inject constructor( private val mediaUploader: MediaUploader, private val serviceClient: ServiceClient, private val draftHelper: DraftHelper, - private val saveTootHelper: SaveTootHelper, private val db: AppDatabase ) : RxAwareViewModel() { private var replyingStatusAuthor: String? = null private var replyingStatusContent: String? = null internal var startingText: String? = null - private var savedTootUid: Int = 0 private var draftId: Int = 0 private var scheduledTootId: String? = null private var startingContentWarning: String = "" @@ -216,9 +214,6 @@ class ComposeViewModel @Inject constructor( } fun deleteDraft() { - if (savedTootUid != 0) { - saveTootHelper.deleteDraft(savedTootUid) - } if (draftId != 0) { draftHelper.deleteDraftAndAttachments(draftId) .subscribe() @@ -291,7 +286,6 @@ class ComposeViewModel @Inject constructor( replyingStatusContent = null, replyingStatusAuthorUsername = null, accountId = accountManager.activeAccount!!.id, - savedTootUid = savedTootUid, draftId = draftId, idempotencyKey = randomAlphanumericString(16), retries = 0 @@ -406,20 +400,8 @@ class ComposeViewModel @Inject constructor( } // recreate media list - val loadedDraftMediaUris = composeOptions?.mediaUrls - val loadedDraftMediaDescriptions: List? = composeOptions?.mediaDescriptions val draftAttachments = composeOptions?.draftAttachments - if (loadedDraftMediaUris != null && loadedDraftMediaDescriptions != null) { - // when coming from SavedTootActivity - loadedDraftMediaUris.zip(loadedDraftMediaDescriptions) - .forEach { (uri, description) -> - pickMedia(uri.toUri()).observeForever { errorOrItem -> - if (errorOrItem.isRight() && description != null) { - updateDescription(errorOrItem.asRight().localId, description) - } - } - } - } else if (draftAttachments != null) { + if (draftAttachments != null) { // when coming from DraftActivity draftAttachments.forEach { attachment -> pickMedia(attachment.uri, attachment.description) } } else composeOptions?.mediaAttachments?.forEach { a -> @@ -432,7 +414,6 @@ class ComposeViewModel @Inject constructor( addUploadedMedia(a.id, mediaType, a.url.toUri(), a.description) } - savedTootUid = composeOptions?.savedTootUid ?: 0 draftId = composeOptions?.draftId ?: 0 scheduledTootId = composeOptions?.scheduledTootId startingText = composeOptions?.tootText diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt index ddf8a8385..5f246905e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt @@ -19,19 +19,15 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.util.Log -import android.view.Menu -import android.view.MenuItem import android.widget.LinearLayout import android.widget.Toast import androidx.activity.viewModels -import androidx.lifecycle.Lifecycle import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.snackbar.Snackbar import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.R -import com.keylesspalace.tusky.SavedTootActivity import com.keylesspalace.tusky.components.compose.ComposeActivity import com.keylesspalace.tusky.databinding.ActivityDraftsBinding import com.keylesspalace.tusky.db.DraftEntity @@ -40,7 +36,6 @@ import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.show import com.uber.autodispose.android.lifecycle.autoDispose import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.schedulers.Schedulers import retrofit2.HttpException import javax.inject.Inject @@ -54,8 +49,6 @@ class DraftsActivity : BaseActivity(), DraftActionListener { private lateinit var binding: ActivityDraftsBinding private lateinit var bottomSheet: BottomSheetBehavior - private var oldDraftsButton: MenuItem? = null - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -70,7 +63,7 @@ class DraftsActivity : BaseActivity(), DraftActionListener { setDisplayShowHomeEnabled(true) } - binding.draftsErrorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_saved_status) + binding.draftsErrorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_drafts) val adapter = DraftsAdapter(this) @@ -92,34 +85,6 @@ class DraftsActivity : BaseActivity(), DraftActionListener { } } - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.drafts, menu) - oldDraftsButton = menu.findItem(R.id.action_old_drafts) - viewModel.showOldDraftsButton() - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .autoDispose(this, Lifecycle.Event.ON_DESTROY) - .subscribe { showOldDraftsButton -> - oldDraftsButton?.isVisible = showOldDraftsButton - } - return true - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - android.R.id.home -> { - onBackPressed() - return true - } - R.id.action_old_drafts -> { - val intent = Intent(this, SavedTootActivity::class.java) - startActivityWithSlideInAnimation(intent) - return true - } - } - return super.onOptionsItemSelected(item) - } - override fun onOpenDraft(draft: DraftEntity) { if (draft.inReplyToId != null) { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsViewModel.kt index f928b6d03..8beccfb74 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsViewModel.kt @@ -22,7 +22,6 @@ import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.db.DraftEntity import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.network.MastodonApi -import io.reactivex.Observable import io.reactivex.Single import javax.inject.Inject @@ -37,11 +36,6 @@ class DraftsViewModel @Inject constructor( private val deletedDrafts: MutableList = mutableListOf() - fun showOldDraftsButton(): Observable { - return database.tootDao().savedTootCount() - .map { count -> count > 0 } - } - fun deleteDraft(draft: DraftEntity) { // this does not immediately delete media files to avoid unnecessary file operations // in case the user decides to restore the draft diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java index d35fd3891..1a950feec 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java +++ b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java @@ -24,16 +24,17 @@ import androidx.sqlite.db.SupportSQLiteDatabase; import com.keylesspalace.tusky.TabDataKt; import com.keylesspalace.tusky.components.conversation.ConversationEntity; +import java.io.File; + /** * DB version & declare DAO */ -@Database(entities = { TootEntity.class, DraftEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class, +@Database(entities = { DraftEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class, TimelineAccountEntity.class, ConversationEntity.class - }, version = 25) + }, version = 26) public abstract class AppDatabase extends RoomDatabase { - public abstract TootDao tootDao(); public abstract AccountDao accountDao(); public abstract InstanceDao instanceDao(); public abstract ConversationsDao conversationDao(); @@ -365,4 +366,31 @@ public abstract class AppDatabase extends RoomDatabase { ); } }; + + public static class Migration25_26 extends Migration { + + private final File oldDraftDirectory; + + public Migration25_26(File oldDraftDirectory) { + super(25, 26); + this.oldDraftDirectory = oldDraftDirectory; + } + + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("DROP TABLE `TootEntity`"); + + if (oldDraftDirectory != null && oldDraftDirectory.isDirectory()) { + File[] oldDraftFiles = oldDraftDirectory.listFiles(); + if (oldDraftFiles != null) { + for (File file : oldDraftFiles) { + if (!file.isDirectory()) { + file.delete(); + } + } + } + + } + } + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/db/TootDao.java b/app/src/main/java/com/keylesspalace/tusky/db/TootDao.java deleted file mode 100644 index f46c2753a..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/db/TootDao.java +++ /dev/null @@ -1,45 +0,0 @@ -/* Copyright 2017 Andrew Dawson - * - * This file is a part of Tusky. - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky; if not, - * see . */ - -package com.keylesspalace.tusky.db; - -import androidx.room.Dao; -import androidx.room.Query; - -import java.util.List; - -import io.reactivex.Observable; - -/** - * Created by cto3543 on 28/06/2017. - * - * DAO to fetch and update toots in the DB. - */ - -@Dao -public interface TootDao { - - @Query("SELECT * FROM TootEntity ORDER BY uid DESC") - List loadAll(); - - @Query("DELETE FROM TootEntity WHERE uid = :uid") - int delete(int uid); - - @Query("SELECT * FROM TootEntity WHERE uid = :uid") - TootEntity find(int uid); - - @Query("SELECT COUNT(*) FROM TootEntity") - Observable savedTootCount(); -} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/db/TootEntity.java b/app/src/main/java/com/keylesspalace/tusky/db/TootEntity.java deleted file mode 100644 index b4b258d5c..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/db/TootEntity.java +++ /dev/null @@ -1,151 +0,0 @@ -/* Copyright 2017 Andrew Dawson - * - * This file is a part of Tusky. - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky; if not, - * see . */ - -package com.keylesspalace.tusky.db; - -import com.google.gson.Gson; -import com.keylesspalace.tusky.entity.NewPoll; -import com.keylesspalace.tusky.entity.Status; - -import androidx.annotation.Nullable; -import androidx.room.ColumnInfo; -import androidx.room.Entity; -import androidx.room.PrimaryKey; -import androidx.room.TypeConverter; -import androidx.room.TypeConverters; - -/** - * Toot model. - */ - -@Entity -@TypeConverters(TootEntity.Converters.class) -public class TootEntity { - @PrimaryKey(autoGenerate = true) - private final int uid; - - @ColumnInfo(name = "text") - private final String text; - - @ColumnInfo(name = "urls") - private final String urls; - - @ColumnInfo(name = "descriptions") - private final String descriptions; - - @ColumnInfo(name = "contentWarning") - private final String contentWarning; - - @ColumnInfo(name = "inReplyToId") - private final String inReplyToId; - - @Nullable - @ColumnInfo(name = "inReplyToText") - private final String inReplyToText; - - @Nullable - @ColumnInfo(name = "inReplyToUsername") - private final String inReplyToUsername; - - @ColumnInfo(name = "visibility") - private final Status.Visibility visibility; - - @Nullable - @ColumnInfo(name = "poll") - private final NewPoll poll; - - public TootEntity(int uid, String text, String urls, String descriptions, String contentWarning, String inReplyToId, - @Nullable String inReplyToText, @Nullable String inReplyToUsername, - Status.Visibility visibility, @Nullable NewPoll poll) { - this.uid = uid; - this.text = text; - this.urls = urls; - this.descriptions = descriptions; - this.contentWarning = contentWarning; - this.inReplyToId = inReplyToId; - this.inReplyToText = inReplyToText; - this.inReplyToUsername = inReplyToUsername; - this.visibility = visibility; - this.poll = poll; - } - - public String getText() { - return text; - } - - public String getContentWarning() { - return contentWarning; - } - - public int getUid() { - return uid; - } - - public String getUrls() { - return urls; - } - - public String getDescriptions() { - return descriptions; - } - - public String getInReplyToId() { - return inReplyToId; - } - - @Nullable - public String getInReplyToText() { - return inReplyToText; - } - - @Nullable - public String getInReplyToUsername() { - return inReplyToUsername; - } - - public Status.Visibility getVisibility() { - return visibility; - } - - @Nullable - public NewPoll getPoll() { - return poll; - } - - public static final class Converters { - - private static final Gson gson = new Gson(); - - @TypeConverter - public Status.Visibility visibilityFromInt(int number) { - return Status.Visibility.byNum(number); - } - - @TypeConverter - public int intFromVisibility(Status.Visibility visibility) { - return visibility == null ? Status.Visibility.UNKNOWN.getNum() : visibility.getNum(); - } - - @TypeConverter - public String pollToString(NewPoll poll) { - return gson.toJson(poll); - } - - @TypeConverter - public NewPoll stringToPoll(String poll) { - return gson.fromJson(poll, NewPoll.class); - } - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt index 2e82d6407..cdf10224b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt @@ -79,9 +79,6 @@ abstract class ActivitiesModule { @ContributesAndroidInjector abstract fun contributesSplashActivity(): SplashActivity - @ContributesAndroidInjector - abstract fun contributesSavedTootActivity(): SavedTootActivity - @ContributesAndroidInjector(modules = [FragmentBuildersModule::class]) abstract fun contributesPreferencesActivity(): PreferencesActivity diff --git a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt index 13266851b..2a699ee33 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt @@ -82,7 +82,9 @@ class AppModule { AppDatabase.MIGRATION_13_14, AppDatabase.MIGRATION_14_15, AppDatabase.MIGRATION_15_16, AppDatabase.MIGRATION_16_17, AppDatabase.MIGRATION_17_18, AppDatabase.MIGRATION_18_19, AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21, AppDatabase.MIGRATION_21_22, - AppDatabase.MIGRATION_22_23, AppDatabase.MIGRATION_23_24, AppDatabase.MIGRATION_24_25) + AppDatabase.MIGRATION_22_23, AppDatabase.MIGRATION_23_24, AppDatabase.MIGRATION_24_25, + AppDatabase.Migration25_26(appContext.getExternalFilesDir("Tusky")) + ) .build() } diff --git a/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt b/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt index 911f58c1b..b0ecc4ad5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt +++ b/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt @@ -101,7 +101,6 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() { replyingStatusContent = null, replyingStatusAuthorUsername = null, accountId = account.id, - savedTootUid = -1, draftId = -1, idempotencyKey = randomAlphanumericString(16), retries = 0 diff --git a/app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt b/app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt index c6b07bb72..460c83e5e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt +++ b/app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt @@ -26,7 +26,6 @@ import com.keylesspalace.tusky.entity.NewPoll import com.keylesspalace.tusky.entity.NewStatus import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.util.SaveTootHelper import dagger.android.AndroidInjection import kotlinx.parcelize.Parcelize import retrofit2.Call @@ -49,8 +48,6 @@ class SendTootService : Service(), Injectable { lateinit var database: AppDatabase @Inject lateinit var draftHelper: DraftHelper - @Inject - lateinit var saveTootHelper: SaveTootHelper private val tootsToSend = ConcurrentHashMap() private val sendCalls = ConcurrentHashMap>() @@ -162,9 +159,6 @@ class SendTootService : Service(), Injectable { if (response.isSuccessful) { // If the status was loaded from a draft, delete the draft and associated media files. - if (tootToSend.savedTootUid != 0) { - saveTootHelper.deleteDraft(tootToSend.savedTootUid) - } if (tootToSend.draftId != 0) { draftHelper.deleteDraftAndAttachments(tootToSend.draftId) .subscribe() @@ -332,7 +326,6 @@ data class TootToSend( val replyingStatusContent: String?, val replyingStatusAuthorUsername: String?, val accountId: Long, - val savedTootUid: Int, val draftId: Int, val idempotencyKey: String, var retries: Int diff --git a/app/src/main/java/com/keylesspalace/tusky/util/SaveTootHelper.java b/app/src/main/java/com/keylesspalace/tusky/util/SaveTootHelper.java deleted file mode 100644 index 29693550d..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/util/SaveTootHelper.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.keylesspalace.tusky.util; - -import android.content.Context; -import android.net.Uri; -import android.util.Log; - -import androidx.annotation.NonNull; - -import com.google.gson.Gson; -import com.google.gson.reflect.TypeToken; -import com.keylesspalace.tusky.db.AppDatabase; -import com.keylesspalace.tusky.db.TootDao; -import com.keylesspalace.tusky.db.TootEntity; - -import java.util.ArrayList; - -import javax.inject.Inject; - -public final class SaveTootHelper { - - private static final String TAG = "SaveTootHelper"; - - private TootDao tootDao; - private Context context; - private Gson gson = new Gson(); - - @Inject - public SaveTootHelper(@NonNull AppDatabase appDatabase, @NonNull Context context) { - this.tootDao = appDatabase.tootDao(); - this.context = context; - } - - public void deleteDraft(int tootId) { - TootEntity item = tootDao.find(tootId); - if (item != null) { - deleteDraft(item); - } - } - - public void deleteDraft(@NonNull TootEntity item) { - // Delete any media files associated with the status. - ArrayList uris = gson.fromJson(item.getUrls(), - new TypeToken>() { - }.getType()); - if (uris != null) { - for (String uriString : uris) { - Uri uri = Uri.parse(uriString); - if (context.getContentResolver().delete(uri, null, null) == 0) { - Log.e(TAG, String.format("Did not delete file %s.", uriString)); - } - } - } - // update DB - tootDao.delete(item.getUid()); - } - -} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_saved_toot.xml b/app/src/main/res/layout/activity_saved_toot.xml deleted file mode 100644 index 1771135e7..000000000 --- a/app/src/main/res/layout/activity_saved_toot.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/item_saved_toot.xml b/app/src/main/res/layout/item_saved_toot.xml deleted file mode 100644 index c95473f27..000000000 --- a/app/src/main/res/layout/item_saved_toot.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/drafts.xml b/app/src/main/res/menu/drafts.xml deleted file mode 100644 index bbc9202f4..000000000 --- a/app/src/main/res/menu/drafts.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index c2098f070..1e1d44fc9 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -101,7 +101,7 @@ موافقة رفض البحث - المسودات + المسودات كيفية عرض التبويق تحذير عن المحتوى لوحة مفاتيح الإيموجي @@ -473,7 +473,7 @@ أضيف إلى الفواصل المرجعية اختر قائمة القائمة - ليس لديك أية مسودات. + ليس لديك أية مسودات. ليس لديك أية منشورات مُبرمَجة للنشر. يجب أن يكون حجم الملفات الصوتية أقل مِن 40 ميغابايت. تُقدّر أدنى فترة لبرمجة النشر في ماستدون بـ 5 دقائق. diff --git a/app/src/main/res/values-ber/strings.xml b/app/src/main/res/values-ber/strings.xml index 9726d4ff0..b6d317b5a 100644 --- a/app/src/main/res/values-ber/strings.xml +++ b/app/src/main/res/values-ber/strings.xml @@ -28,7 +28,7 @@ ⴸ ⴰⵛⵓ ⵓⴸ ⵜⵜⵓⵎⵎⴰⵏⵜ\? ⵉⵙⵎⴻⵏⵢⵉⴼⴻⵏ ⴽⴽⴻⵙ - ⵉⵔⴻⵡⵡⴰⵢⴻⵏ + ⵉⵔⴻⵡⵡⴰⵢⴻⵏ ⵉⵎⵙⴻⵇⴷⴰⵛⴻⵏ ⵜⵙⵡⴰⵃⵍⴻⵎ ⵉⵛⵛⴰⵔⴻⵏ ⴰⵏⵜⴰ ⵝⵓⵎⵎⴰⵏⵜ\? diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 0056bfdb7..a3db937bc 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -320,7 +320,7 @@ Предупреждение за съдържание Видимост на публикация Планирани публикации - Чернови + Чернови Търсене Отхвърляне Приемане @@ -442,9 +442,6 @@ Възникна грешка. Черновата е изтрита Неуспешно зареждане на информация за отговор - Стари чернови - Функцията за чернови в Tusky е напълно преработена, за да бъде по-бърза, по-лесна за ползване и по-малко бъгава. -\n Все още можете да осъществите достъп до старите си чернови чрез бутон на екрана за нови чернови, но те ще бъдат премахнати при бъдеща актуализация! Тази публикация не успя да се изпрати! Наистина ли искате да изтриете списъка %s\? @@ -470,7 +467,7 @@ Mastodon има минимален интервал за планиране от 5 минути. Няма оповестявания. Нямате планирани състояния. - Нямате чернови. + Нямате чернови. Грешка при търсенето на публикация %s Редакция Избор %d diff --git a/app/src/main/res/values-bn-rBD/strings.xml b/app/src/main/res/values-bn-rBD/strings.xml index aa285d3d1..aec035d4b 100644 --- a/app/src/main/res/values-bn-rBD/strings.xml +++ b/app/src/main/res/values-bn-rBD/strings.xml @@ -236,7 +236,7 @@ ইমোজি কীবোর্ড সতর্কবার্তা টুট দৃশ্যমানতা - ড্রাফটগুলি + ড্রাফটগুলি অনুসন্ধান প্রত্যাখ্যান গ্রহণ @@ -331,7 +331,7 @@ আলাপ বন্ধ করো মাস্টোডনের সর্বনিম্ন ৫ মিনিটের সময়সূচীর বিরতি আছে। অডিও ফাইলগুলি অবশ্যই ৪০MB এর চেয়ে কম হওয়া উচিত। - তোমার কোনো খসড়া নেই। + তোমার কোনো খসড়া নেই। তোমার কোনো সময়সূচীত স্ট্যাটাস নেই। তালিকা তালিকা নির্বাচন করো diff --git a/app/src/main/res/values-bn-rIN/strings.xml b/app/src/main/res/values-bn-rIN/strings.xml index c13a880de..67a9225d0 100644 --- a/app/src/main/res/values-bn-rIN/strings.xml +++ b/app/src/main/res/values-bn-rIN/strings.xml @@ -102,7 +102,7 @@ গ্রহণ প্রত্যাখ্যান অনুসন্ধান - খসড়াগুলো + খসড়াগুলো টুট দৃশ্যমানতা সতর্কবার্তা ইমোজি কীবোর্ড @@ -473,7 +473,7 @@ ট্যাবের মাঝে সোয়াইপ সংকেত চালু করো টাইমলাইনে লিঙ্ক প্রিভিউ দেখাও তোমার কোনো সময়সূচীত স্ট্যাটাস নেই। - তোমার কোনো খসড়া নেই। + তোমার কোনো খসড়া নেই। মাস্টোডনের সর্বনিম্ন ৫ মিনিটের সময়সূচীর বিরতি আছে। শীর্ষস্থানীয় সরঞ্জামদণ্ডের শিরোনামটি লুকান \ No newline at end of file diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 8dbaf1218..fbad68d5c 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -80,7 +80,7 @@ D\'acord Rebutja Cerca - Esborranys + Esborranys S\'està baixant %1$s Copia l\'enllaç Comparteix l\'URL del toot a… @@ -433,7 +433,7 @@ S\'ha produït un error en cercar la publicació %s No tens cap estat planificat. Els fitxers d\'àudio han de ser de mida menor de 40MB. - No teniu cap esborrany. + No teniu cap esborrany. L\'interval mínim de planificació a Mastodon és de 5 minuts. Peticions de seguiment Mostra el diàleg de confirmació abans de promoure @@ -496,7 +496,6 @@ S\'ha esborrat el tut del qual en vau fer un esborrany de resposta S\'ha eliminat l\'esborrany No s\'ha pogut carregar la informació de la resposta - Esborranys antics No s\'ha pogut enviar aquest tut! Segur que voleu esborrar la llista %s\? diff --git a/app/src/main/res/values-ckb/strings.xml b/app/src/main/res/values-ckb/strings.xml index 5fb07bed5..c5702f28c 100644 --- a/app/src/main/res/values-ckb/strings.xml +++ b/app/src/main/res/values-ckb/strings.xml @@ -34,7 +34,7 @@ ئاگاداری ناوەڕۆک بینینی توت توتی خشتەکراو - ڕەشنووسەکان + ڕەشنووسەکان گەڕان ڕەتکردنەوە ڕازیبون @@ -257,7 +257,7 @@ ماستۆدۆن کەمترین ماوەی خشتەی هەیە لە ٥ خولەک. هیچ ڕاگه یه نراوێک له بەرده رنه کەون. هیچ بارێکی خشتەکراوت نیە. - هیچ ڕەشنووسێکت نییە. + هیچ ڕەشنووسێکت نییە. هەڵە لە گەڕان بەدوای بابەت %s دەستکاریکردن هەڵبژاردنی %d diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 83cb9751d..0a6fa994e 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -101,7 +101,7 @@ Přijmout Zamítnout Hledat - Koncepty + Koncepty Viditelnost tootu Varování o obsahu Klávesnice s emoji @@ -465,7 +465,7 @@ Ukazovat náhledy k odkazům Mastodon neumožňuje pracovat s intervalem menším než 5 minut. Zatím zde nemáte žádné naplánované statusy. - Zatím zde nejsou žádné koncepty. + Zatím zde nejsou žádné koncepty. Možnost přetahování prstem pro přechod mezi kartami Seznam Přidat hashtag diff --git a/app/src/main/res/values-cy/strings.xml b/app/src/main/res/values-cy/strings.xml index c5a8af44b..c1a31d97b 100644 --- a/app/src/main/res/values-cy/strings.xml +++ b/app/src/main/res/values-cy/strings.xml @@ -92,7 +92,7 @@ Derbyn Gwrthod Chwilio - Drafftiau + Drafftiau Pwy all weld Tŵt Rhybudd cynnwys Bysellfwrdd emoji diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index a67fafff0..9af9b790a 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -101,7 +101,7 @@ Akzeptieren Ablehnen Suche - Entwürfe + Entwürfe Beitragssichtbarkeit Inhaltswarnung Emoji @@ -436,7 +436,7 @@ Liste auswählen Liste Fehler beim Nachschlagen von Post %s - Du hast keine Entwürfe. + Du hast keine Entwürfe. Du hast keine geplanten Beiträge. Das Datum des geplanten Toots muss mindestens 5 Minuten in der Zukunft liegen. Benachrichtigungen über neue Folgeanfragen @@ -480,9 +480,6 @@ Ankündigungen Der Beitrag auf den du antworten willst wurde gelöscht Entwurf gelöscht - Alte Entwürfe - Das \"Entwürfe\"-Feature in Tusky wurde komplett neu gestaltet um schneller und benutzerfreundlicher zu sein. -\nDu kannst deine alten Entwürfe noch hinter einem Button bei den neuen Entwürfen finden, aber sie werden mit einem zukünftigen Update gelöscht! Dieser Beitrag konnte nicht gesendet werden! Willst du die Liste %s wirklich löschen\? diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index b55353d49..59aa8fa9f 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -101,7 +101,7 @@ Rajtigi Rifuzi Serĉi - Malnetoj + Malnetoj Videblo de la mesaĝo Enhava averto Klavaro de emoĝioj @@ -438,7 +438,7 @@ Listo Eraro dum elserĉo de la mesaĝo %s Aŭdia dosiero devas esti malpli ol 40MB. - Vi ne havas iun ajn malneton. + Vi ne havas iun ajn malneton. Vi ne havas iun ajn planitan mesaĝon. Petoj de sekvado Kradvortoj diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 127a8ed02..7e6695499 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -99,7 +99,7 @@ Aceptar Rechazar Buscar - Borradores + Borradores Visibilidad del estado Aviso de contenido Teclado de emojis @@ -451,7 +451,7 @@ Seleccionar lista Lista Los ficheros de audio deben ser menores de 40MB. - No tienes ningún borrador. + No tienes ningún borrador. No tienes ningún estado programado. Mastodon tiene un intervalo de programación mínimo de 5 minutos. Solicitudes @@ -506,9 +506,6 @@ El toot al que redactaste una respuesta ha sido eliminado Borrador eliminado Error al cargar la información de respuesta - Borradores antiguos - La función de borrador en Tusky se ha rediseñado por completo para que sea más rápida, más fácil de usar y con menos errores. -\nAún puede acceder a sus borradores antiguos a través de un botón en la pantalla de borradores nuevos, ¡pero se eliminarán en una actualización futura! ¡Este toot no se pudo enviar! ¿Realmente quieres eliminar la lista %s\? Indefinido diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index 2430e0644..df85128fa 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -93,7 +93,7 @@ Onartu Ukatu Bilatu - Zirriborroak + Zirriborroak Tutaren ikusgarritasuna Edukiaren abisua Emoji teklatua @@ -444,7 +444,7 @@ Audioak 40MB baino gutxiago izan behar ditu. Aukeratu zerrenda Zerrenda - Ez duzu zirriborrorik. + Ez duzu zirriborrorik. Ez duzu tut programaturik. Mastodonek gutxienez 5 minutuko programazio-tartea du. Eskakizunak diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 80c2911c4..5cd052072 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -93,7 +93,7 @@ پذیرش رد جست‌وجو - پیش‌نویس‌ها + پیش‌نویس‌ها نمایانی بوق هشدار محتوا صفحه‌کلید اموجی @@ -439,7 +439,7 @@ نشان‌شده گزینش فهرست فهرست - هیچ پیش‌نویسی ندارید. + هیچ پیش‌نویسی ندارید. هیچ وضعیت زمان‌بسته‌ای ندارید. ماستودون، بازهٔ زمان‌بندی‌ای با کمینهٔ ۵ دقیقه دارد. نمایش گفت‌وگوی تأیید، پیش از تقویت @@ -485,7 +485,6 @@ عدم اشتراک اشتراک پیش‌نویس حذف شد - پیش‌نویس‌های قدیمی فرستادن این بوق شکست خورد! نهفتن آمار کمی روی نمایه‌ها نهفتن آمار کمی روی فرسته‌ها @@ -509,8 +508,6 @@ \n - آمار پی‌گیر و فرسته روی نمایه‌ها \n \n فرستادن آگاهی‌ها تأثیر نمی‌پذیرد، ولی می‌توانید ترجیحات آگاهیتان را به صورت دستی بازبینی کنید. - ویژگی پیش‌نویس در تاسکی به صورت کامل بازطرّاحی شده تا سریع‌تر، کاربرپسندتر و کم‌مشکل‌تر باشد. -\n همجنان می‌توانید از طریق دکمه‌ای دز صفحهٔ پیش‌نویس‌های جدید، به پیش‌نویس‌های قدیمیتان دسترسی داشته باشید، ولی در به‌روز رسانی آینده برداشته خواهند شد! واقعاً می‌خواهید فهرست %s را حذف کنید؟ نمی‌توانید بیش از %1$d رسانه بارگذارید. diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 41d01a443..df6461abd 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -101,7 +101,7 @@ Accepter Refuser Rechercher - Brouillons + Brouillons Visibilité du pouet Contenu sensible Clavier d’émojis @@ -456,7 +456,7 @@ Sélectionner la liste Liste Les fichiers audio doivent avoir moins de 40 Mo. - Vous n’avez aucun brouillon. + Vous n’avez aucun brouillon. Vous n’avez aucun pouet planifié. L’intervalle minimum de planification sur Mastodon est de 5 minutes. Demandes d\'abonnement diff --git a/app/src/main/res/values-ga/strings.xml b/app/src/main/res/values-ga/strings.xml index 18261a959..7ebc79bac 100644 --- a/app/src/main/res/values-ga/strings.xml +++ b/app/src/main/res/values-ga/strings.xml @@ -109,7 +109,7 @@ Rabhadh ábhair Infheictheacht tút Tútanna sceidealta - Dréachtaí + Dréachtaí Diúltaigh Glac Cealaigh @@ -463,7 +463,7 @@ Rogha %d Cuir in Eagar Earráid agus an post á lorg %s - Níl aon dréachtaí agat. + Níl aon dréachtaí agat. Níl aon stádas sceidealta agat. Tá eatramh sceidealaithe íosta 5 nóiméad ag Mastodon. Taispeáin réamhamhairc nasc in amlínte diff --git a/app/src/main/res/values-gd/strings.xml b/app/src/main/res/values-gd/strings.xml index 13d53a29b..ff23b95df 100644 --- a/app/src/main/res/values-gd/strings.xml +++ b/app/src/main/res/values-gd/strings.xml @@ -47,7 +47,7 @@ Sgrìobh Seall na brosnachaidhean Seall na brosnachaidhean - Dreachdan + Dreachdan Annsachdan Brathan Brathan @@ -58,10 +58,7 @@ Bha againn ris a’ phost a bha thu airson freagairt dha a thoirt air falbh Chaidh an dreach a sguabadh às Cha deach leinn fiosrachadh na freagairte a luchdadh - Seann-dreachdan - Chaidh dealbhadh gu tur ùr a chur air gleus nan dreachdan aig Tusky ach am biodh e nas luaithe, nas fhasa cleachdadh is nas lugha de bhugaichean ann. -\n Gheibh thu grèim air na seann-dreachdan agad fhathast le putan air sgrìn ùr nan dreachdan ach thèid an toirt air falbh le ùrachadh ri teachd! - Cha b’ urrainn dhuinn am post a chur! + Cha b’ urrainn dhuinn an dùd a chur! Ceanglachain Fuaim A bheil thu cinnteach gu bheil thu airson an liosta %s a sguabadh às\? @@ -108,7 +105,7 @@ Seall ro-sheallaidhean air ceanglaichean sna loidhnichean-ama Feumaidh co-dhiù 5 mionaidean a bhith eadar staidean sgeidealaichte air Mastodon. Chan eil staid sam bith air an sgeideal agad. - Chan eil dreachd sam bith agad. + Chan eil dreachd sam bith agad. Thachair mearachd le lorg a’ phuist %s Roghainn %d Iomadh roghainn diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index bbba46cfe..150a40785 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -3,7 +3,7 @@ Aviso sobre o contido Visibilidade do toot Toots programados - Borradores + Borradores Buscar Rexeitar Aceptar @@ -438,9 +438,6 @@ Que contas\? Borrador eliminado Fallou a carga da información da Resposta - Borradores antigos - Os borradores en Tusky foron redeseñados para ser máis rápidos, amigables para a usuaria e con menos fallos. -\nAínda podes acceder aos antigos borradores a través do botón na pantalla de novos borradores, pero eliminarémolo en futuras actualizacións! Fallou o envío do toot! Tes a certeza de querer eliminar a listaxe %s\? @@ -467,7 +464,7 @@ Mastodon ten un intervalo mínimo de 5 minutos para as programacións. Non hai anuncios. Non tes estados programados. - Non tes borradores. + Non tes borradores. Erro ao buscar publicación %s Editar Opción %d diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index dd1a60e86..ddd50890a 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -152,7 +152,7 @@ टूट दृश्यता अनुसूचित टूट स्वीकार करें - ड्राफ्ट + ड्राफ्ट अस्वीकार करें पूर्ववत करें संपादित करें @@ -257,7 +257,7 @@ और लोड करें टाइमलाइन में लिंक प्रीव्यू दिखाएं मास्टोडन का न्यूनतम शेड्यूलिंग अंतराल 5 मिनट है। - आपके पास कोई ड्राफ्ट नहीं है। + आपके पास कोई ड्राफ्ट नहीं है। %s पोस्ट खोजने में त्रुटि टैब के बीच स्विच करने के लिए स्वाइप जेस्चर को सक्षम करें सूचना फ़िल्टर दिखाएं diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index c76bc0650..4596127b4 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -99,7 +99,7 @@ Elfogadás Elutasítás Keresés - Piszkozatok + Piszkozatok Tülkök láthatósága Tartalom figyelmeztetés Emoji billentyűzet @@ -450,7 +450,7 @@ Lista kiválasztása Lista A hangfájloknak kisebbnek kell lenniük, mint 40 MB. - Nincs egy piszkozatod sem. + Nincs egy piszkozatod sem. Nincs egy ütemezett tülköd sem. A Mastodonban a legrövidebb ütemezhető időintervallum 5 perc. Követési kérelmek @@ -487,9 +487,6 @@ A Tülköt, melyre válaszul piszkozatot készítettél törölték Piszkozat törölve Nem sikerült a Válasz információit betölteni - Régi Piszkozatok - A Tusky piszkozat funkcióját teljesen újraterveztük, hogy gyorsabb, felhasználóbarátabb és hibamentesebb legyen. -\nTovábbra is elérheted a régi piszkozataidat egy gombbal az új piszkozatok képernyőjén, de ezeket egy későbbi frissítésben el fogjuk törölni! Ez a tülk nem küldődött el! Tényleg le akarod törölni a %s listát\? diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index 1a23be14f..af8b146e6 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -115,7 +115,7 @@ Afturkalla Samþykkja Hafna - Drög + Drög Áætluð tíst Sýnileiki tísts Aðvörun vegna efnis @@ -416,7 +416,7 @@ Valkostur %d Breyta Villa við að fletta upp færslunni %s - Þú ert ekki með nein drög. + Þú ert ekki með nein drög. Þú ert ekki með neinar áætlaðar stöðufærslur. Hljóðskrár verða að vera minni en 40MB. Mastodon er með 5 mínútna lágmarksbil fyrir áætlaðar aðgerðir. @@ -475,8 +475,6 @@ Afþagga %s %s bað um að fylgjast með þér Tilkynningar - Gerð draga í Tusky hefur verið endurhönnuð til að verða fljótlegri, notendavænni og gallalaus. -\n Þú getur áfram nýtt eldri drög í gegnum sérstakan hnapp í glugganum fyrir drög, en sá eiginleiki verður fjarlægður í framtíðaruppfærslu! Sumar upplýsingar sem gætu haft áhrif á andlega vellíðan þína verða faldar. Þetta hefur áhrif á: \n \n - Eftirlæti/Endurbirtingar/Tilkynningar um fylgjendabeiðnir @@ -490,7 +488,6 @@ Tístið sem þú gerðir drög að svari við hefur veriið fjarlægt Eyddi drögum Mistókst að hlaða inn svarupplýsingum - Eldri drög Mistókst að senda þetta tíst! Viðhengi Hljóð diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 3c8093557..c84301b13 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -101,7 +101,7 @@ Accetta Rifiuta Cerca - Bozze + Bozze Visibilità dei toot Avviso per il contenuto Tastiera emoji @@ -454,7 +454,7 @@ Programma un toot RIpristina %1$s • %2$s - Non hai bozze. + Non hai bozze. %s persona %s persone diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 9185911c3..d602f37b8 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -98,7 +98,7 @@ 許可 拒否 検索 - 下書き + 下書き トゥートの公開範囲 注意書き 絵文字キーボード @@ -411,7 +411,7 @@ %sさんがあなたにフォローリクエストしました \@%sさんを通報しました 予約した投稿はありません。 - 下書きはありません。 + 下書きはありません。 項目 %d このアカウントは外部のサーバーにあります。匿名化された通報の複製をそちらにも送信しますか? 通報をサーバーのモデレーターに送信します。以下にこのアカウントを通報理由を入力できます: diff --git a/app/src/main/res/values-kab/strings.xml b/app/src/main/res/values-kab/strings.xml index ad5a99d92..d63c92792 100644 --- a/app/src/main/res/values-kab/strings.xml +++ b/app/src/main/res/values-kab/strings.xml @@ -40,14 +40,14 @@ Ldi deg uminig Bḍu Sgugem - Irewwayen + Irewwayen Sken-d ismenyifen Ismenyifen %1$s n usmenyaf %1$s n ismenyifen - Ur tesɛiḍ ara irewwayen. + Ur tesɛiḍ ara irewwayen. Tella-d tucḍa. Tilɣa D acu i ttummant\? diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index c955454a1..b4e2bf624 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -105,7 +105,7 @@ 수락 거절 검색 - 임시 저장 + 임시 저장 공개 범위 열람 주의 이모지 추가 diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index 4f4a7868b..81a89d731 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -106,7 +106,7 @@ പിന്തുടാനുള്ള അപേക്ഷകൾ ബൂട്ട്‌സ് കാണിക്കുക മുന്‍നിശ്ചയിച്ച ടൂറ്റ്‌സ് - കരടുകൾ + കരടുകൾ തിരുത്ത് അറിയിപ്പുകൾ ടാബുകൾ diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 4ff5a90e0..8c5da5abe 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -101,7 +101,7 @@ Goedkeuren Afwijzen Zoeken - Concepten + Concepten Zichtbaarheid toot Tekstwaarschuwing Emojis @@ -456,8 +456,8 @@ Zoeken mislukt Poll Fout tijdens opzoeken toot %s - Je hebt nog geen concepten. - Je hebt nog geen ingeplande toots. + Je hebt nog geen concepten + Je hebt nog geen ingeplande toots Om in te plannen moet je in Mastodon een minimum interval van 5 minuten gebruiken. Volgverzoeken Hashtags diff --git a/app/src/main/res/values-no-rNB/strings.xml b/app/src/main/res/values-no-rNB/strings.xml index f6c9f86ba..40a7607c4 100644 --- a/app/src/main/res/values-no-rNB/strings.xml +++ b/app/src/main/res/values-no-rNB/strings.xml @@ -101,7 +101,7 @@ Aksepter Avvis Søk - Kladder + Kladder Toot-synlighet Innholdsadvarsel Emoji-tastatur @@ -441,7 +441,7 @@ Velg liste Liste Du har ingen planlagte statuser. - Du har ikke lagret noen kladder. + Du har ikke lagret noen kladder. Lydfiler må være mindre enn 40MB. Mastodon har et minimums planleggingsinterval på 5 minutter. Vis forhåndsvisning av linker i tidslinjer @@ -503,9 +503,6 @@ Tootet du kladdet et svar til har blitt fjernet Kladd slettet Lasting av svarinformasjon feilet - Gamle kladder - KladdfunksjonaLiteten i Tusky er skrevet om og er nå kjappere, mer brukervennlig, og med færre feil. -\nGamle kladder er fortsatt tilgjengelige via en knapp på den nye kladdskjermen, men de vil bli fjernet i en fremtidig oppdatering! Sending av toot feilet! Animer egendefinerte emojis Avslutt abonnementet diff --git a/app/src/main/res/values-oc/strings.xml b/app/src/main/res/values-oc/strings.xml index 4de5640d6..101303192 100644 --- a/app/src/main/res/values-oc/strings.xml +++ b/app/src/main/res/values-oc/strings.xml @@ -91,7 +91,7 @@ Acceptar Regetar Cercar - Borrolhons + Borrolhons Visibilitat del tut Avis de contengut Clavièr Emoji @@ -450,7 +450,7 @@ Seleccionar la list Lista Los fichièrs àudio devon èsser inferiors a 40 Mo. - Avètz pas cap de borrolhon. + Avètz pas cap de borrolhon. Avètz pas cap de tut planificat. L’interval minimum de planificacion sus Mastodon e de 5 minutas. Demandas d’abonament diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 5ec0dbf5c..716e07a61 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -89,7 +89,7 @@ Akceptuj Odrzuć Szukaj - Szkice + Szkice Widoczność wpisu Ostrzeżenie o zawartości Klawiatura emoji @@ -461,7 +461,7 @@ Wybierz listę Lista Pliki audio muszą być mniejsze niż 40MB. - Nie masz żadnych szkiców. + Nie masz żadnych szkiców. Nie masz żadnych zaplanowanych wpisów. Mastodon umożliwia wysłanie minimalnie 5 minut od zaplanowania. Prośby o możliwość śledzenia diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index cf6cfdf2f..685049d0b 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -94,7 +94,7 @@ Aceitar Rejeitar Pesquisar - Rascunhos + Rascunhos Privacidade do toot Aviso de Conteúdo Teclado de emojis @@ -449,7 +449,7 @@ Lista Sem toots agendados. O áudio deve ser menor que 40MB. - Sem rascunhos. + Sem rascunhos. Mastodon possui um intervalo mínimo de 5 minutos para agendar. Seguidores pendentes %s quer te seguir @@ -484,9 +484,6 @@ Erro ao enviar o toot! O toot em que se rascunhou uma resposta foi excluído Rascunho excluído - A função de rascunhos no Tusky foi totalmente redesenhada para ser mais rápida, mais fácil e com menos erros. -\nÉ possível acessar rascunhos antigos através de um botão na tela de novos rascunhos, mas serão removidos numa futura atualização! - Rascunhos antigos Não é possível anexar mais de %1$d arquivos de mídia. diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index dd90ed48b..ef52a679b 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -102,7 +102,7 @@ Принять Отклонить Поиск - Черновики + Черновики Видимость поста Предупреждение о контенте Эмодзи-клавиатура @@ -483,8 +483,8 @@ Список Аудиофайлы должны быть меньше 40МБ. Ошибка поиска поста %s - У вас нет черновиков. - У вас нет запланированных постов. + У вас нет черновиков. + У вас нет запланированный постов. Минимальный интервал планирования в Mastodon составляет 5 минут. Показывать диалог подтверждения перед продвижением Показывать предпросмотр ссылок в лентах diff --git a/app/src/main/res/values-sa/strings.xml b/app/src/main/res/values-sa/strings.xml index 168b1baf1..29994122c 100644 --- a/app/src/main/res/values-sa/strings.xml +++ b/app/src/main/res/values-sa/strings.xml @@ -131,7 +131,7 @@ विषयप्रत्यादेशः दौत्यसुदर्शता कालबद्धदौत्यानि - लेखविकर्षाः + लेखविकर्षाः अन्विष्यताम् अस्वीक्रियताम् स्वीक्रियताम् @@ -366,7 +366,7 @@ प्रकाशनंं नश्यताम् मूलदर्शकेभ्यः प्रकाश्यताम् %1$s मित्रमत्र प्रस्थितम्: - न लेखविकर्षास्ते सन्ति । + न लेखविकर्षास्ते सन्ति । %1$s उच्चैःस्थितायाः साधनशालकायाः शीर्षकं छाद्यताम् प्रकाशनात् प्राक् पुष्टिसंवादमञ्जूषा दर्शनीया diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 14af6584d..5b026442f 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -124,7 +124,7 @@ %1$s a %2$s Upraviť Hashtagy - Koncepty + Koncepty Upraviť Oznámenia Oznámenia diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index 033c89b9b..f2f4487e0 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -96,7 +96,7 @@ Sprejmi Zavrni Iskanje - Osnutki + Osnutki Vidljivost tuta Opozorilo o vsebini Tipkovnica z emotikoni diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index dc62bf6d8..d94fee33c 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -101,7 +101,7 @@ Acceptera Avvisa Sök - Utkast + Utkast Toot synlighet Innehållsvarning Emoji-tangentbord @@ -457,7 +457,7 @@ Lista Du har inga schemalagda statusar. Ljudfiler måste vara mindre än 40MB. - Du har inga utkast. + Du har inga utkast. Mastodon har ett minimalt schemaläggningsintervall på 5 minuter. Tysta konversation Visa bekräftelse innan knuff diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 557911456..f197abf25 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -84,7 +84,7 @@ ஏற்கவும் நிராகரி தேடு - வரைவுகள் + வரைவுகள் Toot புலப்படும் தன்மை உள்ளடக்க எச்சரிக்கை Emoji விசைபலகை diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index f0554fe76..9d188dce4 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -321,7 +321,7 @@ เตือนเนื้อหา การมองเห็น Toot Toot แบบตั้งเวลา - ฉบับร่าง + ฉบับร่าง ปฏิเสธ ยอมรับ ยกเลิก @@ -447,7 +447,7 @@ แสดงตัวอย่างลิงก์ในไทม์ไลน์ Mastodon กำหนดเวลาขั้นต่ำ 5 นาที ไม่มีสถานะแบบตั้งเวลาใด ๆ - ไม่มีฉบับร่างใด ๆ + ไม่มีฉบับร่างใด ๆ การค้นหาโพสต์ %s เกิดข้อผิดผลาด แก้ไข ตัวเลือกที่ %d @@ -472,8 +472,6 @@ แจ้งเตือน Limit timeline แจ้งเตือน Review ใครบางคนที่ฉันได้ติดตาม ได้เผยแพร่โพสต์ใหม่ - ฟีเจอร์ฉบับร่างใน Tusky ได้รับการออกแบบใหม่ทั้งหมดเพื่อให้เร็วขึ้นเป็นมิตรกับผู้ใช้มากขึ้นและบั๊กน้อยลง -\n คุณยังสามารถเข้าถึงฉบับร่างเก่าผ่านปุ่มในหน้าฉบับร่างใหม่ แต่จะถูกลบออกในการอัปเดตในอนาคต! ซ่อนสถิติเชิงปริมาณในโปรไฟล์ ซ่อนสถิติเชิงปริมาณของโพสต์ สุขภาวะ @@ -482,7 +480,6 @@ โพสต์ที่คุณได้ร่างตอบไว้ ถูกลบแลัว ลบฉบับร่างแล้ว ล้มเหลวในการโหลดข้อมูลตอบกลับ - ฉบับร่างเก่า คุณต้องการลบลิสต์ %s ใช่ไหม\? คุณไม่สามารถอัปโหลดไฟล์แนบมากกว่า %1$d ได้ diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 87638c87f..0150203bb 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -99,7 +99,7 @@ Kabul et Reddet Ara - Taslaklar + Taslaklar Toot görünürlüğü İçerik uyarı İfade klavyesi @@ -443,7 +443,7 @@ Bildirilemedi Seçenek %d %s gönderisi aranırken hata oluştu - Hiç taslağınız yok. + Hiç taslağınız yok. Zamanlanmış durumunuz yok. Kendi kitlenize yükseltin Hashtags\'ler diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 6144773a8..93910a137 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -58,7 +58,7 @@ Посилання Попередження про вміст Заплановані дмухи - Чернетки + Чернетки Відхилити Прийняти Скасувати @@ -458,9 +458,6 @@ Дмух, для якого ви створили чернетку відповіді, вилучено Чернетку видалено Не вдалося завантажити дані відповіді - Старі чернетки - Функція чернетки в Tusky була повністю перероблена, щоб бути швидшою, зручнішою для користувачів і з меншою кількістю вад. -\n Ви все ще можете отримати доступ до своїх старих чернеток за допомогою кнопки на екрані нових чернеток, але вони будуть вилучені в майбутньому оновленні! Не вдалося надіслати цей дмух! Ви дійсно хочете видалити список %s\? @@ -481,7 +478,7 @@ Найкоротший час планування Mastodon становить 5 хвилин. Оголошень немає. Черга статусів порожня. - У вас немає чернеток. + У вас немає чернеток. Помилка пошуку допису %s Увімкнути перемикання між вкладками жестом проведення пальцем Показати фільтр сповіщень diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 23e9fd170..565891286 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -117,7 +117,7 @@ Nội dung nhạy cảm Công khai Tút đã lên lịch - Nháp + Nháp Từ chối Đồng ý Trở về @@ -315,7 +315,7 @@ Hiện xem trước của link Mastodon giới hạn tối thiểu 5 phút. Bạn không có tút đã lên lịch. - Bạn không có bản nháp nào. + Bạn không có bản nháp nào. Sửa Lựa chọn %d Cho phép chọn nhiều lựa chọn @@ -490,11 +490,8 @@ Đính kèm Âm thanh Tút bạn lên lịch đã bị hủy bỏ - Tút lên lịch cũ Tút lên lịch đã xóa Chưa tải được bình luận - Tính năng lên lịch đăng tút của Tusky được thiết kế lại hoàn toàn để nhanh hơn, thân thiện hơn và ít lỗi hơn. -\nBạn vẫn có thể xem lại bản nháp cũ nhưng chúng sẽ bị xóa bỏ trong bản cập nhật tương lai! Đăng tút không thành công! Emoji động Ngưng nhận thông báo diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index b1bd8df67..5f8f8f308 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -102,7 +102,7 @@ 接受 拒绝 搜索 - 草稿 + 草稿 设置嘟文可见范围 设置内容提醒 插入表情符号 @@ -454,7 +454,7 @@ 选择 %d 编辑 查找嘟文时出错 %s - 您没有草稿。 + 您没有草稿。 您没有任何定时嘟文。 Mastodon的最小预订时间为5分钟。 关注请求 @@ -492,9 +492,6 @@ 该草稿回复的原嘟文已被删除 草稿已删除 加载回复信息失败 - 旧草稿 - Tusky 的草稿功能已被重新设计,现在它更快、更友好,Bug也更少。 -\n 旧草稿依然可以通过新草稿页面的按钮查看,但他们将在未来版本中移除! 嘟文发送失败! 确认删除列表 %s? diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 94bb613b8..059d20319 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -102,7 +102,7 @@ 接受 拒絕 搜尋 - 草稿 + 草稿 設定嘟文可見範圍 設定敏感內容警告 插入表情符號 @@ -451,7 +451,6 @@ 你的草稿欲回覆的原嘟文已被刪除 草稿已刪除 載入回覆資訊失敗 - 舊的草稿 這條嘟文發送失敗! 你確定要刪除列表 %s? @@ -465,7 +464,7 @@ Mastodon 的最短發文間隔限制為 5 分鐘。 沒有公告。 你沒有任何已排程的嘟文。 - 你沒有任何草稿。 + 你沒有任何草稿。 尋找嘟文時發生錯誤 %s 選項 %d 多個選項 diff --git a/app/src/main/res/values-zh-rMO/strings.xml b/app/src/main/res/values-zh-rMO/strings.xml index 7fe854602..b26412b82 100644 --- a/app/src/main/res/values-zh-rMO/strings.xml +++ b/app/src/main/res/values-zh-rMO/strings.xml @@ -102,7 +102,7 @@ 接受 拒絕 搜尋 - 草稿 + 草稿 設定嘟文可見範圍 敏感內容警告 插入表情符號 diff --git a/app/src/main/res/values-zh-rSG/strings.xml b/app/src/main/res/values-zh-rSG/strings.xml index 23c149097..c0d4a621a 100644 --- a/app/src/main/res/values-zh-rSG/strings.xml +++ b/app/src/main/res/values-zh-rSG/strings.xml @@ -102,7 +102,7 @@ 接受 拒绝 搜索 - 草稿 + 草稿 设置嘟文可见范围 设置内容提醒信息 插入表情符号 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index ee1754389..ea38b585a 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -102,7 +102,7 @@ 接受 拒絕 搜尋 - 草稿 + 草稿 設定嘟文可見範圍 敏感內容警告 插入表情符號 @@ -440,8 +440,6 @@ 編輯 書籤 音檔必需小於40MB。 - Tusky 的草稿功能已重新設計,更快、更好用、更少問題。 -\n 你還是可以在草稿頁面中查看你的先前的舊草稿,但它們在未來的某次更新中將會被移除! 隱藏個人頁面中的狀態數量資訊 隱藏貼文上的狀態數量資訊 限制時間軸通知 @@ -463,7 +461,6 @@ 你的草稿欲回覆的原嘟文已被刪除 草稿已刪除 載入回覆資訊失敗 - 舊的草稿 這條嘟文發送失敗! 附件 錄音 @@ -508,7 +505,7 @@ 取消靜音對話 靜音對話 Mastodon 的最短發文間隔限制為 5 分鐘。 - 你沒有任何草稿。 + 你沒有任何草稿。 你沒有任何已排程的嘟文。 列表 選擇列表 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 52f777b5d..6dd78e760 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -127,7 +127,7 @@ Accept Reject Search - Drafts + Drafts Scheduled toots Toot visibility Content warning @@ -583,7 +583,7 @@ Edit Error looking up post %s - You don\'t have any drafts. + You don\'t have any drafts. You don\'t have any scheduled statuses. There are no announcements. Mastodon has a minimum scheduling interval of 5 minutes. @@ -611,13 +611,6 @@ Do you really want to delete the list %s? This toot failed to send! - - - The draft feature in Tusky has been completely redesigned to be faster, more user friendly and less buggy.\n - You can still access your old drafts via a button on the new drafts screen, - but they will be removed in a future update! - - Old Drafts Failed loading Reply information Draft deleted The Toot you drafted a reply to has been removed diff --git a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt index bd9be3b21..e0b0d847a 100644 --- a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt @@ -31,7 +31,6 @@ import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Instance import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.service.ServiceClient -import com.keylesspalace.tusky.util.SaveTootHelper import com.nhaarman.mockitokotlin2.any import io.reactivex.Single import io.reactivex.SingleObserver @@ -116,7 +115,6 @@ class ComposeActivityTest { mock(MediaUploader::class.java), mock(ServiceClient::class.java), mock(DraftHelper::class.java), - mock(SaveTootHelper::class.java), dbMock ) activity.intent = Intent(activity, ComposeActivity::class.java).apply { From 40b24cd242ae7f28ecf4fd087678fa12c5975671 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sun, 16 May 2021 19:53:27 +0200 Subject: [PATCH 37/92] migrate to RxJava3 (#2146) * migrate to RxJava3 * remove unused import --- app/build.gradle | 14 ++++---- .../tusky/AccountsInListFragment.kt | 6 ++-- .../tusky/BottomSheetActivity.kt | 6 ++-- .../com/keylesspalace/tusky/ListsActivity.kt | 6 ++-- .../com/keylesspalace/tusky/MainActivity.kt | 6 ++-- .../tusky/TabPreferenceActivity.kt | 10 +++--- .../keylesspalace/tusky/TuskyApplication.kt | 4 +-- .../keylesspalace/tusky/ViewMediaActivity.kt | 10 +++--- .../tusky/appstore/CacheUpdater.kt | 6 ++-- .../keylesspalace/tusky/appstore/EventsHub.kt | 4 +-- .../announcements/AnnouncementsViewModel.kt | 35 +++++++++---------- .../components/compose/ComposeViewModel.kt | 18 +++++----- .../tusky/components/compose/MediaUploader.kt | 6 ++-- .../conversation/ConversationsRepository.kt | 4 +-- .../conversation/ConversationsViewModel.kt | 2 +- .../tusky/components/drafts/DraftHelper.kt | 12 ++++--- .../tusky/components/drafts/DraftsActivity.kt | 7 ++-- .../components/drafts/DraftsViewModel.kt | 2 +- .../fragment/InstanceListFragment.kt | 6 ++-- .../notifications/NotificationHelper.java | 4 +-- .../components/preference/EmojiPreference.kt | 4 +-- .../components/report/ReportViewModel.kt | 4 +-- .../report/adapter/StatusesDataSource.kt | 4 +-- .../adapter/StatusesDataSourceFactory.kt | 2 +- .../report/adapter/StatusesRepository.kt | 2 +- .../scheduled/ScheduledTootDataSource.kt | 4 +-- .../scheduled/ScheduledTootViewModel.kt | 2 +- .../components/search/SearchViewModel.kt | 5 +-- .../search/adapter/SearchDataSource.kt | 4 +-- .../search/adapter/SearchDataSourceFactory.kt | 2 +- .../search/adapter/SearchRepository.kt | 2 +- .../fragments/SearchStatusesFragment.kt | 6 ++-- .../tusky/db/ConversationsDao.kt | 2 +- .../com/keylesspalace/tusky/db/DraftDao.kt | 4 +-- .../com/keylesspalace/tusky/db/InstanceDao.kt | 2 +- .../com/keylesspalace/tusky/db/TimelineDao.kt | 2 +- .../keylesspalace/tusky/di/NetworkModule.kt | 4 +-- .../tusky/fragment/AccountListFragment.kt | 8 ++--- .../tusky/fragment/AccountMediaFragment.kt | 8 ++--- .../tusky/fragment/NotificationsFragment.java | 32 ++++++++--------- .../tusky/fragment/SFragment.java | 12 +++---- .../tusky/fragment/TimelineFragment.kt | 10 +++--- .../tusky/fragment/ViewImageFragment.kt | 2 +- .../tusky/fragment/ViewThreadFragment.java | 20 +++++------ .../tusky/network/MastodonApi.kt | 4 +-- .../tusky/network/TimelineCases.kt | 6 ++-- .../tusky/repository/TimelineRepository.kt | 4 +-- .../tusky/util/EmojiCompatFont.kt | 6 ++-- .../keylesspalace/tusky/util/LiveDataUtil.kt | 7 ++-- .../tusky/util/RxAwareViewModel.kt | 4 +-- .../tusky/util/ShareShortcutHelper.kt | 4 +-- .../tusky/viewdata/NotificationViewData.java | 4 +-- .../tusky/viewmodel/AccountViewModel.kt | 4 +-- .../viewmodel/AccountsInListViewModel.kt | 4 +-- .../tusky/viewmodel/EditProfileViewModel.kt | 8 ++--- .../tusky/viewmodel/ListsViewModel.kt | 7 ++-- .../tusky/BottomSheetActivityTest.kt | 8 ++--- .../tusky/ComposeActivityTest.kt | 4 +-- .../tusky/fragment/TimelineRepositoryTest.kt | 8 ++--- 59 files changed, 200 insertions(+), 197 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 06b6a3ea2..76621a8c1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -129,14 +129,14 @@ dependencies { implementation "androidx.viewpager2:viewpager2:1.0.0" implementation "androidx.work:work-runtime:2.4.0" implementation "androidx.room:room-runtime:$roomVersion" - implementation "androidx.room:room-rxjava2:$roomVersion" + implementation "androidx.room:room-rxjava3:$roomVersion" kapt "androidx.room:room-compiler:$roomVersion" implementation "com.google.android.material:material:1.3.0" implementation "com.squareup.retrofit2:retrofit:$retrofitVersion" implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion" - implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofitVersion" + implementation "com.squareup.retrofit2:adapter-rxjava3:$retrofitVersion" implementation "com.squareup.okhttp3:okhttp:$okhttpVersion" implementation "com.squareup.okhttp3:logging-interceptor:$okhttpVersion" @@ -146,12 +146,12 @@ dependencies { implementation "com.github.bumptech.glide:glide:$glideVersion" implementation "com.github.bumptech.glide:okhttp3-integration:$glideVersion" - implementation "io.reactivex.rxjava2:rxjava:2.2.20" - implementation "io.reactivex.rxjava2:rxandroid:2.1.1" - implementation "io.reactivex.rxjava2:rxkotlin:2.4.0" + implementation "io.reactivex.rxjava3:rxjava:3.0.12" + implementation "io.reactivex.rxjava3:rxandroid:3.0.0" + implementation "io.reactivex.rxjava3:rxkotlin:3.0.1" - implementation "com.uber.autodispose:autodispose-android-archcomponents:1.4.0" - implementation "com.uber.autodispose:autodispose:1.4.0" + implementation "com.uber.autodispose2:autodispose-androidx-lifecycle:2.0.0" + implementation "com.uber.autodispose2:autodispose:2.0.0" implementation "com.google.dagger:dagger:$daggerVersion" kapt "com.google.dagger:dagger-compiler:$daggerVersion" diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt b/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt index df381aa17..59859b313 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt @@ -28,6 +28,8 @@ import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.ListAdapter +import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from +import autodispose2.autoDispose import com.keylesspalace.tusky.databinding.FragmentAccountsInListBinding import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding import com.keylesspalace.tusky.di.Injectable @@ -37,9 +39,7 @@ import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel import com.keylesspalace.tusky.viewmodel.State -import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from -import com.uber.autodispose.autoDispose -import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import java.io.IOException import javax.inject.Inject diff --git a/app/src/main/java/com/keylesspalace/tusky/BottomSheetActivity.kt b/app/src/main/java/com/keylesspalace/tusky/BottomSheetActivity.kt index add210632..6fd42b8d4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/BottomSheetActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/BottomSheetActivity.kt @@ -22,12 +22,12 @@ import android.widget.LinearLayout import android.widget.Toast import androidx.annotation.VisibleForTesting import androidx.lifecycle.Lifecycle +import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider +import autodispose2.autoDispose import com.google.android.material.bottomsheet.BottomSheetBehavior import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.LinkHelper -import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider -import com.uber.autodispose.autoDispose -import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import java.net.URI import java.net.URISyntaxException import javax.inject.Inject diff --git a/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt index be995e9ee..5812a10ff 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt @@ -30,6 +30,8 @@ import androidx.appcompat.app.AlertDialog import androidx.recyclerview.widget.* import androidx.recyclerview.widget.ListAdapter import at.connyduck.sparkbutton.helpers.Utils +import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from +import autodispose2.autoDispose import com.google.android.material.snackbar.Snackbar import com.keylesspalace.tusky.databinding.ActivityListsBinding import com.keylesspalace.tusky.di.Injectable @@ -44,11 +46,9 @@ import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.sizeDp -import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from -import com.uber.autodispose.autoDispose import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector -import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import javax.inject.Inject /** diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt index b7763bcd5..5333d764b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt @@ -37,6 +37,7 @@ import androidx.emoji.text.EmojiCompat.InitCallback import androidx.lifecycle.Lifecycle import androidx.preference.PreferenceManager import androidx.viewpager2.widget.MarginPageTransformer +import autodispose2.androidx.lifecycle.autoDispose import com.bumptech.glide.Glide import com.bumptech.glide.RequestManager import com.bumptech.glide.load.resource.bitmap.RoundedCorners @@ -81,11 +82,10 @@ import com.mikepenz.materialdrawer.model.* import com.mikepenz.materialdrawer.model.interfaces.* import com.mikepenz.materialdrawer.util.* import com.mikepenz.materialdrawer.widget.AccountHeaderView -import com.uber.autodispose.android.lifecycle.autoDispose import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.schedulers.Schedulers +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.schedulers.Schedulers import javax.inject.Inject class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInjector { diff --git a/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt b/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt index 67cd9cb61..acb5529e8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt @@ -31,6 +31,8 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.transition.TransitionManager import at.connyduck.sparkbutton.helpers.Utils +import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from +import autodispose2.autoDispose import com.google.android.material.transition.MaterialArcMotion import com.google.android.material.transition.MaterialContainerTransform import com.keylesspalace.tusky.adapter.ItemInteractionListener @@ -44,11 +46,9 @@ import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.onTextChanged import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.visible -import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from -import com.uber.autodispose.autoDispose -import io.reactivex.Single -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.schedulers.Schedulers +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.schedulers.Schedulers import java.util.regex.Pattern import javax.inject.Inject diff --git a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt index 562f644e1..629696bfc 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt +++ b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt @@ -22,16 +22,16 @@ import android.util.Log import androidx.emoji.text.EmojiCompat import androidx.preference.PreferenceManager import androidx.work.WorkManager +import autodispose2.AutoDisposePlugins import com.keylesspalace.tusky.components.notifications.NotificationWorkerFactory import com.keylesspalace.tusky.di.AppInjector import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.util.EmojiCompatFont import com.keylesspalace.tusky.util.LocaleManager import com.keylesspalace.tusky.util.ThemeUtils -import com.uber.autodispose.AutoDisposePlugins import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector -import io.reactivex.plugins.RxJavaPlugins +import io.reactivex.rxjava3.plugins.RxJavaPlugins import org.conscrypt.Conscrypt import java.security.Security import javax.inject.Inject diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt index 86205b29f..831d5f3f0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt @@ -41,6 +41,8 @@ import androidx.fragment.app.FragmentActivity import androidx.lifecycle.Lifecycle import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.widget.ViewPager2 +import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider +import autodispose2.autoDispose import com.bumptech.glide.Glide import com.bumptech.glide.request.FutureTarget import com.keylesspalace.tusky.BuildConfig.APPLICATION_ID @@ -52,11 +54,9 @@ import com.keylesspalace.tusky.pager.ImagePagerAdapter import com.keylesspalace.tusky.util.getTemporaryMediaFilename import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.viewdata.AttachmentViewData -import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider -import com.uber.autodispose.autoDispose -import io.reactivex.Single -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.schedulers.Schedulers +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.schedulers.Schedulers import java.io.File import java.io.FileNotFoundException import java.io.FileOutputStream diff --git a/app/src/main/java/com/keylesspalace/tusky/appstore/CacheUpdater.kt b/app/src/main/java/com/keylesspalace/tusky/appstore/CacheUpdater.kt index 6404de5ce..d418321fe 100644 --- a/app/src/main/java/com/keylesspalace/tusky/appstore/CacheUpdater.kt +++ b/app/src/main/java/com/keylesspalace/tusky/appstore/CacheUpdater.kt @@ -3,9 +3,9 @@ package com.keylesspalace.tusky.appstore import com.google.gson.Gson import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AppDatabase -import io.reactivex.Single -import io.reactivex.disposables.Disposable -import io.reactivex.schedulers.Schedulers +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.disposables.Disposable +import io.reactivex.rxjava3.schedulers.Schedulers import javax.inject.Inject class CacheUpdater @Inject constructor( diff --git a/app/src/main/java/com/keylesspalace/tusky/appstore/EventsHub.kt b/app/src/main/java/com/keylesspalace/tusky/appstore/EventsHub.kt index ceaf51331..d2f182d81 100644 --- a/app/src/main/java/com/keylesspalace/tusky/appstore/EventsHub.kt +++ b/app/src/main/java/com/keylesspalace/tusky/appstore/EventsHub.kt @@ -1,7 +1,7 @@ package com.keylesspalace.tusky.appstore -import io.reactivex.Observable -import io.reactivex.subjects.PublishSubject +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.subjects.PublishSubject interface Event interface Dispatchable : Event diff --git a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt index 964cc739a..3a09b5b03 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt @@ -28,7 +28,7 @@ import com.keylesspalace.tusky.entity.Emoji import com.keylesspalace.tusky.entity.Instance import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.* -import io.reactivex.rxkotlin.Singles +import io.reactivex.rxjava3.core.Single import javax.inject.Inject class AnnouncementsViewModel @Inject constructor( @@ -45,25 +45,24 @@ class AnnouncementsViewModel @Inject constructor( val emojis: LiveData> = emojisMutable init { - Singles.zip( - mastodonApi.getCustomEmojis(), + Single.zip(mastodonApi.getCustomEmojis(), appDatabase.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!) .map> { Either.Left(it) } - .onErrorResumeNext( - mastodonApi.getInstance() - .map { Either.Right(it) } - ) - ) { emojis, either -> - either.asLeftOrNull()?.copy(emojiList = emojis) - ?: InstanceEntity( - accountManager.activeAccount?.domain!!, - emojis, - either.asRight().maxTootChars, - either.asRight().pollLimits?.maxOptions, - either.asRight().pollLimits?.maxOptionChars, - either.asRight().version - ) - } + .onErrorResumeNext { + mastodonApi.getInstance() + .map { Either.Right(it) } + }, + { emojis, either -> + either.asLeftOrNull()?.copy(emojiList = emojis) + ?: InstanceEntity( + accountManager.activeAccount?.domain!!, + emojis, + either.asRight().maxTootChars, + either.asRight().pollLimits?.maxOptions, + either.asRight().pollLimits?.maxOptionChars, + either.asRight().version + ) + }) .doOnSuccess { appDatabase.instanceDao().insertOrReplace(it) } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt index 28019b1ae..f9283ed12 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt @@ -33,9 +33,9 @@ import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.service.ServiceClient import com.keylesspalace.tusky.service.TootToSend import com.keylesspalace.tusky.util.* -import io.reactivex.Observable.just -import io.reactivex.disposables.Disposable -import io.reactivex.rxkotlin.Singles +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.disposables.Disposable import java.util.* import javax.inject.Inject @@ -89,7 +89,7 @@ class ComposeViewModel @Inject constructor( init { - Singles.zip(api.getCustomEmojis(), api.getInstance()) { emojis, instance -> + Single.zip(api.getCustomEmojis(), api.getInstance(), { emojis, instance -> InstanceEntity( instance = accountManager.activeAccount?.domain!!, emojiList = emojis, @@ -98,13 +98,13 @@ class ComposeViewModel @Inject constructor( maxPollOptionLength = instance.pollLimits?.maxOptionChars, version = instance.version ) - } + }) .doOnSuccess { db.instanceDao().insertOrReplace(it) } - .onErrorResumeNext( - db.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!) - ) + .onErrorResumeNext { + db.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!) + } .subscribe({ instanceEntity -> emoji.postValue(instanceEntity.emojiList) instance.postValue(instanceEntity) @@ -257,7 +257,7 @@ class ComposeViewModel @Inject constructor( val deletionObservable = if (isEditingScheduledToot) { api.deleteScheduledStatus(scheduledTootId.toString()).toObservable().map { } } else { - just(Unit) + Observable.just(Unit) }.toLiveData() val sendObservable = media diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt index 8ff7dcf32..cf5da80d0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt @@ -29,9 +29,9 @@ import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.ProgressRequestBody import com.keylesspalace.tusky.util.* -import io.reactivex.Observable -import io.reactivex.Single -import io.reactivex.schedulers.Schedulers +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.schedulers.Schedulers import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody import java.io.File diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRepository.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRepository.kt index 3cb4745ad..d4c8c72ed 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRepository.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRepository.kt @@ -11,8 +11,8 @@ import com.keylesspalace.tusky.entity.Conversation import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.Listing import com.keylesspalace.tusky.util.NetworkState -import io.reactivex.Single -import io.reactivex.schedulers.Schedulers +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.schedulers.Schedulers import retrofit2.Call import retrofit2.Callback import retrofit2.Response diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt index c6fa84b40..048024277 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt @@ -11,7 +11,7 @@ import com.keylesspalace.tusky.network.TimelineCases import com.keylesspalace.tusky.util.Listing import com.keylesspalace.tusky.util.NetworkState import com.keylesspalace.tusky.util.RxAwareViewModel -import io.reactivex.schedulers.Schedulers +import io.reactivex.rxjava3.schedulers.Schedulers import javax.inject.Inject class ConversationsViewModel @Inject constructor( diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt index 5038ac00c..e9fe72383 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt @@ -28,10 +28,10 @@ import com.keylesspalace.tusky.db.DraftEntity import com.keylesspalace.tusky.entity.NewPoll import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.util.IOUtils -import io.reactivex.Completable -import io.reactivex.Observable -import io.reactivex.Single -import io.reactivex.schedulers.Schedulers +import io.reactivex.rxjava3.core.Completable +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.schedulers.Schedulers import java.io.File import java.text.SimpleDateFormat import java.util.* @@ -124,7 +124,9 @@ class DraftHelper @Inject constructor( fun deleteDraftAndAttachments(draftId: Int): Completable { return draftDao.find(draftId) .flatMapCompletable { draft -> - deleteDraftAndAttachments(draft) + draft?.let { + deleteDraftAndAttachments(it) + } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt index 5f246905e..e70050160 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt @@ -24,6 +24,8 @@ import android.widget.Toast import androidx.activity.viewModels import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager +import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from +import autodispose2.autoDispose import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.snackbar.Snackbar import com.keylesspalace.tusky.BaseActivity @@ -34,8 +36,7 @@ import com.keylesspalace.tusky.db.DraftEntity import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.show -import com.uber.autodispose.android.lifecycle.autoDispose -import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import retrofit2.HttpException import javax.inject.Inject @@ -91,7 +92,7 @@ class DraftsActivity : BaseActivity(), DraftActionListener { bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED viewModel.getToot(draft.inReplyToId) .observeOn(AndroidSchedulers.mainThread()) - .autoDispose(this) + .autoDispose(from(this)) .subscribe({ status -> val composeOptions = ComposeActivity.ComposeOptions( draftId = draft.id, diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsViewModel.kt index 8beccfb74..aaf878153 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsViewModel.kt @@ -22,7 +22,7 @@ import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.db.DraftEntity import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.network.MastodonApi -import io.reactivex.Single +import io.reactivex.rxjava3.core.Single import javax.inject.Inject class DraftsViewModel @Inject constructor( diff --git a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/fragment/InstanceListFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/fragment/InstanceListFragment.kt index 005432d88..1a1392d42 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/fragment/InstanceListFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/fragment/InstanceListFragment.kt @@ -8,6 +8,8 @@ import androidx.lifecycle.Lifecycle import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from +import autodispose2.autoDispose import com.google.android.material.snackbar.Snackbar import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.instancemute.adapter.DomainMutesAdapter @@ -20,9 +22,7 @@ import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.view.EndlessOnScrollListener -import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from -import com.uber.autodispose.autoDispose -import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import retrofit2.Call import retrofit2.Callback import retrofit2.Response diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java index 84a07490f..d6b9cb99c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java @@ -70,8 +70,8 @@ import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; -import io.reactivex.Single; -import io.reactivex.schedulers.Schedulers; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.schedulers.Schedulers; import static com.keylesspalace.tusky.viewdata.PollViewDataKt.buildDescription; diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt index 2d32723fc..d045350f3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt @@ -25,8 +25,8 @@ import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.SYSTEM_DEFAULT import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.TWEMOJI import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.show -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.disposables.Disposable +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.disposables.Disposable import okhttp3.OkHttpClient import kotlin.system.exitProcess diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt index fdc731632..f2a97be56 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt @@ -28,8 +28,8 @@ import com.keylesspalace.tusky.entity.Relationship import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.* -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.schedulers.Schedulers +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.schedulers.Schedulers import javax.inject.Inject class ReportViewModel @Inject constructor( diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesDataSource.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesDataSource.kt index 10635dda1..9566214c5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesDataSource.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesDataSource.kt @@ -21,8 +21,8 @@ import androidx.paging.ItemKeyedDataSource import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.NetworkState -import io.reactivex.disposables.CompositeDisposable -import io.reactivex.functions.BiFunction +import io.reactivex.rxjava3.disposables.CompositeDisposable +import io.reactivex.rxjava3.functions.BiFunction import java.util.concurrent.Executor class StatusesDataSource(private val accountId: String, diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesDataSourceFactory.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesDataSourceFactory.kt index 4cf8ff1c3..1afdc3c01 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesDataSourceFactory.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesDataSourceFactory.kt @@ -19,7 +19,7 @@ import androidx.lifecycle.MutableLiveData import androidx.paging.DataSource import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.network.MastodonApi -import io.reactivex.disposables.CompositeDisposable +import io.reactivex.rxjava3.disposables.CompositeDisposable import java.util.concurrent.Executor class StatusesDataSourceFactory( diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesRepository.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesRepository.kt index cea3080ea..eb7866ac3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesRepository.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesRepository.kt @@ -21,7 +21,7 @@ import androidx.paging.toLiveData import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.BiListing -import io.reactivex.disposables.CompositeDisposable +import io.reactivex.rxjava3.disposables.CompositeDisposable import java.util.concurrent.Executors import javax.inject.Inject import javax.inject.Singleton diff --git a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootDataSource.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootDataSource.kt index 6c9ba31bd..09d05bcc5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootDataSource.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootDataSource.kt @@ -22,8 +22,8 @@ import androidx.paging.ItemKeyedDataSource import com.keylesspalace.tusky.entity.ScheduledStatus import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.NetworkState -import io.reactivex.disposables.CompositeDisposable -import io.reactivex.rxkotlin.addTo +import io.reactivex.rxjava3.disposables.CompositeDisposable +import io.reactivex.rxjava3.kotlin.addTo class ScheduledTootDataSourceFactory( private val mastodonApi: MastodonApi, diff --git a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootViewModel.kt index 3584168ee..f07ca2b97 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootViewModel.kt @@ -23,7 +23,7 @@ import com.keylesspalace.tusky.appstore.StatusScheduledEvent import com.keylesspalace.tusky.entity.ScheduledStatus import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.RxAwareViewModel -import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import javax.inject.Inject class ScheduledTootViewModel @Inject constructor( diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt index 57c78ef8e..7052e8b74 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt @@ -13,8 +13,9 @@ import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.TimelineCases import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.viewdata.StatusViewData -import io.reactivex.Single -import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.core.Single + import javax.inject.Inject class SearchViewModel @Inject constructor( diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchDataSource.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchDataSource.kt index 2b706288e..a1ed9454a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchDataSource.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchDataSource.kt @@ -21,8 +21,8 @@ import com.keylesspalace.tusky.components.search.SearchType import com.keylesspalace.tusky.entity.SearchResult import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.NetworkState -import io.reactivex.disposables.CompositeDisposable -import io.reactivex.rxkotlin.addTo +import io.reactivex.rxjava3.disposables.CompositeDisposable +import io.reactivex.rxjava3.kotlin.addTo import java.util.concurrent.Executor class SearchDataSource( diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchDataSourceFactory.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchDataSourceFactory.kt index b47da7017..b19976706 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchDataSourceFactory.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchDataSourceFactory.kt @@ -20,7 +20,7 @@ import androidx.paging.DataSource import com.keylesspalace.tusky.components.search.SearchType import com.keylesspalace.tusky.entity.SearchResult import com.keylesspalace.tusky.network.MastodonApi -import io.reactivex.disposables.CompositeDisposable +import io.reactivex.rxjava3.disposables.CompositeDisposable import java.util.concurrent.Executor class SearchDataSourceFactory( diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchRepository.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchRepository.kt index 28d9564dc..4425542e6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchRepository.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchRepository.kt @@ -22,7 +22,7 @@ import com.keylesspalace.tusky.components.search.SearchType import com.keylesspalace.tusky.entity.SearchResult import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.Listing -import io.reactivex.disposables.CompositeDisposable +import io.reactivex.rxjava3.disposables.CompositeDisposable import java.util.concurrent.Executors class SearchRepository(private val mastodonApi: MastodonApi) { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt index f2ea85c0d..a14fc84eb 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt @@ -38,6 +38,8 @@ import androidx.paging.PagedListAdapter import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager +import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from +import autodispose2.autoDispose import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.MainActivity import com.keylesspalace.tusky.R @@ -60,9 +62,7 @@ import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.view.showMuteAccountDialog import com.keylesspalace.tusky.viewdata.AttachmentViewData import com.keylesspalace.tusky.viewdata.StatusViewData -import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from -import com.uber.autodispose.autoDispose -import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers class SearchStatusesFragment : SearchFragment>(), StatusActionListener { diff --git a/app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt index 00f32f533..aae30826f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt @@ -18,7 +18,7 @@ package com.keylesspalace.tusky.db import androidx.paging.DataSource import androidx.room.* import com.keylesspalace.tusky.components.conversation.ConversationEntity -import io.reactivex.Single +import io.reactivex.rxjava3.core.Single @Dao interface ConversationsDao { diff --git a/app/src/main/java/com/keylesspalace/tusky/db/DraftDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/DraftDao.kt index 065af1aed..5e6f21b4c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/DraftDao.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/DraftDao.kt @@ -20,8 +20,8 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query -import io.reactivex.Completable -import io.reactivex.Single +import io.reactivex.rxjava3.core.Completable +import io.reactivex.rxjava3.core.Single @Dao interface DraftDao { diff --git a/app/src/main/java/com/keylesspalace/tusky/db/InstanceDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/InstanceDao.kt index 0c78349ef..52fc3aa86 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/InstanceDao.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/InstanceDao.kt @@ -19,7 +19,7 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query -import io.reactivex.Single +import io.reactivex.rxjava3.core.Single @Dao interface InstanceDao { diff --git a/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt index 9c50ad03b..82e97aed1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt @@ -6,7 +6,7 @@ import androidx.room.OnConflictStrategy.IGNORE import androidx.room.OnConflictStrategy.REPLACE import androidx.room.Query import androidx.room.Transaction -import io.reactivex.Single +import io.reactivex.rxjava3.core.Single @Dao abstract class TimelineDao { diff --git a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt index fb232a64c..89e28553c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt @@ -34,7 +34,7 @@ import okhttp3.OkHttp import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit -import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory +import retrofit2.adapter.rxjava3.RxJava3CallAdapterFactory import retrofit2.converter.gson.GsonConverterFactory import retrofit2.create import java.net.InetSocketAddress @@ -110,7 +110,7 @@ class NetworkModule { return Retrofit.Builder().baseUrl("https://" + MastodonApi.PLACEHOLDER_DOMAIN) .client(httpClient) .addConverterFactory(GsonConverterFactory.create(gson)) - .addCallAdapterFactory(RxJava2CallAdapterFactory.createAsync()) + .addCallAdapterFactory(RxJava3CallAdapterFactory.create()) .build() } diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt index cf7050b80..dc49fbc22 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt @@ -26,6 +26,8 @@ import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.SimpleItemAnimator +import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from +import autodispose2.autoDispose import com.google.android.material.snackbar.Snackbar import com.keylesspalace.tusky.AccountActivity import com.keylesspalace.tusky.AccountListActivity.Type @@ -45,10 +47,8 @@ import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.view.EndlessOnScrollListener -import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from -import com.uber.autodispose.autoDispose -import io.reactivex.Single -import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.core.Single import retrofit2.Response import java.io.IOException import java.util.* diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt index c299e1153..588e22b6a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt @@ -27,6 +27,7 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.Lifecycle import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView +import autodispose2.androidx.lifecycle.autoDispose import com.bumptech.glide.Glide import com.keylesspalace.tusky.R import com.keylesspalace.tusky.ViewMediaActivity @@ -43,10 +44,9 @@ import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.view.SquareImageView import com.keylesspalace.tusky.viewdata.AttachmentViewData -import com.uber.autodispose.android.lifecycle.autoDispose -import io.reactivex.SingleObserver -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.disposables.Disposable +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.core.SingleObserver +import io.reactivex.rxjava3.disposables.Disposable import retrofit2.Response import java.io.IOException import java.util.* diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java index a6a3e311e..c6bef703a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java @@ -99,18 +99,18 @@ import java.util.concurrent.TimeUnit; import javax.inject.Inject; import at.connyduck.sparkbutton.helpers.Utils; -import io.reactivex.Observable; -import io.reactivex.Single; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.CompositeDisposable; -import io.reactivex.disposables.Disposable; +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.disposables.CompositeDisposable; +import io.reactivex.rxjava3.disposables.Disposable; import kotlin.Unit; import kotlin.collections.CollectionsKt; import kotlin.jvm.functions.Function1; +import static autodispose2.AutoDispose.autoDisposable; +import static autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from; import static com.keylesspalace.tusky.util.StringUtils.isLessThan; -import static com.uber.autodispose.AutoDispose.autoDisposable; -import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from; public class NotificationsFragment extends SFragment implements SwipeRefreshLayout.OnRefreshListener, @@ -383,7 +383,7 @@ public class NotificationsFragment extends SFragment implements eventHub.getEvents() .observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .to(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) .subscribe(event -> { if (event instanceof FavoriteEvent) { handleFavEvent((FavoriteEvent) event); @@ -425,7 +425,7 @@ public class NotificationsFragment extends SFragment implements Objects.requireNonNull(status, "Reblog on notification without status"); timelineCases.reblog(status, reblog) .observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this))) + .to(autoDisposable(from(this))) .subscribe( (newStatus) -> setReblogForStatus(position, status, reblog), (t) -> Log.d(getClass().getSimpleName(), @@ -460,7 +460,7 @@ public class NotificationsFragment extends SFragment implements timelineCases.favourite(status, favourite) .observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this))) + .to(autoDisposable(from(this))) .subscribe( (newStatus) -> setFavouriteForStatus(position, status, favourite), (t) -> Log.d(getClass().getSimpleName(), @@ -495,7 +495,7 @@ public class NotificationsFragment extends SFragment implements timelineCases.bookmark(status, bookmark) .observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this))) + .to(autoDisposable(from(this))) .subscribe( (newStatus) -> setBookmarkForStatus(position, status, bookmark), (t) -> Log.d(getClass().getSimpleName(), @@ -529,7 +529,7 @@ public class NotificationsFragment extends SFragment implements timelineCases.voteInPoll(status, choices) .observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this))) + .to(autoDisposable(from(this))) .subscribe( (newPoll) -> setVoteForPoll(position, newPoll), (t) -> Log.d(TAG, @@ -687,7 +687,7 @@ public class NotificationsFragment extends SFragment implements //Execute clear notifications request mastodonApi.clearNotifications() .observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .to(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) .subscribe( response -> { // nothing to do @@ -832,7 +832,7 @@ public class NotificationsFragment extends SFragment implements mastodonApi.authorizeFollowRequest(id) : mastodonApi.rejectFollowRequest(id); request.observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .to(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) .subscribe( (relationship) -> fullyRefreshWithProgressBar(true), (error) -> Log.e(TAG, String.format("Failed to %s account id %s", accept ? "accept" : "reject", id)) @@ -952,7 +952,7 @@ public class NotificationsFragment extends SFragment implements Disposable notificationCall = mastodonApi.notifications(fromId, uptoId, LOAD_AT_ONCE, showNotificationsFilter ? notificationFilter : null) .observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .to(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) .subscribe( response -> { if (response.isSuccessful()) { @@ -1284,7 +1284,7 @@ public class NotificationsFragment extends SFragment implements if (!useAbsoluteTime) { Observable.interval(1, TimeUnit.MINUTES) .observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this, Lifecycle.Event.ON_PAUSE))) + .to(autoDisposable(from(this, Lifecycle.Event.ON_PAUSE))) .subscribe( interval -> updateAdapter() ); diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java index ef1074a39..3ae843c51 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java @@ -73,14 +73,14 @@ import java.util.regex.Pattern; import javax.inject.Inject; -import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import kotlin.Unit; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; -import static com.uber.autodispose.AutoDispose.autoDisposable; -import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from; +import static autodispose2.AutoDispose.autoDisposable; +import static autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from; /* Note from Andrew on Jan. 22, 2017: This class is a design problem for me, so I left it with an * awkward name. TimelineFragment and NotificationFragment have significant overlap but the nature @@ -323,7 +323,7 @@ public abstract class SFragment extends Fragment implements Injectable { timelineCases.muteConversation(status, status.getMuted() == null || !status.getMuted()) .onErrorReturnItem(status) .observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .to(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) .subscribe(); return true; } @@ -416,7 +416,7 @@ public abstract class SFragment extends Fragment implements Injectable { .setPositiveButton(android.R.string.ok, (dialogInterface, i) -> { timelineCases.delete(id) .observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .to(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) .subscribe( deletedStatus -> { }, @@ -439,7 +439,7 @@ public abstract class SFragment extends Fragment implements Injectable { .setPositiveButton(android.R.string.ok, (dialogInterface, i) -> { timelineCases.delete(id) .observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .to(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) .subscribe(deletedStatus -> { removeItem(position); diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.kt index 1af707fc2..bd0378daa 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.kt @@ -35,6 +35,8 @@ import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.SimpleItemAnimator import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener import at.connyduck.sparkbutton.helpers.Utils +import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from +import autodispose2.autoDispose import com.keylesspalace.tusky.AccountListActivity import com.keylesspalace.tusky.AccountListActivity.Companion.newIntent import com.keylesspalace.tusky.BaseActivity @@ -85,11 +87,9 @@ import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.view.EndlessOnScrollListener import com.keylesspalace.tusky.viewdata.StatusViewData -import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from -import com.uber.autodispose.autoDispose -import io.reactivex.Observable -import io.reactivex.Single -import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.core.Single import retrofit2.Response import java.io.IOException import java.util.concurrent.TimeUnit diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt index c68cfb5f5..ceb2f365d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt @@ -37,7 +37,7 @@ import com.keylesspalace.tusky.databinding.FragmentViewImageBinding import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.visible -import io.reactivex.subjects.BehaviorSubject +import io.reactivex.rxjava3.subjects.BehaviorSubject import kotlin.math.abs class ViewImageFragment : ViewMediaFragment() { diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java index 0c415e1fb..4f00439a3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java @@ -73,10 +73,10 @@ import java.util.Locale; import javax.inject.Inject; -import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; -import static com.uber.autodispose.AutoDispose.autoDisposable; -import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from; +import static autodispose2.AutoDispose.autoDisposable; +import static autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from; public final class ViewThreadFragment extends SFragment implements SwipeRefreshLayout.OnRefreshListener, StatusActionListener, Injectable { @@ -182,7 +182,7 @@ public final class ViewThreadFragment extends SFragment implements eventHub.getEvents() .observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .to(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) .subscribe(event -> { if (event instanceof FavoriteEvent) { handleFavEvent((FavoriteEvent) event); @@ -241,7 +241,7 @@ public final class ViewThreadFragment extends SFragment implements timelineCases.reblog(statuses.get(position), reblog) .observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this))) + .to(autoDisposable(from(this))) .subscribe( (newStatus) -> updateStatus(position, newStatus), (t) -> Log.d(TAG, @@ -255,7 +255,7 @@ public final class ViewThreadFragment extends SFragment implements timelineCases.favourite(statuses.get(position), favourite) .observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this))) + .to(autoDisposable(from(this))) .subscribe( (newStatus) -> updateStatus(position, newStatus), (t) -> Log.d(TAG, @@ -269,7 +269,7 @@ public final class ViewThreadFragment extends SFragment implements timelineCases.bookmark(statuses.get(position), bookmark) .observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this))) + .to(autoDisposable(from(this))) .subscribe( (newStatus) -> updateStatus(position, newStatus), (t) -> Log.d(TAG, @@ -416,7 +416,7 @@ public final class ViewThreadFragment extends SFragment implements timelineCases.voteInPoll(status, choices) .observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this))) + .to(autoDisposable(from(this))) .subscribe( (newPoll) -> setVoteForPoll(position, newPoll), (t) -> Log.d(TAG, @@ -462,7 +462,7 @@ public final class ViewThreadFragment extends SFragment implements private void sendStatusRequest(final String id) { mastodonApi.status(id) .observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .to(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) .subscribe( status -> { int position = setStatus(status); @@ -475,7 +475,7 @@ public final class ViewThreadFragment extends SFragment implements private void sendThreadRequest(final String id) { mastodonApi.statusContext(id) .observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .to(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) .subscribe( context -> { swipeRefreshLayout.setRefreshing(false); diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt index 28ac77b6a..ae6fc3c2d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -16,8 +16,8 @@ package com.keylesspalace.tusky.network import com.keylesspalace.tusky.entity.* -import io.reactivex.Completable -import io.reactivex.Single +import io.reactivex.rxjava3.core.Completable +import io.reactivex.rxjava3.core.Single import okhttp3.MultipartBody import okhttp3.RequestBody import okhttp3.ResponseBody diff --git a/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt b/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt index 6e79f0755..f124faed1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt @@ -20,9 +20,9 @@ import com.keylesspalace.tusky.appstore.* import com.keylesspalace.tusky.entity.DeletedStatus import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.entity.Status -import io.reactivex.Single -import io.reactivex.disposables.CompositeDisposable -import io.reactivex.rxkotlin.addTo +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.disposables.CompositeDisposable +import io.reactivex.rxjava3.kotlin.addTo import java.lang.IllegalStateException /** diff --git a/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt b/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt index b3e12aeb0..d6f25a161 100644 --- a/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt +++ b/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt @@ -14,8 +14,8 @@ import com.keylesspalace.tusky.util.Either import com.keylesspalace.tusky.util.dec import com.keylesspalace.tusky.util.inc import com.keylesspalace.tusky.util.trimTrailingWhitespace -import io.reactivex.Single -import io.reactivex.schedulers.Schedulers +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.schedulers.Schedulers import java.io.IOException import java.util.* import java.util.concurrent.TimeUnit diff --git a/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.kt b/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.kt index d0a0e443e..4bce7b8f4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.kt @@ -8,9 +8,9 @@ import androidx.annotation.StringRes import androidx.annotation.VisibleForTesting import com.keylesspalace.tusky.R import de.c1710.filemojicompat.FileEmojiCompatConfig -import io.reactivex.Observable -import io.reactivex.ObservableEmitter -import io.reactivex.schedulers.Schedulers +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.core.ObservableEmitter +import io.reactivex.rxjava3.schedulers.Schedulers import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response diff --git a/app/src/main/java/com/keylesspalace/tusky/util/LiveDataUtil.kt b/app/src/main/java/com/keylesspalace/tusky/util/LiveDataUtil.kt index b0048aefb..822a8da6b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/LiveDataUtil.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/LiveDataUtil.kt @@ -16,9 +16,10 @@ package com.keylesspalace.tusky.util import androidx.lifecycle.* -import io.reactivex.BackpressureStrategy -import io.reactivex.Observable -import io.reactivex.Single +import io.reactivex.rxjava3.core.BackpressureStrategy +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.core.Single + inline fun LiveData.map(crossinline mapFunction: (X) -> Y): LiveData = Transformations.map(this) { input -> mapFunction(input) } diff --git a/app/src/main/java/com/keylesspalace/tusky/util/RxAwareViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/util/RxAwareViewModel.kt index c78b0f787..62af3ba56 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/RxAwareViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/RxAwareViewModel.kt @@ -2,8 +2,8 @@ package com.keylesspalace.tusky.util import androidx.annotation.CallSuper import androidx.lifecycle.ViewModel -import io.reactivex.disposables.CompositeDisposable -import io.reactivex.disposables.Disposable +import io.reactivex.rxjava3.disposables.CompositeDisposable +import io.reactivex.rxjava3.disposables.Disposable open class RxAwareViewModel : ViewModel() { val disposables = CompositeDisposable() diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ShareShortcutHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/ShareShortcutHelper.kt index 1acf22605..11b7e2ccd 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ShareShortcutHelper.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/ShareShortcutHelper.kt @@ -31,8 +31,8 @@ import com.keylesspalace.tusky.MainActivity import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.notifications.NotificationHelper import com.keylesspalace.tusky.db.AccountEntity -import io.reactivex.Single -import io.reactivex.schedulers.Schedulers +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.schedulers.Schedulers fun updateShortcut(context: Context, account: AccountEntity) { diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java b/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java index 6438a25de..3256c1595 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java @@ -15,13 +15,13 @@ package com.keylesspalace.tusky.viewdata; +import androidx.annotation.Nullable; + import com.keylesspalace.tusky.entity.Account; import com.keylesspalace.tusky.entity.Notification; import java.util.Objects; -import io.reactivex.annotations.Nullable; - /** * Created by charlag on 12/07/2017. *

diff --git a/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountViewModel.kt index 1837652e6..129959f92 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountViewModel.kt @@ -10,8 +10,8 @@ import com.keylesspalace.tusky.entity.IdentityProof import com.keylesspalace.tusky.entity.Relationship import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.* -import io.reactivex.Single -import io.reactivex.disposables.Disposable +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.disposables.Disposable import retrofit2.Call import retrofit2.Callback import retrofit2.Response diff --git a/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountsInListViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountsInListViewModel.kt index 1dc412283..e65decae6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountsInListViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountsInListViewModel.kt @@ -24,8 +24,8 @@ import com.keylesspalace.tusky.util.Either.Left import com.keylesspalace.tusky.util.Either.Right import com.keylesspalace.tusky.util.RxAwareViewModel import com.keylesspalace.tusky.util.withoutFirstWhich -import io.reactivex.Observable -import io.reactivex.subjects.BehaviorSubject +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.subjects.BehaviorSubject import javax.inject.Inject data class State(val accounts: Either>, val searchResult: List?) diff --git a/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt index 24a73396e..f75dbad5d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt @@ -31,10 +31,10 @@ import com.keylesspalace.tusky.entity.Instance import com.keylesspalace.tusky.entity.StringField import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.* -import io.reactivex.Single -import io.reactivex.disposables.CompositeDisposable -import io.reactivex.rxkotlin.addTo -import io.reactivex.schedulers.Schedulers +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.disposables.CompositeDisposable +import io.reactivex.rxjava3.kotlin.addTo +import io.reactivex.rxjava3.schedulers.Schedulers import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.toRequestBody diff --git a/app/src/main/java/com/keylesspalace/tusky/viewmodel/ListsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/viewmodel/ListsViewModel.kt index 22f509b59..650636e61 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewmodel/ListsViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewmodel/ListsViewModel.kt @@ -21,14 +21,13 @@ import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.RxAwareViewModel import com.keylesspalace.tusky.util.replacedFirstWhich import com.keylesspalace.tusky.util.withoutFirstWhich -import io.reactivex.Observable -import io.reactivex.subjects.BehaviorSubject -import io.reactivex.subjects.PublishSubject +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.subjects.BehaviorSubject +import io.reactivex.rxjava3.subjects.PublishSubject import java.io.IOException import java.net.ConnectException import javax.inject.Inject - internal class ListsViewModel @Inject constructor(private val api: MastodonApi) : RxAwareViewModel() { enum class LoadingState { INITIAL, LOADING, LOADED, ERROR_NETWORK, ERROR_OTHER diff --git a/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt index 86d0925f3..d7eac796a 100644 --- a/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt @@ -22,10 +22,10 @@ import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.SearchResult import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.network.MastodonApi -import io.reactivex.Single -import io.reactivex.android.plugins.RxAndroidPlugins -import io.reactivex.plugins.RxJavaPlugins -import io.reactivex.schedulers.TestScheduler +import io.reactivex.rxjava3.android.plugins.RxAndroidPlugins +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.plugins.RxJavaPlugins +import io.reactivex.rxjava3.schedulers.TestScheduler import org.junit.Assert import org.junit.Before import org.junit.Test diff --git a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt index e0b0d847a..6bbdc1897 100644 --- a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt @@ -32,8 +32,8 @@ import com.keylesspalace.tusky.entity.Instance import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.service.ServiceClient import com.nhaarman.mockitokotlin2.any -import io.reactivex.Single -import io.reactivex.SingleObserver +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.core.SingleObserver import org.junit.Assert.* import org.junit.Before import org.junit.Test diff --git a/app/src/test/java/com/keylesspalace/tusky/fragment/TimelineRepositoryTest.kt b/app/src/test/java/com/keylesspalace/tusky/fragment/TimelineRepositoryTest.kt index 7a7c3f7d2..27ff5f62c 100644 --- a/app/src/test/java/com/keylesspalace/tusky/fragment/TimelineRepositoryTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/fragment/TimelineRepositoryTest.kt @@ -16,10 +16,10 @@ import com.nhaarman.mockitokotlin2.isNull import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions import com.nhaarman.mockitokotlin2.whenever -import io.reactivex.Single -import io.reactivex.plugins.RxJavaPlugins -import io.reactivex.schedulers.Schedulers -import io.reactivex.schedulers.TestScheduler +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.plugins.RxJavaPlugins +import io.reactivex.rxjava3.schedulers.Schedulers +import io.reactivex.rxjava3.schedulers.TestScheduler import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test From 3af8874b877edc42881342b1730ea09a7dbf0acd Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Wed, 19 May 2021 07:40:45 +0200 Subject: [PATCH 38/92] upgrade android gradle plugin to 4.2.1 (#2160) * upgrade android gradle plugin to 4.2.1 * upgrade android gradle plugin to 4.2.1 --- app/build.gradle | 10 +--------- build.gradle | 2 +- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 76621a8c1..2ee231ff6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -34,7 +34,6 @@ android { kapt { arguments { arg("room.schemaLocation", "$projectDir/schemas") - arg("room.incremental", "true") } } } @@ -60,10 +59,6 @@ android { lintOptions { disable 'MissingTranslation' } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } buildFeatures { viewBinding true } @@ -88,11 +83,8 @@ android { enableSplit = false } } -} - -project.tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { kotlinOptions { - jvmTarget = "1.8" + jvmTarget = '1.8' } } diff --git a/build.gradle b/build.gradle index d6ef9eec0..94911834e 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.2' + classpath 'com.android.tools.build:gradle:4.2.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } From 9c9f8ebf2cab258a8571ec284669d6010cd28a0d Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Wed, 19 May 2021 07:40:56 +0200 Subject: [PATCH 39/92] upgrade gradle to 7.0.2 (#2161) --- gradle.properties | 2 -- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/gradle.properties b/gradle.properties index bada7909d..8144ece09 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,8 +14,6 @@ org.gradle.jvmargs=-Xmx4096m # use parallel execution org.gradle.parallel=true -# enable file system watching -org.gradle.vfs.watch=true android.enableR8.fullMode=true android.useAndroidX=true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1c4bcc29e..29e413457 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 751109ac3997fd0deaa5c03483c1dab38a085b67 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Fri, 21 May 2021 17:51:35 +0200 Subject: [PATCH 40/92] upgrade kotlin to 1.5.0 (#2162) * upgrade kotlin to 1.5.0 * don't explicitly set kotlin jvmtarget --- app/build.gradle | 3 --- .../main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt | 2 +- .../tusky/components/compose/ComposeViewModel.kt | 4 ++-- .../main/java/com/keylesspalace/tusky/db/AccountManager.kt | 2 +- .../java/com/keylesspalace/tusky/util/CustomEmojiHelper.kt | 2 -- build.gradle | 2 +- 6 files changed, 5 insertions(+), 10 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 2ee231ff6..5f280a348 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -83,9 +83,6 @@ android { enableSplit = false } } - kotlinOptions { - jvmTarget = '1.8' - } } ext.lifecycleVersion = "2.2.0" diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt index 2640caaca..c5656115f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt @@ -30,7 +30,7 @@ class EmojiAdapter( ) : RecyclerView.Adapter>() { private val emojiList : List = emojiList.filter { emoji -> emoji.visibleInPicker == null || emoji.visibleInPicker } - .sortedBy { it.shortcode.toLowerCase(Locale.ROOT) } + .sortedBy { it.shortcode.lowercase(Locale.ROOT) } override fun getItemCount() = emojiList.size diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt index f9283ed12..2e38fbe84 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt @@ -351,11 +351,11 @@ class ComposeViewModel @Inject constructor( ':' -> { val emojiList = emoji.value ?: return emptyList() - val incomplete = token.substring(1).toLowerCase(Locale.ROOT) + val incomplete = token.substring(1).lowercase(Locale.ROOT) val results = ArrayList() val resultsInside = ArrayList() for (emoji in emojiList) { - val shortcode = emoji.shortcode.toLowerCase(Locale.ROOT) + val shortcode = emoji.shortcode.lowercase(Locale.ROOT) if (shortcode.startsWith(incomplete)) { results.add(ComposeAutoCompleteAdapter.EmojiResult(emoji)) } else if (shortcode.indexOf(incomplete, 1) != -1) { diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt b/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt index fc10adb63..52650f77a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt @@ -65,7 +65,7 @@ class AccountManager @Inject constructor(db: AppDatabase) { val maxAccountId = accounts.maxByOrNull { it.id }?.id ?: 0 val newAccountId = maxAccountId + 1 - activeAccount = AccountEntity(id = newAccountId, domain = domain.toLowerCase(Locale.ROOT), accessToken = accessToken, isActive = true) + activeAccount = AccountEntity(id = newAccountId, domain = domain.lowercase(Locale.ROOT), accessToken = accessToken, isActive = true) } diff --git a/app/src/main/java/com/keylesspalace/tusky/util/CustomEmojiHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/CustomEmojiHelper.kt index 7521afe40..f63c44b84 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/CustomEmojiHelper.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/CustomEmojiHelper.kt @@ -31,8 +31,6 @@ import com.keylesspalace.tusky.entity.Emoji import java.lang.ref.WeakReference import java.util.regex.Pattern -import androidx.preference.PreferenceManager -import com.keylesspalace.tusky.settings.PrefKeys /** * replaces emoji shortcodes in a text with EmojiSpans diff --git a/build.gradle b/build.gradle index 94911834e..c05263c73 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.4.31' + ext.kotlin_version = '1.5.0' repositories { google() jcenter() From 387e62ea4b8de7ed079bfc33f472d5aab96601dd Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Fri, 21 May 2021 17:51:47 +0200 Subject: [PATCH 41/92] get rid of jcenter (#2163) * get rid of jcenter * fix BottomSheetActivityTest * update Android Image Cropper license --- app/build.gradle | 5 +++-- app/src/main/AndroidManifest.xml | 2 +- .../com/keylesspalace/tusky/EditProfileActivity.kt | 12 ++++++++---- app/src/main/res/layout/activity_license.xml | 2 +- .../keylesspalace/tusky/BottomSheetActivityTest.kt | 6 ++++++ build.gradle | 4 ++-- 6 files changed, 21 insertions(+), 10 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 5f280a348..c06dfde8c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -156,9 +156,9 @@ dependencies { implementation "com.mikepenz:materialdrawer-iconics:$materialdrawerVersion" implementation 'com.mikepenz:google-material-typeface:3.0.1.4.original-kotlin@aar' - implementation "com.theartofdev.edmodo:android-image-cropper:2.8.0" + implementation "com.github.CanHub:Android-Image-Cropper:3.1.0" - implementation "de.c1710:filemojicompat:1.0.17" + implementation "de.c1710:filemojicompat:1.0.18" testImplementation "androidx.test.ext:junit:1.1.2" testImplementation "org.robolectric:robolectric:4.4" @@ -168,4 +168,5 @@ dependencies { androidTestImplementation "androidx.test.espresso:espresso-core:3.3.0" androidTestImplementation "androidx.room:room-testing:$roomVersion" androidTestImplementation "androidx.test.ext:junit:1.1.2" + testImplementation "androidx.arch.core:core-testing:2.1.0" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 770d45af6..a4b29cf22 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -123,7 +123,7 @@ { val result = CropImage.getActivityResult(data) when (resultCode) { - Activity.RESULT_OK -> beginResize(result.uri) + Activity.RESULT_OK -> beginResize(result?.uriContent) CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE -> onResizeFailure() else -> endMediaPicking() } @@ -382,7 +382,12 @@ class EditProfileActivity : BaseActivity(), Injectable { } } - private fun beginResize(uri: Uri) { + private fun beginResize(uri: Uri?) { + if(uri == null) { + currentlyPicking = PickType.NOTHING + return + } + beginMediaPicking() when (currentlyPicking) { @@ -398,7 +403,6 @@ class EditProfileActivity : BaseActivity(), Injectable { } currentlyPicking = PickType.NOTHING - } private fun onResizeFailure() { diff --git a/app/src/main/res/layout/activity_license.xml b/app/src/main/res/layout/activity_license.xml index a8401f139..76a9dc5da 100644 --- a/app/src/main/res/layout/activity_license.xml +++ b/app/src/main/res/layout/activity_license.xml @@ -171,7 +171,7 @@ android:layout_marginStart="12dp" android:layout_marginTop="12dp" license:license="@string/license_apache_2" - license:link="https://github.com/ArthurHub/Android-Image-Cropper" + license:link="https://github.com/CanHub/Android-Image-Cropper" license:name="Android Image Cropper" /> Date: Fri, 21 May 2021 17:52:03 +0200 Subject: [PATCH 42/92] don't upscale images in caption dialog (#2165) * don't upscale images in caption dialog * don't upscale images in caption dialog --- .../tusky/components/compose/dialog/CaptionDialog.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt index e356cd3fb..adc72cd31 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt @@ -31,6 +31,7 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData import at.connyduck.sparkbutton.helpers.Utils import com.bumptech.glide.Glide +import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy import com.bumptech.glide.request.target.CustomTarget import com.bumptech.glide.request.transition.Transition import com.github.chrisbanes.photoview.PhotoView @@ -97,10 +98,10 @@ fun T.makeCaptionDialog(existingDescription: String?, dialog.show() - // Load the image and manually set it into the ImageView because it doesn't have a fixed - // size. Maybe we should limit the size of CustomTarget + // Load the image and manually set it into the ImageView because it doesn't have a fixed size. Glide.with(this) .load(previewUri) + .downsample(DownsampleStrategy.CENTER_INSIDE) .into(object : CustomTarget(4096, 4096) { override fun onLoadCleared(placeholder: Drawable?) { imageView.setImageDrawable(placeholder) From 09da9105f64c5d8f617724b7b5be17b17bb17b0d Mon Sep 17 00:00:00 2001 From: nailyk-weblate Date: Mon, 17 May 2021 09:57:38 +0000 Subject: [PATCH 43/92] Update translation files Updated by "Cleanup translation files" hook in Weblate. Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/ --- app/src/main/res/values-bn-rBD/strings.xml | 3 --- app/src/main/res/values-nl/strings.xml | 1 - app/src/main/res/values-ru/strings.xml | 3 --- 3 files changed, 7 deletions(-) diff --git a/app/src/main/res/values-bn-rBD/strings.xml b/app/src/main/res/values-bn-rBD/strings.xml index aec035d4b..a8be9e394 100644 --- a/app/src/main/res/values-bn-rBD/strings.xml +++ b/app/src/main/res/values-bn-rBD/strings.xml @@ -447,8 +447,6 @@ সদস্যতা আছে এমন একজন টুট দিয়েছে কোনো ঘোষণা নেই। যদিও তোমার অ্যাকাউন্ট রুদ্ধকৃত না, %1$s রা ভেবেছে এই অ্যাকাউন্টগুলোর অনুসরণ অনুরোধ তোমার পরীক্ষা করা উচিত। - নতুন খসড়া বৈশিষ্ট দ্রুততর হওয়ার জন্য নতুনভাবে নকশা করা হয়েছে, যা সহজে ব্যবহারযোগ্য ও কম সমস্যাপূর্ণ। -\n আগের খসড়াগুলো খসড়া পাতার বোতাম দিয়ে যেতে পারো, কিন্তু ভবিষ্যত হালনাগাদে তা সরিয়ে ফেলা হবে! যে টুটের উত্তর খসড়া করেছিলে তা মুছে ফেলা হয়েছে এই তালিকাটা আসলেই মুছতে চাও\? @@ -471,7 +469,6 @@ এই অ্যাকাউন্ট নিয়ে তোমার ব্যক্তিগত লেখা শীর্ষস্থানীয় সরঞ্জামের শিরোনামটি লুকাও খসড়া মুছো হয়েছে - পুরোনো খসড়া বিজ্ঞপ্তি সুস্থতা সময়হীন diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 8c5da5abe..af3a53f8a 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -513,7 +513,6 @@ Concept verwijderd Kwantitatieve statistieken voor toots verbergen Laden van reactie-informatie mislukt - Oude concepten Kwantitatieve statistieken in profielen verbergen Hoofd navigatiepositie \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index ef52a679b..6615185c7 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -544,9 +544,6 @@ Подписаться Пост, на который вы написали ответ, был удалён Не удалось загрузить информацию об ответе - Функция черновика в Tusky была полностью переработана, чтобы сделать её более быстрой, удобной и стабильной. -\nВы по-прежнему можете получить доступ к своим старым черновикам с помощью кнопки на экране новых черновиков, но они будут удалены в будущем обновлении! - Старые черновики Черновик удалён Этот пост не удалось отправить! Вы действительно хотите удалить список %s\? From 862165ccbc6cdd98e45e83eff4875edeefa9a3e3 Mon Sep 17 00:00:00 2001 From: Ho Nhat Duy Date: Mon, 17 May 2021 09:57:38 +0000 Subject: [PATCH 44/92] Translated using Weblate (Vietnamese) Currently translated at 100.0% (458 of 458 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/vi/ --- app/src/main/res/values-vi/strings.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 565891286..b18635412 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -296,10 +296,10 @@ Luôn hiện nội dung nhạy cảm Đang theo dõi bạn %ds - %d phút trước - %d giờ trước - %d ngày trước - %d năm trước + %d phút + %d giờ + %d ngày + %d năm %ds %d phút %d giờ From 2632c4c5bd74dd1845044646c6fe218a88bf9de6 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sat, 22 May 2021 13:15:40 +0000 Subject: [PATCH 45/92] update Glide to 4.12.0 --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index c06dfde8c..1a1d6efb9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -89,7 +89,7 @@ ext.lifecycleVersion = "2.2.0" ext.roomVersion = '2.3.0' ext.retrofitVersion = '2.9.0' ext.okhttpVersion = '4.9.0' -ext.glideVersion = '4.11.0' +ext.glideVersion = '4.12.0' ext.daggerVersion = '2.30.1' ext.materialdrawerVersion = '8.2.0' From f385aae3eeb3e505162965662059918dbdedc60b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BF=BB=E8=A8=B3=E8=80=85X?= Date: Sat, 22 May 2021 13:15:40 +0000 Subject: [PATCH 46/92] Translated using Weblate (Japanese) Currently translated at 92.1% (422 of 458 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/ja/ Translated using Weblate (Japanese) Currently translated at 91.7% (420 of 458 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/ja/ --- app/src/main/res/values-ja/strings.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index d602f37b8..ec7e2a6a7 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -464,4 +464,7 @@ Mastodonにおける予約までの最小間隔は5分です。 %sさんがトゥートしました お知らせ + 本当に %s のすべてをブロックするのですか? そのドメインからのコンテンツは、公開タイムラインや通知に表示されなくなります。また、そのドメインのフォロワーは削除されます。 + 音声 + ドメイン全体を非表示 \ No newline at end of file From 5b2bb1c60ed7d6fc40f40fa411cef6a611916200 Mon Sep 17 00:00:00 2001 From: edave64 Date: Sat, 22 May 2021 13:15:41 +0000 Subject: [PATCH 47/92] Translated using Weblate (German) Currently translated at 100.0% (458 of 458 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/de/ Translated using Weblate (German) Currently translated at 99.1% (454 of 458 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/de/ --- app/src/main/res/values-de/strings.xml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 9af9b790a..4c6ac1b3e 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -294,7 +294,7 @@ Die Blob–Emojis aus Android 4.4–7.1 Die Standard-Emojis von Mastodon Die aktuellen Emojis von Google - Download fehlgeschlagen. + Download fehlgeschlagen Bot %1$s ist umgezogen auf: An ursprüngliches Publikum teilen @@ -357,7 +357,7 @@ Beitrag erstellen Bot-Hinweis anzeigen Bist du dir sicher, dass du alle deine Benachrichtigungen dauerhaft löschen möchtest\? - " %1$s • %2$s" + %1$s • %2$s %s Stimme %s Stimmen @@ -496,7 +496,7 @@ GIF-Emojis animieren Jemand, den ich abonniert habe, etwas Neues veröffentlicht %s hat gerade etwas gepostet - %dm + %d Min. Benachrichtigungen überprüfen Informationen, die dein Wohlbefinden beeinflussen könnten, werden versteckt. Das beinhaltet \n @@ -511,4 +511,10 @@ Timeline-Benachrichtigungen einschränken Abonnieren nicht mehr abonnieren + in %d M. + in %d St. + Antwortinformationen konnten nicht geladen werden + %d T. + in %d J. + in %d Sek. \ No newline at end of file From a1dfbee669bbb42cab4b61c2864e11a4fa18f0ad Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sat, 22 May 2021 17:48:00 +0200 Subject: [PATCH 48/92] update dagger to 2.35.1 (#2167) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index c06dfde8c..0703141eb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -90,7 +90,7 @@ ext.roomVersion = '2.3.0' ext.retrofitVersion = '2.9.0' ext.okhttpVersion = '4.9.0' ext.glideVersion = '4.11.0' -ext.daggerVersion = '2.30.1' +ext.daggerVersion = '2.35.1' ext.materialdrawerVersion = '8.2.0' // if libraries are changed here, they should also be changed in LicenseActivity From d2cdaae1291e935851bddadd1dac1598cfc7f3fc Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sat, 22 May 2021 17:48:17 +0200 Subject: [PATCH 49/92] update okhttp to 4.9.1 (#2168) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 0703141eb..54419025a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -88,7 +88,7 @@ android { ext.lifecycleVersion = "2.2.0" ext.roomVersion = '2.3.0' ext.retrofitVersion = '2.9.0' -ext.okhttpVersion = '4.9.0' +ext.okhttpVersion = '4.9.1' ext.glideVersion = '4.11.0' ext.daggerVersion = '2.35.1' ext.materialdrawerVersion = '8.2.0' From ca5c4558819314f45adb0fee20b6edbbe79c0a0d Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sat, 22 May 2021 17:50:08 +0200 Subject: [PATCH 50/92] update AndroidX, use ActivityResultContracts (#2170) * update AndroidX, use ActivityResultContracts * make allowMultiple setable in PickMediaFiles * add license headers to PickMediaFiles --- app/build.gradle | 12 +- .../com/keylesspalace/tusky/BaseActivity.java | 1 + .../components/compose/ComposeActivity.kt | 243 ++++++++---------- .../tusky/util/PickMediaFiles.kt | 52 ++++ 4 files changed, 165 insertions(+), 143 deletions(-) create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/PickMediaFiles.kt diff --git a/app/build.gradle b/app/build.gradle index 54419025a..2ddae5ee9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -85,7 +85,7 @@ android { } } -ext.lifecycleVersion = "2.2.0" +ext.lifecycleVersion = "2.3.1" ext.roomVersion = '2.3.0' ext.retrofitVersion = '2.9.0' ext.okhttpVersion = '4.9.1' @@ -97,16 +97,16 @@ ext.materialdrawerVersion = '8.2.0' dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - implementation "androidx.core:core-ktx:1.3.2" - implementation "androidx.appcompat:appcompat:1.2.0" - implementation "androidx.fragment:fragment-ktx:1.2.5" + implementation "androidx.core:core-ktx:1.5.0" + implementation "androidx.appcompat:appcompat:1.3.0" + implementation "androidx.fragment:fragment-ktx:1.3.3" implementation "androidx.browser:browser:1.3.0" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation "androidx.recyclerview:recyclerview:1.2.0" implementation "androidx.exifinterface:exifinterface:1.3.2" implementation "androidx.cardview:cardview:1.0.0" implementation "androidx.preference:preference-ktx:1.1.1" - implementation "androidx.sharetarget:sharetarget:1.0.0" + implementation "androidx.sharetarget:sharetarget:1.1.0" implementation "androidx.emoji:emoji:1.1.0" implementation "androidx.emoji:emoji-appcompat:1.1.0" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion" @@ -116,7 +116,7 @@ dependencies { implementation "androidx.constraintlayout:constraintlayout:2.0.4" implementation "androidx.paging:paging-runtime-ktx:2.1.2" implementation "androidx.viewpager2:viewpager2:1.0.0" - implementation "androidx.work:work-runtime:2.4.0" + implementation "androidx.work:work-runtime:2.5.0" implementation "androidx.room:room-runtime:$roomVersion" implementation "androidx.room:room-rxjava3:$roomVersion" kapt "androidx.room:room-compiler:$roomVersion" diff --git a/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java b/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java index 92994f165..e348f0364 100644 --- a/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java @@ -199,6 +199,7 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requesters.containsKey(requestCode)) { PermissionRequester requester = requesters.remove(requestCode); requester.onRequestPermissionsResult(permissions, grantResults); diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt index f71f26536..9458b26a3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt @@ -16,7 +16,6 @@ package com.keylesspalace.tusky.components.compose import android.Manifest -import android.app.Activity import android.app.ProgressDialog import android.content.Context import android.content.Intent @@ -28,13 +27,13 @@ import android.net.Uri import android.os.Build import android.os.Bundle import android.os.Parcelable -import android.provider.MediaStore import android.util.Log import android.view.KeyEvent import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.widget.* +import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.annotation.ColorInt import androidx.annotation.StringRes @@ -85,12 +84,12 @@ import kotlin.math.max import kotlin.math.min class ComposeActivity : BaseActivity(), - ComposeOptionsListener, - ComposeAutoCompleteAdapter.AutocompletionProvider, - OnEmojiSelectedListener, - Injectable, - InputConnectionCompat.OnCommitContentListener, - ComposeScheduleView.OnTimeSetListener { + ComposeOptionsListener, + ComposeAutoCompleteAdapter.AutocompletionProvider, + OnEmojiSelectedListener, + Injectable, + InputConnectionCompat.OnCommitContentListener, + ComposeScheduleView.OnTimeSetListener { @Inject lateinit var viewModelFactory: ViewModelFactory @@ -114,6 +113,21 @@ class ComposeActivity : BaseActivity(), private val maxUploadMediaNumber = 4 private var mediaCount = 0 + private val takePicture = registerForActivityResult(ActivityResultContracts.TakePicture()) { success -> + if (success) { + pickMedia(photoUploadUri!!) + } + } + private val pickMediaFile = registerForActivityResult(PickMediaFiles()) { uris -> + if (mediaCount + uris.size > maxUploadMediaNumber) { + Toast.makeText(this, resources.getQuantityString(R.plurals.error_upload_max_media_reached, maxUploadMediaNumber, maxUploadMediaNumber), Toast.LENGTH_SHORT).show() + } else { + uris.forEach { uri -> + pickMedia(uri) + } + } + } + public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -130,16 +144,16 @@ class ComposeActivity : BaseActivity(), setupAvatar(preferences, activeAccount) val mediaAdapter = MediaPreviewAdapter( - this, - onAddCaption = { item -> - makeCaptionDialog(item.description, item.uri) { newDescription -> - viewModel.updateDescription(item.localId, newDescription) - } - }, - onRemove = this::removeMediaFromQueue + this, + onAddCaption = { item -> + makeCaptionDialog(item.description, item.uri) { newDescription -> + viewModel.updateDescription(item.localId, newDescription) + } + }, + onRemove = this::removeMediaFromQueue ) binding.composeMediaPreviewBar.layoutManager = - LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) + LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) binding.composeMediaPreviewBar.adapter = mediaAdapter binding.composeMediaPreviewBar.itemAnimator = null @@ -255,11 +269,11 @@ class ComposeActivity : BaseActivity(), binding.composeEditField.setOnKeyListener { _, keyCode, event -> this.onKeyDown(keyCode, event) } binding.composeEditField.setAdapter( - ComposeAutoCompleteAdapter( - this, - preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false), - preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) - ) + ComposeAutoCompleteAdapter( + this, + preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false), + preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) + ) ) binding.composeEditField.setTokenizer(ComposeTokenizer()) @@ -275,7 +289,7 @@ class ComposeActivity : BaseActivity(), // work around Android platform bug -> https://issuetracker.google.com/issues/67102093 if (Build.VERSION.SDK_INT == Build.VERSION_CODES.O - || Build.VERSION.SDK_INT == Build.VERSION_CODES.O_MR1) { + || Build.VERSION.SDK_INT == Build.VERSION_CODES.O_MR1) { binding.composeEditField.setLayerType(View.LAYER_TYPE_SOFTWARE, null) } } @@ -390,13 +404,13 @@ class ComposeActivity : BaseActivity(), val animateAvatars = preferences.getBoolean("animateGifAvatars", false) loadAvatar( - activeAccount.profilePictureUrl, - binding.composeAvatar, - avatarSize / 8, - animateAvatars + activeAccount.profilePictureUrl, + binding.composeAvatar, + avatarSize / 8, + animateAvatars ) binding.composeAvatar.contentDescription = getString(R.string.compose_active_account_description, - activeAccount.fullName) + activeAccount.fullName) } private fun replaceTextAtCaret(text: CharSequence) { @@ -602,10 +616,10 @@ class ComposeActivity : BaseActivity(), addMediaBehavior.removeBottomSheetCallback(this) if (ContextCompat.checkSelfPermission(this@ComposeActivity, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this@ComposeActivity, - arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), - PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE) + arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), + PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE) } else { - initiateMediaPicking() + pickMediaFile.launch(true) } } } @@ -620,7 +634,7 @@ class ComposeActivity : BaseActivity(), addMediaBehavior.state = BottomSheetBehavior.STATE_COLLAPSED val instanceParams = viewModel.instanceParams.value!! showAddPollDialog(this, viewModel.poll.value, instanceParams.pollMaxOptions, - instanceParams.pollMaxLength, viewModel::updatePoll) + instanceParams.pollMaxLength, viewModel::updatePoll) } private fun setupPollView() { @@ -740,8 +754,8 @@ class ComposeActivity : BaseActivity(), } else if (characterCount <= maximumTootCharacters) { if (viewModel.media.value!!.isNotEmpty()) { finishingUploadDialog = ProgressDialog.show( - this, getString(R.string.dialog_title_finishing_media_upload), - getString(R.string.dialog_message_uploading_media), true, true) + this, getString(R.string.dialog_title_finishing_media_upload), + getString(R.string.dialog_message_uploading_media), true, true) } viewModel.sendStatus(contentText, spoilerText).observe(this, { @@ -755,20 +769,20 @@ class ComposeActivity : BaseActivity(), } } - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, - grantResults: IntArray) { + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if (requestCode == PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE) { if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - initiateMediaPicking() + pickMediaFile.launch(true) } else { - val bar = Snackbar.make(binding.activityCompose, R.string.error_media_upload_permission, - Snackbar.LENGTH_SHORT).apply { - + Snackbar.make(binding.activityCompose, R.string.error_media_upload_permission, + Snackbar.LENGTH_SHORT).apply { + setAction(R.string.action_retry) { onMediaPick() } + //necessary so snackbar is shown over everything + view.elevation = resources.getDimension(R.dimen.compose_activity_snackbar_elevation) + show() } - bar.setAction(R.string.action_retry) { onMediaPick() } - //necessary so snackbar is shown over everything - bar.view.elevation = resources.getDimension(R.dimen.compose_activity_snackbar_elevation) - bar.show() } } } @@ -776,50 +790,32 @@ class ComposeActivity : BaseActivity(), private fun initiateCameraApp() { addMediaBehavior.state = BottomSheetBehavior.STATE_COLLAPSED - // We don't need to ask for permission in this case, because the used calls require - // android.permission.WRITE_EXTERNAL_STORAGE only on SDKs *older* than Kitkat, which was - // way before permission dialogues have been introduced. - val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) - if (intent.resolveActivity(packageManager) != null) { - val photoFile: File = try { - createNewImageFile(this) - } catch (ex: IOException) { - displayTransientError(R.string.error_media_upload_opening) - return - } - - // Continue only if the File was successfully created - photoUploadUri = FileProvider.getUriForFile(this, - BuildConfig.APPLICATION_ID + ".fileprovider", - photoFile) - intent.putExtra(MediaStore.EXTRA_OUTPUT, photoUploadUri) - startActivityForResult(intent, MEDIA_TAKE_PHOTO_RESULT) + val photoFile: File = try { + createNewImageFile(this) + } catch (ex: IOException) { + displayTransientError(R.string.error_media_upload_opening) + return } - } - private fun initiateMediaPicking() { - val intent = Intent(Intent.ACTION_GET_CONTENT) - intent.addCategory(Intent.CATEGORY_OPENABLE) - - val mimeTypes = arrayOf("image/*", "video/*", "audio/*") - intent.type = "*/*" - intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes) - intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) - startActivityForResult(intent, MEDIA_PICK_RESULT) + // Continue only if the File was successfully created + photoUploadUri = FileProvider.getUriForFile(this, + BuildConfig.APPLICATION_ID + ".fileprovider", + photoFile) + takePicture.launch(photoUploadUri) } private fun enableButton(button: ImageButton, clickable: Boolean, colorActive: Boolean) { button.isEnabled = clickable ThemeUtils.setDrawableTint(this, button.drawable, - if (colorActive) android.R.attr.textColorTertiary - else R.attr.textColorDisabled) + if (colorActive) android.R.attr.textColorTertiary + else R.attr.textColorDisabled) } private fun enablePollButton(enable: Boolean) { binding.addPollTextActionTextView.isEnabled = enable val textColor = ThemeUtils.getColor(this, - if (enable) android.R.attr.textColorTertiary - else R.attr.textColorDisabled) + if (enable) android.R.attr.textColorTertiary + else R.attr.textColorDisabled) binding.addPollTextActionTextView.setTextColor(textColor) binding.addPollTextActionTextView.compoundDrawablesRelative[0].colorFilter = PorterDuffColorFilter(textColor, PorterDuff.Mode.SRC_IN) } @@ -828,31 +824,6 @@ class ComposeActivity : BaseActivity(), viewModel.removeMediaFromQueue(item) } - override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) { - super.onActivityResult(requestCode, resultCode, intent) - if (resultCode == Activity.RESULT_OK && requestCode == MEDIA_PICK_RESULT && intent != null) { - if (intent.data != null) { - // Single media, upload it and done. - pickMedia(intent.data!!) - } else if (intent.clipData != null) { - val clipData = intent.clipData!! - val count = clipData.itemCount - if (mediaCount + count > maxUploadMediaNumber) { - // check if exist media + upcoming media > 4, then prob error message. - Toast.makeText(this, resources.getQuantityString(R.plurals.error_upload_max_media_reached, maxUploadMediaNumber, maxUploadMediaNumber), Toast.LENGTH_SHORT).show() - } else { - // if not grater then 4, upload all multiple media. - for (i in 0 until count) { - val imageUri = clipData.getItemAt(i).getUri() - pickMedia(imageUri) - } - } - } - } else if (resultCode == Activity.RESULT_OK && requestCode == MEDIA_TAKE_PHOTO_RESULT) { - pickMedia(photoUploadUri!!) - } - } - private fun pickMedia(uri: Uri, contentInfoCompat: InputContentInfoCompat? = null) { withLifecycleContext { viewModel.pickMedia(uri).observe { exceptionOrItem -> @@ -908,9 +879,9 @@ class ComposeActivity : BaseActivity(), override fun onBackPressed() { // Acting like a teen: deliberately ignoring parent. if (composeOptionsBehavior.state == BottomSheetBehavior.STATE_EXPANDED || - addMediaBehavior.state == BottomSheetBehavior.STATE_EXPANDED || - emojiBehavior.state == BottomSheetBehavior.STATE_EXPANDED || - scheduleBehavior.state == BottomSheetBehavior.STATE_EXPANDED) { + addMediaBehavior.state == BottomSheetBehavior.STATE_EXPANDED || + emojiBehavior.state == BottomSheetBehavior.STATE_EXPANDED || + scheduleBehavior.state == BottomSheetBehavior.STATE_EXPANDED) { composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN @@ -945,12 +916,12 @@ class ComposeActivity : BaseActivity(), val contentWarning = binding.composeContentWarningField.text.toString() if (viewModel.didChange(contentText, contentWarning)) { AlertDialog.Builder(this) - .setMessage(R.string.compose_save_draft) - .setPositiveButton(R.string.action_save) { _, _ -> - saveDraftAndFinish(contentText, contentWarning) - } - .setNegativeButton(R.string.action_delete) { _, _ -> deleteDraftAndFinish() } - .show() + .setMessage(R.string.compose_save_draft) + .setPositiveButton(R.string.action_save) { _, _ -> + saveDraftAndFinish(contentText, contentWarning) + } + .setNegativeButton(R.string.action_delete) { _, _ -> deleteDraftAndFinish() } + .show() } else { finishWithoutSlideOutAnimation() } @@ -982,13 +953,13 @@ class ComposeActivity : BaseActivity(), } data class QueuedMedia( - val localId: Long, - val uri: Uri, - val type: Type, - val mediaSize: Long, - val uploadPercent: Int = 0, - val id: String? = null, - val description: String? = null + val localId: Long, + val uri: Uri, + val type: Type, + val mediaSize: Long, + val uploadPercent: Int = 0, + val id: String? = null, + val description: String? = null ) { enum class Type { IMAGE, VIDEO, AUDIO; @@ -1011,31 +982,29 @@ class ComposeActivity : BaseActivity(), @Parcelize data class ComposeOptions( - // Let's keep fields var until all consumers are Kotlin - var scheduledTootId: String? = null, - var draftId: Int? = null, - var tootText: String? = null, - var mediaUrls: List? = null, - var mediaDescriptions: List? = null, - var mentionedUsernames: Set? = null, - var inReplyToId: String? = null, - var replyVisibility: Status.Visibility? = null, - var visibility: Status.Visibility? = null, - var contentWarning: String? = null, - var replyingStatusAuthor: String? = null, - var replyingStatusContent: String? = null, - var mediaAttachments: List? = null, - var draftAttachments: List? = null, - var scheduledAt: String? = null, - var sensitive: Boolean? = null, - var poll: NewPoll? = null, - var modifiedInitialState: Boolean? = null + // Let's keep fields var until all consumers are Kotlin + var scheduledTootId: String? = null, + var draftId: Int? = null, + var tootText: String? = null, + var mediaUrls: List? = null, + var mediaDescriptions: List? = null, + var mentionedUsernames: Set? = null, + var inReplyToId: String? = null, + var replyVisibility: Status.Visibility? = null, + var visibility: Status.Visibility? = null, + var contentWarning: String? = null, + var replyingStatusAuthor: String? = null, + var replyingStatusContent: String? = null, + var mediaAttachments: List? = null, + var draftAttachments: List? = null, + var scheduledAt: String? = null, + var sensitive: Boolean? = null, + var poll: NewPoll? = null, + var modifiedInitialState: Boolean? = null ) : Parcelable companion object { private const val TAG = "ComposeActivity" // logging tag - private const val MEDIA_PICK_RESULT = 1 - private const val MEDIA_TAKE_PHOTO_RESULT = 2 private const val PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1 internal const val COMPOSE_OPTIONS_EXTRA = "COMPOSE_OPTIONS" diff --git a/app/src/main/java/com/keylesspalace/tusky/util/PickMediaFiles.kt b/app/src/main/java/com/keylesspalace/tusky/util/PickMediaFiles.kt new file mode 100644 index 000000000..ae09d9e4f --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/PickMediaFiles.kt @@ -0,0 +1,52 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.util + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.net.Uri +import androidx.activity.result.contract.ActivityResultContract + +class PickMediaFiles : ActivityResultContract>() { + override fun createIntent(context: Context, allowMultiple: Boolean): Intent { + return Intent(Intent.ACTION_GET_CONTENT) + .addCategory(Intent.CATEGORY_OPENABLE) + .setType("*/*") + .apply { + putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*", "video/*", "audio/*")) + putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple) + } + } + + override fun parseResult(resultCode: Int, intent: Intent?): List { + if (resultCode == Activity.RESULT_OK) { + val intentData = intent?.data + val clipData = intent?.clipData + if (intentData != null) { + // Single media, upload it and done. + return listOf(intentData) + } else if (clipData != null) { + val result: MutableList = mutableListOf() + for (i in 0 until clipData.itemCount) { + result.add(clipData.getItemAt(i).uri) + } + return result + } + } + return emptyList() + } +} \ No newline at end of file From 8b56e9bc2724d3e17543638e1d251e37d8d7064b Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sat, 22 May 2021 18:41:28 +0200 Subject: [PATCH 51/92] update Glide to 4.12.0 (#2169) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 2ddae5ee9..12ee38948 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -89,7 +89,7 @@ ext.lifecycleVersion = "2.3.1" ext.roomVersion = '2.3.0' ext.retrofitVersion = '2.9.0' ext.okhttpVersion = '4.9.1' -ext.glideVersion = '4.11.0' +ext.glideVersion = '4.12.0' ext.daggerVersion = '2.35.1' ext.materialdrawerVersion = '8.2.0' From 1f8be85c52ddce739b6f102bf5670a1e968d37e9 Mon Sep 17 00:00:00 2001 From: edave64 Date: Sat, 22 May 2021 13:15:41 +0000 Subject: [PATCH 52/92] Translated using Weblate (German) Currently translated at 100.0% (13 of 13 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/de/ --- fastlane/metadata/android/de/changelogs/58.txt | 11 +++++++++++ fastlane/metadata/android/de/changelogs/72.txt | 11 +++++++++++ fastlane/metadata/android/de/changelogs/77.txt | 10 ++++++++++ fastlane/metadata/android/de/changelogs/80.txt | 7 +++++++ 4 files changed, 39 insertions(+) create mode 100644 fastlane/metadata/android/de/changelogs/58.txt create mode 100644 fastlane/metadata/android/de/changelogs/72.txt create mode 100644 fastlane/metadata/android/de/changelogs/77.txt create mode 100644 fastlane/metadata/android/de/changelogs/80.txt diff --git a/fastlane/metadata/android/de/changelogs/58.txt b/fastlane/metadata/android/de/changelogs/58.txt new file mode 100644 index 000000000..e16973f94 --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/58.txt @@ -0,0 +1,11 @@ +Tusky v6.0 + +- Timelinefilter wurden in die Kontoeinstellungen verschoben und werden mit dem Server synchronisiert +- Hashtags können jetzt als eigene Tabs hinzugefügt werden +- Listen können jetzt bearbeitet werden +- Sicherheit: TLS 1.0 und TLS 1.1 entfernt, Unterstützung für TLS 1.3 auf Android 6+ hinzugefügt +- Automatische Vorschläge von Emojis beim Tippen +- "Systemthema verwenden" hinzugefügt +- Verbesserte Barrierefreiheit +- Eine Sprache kann jetzt in der App gesetzt werden +- Fehlerkorrekturen diff --git a/fastlane/metadata/android/de/changelogs/72.txt b/fastlane/metadata/android/de/changelogs/72.txt new file mode 100644 index 000000000..587058631 --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/72.txt @@ -0,0 +1,11 @@ +Tusky v11.0 + +- Benachrichtigungen über neue Folgeanfragen wenn das Konto gesperrt ist +- Neue Funktionen die in den Einstellungen aktiviert werden können: + - Wischgeste zum Wechseln zwischen Tabs + - Bestätigung vor dem Teilen eines Beitrags + - Linkvorschauen in Timelines +- Konversationen können jetzt stummgeschaltet werden +- Umfrageergebnisse für Umfragen mit Mehrfachauswahl sind jetzt einfacher zu verstehen +- Viele Fehlerkorrekturen, primär beim Postverfassen +- Verbesserte Übersetzungen diff --git a/fastlane/metadata/android/de/changelogs/77.txt b/fastlane/metadata/android/de/changelogs/77.txt new file mode 100644 index 000000000..f88fc4d64 --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/77.txt @@ -0,0 +1,10 @@ +Tusky v13.0 + +- Unterstützung für Profilnotizen (Mastodon 3.2.0 Funktion) +- Unterstützung für Ankündigungen von Administratoren (Mastodon 3.1.0 Funktion) + +- Der Avatar des ausgewählten Kontos wird nun in der Hauptnavigation angezeigt +- Klicken auf einen Anzeigenamen in einer Timeline öffnet jetzt das Profil dieses Nutzers + +- Viele Fehlerkorrekturen und kleine Verbesserungen +- Verbesserte Übersetzungen diff --git a/fastlane/metadata/android/de/changelogs/80.txt b/fastlane/metadata/android/de/changelogs/80.txt new file mode 100644 index 000000000..e8115536c --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/80.txt @@ -0,0 +1,7 @@ +Tusky v14.0 + +- Notifikationen wenn ein Nutzer dem du folgst postet - Klicke auf das Glockenicon in deren Profil! (Funktion von Mastodon 3.3.0) +- Die Entwurfsfunktion in Tusky wurde vollständig neu gestaltet um schneller, nutzerfreundlicher und weniger fehleranfällig zu sein. +- Ein neue Wohlbefinden-Modus der dir erlaubt bestimmte Funktionen von Tusky zu beschränken wurde hinzugefügt. +- Tusky kann jetzt animierte GIF-Emojis darstellen. +Alle Änderungen: https://github.com/tuskyapp/Tusky/releases From d5e539fd6479ace3acdc13530a94bd4daf497a80 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sat, 22 May 2021 19:24:40 +0200 Subject: [PATCH 53/92] cleanup MainActivity / last remnants of SavedToots (#2174) --- app/src/main/AndroidManifest.xml | 3 --- .../java/com/keylesspalace/tusky/MainActivity.kt | 13 +++---------- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a4b29cf22..80b76b845 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -34,9 +34,6 @@ android:resource="@xml/share_shortcuts" /> - diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt index 5333d764b..aea0211a2 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt @@ -44,7 +44,6 @@ import com.bumptech.glide.load.resource.bitmap.RoundedCorners import com.bumptech.glide.request.target.CustomTarget import com.bumptech.glide.request.target.FixedSizeDrawable import com.bumptech.glide.request.transition.Transition -import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout.OnTabSelectedListener import com.google.android.material.tabs.TabLayoutMediator @@ -61,7 +60,6 @@ import com.keylesspalace.tusky.components.scheduled.ScheduledTootActivity import com.keylesspalace.tusky.components.search.SearchActivity import com.keylesspalace.tusky.databinding.ActivityMainBinding import com.keylesspalace.tusky.db.AccountEntity -import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.fragment.SFragment import com.keylesspalace.tusky.interfaces.AccountSelectionListener @@ -101,9 +99,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje @Inject lateinit var conversationRepository: ConversationsRepository - @Inject - lateinit var appDb: AppDatabase - @Inject lateinit var draftHelper: DraftHelper @@ -134,10 +129,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje super.onCreate(savedInstanceState) val activeAccount = accountManager.activeAccount - if (activeAccount == null) { - // will be redirected to LoginActivity by BaseActivity - return - } + ?: return // will be redirected to LoginActivity by BaseActivity + var showNotificationTab = false if (intent != null) { /** there are two possibilities the accountId can be passed to MainActivity: @@ -753,7 +746,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje header.setActiveProfile(accountManager.activeAccount!!.id) } - override fun getActionButton(): FloatingActionButton? = binding.composeButton + override fun getActionButton() = binding.composeButton override fun androidInjector() = androidInjector From 59c62204c7ddd086ff46411d83ce8a1cd8e72998 Mon Sep 17 00:00:00 2001 From: Ivan Kupalov Date: Thu, 27 May 2021 16:41:54 +0200 Subject: [PATCH 54/92] Fix crash in NotificationsAdapter when spoiler is null. (#2178) --- .../tusky/adapter/NotificationsAdapter.java | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java index b2f12b81a..0a872c6f1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java @@ -643,12 +643,17 @@ public class NotificationsAdapter extends RecyclerView.Adapter { ); LinkHelper.setClickableText(statusContent, emojifiedText, statusViewData.getMentions(), listener); - CharSequence emojifiedContentWarning = CustomEmojiHelper.emojify( - statusViewData.getSpoilerText(), - statusViewData.getStatusEmojis(), - contentWarningDescriptionTextView, - statusDisplayOptions.animateEmojis() - ); + CharSequence emojifiedContentWarning; + if (statusViewData.getSpoilerText() != null) { + emojifiedContentWarning = CustomEmojiHelper.emojify( + statusViewData.getSpoilerText(), + statusViewData.getStatusEmojis(), + contentWarningDescriptionTextView, + statusDisplayOptions.animateEmojis() + ); + } else { + emojifiedContentWarning = ""; + } contentWarningDescriptionTextView.setText(emojifiedContentWarning); } From 30ed9a4d1cfc98d52f66275450fbc04f71e4db41 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Fri, 21 May 2021 17:52:03 +0200 Subject: [PATCH 55/92] don't upscale images in caption dialog (#2165) * don't upscale images in caption dialog * don't upscale images in caption dialog --- .../tusky/components/compose/dialog/CaptionDialog.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt index e356cd3fb..adc72cd31 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt @@ -31,6 +31,7 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData import at.connyduck.sparkbutton.helpers.Utils import com.bumptech.glide.Glide +import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy import com.bumptech.glide.request.target.CustomTarget import com.bumptech.glide.request.transition.Transition import com.github.chrisbanes.photoview.PhotoView @@ -97,10 +98,10 @@ fun T.makeCaptionDialog(existingDescription: String?, dialog.show() - // Load the image and manually set it into the ImageView because it doesn't have a fixed - // size. Maybe we should limit the size of CustomTarget + // Load the image and manually set it into the ImageView because it doesn't have a fixed size. Glide.with(this) .load(previewUri) + .downsample(DownsampleStrategy.CENTER_INSIDE) .into(object : CustomTarget(4096, 4096) { override fun onLoadCleared(placeholder: Drawable?) { imageView.setImageDrawable(placeholder) From a85568abdf83def8465b619b5d6e5be007430dd5 Mon Sep 17 00:00:00 2001 From: Conny Duck Date: Mon, 31 May 2021 14:25:19 +0200 Subject: [PATCH 56/92] Release 83 --- app/build.gradle | 4 ++-- fastlane/metadata/android/en-US/changelogs/83.txt | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/83.txt diff --git a/app/build.gradle b/app/build.gradle index 06b6a3ea2..d61e1082b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -20,8 +20,8 @@ android { applicationId APP_ID minSdkVersion 21 targetSdkVersion 29 - versionCode 82 - versionName "15.0" + versionCode 83 + versionName "15.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true diff --git a/fastlane/metadata/android/en-US/changelogs/83.txt b/fastlane/metadata/android/en-US/changelogs/83.txt new file mode 100644 index 000000000..5eb77c2b8 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/83.txt @@ -0,0 +1,3 @@ +Tusky v15.1 + +This release fixes a crash when captioning images From e032d38d56fe7618f49bced6cdbcc02673ffa6e1 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Mon, 31 May 2021 15:16:07 +0200 Subject: [PATCH 57/92] fix LiveData nullability issues (#2181) --- .../components/announcements/AnnouncementsViewModel.kt | 2 +- .../components/conversation/ConversationsRepository.kt | 2 +- .../tusky/components/report/ReportViewModel.kt | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt index 3a09b5b03..2b4d61a40 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt @@ -67,7 +67,7 @@ class AnnouncementsViewModel @Inject constructor( appDatabase.instanceDao().insertOrReplace(it) } .subscribe({ - emojisMutable.postValue(it.emojiList) + emojisMutable.postValue(it.emojiList.orEmpty()) }, { Log.w(TAG, "Failed to get custom emojis.", it) }) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRepository.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRepository.kt index d4c8c72ed..e3703cbfe 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRepository.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRepository.kt @@ -71,7 +71,7 @@ class ConversationsRepository @Inject constructor(val mastodonApi: MastodonApi, // we are using a mutable live data to trigger refresh requests which eventually calls // refresh method and gets a new live data. Each refresh request by the user becomes a newly // dispatched data in refreshTrigger - val refreshTrigger = MutableLiveData() + val refreshTrigger = MutableLiveData() val refreshState = Transformations.switchMap(refreshTrigger) { refresh(accountId, true) } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt index f2a97be56..e5691473d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt @@ -37,8 +37,8 @@ class ReportViewModel @Inject constructor( private val eventHub: EventHub, private val statusesRepository: StatusesRepository) : RxAwareViewModel() { - private val navigationMutable = MutableLiveData() - val navigation: LiveData = navigationMutable + private val navigationMutable = MutableLiveData() + val navigation: LiveData = navigationMutable private val muteStateMutable = MutableLiveData>() val muteState: LiveData> = muteStateMutable @@ -49,8 +49,8 @@ class ReportViewModel @Inject constructor( private val reportingStateMutable = MutableLiveData>() var reportingState: LiveData> = reportingStateMutable - private val checkUrlMutable = MutableLiveData() - val checkUrl: LiveData = checkUrlMutable + private val checkUrlMutable = MutableLiveData() + val checkUrl: LiveData = checkUrlMutable private val repoResult = MutableLiveData>() val statuses: LiveData> = Transformations.switchMap(repoResult) { it.pagedList } From 3301643c1d1a83d6b4676e98ada81fb22efd6273 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Tue, 1 Jun 2021 19:46:07 +0200 Subject: [PATCH 58/92] update to SDK 30 and fix deprecations (#2173) * update to SDk 30 and fix deprecations * remove unnecessary .run * revert ViewMediaActivity change --- app/build.gradle | 4 ++-- .../keylesspalace/tusky/AccountActivity.kt | 19 +++++++++---------- .../com/keylesspalace/tusky/MainActivity.kt | 4 ++-- .../keylesspalace/tusky/ViewMediaActivity.kt | 3 ++- .../compose/dialog/AddPollOptionsAdapter.kt | 6 +++--- .../compose/dialog/CaptionDialog.kt | 4 ---- .../com/keylesspalace/tusky/di/AppInjector.kt | 12 ++++++------ .../tusky/fragment/NotificationsFragment.java | 4 ++-- .../receiver/SendStatusBroadcastReceiver.kt | 6 +++--- 9 files changed, 29 insertions(+), 33 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 94e725c83..91874f1c9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -15,11 +15,11 @@ def getGitSha = { } android { - compileSdkVersion 29 + compileSdkVersion 30 defaultConfig { applicationId APP_ID minSdkVersion 21 - targetSdkVersion 29 + targetSdkVersion 30 versionCode 83 versionName "15.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt index 57f384751..a408bc43f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt @@ -34,13 +34,16 @@ import androidx.annotation.Px import androidx.appcompat.app.AlertDialog import androidx.core.app.ActivityOptionsCompat import androidx.core.content.ContextCompat +import androidx.core.view.ViewCompat +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsCompat.Type.systemBars import androidx.emoji.text.EmojiCompat import androidx.preference.PreferenceManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.viewpager2.widget.MarginPageTransformer import com.bumptech.glide.Glide import com.google.android.material.appbar.AppBarLayout -import com.google.android.material.appbar.CollapsingToolbarLayout import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.shape.MaterialShapeDrawable import com.google.android.material.shape.ShapeAppearanceModel @@ -170,7 +173,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI binding.accountFieldList.layoutManager = LinearLayoutManager(this) binding.accountFieldList.adapter = accountFieldAdapter - val accountListClickListener = { v: View -> val type = when (v.id) { R.id.accountFollowers -> AccountListActivity.Type.FOLLOWERS @@ -237,13 +239,11 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI private fun setupToolbar() { // set toolbar top margin according to system window insets - binding.accountCoordinatorLayout.setOnApplyWindowInsetsListener { _, insets -> - val top = insets.systemWindowInsetTop - - val toolbarParams = binding.accountToolbar.layoutParams as CollapsingToolbarLayout.LayoutParams + ViewCompat.setOnApplyWindowInsetsListener(binding.accountCoordinatorLayout) { _, insets -> + val top = insets.getInsets(systemBars()).top + val toolbarParams = binding.accountToolbar.layoutParams as ViewGroup.MarginLayoutParams toolbarParams.topMargin = top - - insets.consumeSystemWindowInsets() + WindowInsetsCompat.CONSUMED } // Setup the toolbar. @@ -318,8 +318,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI } private fun makeNotificationBarTransparent() { - val decorView = window.decorView - decorView.systemUiVisibility = decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + WindowCompat.setDecorFitsSystemWindows(window, false) window.statusBarColor = statusBarColorTransparent } diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt index aea0211a2..972c15f3c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt @@ -614,7 +614,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje AlertDialog.Builder(this) .setTitle(R.string.action_logout) .setMessage(getString(R.string.action_logout_confirm, activeAccount.fullName)) - .setPositiveButton(android.R.string.yes) { _: DialogInterface?, _: Int -> + .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> NotificationHelper.deleteNotificationChannelsForAccount(activeAccount, this) cacheUpdater.clearForUser(activeAccount.id) conversationRepository.deleteCacheForAccount(activeAccount.id) @@ -632,7 +632,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje startActivity(intent) finishWithoutSlideOutAnimation() } - .setNegativeButton(android.R.string.no, null) + .setNegativeButton(android.R.string.cancel, null) .show() } } diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt index 831d5f3f0..b266444e3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt @@ -49,8 +49,8 @@ import com.keylesspalace.tusky.BuildConfig.APPLICATION_ID import com.keylesspalace.tusky.databinding.ActivityViewMediaBinding import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.fragment.ViewImageFragment -import com.keylesspalace.tusky.pager.SingleImagePagerAdapter import com.keylesspalace.tusky.pager.ImagePagerAdapter +import com.keylesspalace.tusky.pager.SingleImagePagerAdapter import com.keylesspalace.tusky.util.getTemporaryMediaFilename import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.viewdata.AttachmentViewData @@ -138,6 +138,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener } window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LOW_PROFILE + window.statusBarColor = Color.BLACK window.sharedElementEnterTransition.addListener(object : NoopTransitionListener { override fun onTransitionEnd(transition: Transition) { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollOptionsAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollOptionsAdapter.kt index 6a0b6a871..c3da2c1c2 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollOptionsAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollOptionsAdapter.kt @@ -47,7 +47,7 @@ class AddPollOptionsAdapter( binding.optionEditText.filters = arrayOf(InputFilter.LengthFilter(maxOptionLength)) binding.optionEditText.onTextChanged { s, _, _, _ -> - val pos = holder.adapterPosition + val pos = holder.bindingAdapterPosition if(pos != RecyclerView.NO_POSITION) { options[pos] = s.toString() onOptionChanged(validateInput()) @@ -68,8 +68,8 @@ class AddPollOptionsAdapter( holder.binding.deleteButton.setOnClickListener { holder.binding.optionEditText.clearFocus() - options.removeAt(holder.adapterPosition) - notifyItemRemoved(holder.adapterPosition) + options.removeAt(holder.bindingAdapterPosition) + notifyItemRemoved(holder.bindingAdapterPosition) onOptionRemoved(validateInput()) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt index adc72cd31..13601c1a8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt @@ -21,7 +21,6 @@ import android.graphics.drawable.Drawable import android.net.Uri import android.text.InputFilter import android.text.InputType -import android.util.DisplayMetrics import android.view.WindowManager import android.widget.EditText import android.widget.LinearLayout @@ -54,9 +53,6 @@ fun T.makeCaptionDialog(existingDescription: String?, maximumScale = 6f } - val displayMetrics = DisplayMetrics() - windowManager.defaultDisplay.getMetrics(displayMetrics) - val margin = Utils.dpToPx(this, 4) dialogLayout.addView(imageView) (imageView.layoutParams as LinearLayout.LayoutParams).weight = 1f diff --git a/app/src/main/java/com/keylesspalace/tusky/di/AppInjector.kt b/app/src/main/java/com/keylesspalace/tusky/di/AppInjector.kt index bd06bfc1e..21fd184a6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/AppInjector.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/AppInjector.kt @@ -41,22 +41,22 @@ object AppInjector { handleActivity(activity) } - override fun onActivityPaused(activity: Activity?) { + override fun onActivityPaused(activity: Activity) { } - override fun onActivityResumed(activity: Activity?) { + override fun onActivityResumed(activity: Activity) { } - override fun onActivityStarted(activity: Activity?) { + override fun onActivityStarted(activity: Activity) { } - override fun onActivityDestroyed(activity: Activity?) { + override fun onActivityDestroyed(activity: Activity) { } - override fun onActivitySaveInstanceState(activity: Activity?, outState: Bundle?) { + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { } - override fun onActivityStopped(activity: Activity?) { + override fun onActivityStopped(activity: Activity) { } }) diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java index c6bef703a..deec5888f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java @@ -306,8 +306,8 @@ public class NotificationsFragment extends SFragment implements private void confirmClearNotifications() { new AlertDialog.Builder(getContext()) .setMessage(R.string.notification_clear_text) - .setPositiveButton(android.R.string.yes, (DialogInterface dia, int which) -> clearNotifications()) - .setNegativeButton(android.R.string.no, null) + .setPositiveButton(android.R.string.ok, (DialogInterface dia, int which) -> clearNotifications()) + .setNegativeButton(android.R.string.cancel, null) .show(); } diff --git a/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt b/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt index b0ecc4ad5..c69de81c3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt +++ b/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt @@ -51,8 +51,8 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() { val senderFullName = intent.getStringExtra(NotificationHelper.KEY_SENDER_ACCOUNT_FULL_NAME) val citedStatusId = intent.getStringExtra(NotificationHelper.KEY_CITED_STATUS_ID) val visibility = intent.getSerializableExtra(NotificationHelper.KEY_VISIBILITY) as Status.Visibility - val spoiler = intent.getStringExtra(NotificationHelper.KEY_SPOILER) - val mentions = intent.getStringArrayExtra(NotificationHelper.KEY_MENTIONS) + val spoiler = intent.getStringExtra(NotificationHelper.KEY_SPOILER) ?: "" + val mentions = intent.getStringArrayExtra(NotificationHelper.KEY_MENTIONS) ?: emptyArray() val citedText = intent.getStringExtra(NotificationHelper.KEY_CITED_TEXT) val localAuthorId = intent.getStringExtra(NotificationHelper.KEY_CITED_AUTHOR_LOCAL) @@ -69,7 +69,7 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() { val builder = NotificationCompat.Builder(context, NotificationHelper.CHANNEL_MENTION + senderIdentifier) .setSmallIcon(R.drawable.ic_notify) - .setColor(ContextCompat.getColor(context, (R.color.tusky_blue))) + .setColor(ContextCompat.getColor(context, R.color.tusky_blue)) .setGroup(senderFullName) .setDefaults(0) // So it doesn't ring twice, notify only in Target callback From ea3e7f4ce942c927d73a1b852b20aa5d00ee307e Mon Sep 17 00:00:00 2001 From: Vegard Skjefstad Date: Wed, 2 Jun 2021 00:01:12 +0000 Subject: [PATCH 59/92] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegian?= =?UTF-8?q?=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (14 of 14 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/nb_NO/ --- fastlane/metadata/android/nb-NO/changelogs/83.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 fastlane/metadata/android/nb-NO/changelogs/83.txt diff --git a/fastlane/metadata/android/nb-NO/changelogs/83.txt b/fastlane/metadata/android/nb-NO/changelogs/83.txt new file mode 100644 index 000000000..3d7767f5c --- /dev/null +++ b/fastlane/metadata/android/nb-NO/changelogs/83.txt @@ -0,0 +1,3 @@ +Tusky v15.1 + +Denne versjonen retter en programfeil ved skriving av bildetekst From d588bbf89f2da10542d04e11283fa170f802f44a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Gera?= Date: Wed, 2 Jun 2021 00:01:13 +0000 Subject: [PATCH 60/92] Translated using Weblate (Hungarian) Currently translated at 100.0% (14 of 14 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/hu/ --- fastlane/metadata/android/hu/changelogs/83.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 fastlane/metadata/android/hu/changelogs/83.txt diff --git a/fastlane/metadata/android/hu/changelogs/83.txt b/fastlane/metadata/android/hu/changelogs/83.txt new file mode 100644 index 000000000..26718d870 --- /dev/null +++ b/fastlane/metadata/android/hu/changelogs/83.txt @@ -0,0 +1,3 @@ +Tusky v15.1 + +Ez a kiadás egy képek feliratozása közben jelentkező hibát javít From d432d9f184adaad64daa3d315a8f2c29d9482081 Mon Sep 17 00:00:00 2001 From: cami Date: Thu, 3 Jun 2021 21:49:37 +0000 Subject: [PATCH 61/92] Translated using Weblate (German) Currently translated at 100.0% (458 of 458 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/de/ --- app/src/main/res/values-de/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 4c6ac1b3e..45d7ed50c 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -365,8 +365,8 @@ endet um %s Geschlossen Abstimmen - Eine Umfrage in der du abgestimmt hast ist vorbei - Eine Umfrage die du erstellt hast ist vorbei + Eine Umfrage, in der du abgestimmt hast, ist vorbei + Eine Umfrage, die du erstellt hast, ist vorbei %d Tag verbleibend %d Tage verbleibend From 44a5b42cac856ed2cad4d998c17a68dca33fc9fb Mon Sep 17 00:00:00 2001 From: Ivan Kupalov Date: Fri, 11 Jun 2021 20:15:40 +0200 Subject: [PATCH 62/92] Timeline refactor (#2175) * Move Timeline files into their own package * Introduce TimelineViewModel, add coroutines * Simplify StatusViewData * Handle timeilne fetch errors * Rework filters, fix ViewThreadFragment * Fix NotificationsFragment * Simplify Notifications and Thread, handle pin * Redo loading in TimelineViewModel * Improve error handling in TimelineViewModel * Rewrite actions in TimelineViewModel * Apply feedback after timeline factoring review * Handle initial failure in timeline correctly --- app/build.gradle | 2 + .../keylesspalace/tusky/TimelineDAOTest.kt | 2 +- .../keylesspalace/tusky/FiltersActivity.kt | 42 +- .../com/keylesspalace/tusky/ListsActivity.kt | 4 +- .../com/keylesspalace/tusky/MainActivity.kt | 1 - .../tusky/ModalTimelineActivity.kt | 9 +- .../keylesspalace/tusky/StatusListActivity.kt | 4 +- .../java/com/keylesspalace/tusky/TabData.kt | 11 +- .../keylesspalace/tusky/ViewTagActivity.java | 2 +- .../tusky/adapter/NotificationsAdapter.java | 32 +- .../tusky/adapter/PlaceholderViewHolder.java | 2 +- .../tusky/adapter/StatusBaseViewHolder.java | 90 +- .../adapter/StatusDetailedViewHolder.java | 9 +- .../tusky/adapter/StatusViewHolder.java | 24 +- .../keylesspalace/tusky/appstore/Events.kt | 1 + .../conversation/ConversationEntity.kt | 6 +- .../conversation/ConversationsFragment.kt | 6 +- .../conversation/ConversationsViewModel.kt | 118 +- .../notifications/NotificationHelper.java | 3 +- .../report/adapter/StatusViewHolder.kt | 2 +- .../components/search/SearchViewModel.kt | 187 +-- .../search/adapter/SearchStatusesAdapter.kt | 2 +- .../fragments/SearchStatusesFragment.kt | 2 +- .../timeline}/TimelineAdapter.java | 4 +- .../components/timeline/TimelineFragment.kt | 563 ++++++++ .../components/timeline/TimelineRepository.kt | 413 ++++++ .../components/timeline/TimelineViewModel.kt | 903 ++++++++++++ .../com/keylesspalace/tusky/db/Converters.kt | 6 +- .../tusky/di/FragmentBuildersModule.kt | 1 + .../tusky/di/RepositoryModule.kt | 4 +- .../tusky/di/ViewModelFactory.kt | 7 + .../tusky/entity/Notification.kt | 26 +- .../com/keylesspalace/tusky/entity/Status.kt | 15 +- .../tusky/fragment/NotificationsFragment.java | 277 ++-- .../tusky/fragment/SFragment.java | 131 +- .../tusky/fragment/TimelineFragment.kt | 1265 ----------------- .../tusky/fragment/ViewThreadFragment.java | 252 ++-- .../interfaces/StatusActionListener.java | 2 +- .../tusky/network/FilterModel.kt | 56 + .../tusky/network/MastodonApi.kt | 2 +- .../tusky/network/TimelineCases.kt | 123 +- .../tusky/pager/AccountPagerAdapter.kt | 9 +- .../tusky/repository/TimelineRepository.kt | 392 ----- .../keylesspalace/tusky/util/LinkHelper.java | 9 +- .../util/ListStatusAccessibilityDelegate.kt | 227 +-- .../com/keylesspalace/tusky/util/ListUtils.kt | 4 + .../keylesspalace/tusky/util/StringUtils.kt | 9 + .../tusky/util/ViewDataUtils.java | 86 -- .../keylesspalace/tusky/util/ViewDataUtils.kt | 53 + .../view/ConversationLineItemDecoration.kt | 4 +- .../tusky/viewdata/AttachmentViewData.kt | 7 - .../tusky/viewdata/NotificationViewData.java | 8 +- .../tusky/viewdata/StatusViewData.java | 677 --------- .../tusky/viewdata/StatusViewData.kt | 144 ++ .../tusky/BottomSheetActivityTest.kt | 2 +- .../com/keylesspalace/tusky/FilterTest.kt | 298 ++-- .../timeline}/TimelineRepositoryTest.kt | 251 ++-- .../timeline/TimelineViewModelTest.kt | 783 ++++++++++ 58 files changed, 3956 insertions(+), 3618 deletions(-) rename app/src/main/java/com/keylesspalace/tusky/{adapter => components/timeline}/TimelineAdapter.java (96%) create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineRepository.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineViewModel.kt delete mode 100644 app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/network/FilterModel.kt delete mode 100644 app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt delete mode 100644 app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.kt delete mode 100644 app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt rename app/src/test/java/com/keylesspalace/tusky/{fragment => components/timeline}/TimelineRepositoryTest.kt (60%) create mode 100644 app/src/test/java/com/keylesspalace/tusky/components/timeline/TimelineViewModelTest.kt diff --git a/app/build.gradle b/app/build.gradle index 91874f1c9..ca917abdf 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -119,6 +119,8 @@ dependencies { implementation "androidx.work:work-runtime:2.5.0" implementation "androidx.room:room-runtime:$roomVersion" implementation "androidx.room:room-rxjava3:$roomVersion" + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-rx3:1.5.0' kapt "androidx.room:room-compiler:$roomVersion" implementation "com.google.android.material:material:1.3.0" diff --git a/app/src/androidTest/java/com/keylesspalace/tusky/TimelineDAOTest.kt b/app/src/androidTest/java/com/keylesspalace/tusky/TimelineDAOTest.kt index da55b08b7..92288bba2 100644 --- a/app/src/androidTest/java/com/keylesspalace/tusky/TimelineDAOTest.kt +++ b/app/src/androidTest/java/com/keylesspalace/tusky/TimelineDAOTest.kt @@ -5,7 +5,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.keylesspalace.tusky.db.* import com.keylesspalace.tusky.entity.Status -import com.keylesspalace.tusky.repository.TimelineRepository +import com.keylesspalace.tusky.components.timeline.TimelineRepository import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertNull diff --git a/app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt b/app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt index 7e91db07d..1fe165b74 100644 --- a/app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt @@ -5,6 +5,7 @@ import android.widget.AdapterView import android.widget.ArrayAdapter import android.widget.Toast import androidx.appcompat.app.AlertDialog +import androidx.lifecycle.lifecycleScope import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.PreferenceChangedEvent import com.keylesspalace.tusky.databinding.ActivityFiltersBinding @@ -14,11 +15,14 @@ import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.viewBinding +import kotlinx.coroutines.launch +import kotlinx.coroutines.rx3.await import okhttp3.ResponseBody import retrofit2.Call import retrofit2.Callback import retrofit2.Response import java.io.IOException +import java.lang.Exception import javax.inject.Inject class FiltersActivity: BaseActivity() { @@ -162,37 +166,29 @@ class FiltersActivity: BaseActivity() { binding.addFilterButton.hide() binding.filterProgressBar.show() - api.getFilters().enqueue(object : Callback> { - override fun onResponse(call: Call>, response: Response>) { - val filterResponse = response.body() - if(response.isSuccessful && filterResponse != null) { - - filters = filterResponse.filter { filter -> filter.context.contains(context) }.toMutableList() - refreshFilterDisplay() - - binding.filtersView.show() - binding.addFilterButton.show() - binding.filterProgressBar.hide() - } else { - binding.filterProgressBar.hide() - binding.filterMessageView.show() - binding.filterMessageView.setup(R.drawable.elephant_error, - R.string.error_generic) { loadFilters() } - } - } - - override fun onFailure(call: Call>, t: Throwable) { + lifecycleScope.launch { + val newFilters = try { + api.getFilters().await() + } catch (t: Exception) { binding.filterProgressBar.hide() binding.filterMessageView.show() if (t is IOException) { binding.filterMessageView.setup(R.drawable.elephant_offline, - R.string.error_network) { loadFilters() } + R.string.error_network) { loadFilters() } } else { binding.filterMessageView.setup(R.drawable.elephant_error, - R.string.error_generic) { loadFilters() } + R.string.error_generic) { loadFilters() } } + return@launch } - }) + + filters = newFilters.filter { it.context.contains(context) }.toMutableList() + refreshFilterDisplay() + + binding.filtersView.show() + binding.addFilterButton.show() + binding.filterProgressBar.hide() + } } companion object { diff --git a/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt index 5812a10ff..cb6acd866 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt @@ -37,7 +37,7 @@ import com.keylesspalace.tusky.databinding.ActivityListsBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.entity.MastoList -import com.keylesspalace.tusky.fragment.TimelineFragment +import com.keylesspalace.tusky.components.timeline.TimelineViewModel import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.viewmodel.ListsViewModel import com.keylesspalace.tusky.viewmodel.ListsViewModel.Event.* @@ -182,7 +182,7 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector { private fun onListSelected(listId: String) { startActivityWithSlideInAnimation( - ModalTimelineActivity.newIntent(this, TimelineFragment.Kind.LIST, listId)) + ModalTimelineActivity.newIntent(this, TimelineViewModel.Kind.LIST, listId)) } private fun openListSettings(list: MastoList) { diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt index 972c15f3c..96eb1e34c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt @@ -595,7 +595,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje private fun changeAccount(newSelectedId: Long, forward: Intent?) { cacheUpdater.stop() - SFragment.flushFilters() accountManager.setActiveAccount(newSelectedId) val intent = Intent(this, MainActivity::class.java) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK diff --git a/app/src/main/java/com/keylesspalace/tusky/ModalTimelineActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ModalTimelineActivity.kt index 64c229179..000cf3a9f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ModalTimelineActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/ModalTimelineActivity.kt @@ -5,7 +5,8 @@ import android.content.Intent import android.os.Bundle import com.google.android.material.floatingactionbutton.FloatingActionButton import com.keylesspalace.tusky.databinding.ActivityModalTimelineBinding -import com.keylesspalace.tusky.fragment.TimelineFragment +import com.keylesspalace.tusky.components.timeline.TimelineFragment +import com.keylesspalace.tusky.components.timeline.TimelineViewModel import com.keylesspalace.tusky.interfaces.ActionButtonActivity import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector @@ -29,8 +30,8 @@ class ModalTimelineActivity : BottomSheetActivity(), ActionButtonActivity, HasAn } if (supportFragmentManager.findFragmentById(R.id.contentFrame) == null) { - val kind = intent?.getSerializableExtra(ARG_KIND) as? TimelineFragment.Kind - ?: TimelineFragment.Kind.HOME + val kind = intent?.getSerializableExtra(ARG_KIND) as? TimelineViewModel.Kind + ?: TimelineViewModel.Kind.HOME val argument = intent?.getStringExtra(ARG_ARG) supportFragmentManager.beginTransaction() .replace(R.id.contentFrame, TimelineFragment.newInstance(kind, argument)) @@ -47,7 +48,7 @@ class ModalTimelineActivity : BottomSheetActivity(), ActionButtonActivity, HasAn private const val ARG_ARG = "arg" @JvmStatic - fun newIntent(context: Context, kind: TimelineFragment.Kind, + fun newIntent(context: Context, kind: TimelineViewModel.Kind, argument: String?): Intent { val intent = Intent(context, ModalTimelineActivity::class.java) intent.putExtra(ARG_KIND, kind) diff --git a/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt b/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt index b2691ee94..219d47df5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt @@ -21,8 +21,8 @@ import android.os.Bundle import androidx.fragment.app.commit import com.keylesspalace.tusky.databinding.ActivityStatuslistBinding -import com.keylesspalace.tusky.fragment.TimelineFragment -import com.keylesspalace.tusky.fragment.TimelineFragment.Kind +import com.keylesspalace.tusky.components.timeline.TimelineFragment +import com.keylesspalace.tusky.components.timeline.TimelineViewModel.Kind import javax.inject.Inject diff --git a/app/src/main/java/com/keylesspalace/tusky/TabData.kt b/app/src/main/java/com/keylesspalace/tusky/TabData.kt index 4bf123b89..5eabec5f4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TabData.kt +++ b/app/src/main/java/com/keylesspalace/tusky/TabData.kt @@ -21,7 +21,8 @@ import androidx.annotation.StringRes import androidx.fragment.app.Fragment import com.keylesspalace.tusky.components.conversation.ConversationsFragment import com.keylesspalace.tusky.fragment.NotificationsFragment -import com.keylesspalace.tusky.fragment.TimelineFragment +import com.keylesspalace.tusky.components.timeline.TimelineFragment +import com.keylesspalace.tusky.components.timeline.TimelineViewModel /** this would be a good case for a sealed class, but that does not work nice with Room */ @@ -47,7 +48,7 @@ fun createTabDataFromId(id: String, arguments: List = emptyList()): TabD HOME, R.string.title_home, R.drawable.ic_home_24dp, - { TimelineFragment.newInstance(TimelineFragment.Kind.HOME) } + { TimelineFragment.newInstance(TimelineViewModel.Kind.HOME) } ) NOTIFICATIONS -> TabData( NOTIFICATIONS, @@ -59,13 +60,13 @@ fun createTabDataFromId(id: String, arguments: List = emptyList()): TabD LOCAL, R.string.title_public_local, R.drawable.ic_local_24dp, - { TimelineFragment.newInstance(TimelineFragment.Kind.PUBLIC_LOCAL) } + { TimelineFragment.newInstance(TimelineViewModel.Kind.PUBLIC_LOCAL) } ) FEDERATED -> TabData( FEDERATED, R.string.title_public_federated, R.drawable.ic_public_24dp, - { TimelineFragment.newInstance(TimelineFragment.Kind.PUBLIC_FEDERATED) } + { TimelineFragment.newInstance(TimelineViewModel.Kind.PUBLIC_FEDERATED) } ) DIRECT -> TabData( DIRECT, @@ -85,7 +86,7 @@ fun createTabDataFromId(id: String, arguments: List = emptyList()): TabD LIST, R.string.list, R.drawable.ic_list, - { args -> TimelineFragment.newInstance(TimelineFragment.Kind.LIST, args.getOrNull(0).orEmpty()) }, + { args -> TimelineFragment.newInstance(TimelineViewModel.Kind.LIST, args.getOrNull(0).orEmpty()) }, arguments, { arguments.getOrNull(1).orEmpty() } ) diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java b/app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java index 0ff6ff565..0071924bc 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java @@ -25,7 +25,7 @@ import androidx.appcompat.widget.Toolbar; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentTransaction; -import com.keylesspalace.tusky.fragment.TimelineFragment; +import com.keylesspalace.tusky.components.timeline.TimelineFragment; import java.util.Collections; diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java index 0a872c6f1..b42f42782 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java @@ -43,6 +43,7 @@ import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding; import com.keylesspalace.tusky.entity.Account; import com.keylesspalace.tusky.entity.Emoji; import com.keylesspalace.tusky.entity.Notification; +import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.interfaces.AccountActionListener; import com.keylesspalace.tusky.interfaces.LinkListener; import com.keylesspalace.tusky.interfaces.StatusActionListener; @@ -195,14 +196,15 @@ public class NotificationsAdapter extends RecyclerView.Adapter { } else { holder.showNotificationContent(true); - holder.setDisplayName(statusViewData.getUserFullName(), statusViewData.getAccountEmojis()); - holder.setUsername(statusViewData.getNickname()); - holder.setCreatedAt(statusViewData.getCreatedAt()); + Status status = statusViewData.getActionable(); + holder.setDisplayName(status.getAccount().getDisplayName(), status.getAccount().getEmojis()); + holder.setUsername(status.getAccount().getUsername()); + holder.setCreatedAt(status.getCreatedAt()); - if(concreteNotificaton.getType() == Notification.Type.STATUS) { - holder.setAvatar(statusViewData.getAvatar(), statusViewData.isBot()); + if (concreteNotificaton.getType() == Notification.Type.STATUS) { + holder.setAvatar(status.getAccount().getAvatar(), status.getAccount().getBot()); } else { - holder.setAvatars(statusViewData.getAvatar(), + holder.setAvatars(status.getAccount().getAvatar(), concreteNotificaton.getAccount().getAvatar()); } } @@ -215,7 +217,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter { if (payloadForHolder instanceof List) for (Object item : (List) payloadForHolder) { if (StatusBaseViewHolder.Key.KEY_CREATED.equals(item) && statusViewData != null) { - holder.setCreatedAt(statusViewData.getCreatedAt()); + holder.setCreatedAt(statusViewData.getStatus().getActionableStatus().getCreatedAt()); } } } @@ -386,7 +388,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter { private StatusViewData.Concrete statusViewData; private SimpleDateFormat shortSdf; private SimpleDateFormat longSdf; - + private int avatarRadius48dp; private int avatarRadius36dp; private int avatarRadius24dp; @@ -415,7 +417,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter { statusContent.setOnClickListener(this); shortSdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault()); longSdf = new SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault()); - + this.avatarRadius48dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_48dp); this.avatarRadius36dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_36dp); this.avatarRadius24dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_24dp); @@ -531,7 +533,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter { message.setText(emojifiedText); if (statusViewData != null) { - boolean hasSpoiler = !TextUtils.isEmpty(statusViewData.getSpoilerText()); + boolean hasSpoiler = !TextUtils.isEmpty(statusViewData.getStatus().getSpoilerText()); contentWarningDescriptionTextView.setVisibility(hasSpoiler ? View.VISIBLE : View.GONE); contentWarningButton.setVisibility(hasSpoiler ? View.VISIBLE : View.GONE); if (statusViewData.isExpanded()) { @@ -586,7 +588,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter { notificationAvatar.setVisibility(View.VISIBLE); ImageLoadingHelper.loadAvatar(notificationAvatarUrl, notificationAvatar, - avatarRadius24dp, statusDisplayOptions.animateAvatars()); + avatarRadius24dp, statusDisplayOptions.animateAvatars()); } @Override @@ -607,7 +609,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter { private void setupContentAndSpoiler(final LinkListener listener) { boolean shouldShowContentIfSpoiler = statusViewData.isExpanded(); - boolean hasSpoiler = !TextUtils.isEmpty(statusViewData.getSpoilerText()); + boolean hasSpoiler = !TextUtils.isEmpty(statusViewData.getStatus(). getSpoilerText()); if (!shouldShowContentIfSpoiler && hasSpoiler) { statusContent.setVisibility(View.GONE); } else { @@ -615,7 +617,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter { } Spanned content = statusViewData.getContent(); - List emojis = statusViewData.getStatusEmojis(); + List emojis = statusViewData.getActionable().getEmojis(); if (statusViewData.isCollapsible() && (statusViewData.isExpanded() || !hasSpoiler)) { contentCollapseButton.setOnClickListener(view -> { @@ -641,13 +643,13 @@ public class NotificationsAdapter extends RecyclerView.Adapter { CharSequence emojifiedText = CustomEmojiHelper.emojify( content, emojis, statusContent, statusDisplayOptions.animateEmojis() ); - LinkHelper.setClickableText(statusContent, emojifiedText, statusViewData.getMentions(), listener); + LinkHelper.setClickableText(statusContent, emojifiedText, statusViewData.getActionable().getMentions(), listener); CharSequence emojifiedContentWarning; if (statusViewData.getSpoilerText() != null) { emojifiedContentWarning = CustomEmojiHelper.emojify( statusViewData.getSpoilerText(), - statusViewData.getStatusEmojis(), + statusViewData.getActionable().getEmojis(), contentWarningDescriptionTextView, statusDisplayOptions.animateEmojis() ); diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/PlaceholderViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/PlaceholderViewHolder.java index f8f1a0b53..9f85d9817 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/PlaceholderViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/PlaceholderViewHolder.java @@ -28,7 +28,7 @@ public final class PlaceholderViewHolder extends RecyclerView.ViewHolder { private Button loadMoreButton; private ProgressBar progressBar; - PlaceholderViewHolder(View itemView) { + public PlaceholderViewHolder(View itemView) { super(itemView); loadMoreButton = itemView.findViewById(R.id.button_load_more); progressBar = itemView.findViewById(R.id.progressBar); diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java index d6cee6261..36198d280 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java @@ -201,7 +201,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { protected void setSpoilerAndContent(boolean expanded, @NonNull Spanned content, @Nullable String spoilerText, - @Nullable Status.Mention[] mentions, + @Nullable List mentions, @NonNull List emojis, @Nullable PollViewData poll, @NonNull StatusDisplayOptions statusDisplayOptions, @@ -243,7 +243,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { private void setTextVisible(boolean sensitive, boolean expanded, Spanned content, - Status.Mention[] mentions, + List mentions, List emojis, @Nullable PollViewData poll, StatusDisplayOptions statusDisplayOptions, @@ -708,21 +708,23 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { this.setupWithStatus(status, listener, statusDisplayOptions, null); } - protected void setupWithStatus(StatusViewData.Concrete status, - final StatusActionListener listener, - StatusDisplayOptions statusDisplayOptions, - @Nullable Object payloads) { + public void setupWithStatus(StatusViewData.Concrete status, + final StatusActionListener listener, + StatusDisplayOptions statusDisplayOptions, + @Nullable Object payloads) { if (payloads == null) { - setDisplayName(status.getUserFullName(), status.getAccountEmojis(), statusDisplayOptions); - setUsername(status.getNickname()); - setCreatedAt(status.getCreatedAt(), statusDisplayOptions); - setIsReply(status.getInReplyToId() != null); - setAvatar(status.getAvatar(), status.getRebloggedAvatar(), status.isBot(), statusDisplayOptions); - setReblogged(status.isReblogged()); - setFavourited(status.isFavourited()); - setBookmarked(status.isBookmarked()); - List attachments = status.getAttachments(); - boolean sensitive = status.isSensitive(); + Status actionable = status.getActionable(); + setDisplayName(actionable.getAccount().getDisplayName(), actionable.getAccount().getEmojis(), statusDisplayOptions); + setUsername(status.getUsername()); + setCreatedAt(actionable.getCreatedAt(), statusDisplayOptions); + setIsReply(actionable.getInReplyToId() != null); + setAvatar(actionable.getAccount().getAvatar(), status.getRebloggedAvatar(), + actionable.getAccount().getBot(), statusDisplayOptions); + setReblogged(actionable.getReblogged()); + setFavourited(actionable.getFavourited()); + setBookmarked(actionable.getBookmarked()); + List attachments = actionable.getAttachments(); + boolean sensitive = actionable.getSensitive(); if (statusDisplayOptions.mediaPreviewEnabled() && hasPreviewableAttachment(attachments)) { setMediaPreviews(attachments, sensitive, listener, status.isShowingContent(), statusDisplayOptions.useBlurhash()); @@ -747,11 +749,14 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { setupCard(status, statusDisplayOptions.cardViewMode(), statusDisplayOptions); } - setupButtons(listener, status.getSenderId(), status.getContent().toString(), + setupButtons(listener, actionable.getAccount().getId(), status.getContent().toString(), statusDisplayOptions); - setRebloggingEnabled(status.getRebloggingEnabled(), status.getVisibility()); + setRebloggingEnabled(actionable.rebloggingAllowed(), actionable.getVisibility()); - setSpoilerAndContent(status.isExpanded(), status.getContent(), status.getSpoilerText(), status.getMentions(), status.getStatusEmojis(), status.getPoll(), statusDisplayOptions, listener); + setSpoilerAndContent(status.isExpanded(), status.getContent(), status.getSpoilerText(), + actionable.getMentions(), actionable.getEmojis(), + PollViewDataKt.toViewData(actionable.getPoll()), statusDisplayOptions, + listener); setDescriptionForStatus(status, statusDisplayOptions); @@ -765,7 +770,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { if (payloads instanceof List) for (Object item : (List) payloads) { if (Key.KEY_CREATED.equals(item)) { - setCreatedAt(status.getCreatedAt(), statusDisplayOptions); + setCreatedAt(status.getActionable().getCreatedAt(), statusDisplayOptions); } } @@ -784,21 +789,22 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { private void setDescriptionForStatus(@NonNull StatusViewData.Concrete status, StatusDisplayOptions statusDisplayOptions) { Context context = itemView.getContext(); + Status actionable = status.getActionable(); String description = context.getString(R.string.description_status, - status.getUserFullName(), + actionable.getAccount().getDisplayName(), getContentWarningDescription(context, status), - (TextUtils.isEmpty(status.getSpoilerText()) || !status.isSensitive() || status.isExpanded() ? status.getContent() : ""), - getCreatedAtDescription(status.getCreatedAt(), statusDisplayOptions), + (TextUtils.isEmpty(status.getSpoilerText()) || !actionable.getSensitive() || status.isExpanded() ? status.getContent() : ""), + getCreatedAtDescription(actionable.getCreatedAt(), statusDisplayOptions), getReblogDescription(context, status), - status.getNickname(), - status.isReblogged() ? context.getString(R.string.description_status_reblogged) : "", - status.isFavourited() ? context.getString(R.string.description_status_favourited) : "", - status.isBookmarked() ? context.getString(R.string.description_status_bookmarked) : "", + status.getUsername(), + actionable.getReblogged() ? context.getString(R.string.description_status_reblogged) : "", + actionable.getFavourited() ? context.getString(R.string.description_status_favourited) : "", + actionable.getBookmarked() ? context.getString(R.string.description_status_bookmarked) : "", getMediaDescription(context, status), - getVisibilityDescription(context, status.getVisibility()), - getFavsText(context, status.getFavouritesCount()), - getReblogsText(context, status.getReblogsCount()), + getVisibilityDescription(context, actionable.getVisibility()), + getFavsText(context, actionable.getFavouritesCount()), + getReblogsText(context, actionable.getReblogsCount()), getPollDescription(status, context, statusDisplayOptions) ); itemView.setContentDescription(description); @@ -806,10 +812,10 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { private static CharSequence getReblogDescription(Context context, @NonNull StatusViewData.Concrete status) { - String rebloggedUsername = status.getRebloggedByUsername(); - if (rebloggedUsername != null) { + Status reblog = status.getRebloggingStatus(); + if (reblog != null) { return context - .getString(R.string.status_boosted_format, rebloggedUsername); + .getString(R.string.status_boosted_format, reblog.getAccount().getUsername()); } else { return ""; } @@ -817,11 +823,11 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { private static CharSequence getMediaDescription(Context context, @NonNull StatusViewData.Concrete status) { - if (status.getAttachments().isEmpty()) { + if (status.getActionable().getAttachments().isEmpty()) { return ""; } StringBuilder mediaDescriptions = CollectionsKt.fold( - status.getAttachments(), + status.getActionable().getAttachments(), new StringBuilder(), (builder, a) -> { if (a.getDescription() == null) { @@ -874,7 +880,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { private CharSequence getPollDescription(@NonNull StatusViewData.Concrete status, Context context, StatusDisplayOptions statusDisplayOptions) { - PollViewData poll = status.getPoll(); + PollViewData poll = PollViewDataKt.toViewData(status.getActionable().getPoll()); if (poll == null) { return ""; } else { @@ -980,7 +986,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { StatusDisplayOptions statusDisplayOptions, Context context) { String votesText; - if(poll.getVotersCount() == null) { + if (poll.getVotersCount() == null) { String voters = numberFormat.format(poll.getVotesCount()); votesText = context.getResources().getQuantityString(R.plurals.poll_info_votes, poll.getVotesCount(), voters); } else { @@ -1004,12 +1010,12 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { } protected void setupCard(StatusViewData.Concrete status, CardViewMode cardViewMode, StatusDisplayOptions statusDisplayOptions) { + final Card card = status.getActionable().getCard(); if (cardViewMode != CardViewMode.NONE && - status.getAttachments().size() == 0 && - status.getCard() != null && - !TextUtils.isEmpty(status.getCard().getUrl()) && + status.getActionable().getAttachments().size() == 0 && + card != null && + !TextUtils.isEmpty(card.getUrl()) && (!status.isCollapsible() || !status.isCollapsed())) { - final Card card = status.getCard(); cardView.setVisibility(View.VISIBLE); cardTitle.setText(card.getTitle()); if (TextUtils.isEmpty(card.getDescription()) && TextUtils.isEmpty(card.getAuthorName())) { @@ -1028,7 +1034,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { // Statuses from other activitypub sources can be marked sensitive even if there's no media, // so let's blur the preview in that case // If media previews are disabled, show placeholder for cards as well - if (statusDisplayOptions.mediaPreviewEnabled() && !status.isSensitive() && !TextUtils.isEmpty(card.getImage())) { + if (statusDisplayOptions.mediaPreviewEnabled() && !status.getActionable().getSensitive() && !TextUtils.isEmpty(card.getImage())) { int topLeftRadius = 0; int topRightRadius = 0; diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java index abb8ca85a..ef2c704d6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java @@ -101,7 +101,7 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder { } @Override - protected void setupWithStatus(final StatusViewData.Concrete status, + public void setupWithStatus(final StatusViewData.Concrete status, final StatusActionListener listener, StatusDisplayOptions statusDisplayOptions, @Nullable Object payloads) { @@ -110,12 +110,13 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder { if (payloads == null) { if (!statusDisplayOptions.hideStats()) { - setReblogAndFavCount(status.getReblogsCount(), status.getFavouritesCount(), listener); + setReblogAndFavCount(status.getActionable().getReblogsCount(), + status.getActionable().getFavouritesCount(), listener); } else { hideQuantitativeStats(); } - setApplication(status.getApplication()); + setApplication(status.getActionable().getApplication()); View.OnLongClickListener longClickListener = view -> { TextView textView = (TextView) view; @@ -130,7 +131,7 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder { content.setOnLongClickListener(longClickListener); contentWarningDescription.setOnLongClickListener(longClickListener); - setStatusVisibility(status.getVisibility()); + setStatusVisibility(status.getActionable().getVisibility()); } } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java index 68d64a698..684159892 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java @@ -26,6 +26,8 @@ import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.entity.Emoji; +import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.util.CustomEmojiHelper; import com.keylesspalace.tusky.util.SmartLengthInputFilter; @@ -33,6 +35,8 @@ import com.keylesspalace.tusky.util.StatusDisplayOptions; import com.keylesspalace.tusky.util.StringUtils; import com.keylesspalace.tusky.viewdata.StatusViewData; +import java.util.List; + import at.connyduck.sparkbutton.helpers.Utils; public class StatusViewHolder extends StatusBaseViewHolder { @@ -54,19 +58,21 @@ public class StatusViewHolder extends StatusBaseViewHolder { } @Override - protected void setupWithStatus(StatusViewData.Concrete status, - final StatusActionListener listener, - StatusDisplayOptions statusDisplayOptions, - @Nullable Object payloads) { + public void setupWithStatus(StatusViewData.Concrete status, + final StatusActionListener listener, + StatusDisplayOptions statusDisplayOptions, + @Nullable Object payloads) { if (payloads == null) { setupCollapsedState(status, listener); - String rebloggedByDisplayName = status.getRebloggedByUsername(); - if (rebloggedByDisplayName == null) { + Status reblogging = status.getRebloggingStatus(); + if (reblogging == null) { hideStatusInfo(); } else { - setRebloggedByDisplayName(rebloggedByDisplayName, status, statusDisplayOptions); + String rebloggedByDisplayName = reblogging.getAccount().getDisplayName(); + setRebloggedByDisplayName(rebloggedByDisplayName, + reblogging.getAccount().getEmojis(), statusDisplayOptions); statusInfo.setOnClickListener(v -> listener.onOpenReblog(getBindingAdapterPosition())); } @@ -76,13 +82,13 @@ public class StatusViewHolder extends StatusBaseViewHolder { } private void setRebloggedByDisplayName(final CharSequence name, - final StatusViewData.Concrete status, + final List accountEmoji, final StatusDisplayOptions statusDisplayOptions) { Context context = statusInfo.getContext(); CharSequence wrappedName = StringUtils.unicodeWrap(name); CharSequence boostedText = context.getString(R.string.status_boosted_format, wrappedName); CharSequence emojifiedText = CustomEmojiHelper.emojify( - boostedText, status.getRebloggedByAccountEmojis(), statusInfo, statusDisplayOptions.animateEmojis() + boostedText, accountEmoji, statusInfo, statusDisplayOptions.animateEmojis() ); statusInfo.setText(emojifiedText); statusInfo.setVisibility(View.VISIBLE); diff --git a/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt b/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt index 288de430f..13baf07f0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt +++ b/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt @@ -21,3 +21,4 @@ data class MainTabsChangedEvent(val newTabs: List) : Dispatchable data class PollVoteEvent(val statusId: String, val poll: Poll) : Dispatchable data class DomainMuteEvent(val instance: String): Dispatchable data class AnnouncementReadEvent(val announcementId: String): Dispatchable +data class PinEvent(val statusId: String, val pinned: Boolean): Dispatchable diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt index e35d460d4..0ecfe3b5e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt @@ -73,7 +73,7 @@ data class ConversationStatusEntity( val sensitive: Boolean, val spoilerText: String, val attachments: ArrayList, - val mentions: Array, + val mentions: List, val showingHiddenContent: Boolean, val expanded: Boolean, val collapsible: Boolean, @@ -101,7 +101,7 @@ data class ConversationStatusEntity( if (sensitive != other.sensitive) return false if (spoilerText != other.spoilerText) return false if (attachments != other.attachments) return false - if (!mentions.contentEquals(other.mentions)) return false + if (mentions != other.mentions) return false if (showingHiddenContent != other.showingHiddenContent) return false if (expanded != other.expanded) return false if (collapsible != other.collapsible) return false @@ -125,7 +125,7 @@ data class ConversationStatusEntity( result = 31 * result + sensitive.hashCode() result = 31 * result + spoilerText.hashCode() result = 31 * result + attachments.hashCode() - result = 31 * result + mentions.contentHashCode() + result = 31 * result + mentions.hashCode() result = 31 * result + showingHiddenContent.hashCode() result = 31 * result + expanded.hashCode() result = 31 * result + collapsible.hashCode() diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt index 009c62f61..43f250c79 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt @@ -40,6 +40,7 @@ import com.keylesspalace.tusky.util.NetworkState import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.viewBinding +import com.keylesspalace.tusky.viewdata.AttachmentViewData import javax.inject.Inject class ConversationsFragment : SFragment(), StatusActionListener, Injectable, ReselectableFragment { @@ -132,13 +133,14 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) { viewModel.conversations.value?.getOrNull(position)?.lastStatus?.let { - viewMedia(attachmentIndex, it.toStatus(), view) + viewMedia(attachmentIndex, AttachmentViewData.list(it.toStatus()), view) } } override fun onViewThread(position: Int) { viewModel.conversations.value?.getOrNull(position)?.lastStatus?.let { - viewThread(it.toStatus()) + val status = it.toStatus() + viewThread(status.actionableId, status.actionableStatus.url) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt index 048024277..5f2b9cdb8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt @@ -15,17 +15,20 @@ import io.reactivex.rxjava3.schedulers.Schedulers import javax.inject.Inject class ConversationsViewModel @Inject constructor( - private val repository: ConversationsRepository, - private val timelineCases: TimelineCases, - private val database: AppDatabase, - private val accountManager: AccountManager + private val repository: ConversationsRepository, + private val timelineCases: TimelineCases, + private val database: AppDatabase, + private val accountManager: AccountManager ) : RxAwareViewModel() { private val repoResult = MutableLiveData>() - val conversations: LiveData> = Transformations.switchMap(repoResult) { it.pagedList } - val networkState: LiveData = Transformations.switchMap(repoResult) { it.networkState } - val refreshState: LiveData = Transformations.switchMap(repoResult) { it.refreshState } + val conversations: LiveData> = + Transformations.switchMap(repoResult) { it.pagedList } + val networkState: LiveData = + Transformations.switchMap(repoResult) { it.networkState } + val refreshState: LiveData = + Transformations.switchMap(repoResult) { it.refreshState } fun load() { val accountId = accountManager.activeAccount?.id ?: return @@ -45,57 +48,76 @@ class ConversationsViewModel @Inject constructor( fun favourite(favourite: Boolean, position: Int) { conversations.value?.getOrNull(position)?.let { conversation -> - timelineCases.favourite(conversation.lastStatus.toStatus(), favourite) - .flatMap { - val newConversation = conversation.copy( - lastStatus = conversation.lastStatus.copy(favourited = favourite) - ) + timelineCases.favourite(conversation.lastStatus.id, favourite) + .flatMap { + val newConversation = conversation.copy( + lastStatus = conversation.lastStatus.copy(favourited = favourite) + ) - database.conversationDao().insert(newConversation) - } - .subscribeOn(Schedulers.io()) - .doOnError { t -> Log.w("ConversationViewModel", "Failed to favourite conversation", t) } - .onErrorReturnItem(0) - .subscribe() - .autoDispose() + database.conversationDao().insert(newConversation) + } + .subscribeOn(Schedulers.io()) + .doOnError { t -> + Log.w( + "ConversationViewModel", + "Failed to favourite conversation", + t + ) + } + .onErrorReturnItem(0) + .subscribe() + .autoDispose() } } fun bookmark(bookmark: Boolean, position: Int) { conversations.value?.getOrNull(position)?.let { conversation -> - timelineCases.bookmark(conversation.lastStatus.toStatus(), bookmark) - .flatMap { - val newConversation = conversation.copy( - lastStatus = conversation.lastStatus.copy(bookmarked = bookmark) - ) + timelineCases.bookmark(conversation.lastStatus.id, bookmark) + .flatMap { + val newConversation = conversation.copy( + lastStatus = conversation.lastStatus.copy(bookmarked = bookmark) + ) - database.conversationDao().insert(newConversation) - } - .subscribeOn(Schedulers.io()) - .doOnError { t -> Log.w("ConversationViewModel", "Failed to bookmark conversation", t) } - .onErrorReturnItem(0) - .subscribe() - .autoDispose() + database.conversationDao().insert(newConversation) + } + .subscribeOn(Schedulers.io()) + .doOnError { t -> + Log.w( + "ConversationViewModel", + "Failed to bookmark conversation", + t + ) + } + .onErrorReturnItem(0) + .subscribe() + .autoDispose() } } fun voteInPoll(position: Int, choices: MutableList) { conversations.value?.getOrNull(position)?.let { conversation -> - timelineCases.voteInPoll(conversation.lastStatus.toStatus(), choices) - .flatMap { poll -> - val newConversation = conversation.copy( - lastStatus = conversation.lastStatus.copy(poll = poll) - ) + val poll = conversation.lastStatus.poll ?: return + timelineCases.voteInPoll(conversation.lastStatus.id, poll.id, choices) + .flatMap { newPoll -> + val newConversation = conversation.copy( + lastStatus = conversation.lastStatus.copy(poll = newPoll) + ) - database.conversationDao().insert(newConversation) - } - .subscribeOn(Schedulers.io()) - .doOnError { t -> Log.w("ConversationViewModel", "Failed to favourite conversation", t) } - .onErrorReturnItem(0) - .subscribe() - .autoDispose() + database.conversationDao().insert(newConversation) + } + .subscribeOn(Schedulers.io()) + .doOnError { t -> + Log.w( + "ConversationViewModel", + "Failed to favourite conversation", + t + ) + } + .onErrorReturnItem(0) + .subscribe() + .autoDispose() } } @@ -103,7 +125,7 @@ class ConversationsViewModel @Inject constructor( fun expandHiddenStatus(expanded: Boolean, position: Int) { conversations.value?.getOrNull(position)?.let { conversation -> val newConversation = conversation.copy( - lastStatus = conversation.lastStatus.copy(expanded = expanded) + lastStatus = conversation.lastStatus.copy(expanded = expanded) ) saveConversationToDb(newConversation) } @@ -112,7 +134,7 @@ class ConversationsViewModel @Inject constructor( fun collapseLongStatus(collapsed: Boolean, position: Int) { conversations.value?.getOrNull(position)?.let { conversation -> val newConversation = conversation.copy( - lastStatus = conversation.lastStatus.copy(collapsed = collapsed) + lastStatus = conversation.lastStatus.copy(collapsed = collapsed) ) saveConversationToDb(newConversation) } @@ -121,7 +143,7 @@ class ConversationsViewModel @Inject constructor( fun showContent(showing: Boolean, position: Int) { conversations.value?.getOrNull(position)?.let { conversation -> val newConversation = conversation.copy( - lastStatus = conversation.lastStatus.copy(showingHiddenContent = showing) + lastStatus = conversation.lastStatus.copy(showingHiddenContent = showing) ) saveConversationToDb(newConversation) } @@ -135,8 +157,8 @@ class ConversationsViewModel @Inject constructor( private fun saveConversationToDb(conversation: ConversationEntity) { database.conversationDao().insert(conversation) - .subscribeOn(Schedulers.io()) - .subscribe() + .subscribeOn(Schedulers.io()) + .subscribe() } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java index d6b9cb99c..2218e0b64 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java @@ -316,7 +316,7 @@ public class NotificationHelper { Status actionableStatus = status.getActionableStatus(); Status.Visibility replyVisibility = actionableStatus.getVisibility(); String contentWarning = actionableStatus.getSpoilerText(); - Status.Mention[] mentions = actionableStatus.getMentions(); + List mentions = actionableStatus.getMentions(); List mentionedUsernames = new ArrayList<>(); mentionedUsernames.add(actionableStatus.getAccount().getUsername()); for (Status.Mention mention : mentions) { @@ -381,7 +381,6 @@ public class NotificationHelper { NotificationChannelGroup channelGroup = new NotificationChannelGroup(account.getIdentifier(), account.getFullName()); - //noinspection ConstantConditions notificationManager.createNotificationChannelGroup(channelGroup); for (int i = 0; i < channelIds.length; i++) { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt index 90579a92c..5ac3dd6a1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt @@ -118,7 +118,7 @@ class StatusViewHolder( private fun setTextVisible(expanded: Boolean, content: Spanned, - mentions: Array?, + mentions: List?, emojis: List, listener: LinkListener) { if (expanded) { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt index 7052e8b74..2fdb76293 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt @@ -15,13 +15,12 @@ import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.viewdata.StatusViewData import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Single - import javax.inject.Inject class SearchViewModel @Inject constructor( - mastodonApi: MastodonApi, - private val timelineCases: TimelineCases, - private val accountManager: AccountManager + mastodonApi: MastodonApi, + private val timelineCases: TimelineCases, + private val accountManager: AccountManager ) : RxAwareViewModel() { var currentQuery: String = "" @@ -36,93 +35,109 @@ class SearchViewModel @Inject constructor( val alwaysShowSensitiveMedia = activeAccount?.alwaysShowSensitiveMedia ?: false val alwaysOpenSpoiler = activeAccount?.alwaysOpenSpoiler ?: false - private val statusesRepository = SearchRepository>(mastodonApi) + private val statusesRepository = + SearchRepository>(mastodonApi) private val accountsRepository = SearchRepository(mastodonApi) private val hashtagsRepository = SearchRepository(mastodonApi) private val repoResultStatus = MutableLiveData>>() - val statuses: LiveData>> = repoResultStatus.switchMap { it.pagedList } + val statuses: LiveData>> = + repoResultStatus.switchMap { it.pagedList } val networkStateStatus: LiveData = repoResultStatus.switchMap { it.networkState } - val networkStateStatusRefresh: LiveData = repoResultStatus.switchMap { it.refreshState } + val networkStateStatusRefresh: LiveData = + repoResultStatus.switchMap { it.refreshState } private val repoResultAccount = MutableLiveData>() val accounts: LiveData> = repoResultAccount.switchMap { it.pagedList } - val networkStateAccount: LiveData = repoResultAccount.switchMap { it.networkState } - val networkStateAccountRefresh: LiveData = repoResultAccount.switchMap { it.refreshState } + val networkStateAccount: LiveData = + repoResultAccount.switchMap { it.networkState } + val networkStateAccountRefresh: LiveData = + repoResultAccount.switchMap { it.refreshState } private val repoResultHashTag = MutableLiveData>() val hashtags: LiveData> = repoResultHashTag.switchMap { it.pagedList } - val networkStateHashTag: LiveData = repoResultHashTag.switchMap { it.networkState } - val networkStateHashTagRefresh: LiveData = repoResultHashTag.switchMap { it.refreshState } + val networkStateHashTag: LiveData = + repoResultHashTag.switchMap { it.networkState } + val networkStateHashTagRefresh: LiveData = + repoResultHashTag.switchMap { it.refreshState } private val loadedStatuses = ArrayList>() fun search(query: String) { loadedStatuses.clear() - repoResultStatus.value = statusesRepository.getSearchData(SearchType.Status, query, disposables, initialItems = loadedStatuses) { - it?.statuses?.map { status -> Pair(status, ViewDataUtils.statusToViewData(status, alwaysShowSensitiveMedia, alwaysOpenSpoiler)!!) } - .orEmpty() - .apply { - loadedStatuses.addAll(this) - } - } - repoResultAccount.value = accountsRepository.getSearchData(SearchType.Account, query, disposables) { - it?.accounts.orEmpty() + repoResultStatus.value = statusesRepository.getSearchData( + SearchType.Status, + query, + disposables, + initialItems = loadedStatuses + ) { + it?.statuses?.map { status -> + Pair( + status, + status.toViewData(alwaysShowSensitiveMedia, alwaysOpenSpoiler) + ) + } + .orEmpty() + .apply { + loadedStatuses.addAll(this) + } } + repoResultAccount.value = + accountsRepository.getSearchData(SearchType.Account, query, disposables) { + it?.accounts.orEmpty() + } val hashtagQuery = if (query.startsWith("#")) query else "#$query" repoResultHashTag.value = - hashtagsRepository.getSearchData(SearchType.Hashtag, hashtagQuery, disposables) { - it?.hashtags.orEmpty() - } + hashtagsRepository.getSearchData(SearchType.Hashtag, hashtagQuery, disposables) { + it?.hashtags.orEmpty() + } } fun removeItem(status: Pair) { timelineCases.delete(status.first.id) - .subscribe({ - if (loadedStatuses.remove(status)) - repoResultStatus.value?.refresh?.invoke() - }, { - err -> Log.d(TAG, "Failed to delete status", err) - }) - .autoDispose() + .subscribe({ + if (loadedStatuses.remove(status)) + repoResultStatus.value?.refresh?.invoke() + }, { err -> + Log.d(TAG, "Failed to delete status", err) + }) + .autoDispose() } fun expandedChange(status: Pair, expanded: Boolean) { val idx = loadedStatuses.indexOf(status) if (idx >= 0) { - val newPair = Pair(status.first, StatusViewData.Builder(status.second).setIsExpanded(expanded).createStatusViewData()) + val newPair = Pair(status.first, status.second.copy(isExpanded = expanded)) loadedStatuses[idx] = newPair repoResultStatus.value?.refresh?.invoke() } } fun reblog(status: Pair, reblog: Boolean) { - timelineCases.reblog(status.first, reblog) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { setRebloggedForStatus(status, reblog) }, - { err -> Log.d(TAG, "Failed to reblog status ${status.first.id}", err) } - ) - .autoDispose() + timelineCases.reblog(status.first.id, reblog) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { setRebloggedForStatus(status, reblog) }, + { err -> Log.d(TAG, "Failed to reblog status ${status.first.id}", err) } + ) + .autoDispose() } - private fun setRebloggedForStatus(status: Pair, reblog: Boolean) { + private fun setRebloggedForStatus( + status: Pair, + reblog: Boolean + ) { status.first.reblogged = reblog status.first.reblog?.reblogged = reblog - val idx = loadedStatuses.indexOf(status) - if (idx >= 0) { - val newPair = Pair(status.first, StatusViewData.Builder(status.second).setReblogged(reblog).createStatusViewData()) - loadedStatuses[idx] = newPair - repoResultStatus.value?.refresh?.invoke() - } + repoResultStatus.value?.refresh?.invoke() } fun contentHiddenChange(status: Pair, isShowing: Boolean) { val idx = loadedStatuses.indexOf(status) if (idx >= 0) { - val newPair = Pair(status.first, StatusViewData.Builder(status.second).setIsShowingSensitiveContent(isShowing).createStatusViewData()) + val newPair = Pair(status.first, status.second.copy(isShowingContent = isShowing)) loadedStatuses[idx] = newPair repoResultStatus.value?.refresh?.invoke() } @@ -131,7 +146,7 @@ class SearchViewModel @Inject constructor( fun collapsedChange(status: Pair, collapsed: Boolean) { val idx = loadedStatuses.indexOf(status) if (idx >= 0) { - val newPair = Pair(status.first, StatusViewData.Builder(status.second).setCollapsed(collapsed).createStatusViewData()) + val newPair = Pair(status.first, status.second.copy(isCollapsed = collapsed)) loadedStatuses[idx] = newPair repoResultStatus.value?.refresh?.invoke() } @@ -140,54 +155,46 @@ class SearchViewModel @Inject constructor( fun voteInPoll(status: Pair, choices: MutableList) { val votedPoll = status.first.actionableStatus.poll!!.votedCopy(choices) updateStatus(status, votedPoll) - timelineCases.voteInPoll(status.first, choices) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { newPoll -> updateStatus(status, newPoll) }, - { t -> - Log.d(TAG, - "Failed to vote in poll: ${status.first.id}", t) - } - ) - .autoDispose() + timelineCases.voteInPoll(status.first.id, votedPoll.id, choices) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { newPoll -> updateStatus(status, newPoll) }, + { t -> + Log.d( + TAG, + "Failed to vote in poll: ${status.first.id}", t + ) + } + ) + .autoDispose() } private fun updateStatus(status: Pair, newPoll: Poll) { val idx = loadedStatuses.indexOf(status) if (idx >= 0) { - - val newViewData = StatusViewData.Builder(status.second) - .setPoll(newPoll) - .createStatusViewData() - loadedStatuses[idx] = Pair(status.first, newViewData) + val newStatus = status.first.copy(poll = newPoll) + val newViewData = status.second.copy(status = newStatus) + loadedStatuses[idx] = Pair(newStatus, newViewData) repoResultStatus.value?.refresh?.invoke() } } fun favorite(status: Pair, isFavorited: Boolean) { - val idx = loadedStatuses.indexOf(status) - if (idx >= 0) { - val newPair = Pair(status.first, StatusViewData.Builder(status.second).setFavourited(isFavorited).createStatusViewData()) - loadedStatuses[idx] = newPair - repoResultStatus.value?.refresh?.invoke() - } - timelineCases.favourite(status.first, isFavorited) - .onErrorReturnItem(status.first) - .subscribe() - .autoDispose() + status.first.favourited = isFavorited + repoResultStatus.value?.refresh?.invoke() + timelineCases.favourite(status.first.id, isFavorited) + .onErrorReturnItem(status.first) + .subscribe() + .autoDispose() } fun bookmark(status: Pair, isBookmarked: Boolean) { - val idx = loadedStatuses.indexOf(status) - if (idx >= 0) { - val newPair = Pair(status.first, StatusViewData.Builder(status.second).setBookmarked(isBookmarked).createStatusViewData()) - loadedStatuses[idx] = newPair - repoResultStatus.value?.refresh?.invoke() - } - timelineCases.bookmark(status.first, isBookmarked) - .onErrorReturnItem(status.first) - .subscribe() - .autoDispose() + status.first.bookmarked = isBookmarked + repoResultStatus.value?.refresh?.invoke() + timelineCases.bookmark(status.first.id, isBookmarked) + .onErrorReturnItem(status.first) + .subscribe() + .autoDispose() } fun getAllAccountsOrderedByActive(): List { @@ -199,7 +206,7 @@ class SearchViewModel @Inject constructor( } fun pinAccount(status: Status, isPin: Boolean) { - timelineCases.pin(status, isPin) + timelineCases.pin(status.id, isPin) } fun blockAccount(accountId: String) { @@ -217,14 +224,18 @@ class SearchViewModel @Inject constructor( fun muteConversation(status: Pair, mute: Boolean) { val idx = loadedStatuses.indexOf(status) if (idx >= 0) { - val newPair = Pair(status.first, StatusViewData.Builder(status.second).setMuted(mute).createStatusViewData()) + val newStatus = status.first.copy(muted = mute) + val newPair = Pair( + newStatus, + status.second.copy(status = newStatus) + ) loadedStatuses[idx] = newPair repoResultStatus.value?.refresh?.invoke() } - timelineCases.muteConversation(status.first, mute) - .onErrorReturnItem(status.first) - .subscribe() - .autoDispose() + timelineCases.muteConversation(status.first.id, mute) + .onErrorReturnItem(status.first) + .subscribe() + .autoDispose() } companion object { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchStatusesAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchStatusesAdapter.kt index 0fcee37d1..a40414f93 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchStatusesAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchStatusesAdapter.kt @@ -52,7 +52,7 @@ class SearchStatusesAdapter( val STATUS_COMPARATOR = object : DiffUtil.ItemCallback>() { override fun areContentsTheSame(oldItem: Pair, newItem: Pair): Boolean = - oldItem.second.deepEquals(newItem.second) + oldItem.second == newItem.second override fun areItemsTheSame(oldItem: Pair, newItem: Pair): Boolean = oldItem.second.id == newItem.second.id diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt index a14fc84eb..06013ce42 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt @@ -383,7 +383,7 @@ class SearchStatusesFragment : SearchFragment): Boolean { + private fun accountIsInMentions(account: AccountEntity?, mentions: List): Boolean { return mentions.firstOrNull { account?.username == it.username && account.domain == Uri.parse(it.url)?.host } != null diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/TimelineAdapter.java b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineAdapter.java similarity index 96% rename from app/src/main/java/com/keylesspalace/tusky/adapter/TimelineAdapter.java rename to app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineAdapter.java index 4be922d62..ec6954dea 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/TimelineAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineAdapter.java @@ -13,7 +13,7 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky.adapter; +package com.keylesspalace.tusky.components.timeline; import android.view.LayoutInflater; import android.view.View; @@ -24,6 +24,8 @@ import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.adapter.PlaceholderViewHolder; +import com.keylesspalace.tusky.adapter.StatusViewHolder; import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.util.StatusDisplayOptions; import com.keylesspalace.tusky.viewdata.StatusViewData; diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt new file mode 100644 index 000000000..b0fc5d14d --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt @@ -0,0 +1,563 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.timeline + +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.accessibility.AccessibilityManager +import androidx.core.content.ContextCompat +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.* +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener +import at.connyduck.sparkbutton.helpers.Utils +import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.* +import autodispose2.androidx.lifecycle.autoDispose +import com.keylesspalace.tusky.AccountListActivity +import com.keylesspalace.tusky.AccountListActivity.Companion.newIntent +import com.keylesspalace.tusky.BaseActivity +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.adapter.StatusBaseViewHolder +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.PreferenceChangedEvent +import com.keylesspalace.tusky.databinding.FragmentTimelineBinding +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.di.Injectable +import com.keylesspalace.tusky.di.ViewModelFactory +import com.keylesspalace.tusky.fragment.SFragment +import com.keylesspalace.tusky.interfaces.ActionButtonActivity +import com.keylesspalace.tusky.interfaces.RefreshableFragment +import com.keylesspalace.tusky.interfaces.ReselectableFragment +import com.keylesspalace.tusky.interfaces.StatusActionListener +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.util.* +import com.keylesspalace.tusky.view.EndlessOnScrollListener +import com.keylesspalace.tusky.viewdata.AttachmentViewData +import com.keylesspalace.tusky.viewdata.StatusViewData +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.core.Observable +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +class TimelineFragment : SFragment(), OnRefreshListener, StatusActionListener, Injectable, + ReselectableFragment, RefreshableFragment { + + @Inject + lateinit var viewModelFactory: ViewModelFactory + + @Inject + lateinit var eventHub: EventHub + + @Inject + lateinit var accountManager: AccountManager + + private val viewModel: TimelineViewModel by viewModels { viewModelFactory } + + private val binding by viewBinding(FragmentTimelineBinding::bind) + + private lateinit var adapter: TimelineAdapter + + private var isSwipeToRefreshEnabled = true + + private var eventRegistered = false + + private var layoutManager: LinearLayoutManager? = null + private var scrollListener: EndlessOnScrollListener? = null + private var hideFab = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val arguments = requireArguments() + val kind = TimelineViewModel.Kind.valueOf(arguments.getString(KIND_ARG)!!) + val id: String? = if (kind == TimelineViewModel.Kind.USER || + kind == TimelineViewModel.Kind.USER_PINNED || + kind == TimelineViewModel.Kind.USER_WITH_REPLIES || + kind == TimelineViewModel.Kind.LIST + ) { + arguments.getString(ID_ARG)!! + } else { + null + } + + val tags = if (kind == TimelineViewModel.Kind.TAG) { + arguments.getStringArrayList(HASHTAGS_ARG)!! + } else { + listOf() + } + viewModel.init( + kind, + id, + tags, + ) + + viewModel.viewUpdates + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this) + .subscribe { this.updateViews() } + + isSwipeToRefreshEnabled = arguments.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true) + + val preferences = PreferenceManager.getDefaultSharedPreferences(activity) + val statusDisplayOptions = StatusDisplayOptions( + animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false), + mediaPreviewEnabled = accountManager.activeAccount!!.mediaPreviewEnabled, + useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false), + showBotOverlay = preferences.getBoolean(PrefKeys.SHOW_BOT_OVERLAY, true), + useBlurhash = preferences.getBoolean(PrefKeys.USE_BLURHASH, true), + cardViewMode = if (preferences.getBoolean( + PrefKeys.SHOW_CARDS_IN_TIMELINES, + false + ) + ) CardViewMode.INDENTED else CardViewMode.NONE, + confirmReblogs = preferences.getBoolean(PrefKeys.CONFIRM_REBLOGS, true), + hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false), + animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) + ) + adapter = TimelineAdapter( + dataSource, + statusDisplayOptions, + this + ) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_timeline, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + setupSwipeRefreshLayout() + setupRecyclerView() + updateViews() + viewModel.loadInitial() + } + + private fun setupSwipeRefreshLayout() { + binding.swipeRefreshLayout.isEnabled = isSwipeToRefreshEnabled + binding.swipeRefreshLayout.setOnRefreshListener(this) + binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) + } + + private fun setupRecyclerView() { + binding.recyclerView.setAccessibilityDelegateCompat( + ListStatusAccessibilityDelegate(binding.recyclerView, this) + { pos -> viewModel.statuses.getOrNull(pos) } + ) + binding.recyclerView.setHasFixedSize(true) + layoutManager = LinearLayoutManager(context) + binding.recyclerView.layoutManager = layoutManager + val divider = DividerItemDecoration(context, RecyclerView.VERTICAL) + binding.recyclerView.addItemDecoration(divider) + + // CWs are expanded without animation, buttons animate itself, we don't need it basically + (binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false + binding.recyclerView.adapter = adapter + } + + private fun showEmptyView() { + binding.statusView.show() + binding.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + /* This is delayed until onActivityCreated solely because MainActivity.composeButton isn't + * guaranteed to be set until then. */ + scrollListener = if (actionButtonPresent()) { + /* Use a modified scroll listener that both loads more statuses as it goes, and hides + * the follow button on down-scroll. */ + val preferences = PreferenceManager.getDefaultSharedPreferences(context) + hideFab = preferences.getBoolean("fabHide", false) + object : EndlessOnScrollListener(layoutManager) { + override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(view, dx, dy) + val composeButton = (activity as ActionButtonActivity).actionButton + if (composeButton != null) { + if (hideFab) { + if (dy > 0 && composeButton.isShown) { + composeButton.hide() // hides the button if we're scrolling down + } else if (dy < 0 && !composeButton.isShown) { + composeButton.show() // shows it if we are scrolling up + } + } else if (!composeButton.isShown) { + composeButton.show() + } + } + } + + override fun onLoadMore(totalItemsCount: Int, view: RecyclerView) { + this@TimelineFragment.onLoadMore() + } + } + } else { + // Just use the basic scroll listener to load more statuses. + object : EndlessOnScrollListener(layoutManager) { + override fun onLoadMore(totalItemsCount: Int, view: RecyclerView) { + this@TimelineFragment.onLoadMore() + } + } + }.also { + binding.recyclerView.addOnScrollListener(it) + } + + if (!eventRegistered) { + eventHub.events + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this, Lifecycle.Event.ON_DESTROY) + .subscribe { event -> + when (event) { + is PreferenceChangedEvent -> { + onPreferenceChanged(event.preferenceKey) + } + } + } + eventRegistered = true + } + } + + override fun onRefresh() { + binding.swipeRefreshLayout.isEnabled = isSwipeToRefreshEnabled + binding.statusView.hide() + + viewModel.refresh() + } + + override fun onReply(position: Int) { + val status = viewModel.statuses[position].asStatusOrNull() ?: return + super.reply(status.status) + } + + override fun onReblog(reblog: Boolean, position: Int) { + viewModel.reblog(reblog, position) + } + + override fun onFavourite(favourite: Boolean, position: Int) { + viewModel.favorite(favourite, position) + } + + override fun onBookmark(bookmark: Boolean, position: Int) { + viewModel.bookmark(bookmark, position) + } + + override fun onVoteInPoll(position: Int, choices: List) { + viewModel.voteInPoll(position, choices) + } + + override fun onMore(view: View, position: Int) { + val status = viewModel.statuses[position].asStatusOrNull()?.status ?: return + super.more(status, view, position) + } + + override fun onOpenReblog(position: Int) { + val status = viewModel.statuses[position].asStatusOrNull()?.status ?: return + super.openReblog(status) + } + + override fun onExpandedChange(expanded: Boolean, position: Int) { + viewModel.changeExpanded(expanded, position) + updateViews() + } + + override fun onContentHiddenChange(isShowing: Boolean, position: Int) { + viewModel.changeContentHidden(isShowing, position) + updateViews() + } + + override fun onShowReblogs(position: Int) { + val statusId = viewModel.statuses[position].asStatusOrNull()?.id ?: return + val intent = newIntent(requireContext(), AccountListActivity.Type.REBLOGGED, statusId) + (activity as BaseActivity).startActivityWithSlideInAnimation(intent) + } + + override fun onShowFavs(position: Int) { + val statusId = viewModel.statuses[position].asStatusOrNull()?.id ?: return + val intent = newIntent(requireContext(), AccountListActivity.Type.FAVOURITED, statusId) + (activity as BaseActivity).startActivityWithSlideInAnimation(intent) + } + + override fun onLoadMore(position: Int) { + viewModel.loadGap(position) + } + + override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) { + viewModel.changeContentCollapsed(isCollapsed, position) + } + + override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) { + val status = viewModel.statuses[position].asStatusOrNull() ?: return + super.viewMedia( + attachmentIndex, + AttachmentViewData.list(status.actionable), + view + ) + } + + override fun onViewThread(position: Int) { + val status = viewModel.statuses[position].asStatusOrNull() ?: return + super.viewThread(status.actionable.id, status.actionable.url) + } + + override fun onViewTag(tag: String) { + if (viewModel.kind == TimelineViewModel.Kind.TAG && viewModel.tags.size == 1 && + viewModel.tags.contains(tag) + ) { + // If already viewing a tag page, then ignore any request to view that tag again. + return + } + super.viewTag(tag) + } + + override fun onViewAccount(id: String) { + if ((viewModel.kind == TimelineViewModel.Kind.USER || + viewModel.kind == TimelineViewModel.Kind.USER_WITH_REPLIES) && + viewModel.id == id + ) { + /* If already viewing an account page, then any requests to view that account page + * should be ignored. */ + return + } + super.viewAccount(id) + } + + private fun onPreferenceChanged(key: String) { + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) + when (key) { + PrefKeys.FAB_HIDE -> { + hideFab = sharedPreferences.getBoolean(PrefKeys.FAB_HIDE, false) + } + PrefKeys.MEDIA_PREVIEW_ENABLED -> { + val enabled = accountManager.activeAccount!!.mediaPreviewEnabled + val oldMediaPreviewEnabled = adapter.mediaPreviewEnabled + if (enabled != oldMediaPreviewEnabled) { + adapter.mediaPreviewEnabled = enabled + updateViews() + } + } + } + } + + public override fun removeItem(position: Int) { + viewModel.statuses.removeAt(position) + updateViews() + } + + private fun onLoadMore() { + viewModel.loadMore() + } + + private fun actionButtonPresent(): Boolean { + return viewModel.kind != TimelineViewModel.Kind.TAG && + viewModel.kind != TimelineViewModel.Kind.FAVOURITES && + viewModel.kind != TimelineViewModel.Kind.BOOKMARKS && + activity is ActionButtonActivity + } + + private fun updateViews() { + differ.submitList(viewModel.statuses.toList()) + binding.swipeRefreshLayout.isEnabled = viewModel.failure == null + + if (isAdded) { + binding.swipeRefreshLayout.isRefreshing = viewModel.isRefreshing + binding.progressBar.visible(viewModel.isLoadingInitially) + if (viewModel.failure == null && viewModel.statuses.isEmpty() && !viewModel.isLoadingInitially) { + showEmptyView() + } else { + when (viewModel.failure) { + TimelineViewModel.FailureReason.NETWORK -> { + binding.statusView.show() + binding.statusView.setup( + R.drawable.elephant_offline, + R.string.error_network + ) { + binding.statusView.hide() + viewModel.loadInitial() + } + } + TimelineViewModel.FailureReason.OTHER -> { + binding.statusView.show() + binding.statusView.setup( + R.drawable.elephant_error, + R.string.error_generic + ) { + binding.statusView.hide() + viewModel.loadInitial() + } + } + null -> binding.statusView.hide() + } + } + } + } + + private val listUpdateCallback: ListUpdateCallback = object : ListUpdateCallback { + override fun onInserted(position: Int, count: Int) { + if (isAdded) { + adapter.notifyItemRangeInserted(position, count) + val context = context + // scroll up when new items at the top are loaded while being in the first position + // https://github.com/tuskyapp/Tusky/pull/1905#issuecomment-677819724 + if (position == 0 && context != null && adapter.itemCount != count) { + if (isSwipeToRefreshEnabled) { + binding.recyclerView.scrollBy(0, Utils.dpToPx(context, -30)) + } else binding.recyclerView.scrollToPosition(0) + } + } + } + + override fun onRemoved(position: Int, count: Int) { + adapter.notifyItemRangeRemoved(position, count) + } + + override fun onMoved(fromPosition: Int, toPosition: Int) { + adapter.notifyItemMoved(fromPosition, toPosition) + } + + override fun onChanged(position: Int, count: Int, payload: Any?) { + adapter.notifyItemRangeChanged(position, count, payload) + } + } + private val differ = AsyncListDiffer( + listUpdateCallback, + AsyncDifferConfig.Builder(diffCallback).build() + ) + + private val dataSource: TimelineAdapter.AdapterDataSource = + object : TimelineAdapter.AdapterDataSource { + override fun getItemCount(): Int { + return differ.currentList.size + } + + override fun getItemAt(pos: Int): StatusViewData { + return differ.currentList[pos] + } + } + + private var talkBackWasEnabled = false + + override fun onResume() { + super.onResume() + val a11yManager = + ContextCompat.getSystemService(requireContext(), AccessibilityManager::class.java) + + val wasEnabled = talkBackWasEnabled + talkBackWasEnabled = a11yManager?.isEnabled == true + Log.d(TAG, "talkback was enabled: $wasEnabled, now $talkBackWasEnabled") + if (talkBackWasEnabled && !wasEnabled) { + adapter.notifyDataSetChanged() + } + startUpdateTimestamp() + } + + /** + * Start to update adapter every minute to refresh timestamp + * If setting absoluteTimeView is false + * Auto dispose observable on pause + */ + private fun startUpdateTimestamp() { + val preferences = PreferenceManager.getDefaultSharedPreferences(activity) + val useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false) + if (!useAbsoluteTime) { + Observable.interval(1, TimeUnit.MINUTES) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this, Lifecycle.Event.ON_PAUSE) + .subscribe { updateViews() } + } + } + + override fun onReselect() { + if (isAdded) { + layoutManager!!.scrollToPosition(0) + binding.recyclerView.stopScroll() + scrollListener!!.reset() + } + } + + override fun refreshContent() { + onRefresh() + } + + companion object { + private const val TAG = "TimelineF" // logging tag + private const val KIND_ARG = "kind" + private const val ID_ARG = "id" + private const val HASHTAGS_ARG = "hashtags" + private const val ARG_ENABLE_SWIPE_TO_REFRESH = "enableSwipeToRefresh" + + + fun newInstance( + kind: TimelineViewModel.Kind, + hashtagOrId: String? = null, + enableSwipeToRefresh: Boolean = true + ): TimelineFragment { + val fragment = TimelineFragment() + val arguments = Bundle(3) + arguments.putString(KIND_ARG, kind.name) + arguments.putString(ID_ARG, hashtagOrId) + arguments.putBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, enableSwipeToRefresh) + fragment.arguments = arguments + return fragment + } + + @JvmStatic + fun newHashtagInstance(hashtags: List): TimelineFragment { + val fragment = TimelineFragment() + val arguments = Bundle(3) + arguments.putString(KIND_ARG, TimelineViewModel.Kind.TAG.name) + arguments.putStringArrayList(HASHTAGS_ARG, ArrayList(hashtags)) + arguments.putBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true) + fragment.arguments = arguments + return fragment + } + + + private val diffCallback: DiffUtil.ItemCallback = + object : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: StatusViewData, + newItem: StatusViewData + ): Boolean { + return oldItem.viewDataId == newItem.viewDataId + } + + override fun areContentsTheSame( + oldItem: StatusViewData, + newItem: StatusViewData + ): Boolean { + return false // Items are different always. It allows to refresh timestamp on every view holder update + } + + override fun getChangePayload( + oldItem: StatusViewData, + newItem: StatusViewData + ): Any? { + return if (oldItem === newItem) { + // If items are equal - update timestamp only + listOf(StatusBaseViewHolder.Key.KEY_CREATED) + } else // If items are different - update the whole view holder + null + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineRepository.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineRepository.kt new file mode 100644 index 000000000..dac285593 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineRepository.kt @@ -0,0 +1,413 @@ +package com.keylesspalace.tusky.components.timeline + +import android.text.SpannedString +import androidx.core.text.parseAsHtml +import androidx.core.text.toHtml +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import com.keylesspalace.tusky.db.* +import com.keylesspalace.tusky.entity.* +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.components.timeline.TimelineRequestMode.DISK +import com.keylesspalace.tusky.components.timeline.TimelineRequestMode.NETWORK +import com.keylesspalace.tusky.util.Either +import com.keylesspalace.tusky.util.dec +import com.keylesspalace.tusky.util.inc +import com.keylesspalace.tusky.util.trimTrailingWhitespace +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.schedulers.Schedulers +import java.io.IOException +import java.util.* +import java.util.concurrent.TimeUnit +import kotlin.collections.ArrayList + +data class Placeholder(val id: String) + +typealias TimelineStatus = Either + +enum class TimelineRequestMode { + DISK, NETWORK, ANY +} + +interface TimelineRepository { + fun getStatuses( + maxId: String?, sinceId: String?, sincedIdMinusOne: String?, limit: Int, + requestMode: TimelineRequestMode + ): Single> + + companion object { + val CLEANUP_INTERVAL = TimeUnit.DAYS.toMillis(14) + } +} + +class TimelineRepositoryImpl( + private val timelineDao: TimelineDao, + private val mastodonApi: MastodonApi, + private val accountManager: AccountManager, + private val gson: Gson +) : TimelineRepository { + + init { + this.cleanup() + } + + override fun getStatuses( + maxId: String?, sinceId: String?, sincedIdMinusOne: String?, + limit: Int, requestMode: TimelineRequestMode + ): Single> { + val acc = accountManager.activeAccount ?: throw IllegalStateException() + val accountId = acc.id + + return if (requestMode == DISK) { + this.getStatusesFromDb(accountId, maxId, sinceId, limit) + } else { + getStatusesFromNetwork(maxId, sinceId, sincedIdMinusOne, limit, accountId, requestMode) + } + } + + private fun getStatusesFromNetwork( + maxId: String?, sinceId: String?, + sinceIdMinusOne: String?, limit: Int, + accountId: Long, requestMode: TimelineRequestMode + ): Single> { + return mastodonApi.homeTimeline(maxId, sinceIdMinusOne, limit + 1) + .map { response -> + this.saveStatusesToDb(accountId, response.body().orEmpty(), maxId, sinceId) + } + .flatMap { statuses -> + this.addFromDbIfNeeded(accountId, statuses, maxId, sinceId, limit, requestMode) + } + .onErrorResumeNext { error -> + if (error is IOException && requestMode != NETWORK) { + this.getStatusesFromDb(accountId, maxId, sinceId, limit) + } else { + Single.error(error) + } + } + } + + private fun addFromDbIfNeeded( + accountId: Long, statuses: List>, + maxId: String?, sinceId: String?, limit: Int, + requestMode: TimelineRequestMode + ): Single> { + return if (requestMode != NETWORK && statuses.size < 2) { + val newMaxID = if (statuses.isEmpty()) { + maxId + } else { + statuses.last { it.isRight() }.asRight().id + } + this.getStatusesFromDb(accountId, newMaxID, sinceId, limit) + .map { fromDb -> + // If it's just placeholders and less than limit (so we exhausted both + // db and server at this point) + if (fromDb.size < limit && fromDb.all { !it.isRight() }) { + statuses + } else { + statuses + fromDb + } + } + } else { + Single.just(statuses) + } + } + + private fun getStatusesFromDb( + accountId: Long, maxId: String?, sinceId: String?, + limit: Int + ): Single> { + return timelineDao.getStatusesForAccount(accountId, maxId, sinceId, limit) + .subscribeOn(Schedulers.io()) + .map { statuses -> + statuses.map { it.toStatus() } + } + } + + private fun saveStatusesToDb( + accountId: Long, statuses: List, + maxId: String?, sinceId: String? + ): List> { + var placeholderToInsert: Placeholder? = null + + // Look for overlap + val resultStatuses = if (statuses.isNotEmpty() && sinceId != null) { + val indexOfSince = statuses.indexOfLast { it.id == sinceId } + if (indexOfSince == -1) { + // We didn't find the status which must be there. Add a placeholder + placeholderToInsert = Placeholder(sinceId.inc()) + statuses.mapTo(mutableListOf(), Status::lift) + .apply { + add(Either.Left(placeholderToInsert)) + } + } else { + // There was an overlap. Remove all overlapped statuses. No need for a placeholder. + statuses.mapTo(mutableListOf(), Status::lift) + .apply { + subList(indexOfSince, size).clear() + } + } + } else { + // Just a normal case. + statuses.map(Status::lift) + } + + Single.fromCallable { + + if (statuses.isNotEmpty()) { + timelineDao.deleteRange(accountId, statuses.last().id, statuses.first().id) + } + + for (status in statuses) { + timelineDao.insertInTransaction( + status.toEntity(accountId, gson), + status.account.toEntity(accountId, gson), + status.reblog?.account?.toEntity(accountId, gson) + ) + } + + placeholderToInsert?.let { + timelineDao.insertStatusIfNotThere(placeholderToInsert.toEntity(accountId)) + } + + // If we're loading in the bottom insert placeholder after every load + // (for requests on next launches) but not return it. + if (sinceId == null && statuses.isNotEmpty()) { + timelineDao.insertStatusIfNotThere( + Placeholder(statuses.last().id.dec()).toEntity(accountId) + ) + } + + // There may be placeholders which we thought could be from our TL but they are not + if (statuses.size > 2) { + timelineDao.removeAllPlaceholdersBetween( + accountId, statuses.first().id, + statuses.last().id + ) + } else if (placeholderToInsert == null && maxId != null && sinceId != null) { + timelineDao.removeAllPlaceholdersBetween(accountId, maxId, sinceId) + } + } + .subscribeOn(Schedulers.io()) + .subscribe() + + return resultStatuses + } + + private fun cleanup() { + Schedulers.io().scheduleDirect { + val olderThan = System.currentTimeMillis() - TimelineRepository.CLEANUP_INTERVAL + timelineDao.cleanup(olderThan) + } + } + + private fun TimelineStatusWithAccount.toStatus(): TimelineStatus { + if (this.status.authorServerId == null) { + return Either.Left(Placeholder(this.status.serverId)) + } + + val attachments: ArrayList = gson.fromJson( + status.attachments, + object : TypeToken>() {}.type + ) ?: ArrayList() + val mentions: List = gson.fromJson( + status.mentions, + object : TypeToken>() {}.type + ) ?: listOf() + val application = gson.fromJson(status.application, Status.Application::class.java) + val emojis: List = gson.fromJson( + status.emojis, + object : TypeToken>() {}.type + ) ?: listOf() + val poll: Poll? = gson.fromJson(status.poll, Poll::class.java) + + val reblog = status.reblogServerId?.let { id -> + Status( + id = id, + url = status.url, + account = account.toAccount(gson), + inReplyToId = status.inReplyToId, + inReplyToAccountId = status.inReplyToAccountId, + reblog = null, + content = status.content?.parseAsHtml()?.trimTrailingWhitespace() + ?: SpannedString(""), + createdAt = Date(status.createdAt), + emojis = emojis, + reblogsCount = status.reblogsCount, + favouritesCount = status.favouritesCount, + reblogged = status.reblogged, + favourited = status.favourited, + bookmarked = status.bookmarked, + sensitive = status.sensitive, + spoilerText = status.spoilerText!!, + visibility = status.visibility!!, + attachments = attachments, + mentions = mentions, + application = application, + pinned = false, + muted = status.muted, + poll = poll, + card = null + ) + } + val status = if (reblog != null) { + Status( + id = status.serverId, + url = null, // no url for reblogs + account = this.reblogAccount!!.toAccount(gson), + inReplyToId = null, + inReplyToAccountId = null, + reblog = reblog, + content = SpannedString(""), + createdAt = Date(status.createdAt), // lie but whatever? + emojis = listOf(), + reblogsCount = 0, + favouritesCount = 0, + reblogged = false, + favourited = false, + bookmarked = false, + sensitive = false, + spoilerText = "", + visibility = status.visibility!!, + attachments = ArrayList(), + mentions = listOf(), + application = null, + pinned = false, + muted = status.muted, + poll = null, + card = null + ) + } else { + Status( + id = status.serverId, + url = status.url, + account = account.toAccount(gson), + inReplyToId = status.inReplyToId, + inReplyToAccountId = status.inReplyToAccountId, + reblog = null, + content = status.content?.parseAsHtml()?.trimTrailingWhitespace() + ?: SpannedString(""), + createdAt = Date(status.createdAt), + emojis = emojis, + reblogsCount = status.reblogsCount, + favouritesCount = status.favouritesCount, + reblogged = status.reblogged, + favourited = status.favourited, + bookmarked = status.bookmarked, + sensitive = status.sensitive, + spoilerText = status.spoilerText!!, + visibility = status.visibility!!, + attachments = attachments, + mentions = mentions, + application = application, + pinned = false, + muted = status.muted, + poll = poll, + card = null + ) + } + return Either.Right(status) + } +} + +private val emojisListTypeToken = object : TypeToken>() {} + +fun Account.toEntity(accountId: Long, gson: Gson): TimelineAccountEntity { + return TimelineAccountEntity( + serverId = id, + timelineUserId = accountId, + localUsername = localUsername, + username = username, + displayName = name, + url = url, + avatar = avatar, + emojis = gson.toJson(emojis), + bot = bot + ) +} + +fun TimelineAccountEntity.toAccount(gson: Gson): Account { + return Account( + id = serverId, + localUsername = localUsername, + username = username, + displayName = displayName, + note = SpannedString(""), + url = url, + avatar = avatar, + header = "", + locked = false, + followingCount = 0, + followersCount = 0, + statusesCount = 0, + source = null, + bot = bot, + emojis = gson.fromJson(this.emojis, emojisListTypeToken.type), + fields = null, + moved = null + ) +} + + +fun Placeholder.toEntity(timelineUserId: Long): TimelineStatusEntity { + return TimelineStatusEntity( + serverId = this.id, + url = null, + timelineUserId = timelineUserId, + authorServerId = null, + inReplyToId = null, + inReplyToAccountId = null, + content = null, + createdAt = 0L, + emojis = null, + reblogsCount = 0, + favouritesCount = 0, + reblogged = false, + favourited = false, + bookmarked = false, + sensitive = false, + spoilerText = null, + visibility = null, + attachments = null, + mentions = null, + application = null, + reblogServerId = null, + reblogAccountId = null, + poll = null, + muted = false + ) +} + +fun Status.toEntity( + timelineUserId: Long, + gson: Gson +): TimelineStatusEntity { + val actionable = actionableStatus + return TimelineStatusEntity( + serverId = this.id, + url = actionable.url!!, + timelineUserId = timelineUserId, + authorServerId = actionable.account.id, + inReplyToId = actionable.inReplyToId, + inReplyToAccountId = actionable.inReplyToAccountId, + content = actionable.content.toHtml(), + createdAt = actionable.createdAt.time, + emojis = actionable.emojis.let(gson::toJson), + reblogsCount = actionable.reblogsCount, + favouritesCount = actionable.favouritesCount, + reblogged = actionable.reblogged, + favourited = actionable.favourited, + bookmarked = actionable.bookmarked, + sensitive = actionable.sensitive, + spoilerText = actionable.spoilerText, + visibility = actionable.visibility, + attachments = actionable.attachments.let(gson::toJson), + mentions = actionable.mentions.let(gson::toJson), + application = actionable.application.let(gson::toJson), + reblogServerId = reblog?.id, + reblogAccountId = reblog?.let { this.account.id }, + poll = actionable.poll.let(gson::toJson), + muted = actionable.muted + ) +} + +fun Status.lift(): Either = Either.Right(this) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineViewModel.kt new file mode 100644 index 000000000..49655ad87 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineViewModel.kt @@ -0,0 +1,903 @@ +package com.keylesspalace.tusky.components.timeline + +import android.content.SharedPreferences +import android.util.Log +import androidx.lifecycle.viewModelScope +import com.keylesspalace.tusky.appstore.* +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.entity.Filter +import com.keylesspalace.tusky.entity.Poll +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.network.FilterModel +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.network.TimelineCases +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.util.* +import com.keylesspalace.tusky.viewdata.StatusViewData +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.subjects.PublishSubject +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch +import kotlinx.coroutines.rx3.asFlow +import kotlinx.coroutines.rx3.await +import retrofit2.HttpException +import retrofit2.Response +import java.io.IOException +import javax.inject.Inject + +class TimelineViewModel @Inject constructor( + private val timelineRepo: TimelineRepository, + private val timelineCases: TimelineCases, + private val api: MastodonApi, + private val eventHub: EventHub, + private val accountManager: AccountManager, + private val sharedPreferences: SharedPreferences, + private val filterModel: FilterModel, +) : RxAwareViewModel() { + + enum class FailureReason { + NETWORK, + OTHER, + } + + val viewUpdates: Observable + get() = updateViewSubject + + var kind: Kind = Kind.HOME + private set + + var isLoadingInitially = false + private set + var isRefreshing = false + private set + var bottomLoading = false + private set + var initialUpdateFailed = false + private set + var failure: FailureReason? = null + private set + var id: String? = null + private set + var tags: List = emptyList() + private set + + private var alwaysShowSensitiveMedia = false + private var alwaysOpenSpoilers = false + private var filterRemoveReplies = false + private var filterRemoveReblogs = false + private var didLoadEverythingBottom = false + + private var updateViewSubject = PublishSubject.create() + + /** + * For some timeline kinds we must use LINK headers and not just status ids. + */ + private var nextId: String? = null + + val statuses = mutableListOf() + + fun init( + kind: Kind, + id: String?, + tags: List + ) { + this.kind = kind + this.id = id + this.tags = tags + + if (kind == Kind.HOME) { + filterRemoveReplies = + !sharedPreferences.getBoolean(PrefKeys.TAB_FILTER_HOME_REPLIES, true) + filterRemoveReblogs = + !sharedPreferences.getBoolean(PrefKeys.TAB_FILTER_HOME_BOOSTS, true) + } + this.alwaysShowSensitiveMedia = accountManager.activeAccount!!.alwaysShowSensitiveMedia + this.alwaysOpenSpoilers = accountManager.activeAccount!!.alwaysOpenSpoiler + + viewModelScope.launch { + eventHub.events + .asFlow() + .collect { event -> handleEvent(event) } + } + + reloadFilters() + } + + private suspend fun updateCurrent() { + val topId = statuses.firstIsInstanceOrNull()?.id ?: return + // Request statuses including current top to refresh all of them + val topIdMinusOne = topId.inc() + val statuses = try { + loadStatuses( + maxId = topIdMinusOne, + sinceId = null, + sinceIdMinusOne = null, + TimelineRequestMode.NETWORK, + ) + } catch (t: Exception) { + initialUpdateFailed = true + if (isExpectedRequestException(t)) { + Log.d(TAG, "Failed updating timeline", t) + triggerViewUpdate() + return + } else { + throw t + } + } + + initialUpdateFailed = false + + // When cached timeline is too old, we would replace it with nothing + if (statuses.isNotEmpty()) { + val mutableStatuses = statuses.toMutableList() + filterStatuses(mutableStatuses) + this.statuses.removeAll { item -> + val id = when (item) { + is StatusViewData.Concrete -> item.id + is StatusViewData.Placeholder -> item.id + } + + id == topId || id.isLessThan(topId) + } + this.statuses.addAll(mutableStatuses.toViewData()) + } + triggerViewUpdate() + } + + private fun isExpectedRequestException(t: Exception) = t is IOException || t is HttpException + + fun refresh(): Job { + return viewModelScope.launch { + isRefreshing = true + failure = null + triggerViewUpdate() + + try { + if (initialUpdateFailed) updateCurrent() + loadAbove() + } catch (e: Exception) { + if (isExpectedRequestException(e)) { + Log.e(TAG, "Failed to refresh", e) + } else { + throw e + } + } finally { + isRefreshing = false + triggerViewUpdate() + } + } + } + + /** When reaching the end of list. WIll optionally show spinner in the end of list. */ + fun loadMore(): Job { + return viewModelScope.launch { + if (didLoadEverythingBottom || bottomLoading) { + return@launch + } + if (statuses.isEmpty()) { + loadInitial().join() + return@launch + } + setLoadingPlaceholderBelow() + + val bottomId: String? = + if (kind == Kind.FAVOURITES || kind == Kind.BOOKMARKS) { + nextId + } else { + statuses.lastOrNull { it is StatusViewData.Concrete } + ?.let { (it as StatusViewData.Concrete).id } + } + try { + loadBelow(bottomId) + } catch (e: Exception) { + if (isExpectedRequestException(e)) { + if (statuses.lastOrNull() is StatusViewData.Placeholder) { + statuses.removeAt(statuses.lastIndex) + } + } else { + throw e + } + } finally { + triggerViewUpdate() + } + } + } + + /** Load and insert statuses below the [bottomId]. Does not indicate progress. */ + private suspend fun loadBelow(bottomId: String?) { + this.bottomLoading = true + try { + val statuses = loadStatuses( + bottomId, + null, + null, + TimelineRequestMode.ANY + ) + addStatusesBelow(statuses.toMutableList()) + } finally { + this.bottomLoading = false + } + } + + private fun setLoadingPlaceholderBelow() { + val last = statuses.last() + val placeholder: StatusViewData.Placeholder + if (last is StatusViewData.Concrete) { + val placeholderId = last.id.dec() + placeholder = StatusViewData.Placeholder(placeholderId, true) + statuses.add(placeholder) + } else { + placeholder = last as StatusViewData.Placeholder + } + statuses[statuses.lastIndex] = placeholder + triggerViewUpdate() + } + + private fun addStatusesBelow(statuses: MutableList>) { + val fullFetch = isFullFetch(statuses) + // Remove placeholder in the bottom if it's there + if (this.statuses.isNotEmpty() + && this.statuses.last() !is StatusViewData.Concrete + ) { + this.statuses.removeAt(this.statuses.lastIndex) + } + + // Removing placeholder if it's the last one from the cache + if (statuses.isNotEmpty() && !statuses[statuses.size - 1].isRight()) { + statuses.removeAt(statuses.size - 1) + } + + val oldSize = this.statuses.size + if (this.statuses.isNotEmpty()) { + addItems(statuses) + } else { + updateStatuses(statuses, fullFetch) + } + if (this.statuses.size == oldSize) { + // This may be a brittle check but seems like it works + // Can we check it using headers somehow? Do all server support them? + didLoadEverythingBottom = true + } + } + + fun loadGap(position: Int): Job { + return viewModelScope.launch { + //check bounds before accessing list, + if (statuses.size < position || position <= 0) { + Log.e(TAG, "Wrong gap position: $position") + return@launch + } + + val fromStatus = statuses[position - 1].asStatusOrNull() + val toStatus = statuses[position + 1].asStatusOrNull() + val toMinusOne = statuses.getOrNull(position + 2)?.asStatusOrNull()?.id + if (fromStatus == null || toStatus == null) { + Log.e(TAG, "Failed to load more at $position, wrong placeholder position") + return@launch + } + val placeholder = statuses[position].asPlaceholderOrNull() ?: run { + Log.e(TAG, "Not a placeholder at $position") + return@launch + } + + val newViewData: StatusViewData = StatusViewData.Placeholder(placeholder.id, true) + statuses[position] = newViewData + triggerViewUpdate() + + try { + val statuses = loadStatuses( + fromStatus.id, + toStatus.id, + toMinusOne, + TimelineRequestMode.NETWORK + ) + replacePlaceholderWithStatuses( + statuses.toMutableList(), + isFullFetch(statuses), + position + ) + } catch (t: Exception) { + if (isExpectedRequestException(t)) { + Log.e(TAG, "Failed to load gap", t) + if (statuses[position] is StatusViewData.Placeholder) { + statuses[position] = StatusViewData.Placeholder(placeholder.id, false) + } + } else { + throw t + } + } + } + } + + fun reblog(reblog: Boolean, position: Int): Job = viewModelScope.launch { + val status = statuses[position].asStatusOrNull() ?: return@launch + try { + timelineCases.reblog(status.id, reblog).await() + } catch (t: Exception) { + ifExpected(t) { + Log.d(TAG, "Failed to reblog status " + status.id, t) + + } + } + } + + fun favorite(favorite: Boolean, position: Int): Job = viewModelScope.launch { + val status = statuses[position].asStatusOrNull() ?: return@launch + + try { + timelineCases.favourite(status.id, favorite).await() + } catch (t: Exception) { + ifExpected(t) { + Log.d(TAG, "Failed to favourite status " + status.id, t) + } + } + } + + fun bookmark(bookmark: Boolean, position: Int): Job = viewModelScope.launch { + val status = statuses[position].asStatusOrNull() ?: return@launch + try { + timelineCases.bookmark(status.id, bookmark).await() + } catch (t: Exception) { + ifExpected(t) { + Log.d(TAG, "Failed to favourite status " + status.id, t) + } + } + } + + fun voteInPoll(position: Int, choices: List): Job = viewModelScope.launch { + val status = statuses[position].asStatusOrNull() ?: return@launch + + val poll = status.status.poll ?: run { + Log.w(TAG, "No poll on status ${status.id}") + return@launch + } + + val votedPoll = poll.votedCopy(choices) + updatePoll(status, votedPoll) + + try { + timelineCases.voteInPoll(status.id, poll.id, choices).await() + } catch (t: Exception) { + ifExpected(t) { + Log.d(TAG, "Failed to vote in poll: " + status.id, t) + } + } + } + + private fun updatePoll( + status: StatusViewData.Concrete, + newPoll: Poll + ) { + updateStatusById(status.id) { + it.copy(status = it.status.copy(poll = newPoll)) + } + } + + fun changeExpanded(expanded: Boolean, position: Int) { + updateStatusAt(position) { it.copy(isExpanded = expanded) } + triggerViewUpdate() + } + + fun changeContentHidden(isShowing: Boolean, position: Int) { + updateStatusAt(position) { it.copy(isShowingContent = isShowing) } + triggerViewUpdate() + } + + fun changeContentCollapsed(isCollapsed: Boolean, position: Int) { + updateStatusAt(position) { it.copy(isCollapsed = isCollapsed) } + triggerViewUpdate() + } + + private fun removeAllByAccountId(accountId: String) { + statuses.removeAll { vm -> + val status = vm.asStatusOrNull()?.status ?: return@removeAll false + status.account.id == accountId || status.actionableStatus.account.id == accountId + } + } + + private fun removeAllByInstance(instance: String) { + statuses.removeAll { vd -> + val status = vd.asStatusOrNull()?.status ?: return@removeAll false + LinkHelper.getDomain(status.account.url) == instance + } + } + + private fun triggerViewUpdate() { + this.updateViewSubject.onNext(Unit) + } + + private suspend fun loadStatuses( + maxId: String?, + sinceId: String?, + sinceIdMinusOne: String?, + homeMode: TimelineRequestMode, + ): List { + val statuses = if (kind == Kind.HOME) { + timelineRepo.getStatuses(maxId, sinceId, sinceIdMinusOne, LOAD_AT_ONCE, homeMode) + .await() + } else { + val response = fetchStatusesForKind(maxId, sinceId, LOAD_AT_ONCE).await() + if (response.isSuccessful) { + val newNextId = extractNextId(response) + if (newNextId != null) { + // when we reach the bottom of the list, we won't have a new link. If + // we blindly write `null` here we will start loading from the top + // again. + nextId = newNextId + } + response.body()?.map { Either.Right(it) } ?: listOf() + } else { + throw Exception(response.message()) + } + } + + filterStatuses(statuses.toMutableList()) + + return statuses + } + + private fun updateStatuses( + newStatuses: MutableList>, + fullFetch: Boolean + ) { + if (statuses.isEmpty()) { + statuses.addAll(newStatuses.toViewData()) + } else { + val lastOfNew = newStatuses.lastOrNull() + val index = if (lastOfNew == null) -1 + else statuses.indexOfLast { it.asStatusOrNull()?.id === lastOfNew.asRightOrNull()?.id } + if (index >= 0) { + statuses.subList(0, index).clear() + } + + val newIndex = + newStatuses.indexOfFirst { + it.isRight() && it.asRight().id == (statuses[0] as? StatusViewData.Concrete)?.id + } + if (newIndex == -1) { + if (index == -1 && fullFetch) { + val placeholderId = + newStatuses.last { status -> status.isRight() }.asRight().id.inc() + newStatuses.add(Either.Left(Placeholder(placeholderId))) + } + statuses.addAll(0, newStatuses.toViewData()) + } else { + statuses.addAll(0, newStatuses.subList(0, newIndex).toViewData()) + } + } + // Remove all consecutive placeholders + removeConsecutivePlaceholders() + this.triggerViewUpdate() + } + + private fun filterViewData(viewData: MutableList) { + viewData.removeAll { vd -> + vd.asStatusOrNull()?.status?.let { shouldFilterStatus(it) } ?: false + } + } + + private fun filterStatuses(statuses: MutableList>) { + statuses.removeAll { status -> + status.asRightOrNull()?.let { shouldFilterStatus(it) } ?: false + } + } + + private fun shouldFilterStatus(status: Status): Boolean { + return status.inReplyToId != null && filterRemoveReplies + || status.reblog != null && filterRemoveReblogs + || filterModel.shouldFilterStatus(status.actionableStatus) + } + + private fun extractNextId(response: Response<*>): String? { + val linkHeader = response.headers()["Link"] ?: return null + val links = HttpHeaderLink.parse(linkHeader) + val nextHeader = HttpHeaderLink.findByRelationType(links, "next") ?: return null + val nextLink = nextHeader.uri ?: return null + return nextLink.getQueryParameter("max_id") + } + + private suspend fun tryCache() { + // Request timeline from disk to make it quick, then replace it with timeline from + // the server to update it + val statuses = + timelineRepo.getStatuses(null, null, null, LOAD_AT_ONCE, TimelineRequestMode.DISK) + .await() + + val mutableStatusResponse = statuses.toMutableList() + filterStatuses(mutableStatusResponse) + if (statuses.size > 1) { + clearPlaceholdersForResponse(mutableStatusResponse) + this.statuses.clear() + this.statuses.addAll(statuses.toViewData()) + } + } + + fun loadInitial(): Job { + return viewModelScope.launch { + if (statuses.isNotEmpty() || initialUpdateFailed || isLoadingInitially) { + return@launch + } + isLoadingInitially = true + failure = null + triggerViewUpdate() + + if (kind == Kind.HOME) { + tryCache() + isLoadingInitially = statuses.isEmpty() + updateCurrent() + try { + loadAbove() + } catch (e: Exception) { + Log.e(TAG, "Loading above failed", e) + if (!isExpectedRequestException(e)) { + throw e + } else if (statuses.isEmpty()) { + failure = + if (e is IOException) FailureReason.NETWORK + else FailureReason.OTHER + } + } finally { + isLoadingInitially = false + triggerViewUpdate() + } + } else { + try { + loadBelow(null) + } catch (e: IOException) { + failure = FailureReason.NETWORK + } catch (e: HttpException) { + failure = FailureReason.OTHER + } finally { + isLoadingInitially = false + triggerViewUpdate() + } + } + } + } + + private suspend fun loadAbove() { + var firstOrNull: String? = null + var secondOrNull: String? = null + for (i in statuses.indices) { + val status = statuses[i].asStatusOrNull() ?: continue + firstOrNull = status.id + secondOrNull = statuses.getOrNull(i + 1)?.asStatusOrNull()?.id + break + } + + try { + if (firstOrNull != null) { + triggerViewUpdate() + + val statuses = loadStatuses( + maxId = null, + sinceId = firstOrNull, + sinceIdMinusOne = secondOrNull, + homeMode = TimelineRequestMode.NETWORK + ) + + val fullFetch = isFullFetch(statuses) + updateStatuses(statuses.toMutableList(), fullFetch) + } else { + loadBelow(null) + } + } finally { + triggerViewUpdate() + } + } + + private fun isFullFetch(statuses: List) = statuses.size >= LOAD_AT_ONCE + + private fun fullyRefresh(): Job { + this.statuses.clear() + return loadInitial() + } + + private fun fetchStatusesForKind( + fromId: String?, + uptoId: String?, + limit: Int + ): Single>> { + return when (kind) { + Kind.HOME -> api.homeTimeline(fromId, uptoId, limit) + Kind.PUBLIC_FEDERATED -> api.publicTimeline(null, fromId, uptoId, limit) + Kind.PUBLIC_LOCAL -> api.publicTimeline(true, fromId, uptoId, limit) + Kind.TAG -> { + val firstHashtag = tags[0] + val additionalHashtags = tags.subList(1, tags.size) + api.hashtagTimeline(firstHashtag, additionalHashtags, null, fromId, uptoId, limit) + } + Kind.USER -> api.accountStatuses( + id!!, + fromId, + uptoId, + limit, + excludeReplies = true, + onlyMedia = null, + pinned = null + ) + Kind.USER_PINNED -> api.accountStatuses( + id!!, + fromId, + uptoId, + limit, + excludeReplies = null, + onlyMedia = null, + pinned = true + ) + Kind.USER_WITH_REPLIES -> api.accountStatuses( + id!!, + fromId, + uptoId, + limit, + excludeReplies = null, + onlyMedia = null, + pinned = null + ) + Kind.FAVOURITES -> api.favourites(fromId, uptoId, limit) + Kind.BOOKMARKS -> api.bookmarks(fromId, uptoId, limit) + Kind.LIST -> api.listTimeline(id!!, fromId, uptoId, limit) + } + } + + private fun replacePlaceholderWithStatuses( + newStatuses: MutableList>, + fullFetch: Boolean, pos: Int + ) { + val placeholder = statuses[pos] + if (placeholder is StatusViewData.Placeholder) { + statuses.removeAt(pos) + } + if (newStatuses.isEmpty()) { + return + } + val newViewData = newStatuses + .toViewData() + .toMutableList() + + if (fullFetch) { + newViewData.add(placeholder) + } + statuses.addAll(pos, newViewData) + removeConsecutivePlaceholders() + triggerViewUpdate() + } + + private fun removeConsecutivePlaceholders() { + for (i in 0 until statuses.size - 1) { + if (statuses[i] is StatusViewData.Placeholder && + statuses[i + 1] is StatusViewData.Placeholder + ) { + statuses.removeAt(i) + } + } + } + + private fun addItems(newStatuses: List>) { + if (newStatuses.isEmpty()) { + return + } + statuses.addAll(newStatuses.toViewData()) + removeConsecutivePlaceholders() + } + + /** + * For certain requests we don't want to see placeholders, they will be removed some other way + */ + private fun clearPlaceholdersForResponse(statuses: MutableList>) { + statuses.removeAll { status -> status.isLeft() } + } + + private fun handleReblogEvent(reblogEvent: ReblogEvent) { + updateStatusById(reblogEvent.statusId) { + it.copy(status = it.status.copy(reblogged = reblogEvent.reblog)) + } + } + + private fun handleFavEvent(favEvent: FavoriteEvent) { + updateStatusById(favEvent.statusId) { + it.copy(status = it.status.copy(favourited = favEvent.favourite)) + } + } + + private fun handleBookmarkEvent(bookmarkEvent: BookmarkEvent) { + updateStatusById(bookmarkEvent.statusId) { + it.copy(status = it.status.copy(bookmarked = bookmarkEvent.bookmark)) + } + } + + private fun handlePinEvent(pinEvent: PinEvent) { + updateStatusById(pinEvent.statusId) { + it.copy(status = it.status.copy(pinned = pinEvent.pinned)) + } + } + + private fun handleStatusComposeEvent(status: Status) { + when (kind) { + Kind.HOME, Kind.PUBLIC_FEDERATED, Kind.PUBLIC_LOCAL -> refresh() + Kind.USER, Kind.USER_WITH_REPLIES -> if (status.account.id == id) { + refresh() + } else { + return + } + Kind.TAG, Kind.FAVOURITES, Kind.LIST, Kind.BOOKMARKS, Kind.USER_PINNED -> return + } + } + + private fun deleteStatusById(id: String) { + for (i in statuses.indices) { + val either = statuses[i] + if (either.asStatusOrNull()?.id == id) { + statuses.removeAt(i) + break + } + } + } + + private fun onPreferenceChanged(key: String) { + when (key) { + PrefKeys.TAB_FILTER_HOME_REPLIES -> { + val filter = sharedPreferences.getBoolean(PrefKeys.TAB_FILTER_HOME_REPLIES, true) + val oldRemoveReplies = filterRemoveReplies + filterRemoveReplies = kind == Kind.HOME && !filter + if (statuses.isNotEmpty() && oldRemoveReplies != filterRemoveReplies) { + fullyRefresh() + } + } + PrefKeys.TAB_FILTER_HOME_BOOSTS -> { + val filter = sharedPreferences.getBoolean(PrefKeys.TAB_FILTER_HOME_BOOSTS, true) + val oldRemoveReblogs = filterRemoveReblogs + filterRemoveReblogs = kind == Kind.HOME && !filter + if (statuses.isNotEmpty() && oldRemoveReblogs != filterRemoveReblogs) { + fullyRefresh() + } + } + Filter.HOME, Filter.NOTIFICATIONS, Filter.THREAD, Filter.PUBLIC, Filter.ACCOUNT -> { + if (filterContextMatchesKind(kind, listOf(key))) { + reloadFilters() + } + } + PrefKeys.ALWAYS_SHOW_SENSITIVE_MEDIA -> { + // it is ok if only newly loaded statuses are affected, no need to fully refresh + alwaysShowSensitiveMedia = + accountManager.activeAccount!!.alwaysShowSensitiveMedia + } + } + } + + // public for now + fun filterContextMatchesKind( + kind: Kind, + filterContext: List + ): Boolean { + // home, notifications, public, thread + return when (kind) { + Kind.HOME, Kind.LIST -> filterContext.contains( + Filter.HOME + ) + Kind.PUBLIC_FEDERATED, Kind.PUBLIC_LOCAL, Kind.TAG -> filterContext.contains( + Filter.PUBLIC + ) + Kind.FAVOURITES -> filterContext.contains(Filter.PUBLIC) || filterContext.contains( + Filter.NOTIFICATIONS + ) + Kind.USER, Kind.USER_WITH_REPLIES, Kind.USER_PINNED -> filterContext.contains( + Filter.ACCOUNT + ) + else -> false + } + } + + private fun handleEvent(event: Event) { + when (event) { + is FavoriteEvent -> handleFavEvent(event) + is ReblogEvent -> handleReblogEvent(event) + is BookmarkEvent -> handleBookmarkEvent(event) + is PinEvent -> handlePinEvent(event) + is MuteConversationEvent -> fullyRefresh() + is UnfollowEvent -> { + if (kind == Kind.HOME) { + val id = event.accountId + removeAllByAccountId(id) + } + } + is BlockEvent -> { + if (kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && kind != Kind.USER_PINNED) { + val id = event.accountId + removeAllByAccountId(id) + } + } + is MuteEvent -> { + if (kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && kind != Kind.USER_PINNED) { + val id = event.accountId + removeAllByAccountId(id) + } + } + is DomainMuteEvent -> { + if (kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && kind != Kind.USER_PINNED) { + val instance = event.instance + removeAllByInstance(instance) + } + } + is StatusDeletedEvent -> { + if (kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && kind != Kind.USER_PINNED) { + val id = event.statusId + deleteStatusById(id) + } + } + is StatusComposedEvent -> { + val status = event.status + handleStatusComposeEvent(status) + } + is PreferenceChangedEvent -> { + onPreferenceChanged(event.preferenceKey) + } + } + } + + private inline fun updateStatusById( + id: String, + updater: (StatusViewData.Concrete) -> StatusViewData.Concrete + ) { + val pos = statuses.indexOfFirst { it.asStatusOrNull()?.id == id } + if (pos == -1) return + updateStatusAt(pos, updater) + } + + private inline fun updateStatusAt( + position: Int, + updater: (StatusViewData.Concrete) -> StatusViewData.Concrete + ) { + val status = statuses.getOrNull(position)?.asStatusOrNull() ?: return + statuses[position] = updater(status) + triggerViewUpdate() + } + + private fun List.toViewData(): List = this.map { + when (it) { + is Either.Right -> it.value.toViewData( + alwaysShowSensitiveMedia, + alwaysOpenSpoilers + ) + is Either.Left -> StatusViewData.Placeholder(it.value.id, false) + } + } + + private fun reloadFilters() { + viewModelScope.launch { + val filters = try { + api.getFilters().await() + } catch (t: Exception) { + Log.e(TAG, "Failed to fetch filters", t) + return@launch + } + filterModel.initWithFilters(filters.filter { + filterContextMatchesKind(kind, it.context) + }) + filterViewData(this@TimelineViewModel.statuses) + } + } + + private inline fun ifExpected( + t: Exception, + cb: () -> Unit + ) { + if (isExpectedRequestException(t)) { + cb() + } else { + throw t + } + } + + + companion object { + private const val TAG = "TimelineVM" + internal const val LOAD_AT_ONCE = 30 + } + + enum class Kind { + HOME, PUBLIC_LOCAL, PUBLIC_FEDERATED, TAG, USER, USER_PINNED, USER_WITH_REPLIES, FAVOURITES, LIST, BOOKMARKS + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt b/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt index b81ad63e2..48792a1be 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt @@ -105,13 +105,13 @@ class Converters @Inject constructor ( } @TypeConverter - fun mentionArrayToJson(mentionArray: Array?): String? { + fun mentionListToJson(mentionArray: List?): String? { return gson.toJson(mentionArray) } @TypeConverter - fun jsonToMentionArray(mentionListJson: String?): Array? { - return gson.fromJson(mentionListJson, object : TypeToken>() {}.type) + fun jsonToMentionArray(mentionListJson: String?): List? { + return gson.fromJson(mentionListJson, object : TypeToken>() {}.type) } @TypeConverter diff --git a/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt index a1a8c8fee..16ed59cc2 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt @@ -29,6 +29,7 @@ import com.keylesspalace.tusky.components.search.fragments.SearchAccountsFragmen import com.keylesspalace.tusky.components.search.fragments.SearchHashtagsFragment import com.keylesspalace.tusky.components.search.fragments.SearchStatusesFragment import com.keylesspalace.tusky.components.preference.PreferencesFragment +import com.keylesspalace.tusky.components.timeline.TimelineFragment import dagger.Module import dagger.android.ContributesAndroidInjector diff --git a/app/src/main/java/com/keylesspalace/tusky/di/RepositoryModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/RepositoryModule.kt index af8cfb886..5086c752d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/RepositoryModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/RepositoryModule.kt @@ -4,8 +4,8 @@ import com.google.gson.Gson import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.repository.TimelineRepository -import com.keylesspalace.tusky.repository.TimelineRepositoryImpl +import com.keylesspalace.tusky.components.timeline.TimelineRepository +import com.keylesspalace.tusky.components.timeline.TimelineRepositoryImpl import dagger.Module import dagger.Provides diff --git a/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt b/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt index ce83deda8..a71817ecb 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt @@ -11,6 +11,8 @@ import com.keylesspalace.tusky.components.drafts.DraftsViewModel import com.keylesspalace.tusky.components.report.ReportViewModel import com.keylesspalace.tusky.components.scheduled.ScheduledTootViewModel import com.keylesspalace.tusky.components.search.SearchViewModel +import com.keylesspalace.tusky.components.timeline.TimelineFragment +import com.keylesspalace.tusky.components.timeline.TimelineViewModel import com.keylesspalace.tusky.viewmodel.AccountViewModel import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel import com.keylesspalace.tusky.viewmodel.EditProfileViewModel @@ -97,5 +99,10 @@ abstract class ViewModelModule { @ViewModelKey(DraftsViewModel::class) internal abstract fun draftsViewModel(viewModel: DraftsViewModel): ViewModel + @Binds + @IntoMap + @ViewModelKey(TimelineViewModel::class) + internal abstract fun timelineViewModel(viewModel: TimelineViewModel): ViewModel + //Add more ViewModels here } diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt index 0dbefd61b..cb4ce3cbd 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt @@ -22,10 +22,11 @@ import com.google.gson.JsonParseException import com.google.gson.annotations.JsonAdapter data class Notification( - val type: Type, - val id: String, - val account: Account, - val status: Status?) { + val type: Type, + val id: String, + val account: Account, + val status: Status? +) { @JsonAdapter(NotificationTypeAdapter::class) enum class Type(val presentation: String) { @@ -71,18 +72,25 @@ data class Notification( class NotificationTypeAdapter : JsonDeserializer { @Throws(JsonParseException::class) - override fun deserialize(json: JsonElement, typeOfT: java.lang.reflect.Type, context: JsonDeserializationContext): Type { + override fun deserialize( + json: JsonElement, + typeOfT: java.lang.reflect.Type, + context: JsonDeserializationContext + ): Type { return Type.byString(json.asString) } } - + + /** Helper for Java */ + fun copyWithStatus(status: Status?): Notification = copy(status = status) + // for Pleroma compatibility that uses Mention type - fun rewriteToStatusTypeIfNeeded(accountId: String) : Notification { + fun rewriteToStatusTypeIfNeeded(accountId: String): Notification { if (type == Type.MENTION && status != null) { return if (status.mentions.any { - it.id == accountId - }) this else copy(type = Type.STATUS) + it.id == accountId + }) this else copy(type = Type.STATUS) } return this } diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt index aee30e8d7..728cc1b40 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt @@ -22,8 +22,8 @@ import com.google.gson.annotations.SerializedName import java.util.* data class Status( - var id: String, - var url: String?, // not present if it's reblog + val id: String, + val url: String?, // not present if it's reblog val account: Account, @SerializedName("in_reply_to_id") var inReplyToId: String?, @SerializedName("in_reply_to_account_id") val inReplyToAccountId: String?, @@ -40,10 +40,10 @@ data class Status( @SerializedName("spoiler_text") val spoilerText: String, val visibility: Visibility, @SerializedName("media_attachments") var attachments: ArrayList, - val mentions: Array, + val mentions: List, val application: Application?, - var pinned: Boolean?, - var muted: Boolean?, + val pinned: Boolean?, + val muted: Boolean?, val poll: Poll?, val card: Card? ) { @@ -54,6 +54,11 @@ data class Status( val actionableStatus: Status get() = reblog ?: this + /** Helper for Java */ + fun copyWithPoll(poll: Poll?): Status = copy(poll = poll) + + /** Helper for Java */ + fun copyWithPinned(pinned: Boolean): Status = copy(pinned = pinned) enum class Visibility(val num: Int) { UNKNOWN(0), diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java index deec5888f..89f1efcd9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java @@ -58,6 +58,7 @@ import com.keylesspalace.tusky.appstore.BlockEvent; import com.keylesspalace.tusky.appstore.BookmarkEvent; import com.keylesspalace.tusky.appstore.EventHub; import com.keylesspalace.tusky.appstore.FavoriteEvent; +import com.keylesspalace.tusky.appstore.PinEvent; import com.keylesspalace.tusky.appstore.PreferenceChangedEvent; import com.keylesspalace.tusky.appstore.ReblogEvent; import com.keylesspalace.tusky.db.AccountEntity; @@ -83,6 +84,7 @@ import com.keylesspalace.tusky.util.StatusDisplayOptions; import com.keylesspalace.tusky.util.ViewDataUtils; import com.keylesspalace.tusky.view.BackgroundMessageView; import com.keylesspalace.tusky.view.EndlessOnScrollListener; +import com.keylesspalace.tusky.viewdata.AttachmentViewData; import com.keylesspalace.tusky.viewdata.NotificationViewData; import com.keylesspalace.tusky.viewdata.StatusViewData; @@ -92,6 +94,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Locale; import java.util.Objects; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -311,35 +314,6 @@ public class NotificationsFragment extends SFragment implements .show(); } - private void handleFavEvent(FavoriteEvent event) { - Pair posAndNotification = - findReplyPosition(event.getStatusId()); - if (posAndNotification == null) return; - //noinspection ConstantConditions - setFavouriteForStatus(posAndNotification.first, - posAndNotification.second.getStatus(), - event.getFavourite()); - } - - private void handleBookmarkEvent(BookmarkEvent event) { - Pair posAndNotification = - findReplyPosition(event.getStatusId()); - if (posAndNotification == null) return; - //noinspection ConstantConditions - setBookmarkForStatus(posAndNotification.first, - posAndNotification.second.getStatus(), - event.getBookmark()); - } - - private void handleReblogEvent(ReblogEvent event) { - Pair posAndNotification = findReplyPosition(event.getStatusId()); - if (posAndNotification == null) return; - //noinspection ConstantConditions - setReblogForStatus(posAndNotification.first, - posAndNotification.second.getStatus(), - event.getReblog()); - } - @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); @@ -386,11 +360,13 @@ public class NotificationsFragment extends SFragment implements .to(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) .subscribe(event -> { if (event instanceof FavoriteEvent) { - handleFavEvent((FavoriteEvent) event); + setFavouriteForStatus(((FavoriteEvent) event).getStatusId(), ((FavoriteEvent) event).getFavourite()); } else if (event instanceof BookmarkEvent) { - handleBookmarkEvent((BookmarkEvent) event); + setBookmarkForStatus(((BookmarkEvent) event).getStatusId(), ((BookmarkEvent) event).getBookmark()); } else if (event instanceof ReblogEvent) { - handleReblogEvent((ReblogEvent) event); + setReblogForStatus(((ReblogEvent) event).getStatusId(), ((ReblogEvent) event).getReblog()); + } else if (event instanceof PinEvent) { + setPinForStatus(((PinEvent) event).getStatusId(), ((PinEvent) event).getPinned()); } else if (event instanceof BlockEvent) { removeAllByAccountId(((BlockEvent) event).getAccountId()); } else if (event instanceof PreferenceChangedEvent) { @@ -423,34 +399,21 @@ public class NotificationsFragment extends SFragment implements final Notification notification = notifications.get(position).asRight(); final Status status = notification.getStatus(); Objects.requireNonNull(status, "Reblog on notification without status"); - timelineCases.reblog(status, reblog) + timelineCases.reblog(status.getId(), reblog) .observeOn(AndroidSchedulers.mainThread()) .to(autoDisposable(from(this))) .subscribe( - (newStatus) -> setReblogForStatus(position, status, reblog), + (newStatus) -> setReblogForStatus(status.getId(), reblog), (t) -> Log.d(getClass().getSimpleName(), "Failed to reblog status: " + status.getId(), t) ); } - private void setReblogForStatus(int position, Status status, boolean reblog) { - status.setReblogged(reblog); - - if (status.getReblog() != null) { - status.getReblog().setReblogged(reblog); - } - - NotificationViewData.Concrete viewdata = (NotificationViewData.Concrete) notifications.getPairedItem(position); - - StatusViewData.Builder viewDataBuilder = new StatusViewData.Builder(viewdata.getStatusViewData()); - viewDataBuilder.setReblogged(reblog); - - NotificationViewData.Concrete newViewData = new NotificationViewData.Concrete( - viewdata.getType(), viewdata.getId(), viewdata.getAccount(), - viewDataBuilder.createStatusViewData()); - - notifications.setPairedItem(position, newViewData); - updateAdapter(); + private void setReblogForStatus(String statusId, boolean reblog) { + updateStatus(statusId, (s) -> { + s.setReblogged(reblog); + return s; + }); } @Override @@ -458,34 +421,21 @@ public class NotificationsFragment extends SFragment implements final Notification notification = notifications.get(position).asRight(); final Status status = notification.getStatus(); - timelineCases.favourite(status, favourite) + timelineCases.favourite(status.getId(), favourite) .observeOn(AndroidSchedulers.mainThread()) .to(autoDisposable(from(this))) .subscribe( - (newStatus) -> setFavouriteForStatus(position, status, favourite), + (newStatus) -> setFavouriteForStatus(status.getId(), favourite), (t) -> Log.d(getClass().getSimpleName(), "Failed to favourite status: " + status.getId(), t) ); } - private void setFavouriteForStatus(int position, Status status, boolean favourite) { - status.setFavourited(favourite); - - if (status.getReblog() != null) { - status.getReblog().setFavourited(favourite); - } - - NotificationViewData.Concrete viewdata = (NotificationViewData.Concrete) notifications.getPairedItem(position); - - StatusViewData.Builder viewDataBuilder = new StatusViewData.Builder(viewdata.getStatusViewData()); - viewDataBuilder.setFavourited(favourite); - - NotificationViewData.Concrete newViewData = new NotificationViewData.Concrete( - viewdata.getType(), viewdata.getId(), viewdata.getAccount(), - viewDataBuilder.createStatusViewData()); - - notifications.setPairedItem(position, newViewData); - updateAdapter(); + private void setFavouriteForStatus(String statusId, boolean favourite) { + updateStatus(statusId, (s) -> { + s.setFavourited(favourite); + return s; + }); } @Override @@ -493,63 +443,38 @@ public class NotificationsFragment extends SFragment implements final Notification notification = notifications.get(position).asRight(); final Status status = notification.getStatus(); - timelineCases.bookmark(status, bookmark) + timelineCases.bookmark(status.getActionableId(), bookmark) .observeOn(AndroidSchedulers.mainThread()) .to(autoDisposable(from(this))) .subscribe( - (newStatus) -> setBookmarkForStatus(position, status, bookmark), + (newStatus) -> setBookmarkForStatus(status.getId(), bookmark), (t) -> Log.d(getClass().getSimpleName(), "Failed to bookmark status: " + status.getId(), t) ); } - private void setBookmarkForStatus(int position, Status status, boolean bookmark) { - status.setBookmarked(bookmark); - - if (status.getReblog() != null) { - status.getReblog().setBookmarked(bookmark); - } - - NotificationViewData.Concrete viewdata = (NotificationViewData.Concrete) notifications.getPairedItem(position); - - StatusViewData.Builder viewDataBuilder = new StatusViewData.Builder(viewdata.getStatusViewData()); - viewDataBuilder.setBookmarked(bookmark); - - NotificationViewData.Concrete newViewData = new NotificationViewData.Concrete( - viewdata.getType(), viewdata.getId(), viewdata.getAccount(), - viewDataBuilder.createStatusViewData()); - - notifications.setPairedItem(position, newViewData); - updateAdapter(); + private void setBookmarkForStatus(String statusId, boolean bookmark) { + updateStatus(statusId, (s) -> { + s.setBookmarked(bookmark); + return s; + }); } public void onVoteInPoll(int position, @NonNull List choices) { final Notification notification = notifications.get(position).asRight(); - final Status status = notification.getStatus(); - - timelineCases.voteInPoll(status, choices) + final Status status = notification.getStatus().getActionableStatus(); + timelineCases.voteInPoll(status.getId(), status.getPoll().getId(), choices) .observeOn(AndroidSchedulers.mainThread()) .to(autoDisposable(from(this))) .subscribe( - (newPoll) -> setVoteForPoll(position, newPoll), + (newPoll) -> setVoteForPoll(status, newPoll), (t) -> Log.d(TAG, "Failed to vote in poll: " + status.getId(), t) ); } - private void setVoteForPoll(int position, Poll poll) { - - NotificationViewData.Concrete viewdata = (NotificationViewData.Concrete) notifications.getPairedItem(position); - - StatusViewData.Builder viewDataBuilder = new StatusViewData.Builder(viewdata.getStatusViewData()); - viewDataBuilder.setPoll(poll); - - NotificationViewData.Concrete newViewData = new NotificationViewData.Concrete( - viewdata.getType(), viewdata.getId(), viewdata.getAccount(), - viewDataBuilder.createStatusViewData()); - - notifications.setPairedItem(position, newViewData); - updateAdapter(); + private void setVoteForPoll(Status status, Poll poll) { + updateStatus(status.getId(), (s) -> s.copyWithPoll(poll)); } @Override @@ -562,13 +487,17 @@ public class NotificationsFragment extends SFragment implements public void onViewMedia(int position, int attachmentIndex, @Nullable View view) { Notification notification = notifications.get(position).asRightOrNull(); if (notification == null || notification.getStatus() == null) return; - super.viewMedia(attachmentIndex, notification.getStatus(), view); + Status status = notification.getStatus(); + super.viewMedia(attachmentIndex, AttachmentViewData.list(status), view); } @Override public void onViewThread(int position) { Notification notification = notifications.get(position).asRight(); - super.viewThread(notification.getStatus()); + Status status = notification.getStatus(); + if (status == null) return; + ; + super.viewThread(status.getActionableId(), status.getActionableStatus().getUrl()); } @Override @@ -579,30 +508,19 @@ public class NotificationsFragment extends SFragment implements @Override public void onExpandedChange(boolean expanded, int position) { - NotificationViewData.Concrete old = - (NotificationViewData.Concrete) notifications.getPairedItem(position); - StatusViewData.Concrete statusViewData = - new StatusViewData.Builder(old.getStatusViewData()) - .setIsExpanded(expanded) - .createStatusViewData(); - NotificationViewData notificationViewData = new NotificationViewData.Concrete(old.getType(), - old.getId(), old.getAccount(), statusViewData); - notifications.setPairedItem(position, notificationViewData); - updateAdapter(); + updateViewDataAt(position, (vd) -> vd.copyWithExpanded(expanded)); } @Override public void onContentHiddenChange(boolean isShowing, int position) { - NotificationViewData.Concrete old = - (NotificationViewData.Concrete) notifications.getPairedItem(position); - StatusViewData.Concrete statusViewData = - new StatusViewData.Builder(old.getStatusViewData()) - .setIsShowingSensitiveContent(isShowing) - .createStatusViewData(); - NotificationViewData notificationViewData = new NotificationViewData.Concrete(old.getType(), - old.getId(), old.getAccount(), statusViewData); - notifications.setPairedItem(position, notificationViewData); - updateAdapter(); + updateViewDataAt(position, (vd) -> vd.copyWithShowingContent(isShowing)); + } + + private void setPinForStatus(String statusId, boolean pinned) { + updateStatus(statusId, status -> { + status.copyWithPinned(pinned); + return status; + }); } @Override @@ -628,42 +546,74 @@ public class NotificationsFragment extends SFragment implements @Override public void onContentCollapsedChange(boolean isCollapsed, int position) { - if (position < 0 || position >= notifications.size()) { - Log.e(TAG, String.format("Tried to access out of bounds status position: %d of %d", position, notifications.size() - 1)); - return; - } + updateViewDataAt(position, (vd) -> vd.copyWIthCollapsed(isCollapsed)); + ; + } - NotificationViewData notification = notifications.getPairedItem(position); - if (!(notification instanceof NotificationViewData.Concrete)) { - Log.e(TAG, String.format( - "Expected NotificationViewData.Concrete, got %s instead at position: %d of %d", - notification == null ? "null" : notification.getClass().getSimpleName(), + private void updateStatus(String statusId, Function mapper) { + int index = CollectionsKt.indexOfFirst(this.notifications, (s) -> s.isRight() && + s.asRight().getStatus() != null && + s.asRight().getStatus().getId().equals(statusId)); + if (index == -1) return; + + // We have quite some graph here: + // + // Notification --------> Status + // ^ + // | + // StatusViewData + // ^ + // | + // NotificationViewData -----+ + // + // So if we have "new" status we need to update all references to be sure that data is + // up-to-date: + // 1. update status + // 2. update notification + // 3. update statusViewData + // 4. update notificationViewData + + Status oldStatus = notifications.get(index).asRight().getStatus(); + NotificationViewData.Concrete oldViewData = + (NotificationViewData.Concrete) this.notifications.getPairedItem(index); + Status newStatus = mapper.apply(oldStatus); + Notification newNotification = this.notifications.get(index).asRight() + .copyWithStatus(newStatus); + StatusViewData.Concrete newStatusViewData = + Objects.requireNonNull(oldViewData.getStatusViewData()).copyWithStatus(newStatus); + NotificationViewData.Concrete newViewData = oldViewData.copyWithStatus(newStatusViewData); + + notifications.set(index, new Either.Right<>(newNotification)); + notifications.setPairedItem(index, newViewData); + + updateAdapter(); + } + + private void updateViewDataAt(int position, + Function mapper) { + if (position < 0 || position >= notifications.size()) { + String message = String.format( + Locale.getDefault(), + "Tried to access out of bounds status position: %d of %d", position, notifications.size() - 1 - )); + ); + Log.e(TAG, message); return; } + NotificationViewData someViewData = this.notifications.getPairedItem(position); + if (!(someViewData instanceof NotificationViewData.Concrete)) { + return; + } + NotificationViewData.Concrete oldViewData = (NotificationViewData.Concrete) someViewData; + StatusViewData.Concrete oldStatusViewData = oldViewData.getStatusViewData(); + if (oldStatusViewData == null) return; - StatusViewData.Concrete status = ((NotificationViewData.Concrete) notification).getStatusViewData(); - StatusViewData.Concrete updatedStatus = new StatusViewData.Builder(status) - .setCollapsed(isCollapsed) - .createStatusViewData(); + NotificationViewData.Concrete newViewData = + oldViewData.copyWithStatus(mapper.apply(oldStatusViewData)); + notifications.setPairedItem(position, newViewData); - NotificationViewData.Concrete concreteNotification = (NotificationViewData.Concrete) notification; - NotificationViewData updatedNotification = new NotificationViewData.Concrete( - concreteNotification.getType(), - concreteNotification.getId(), - concreteNotification.getAccount(), - updatedStatus - ); - notifications.setPairedItem(position, updatedNotification); updateAdapter(); - - // Since we cannot notify to the RecyclerView right away because it may be scrolling - // we run this when the RecyclerView is done doing measurements and other calculations. - // To test this is not bs: try getting a notification while scrolling, without wrapping - // notifyItemChanged in a .post() call. App will crash. - recyclerView.post(() -> adapter.notifyItemChanged(position, notification)); } @Override @@ -844,8 +794,11 @@ public class NotificationsFragment extends SFragment implements for (Either either : notifications) { Notification notification = either.asRightOrNull(); if (notification != null && notification.getId().equals(notificationId)) { - super.viewThread(notification.getStatus()); - return; + Status status = notification.getStatus(); + if (status != null) { + super.viewThread(status.getActionableId(), status.getActionableStatus().getUrl()); + return; + } } } Log.w(TAG, "Didn't find a notification for ID: " + notificationId); @@ -951,7 +904,7 @@ public class NotificationsFragment extends SFragment implements } Disposable notificationCall = mastodonApi.notifications(fromId, uptoId, LOAD_AT_ONCE, showNotificationsFilter ? notificationFilter : null) - .observeOn(AndroidSchedulers.mainThread()) + .observeOn(AndroidSchedulers.mainThread()) .to(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) .subscribe( response -> { diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java index 3ae843c51..f7dd81591 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java @@ -24,7 +24,6 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Environment; -import android.text.TextUtils; import android.util.Log; import android.view.Menu; import android.view.MenuItem; @@ -33,7 +32,6 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.PopupMenu; import androidx.core.app.ActivityOptionsCompat; @@ -55,8 +53,6 @@ import com.keylesspalace.tusky.db.AccountEntity; import com.keylesspalace.tusky.db.AccountManager; import com.keylesspalace.tusky.di.Injectable; import com.keylesspalace.tusky.entity.Attachment; -import com.keylesspalace.tusky.entity.Filter; -import com.keylesspalace.tusky.entity.PollOption; import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.network.MastodonApi; import com.keylesspalace.tusky.network.TimelineCases; @@ -64,20 +60,14 @@ import com.keylesspalace.tusky.util.LinkHelper; import com.keylesspalace.tusky.view.MuteAccountDialog; import com.keylesspalace.tusky.viewdata.AttachmentViewData; -import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import javax.inject.Inject; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import kotlin.Unit; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; import static autodispose2.AutoDispose.autoDisposable; import static autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from; @@ -96,11 +86,6 @@ public abstract class SFragment extends Fragment implements Injectable { private BottomSheetActivity bottomSheetActivity; - private static List filters; - private boolean filterRemoveRegex; - private Matcher filterRemoveRegexMatcher; - private static final Matcher alphanumeric = Pattern.compile("^\\w+$").matcher(""); - @Inject public MastodonApi mastodonApi; @Inject @@ -131,9 +116,8 @@ public abstract class SFragment extends Fragment implements Injectable { bottomSheetActivity.viewAccount(status.getAccount().getId()); } - protected void viewThread(Status status) { - Status actionableStatus = status.getActionableStatus(); - bottomSheetActivity.viewThread(actionableStatus.getId(), actionableStatus.getUrl()); + protected void viewThread(String statusId, @Nullable String statusUrl) { + bottomSheetActivity.viewThread(statusId, statusUrl); } protected void viewAccount(String accountId) { @@ -149,7 +133,7 @@ public abstract class SFragment extends Fragment implements Injectable { Status actionableStatus = status.getActionableStatus(); Status.Visibility replyVisibility = actionableStatus.getVisibility(); String contentWarning = actionableStatus.getSpoilerText(); - Status.Mention[] mentions = actionableStatus.getMentions(); + List mentions = actionableStatus.getMentions(); Set mentionedUsernames = new LinkedHashSet<>(); mentionedUsernames.add(actionableStatus.getAccount().getUsername()); String loggedInUsername = null; @@ -316,11 +300,11 @@ public abstract class SFragment extends Fragment implements Injectable { return true; } case R.id.pin: { - timelineCases.pin(status, !status.isPinned()); + timelineCases.pin(status.getId(), !status.isPinned()); return true; } case R.id.status_mute_conversation: { - timelineCases.muteConversation(status, status.getMuted() == null || !status.getMuted()) + timelineCases.muteConversation(status.getId(), status.getMuted() == null || !status.getMuted()) .onErrorReturnItem(status) .observeOn(AndroidSchedulers.mainThread()) .to(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) @@ -335,12 +319,12 @@ public abstract class SFragment extends Fragment implements Injectable { private void onMute(String accountId, String accountUsername) { MuteAccountDialog.showMuteAccountDialog( - this.getActivity(), - accountUsername, - (notifications, duration) -> { - timelineCases.mute(accountId, notifications, duration); - return Unit.INSTANCE; - } + this.getActivity(), + accountUsername, + (notifications, duration) -> { + timelineCases.mute(accountId, notifications, duration); + return Unit.INSTANCE; + } ); } @@ -352,7 +336,7 @@ public abstract class SFragment extends Fragment implements Injectable { .show(); } - private static boolean accountIsInMentions(AccountEntity account, Status.Mention[] mentions) { + private static boolean accountIsInMentions(AccountEntity account, List mentions) { if (account == null) { return false; } @@ -368,20 +352,18 @@ public abstract class SFragment extends Fragment implements Injectable { return false; } - protected void viewMedia(int urlIndex, Status status, @Nullable View view) { - final Status actionable = status.getActionableStatus(); - final Attachment active = actionable.getAttachments().get(urlIndex); - Attachment.Type type = active.getType(); + protected void viewMedia(int urlIndex, List attachments, @Nullable View view) { + final AttachmentViewData active = attachments.get(urlIndex); + Attachment.Type type = active.getAttachment().getType(); switch (type) { case GIFV: case VIDEO: case IMAGE: case AUDIO: { - final List attachments = AttachmentViewData.list(actionable); final Intent intent = ViewMediaActivity.newIntent(getContext(), attachments, urlIndex); if (view != null) { - String url = active.getUrl(); + String url = active.getAttachment().getUrl(); ViewCompat.setTransitionName(view, url); ActivityOptionsCompat options = ActivityOptionsCompat.makeSceneTransitionAnimation(getActivity(), @@ -394,7 +376,7 @@ public abstract class SFragment extends Fragment implements Injectable { } default: case UNKNOWN: { - LinkHelper.openLink(active.getUrl(), getContext()); + LinkHelper.openLink(active.getAttachment().getUrl(), getContext()); break; } } @@ -510,83 +492,4 @@ public abstract class SFragment extends Fragment implements Injectable { } }); } - - @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) - public void reloadFilters(boolean forceRefresh) { - if (filters != null && !forceRefresh) { - applyFilters(forceRefresh); - return; - } - - mastodonApi.getFilters().enqueue(new Callback>() { - @Override - public void onResponse(@NonNull Call> call, @NonNull Response> response) { - filters = response.body(); - if (response.isSuccessful() && filters != null) { - applyFilters(forceRefresh); - } else { - Log.e(TAG, "Error getting filters from server"); - } - } - - @Override - public void onFailure(@NonNull Call> call, @NonNull Throwable t) { - Log.e(TAG, "Error getting filters from server", t); - } - }); - } - - protected boolean filterIsRelevant(@NonNull Filter filter) { - // Called when building local filter expression - // Override to select relevant filters for your fragment - return false; - } - - protected void refreshAfterApplyingFilters() { - // Called after filters are updated - // Override to refresh your fragment - } - - @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) - public boolean shouldFilterStatus(Status status) { - - if (filterRemoveRegex && status.getPoll() != null) { - for (PollOption option : status.getPoll().getOptions()) { - if (filterRemoveRegexMatcher.reset(option.getTitle()).find()) { - return true; - } - } - } - - return (filterRemoveRegex && (filterRemoveRegexMatcher.reset(status.getActionableStatus().getContent()).find() - || (!status.getSpoilerText().isEmpty() && filterRemoveRegexMatcher.reset(status.getActionableStatus().getSpoilerText()).find()))); - } - - private void applyFilters(boolean refresh) { - List tokens = new ArrayList<>(); - for (Filter filter : filters) { - if (filterIsRelevant(filter)) { - tokens.add(filterToRegexToken(filter)); - } - } - filterRemoveRegex = !tokens.isEmpty(); - if (filterRemoveRegex) { - filterRemoveRegexMatcher = Pattern.compile(TextUtils.join("|", tokens), Pattern.CASE_INSENSITIVE).matcher(""); - } - if (refresh) { - refreshAfterApplyingFilters(); - } - } - - private static String filterToRegexToken(Filter filter) { - String phrase = filter.getPhrase(); - String quotedPhrase = Pattern.quote(phrase); - return (filter.getWholeWord() && alphanumeric.reset(phrase).matches()) ? // "whole word" should only apply to alphanumeric filters, #1543 - String.format("(^|\\W)%s($|\\W)", quotedPhrase) : - quotedPhrase; - } - - public static void flushFilters() { - filters = null; - } } diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.kt deleted file mode 100644 index bd0378daa..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.kt +++ /dev/null @@ -1,1265 +0,0 @@ -/* Copyright 2021 Tusky Contributors - * - * This file is a part of Tusky. - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky; if not, - * see . */ - -package com.keylesspalace.tusky.fragment - -import android.os.Bundle -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.accessibility.AccessibilityManager -import androidx.core.content.ContextCompat -import androidx.core.util.Pair -import androidx.lifecycle.Lifecycle -import androidx.preference.PreferenceManager -import androidx.recyclerview.widget.AsyncDifferConfig -import androidx.recyclerview.widget.AsyncListDiffer -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.DividerItemDecoration -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.ListUpdateCallback -import androidx.recyclerview.widget.RecyclerView -import androidx.recyclerview.widget.SimpleItemAnimator -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener -import at.connyduck.sparkbutton.helpers.Utils -import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from -import autodispose2.autoDispose -import com.keylesspalace.tusky.AccountListActivity -import com.keylesspalace.tusky.AccountListActivity.Companion.newIntent -import com.keylesspalace.tusky.BaseActivity -import com.keylesspalace.tusky.R -import com.keylesspalace.tusky.adapter.StatusBaseViewHolder -import com.keylesspalace.tusky.adapter.TimelineAdapter -import com.keylesspalace.tusky.appstore.BlockEvent -import com.keylesspalace.tusky.appstore.BookmarkEvent -import com.keylesspalace.tusky.appstore.DomainMuteEvent -import com.keylesspalace.tusky.appstore.Event -import com.keylesspalace.tusky.appstore.EventHub -import com.keylesspalace.tusky.appstore.FavoriteEvent -import com.keylesspalace.tusky.appstore.MuteConversationEvent -import com.keylesspalace.tusky.appstore.MuteEvent -import com.keylesspalace.tusky.appstore.PreferenceChangedEvent -import com.keylesspalace.tusky.appstore.ReblogEvent -import com.keylesspalace.tusky.appstore.StatusComposedEvent -import com.keylesspalace.tusky.appstore.StatusDeletedEvent -import com.keylesspalace.tusky.appstore.UnfollowEvent -import com.keylesspalace.tusky.databinding.FragmentTimelineBinding -import com.keylesspalace.tusky.db.AccountManager -import com.keylesspalace.tusky.di.Injectable -import com.keylesspalace.tusky.entity.Filter -import com.keylesspalace.tusky.entity.Poll -import com.keylesspalace.tusky.entity.Status -import com.keylesspalace.tusky.interfaces.ActionButtonActivity -import com.keylesspalace.tusky.interfaces.RefreshableFragment -import com.keylesspalace.tusky.interfaces.ReselectableFragment -import com.keylesspalace.tusky.interfaces.StatusActionListener -import com.keylesspalace.tusky.repository.Placeholder -import com.keylesspalace.tusky.repository.TimelineRepository -import com.keylesspalace.tusky.repository.TimelineRequestMode -import com.keylesspalace.tusky.settings.PrefKeys -import com.keylesspalace.tusky.util.CardViewMode -import com.keylesspalace.tusky.util.Either -import com.keylesspalace.tusky.util.Either.Left -import com.keylesspalace.tusky.util.Either.Right -import com.keylesspalace.tusky.util.HttpHeaderLink -import com.keylesspalace.tusky.util.LinkHelper -import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate -import com.keylesspalace.tusky.util.PairedList -import com.keylesspalace.tusky.util.StatusDisplayOptions -import com.keylesspalace.tusky.util.ViewDataUtils -import com.keylesspalace.tusky.util.dec -import com.keylesspalace.tusky.util.hide -import com.keylesspalace.tusky.util.inc -import com.keylesspalace.tusky.util.show -import com.keylesspalace.tusky.util.viewBinding -import com.keylesspalace.tusky.view.EndlessOnScrollListener -import com.keylesspalace.tusky.viewdata.StatusViewData -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import io.reactivex.rxjava3.core.Observable -import io.reactivex.rxjava3.core.Single -import retrofit2.Response -import java.io.IOException -import java.util.concurrent.TimeUnit -import javax.inject.Inject - -class TimelineFragment : SFragment(), OnRefreshListener, StatusActionListener, Injectable, ReselectableFragment, RefreshableFragment { - - @Inject - lateinit var eventHub: EventHub - - @Inject - lateinit var timelineRepo: TimelineRepository - - @Inject - lateinit var accountManager: AccountManager - - private val binding by viewBinding(FragmentTimelineBinding::bind) - - private var kind: Kind? = null - private var id: String? = null - private var tags: List = emptyList() - - private lateinit var adapter: TimelineAdapter - - private var isSwipeToRefreshEnabled = true - private var isNeedRefresh = false - - private var eventRegistered = false - - /** - * For some timeline kinds we must use LINK headers and not just status ids. - */ - private var nextId: String? = null - private var layoutManager: LinearLayoutManager? = null - private var scrollListener: EndlessOnScrollListener? = null - private var filterRemoveReplies = false - private var filterRemoveReblogs = false - private var hideFab = false - private var bottomLoading = false - private var didLoadEverythingBottom = false - private var alwaysShowSensitiveMedia = false - private var alwaysOpenSpoiler = false - private var initialUpdateFailed = false - - private val statuses = PairedList, StatusViewData> { input -> - val status = input.asRightOrNull() - if (status != null) { - ViewDataUtils.statusToViewData( - status, - alwaysShowSensitiveMedia, - alwaysOpenSpoiler - ) - } else { - val (id1) = input.asLeft() - StatusViewData.Placeholder(id1, false) - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val arguments = requireArguments() - kind = Kind.valueOf(arguments.getString(KIND_ARG)!!) - if (kind == Kind.USER || kind == Kind.USER_PINNED || kind == Kind.USER_WITH_REPLIES || kind == Kind.LIST) { - id = arguments.getString(ID_ARG)!! - } - if (kind == Kind.TAG) { - tags = arguments.getStringArrayList(HASHTAGS_ARG)!! - } - - isSwipeToRefreshEnabled = arguments.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true) - - val preferences = PreferenceManager.getDefaultSharedPreferences(activity) - val statusDisplayOptions = StatusDisplayOptions( - animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false), - mediaPreviewEnabled = accountManager.activeAccount!!.mediaPreviewEnabled, - useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false), - showBotOverlay = preferences.getBoolean(PrefKeys.SHOW_BOT_OVERLAY, true), - useBlurhash = preferences.getBoolean(PrefKeys.USE_BLURHASH, true), - cardViewMode = if (preferences.getBoolean(PrefKeys.SHOW_CARDS_IN_TIMELINES, false)) CardViewMode.INDENTED else CardViewMode.NONE, - confirmReblogs = preferences.getBoolean(PrefKeys.CONFIRM_REBLOGS, true), - hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false), - animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) - ) - adapter = TimelineAdapter(dataSource, statusDisplayOptions, this) - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_timeline, container, false) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - setupSwipeRefreshLayout() - setupRecyclerView() - updateAdapter() - setupTimelinePreferences() - if (statuses.isEmpty()) { - binding.progressBar.show() - bottomLoading = true - sendInitialRequest() - } else { - binding.progressBar.hide() - if (isNeedRefresh) { - onRefresh() - } - } - } - - private fun sendInitialRequest() { - if (kind == Kind.HOME) { - tryCache() - } else { - sendFetchTimelineRequest(null, null, null, FetchEnd.BOTTOM, -1) - } - } - - private fun tryCache() { - // Request timeline from disk to make it quick, then replace it with timeline from - // the server to update it - timelineRepo.getStatuses(null, null, null, LOAD_AT_ONCE, TimelineRequestMode.DISK) - .observeOn(AndroidSchedulers.mainThread()) - .autoDispose(from(this)) - .subscribe { statuses: List> -> - val mutableStatusResponse = statuses.toMutableList() - filterStatuses(mutableStatusResponse) - if (statuses.size > 1) { - clearPlaceholdersForResponse(mutableStatusResponse) - this.statuses.clear() - this.statuses.addAll(statuses) - updateAdapter() - binding.progressBar.hide() - // Request statuses including current top to refresh all of them - } - updateCurrent() - loadAbove() - } - } - - private fun updateCurrent() { - if (statuses.isEmpty()) { - return - } - val topId = statuses.first { status -> status.isRight() }!!.asRight().id - timelineRepo.getStatuses(topId, null, null, LOAD_AT_ONCE, - TimelineRequestMode.NETWORK) - .observeOn(AndroidSchedulers.mainThread()) - .autoDispose(from(this)) - .subscribe( - { statuses: List> -> - - initialUpdateFailed = false - // When cached timeline is too old, we would replace it with nothing - if (statuses.isNotEmpty()) { - val mutableStatuses = statuses.toMutableList() - filterStatuses(mutableStatuses) - if (!this.statuses.isEmpty()) { - // clear old cached statuses - val iterator = this.statuses.iterator() - while (iterator.hasNext()) { - val item = iterator.next() - if (item.isRight()) { - val (id1) = item.asRight() - if (id1.length < topId.length || id1 < topId) { - iterator.remove() - } - } else { - val (id1) = item.asLeft() - if (id1.length < topId.length || id1 < topId) { - iterator.remove() - } - } - } - } - this.statuses.addAll(mutableStatuses) - updateAdapter() - } - bottomLoading = false - }, - { t: Throwable? -> - Log.d(TAG, "Failed updating timeline", t) - initialUpdateFailed = true - // Indicate that we are not loading anymore - binding.progressBar.hide() - binding.swipeRefreshLayout.isRefreshing = false - }) - } - - private fun setupTimelinePreferences() { - alwaysShowSensitiveMedia = accountManager.activeAccount!!.alwaysShowSensitiveMedia - alwaysOpenSpoiler = accountManager.activeAccount!!.alwaysOpenSpoiler - if (kind == Kind.HOME) { - val preferences = PreferenceManager.getDefaultSharedPreferences(context) - filterRemoveReplies = !preferences.getBoolean("tabFilterHomeReplies", true) - filterRemoveReblogs = !preferences.getBoolean("tabFilterHomeBoosts", true) - } - reloadFilters(false) - } - - override fun filterIsRelevant(filter: Filter): Boolean { - return filterContextMatchesKind(kind, filter.context) - } - - override fun refreshAfterApplyingFilters() { - fullyRefresh() - } - - private fun setupSwipeRefreshLayout() { - binding.swipeRefreshLayout.isEnabled = isSwipeToRefreshEnabled - binding.swipeRefreshLayout.setOnRefreshListener(this) - binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) - } - - private fun setupRecyclerView() { - binding.recyclerView.setAccessibilityDelegateCompat( - ListStatusAccessibilityDelegate(binding.recyclerView, this) - { pos -> statuses.getPairedItemOrNull(pos) } - ) - binding.recyclerView.setHasFixedSize(true) - layoutManager = LinearLayoutManager(context) - binding.recyclerView.layoutManager = layoutManager - val divider = DividerItemDecoration(context, RecyclerView.VERTICAL) - binding.recyclerView.addItemDecoration(divider) - - // CWs are expanded without animation, buttons animate itself, we don't need it basically - (binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false - binding.recyclerView.adapter = adapter - } - - private fun deleteStatusById(id: String) { - for (i in statuses.indices) { - val either = statuses[i] - if (either.isRight() && id == either.asRight().id) { - statuses.remove(either) - updateAdapter() - break - } - } - if (statuses.isEmpty()) { - showEmptyView() - } - } - - private fun showEmptyView() { - binding.statusView.show() - binding.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null) - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - - /* This is delayed until onActivityCreated solely because MainActivity.composeButton isn't - * guaranteed to be set until then. */ - scrollListener = if (actionButtonPresent()) { - /* Use a modified scroll listener that both loads more statuses as it goes, and hides - * the follow button on down-scroll. */ - val preferences = PreferenceManager.getDefaultSharedPreferences(context) - hideFab = preferences.getBoolean("fabHide", false) - object : EndlessOnScrollListener(layoutManager) { - override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) { - super.onScrolled(view, dx, dy) - val composeButton = (activity as ActionButtonActivity).actionButton - if (composeButton != null) { - if (hideFab) { - if (dy > 0 && composeButton.isShown) { - composeButton.hide() // hides the button if we're scrolling down - } else if (dy < 0 && !composeButton.isShown) { - composeButton.show() // shows it if we are scrolling up - } - } else if (!composeButton.isShown) { - composeButton.show() - } - } - } - - override fun onLoadMore(totalItemsCount: Int, view: RecyclerView) { - this@TimelineFragment.onLoadMore() - } - } - } else { - // Just use the basic scroll listener to load more statuses. - object : EndlessOnScrollListener(layoutManager) { - override fun onLoadMore(totalItemsCount: Int, view: RecyclerView) { - this@TimelineFragment.onLoadMore() - } - } - }.also { - binding.recyclerView.addOnScrollListener(it) - } - - if (!eventRegistered) { - eventHub.events - .observeOn(AndroidSchedulers.mainThread()) - .autoDispose(from(this, Lifecycle.Event.ON_DESTROY)) - .subscribe { event: Event? -> - when (event) { - is FavoriteEvent -> handleFavEvent(event) - is ReblogEvent -> handleReblogEvent(event) - is BookmarkEvent -> handleBookmarkEvent(event) - is MuteConversationEvent -> fullyRefresh() - is UnfollowEvent -> { - if (kind == Kind.HOME) { - val id = event.accountId - removeAllByAccountId(id) - } - } - is BlockEvent -> { - if (kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && kind != Kind.USER_PINNED) { - val id = event.accountId - removeAllByAccountId(id) - } - } - is MuteEvent -> { - if (kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && kind != Kind.USER_PINNED) { - val id = event.accountId - removeAllByAccountId(id) - } - } - is DomainMuteEvent -> { - if (kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && kind != Kind.USER_PINNED) { - val instance = event.instance - removeAllByInstance(instance) - } - } - is StatusDeletedEvent -> { - if (kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && kind != Kind.USER_PINNED) { - val id = event.statusId - deleteStatusById(id) - } - } - is StatusComposedEvent -> { - val status = event.status - handleStatusComposeEvent(status) - } - is PreferenceChangedEvent -> { - onPreferenceChanged(event.preferenceKey) - } - } - } - eventRegistered = true - } - } - - override fun onRefresh() { - binding.swipeRefreshLayout.isEnabled = isSwipeToRefreshEnabled - binding.statusView.hide() - isNeedRefresh = false - if (initialUpdateFailed) { - updateCurrent() - } - loadAbove() - } - - private fun loadAbove() { - var firstOrNull: String? = null - var secondOrNull: String? = null - for (i in statuses.indices) { - val status = statuses[i] - if (status.isRight()) { - firstOrNull = status.asRight().id - if (i + 1 < statuses.size && statuses[i + 1].isRight()) { - secondOrNull = statuses[i + 1].asRight().id - } - break - } - } - if (firstOrNull != null) { - sendFetchTimelineRequest(null, firstOrNull, secondOrNull, FetchEnd.TOP, -1) - } else { - sendFetchTimelineRequest(null, null, null, FetchEnd.BOTTOM, -1) - } - } - - override fun onReply(position: Int) { - super.reply(statuses[position].asRight()) - } - - override fun onReblog(reblog: Boolean, position: Int) { - val status = statuses[position].asRight() - timelineCases.reblog(status, reblog) - .observeOn(AndroidSchedulers.mainThread()) - .autoDispose(from(this)) - .subscribe( - { newStatus: Status -> setRebloggedForStatus(position, newStatus, reblog) } - ) { t: Throwable? -> Log.d(TAG, "Failed to reblog status " + status.id, t) } - } - - private fun setRebloggedForStatus(position: Int, status: Status, reblog: Boolean) { - status.reblogged = reblog - if (status.reblog != null) { - status.reblog.reblogged = reblog - } - val actual = findStatusAndPosition(position, status) ?: return - val newViewData: StatusViewData = StatusViewData.Builder(actual.first) - .setReblogged(reblog) - .createStatusViewData() - statuses.setPairedItem(actual.second!!, newViewData) - updateAdapter() - } - - override fun onFavourite(favourite: Boolean, position: Int) { - val status = statuses[position].asRight() - timelineCases.favourite(status, favourite) - .observeOn(AndroidSchedulers.mainThread()) - .autoDispose(from(this)) - .subscribe( - { newStatus: Status -> setFavouriteForStatus(position, newStatus, favourite) }, - { t: Throwable? -> Log.d(TAG, "Failed to favourite status " + status.id, t) } - ) - } - - private fun setFavouriteForStatus(position: Int, status: Status, favourite: Boolean) { - status.favourited = favourite - if (status.reblog != null) { - status.reblog.favourited = favourite - } - val actual = findStatusAndPosition(position, status) ?: return - val newViewData: StatusViewData = StatusViewData.Builder(actual.first) - .setFavourited(favourite) - .createStatusViewData() - statuses.setPairedItem(actual.second!!, newViewData) - updateAdapter() - } - - override fun onBookmark(bookmark: Boolean, position: Int) { - val status = statuses[position].asRight() - timelineCases.bookmark(status, bookmark) - .observeOn(AndroidSchedulers.mainThread()) - .autoDispose(from(this)) - .subscribe( - { newStatus: Status -> setBookmarkForStatus(position, newStatus, bookmark) }, - { t: Throwable? -> Log.d(TAG, "Failed to favourite status " + status.id, t) } - ) - } - - private fun setBookmarkForStatus(position: Int, status: Status, bookmark: Boolean) { - status.bookmarked = bookmark - if (status.reblog != null) { - status.reblog.bookmarked = bookmark - } - val actual = findStatusAndPosition(position, status) ?: return - val newViewData: StatusViewData = StatusViewData.Builder(actual.first) - .setBookmarked(bookmark) - .createStatusViewData() - statuses.setPairedItem(actual.second!!, newViewData) - updateAdapter() - } - - override fun onVoteInPoll(position: Int, choices: List) { - val status = statuses[position].asRight() - val votedPoll = status.actionableStatus.poll!!.votedCopy(choices) - setVoteForPoll(position, status, votedPoll) - timelineCases.voteInPoll(status, choices) - .observeOn(AndroidSchedulers.mainThread()) - .autoDispose(from(this)) - .subscribe( - { newPoll: Poll -> setVoteForPoll(position, status, newPoll) }, - { t: Throwable? -> Log.d(TAG, "Failed to vote in poll: " + status.id, t) } - ) - } - - private fun setVoteForPoll(position: Int, status: Status, newPoll: Poll) { - val actual = findStatusAndPosition(position, status) ?: return - val newViewData: StatusViewData = StatusViewData.Builder(actual.first) - .setPoll(newPoll) - .createStatusViewData() - statuses.setPairedItem(actual.second!!, newViewData) - updateAdapter() - } - - override fun onMore(view: View, position: Int) { - super.more(statuses[position].asRight(), view, position) - } - - override fun onOpenReblog(position: Int) { - super.openReblog(statuses[position].asRight()) - } - - override fun onExpandedChange(expanded: Boolean, position: Int) { - val newViewData: StatusViewData = StatusViewData.Builder( - statuses.getPairedItem(position) as StatusViewData.Concrete) - .setIsExpanded(expanded).createStatusViewData() - statuses.setPairedItem(position, newViewData) - updateAdapter() - } - - override fun onContentHiddenChange(isShowing: Boolean, position: Int) { - val newViewData: StatusViewData = StatusViewData.Builder( - statuses.getPairedItem(position) as StatusViewData.Concrete) - .setIsShowingSensitiveContent(isShowing).createStatusViewData() - statuses.setPairedItem(position, newViewData) - updateAdapter() - } - - override fun onShowReblogs(position: Int) { - val statusId = statuses[position].asRight().id - val intent = newIntent(requireContext(), AccountListActivity.Type.REBLOGGED, statusId) - (activity as BaseActivity).startActivityWithSlideInAnimation(intent) - } - - override fun onShowFavs(position: Int) { - val statusId = statuses[position].asRight().id - val intent = newIntent(requireContext(), AccountListActivity.Type.FAVOURITED, statusId) - (activity as BaseActivity).startActivityWithSlideInAnimation(intent) - } - - override fun onLoadMore(position: Int) { - //check bounds before accessing list, - if (statuses.size >= position && position > 0) { - val fromStatus = statuses[position - 1].asRightOrNull() - val toStatus = statuses[position + 1].asRightOrNull() - val maxMinusOne = if (statuses.size > position + 1 && statuses[position + 2].isRight()) statuses[position + 1].asRight().id else null - if (fromStatus == null || toStatus == null) { - Log.e(TAG, "Failed to load more at $position, wrong placeholder position") - return - } - sendFetchTimelineRequest(fromStatus.id, toStatus.id, maxMinusOne, - FetchEnd.MIDDLE, position) - val (id1) = statuses[position].asLeft() - val newViewData: StatusViewData = StatusViewData.Placeholder(id1, true) - statuses.setPairedItem(position, newViewData) - updateAdapter() - } else { - Log.e(TAG, "error loading more") - } - } - - override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) { - if (position < 0 || position >= statuses.size) { - Log.e(TAG, String.format("Tried to access out of bounds status position: %d of %d", position, statuses.size - 1)) - return - } - val status = statuses.getPairedItem(position) - if (status !is StatusViewData.Concrete) { - // Statuses PairedList contains a base type of StatusViewData.Concrete and also doesn't - // check for null values when adding values to it although this doesn't seem to be an issue. - Log.e(TAG, String.format( - "Expected StatusViewData.Concrete, got %s instead at position: %d of %d", - status?.javaClass?.simpleName ?: "", - position, - statuses.size - 1 - )) - return - } - val updatedStatus: StatusViewData = StatusViewData.Builder(status) - .setCollapsed(isCollapsed) - .createStatusViewData() - statuses.setPairedItem(position, updatedStatus) - updateAdapter() - } - - override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) { - val status = statuses.getOrNull(position)?.asRightOrNull() ?: return - super.viewMedia(attachmentIndex, status, view) - } - - override fun onViewThread(position: Int) { - super.viewThread(statuses[position].asRight()) - } - - override fun onViewTag(tag: String) { - if (kind == Kind.TAG && tags.size == 1 && tags.contains(tag)) { - // If already viewing a tag page, then ignore any request to view that tag again. - return - } - super.viewTag(tag) - } - - override fun onViewAccount(id: String) { - if ((kind == Kind.USER || kind == Kind.USER_WITH_REPLIES) && this.id == id) { - /* If already viewing an account page, then any requests to view that account page - * should be ignored. */ - return - } - super.viewAccount(id) - } - - private fun onPreferenceChanged(key: String) { - val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) - when (key) { - PrefKeys.FAB_HIDE -> { - hideFab = sharedPreferences.getBoolean(PrefKeys.FAB_HIDE, false) - } - PrefKeys.MEDIA_PREVIEW_ENABLED -> { - val enabled = accountManager.activeAccount!!.mediaPreviewEnabled - val oldMediaPreviewEnabled = adapter.mediaPreviewEnabled - if (enabled != oldMediaPreviewEnabled) { - adapter.mediaPreviewEnabled = enabled - fullyRefresh() - } - } - PrefKeys.TAB_FILTER_HOME_REPLIES -> { - val filter = sharedPreferences.getBoolean(PrefKeys.TAB_FILTER_HOME_REPLIES, true) - val oldRemoveReplies = filterRemoveReplies - filterRemoveReplies = kind == Kind.HOME && !filter - if (adapter.itemCount > 1 && oldRemoveReplies != filterRemoveReplies) { - fullyRefresh() - } - } - PrefKeys.TAB_FILTER_HOME_BOOSTS -> { - val filter = sharedPreferences.getBoolean(PrefKeys.TAB_FILTER_HOME_BOOSTS, true) - val oldRemoveReblogs = filterRemoveReblogs - filterRemoveReblogs = kind == Kind.HOME && !filter - if (adapter.itemCount > 1 && oldRemoveReblogs != filterRemoveReblogs) { - fullyRefresh() - } - } - Filter.HOME, Filter.NOTIFICATIONS, Filter.THREAD, Filter.PUBLIC, Filter.ACCOUNT -> { - if (filterContextMatchesKind(kind, listOf(key))) { - reloadFilters(true) - } - } - PrefKeys.ALWAYS_SHOW_SENSITIVE_MEDIA -> { - //it is ok if only newly loaded statuses are affected, no need to fully refresh - alwaysShowSensitiveMedia = accountManager.activeAccount!!.alwaysShowSensitiveMedia - } - } - } - - public override fun removeItem(position: Int) { - statuses.removeAt(position) - updateAdapter() - } - - private fun removeAllByAccountId(accountId: String) { - // using iterator to safely remove items while iterating - val iterator = statuses.iterator() - while (iterator.hasNext()) { - val status = iterator.next().asRightOrNull() - if (status != null && - (status.account.id == accountId || status.actionableStatus.account.id == accountId)) { - iterator.remove() - } - } - updateAdapter() - } - - private fun removeAllByInstance(instance: String) { - // using iterator to safely remove items while iterating - val iterator = statuses.iterator() - while (iterator.hasNext()) { - val status = iterator.next().asRightOrNull() - if (status != null && LinkHelper.getDomain(status.account.url) == instance) { - iterator.remove() - } - } - updateAdapter() - } - - private fun onLoadMore() { - if (didLoadEverythingBottom || bottomLoading) { - return - } - if (statuses.isEmpty()) { - sendInitialRequest() - return - } - bottomLoading = true - val last = statuses[statuses.size - 1] - val placeholder: Placeholder - if (last!!.isRight()) { - val placeholderId = last.asRight().id.dec() - placeholder = Placeholder(placeholderId) - statuses.add(Left(placeholder)) - } else { - placeholder = last.asLeft() - } - statuses.setPairedItem(statuses.size - 1, - StatusViewData.Placeholder(placeholder.id, true)) - updateAdapter() - - val bottomId: String? = if (kind == Kind.FAVOURITES || kind == Kind.BOOKMARKS) { - nextId - } else { - statuses.lastOrNull { it.isRight() }?.asRight()?.id - } - - sendFetchTimelineRequest(bottomId, null, null, FetchEnd.BOTTOM, -1) - } - - private fun fullyRefresh() { - statuses.clear() - updateAdapter() - bottomLoading = true - sendFetchTimelineRequest(null, null, null, FetchEnd.BOTTOM, -1) - } - - private fun actionButtonPresent(): Boolean { - return kind != Kind.TAG && kind != Kind.FAVOURITES && kind != Kind.BOOKMARKS && - activity is ActionButtonActivity - } - - private fun getFetchCallByTimelineType(fromId: String?, uptoId: String?): Single>> { - val api = mastodonApi - return when (kind) { - Kind.HOME -> api.homeTimeline(fromId, uptoId, LOAD_AT_ONCE) - Kind.PUBLIC_FEDERATED -> api.publicTimeline(null, fromId, uptoId, LOAD_AT_ONCE) - Kind.PUBLIC_LOCAL -> api.publicTimeline(true, fromId, uptoId, LOAD_AT_ONCE) - Kind.TAG -> { - val firstHashtag = tags[0] - val additionalHashtags = tags.subList(1, tags.size) - api.hashtagTimeline(firstHashtag, additionalHashtags, null, fromId, uptoId, LOAD_AT_ONCE) - } - Kind.USER -> api.accountStatuses(id!!, fromId, uptoId, LOAD_AT_ONCE, true, null, null) - Kind.USER_PINNED -> api.accountStatuses(id!!, fromId, uptoId, LOAD_AT_ONCE, null, null, true) - Kind.USER_WITH_REPLIES -> api.accountStatuses(id!!, fromId, uptoId, LOAD_AT_ONCE, null, null, null) - Kind.FAVOURITES -> api.favourites(fromId, uptoId, LOAD_AT_ONCE) - Kind.BOOKMARKS -> api.bookmarks(fromId, uptoId, LOAD_AT_ONCE) - Kind.LIST -> api.listTimeline(id!!, fromId, uptoId, LOAD_AT_ONCE) - else -> api.homeTimeline(fromId, uptoId, LOAD_AT_ONCE) - } - } - - private fun sendFetchTimelineRequest(maxId: String?, sinceId: String?, - sinceIdMinusOne: String?, - fetchEnd: FetchEnd, pos: Int) { - if (isAdded && (fetchEnd == FetchEnd.TOP || fetchEnd == FetchEnd.BOTTOM && maxId == null && binding.progressBar.visibility != View.VISIBLE) && !isSwipeToRefreshEnabled) { - binding.topProgressBar.show() - } - if (kind == Kind.HOME) { - // allow getting old statuses/fallbacks for network only for for bottom loading - val mode = if (fetchEnd == FetchEnd.BOTTOM) { - TimelineRequestMode.ANY - } else { - TimelineRequestMode.NETWORK - } - timelineRepo.getStatuses(maxId, sinceId, sinceIdMinusOne, LOAD_AT_ONCE, mode) - .observeOn(AndroidSchedulers.mainThread()) - .autoDispose(from(this)) - .subscribe( - { result: List> -> onFetchTimelineSuccess(result.toMutableList(), fetchEnd, pos) }, - { t: Throwable -> onFetchTimelineFailure(t, fetchEnd, pos) } - ) - } else { - getFetchCallByTimelineType(maxId, sinceId) - .observeOn(AndroidSchedulers.mainThread()) - .autoDispose(from(this)) - .subscribe( - { response: Response> -> - if (response.isSuccessful) { - val newNextId = extractNextId(response) - if (newNextId != null) { - // when we reach the bottom of the list, we won't have a new link. If - // we blindly write `null` here we will start loading from the top - // again. - nextId = newNextId - } - onFetchTimelineSuccess(liftStatusList(response.body()!!).toMutableList(), fetchEnd, pos) - } else { - onFetchTimelineFailure(Exception(response.message()), fetchEnd, pos) - } - } - ) { t: Throwable -> onFetchTimelineFailure(t, fetchEnd, pos) } - } - } - - private fun extractNextId(response: Response<*>): String? { - val linkHeader = response.headers()["Link"] ?: return null - val links = HttpHeaderLink.parse(linkHeader) - val nextHeader = HttpHeaderLink.findByRelationType(links, "next") ?: return null - val nextLink = nextHeader.uri ?: return null - return nextLink.getQueryParameter("max_id") - } - - private fun onFetchTimelineSuccess(statuses: MutableList>, - fetchEnd: FetchEnd, pos: Int) { - - // We filled the hole (or reached the end) if the server returned less statuses than we - // we asked for. - val fullFetch = statuses.size >= LOAD_AT_ONCE - filterStatuses(statuses) - when (fetchEnd) { - FetchEnd.TOP -> { - updateStatuses(statuses, fullFetch) - } - FetchEnd.MIDDLE -> { - replacePlaceholderWithStatuses(statuses, fullFetch, pos) - } - FetchEnd.BOTTOM -> { - if (!this.statuses.isEmpty() - && !this.statuses[this.statuses.size - 1].isRight()) { - this.statuses.removeAt(this.statuses.size - 1) - updateAdapter() - } - if (statuses.isNotEmpty() && !statuses[statuses.size - 1].isRight()) { - // Removing placeholder if it's the last one from the cache - statuses.removeAt(statuses.size - 1) - } - val oldSize = this.statuses.size - if (this.statuses.size > 1) { - addItems(statuses) - } else { - updateStatuses(statuses, fullFetch) - } - if (this.statuses.size == oldSize) { - // This may be a brittle check but seems like it works - // Can we check it using headers somehow? Do all server support them? - didLoadEverythingBottom = true - } - } - } - if (isAdded) { - binding.topProgressBar.hide() - updateBottomLoadingState(fetchEnd) - binding.progressBar.hide() - binding.swipeRefreshLayout.isRefreshing = false - binding.swipeRefreshLayout.isEnabled = true - if (this.statuses.size == 0) { - showEmptyView() - } else { - binding.statusView.hide() - } - } - } - - private fun onFetchTimelineFailure(throwable: Throwable, fetchEnd: FetchEnd, position: Int) { - if (isAdded) { - binding.swipeRefreshLayout.isRefreshing = false - binding.topProgressBar.hide() - if (fetchEnd == FetchEnd.MIDDLE && !statuses[position].isRight()) { - var placeholder = statuses[position].asLeftOrNull() - val newViewData: StatusViewData - if (placeholder == null) { - val (id1) = statuses[position - 1].asRight() - val newId = id1.dec() - placeholder = Placeholder(newId) - } - newViewData = StatusViewData.Placeholder(placeholder.id, false) - statuses.setPairedItem(position, newViewData) - updateAdapter() - } else if (statuses.isEmpty()) { - binding.swipeRefreshLayout.isEnabled = false - binding.statusView.visibility = View.VISIBLE - if (throwable is IOException) { - binding.statusView.setup(R.drawable.elephant_offline, R.string.error_network) { - binding.progressBar.visibility = View.VISIBLE - onRefresh() - } - } else { - binding.statusView.setup(R.drawable.elephant_error, R.string.error_generic) { - binding.progressBar.visibility = View.VISIBLE - onRefresh() - } - } - } - Log.e(TAG, "Fetch Failure: " + throwable.message) - updateBottomLoadingState(fetchEnd) - binding.progressBar.hide() - } - } - - private fun updateBottomLoadingState(fetchEnd: FetchEnd) { - if (fetchEnd == FetchEnd.BOTTOM) { - bottomLoading = false - } - } - - private fun filterStatuses(statuses: MutableList>) { - val it = statuses.iterator() - while (it.hasNext()) { - val status = it.next().asRightOrNull() - if (status != null - && (status.inReplyToId != null && filterRemoveReplies - || status.reblog != null && filterRemoveReblogs - || shouldFilterStatus(status.actionableStatus))) { - it.remove() - } - } - } - - private fun updateStatuses(newStatuses: MutableList>, fullFetch: Boolean) { - if (newStatuses.isEmpty()) { - updateAdapter() - return - } - if (statuses.isEmpty()) { - statuses.addAll(newStatuses) - } else { - val lastOfNew = newStatuses[newStatuses.size - 1] - val index = statuses.indexOf(lastOfNew) - if (index >= 0) { - statuses.subList(0, index).clear() - } - val newIndex = newStatuses.indexOf(statuses[0]) - if (newIndex == -1) { - if (index == -1 && fullFetch) { - val placeholderId = newStatuses.last { status -> status.isRight() }.asRight().id.inc() - newStatuses.add(Left(Placeholder(placeholderId))) - } - statuses.addAll(0, newStatuses) - } else { - statuses.addAll(0, newStatuses.subList(0, newIndex)) - } - } - // Remove all consecutive placeholders - removeConsecutivePlaceholders() - updateAdapter() - } - - private fun removeConsecutivePlaceholders() { - for (i in 0 until statuses.size - 1) { - if (statuses[i].isLeft() && statuses[i + 1].isLeft()) { - statuses.removeAt(i) - } - } - } - - private fun addItems(newStatuses: List?>) { - if (newStatuses.isEmpty()) { - return - } - val last = statuses.last { status -> - status.isRight() - } - - // I was about to replace findStatus with indexOf but it is incorrect to compare value - // types by ID anyway and we should change equals() for Status, I think, so this makes sense - if (last != null && !newStatuses.contains(last)) { - statuses.addAll(newStatuses) - removeConsecutivePlaceholders() - updateAdapter() - } - } - - /** - * For certain requests we don't want to see placeholders, they will be removed some other way - */ - private fun clearPlaceholdersForResponse(statuses: MutableList>) { - statuses.removeAll{ status -> status.isLeft() } - } - - private fun replacePlaceholderWithStatuses(newStatuses: MutableList>, - fullFetch: Boolean, pos: Int) { - val placeholder = statuses[pos] - if (placeholder.isLeft()) { - statuses.removeAt(pos) - } - if (newStatuses.isEmpty()) { - updateAdapter() - return - } - if (fullFetch) { - newStatuses.add(placeholder) - } - statuses.addAll(pos, newStatuses) - removeConsecutivePlaceholders() - updateAdapter() - } - - private fun findStatusOrReblogPositionById(statusId: String): Int { - return statuses.indexOfFirst { either -> - val status = either.asRightOrNull() - status != null && - (statusId == status.id || - (status.reblog != null && statusId == status.reblog.id)) - } - } - - private val statusLifter: Function1> = { value -> Right(value) } - - private fun findStatusAndPosition(position: Int, status: Status): Pair? { - val statusToUpdate: StatusViewData.Concrete - val positionToUpdate: Int - val someOldViewData = statuses.getPairedItem(position) - - // Unlikely, but data could change between the request and response - if (someOldViewData is StatusViewData.Placeholder || - (someOldViewData as StatusViewData.Concrete).id != status.id) { - // try to find the status we need to update - val foundPos = statuses.indexOf(Right(status)) - if (foundPos < 0) return null // okay, it's hopeless, give up - statusToUpdate = statuses.getPairedItem(foundPos) as StatusViewData.Concrete - positionToUpdate = position - } else { - statusToUpdate = someOldViewData - positionToUpdate = position - } - return Pair(statusToUpdate, positionToUpdate) - } - - private fun handleReblogEvent(reblogEvent: ReblogEvent) { - val pos = findStatusOrReblogPositionById(reblogEvent.statusId) - if (pos < 0) return - val status = statuses[pos].asRight() - setRebloggedForStatus(pos, status, reblogEvent.reblog) - } - - private fun handleFavEvent(favEvent: FavoriteEvent) { - val pos = findStatusOrReblogPositionById(favEvent.statusId) - if (pos < 0) return - val status = statuses[pos].asRight() - setFavouriteForStatus(pos, status, favEvent.favourite) - } - - private fun handleBookmarkEvent(bookmarkEvent: BookmarkEvent) { - val pos = findStatusOrReblogPositionById(bookmarkEvent.statusId) - if (pos < 0) return - val status = statuses[pos].asRight() - setBookmarkForStatus(pos, status, bookmarkEvent.bookmark) - } - - private fun handleStatusComposeEvent(status: Status) { - when (kind) { - Kind.HOME, Kind.PUBLIC_FEDERATED, Kind.PUBLIC_LOCAL -> onRefresh() - Kind.USER, Kind.USER_WITH_REPLIES -> if (status.account.id == id) { - onRefresh() - } else { - return - } - Kind.TAG, Kind.FAVOURITES, Kind.LIST, Kind.BOOKMARKS, Kind.USER_PINNED -> return - } - } - - private fun liftStatusList(list: List): List> { - return list.map(statusLifter) - } - - private fun updateAdapter() { - differ.submitList(statuses.pairedCopy) - } - - private val listUpdateCallback: ListUpdateCallback = object : ListUpdateCallback { - override fun onInserted(position: Int, count: Int) { - if (isAdded) { - adapter.notifyItemRangeInserted(position, count) - val context = context - // scroll up when new items at the top are loaded while being in the first position - // https://github.com/tuskyapp/Tusky/pull/1905#issuecomment-677819724 - if (position == 0 && context != null && adapter.itemCount != count) { - if (isSwipeToRefreshEnabled) { - binding.recyclerView.scrollBy(0, Utils.dpToPx(context, -30)) - } else binding.recyclerView.scrollToPosition(0) - } - } - } - - override fun onRemoved(position: Int, count: Int) { - adapter.notifyItemRangeRemoved(position, count) - } - - override fun onMoved(fromPosition: Int, toPosition: Int) { - adapter.notifyItemMoved(fromPosition, toPosition) - } - - override fun onChanged(position: Int, count: Int, payload: Any?) { - adapter.notifyItemRangeChanged(position, count, payload) - } - } - private val differ = AsyncListDiffer(listUpdateCallback, - AsyncDifferConfig.Builder(diffCallback).build()) - - private val dataSource: TimelineAdapter.AdapterDataSource = object : TimelineAdapter.AdapterDataSource { - override fun getItemCount(): Int { - return differ.currentList.size - } - - override fun getItemAt(pos: Int): StatusViewData { - return differ.currentList[pos] - } - } - - private var talkBackWasEnabled = false - - override fun onResume() { - super.onResume() - val a11yManager = ContextCompat.getSystemService(requireContext(), AccessibilityManager::class.java) - - val wasEnabled = talkBackWasEnabled - talkBackWasEnabled = a11yManager?.isEnabled == true - Log.d(TAG, "talkback was enabled: $wasEnabled, now $talkBackWasEnabled") - if (talkBackWasEnabled && !wasEnabled) { - adapter.notifyDataSetChanged() - } - startUpdateTimestamp() - } - - /** - * Start to update adapter every minute to refresh timestamp - * If setting absoluteTimeView is false - * Auto dispose observable on pause - */ - private fun startUpdateTimestamp() { - val preferences = PreferenceManager.getDefaultSharedPreferences(activity) - val useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false) - if (!useAbsoluteTime) { - Observable.interval(1, TimeUnit.MINUTES) - .observeOn(AndroidSchedulers.mainThread()) - .autoDispose(from(this, Lifecycle.Event.ON_PAUSE)) - .subscribe { updateAdapter() } - } - } - - override fun onReselect() { - if (isAdded) { - layoutManager!!.scrollToPosition(0) - binding.recyclerView.stopScroll() - scrollListener!!.reset() - } - } - - override fun refreshContent() { - if (isAdded) { - onRefresh() - } else { - isNeedRefresh = true - } - } - - enum class Kind { - HOME, PUBLIC_LOCAL, PUBLIC_FEDERATED, TAG, USER, USER_PINNED, USER_WITH_REPLIES, FAVOURITES, LIST, BOOKMARKS - } - - private enum class FetchEnd { - TOP, BOTTOM, MIDDLE - } - - companion object { - private const val TAG = "TimelineF" // logging tag - private const val KIND_ARG = "kind" - private const val ID_ARG = "id" - private const val HASHTAGS_ARG = "hashtags" - private const val ARG_ENABLE_SWIPE_TO_REFRESH = "enableSwipeToRefresh" - private const val LOAD_AT_ONCE = 30 - - fun newInstance(kind: Kind, hashtagOrId: String? = null, enableSwipeToRefresh: Boolean = true): TimelineFragment { - val fragment = TimelineFragment() - val arguments = Bundle(3) - arguments.putString(KIND_ARG, kind.name) - arguments.putString(ID_ARG, hashtagOrId) - arguments.putBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, enableSwipeToRefresh) - fragment.arguments = arguments - return fragment - } - - @JvmStatic - fun newHashtagInstance(hashtags: List): TimelineFragment { - val fragment = TimelineFragment() - val arguments = Bundle(3) - arguments.putString(KIND_ARG, Kind.TAG.name) - arguments.putStringArrayList(HASHTAGS_ARG, ArrayList(hashtags)) - arguments.putBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true) - fragment.arguments = arguments - return fragment - } - - private fun filterContextMatchesKind(kind: Kind?, filterContext: List): Boolean { - // home, notifications, public, thread - return when (kind) { - Kind.HOME, Kind.LIST -> filterContext.contains(Filter.HOME) - Kind.PUBLIC_FEDERATED, Kind.PUBLIC_LOCAL, Kind.TAG -> filterContext.contains(Filter.PUBLIC) - Kind.FAVOURITES -> filterContext.contains(Filter.PUBLIC) || filterContext.contains(Filter.NOTIFICATIONS) - Kind.USER, Kind.USER_WITH_REPLIES, Kind.USER_PINNED -> filterContext.contains(Filter.ACCOUNT) - else -> false - } - } - - private val diffCallback: DiffUtil.ItemCallback = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: StatusViewData, newItem: StatusViewData): Boolean { - return oldItem.viewDataId == newItem.viewDataId - } - - override fun areContentsTheSame(oldItem: StatusViewData, newItem: StatusViewData): Boolean { - return false // Items are different always. It allows to refresh timestamp on every view holder update - } - - override fun getChangePayload(oldItem: StatusViewData, newItem: StatusViewData): Any? { - return if (oldItem.deepEquals(newItem)) { - // If items are equal - update timestamp only - listOf(StatusBaseViewHolder.Key.KEY_CREATED) - } else // If items are different - update the whole view holder - null - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java index 4f00439a3..8f5ffcf10 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java @@ -28,7 +28,6 @@ import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.arch.core.util.Function; -import androidx.core.util.Pair; import androidx.lifecycle.Lifecycle; import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.DividerItemDecoration; @@ -48,6 +47,7 @@ import com.keylesspalace.tusky.appstore.BlockEvent; import com.keylesspalace.tusky.appstore.BookmarkEvent; import com.keylesspalace.tusky.appstore.EventHub; import com.keylesspalace.tusky.appstore.FavoriteEvent; +import com.keylesspalace.tusky.appstore.PinEvent; import com.keylesspalace.tusky.appstore.ReblogEvent; import com.keylesspalace.tusky.appstore.StatusComposedEvent; import com.keylesspalace.tusky.appstore.StatusDeletedEvent; @@ -56,6 +56,7 @@ import com.keylesspalace.tusky.entity.Filter; import com.keylesspalace.tusky.entity.Poll; import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.interfaces.StatusActionListener; +import com.keylesspalace.tusky.network.FilterModel; import com.keylesspalace.tusky.network.MastodonApi; import com.keylesspalace.tusky.settings.PrefKeys; import com.keylesspalace.tusky.util.CardViewMode; @@ -64,6 +65,7 @@ import com.keylesspalace.tusky.util.PairedList; import com.keylesspalace.tusky.util.StatusDisplayOptions; import com.keylesspalace.tusky.util.ViewDataUtils; import com.keylesspalace.tusky.view.ConversationLineItemDecoration; +import com.keylesspalace.tusky.viewdata.AttachmentViewData; import com.keylesspalace.tusky.viewdata.StatusViewData; import java.util.ArrayList; @@ -73,7 +75,9 @@ import java.util.Locale; import javax.inject.Inject; +import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import kotlin.collections.CollectionsKt; import static autodispose2.AutoDispose.autoDisposable; import static autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from; @@ -86,6 +90,8 @@ public final class ViewThreadFragment extends SFragment implements public MastodonApi mastodonApi; @Inject public EventHub eventHub; + @Inject + public FilterModel filterModel; private SwipeRefreshLayout swipeRefreshLayout; private RecyclerView recyclerView; @@ -163,7 +169,7 @@ public final class ViewThreadFragment extends SFragment implements recyclerView.addItemDecoration(new ConversationLineItemDecoration(context)); alwaysShowSensitiveMedia = accountManager.getActiveAccount().getAlwaysShowSensitiveMedia(); alwaysOpenSpoiler = accountManager.getActiveAccount().getAlwaysOpenSpoiler(); - reloadFilters(false); + reloadFilters(); recyclerView.setAdapter(adapter); @@ -190,6 +196,8 @@ public final class ViewThreadFragment extends SFragment implements handleReblogEvent((ReblogEvent) event); } else if (event instanceof BookmarkEvent) { handleBookmarkEvent((BookmarkEvent) event); + } else if (event instanceof PinEvent) { + handlePinEvent(((PinEvent) event)); } else if (event instanceof BlockEvent) { removeAllByAccountId(((BlockEvent) event).getAccountId()); } else if (event instanceof StatusComposedEvent) { @@ -203,13 +211,8 @@ public final class ViewThreadFragment extends SFragment implements public void onRevealPressed() { boolean allExpanded = allExpanded(); for (int i = 0; i < statuses.size(); i++) { - StatusViewData.Concrete newViewData = - new StatusViewData.Concrete.Builder(statuses.getPairedItem(i)) - .setIsExpanded(!allExpanded) - .createStatusViewData(); - statuses.setPairedItem(i, newViewData); + updateViewData(i, statuses.getPairedItem(i).copyWithExpanded(!allExpanded)); } - adapter.setStatuses(statuses.getPairedCopy()); updateRevealIcon(); } @@ -239,11 +242,11 @@ public final class ViewThreadFragment extends SFragment implements public void onReblog(final boolean reblog, final int position) { final Status status = statuses.get(position); - timelineCases.reblog(statuses.get(position), reblog) + timelineCases.reblog(statuses.get(position).getId(), reblog) .observeOn(AndroidSchedulers.mainThread()) .to(autoDisposable(from(this))) .subscribe( - (newStatus) -> updateStatus(position, newStatus), + this::replaceStatus, (t) -> Log.d(TAG, "Failed to reblog status: " + status.getId(), t) ); @@ -253,11 +256,11 @@ public final class ViewThreadFragment extends SFragment implements public void onFavourite(final boolean favourite, final int position) { final Status status = statuses.get(position); - timelineCases.favourite(statuses.get(position), favourite) + timelineCases.favourite(statuses.get(position).getId(), favourite) .observeOn(AndroidSchedulers.mainThread()) .to(autoDisposable(from(this))) .subscribe( - (newStatus) -> updateStatus(position, newStatus), + this::replaceStatus, (t) -> Log.d(TAG, "Failed to favourite status: " + status.getId(), t) ); @@ -267,32 +270,29 @@ public final class ViewThreadFragment extends SFragment implements public void onBookmark(final boolean bookmark, final int position) { final Status status = statuses.get(position); - timelineCases.bookmark(statuses.get(position), bookmark) + timelineCases.bookmark(statuses.get(position).getId(), bookmark) .observeOn(AndroidSchedulers.mainThread()) .to(autoDisposable(from(this))) .subscribe( - (newStatus) -> updateStatus(position, newStatus), + this::replaceStatus, (t) -> Log.d(TAG, "Failed to bookmark status: " + status.getId(), t) ); } - private void updateStatus(int position, Status status) { + private void replaceStatus(Status status) { + updateStatus(status.getId(), (__) -> status); + } + + private void updateStatus(String statusId, Function mapper) { + int position = indexOfStatus(statusId); + if (position >= 0 && position < statuses.size()) { - - Status actionableStatus = status.getActionableStatus(); - - StatusViewData.Concrete viewData = new StatusViewData.Builder(statuses.getPairedItem(position)) - .setReblogged(actionableStatus.getReblogged()) - .setReblogsCount(actionableStatus.getReblogsCount()) - .setFavourited(actionableStatus.getFavourited()) - .setBookmarked(actionableStatus.getBookmarked()) - .setFavouritesCount(actionableStatus.getFavouritesCount()) - .createStatusViewData(); - statuses.setPairedItem(position, viewData); - - adapter.setItem(position, viewData, true); - + Status oldStatus = statuses.get(position); + Status newStatus = mapper.apply(oldStatus); + StatusViewData.Concrete oldViewData = statuses.getPairedItem(position); + statuses.set(position, newStatus); + updateViewData(position, oldViewData.copyWithStatus(newStatus)); } } @@ -304,7 +304,7 @@ public final class ViewThreadFragment extends SFragment implements @Override public void onViewMedia(int position, int attachmentIndex, @NonNull View view) { Status status = statuses.get(position); - super.viewMedia(attachmentIndex, status, view); + super.viewMedia(attachmentIndex, AttachmentViewData.list(status), view); } @Override @@ -314,7 +314,7 @@ public final class ViewThreadFragment extends SFragment implements // If already viewing this thread, don't reopen it. return; } - super.viewThread(status); + super.viewThread(status.getActionableId(), status.getActionableStatus().getUrl()); } @Override @@ -325,21 +325,22 @@ public final class ViewThreadFragment extends SFragment implements @Override public void onExpandedChange(boolean expanded, int position) { - StatusViewData.Concrete newViewData = - new StatusViewData.Builder(statuses.getPairedItem(position)) - .setIsExpanded(expanded) - .createStatusViewData(); - statuses.setPairedItem(position, newViewData); - adapter.setItem(position, newViewData, true); + updateViewData( + position, + statuses.getPairedItem(position).copyWithExpanded(expanded) + ); updateRevealIcon(); } @Override public void onContentHiddenChange(boolean isShowing, int position) { - StatusViewData.Concrete newViewData = - new StatusViewData.Builder(statuses.getPairedItem(position)) - .setIsShowingSensitiveContent(isShowing) - .createStatusViewData(); + updateViewData( + position, + statuses.getPairedItem(position).copyWithShowingContent(isShowing) + ); + } + + private void updateViewData(int position, StatusViewData.Concrete newViewData) { statuses.setPairedItem(position, newViewData); adapter.setItem(position, newViewData, true); } @@ -365,28 +366,11 @@ public final class ViewThreadFragment extends SFragment implements @Override public void onContentCollapsedChange(boolean isCollapsed, int position) { - if (position < 0 || position >= statuses.size()) { - Log.e(TAG, String.format("Tried to access out of bounds status position: %d of %d", position, statuses.size() - 1)); - return; - } - - StatusViewData.Concrete status = statuses.getPairedItem(position); - if (status == null) { - // Statuses PairedList contains a base type of StatusViewData.Concrete and also doesn't - // check for null values when adding values to it although this doesn't seem to be an issue. - Log.e(TAG, String.format( - "Expected StatusViewData.Concrete, got null instead at position: %d of %d", - position, - statuses.size() - 1 - )); - return; - } - - StatusViewData.Concrete updatedStatus = new StatusViewData.Builder(status) - .setCollapsed(isCollapsed) - .createStatusViewData(); - statuses.setPairedItem(position, updatedStatus); - recyclerView.post(() -> adapter.setItem(position, updatedStatus, true)); + adapter.setItem( + position, + statuses.getPairedItem(position).copyWIthCollapsed(isCollapsed), + true + ); } @Override @@ -412,28 +396,21 @@ public final class ViewThreadFragment extends SFragment implements public void onVoteInPoll(int position, @NonNull List choices) { final Status status = statuses.get(position).getActionableStatus(); - setVoteForPoll(position, status.getPoll().votedCopy(choices)); + setVoteForPoll(status.getId(), status.getPoll().votedCopy(choices)); - timelineCases.voteInPoll(status, choices) + timelineCases.voteInPoll(status.getId(), status.getPoll().getId(), choices) .observeOn(AndroidSchedulers.mainThread()) .to(autoDisposable(from(this))) .subscribe( - (newPoll) -> setVoteForPoll(position, newPoll), + (newPoll) -> setVoteForPoll(status.getId(), newPoll), (t) -> Log.d(TAG, "Failed to vote in poll: " + status.getId(), t) ); } - private void setVoteForPoll(int position, Poll newPoll) { - - StatusViewData.Concrete viewData = statuses.getPairedItem(position); - - StatusViewData.Concrete newViewData = new StatusViewData.Builder(viewData) - .setPoll(newPoll) - .createStatusViewData(); - statuses.setPairedItem(position, newViewData); - adapter.setItem(position, newViewData, true); + private void setVoteForPoll(String statusId, Poll newPoll) { + updateStatus(statusId, s -> s.copyWithPoll(newPoll)); } private void removeAllByAccountId(String accountId) { @@ -530,7 +507,7 @@ public final class ViewThreadFragment extends SFragment implements ArrayList ancestors = new ArrayList<>(); for (Status status : unfilteredAncestors) - if (!shouldFilterStatus(status)) + if (!filterModel.shouldFilterStatus(status)) ancestors.add(status); // Insert newly fetched ancestors @@ -560,7 +537,7 @@ public final class ViewThreadFragment extends SFragment implements ArrayList descendants = new ArrayList<>(); for (Status status : unfilteredDescendants) - if (!shouldFilterStatus(status)) + if (!filterModel.shouldFilterStatus(status)) descendants.add(status); // Insert newly fetched descendants @@ -581,71 +558,31 @@ public final class ViewThreadFragment extends SFragment implements } private void handleFavEvent(FavoriteEvent event) { - Pair posAndStatus = findStatusAndPos(event.getStatusId()); - if (posAndStatus == null) return; - - boolean favourite = event.getFavourite(); - posAndStatus.second.setFavourited(favourite); - - if (posAndStatus.second.getReblog() != null) { - posAndStatus.second.getReblog().setFavourited(favourite); - } - - StatusViewData.Concrete viewdata = statuses.getPairedItem(posAndStatus.first); - - StatusViewData.Builder viewDataBuilder = new StatusViewData.Builder((viewdata)); - viewDataBuilder.setFavourited(favourite); - - StatusViewData.Concrete newViewData = viewDataBuilder.createStatusViewData(); - - statuses.setPairedItem(posAndStatus.first, newViewData); - adapter.setItem(posAndStatus.first, newViewData, true); + updateStatus(event.getStatusId(), (s) -> { + s.setFavourited(event.getFavourite()); + return s; + }); } private void handleReblogEvent(ReblogEvent event) { - Pair posAndStatus = findStatusAndPos(event.getStatusId()); - if (posAndStatus == null) return; - - boolean reblog = event.getReblog(); - posAndStatus.second.setReblogged(reblog); - - if (posAndStatus.second.getReblog() != null) { - posAndStatus.second.getReblog().setReblogged(reblog); - } - - StatusViewData.Concrete viewdata = statuses.getPairedItem(posAndStatus.first); - - StatusViewData.Builder viewDataBuilder = new StatusViewData.Builder((viewdata)); - viewDataBuilder.setReblogged(reblog); - - StatusViewData.Concrete newViewData = viewDataBuilder.createStatusViewData(); - - statuses.setPairedItem(posAndStatus.first, newViewData); - adapter.setItem(posAndStatus.first, newViewData, true); + updateStatus(event.getStatusId(), (s) -> { + s.setReblogged(event.getReblog()); + return s; + }); } private void handleBookmarkEvent(BookmarkEvent event) { - Pair posAndStatus = findStatusAndPos(event.getStatusId()); - if (posAndStatus == null) return; - - boolean bookmark = event.getBookmark(); - posAndStatus.second.setBookmarked(bookmark); - - if (posAndStatus.second.getReblog() != null) { - posAndStatus.second.getReblog().setBookmarked(bookmark); - } - - StatusViewData.Concrete viewdata = statuses.getPairedItem(posAndStatus.first); - - StatusViewData.Builder viewDataBuilder = new StatusViewData.Builder((viewdata)); - viewDataBuilder.setBookmarked(bookmark); - - StatusViewData.Concrete newViewData = viewDataBuilder.createStatusViewData(); - - statuses.setPairedItem(posAndStatus.first, newViewData); - adapter.setItem(posAndStatus.first, newViewData, true); + updateStatus(event.getStatusId(), (s) -> { + s.setBookmarked(event.getBookmark()); + return s; + }); } + private void handlePinEvent(PinEvent event) { + updateStatus(event.getStatusId(), (s) -> s.copyWithPinned(event.getPinned())); + } + + private void handleStatusComposedEvent(StatusComposedEvent event) { Status eventStatus = event.getStatus(); if (eventStatus.getInReplyToId() == null) return; @@ -671,23 +608,16 @@ public final class ViewThreadFragment extends SFragment implements } private void handleStatusDeletedEvent(StatusDeletedEvent event) { - Pair posAndStatus = findStatusAndPos(event.getStatusId()); - if (posAndStatus == null) return; - - @SuppressWarnings("ConstantConditions") - int pos = posAndStatus.first; - statuses.remove(pos); - adapter.removeItem(pos); + int index = this.indexOfStatus(event.getStatusId()); + if (index != -1) { + statuses.remove(index); + adapter.removeItem(index); + } } - @Nullable - private Pair findStatusAndPos(@NonNull String statusId) { - for (int i = 0; i < statuses.size(); i++) { - if (statusId.equals(statuses.get(i).getId())) { - return new Pair<>(i, statuses.get(i)); - } - } - return null; + + private int indexOfStatus(String statusId) { + return CollectionsKt.indexOfFirst(this.statuses, (s) -> s.getId().equals(statusId)); } private void updateRevealIcon() { @@ -710,13 +640,25 @@ public final class ViewThreadFragment extends SFragment implements ViewThreadActivity.REVEAL_BUTTON_REVEAL); } - @Override - protected boolean filterIsRelevant(@NonNull Filter filter) { - return filter.getContext().contains(Filter.THREAD); + private void reloadFilters() { + mastodonApi.getFilters() + .to(autoDisposable(AndroidLifecycleScopeProvider.from(this))) + .subscribe( + (filters) -> { + List relevantFilters = CollectionsKt.filter( + filters, + (f) -> f.getContext().contains(Filter.THREAD) + ); + filterModel.initWithFilters(relevantFilters); + + recyclerView.post(this::applyFilters); + }, + (t) -> Log.e(TAG, "Failed to load filters", t) + ); } - @Override - protected void refreshAfterApplyingFilters() { - onRefresh(); + private void applyFilters() { + CollectionsKt.removeAll(this.statuses, filterModel::shouldFilterStatus); + adapter.setStatuses(this.statuses.getPairedCopy()); } } diff --git a/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java b/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java index ec37680c5..116e582c8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java +++ b/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java @@ -38,7 +38,7 @@ public interface StatusActionListener extends LinkListener { void onOpenReblog(int position); void onExpandedChange(boolean expanded, int position); void onContentHiddenChange(boolean isShowing, int position); - void onLoadMore(int position); + void onLoadMore(int position); /** * Called when the status {@link android.widget.ToggleButton} responsible for collapsing long diff --git a/app/src/main/java/com/keylesspalace/tusky/network/FilterModel.kt b/app/src/main/java/com/keylesspalace/tusky/network/FilterModel.kt new file mode 100644 index 000000000..3be20c1a9 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/network/FilterModel.kt @@ -0,0 +1,56 @@ +package com.keylesspalace.tusky.network + +import android.text.TextUtils +import com.keylesspalace.tusky.entity.Filter +import com.keylesspalace.tusky.entity.Status +import java.util.regex.Pattern +import javax.inject.Inject + +/** + * One-stop for status filtering logic using Mastodon's filters. + * + * 1. You init with [initWithFilters], this compiles regex pattern. + * 2. You call [shouldFilterStatus] to figure out what to display when you load statuses. + */ +class FilterModel @Inject constructor() { + private var pattern: Pattern? = null + + fun initWithFilters(filters: List) { + this.pattern = makeFilter(filters) + } + + fun shouldFilterStatus(status: Status): Boolean { + // Patterns are expensive and thread-safe, matchers are neither. + val matcher = pattern?.matcher("") ?: return false + + if (status.poll != null) { + val pollMatches = status.poll.options.any { matcher.reset(it.title).find() } + if (pollMatches) return true + } + + val spoilerText = status.actionableStatus.spoilerText + return (matcher.reset(status.actionableStatus.content).find() || + spoilerText.isNotEmpty() && matcher.reset(spoilerText).find()) + } + + private fun filterToRegexToken(filter: Filter): String? { + val phrase = filter.phrase + val quotedPhrase = Pattern.quote(phrase) + return if (filter.wholeWord && ALPHANUMERIC.matcher(phrase).matches()) { + String.format("(^|\\W)%s($|\\W)", quotedPhrase) + } else { + quotedPhrase + } + } + + private fun makeFilter(filters: List): Pattern? { + if (filters.isEmpty()) return null + val tokens = filters.map { filterToRegexToken(it) } + + return Pattern.compile(TextUtils.join("|", tokens), Pattern.CASE_INSENSITIVE); + } + + companion object { + private val ALPHANUMERIC = Pattern.compile("^\\w+$") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt index ae6fc3c2d..dfa681f57 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -49,7 +49,7 @@ interface MastodonApi { fun getInstance(): Single @GET("api/v1/filters") - fun getFilters(): Call> + fun getFilters(): Single> @GET("api/v1/timelines/home") fun homeTimeline( diff --git a/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt b/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt index f124faed1..96fff6f80 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt @@ -30,20 +30,20 @@ import java.lang.IllegalStateException */ interface TimelineCases { - fun reblog(status: Status, reblog: Boolean): Single - fun favourite(status: Status, favourite: Boolean): Single - fun bookmark(status: Status, bookmark: Boolean): Single - fun mute(id: String, notifications: Boolean, duration: Int?) - fun block(id: String) - fun delete(id: String): Single - fun pin(status: Status, pin: Boolean) - fun voteInPoll(status: Status, choices: List): Single - fun muteConversation(status: Status, mute: Boolean): Single + fun reblog(statusId: String, reblog: Boolean): Single + fun favourite(statusId: String, favourite: Boolean): Single + fun bookmark(statusId: String, bookmark: Boolean): Single + fun mute(statusId: String, notifications: Boolean, duration: Int?) + fun block(statusId: String) + fun delete(statusId: String): Single + fun pin(statusId: String, pin: Boolean): Single + fun voteInPoll(statusId: String, pollId: String, choices: List): Single + fun muteConversation(statusId: String, mute: Boolean): Single } class TimelineCasesImpl( - private val mastodonApi: MastodonApi, - private val eventHub: EventHub + private val mastodonApi: MastodonApi, + private val eventHub: EventHub ) : TimelineCases { /** @@ -52,103 +52,92 @@ class TimelineCasesImpl( */ private val cancelDisposable = CompositeDisposable() - override fun reblog(status: Status, reblog: Boolean): Single { - val id = status.actionableId - + override fun reblog(statusId: String, reblog: Boolean): Single { val call = if (reblog) { - mastodonApi.reblogStatus(id) + mastodonApi.reblogStatus(statusId) } else { - mastodonApi.unreblogStatus(id) + mastodonApi.unreblogStatus(statusId) } return call.doAfterSuccess { - eventHub.dispatch(ReblogEvent(status.id, reblog)) + eventHub.dispatch(ReblogEvent(statusId, reblog)) } } - override fun favourite(status: Status, favourite: Boolean): Single { - val id = status.actionableId - + override fun favourite(statusId: String, favourite: Boolean): Single { val call = if (favourite) { - mastodonApi.favouriteStatus(id) + mastodonApi.favouriteStatus(statusId) } else { - mastodonApi.unfavouriteStatus(id) + mastodonApi.unfavouriteStatus(statusId) } return call.doAfterSuccess { - eventHub.dispatch(FavoriteEvent(status.id, favourite)) + eventHub.dispatch(FavoriteEvent(statusId, favourite)) } } - override fun bookmark(status: Status, bookmark: Boolean): Single { - val id = status.actionableId - + override fun bookmark(statusId: String, bookmark: Boolean): Single { val call = if (bookmark) { - mastodonApi.bookmarkStatus(id) + mastodonApi.bookmarkStatus(statusId) } else { - mastodonApi.unbookmarkStatus(id) + mastodonApi.unbookmarkStatus(statusId) } return call.doAfterSuccess { - eventHub.dispatch(BookmarkEvent(status.id, bookmark)) + eventHub.dispatch(BookmarkEvent(statusId, bookmark)) } } - override fun muteConversation(status: Status, mute: Boolean): Single { - val id = status.actionableId - + override fun muteConversation(statusId: String, mute: Boolean): Single { val call = if (mute) { - mastodonApi.muteConversation(id) + mastodonApi.muteConversation(statusId) } else { - mastodonApi.unmuteConversation(id) + mastodonApi.unmuteConversation(statusId) } return call.doAfterSuccess { - eventHub.dispatch(MuteConversationEvent(status.id, mute)) + eventHub.dispatch(MuteConversationEvent(statusId, mute)) } } - override fun mute(id: String, notifications: Boolean, duration: Int?) { - mastodonApi.muteAccount(id, notifications, duration) - .subscribe({ - eventHub.dispatch(MuteEvent(id)) - }, { t -> - Log.w("Failed to mute account", t) - }) - .addTo(cancelDisposable) + override fun mute(statusId: String, notifications: Boolean, duration: Int?) { + mastodonApi.muteAccount(statusId, notifications, duration) + .subscribe({ + eventHub.dispatch(MuteEvent(statusId)) + }, { t -> + Log.w("Failed to mute account", t) + }) + .addTo(cancelDisposable) } - override fun block(id: String) { - mastodonApi.blockAccount(id) - .subscribe({ - eventHub.dispatch(BlockEvent(id)) - }, { t -> - Log.w("Failed to block account", t) - }) - .addTo(cancelDisposable) + override fun block(statusId: String) { + mastodonApi.blockAccount(statusId) + .subscribe({ + eventHub.dispatch(BlockEvent(statusId)) + }, { t -> + Log.w("Failed to block account", t) + }) + .addTo(cancelDisposable) } - override fun delete(id: String): Single { - return mastodonApi.deleteStatus(id) - .doAfterSuccess { - eventHub.dispatch(StatusDeletedEvent(id)) - } + override fun delete(statusId: String): Single { + return mastodonApi.deleteStatus(statusId) + .doAfterSuccess { + eventHub.dispatch(StatusDeletedEvent(statusId)) + } } - override fun pin(status: Status, pin: Boolean) { + override fun pin(statusId: String, pin: Boolean): Single { // Replace with extension method if we use RxKotlin - (if (pin) mastodonApi.pinStatus(status.id) else mastodonApi.unpinStatus(status.id)) - .subscribe({ updatedStatus -> - status.pinned = updatedStatus.pinned - }, {}) - .addTo(this.cancelDisposable) + return (if (pin) mastodonApi.pinStatus(statusId) else mastodonApi.unpinStatus(statusId)) + .doAfterSuccess { + eventHub.dispatch(PinEvent(statusId, pin)) + } } - override fun voteInPoll(status: Status, choices: List): Single { - val pollId = status.actionableStatus.poll?.id - - if(pollId == null || choices.isEmpty()) { + override fun voteInPoll(statusId: String, pollId: String, choices: List): Single { + if (choices.isEmpty()) { return Single.error(IllegalStateException()) } return mastodonApi.voteInPoll(pollId, choices).doAfterSuccess { - eventHub.dispatch(PollVoteEvent(status.id, it)) + eventHub.dispatch(PollVoteEvent(statusId, it)) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/pager/AccountPagerAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/pager/AccountPagerAdapter.kt index f8c026e05..e2e13c66b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/pager/AccountPagerAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/pager/AccountPagerAdapter.kt @@ -18,7 +18,8 @@ package com.keylesspalace.tusky.pager import androidx.fragment.app.* import com.keylesspalace.tusky.fragment.AccountMediaFragment -import com.keylesspalace.tusky.fragment.TimelineFragment +import com.keylesspalace.tusky.components.timeline.TimelineFragment +import com.keylesspalace.tusky.components.timeline.TimelineViewModel import com.keylesspalace.tusky.interfaces.RefreshableFragment import com.keylesspalace.tusky.util.CustomFragmentStateAdapter @@ -32,9 +33,9 @@ class AccountPagerAdapter( override fun createFragment(position: Int): Fragment { return when (position) { - 0 -> TimelineFragment.newInstance(TimelineFragment.Kind.USER, accountId, false) - 1 -> TimelineFragment.newInstance(TimelineFragment.Kind.USER_WITH_REPLIES, accountId, false) - 2 -> TimelineFragment.newInstance(TimelineFragment.Kind.USER_PINNED, accountId, false) + 0 -> TimelineFragment.newInstance(TimelineViewModel.Kind.USER, accountId, false) + 1 -> TimelineFragment.newInstance(TimelineViewModel.Kind.USER_WITH_REPLIES, accountId, false) + 2 -> TimelineFragment.newInstance(TimelineViewModel.Kind.USER_PINNED, accountId, false) 3 -> AccountMediaFragment.newInstance(accountId, false) else -> throw AssertionError("Page $position is out of AccountPagerAdapter bounds") } diff --git a/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt b/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt deleted file mode 100644 index d6f25a161..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt +++ /dev/null @@ -1,392 +0,0 @@ -package com.keylesspalace.tusky.repository - -import android.text.SpannedString -import androidx.core.text.parseAsHtml -import androidx.core.text.toHtml -import com.google.gson.Gson -import com.google.gson.reflect.TypeToken -import com.keylesspalace.tusky.db.* -import com.keylesspalace.tusky.entity.* -import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.repository.TimelineRequestMode.DISK -import com.keylesspalace.tusky.repository.TimelineRequestMode.NETWORK -import com.keylesspalace.tusky.util.Either -import com.keylesspalace.tusky.util.dec -import com.keylesspalace.tusky.util.inc -import com.keylesspalace.tusky.util.trimTrailingWhitespace -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.schedulers.Schedulers -import java.io.IOException -import java.util.* -import java.util.concurrent.TimeUnit -import kotlin.collections.ArrayList - -data class Placeholder(val id: String) - -typealias TimelineStatus = Either - -enum class TimelineRequestMode { - DISK, NETWORK, ANY -} - -interface TimelineRepository { - fun getStatuses(maxId: String?, sinceId: String?, sincedIdMinusOne: String?, limit: Int, - requestMode: TimelineRequestMode): Single> - - companion object { - val CLEANUP_INTERVAL = TimeUnit.DAYS.toMillis(14) - } -} - -class TimelineRepositoryImpl( - private val timelineDao: TimelineDao, - private val mastodonApi: MastodonApi, - private val accountManager: AccountManager, - private val gson: Gson -) : TimelineRepository { - - init { - this.cleanup() - } - - override fun getStatuses(maxId: String?, sinceId: String?, sincedIdMinusOne: String?, - limit: Int, requestMode: TimelineRequestMode - ): Single> { - val acc = accountManager.activeAccount ?: throw IllegalStateException() - val accountId = acc.id - - return if (requestMode == DISK) { - this.getStatusesFromDb(accountId, maxId, sinceId, limit) - } else { - getStatusesFromNetwork(maxId, sinceId, sincedIdMinusOne, limit, accountId, requestMode) - } - } - - private fun getStatusesFromNetwork(maxId: String?, sinceId: String?, - sinceIdMinusOne: String?, limit: Int, - accountId: Long, requestMode: TimelineRequestMode - ): Single> { - return mastodonApi.homeTimeline(maxId, sinceIdMinusOne, limit + 1) - .map { response -> - this.saveStatusesToDb(accountId, response.body().orEmpty(), maxId, sinceId) - } - .flatMap { statuses -> - this.addFromDbIfNeeded(accountId, statuses, maxId, sinceId, limit, requestMode) - } - .onErrorResumeNext { error -> - if (error is IOException && requestMode != NETWORK) { - this.getStatusesFromDb(accountId, maxId, sinceId, limit) - } else { - Single.error(error) - } - } - } - - private fun addFromDbIfNeeded(accountId: Long, statuses: List>, - maxId: String?, sinceId: String?, limit: Int, - requestMode: TimelineRequestMode - ): Single> { - return if (requestMode != NETWORK && statuses.size < 2) { - val newMaxID = if (statuses.isEmpty()) { - maxId - } else { - statuses.last { it.isRight() }.asRight().id - } - this.getStatusesFromDb(accountId, newMaxID, sinceId, limit) - .map { fromDb -> - // If it's just placeholders and less than limit (so we exhausted both - // db and server at this point) - if (fromDb.size < limit && fromDb.all { !it.isRight() }) { - statuses - } else { - statuses + fromDb - } - } - } else { - Single.just(statuses) - } - } - - private fun getStatusesFromDb(accountId: Long, maxId: String?, sinceId: String?, - limit: Int): Single> { - return timelineDao.getStatusesForAccount(accountId, maxId, sinceId, limit) - .subscribeOn(Schedulers.io()) - .map { statuses -> - statuses.map { it.toStatus() } - } - } - - private fun saveStatusesToDb(accountId: Long, statuses: List, - maxId: String?, sinceId: String? - ): List> { - var placeholderToInsert: Placeholder? = null - - // Look for overlap - val resultStatuses = if (statuses.isNotEmpty() && sinceId != null) { - val indexOfSince = statuses.indexOfLast { it.id == sinceId } - if (indexOfSince == -1) { - // We didn't find the status which must be there. Add a placeholder - placeholderToInsert = Placeholder(sinceId.inc()) - statuses.mapTo(mutableListOf(), Status::lift) - .apply { - add(Either.Left(placeholderToInsert)) - } - } else { - // There was an overlap. Remove all overlapped statuses. No need for a placeholder. - statuses.mapTo(mutableListOf(), Status::lift) - .apply { - subList(indexOfSince, size).clear() - } - } - } else { - // Just a normal case. - statuses.map(Status::lift) - } - - Single.fromCallable { - - if(statuses.isNotEmpty()) { - timelineDao.deleteRange(accountId, statuses.last().id, statuses.first().id) - } - - for (status in statuses) { - timelineDao.insertInTransaction( - status.toEntity(accountId, gson), - status.account.toEntity(accountId, gson), - status.reblog?.account?.toEntity(accountId, gson) - ) - } - - placeholderToInsert?.let { - timelineDao.insertStatusIfNotThere(placeholderToInsert.toEntity(accountId)) - } - - // If we're loading in the bottom insert placeholder after every load - // (for requests on next launches) but not return it. - if (sinceId == null && statuses.isNotEmpty()) { - timelineDao.insertStatusIfNotThere( - Placeholder(statuses.last().id.dec()).toEntity(accountId)) - } - - // There may be placeholders which we thought could be from our TL but they are not - if (statuses.size > 2) { - timelineDao.removeAllPlaceholdersBetween(accountId, statuses.first().id, - statuses.last().id) - } else if (placeholderToInsert == null && maxId != null && sinceId != null) { - timelineDao.removeAllPlaceholdersBetween(accountId, maxId, sinceId) - } - } - .subscribeOn(Schedulers.io()) - .subscribe() - - return resultStatuses - } - - private fun cleanup() { - Schedulers.io().scheduleDirect { - val olderThan = System.currentTimeMillis() - TimelineRepository.CLEANUP_INTERVAL - timelineDao.cleanup(olderThan) - } - } - - private fun TimelineStatusWithAccount.toStatus(): TimelineStatus { - if (this.status.authorServerId == null) { - return Either.Left(Placeholder(this.status.serverId)) - } - - val attachments: ArrayList = gson.fromJson(status.attachments, - object : TypeToken>() {}.type) ?: ArrayList() - val mentions: Array = gson.fromJson(status.mentions, - Array::class.java) ?: arrayOf() - val application = gson.fromJson(status.application, Status.Application::class.java) - val emojis: List = gson.fromJson(status.emojis, - object : TypeToken>() {}.type) ?: listOf() - val poll: Poll? = gson.fromJson(status.poll, Poll::class.java) - - val reblog = status.reblogServerId?.let { id -> - Status( - id = id, - url = status.url, - account = account.toAccount(gson), - inReplyToId = status.inReplyToId, - inReplyToAccountId = status.inReplyToAccountId, - reblog = null, - content = status.content?.parseAsHtml()?.trimTrailingWhitespace() ?: SpannedString(""), - createdAt = Date(status.createdAt), - emojis = emojis, - reblogsCount = status.reblogsCount, - favouritesCount = status.favouritesCount, - reblogged = status.reblogged, - favourited = status.favourited, - bookmarked = status.bookmarked, - sensitive = status.sensitive, - spoilerText = status.spoilerText!!, - visibility = status.visibility!!, - attachments = attachments, - mentions = mentions, - application = application, - pinned = false, - muted = status.muted, - poll = poll, - card = null - ) - } - val status = if (reblog != null) { - Status( - id = status.serverId, - url = null, // no url for reblogs - account = this.reblogAccount!!.toAccount(gson), - inReplyToId = null, - inReplyToAccountId = null, - reblog = reblog, - content = SpannedString(""), - createdAt = Date(status.createdAt), // lie but whatever? - emojis = listOf(), - reblogsCount = 0, - favouritesCount = 0, - reblogged = false, - favourited = false, - bookmarked = false, - sensitive = false, - spoilerText = "", - visibility = status.visibility!!, - attachments = ArrayList(), - mentions = arrayOf(), - application = null, - pinned = false, - muted = status.muted, - poll = null, - card = null - ) - } else { - Status( - id = status.serverId, - url = status.url, - account = account.toAccount(gson), - inReplyToId = status.inReplyToId, - inReplyToAccountId = status.inReplyToAccountId, - reblog = null, - content = status.content?.parseAsHtml()?.trimTrailingWhitespace() ?: SpannedString(""), - createdAt = Date(status.createdAt), - emojis = emojis, - reblogsCount = status.reblogsCount, - favouritesCount = status.favouritesCount, - reblogged = status.reblogged, - favourited = status.favourited, - bookmarked = status.bookmarked, - sensitive = status.sensitive, - spoilerText = status.spoilerText!!, - visibility = status.visibility!!, - attachments = attachments, - mentions = mentions, - application = application, - pinned = false, - muted = status.muted, - poll = poll, - card = null - ) - } - return Either.Right(status) - } -} - -private val emojisListTypeToken = object : TypeToken>() {} - -fun Account.toEntity(accountId: Long, gson: Gson): TimelineAccountEntity { - return TimelineAccountEntity( - serverId = id, - timelineUserId = accountId, - localUsername = localUsername, - username = username, - displayName = name, - url = url, - avatar = avatar, - emojis = gson.toJson(emojis), - bot = bot - ) -} - -fun TimelineAccountEntity.toAccount(gson: Gson): Account { - return Account( - id = serverId, - localUsername = localUsername, - username = username, - displayName = displayName, - note = SpannedString(""), - url = url, - avatar = avatar, - header = "", - locked = false, - followingCount = 0, - followersCount = 0, - statusesCount = 0, - source = null, - bot = bot, - emojis = gson.fromJson(this.emojis, emojisListTypeToken.type), - fields = null, - moved = null - ) -} - - -fun Placeholder.toEntity(timelineUserId: Long): TimelineStatusEntity { - return TimelineStatusEntity( - serverId = this.id, - url = null, - timelineUserId = timelineUserId, - authorServerId = null, - inReplyToId = null, - inReplyToAccountId = null, - content = null, - createdAt = 0L, - emojis = null, - reblogsCount = 0, - favouritesCount = 0, - reblogged = false, - favourited = false, - bookmarked = false, - sensitive = false, - spoilerText = null, - visibility = null, - attachments = null, - mentions = null, - application = null, - reblogServerId = null, - reblogAccountId = null, - poll = null, - muted = false - ) -} - -fun Status.toEntity(timelineUserId: Long, - gson: Gson): TimelineStatusEntity { - val actionable = actionableStatus - return TimelineStatusEntity( - serverId = this.id, - url = actionable.url!!, - timelineUserId = timelineUserId, - authorServerId = actionable.account.id, - inReplyToId = actionable.inReplyToId, - inReplyToAccountId = actionable.inReplyToAccountId, - content = actionable.content.toHtml(), - createdAt = actionable.createdAt.time, - emojis = actionable.emojis.let(gson::toJson), - reblogsCount = actionable.reblogsCount, - favouritesCount = actionable.favouritesCount, - reblogged = actionable.reblogged, - favourited = actionable.favourited, - bookmarked = actionable.bookmarked, - sensitive = actionable.sensitive, - spoilerText = actionable.spoilerText, - visibility = actionable.visibility, - attachments = actionable.attachments.let(gson::toJson), - mentions = actionable.mentions.let(gson::toJson), - application = actionable.application.let(gson::toJson), - reblogServerId = reblog?.id, - reblogAccountId = reblog?.let { this.account.id }, - poll = actionable.poll.let(gson::toJson), - muted = actionable.muted - ) -} - -fun Status.lift(): Either = Either.Right(this) diff --git a/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java b/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java index ec0c8a3e2..b0a475742 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java @@ -40,6 +40,7 @@ import com.keylesspalace.tusky.interfaces.LinkListener; import java.net.URI; import java.net.URISyntaxException; +import java.util.List; public class LinkHelper { public static String getDomain(String urlString) { @@ -69,7 +70,7 @@ public class LinkHelper { * @param listener to notify about particular spans that are clicked */ public static void setClickableText(TextView view, CharSequence content, - @Nullable Status.Mention[] mentions, final LinkListener listener) { + @Nullable List mentions, final LinkListener listener) { SpannableStringBuilder builder = SpannableStringBuilder.valueOf(content); URLSpan[] urlSpans = builder.getSpans(0, content.length(), URLSpan.class); for (URLSpan span : urlSpans) { @@ -85,7 +86,7 @@ public class LinkHelper { @Override public void onClick(@NonNull View widget) { listener.onViewTag(tag); } }; - } else if (text.charAt(0) == '@' && mentions != null && mentions.length > 0) { + } else if (text.charAt(0) == '@' && mentions != null && mentions.size() > 0) { String accountUsername = text.subSequence(1, text.length()).toString(); /* There may be multiple matches for users on different instances with the same * username. If a match has the same domain we know it's for sure the same, but if @@ -141,8 +142,8 @@ public class LinkHelper { * @param listener to notify about particular spans that are clicked */ public static void setClickableMentions( - TextView view, @Nullable Status.Mention[] mentions, final LinkListener listener) { - if (mentions == null || mentions.length == 0) { + TextView view, @Nullable List mentions, final LinkListener listener) { + if (mentions == null || mentions.size() == 0) { view.setText(null); return; } diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ListStatusAccessibilityDelegate.kt b/app/src/main/java/com/keylesspalace/tusky/util/ListStatusAccessibilityDelegate.kt index 859162da3..93e0c67bb 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ListStatusAccessibilityDelegate.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/ListStatusAccessibilityDelegate.kt @@ -27,9 +27,9 @@ fun interface StatusProvider { } class ListStatusAccessibilityDelegate( - private val recyclerView: RecyclerView, - private val statusActionListener: StatusActionListener, - private val statusProvider: StatusProvider + private val recyclerView: RecyclerView, + private val statusActionListener: StatusActionListener, + private val statusProvider: StatusProvider ) : RecyclerViewAccessibilityDelegate(recyclerView) { private val a11yManager = context.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager @@ -39,8 +39,10 @@ class ListStatusAccessibilityDelegate( private val context: Context get() = recyclerView.context private val itemDelegate = object : RecyclerViewAccessibilityDelegate.ItemDelegate(this) { - override fun onInitializeAccessibilityNodeInfo(host: View, - info: AccessibilityNodeInfoCompat) { + override fun onInitializeAccessibilityNodeInfo( + host: View, + info: AccessibilityNodeInfoCompat + ) { super.onInitializeAccessibilityNodeInfo(host, info) val pos = recyclerView.getChildAdapterPosition(host) @@ -52,44 +54,51 @@ class ListStatusAccessibilityDelegate( info.addAction(replyAction) - if (status.rebloggingEnabled) { - info.addAction(if (status.isReblogged) unreblogAction else reblogAction) + val actionable = status.actionable + if (actionable.rebloggingAllowed()) { + info.addAction(if (actionable.reblogged) unreblogAction else reblogAction) } - info.addAction(if (status.isFavourited) unfavouriteAction else favouriteAction) - info.addAction(if (status.isBookmarked) unbookmarkAction else bookmarkAction) + info.addAction(if (actionable.favourited) unfavouriteAction else favouriteAction) + info.addAction(if (actionable.bookmarked) unbookmarkAction else bookmarkAction) val mediaActions = intArrayOf( - R.id.action_open_media_1, - R.id.action_open_media_2, - R.id.action_open_media_3, - R.id.action_open_media_4) - val attachmentCount = min(status.attachments.size, MAX_MEDIA_ATTACHMENTS) + R.id.action_open_media_1, + R.id.action_open_media_2, + R.id.action_open_media_3, + R.id.action_open_media_4 + ) + val attachmentCount = min(actionable.attachments.size, MAX_MEDIA_ATTACHMENTS) for (i in 0 until attachmentCount) { - info.addAction(AccessibilityActionCompat( + info.addAction( + AccessibilityActionCompat( mediaActions[i], - context.getString(R.string.action_open_media_n, i + 1))) + context.getString(R.string.action_open_media_n, i + 1) + ) + ) } info.addAction(openProfileAction) if (getLinks(status).any()) info.addAction(linksAction) - val mentions = status.mentions - if (mentions != null && mentions.isNotEmpty()) info.addAction(mentionsAction) + val mentions = actionable.mentions + if (mentions.isNotEmpty()) info.addAction(mentionsAction) if (getHashtags(status).any()) info.addAction(hashtagsAction) - if (!status.rebloggedByUsername.isNullOrEmpty()) { + if (!status.status.reblog?.account?.username.isNullOrEmpty()) { info.addAction(openRebloggerAction) } - if (status.reblogsCount > 0) info.addAction(openRebloggedByAction) - if (status.favouritesCount > 0) info.addAction(openFavsAction) + if (actionable.reblogsCount > 0) info.addAction(openRebloggedByAction) + if (actionable.favouritesCount > 0) info.addAction(openFavsAction) info.addAction(moreAction) } } - override fun performAccessibilityAction(host: View, action: Int, - args: Bundle?): Boolean { + override fun performAccessibilityAction( + host: View, action: Int, + args: Bundle? + ): Boolean { val pos = recyclerView.getChildAdapterPosition(host) when (action) { R.id.action_reply -> { @@ -105,7 +114,8 @@ class ListStatusAccessibilityDelegate( R.id.action_open_profile -> { interrupt() statusActionListener.onViewAccount( - (statusProvider.getStatus(pos) as StatusViewData.Concrete).senderId) + (statusProvider.getStatus(pos) as StatusViewData.Concrete).actionable.account.id + ) } R.id.action_open_media_1 -> { interrupt() @@ -166,43 +176,51 @@ class ListStatusAccessibilityDelegate( val links = getLinks(status).toList() val textLinks = links.map { item -> item.link } AlertDialog.Builder(host.context) - .setTitle(R.string.title_links_dialog) - .setAdapter(ArrayAdapter( - host.context, - android.R.layout.simple_list_item_1, - textLinks) - ) { _, which -> LinkHelper.openLink(links[which].link, host.context) } - .show() - .let { forceFocus(it.listView) } + .setTitle(R.string.title_links_dialog) + .setAdapter( + ArrayAdapter( + host.context, + android.R.layout.simple_list_item_1, + textLinks + ) + ) { _, which -> LinkHelper.openLink(links[which].link, host.context) } + .show() + .let { forceFocus(it.listView) } } private fun showMentionsDialog(host: View) { val status = getStatus(host) as? StatusViewData.Concrete ?: return - val mentions = status.mentions ?: return + val mentions = status.actionable.mentions val stringMentions = mentions.map { it.username } AlertDialog.Builder(host.context) - .setTitle(R.string.title_mentions_dialog) - .setAdapter(ArrayAdapter(host.context, - android.R.layout.simple_list_item_1, stringMentions) - ) { _, which -> - statusActionListener.onViewAccount(mentions[which].id) - } - .show() - .let { forceFocus(it.listView) } + .setTitle(R.string.title_mentions_dialog) + .setAdapter( + ArrayAdapter( + host.context, + android.R.layout.simple_list_item_1, stringMentions + ) + ) { _, which -> + statusActionListener.onViewAccount(mentions[which].id) + } + .show() + .let { forceFocus(it.listView) } } private fun showHashtagsDialog(host: View) { val status = getStatus(host) as? StatusViewData.Concrete ?: return val tags = getHashtags(status).map { it.subSequence(1, it.length) }.toList() AlertDialog.Builder(host.context) - .setTitle(R.string.title_hashtags_dialog) - .setAdapter(ArrayAdapter(host.context, - android.R.layout.simple_list_item_1, tags) - ) { _, which -> - statusActionListener.onViewTag(tags[which].toString()) - } - .show() - .let { forceFocus(it.listView) } + .setTitle(R.string.title_hashtags_dialog) + .setAdapter( + ArrayAdapter( + host.context, + android.R.layout.simple_list_item_1, tags + ) + ) { _, which -> + statusActionListener.onViewTag(tags[which].toString()) + } + .show() + .let { forceFocus(it.listView) } } private fun getStatus(childView: View): StatusViewData { @@ -215,14 +233,15 @@ class ListStatusAccessibilityDelegate( val content = status.content return if (content is Spannable) { content.getSpans(0, content.length, URLSpan::class.java) - .asSequence() - .map { span -> - val text = content.subSequence( - content.getSpanStart(span), - content.getSpanEnd(span)) - if (isHashtag(text)) null else LinkSpanInfo(text.toString(), span.url) - } - .filterNotNull() + .asSequence() + .map { span -> + val text = content.subSequence( + content.getSpanStart(span), + content.getSpanEnd(span) + ) + if (isHashtag(text)) null else LinkSpanInfo(text.toString(), span.url) + } + .filterNotNull() } else { emptySequence() } @@ -231,11 +250,11 @@ class ListStatusAccessibilityDelegate( private fun getHashtags(status: StatusViewData.Concrete): Sequence { val content = status.content return content.getSpans(0, content.length, Object::class.java) - .asSequence() - .map { span -> - content.subSequence(content.getSpanStart(span), content.getSpanEnd(span)) - } - .filter(this::isHashtag) + .asSequence() + .map { span -> + content.subSequence(content.getSpanStart(span), content.getSpanEnd(span)) + } + .filter(this::isHashtag) } private fun forceFocus(host: View) { @@ -253,72 +272,88 @@ class ListStatusAccessibilityDelegate( private fun isHashtag(text: CharSequence) = text.startsWith("#") private val collapseCwAction = AccessibilityActionCompat( - R.id.action_collapse_cw, - context.getString(R.string.status_content_warning_show_less)) + R.id.action_collapse_cw, + context.getString(R.string.status_content_warning_show_less) + ) private val expandCwAction = AccessibilityActionCompat( - R.id.action_expand_cw, - context.getString(R.string.status_content_warning_show_more)) + R.id.action_expand_cw, + context.getString(R.string.status_content_warning_show_more) + ) private val replyAction = AccessibilityActionCompat( - R.id.action_reply, - context.getString(R.string.action_reply)) + R.id.action_reply, + context.getString(R.string.action_reply) + ) private val unreblogAction = AccessibilityActionCompat( - R.id.action_unreblog, - context.getString(R.string.action_unreblog)) + R.id.action_unreblog, + context.getString(R.string.action_unreblog) + ) private val reblogAction = AccessibilityActionCompat( - R.id.action_reblog, - context.getString(R.string.action_reblog)) + R.id.action_reblog, + context.getString(R.string.action_reblog) + ) private val unfavouriteAction = AccessibilityActionCompat( - R.id.action_unfavourite, - context.getString(R.string.action_unfavourite)) + R.id.action_unfavourite, + context.getString(R.string.action_unfavourite) + ) private val favouriteAction = AccessibilityActionCompat( - R.id.action_favourite, - context.getString(R.string.action_favourite)) + R.id.action_favourite, + context.getString(R.string.action_favourite) + ) private val bookmarkAction = AccessibilityActionCompat( - R.id.action_bookmark, - context.getString(R.string.action_bookmark)) + R.id.action_bookmark, + context.getString(R.string.action_bookmark) + ) private val unbookmarkAction = AccessibilityActionCompat( - R.id.action_unbookmark, - context.getString(R.string.action_bookmark)) + R.id.action_unbookmark, + context.getString(R.string.action_bookmark) + ) private val openProfileAction = AccessibilityActionCompat( - R.id.action_open_profile, - context.getString(R.string.action_view_profile)) + R.id.action_open_profile, + context.getString(R.string.action_view_profile) + ) private val linksAction = AccessibilityActionCompat( - R.id.action_links, - context.getString(R.string.action_links)) + R.id.action_links, + context.getString(R.string.action_links) + ) private val mentionsAction = AccessibilityActionCompat( - R.id.action_mentions, - context.getString(R.string.action_mentions)) + R.id.action_mentions, + context.getString(R.string.action_mentions) + ) private val hashtagsAction = AccessibilityActionCompat( - R.id.action_hashtags, - context.getString(R.string.action_hashtags)) + R.id.action_hashtags, + context.getString(R.string.action_hashtags) + ) private val openRebloggerAction = AccessibilityActionCompat( - R.id.action_open_reblogger, - context.getString(R.string.action_open_reblogger)) + R.id.action_open_reblogger, + context.getString(R.string.action_open_reblogger) + ) private val openRebloggedByAction = AccessibilityActionCompat( - R.id.action_open_reblogged_by, - context.getString(R.string.action_open_reblogged_by)) + R.id.action_open_reblogged_by, + context.getString(R.string.action_open_reblogged_by) + ) private val openFavsAction = AccessibilityActionCompat( - R.id.action_open_faved_by, - context.getString(R.string.action_open_faved_by)) + R.id.action_open_faved_by, + context.getString(R.string.action_open_faved_by) + ) private val moreAction = AccessibilityActionCompat( - R.id.action_more, - context.getString(R.string.action_more) + R.id.action_more, + context.getString(R.string.action_more) ) private data class LinkSpanInfo(val text: String, val link: String) diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ListUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/ListUtils.kt index 8a5223ce1..28ef0c63a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ListUtils.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/ListUtils.kt @@ -52,4 +52,8 @@ inline fun List.replacedFirstWhich(replacement: T, predicate: (T) -> Bool newList[index] = replacement } return newList +} + +inline fun Iterable<*>.firstIsInstanceOrNull(): R? { + return firstOrNull { it is R }?.let { it as R } } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/util/StringUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/StringUtils.kt index 83eaeafad..23423b591 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/StringUtils.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/StringUtils.kt @@ -73,6 +73,15 @@ fun String.isLessThan(other: String): Boolean { } } +fun String.idCompareTo(other: String): Int { + return when { + this === other -> 0 + this.length < other.length -> -1 + this.length > other.length -> 1 + else -> this.compareTo(other) + } +} + fun Spanned.trimTrailingWhitespace(): Spanned { var i = length do { diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java deleted file mode 100644 index 2e8e67efc..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java +++ /dev/null @@ -1,86 +0,0 @@ -/* Copyright 2017 Andrew Dawson - * - * This file is a part of Tusky. - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky; if not, - * see . */ - -package com.keylesspalace.tusky.util; - -import androidx.annotation.Nullable; - -import com.keylesspalace.tusky.entity.Notification; -import com.keylesspalace.tusky.entity.Status; -import com.keylesspalace.tusky.viewdata.NotificationViewData; -import com.keylesspalace.tusky.viewdata.StatusViewData; - -/** - * Created by charlag on 12/07/2017. - */ - -public final class ViewDataUtils { - @Nullable - public static StatusViewData.Concrete statusToViewData(@Nullable Status status, - boolean alwaysShowSensitiveMedia, - boolean alwaysOpenSpoiler) { - if (status == null) return null; - Status visibleStatus = status.getReblog() == null ? status : status.getReblog(); - return new StatusViewData.Builder().setId(status.getId()) - .setAttachments(visibleStatus.getAttachments()) - .setAvatar(visibleStatus.getAccount().getAvatar()) - .setContent(visibleStatus.getContent()) - .setCreatedAt(visibleStatus.getCreatedAt()) - .setReblogsCount(visibleStatus.getReblogsCount()) - .setFavouritesCount(visibleStatus.getFavouritesCount()) - .setInReplyToId(visibleStatus.getInReplyToId()) - .setFavourited(visibleStatus.getFavourited()) - .setBookmarked(visibleStatus.getBookmarked()) - .setReblogged(visibleStatus.getReblogged()) - .setIsExpanded(alwaysOpenSpoiler) - .setIsShowingSensitiveContent(false) - .setMentions(visibleStatus.getMentions()) - .setNickname(visibleStatus.getAccount().getUsername()) - .setRebloggedAvatar(status.getReblog() == null ? null : status.getAccount().getAvatar()) - .setSensitive(visibleStatus.getSensitive()) - .setIsShowingSensitiveContent(alwaysShowSensitiveMedia || !visibleStatus.getSensitive()) - .setSpoilerText(visibleStatus.getSpoilerText()) - .setRebloggedByUsername(status.getReblog() == null ? null : status.getAccount().getName()) - .setUserFullName(visibleStatus.getAccount().getName()) - .setVisibility(visibleStatus.getVisibility()) - .setSenderId(visibleStatus.getAccount().getId()) - .setRebloggingEnabled(visibleStatus.rebloggingAllowed()) - .setApplication(visibleStatus.getApplication()) - .setStatusEmojis(visibleStatus.getEmojis()) - .setAccountEmojis(visibleStatus.getAccount().getEmojis()) - .setRebloggedByEmojis(status.getReblog() == null ? null : status.getAccount().getEmojis()) - .setCollapsible(SmartLengthInputFilterKt.shouldTrimStatus(visibleStatus.getContent())) - .setCollapsed(true) - .setPoll(visibleStatus.getPoll()) - .setCard(visibleStatus.getCard()) - .setIsBot(visibleStatus.getAccount().getBot()) - .createStatusViewData(); - } - - public static NotificationViewData.Concrete notificationToViewData(Notification notification, - boolean alwaysShowSensitiveData, - boolean alwaysOpenSpoiler) { - return new NotificationViewData.Concrete( - notification.getType(), - notification.getId(), - notification.getAccount(), - statusToViewData( - notification.getStatus(), - alwaysShowSensitiveData, - alwaysOpenSpoiler - ) - ); - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.kt new file mode 100644 index 000000000..21e522f03 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.kt @@ -0,0 +1,53 @@ +@file:JvmName("ViewDataUtils") + +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ +package com.keylesspalace.tusky.util + +import com.keylesspalace.tusky.entity.Notification +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.viewdata.NotificationViewData +import com.keylesspalace.tusky.viewdata.StatusViewData +import com.keylesspalace.tusky.viewdata.toViewData +import java.util.* + +@JvmName("statusToViewData") +fun Status.toViewData( + alwaysShowSensitiveMedia: Boolean, + alwaysOpenSpoiler: Boolean +): StatusViewData.Concrete { + val visibleStatus = this.reblog ?: this + + return StatusViewData.Concrete( + status = this, + isShowingContent = alwaysShowSensitiveMedia || !visibleStatus.sensitive, + isCollapsible = shouldTrimStatus(visibleStatus.content), + isCollapsed = false, + isExpanded = alwaysOpenSpoiler, + ) +} + +@JvmName("notificationToViewData") +fun Notification.toViewData( + alwaysShowSensitiveData: Boolean, + alwaysOpenSpoiler: Boolean +): NotificationViewData.Concrete { + return NotificationViewData.Concrete( + this.type, + this.id, + this.account, + this.status?.toViewData(alwaysShowSensitiveData, alwaysOpenSpoiler) + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/view/ConversationLineItemDecoration.kt b/app/src/main/java/com/keylesspalace/tusky/view/ConversationLineItemDecoration.kt index 4011d69d3..7be8d06e5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/view/ConversationLineItemDecoration.kt +++ b/app/src/main/java/com/keylesspalace/tusky/view/ConversationLineItemDecoration.kt @@ -47,13 +47,13 @@ class ConversationLineItemDecoration(private val context: Context) : RecyclerVie val dividerBottom: Int if (current != null) { val above = adapter.getItem(position - 1) - dividerTop = if (above != null && above.id == current.inReplyToId) { + dividerTop = if (above != null && above.id == current.status.inReplyToId) { child.top } else { child.top + avatarMargin } val below = adapter.getItem(position + 1) - dividerBottom = if (below != null && current.id == below.inReplyToId && + dividerBottom = if (below != null && current.id == below.status.inReplyToId && adapter.detailedStatusPosition != position) { child.bottom } else { diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/AttachmentViewData.kt b/app/src/main/java/com/keylesspalace/tusky/viewdata/AttachmentViewData.kt index a7b2bffc7..f2e42e404 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewdata/AttachmentViewData.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/AttachmentViewData.kt @@ -19,12 +19,5 @@ data class AttachmentViewData( AttachmentViewData(it, actionable.id, actionable.url!!) } } - - fun list(attachments: List): List { - return attachments.map { - AttachmentViewData(it, it.id, it.url) - } - } - } } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java b/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java index 3256c1595..409b858d2 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java @@ -86,9 +86,7 @@ public abstract class NotificationViewData { return type == concrete.type && Objects.equals(id, concrete.id) && account.getId().equals(concrete.account.getId()) && - (statusViewData == concrete.statusViewData || - statusViewData != null && - statusViewData.deepEquals(concrete.statusViewData)); + (Objects.equals(statusViewData, concrete.statusViewData)); } @Override @@ -96,6 +94,10 @@ public abstract class NotificationViewData { return Objects.hash(type, id, account, statusViewData); } + + public Concrete copyWithStatus(@Nullable StatusViewData.Concrete statusViewData) { + return new Concrete(type, id, account, statusViewData); + } } public static final class Placeholder extends NotificationViewData { diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java deleted file mode 100644 index 10820fbdd..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java +++ /dev/null @@ -1,677 +0,0 @@ -/* Copyright 2017 Andrew Dawson - * - * This file is a part of Tusky. - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky; if not, - * see . */ - -package com.keylesspalace.tusky.viewdata; - -import android.os.Build; -import android.text.SpannableStringBuilder; -import android.text.Spanned; - -import androidx.annotation.Nullable; - -import com.keylesspalace.tusky.entity.Attachment; -import com.keylesspalace.tusky.entity.Card; -import com.keylesspalace.tusky.entity.Emoji; -import com.keylesspalace.tusky.entity.Poll; -import com.keylesspalace.tusky.entity.Status; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Date; -import java.util.List; -import java.util.Objects; - -/** - * Created by charlag on 11/07/2017. - *

- * Class to represent data required to display either a notification or a placeholder. - * It is either a {@link StatusViewData.Concrete} or a {@link StatusViewData.Placeholder}. - */ - -public abstract class StatusViewData { - - private StatusViewData() { } - - public abstract long getViewDataId(); - - public abstract boolean deepEquals(StatusViewData other); - - public static final class Concrete extends StatusViewData { - private static final char SOFT_HYPHEN = '\u00ad'; - private static final char ASCII_HYPHEN = '-'; - - private final String id; - private final Spanned content; - final boolean reblogged; - final boolean favourited; - final boolean bookmarked; - private final boolean muted; - @Nullable - private final String spoilerText; - private final Status.Visibility visibility; - private final List attachments; - @Nullable - private final String rebloggedByUsername; - @Nullable - private final String rebloggedAvatar; - private final boolean isSensitive; - final boolean isExpanded; - private final boolean isShowingContent; - private final String userFullName; - private final String nickname; - private final String avatar; - private final Date createdAt; - private final int reblogsCount; - private final int favouritesCount; - @Nullable - private final String inReplyToId; - // I would rather have something else but it would be too much of a rewrite - @Nullable - private final Status.Mention[] mentions; - private final String senderId; - private final boolean rebloggingEnabled; - private final Status.Application application; - private final List statusEmojis; - private final List accountEmojis; - private final List rebloggedByAccountEmojis; - @Nullable - private final Card card; - private final boolean isCollapsible; /** Whether the status meets the requirement to be collapse */ - final boolean isCollapsed; /** Whether the status is shown partially or fully */ - @Nullable - private final PollViewData poll; - private final boolean isBot; - - public Concrete(String id, Spanned content, boolean reblogged, boolean favourited, boolean bookmarked, boolean muted, - @Nullable String spoilerText, Status.Visibility visibility, List attachments, - @Nullable String rebloggedByUsername, @Nullable String rebloggedAvatar, boolean sensitive, boolean isExpanded, - boolean isShowingContent, String userFullName, String nickname, String avatar, - Date createdAt, int reblogsCount, int favouritesCount, @Nullable String inReplyToId, - @Nullable Status.Mention[] mentions, String senderId, boolean rebloggingEnabled, - Status.Application application, List statusEmojis, List accountEmojis, List rebloggedByAccountEmojis, @Nullable Card card, - boolean isCollapsible, boolean isCollapsed, @Nullable PollViewData poll, boolean isBot) { - - this.id = id; - if (Build.VERSION.SDK_INT == 23) { - // https://github.com/tuskyapp/Tusky/issues/563 - this.content = replaceCrashingCharacters(content); - this.spoilerText = spoilerText == null ? null : replaceCrashingCharacters(spoilerText).toString(); - this.nickname = replaceCrashingCharacters(nickname).toString(); - } else { - this.content = content; - this.spoilerText = spoilerText; - this.nickname = nickname; - } - this.reblogged = reblogged; - this.favourited = favourited; - this.bookmarked = bookmarked; - this.muted = muted; - this.visibility = visibility; - this.attachments = attachments; - this.rebloggedByUsername = rebloggedByUsername; - this.rebloggedAvatar = rebloggedAvatar; - this.isSensitive = sensitive; - this.isExpanded = isExpanded; - this.isShowingContent = isShowingContent; - this.userFullName = userFullName; - this.avatar = avatar; - this.createdAt = createdAt; - this.reblogsCount = reblogsCount; - this.favouritesCount = favouritesCount; - this.inReplyToId = inReplyToId; - this.mentions = mentions; - this.senderId = senderId; - this.rebloggingEnabled = rebloggingEnabled; - this.application = application; - this.statusEmojis = statusEmojis; - this.accountEmojis = accountEmojis; - this.rebloggedByAccountEmojis = rebloggedByAccountEmojis; - this.card = card; - this.isCollapsible = isCollapsible; - this.isCollapsed = isCollapsed; - this.poll = poll; - this.isBot = isBot; - } - - public String getId() { - return id; - } - - public Spanned getContent() { - return content; - } - - public boolean isReblogged() { - return reblogged; - } - - public boolean isFavourited() { - return favourited; - } - - public boolean isBookmarked() { - return bookmarked; - } - - public boolean isMuted() { - return muted; - } - - @Nullable - public String getSpoilerText() { - return spoilerText; - } - - public Status.Visibility getVisibility() { - return visibility; - } - - public List getAttachments() { - return attachments; - } - - @Nullable - public String getRebloggedByUsername() { - return rebloggedByUsername; - } - - public boolean isSensitive() { - return isSensitive; - } - - public boolean isExpanded() { - return isExpanded; - } - - public boolean isShowingContent() { - return isShowingContent; - } - - public boolean isBot(){ return isBot; } - - @Nullable - public String getRebloggedAvatar() { - return rebloggedAvatar; - } - - public String getUserFullName() { - return userFullName; - } - - public String getNickname() { - return nickname; - } - - public String getAvatar() { - return avatar; - } - - public Date getCreatedAt() { - return createdAt; - } - - public int getReblogsCount() { - return reblogsCount; - } - - public int getFavouritesCount() { - return favouritesCount; - } - - @Nullable - public String getInReplyToId() { - return inReplyToId; - } - - public String getSenderId() { - return senderId; - } - - public Boolean getRebloggingEnabled() { - return rebloggingEnabled; - } - - @Nullable - public Status.Mention[] getMentions() { - return mentions; - } - - public Status.Application getApplication() { - return application; - } - - public List getStatusEmojis() { - return statusEmojis; - } - - public List getAccountEmojis() { - return accountEmojis; - } - - public List getRebloggedByAccountEmojis() { - return rebloggedByAccountEmojis; - } - - @Nullable - public Card getCard() { - return card; - } - - /** - * Specifies whether the content of this post is allowed to be collapsed or if it should show - * all content regardless. - * - * @return Whether the post is collapsible or never collapsed. - */ - public boolean isCollapsible() { - return isCollapsible; - } - - /** - * Specifies whether the content of this post is currently limited in visibility to the first - * 500 characters or not. - * - * @return Whether the post is collapsed or fully expanded. - */ - public boolean isCollapsed() { - return isCollapsed; - } - - @Nullable - public PollViewData getPoll() { - return poll; - } - - @Override public long getViewDataId() { - // Chance of collision is super low and impact of mistake is low as well - return id.hashCode(); - } - - public boolean deepEquals(StatusViewData o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Concrete concrete = (Concrete) o; - return reblogged == concrete.reblogged && - favourited == concrete.favourited && - bookmarked == concrete.bookmarked && - isSensitive == concrete.isSensitive && - isExpanded == concrete.isExpanded && - isShowingContent == concrete.isShowingContent && - isBot == concrete.isBot && - reblogsCount == concrete.reblogsCount && - favouritesCount == concrete.favouritesCount && - rebloggingEnabled == concrete.rebloggingEnabled && - Objects.equals(id, concrete.id) && - Objects.equals(content, concrete.content) && - Objects.equals(spoilerText, concrete.spoilerText) && - visibility == concrete.visibility && - Objects.equals(attachments, concrete.attachments) && - Objects.equals(rebloggedByUsername, concrete.rebloggedByUsername) && - Objects.equals(rebloggedAvatar, concrete.rebloggedAvatar) && - Objects.equals(userFullName, concrete.userFullName) && - Objects.equals(nickname, concrete.nickname) && - Objects.equals(avatar, concrete.avatar) && - Objects.equals(createdAt, concrete.createdAt) && - Objects.equals(inReplyToId, concrete.inReplyToId) && - Arrays.equals(mentions, concrete.mentions) && - Objects.equals(senderId, concrete.senderId) && - Objects.equals(application, concrete.application) && - Objects.equals(statusEmojis, concrete.statusEmojis) && - Objects.equals(accountEmojis, concrete.accountEmojis) && - Objects.equals(rebloggedByAccountEmojis, concrete.rebloggedByAccountEmojis) && - Objects.equals(card, concrete.card) && - Objects.equals(poll, concrete.poll) - && isCollapsed == concrete.isCollapsed; - } - - static Spanned replaceCrashingCharacters(Spanned content) { - return (Spanned) replaceCrashingCharacters((CharSequence) content); - } - - static CharSequence replaceCrashingCharacters(CharSequence content) { - boolean replacing = false; - SpannableStringBuilder builder = null; - int length = content.length(); - - for (int index = 0; index < length; ++index) { - char character = content.charAt(index); - - // If there are more than one or two, switch to a map - if (character == SOFT_HYPHEN) { - if (!replacing) { - replacing = true; - builder = new SpannableStringBuilder(content, 0, index); - } - builder.append(ASCII_HYPHEN); - } else if (replacing) { - builder.append(character); - } - } - - return replacing ? builder : content; - } - } - - public static final class Placeholder extends StatusViewData { - private final boolean isLoading; - private final String id; - - public Placeholder(String id, boolean isLoading) { - this.id = id; - this.isLoading = isLoading; - } - - public boolean isLoading() { - return isLoading; - } - - public String getId() { - return id; - } - - @Override public long getViewDataId() { - return id.hashCode(); - } - - @Override public boolean deepEquals(StatusViewData other) { - if (!(other instanceof Placeholder)) return false; - Placeholder that = (Placeholder) other; - return isLoading == that.isLoading && id.equals(that.id); - } - - @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - Placeholder that = (Placeholder) o; - - return deepEquals(that); - } - - @Override - public int hashCode() { - int result = (isLoading ? 1 : 0); - result = 31 * result + id.hashCode(); - return result; - } - } - - public static class Builder { - private String id; - private Spanned content; - private boolean reblogged; - private boolean favourited; - private boolean bookmarked; - private boolean muted; - private String spoilerText; - private Status.Visibility visibility; - private List attachments; - private String rebloggedByUsername; - private String rebloggedAvatar; - private boolean isSensitive; - private boolean isExpanded; - private boolean isShowingContent; - private String userFullName; - private String nickname; - private String avatar; - private Date createdAt; - private int reblogsCount; - private int favouritesCount; - private String inReplyToId; - private Status.Mention[] mentions; - private String senderId; - private boolean rebloggingEnabled; - private Status.Application application; - private List statusEmojis; - private List accountEmojis; - private List rebloggedByAccountEmojis; - private Card card; - private boolean isCollapsible; /** Whether the status meets the requirement to be collapsed */ - private boolean isCollapsed; /** Whether the status is shown partially or fully */ - private PollViewData poll; - private boolean isBot; - - public Builder() { - } - - public Builder(final StatusViewData.Concrete viewData) { - id = viewData.id; - content = viewData.content; - reblogged = viewData.reblogged; - favourited = viewData.favourited; - bookmarked = viewData.bookmarked; - muted = viewData.muted; - spoilerText = viewData.spoilerText; - visibility = viewData.visibility; - attachments = viewData.attachments == null ? null : new ArrayList<>(viewData.attachments); - rebloggedByUsername = viewData.rebloggedByUsername; - rebloggedAvatar = viewData.rebloggedAvatar; - isSensitive = viewData.isSensitive; - isExpanded = viewData.isExpanded; - isShowingContent = viewData.isShowingContent; - userFullName = viewData.userFullName; - nickname = viewData.nickname; - avatar = viewData.avatar; - createdAt = new Date(viewData.createdAt.getTime()); - reblogsCount = viewData.reblogsCount; - favouritesCount = viewData.favouritesCount; - inReplyToId = viewData.inReplyToId; - mentions = viewData.mentions == null ? null : viewData.mentions.clone(); - senderId = viewData.senderId; - rebloggingEnabled = viewData.rebloggingEnabled; - application = viewData.application; - statusEmojis = viewData.getStatusEmojis(); - accountEmojis = viewData.getAccountEmojis(); - rebloggedByAccountEmojis = viewData.getRebloggedByAccountEmojis(); - card = viewData.getCard(); - isCollapsible = viewData.isCollapsible(); - isCollapsed = viewData.isCollapsed(); - poll = viewData.poll; - isBot = viewData.isBot(); - } - - public Builder setId(String id) { - this.id = id; - return this; - } - - public Builder setContent(Spanned content) { - this.content = content; - return this; - } - - public Builder setReblogged(boolean reblogged) { - this.reblogged = reblogged; - return this; - } - - public Builder setFavourited(boolean favourited) { - this.favourited = favourited; - return this; - } - - public Builder setBookmarked(boolean bookmarked) { - this.bookmarked = bookmarked; - return this; - } - - public Builder setMuted(boolean muted) { - this.muted = muted; - return this; - } - - public Builder setSpoilerText(String spoilerText) { - this.spoilerText = spoilerText; - return this; - } - - public Builder setVisibility(Status.Visibility visibility) { - this.visibility = visibility; - return this; - } - - public Builder setAttachments(List attachments) { - this.attachments = attachments; - return this; - } - - public Builder setRebloggedByUsername(String rebloggedByUsername) { - this.rebloggedByUsername = rebloggedByUsername; - return this; - } - - public Builder setRebloggedAvatar(String rebloggedAvatar) { - this.rebloggedAvatar = rebloggedAvatar; - return this; - } - - public Builder setSensitive(boolean sensitive) { - this.isSensitive = sensitive; - return this; - } - - public Builder setIsExpanded(boolean isExpanded) { - this.isExpanded = isExpanded; - return this; - } - - public Builder setIsShowingSensitiveContent(boolean isShowingSensitiveContent) { - this.isShowingContent = isShowingSensitiveContent; - return this; - } - - public Builder setIsBot(boolean isBot) { - this.isBot = isBot; - return this; - } - - public Builder setUserFullName(String userFullName) { - this.userFullName = userFullName; - return this; - } - - public Builder setNickname(String nickname) { - this.nickname = nickname; - return this; - } - - public Builder setAvatar(String avatar) { - this.avatar = avatar; - return this; - } - - public Builder setCreatedAt(Date createdAt) { - this.createdAt = createdAt; - return this; - } - - public Builder setReblogsCount(int reblogsCount) { - this.reblogsCount = reblogsCount; - return this; - } - - public Builder setFavouritesCount(int favouritesCount) { - this.favouritesCount = favouritesCount; - return this; - } - - public Builder setInReplyToId(String inReplyToId) { - this.inReplyToId = inReplyToId; - return this; - } - - public Builder setMentions(Status.Mention[] mentions) { - this.mentions = mentions; - return this; - } - - public Builder setSenderId(String senderId) { - this.senderId = senderId; - return this; - } - - public Builder setRebloggingEnabled(boolean rebloggingEnabled) { - this.rebloggingEnabled = rebloggingEnabled; - return this; - } - - public Builder setApplication(Status.Application application) { - this.application = application; - return this; - } - - public Builder setStatusEmojis(List emojis) { - this.statusEmojis = emojis; - return this; - } - - public Builder setAccountEmojis(List emojis) { - this.accountEmojis = emojis; - return this; - } - - public Builder setRebloggedByEmojis(List emojis) { - this.rebloggedByAccountEmojis = emojis; - return this; - } - - public Builder setCard(Card card) { - this.card = card; - return this; - } - - /** - * Configure the {@link com.keylesspalace.tusky.viewdata.StatusViewData} to support collapsing - * its content limiting the visible length when collapsed at 500 characters, - * - * @param collapsible Whether the status should support being collapsed or not. - * @return This {@link com.keylesspalace.tusky.viewdata.StatusViewData.Builder} instance. - */ - public Builder setCollapsible(boolean collapsible) { - isCollapsible = collapsible; - return this; - } - - /** - * Configure the {@link com.keylesspalace.tusky.viewdata.StatusViewData} to start in a collapsed - * state, hiding partially the content of the post if it exceeds a certain amount of characters. - * - * @param collapsed Whether to show the full content of the status or not. - * @return This {@link com.keylesspalace.tusky.viewdata.StatusViewData.Builder} instance. - */ - public Builder setCollapsed(boolean collapsed) { - isCollapsed = collapsed; - return this; - } - - public Builder setPoll(Poll poll) { - this.poll = PollViewDataKt.toViewData(poll); - return this; - } - - public StatusViewData.Concrete createStatusViewData() { - if (this.statusEmojis == null) statusEmojis = Collections.emptyList(); - if (this.accountEmojis == null) accountEmojis = Collections.emptyList(); - if (this.createdAt == null) createdAt = new Date(); - - return new StatusViewData.Concrete(id, content, reblogged, favourited, bookmarked, muted, spoilerText, - visibility, attachments, rebloggedByUsername, rebloggedAvatar, isSensitive, isExpanded, - isShowingContent, userFullName, nickname, avatar, createdAt, reblogsCount, - favouritesCount, inReplyToId, mentions, senderId, rebloggingEnabled, application, - statusEmojis, accountEmojis, rebloggedByAccountEmojis, card, isCollapsible, isCollapsed, poll, isBot); - } - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt new file mode 100644 index 000000000..d02569d41 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt @@ -0,0 +1,144 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ +package com.keylesspalace.tusky.viewdata + +import android.os.Build +import android.text.SpannableStringBuilder +import android.text.Spanned +import com.keylesspalace.tusky.entity.Status + +/** + * Created by charlag on 11/07/2017. + * + * + * Class to represent data required to display either a notification or a placeholder. + * It is either a [StatusViewData.Concrete] or a [StatusViewData.Placeholder]. + */ +sealed class StatusViewData private constructor() { + abstract val viewDataId: Long + + data class Concrete( + val status: Status, + val isExpanded: Boolean, + val isShowingContent: Boolean, + /** + * Specifies whether the content of this post is allowed to be collapsed or if it should show + * all content regardless. + * + * @return Whether the post is collapsible or never collapsed. + */ + val isCollapsible: Boolean, + /** + * Specifies whether the content of this post is currently limited in visibility to the first + * 500 characters or not. + * + * @return Whether the post is collapsed or fully expanded. + */ + /** Whether the status meets the requirement to be collapse */ + val isCollapsed: Boolean, + ) : StatusViewData() { + override val viewDataId: Long + get() = status.id.hashCode().toLong() + + val content: Spanned + val spoilerText: String + val username: String + + val actionable: Status + get() = status.actionableStatus + + val rebloggedAvatar: String? + get() = status.reblog?.account?.avatar + + val rebloggingStatus: Status? + get() = if (status.reblog != null) status else null + + init { + if (Build.VERSION.SDK_INT == 23) { + // https://github.com/tuskyapp/Tusky/issues/563 + this.content = replaceCrashingCharacters(status.actionableStatus.content) + this.spoilerText = + replaceCrashingCharacters(status.actionableStatus.spoilerText).toString() + this.username = + replaceCrashingCharacters(status.actionableStatus.account.username).toString() + } else { + this.content = status.actionableStatus.content + this.spoilerText = status.actionableStatus.spoilerText + this.username = status.actionableStatus.account.username + } + } + + companion object { + private const val SOFT_HYPHEN = '\u00ad' + private const val ASCII_HYPHEN = '-' + fun replaceCrashingCharacters(content: Spanned): Spanned { + return replaceCrashingCharacters(content as CharSequence) as Spanned + } + + fun replaceCrashingCharacters(content: CharSequence?): CharSequence? { + var replacing = false + var builder: SpannableStringBuilder? = null + val length = content!!.length + for (index in 0 until length) { + val character = content[index] + + // If there are more than one or two, switch to a map + if (character == SOFT_HYPHEN) { + if (!replacing) { + replacing = true + builder = SpannableStringBuilder(content, 0, index) + } + builder!!.append(ASCII_HYPHEN) + } else if (replacing) { + builder!!.append(character) + } + } + return if (replacing) builder else content + } + } + + val id: String + get() = status.id + + /** Helper for Java */ + fun copyWithStatus(status: Status): Concrete { + return copy(status = status) + } + + /** Helper for Java */ + fun copyWithExpanded(isExpanded: Boolean): Concrete { + return copy(isExpanded = isExpanded) + } + + /** Helper for Java */ + fun copyWithShowingContent(isShowingContent: Boolean): Concrete { + return copy(isShowingContent = isShowingContent) + } + + /** Helper for Java */ + fun copyWIthCollapsed(isCollapsed: Boolean): Concrete { + return copy(isCollapsed = isCollapsed) + } + } + + data class Placeholder(val id: String, val isLoading: Boolean) : StatusViewData() { + override val viewDataId: Long + get() = id.hashCode().toLong() + } + + fun asStatusOrNull() = this as? Concrete + + fun asPlaceholderOrNull() = this as? Placeholder +} diff --git a/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt index 4ff64536c..3b72d2ae2 100644 --- a/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt @@ -91,7 +91,7 @@ class BottomSheetActivityTest { "", Status.Visibility.PUBLIC, ArrayList(), - arrayOf(), + listOf(), null, pinned = false, muted = false, diff --git a/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt b/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt index 6b35d4a40..71f0377fd 100644 --- a/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt @@ -1,260 +1,186 @@ package com.keylesspalace.tusky -import android.os.Bundle import android.text.SpannedString import androidx.test.ext.junit.runners.AndroidJUnit4 import com.keylesspalace.tusky.entity.Filter import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.entity.PollOption import com.keylesspalace.tusky.entity.Status -import com.keylesspalace.tusky.fragment.SFragment +import com.keylesspalace.tusky.network.FilterModel import com.keylesspalace.tusky.network.MastodonApi +import com.nhaarman.mockitokotlin2.doReturn import com.nhaarman.mockitokotlin2.mock -import okhttp3.Request -import okio.Timeout +import io.reactivex.rxjava3.core.Single import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mockito -import org.robolectric.Robolectric import org.robolectric.annotation.Config -import retrofit2.Call -import retrofit2.Callback -import retrofit2.Response import java.util.* @Config(sdk = [28]) @RunWith(AndroidJUnit4::class) class FilterTest { - private val fragment = FakeFragment() + lateinit var filterModel: FilterModel @Before fun setup() { + filterModel = FilterModel() + val filters = listOf( + Filter( + id = "123", + phrase = "badWord", + context = listOf(Filter.HOME), + expiresAt = null, + irreversible = false, + wholeWord = false + ), + Filter( + id = "123", + phrase = "badWholeWord", + context = listOf(Filter.HOME, Filter.PUBLIC), + expiresAt = null, + irreversible = false, + wholeWord = true + ), + Filter( + id = "123", + phrase = "@twitter.com", + context = listOf(Filter.HOME), + expiresAt = null, + irreversible = false, + wholeWord = true + ) + ) - val controller = Robolectric.buildActivity(FakeActivity::class.java) - val activity = controller.get() - - activity.accountManager = mock() - val apiMock = Mockito.mock(MastodonApi::class.java) - Mockito.`when`(apiMock.getFilters()).thenReturn(object: Call> { - override fun isExecuted(): Boolean { - return false - } - override fun clone(): Call> { - throw Error("not implemented") - } - override fun isCanceled(): Boolean { - throw Error("not implemented") - } - override fun cancel() { - throw Error("not implemented") - } - override fun execute(): Response> { - throw Error("not implemented") - } - override fun request(): Request { - throw Error("not implemented") - } - - override fun enqueue(callback: Callback>) { - callback.onResponse( - this, - Response.success( - listOf( - Filter( - id = "123", - phrase = "badWord", - context = listOf(Filter.HOME), - expiresAt = null, - irreversible = false, - wholeWord = false - ), - Filter( - id = "123", - phrase = "badWholeWord", - context = listOf(Filter.HOME, Filter.PUBLIC), - expiresAt = null, - irreversible = false, - wholeWord = true - ), - Filter( - id = "123", - phrase = "wrongContext", - context = listOf(Filter.PUBLIC), - expiresAt = null, - irreversible = false, - wholeWord = true - ), - Filter( - id = "123", - phrase = "@twitter.com", - context = listOf(Filter.HOME), - expiresAt = null, - irreversible = false, - wholeWord = true - ) - ) - ) - ) - } - - override fun timeout(): Timeout { - throw Error("not implemented") - } - }) - - activity.mastodonApi = apiMock - - - controller.create().start() - - fragment.mastodonApi = apiMock - - - activity.supportFragmentManager.beginTransaction() - .replace(R.id.mainDrawerLayout, fragment, "fragment") - .commit() - - fragment.reloadFilters(false) - + filterModel.initWithFilters(filters) } @Test fun shouldNotFilter() { - assertFalse(fragment.shouldFilterStatus( + assertFalse( + filterModel.shouldFilterStatus( mockStatus(content = "should not be filtered") - )) - } - - @Test - fun shouldNotFilter_whenContextDoesNotMatch() { - assertFalse(fragment.shouldFilterStatus( - mockStatus(content = "one two wrongContext three") - )) + ) + ) } @Test fun shouldFilter_whenContentMatchesBadWord() { - assertTrue(fragment.shouldFilterStatus( + assertTrue( + filterModel.shouldFilterStatus( mockStatus(content = "one two badWord three") - )) + ) + ) } @Test fun shouldFilter_whenContentMatchesBadWordPart() { - assertTrue(fragment.shouldFilterStatus( + assertTrue( + filterModel.shouldFilterStatus( mockStatus(content = "one two badWordPart three") - )) + ) + ) } @Test fun shouldFilter_whenContentMatchesBadWholeWord() { - assertTrue(fragment.shouldFilterStatus( + assertTrue( + filterModel.shouldFilterStatus( mockStatus(content = "one two badWholeWord three") - )) + ) + ) } @Test fun shouldNotFilter_whenContentDoesNotMatchWholeWord() { - assertFalse(fragment.shouldFilterStatus( + assertFalse( + filterModel.shouldFilterStatus( mockStatus(content = "one two badWholeWordTest three") - )) + ) + ) } @Test fun shouldFilter_whenSpoilerTextDoesMatch() { - assertTrue(fragment.shouldFilterStatus( + assertTrue( + filterModel.shouldFilterStatus( mockStatus( - content = "should not be filtered", - spoilerText = "badWord should be filtered" + content = "should not be filtered", + spoilerText = "badWord should be filtered" ) - )) + ) + ) } @Test fun shouldFilter_whenPollTextDoesMatch() { - assertTrue(fragment.shouldFilterStatus( + assertTrue( + filterModel.shouldFilterStatus( mockStatus( - content = "should not be filtered", - spoilerText = "should not be filtered", - pollOptions = listOf("should not be filtered", "badWord") + content = "should not be filtered", + spoilerText = "should not be filtered", + pollOptions = listOf("should not be filtered", "badWord") ) - )) + ) + ) } @Test fun shouldFilterPartialWord_whenWholeWordFilterContainsNonAlphanumericCharacters() { - assertTrue(fragment.shouldFilterStatus( + assertTrue( + filterModel.shouldFilterStatus( mockStatus(content = "one two someone@twitter.com three") - )) - } - - private fun mockStatus( - content: String = "", - spoilerText: String = "", - pollOptions: List? = null - ): Status { - return Status( - id = "123", - url = "https://mastodon.social/@Tusky/100571663297225812", - account = mock(), - inReplyToId = null, - inReplyToAccountId = null, - reblog = null, - content = SpannedString(content), - createdAt = Date(), - emojis = emptyList(), - reblogsCount = 0, - favouritesCount = 0, - reblogged = false, - favourited = false, - bookmarked = false, - sensitive = false, - spoilerText = spoilerText, - visibility = Status.Visibility.PUBLIC, - attachments = arrayListOf(), - mentions = emptyArray(), - application = null, - pinned = false, - muted = false, - poll = if (pollOptions != null) { - Poll( - id = "1234", - expiresAt = null, - expired = false, - multiple = false, - votesCount = 0, - votersCount = 0, - options = pollOptions.map { - PollOption(it, 0) - }, - voted = false - ) - } else null, - card = null + ) ) } -} - -class FakeActivity: BottomSheetActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - } -} - -class FakeFragment: SFragment() { - override fun removeItem(position: Int) { + private fun mockStatus( + content: String = "", + spoilerText: String = "", + pollOptions: List? = null + ): Status { + return Status( + id = "123", + url = "https://mastodon.social/@Tusky/100571663297225812", + account = mock(), + inReplyToId = null, + inReplyToAccountId = null, + reblog = null, + content = SpannedString(content), + createdAt = Date(), + emojis = emptyList(), + reblogsCount = 0, + favouritesCount = 0, + reblogged = false, + favourited = false, + bookmarked = false, + sensitive = false, + spoilerText = spoilerText, + visibility = Status.Visibility.PUBLIC, + attachments = arrayListOf(), + mentions = listOf(), + application = null, + pinned = false, + muted = false, + poll = if (pollOptions != null) { + Poll( + id = "1234", + expiresAt = null, + expired = false, + multiple = false, + votesCount = 0, + votersCount = 0, + options = pollOptions.map { + PollOption(it, 0) + }, + voted = false + ) + } else null, + card = null + ) } - override fun onReblog(reblog: Boolean, position: Int) { - } - - override fun filterIsRelevant(filter: Filter): Boolean { - return filter.context.contains(Filter.HOME) - } } \ No newline at end of file diff --git a/app/src/test/java/com/keylesspalace/tusky/fragment/TimelineRepositoryTest.kt b/app/src/test/java/com/keylesspalace/tusky/components/timeline/TimelineRepositoryTest.kt similarity index 60% rename from app/src/test/java/com/keylesspalace/tusky/fragment/TimelineRepositoryTest.kt rename to app/src/test/java/com/keylesspalace/tusky/components/timeline/TimelineRepositoryTest.kt index 27ff5f62c..a11fda67d 100644 --- a/app/src/test/java/com/keylesspalace/tusky/fragment/TimelineRepositoryTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/components/timeline/TimelineRepositoryTest.kt @@ -1,4 +1,4 @@ -package com.keylesspalace.tusky.fragment +package com.keylesspalace.tusky.components.timeline import android.text.SpannableString import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -10,7 +10,6 @@ import com.keylesspalace.tusky.db.TimelineStatusWithAccount import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.repository.* import com.keylesspalace.tusky.util.Either import com.nhaarman.mockitokotlin2.isNull import com.nhaarman.mockitokotlin2.verify @@ -54,10 +53,10 @@ class TimelineRepositoryTest { private val limit = 30 private val account = AccountEntity( - id = 2, - accessToken = "token", - domain = "domain.com", - isActive = true + id = 2, + accessToken = "token", + domain = "domain.com", + isActive = true ) @Before @@ -74,13 +73,13 @@ class TimelineRepositoryTest { @Test fun testNetworkUnbounded() { val statuses = listOf( - makeStatus("3"), - makeStatus("2") + makeStatus("3"), + makeStatus("2") ) whenever(mastodonApi.homeTimeline(isNull(), isNull(), anyInt())) - .thenReturn(Single.just(Response.success(statuses))) + .thenReturn(Single.just(Response.success(statuses))) val result = subject.getStatuses(null, null, null, limit, TimelineRequestMode.NETWORK) - .blockingGet() + .blockingGet() assertEquals(statuses.map(Status::lift), result) testScheduler.advanceTimeBy(100, TimeUnit.SECONDS) @@ -90,9 +89,9 @@ class TimelineRepositoryTest { verify(timelineDao).insertStatusIfNotThere(Placeholder("1").toEntity(account.id)) for (status in statuses) { verify(timelineDao).insertInTransaction( - status.toEntity(account.id, gson), - status.account.toEntity(account.id, gson), - null + status.toEntity(account.id, gson), + status.account.toEntity(account.id, gson), + null ) } verify(timelineDao).cleanup(anyLong()) @@ -102,34 +101,38 @@ class TimelineRepositoryTest { @Test fun testNetworkLoadingTopNoGap() { val response = listOf( - makeStatus("4"), - makeStatus("3"), - makeStatus("2") + makeStatus("4"), + makeStatus("3"), + makeStatus("2") ) val sinceId = "2" val sinceIdMinusOne = "1" whenever(mastodonApi.homeTimeline(null, sinceIdMinusOne, limit + 1)) - .thenReturn(Single.just(Response.success(response))) - val result = subject.getStatuses(null, sinceId, sinceIdMinusOne, limit, - TimelineRequestMode.NETWORK) - .blockingGet() + .thenReturn(Single.just(Response.success(response))) + val result = subject.getStatuses( + null, sinceId, sinceIdMinusOne, limit, + TimelineRequestMode.NETWORK + ) + .blockingGet() assertEquals( - response.subList(0, 2).map(Status::lift), - result + response.subList(0, 2).map(Status::lift), + result ) testScheduler.advanceTimeBy(100, TimeUnit.SECONDS) verify(timelineDao).deleteRange(account.id, response.last().id, response.first().id) // We assume for now that overlapped one is inserted but it's not that important for (status in response) { verify(timelineDao).insertInTransaction( - status.toEntity(account.id, gson), - status.account.toEntity(account.id, gson), - null + status.toEntity(account.id, gson), + status.account.toEntity(account.id, gson), + null ) } - verify(timelineDao).removeAllPlaceholdersBetween(account.id, response.first().id, - response.last().id) + verify(timelineDao).removeAllPlaceholdersBetween( + account.id, response.first().id, + response.last().id + ) verify(timelineDao).cleanup(anyLong()) verifyNoMoreInteractions(timelineDao) } @@ -137,16 +140,18 @@ class TimelineRepositoryTest { @Test fun testNetworkLoadingTopWithGap() { val response = listOf( - makeStatus("5"), - makeStatus("4") + makeStatus("5"), + makeStatus("4") ) val sinceId = "2" val sinceIdMinusOne = "1" whenever(mastodonApi.homeTimeline(null, sinceIdMinusOne, limit + 1)) - .thenReturn(Single.just(Response.success(response))) - val result = subject.getStatuses(null, sinceId, sinceIdMinusOne, limit, - TimelineRequestMode.NETWORK) - .blockingGet() + .thenReturn(Single.just(Response.success(response))) + val result = subject.getStatuses( + null, sinceId, sinceIdMinusOne, limit, + TimelineRequestMode.NETWORK + ) + .blockingGet() val placeholder = Placeholder("3") assertEquals(response.map(Status::lift) + Either.Left(placeholder), result) @@ -154,9 +159,9 @@ class TimelineRepositoryTest { verify(timelineDao).deleteRange(account.id, response.last().id, response.first().id) for (status in response) { verify(timelineDao).insertInTransaction( - status.toEntity(account.id, gson), - status.account.toEntity(account.id, gson), - null + status.toEntity(account.id, gson), + status.account.toEntity(account.id, gson), + null ) } verify(timelineDao).insertStatusIfNotThere(placeholder.toEntity(account.id)) @@ -174,36 +179,40 @@ class TimelineRepositoryTest { // 1 val response = listOf( - makeStatus("5"), - makeStatus("4"), - makeStatus("3"), - makeStatus("2") + makeStatus("5"), + makeStatus("4"), + makeStatus("3"), + makeStatus("2") ) val sinceId = "2" val sinceIdMinusOne = "1" val maxId = "3" whenever(mastodonApi.homeTimeline(maxId, sinceIdMinusOne, limit + 1)) - .thenReturn(Single.just(Response.success(response))) - val result = subject.getStatuses(maxId, sinceId, sinceIdMinusOne, limit, - TimelineRequestMode.NETWORK) - .blockingGet() + .thenReturn(Single.just(Response.success(response))) + val result = subject.getStatuses( + maxId, sinceId, sinceIdMinusOne, limit, + TimelineRequestMode.NETWORK + ) + .blockingGet() assertEquals( - response.subList(0, response.lastIndex).map(Status::lift), - result + response.subList(0, response.lastIndex).map(Status::lift), + result ) testScheduler.advanceTimeBy(100, TimeUnit.SECONDS) verify(timelineDao).deleteRange(account.id, response.last().id, response.first().id) // We assume for now that overlapped one is inserted but it's not that important for (status in response) { verify(timelineDao).insertInTransaction( - status.toEntity(account.id, gson), - status.account.toEntity(account.id, gson), - null + status.toEntity(account.id, gson), + status.account.toEntity(account.id, gson), + null ) } - verify(timelineDao).removeAllPlaceholdersBetween(account.id, response.first().id, - response.last().id) + verify(timelineDao).removeAllPlaceholdersBetween( + account.id, response.first().id, + response.last().id + ) verify(timelineDao).cleanup(anyLong()) verifyNoMoreInteractions(timelineDao) } @@ -218,23 +227,25 @@ class TimelineRepositoryTest { // 1 val response = listOf( - makeStatus("6"), - makeStatus("5"), - makeStatus("4") + makeStatus("6"), + makeStatus("5"), + makeStatus("4") ) val sinceId = "2" val sinceIdMinusOne = "1" val maxId = "4" whenever(mastodonApi.homeTimeline(maxId, sinceIdMinusOne, limit + 1)) - .thenReturn(Single.just(Response.success(response))) - val result = subject.getStatuses(maxId, sinceId, sinceIdMinusOne, limit, - TimelineRequestMode.NETWORK) - .blockingGet() + .thenReturn(Single.just(Response.success(response))) + val result = subject.getStatuses( + maxId, sinceId, sinceIdMinusOne, limit, + TimelineRequestMode.NETWORK + ) + .blockingGet() val placeholder = Placeholder("3") assertEquals( - response.map(Status::lift) + Either.Left(placeholder), - result + response.map(Status::lift) + Either.Left(placeholder), + result ) testScheduler.advanceTimeBy(100, TimeUnit.SECONDS) // We assume for now that overlapped one is inserted but it's not that important @@ -243,13 +254,15 @@ class TimelineRepositoryTest { for (status in response) { verify(timelineDao).insertInTransaction( - status.toEntity(account.id, gson), - status.account.toEntity(account.id, gson), - null + status.toEntity(account.id, gson), + status.account.toEntity(account.id, gson), + null ) } - verify(timelineDao).removeAllPlaceholdersBetween(account.id, response.first().id, - response.last().id) + verify(timelineDao).removeAllPlaceholdersBetween( + account.id, response.first().id, + response.last().id + ) verify(timelineDao).insertStatusIfNotThere(placeholder.toEntity(account.id)) verify(timelineDao).cleanup(anyLong()) verifyNoMoreInteractions(timelineDao) @@ -265,11 +278,11 @@ class TimelineRepositoryTest { dbResult.account = status.account.toEntity(account.id, gson) whenever(mastodonApi.homeTimeline(any(), any(), any())) - .thenReturn(Single.just(Response.success((listOf(status))))) + .thenReturn(Single.just(Response.success((listOf(status))))) whenever(timelineDao.getStatusesForAccount(account.id, status.id, null, 30)) - .thenReturn(Single.just(listOf(dbResult))) + .thenReturn(Single.just(listOf(dbResult))) val result = subject.getStatuses(null, null, null, limit, TimelineRequestMode.ANY) - .blockingGet() + .blockingGet() assertEquals(listOf(status, dbStatus).map(Status::lift), result) } @@ -283,60 +296,60 @@ class TimelineRepositoryTest { dbResult2.status = Placeholder("1").toEntity(account.id) whenever(mastodonApi.homeTimeline(any(), any(), any())) - .thenReturn(Single.just(Response.success(listOf(status)))) + .thenReturn(Single.just(Response.success(listOf(status)))) whenever(timelineDao.getStatusesForAccount(account.id, status.id, null, 30)) - .thenReturn(Single.just(listOf(dbResult, dbResult2))) + .thenReturn(Single.just(listOf(dbResult, dbResult2))) val result = subject.getStatuses(null, null, null, limit, TimelineRequestMode.ANY) - .blockingGet() + .blockingGet() assertEquals(listOf(status).map(Status::lift), result) } +} - private fun makeStatus(id: String, account: Account = makeAccount(id)): Status { - return Status( - id = id, - account = account, - content = SpannableString("hello$id"), - createdAt = Date(), - emojis = listOf(), - reblogsCount = 3, - favouritesCount = 5, - sensitive = false, - visibility = Status.Visibility.PUBLIC, - spoilerText = "", - reblogged = true, - favourited = false, - bookmarked = false, - attachments = ArrayList(), - mentions = arrayOf(), - application = null, - inReplyToAccountId = null, - inReplyToId = null, - pinned = false, - muted = false, - reblog = null, - url = "http://example.com/statuses/$id", - poll = null, - card = null - ) - } +fun makeAccount(id: String): Account { + return Account( + id = id, + localUsername = "test$id", + username = "test$id@example.com", + displayName = "Example Account $id", + note = SpannableString("Note! $id"), + url = "https://example.com/@test$id", + avatar = "avatar$id", + header = "Header$id", + followersCount = 300, + followingCount = 400, + statusesCount = 1000, + bot = false, + emojis = listOf(), + fields = null, + source = null + ) +} - private fun makeAccount(id: String): Account { - return Account( - id = id, - localUsername = "test$id", - username = "test$id@example.com", - displayName = "Example Account $id", - note = SpannableString("Note! $id"), - url = "https://example.com/@test$id", - avatar = "avatar$id", - header = "Header$id", - followersCount = 300, - followingCount = 400, - statusesCount = 1000, - bot = false, - emojis = listOf(), - fields = null, - source = null - ) - } -} \ No newline at end of file +fun makeStatus(id: String, account: Account = makeAccount(id)): Status { + return Status( + id = id, + account = account, + content = SpannableString("hello$id"), + createdAt = Date(), + emojis = listOf(), + reblogsCount = 3, + favouritesCount = 5, + sensitive = false, + visibility = Status.Visibility.PUBLIC, + spoilerText = "", + reblogged = true, + favourited = false, + bookmarked = false, + attachments = ArrayList(), + mentions = listOf(), + application = null, + inReplyToAccountId = null, + inReplyToId = null, + pinned = false, + muted = false, + reblog = null, + url = "http://example.com/statuses/$id", + poll = null, + card = null + ) +} diff --git a/app/src/test/java/com/keylesspalace/tusky/components/timeline/TimelineViewModelTest.kt b/app/src/test/java/com/keylesspalace/tusky/components/timeline/TimelineViewModelTest.kt new file mode 100644 index 000000000..b70972dbb --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/components/timeline/TimelineViewModelTest.kt @@ -0,0 +1,783 @@ +package com.keylesspalace.tusky.components.timeline + +import android.content.SharedPreferences +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.components.timeline.TimelineViewModel.Companion.LOAD_AT_ONCE +import com.keylesspalace.tusky.db.AccountEntity +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.entity.Poll +import com.keylesspalace.tusky.entity.PollOption +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.network.FilterModel +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.network.TimelineCases +import com.keylesspalace.tusky.util.Either +import com.keylesspalace.tusky.util.toViewData +import com.keylesspalace.tusky.viewdata.StatusViewData +import com.nhaarman.mockitokotlin2.* +import io.reactivex.rxjava3.annotations.NonNull +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.observers.TestObserver +import io.reactivex.rxjava3.subjects.PublishSubject +import kotlinx.coroutines.runBlocking +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import org.robolectric.annotation.Config +import org.robolectric.shadows.ShadowLog +import retrofit2.Response +import java.io.IOException + + +@Config(sdk = [29]) +class TimelineViewModelTest { + lateinit var timelineRepository: TimelineRepository + lateinit var timelineCases: TimelineCases + lateinit var mastodonApi: MastodonApi + lateinit var eventHub: EventHub + lateinit var viewModel: TimelineViewModel + lateinit var accountManager: AccountManager + lateinit var sharedPreference: SharedPreferences + + @Before + fun setup() { + ShadowLog.stream = System.out + timelineRepository = mock() + timelineCases = mock() + mastodonApi = mock() + eventHub = mock { + on { events } doReturn Observable.never() + } + val account = AccountEntity( + 0, + "domain", + "accessToken", + isActive = true, + ) + + accountManager = mock { + on { activeAccount } doReturn account + } + sharedPreference = mock() + viewModel = TimelineViewModel( + timelineRepository, + timelineCases, + mastodonApi, + eventHub, + accountManager, + sharedPreference, + FilterModel() + ) + } + + @Test + fun `loadInitial, home, without cache, empty response`() { + val initialResponse = listOf() + setCachedResponse(initialResponse) + + // loadAbove -> loadBelow + whenever( + timelineRepository.getStatuses( + maxId = null, + sinceId = null, + sincedIdMinusOne = null, + requestMode = TimelineRequestMode.ANY, + limit = LOAD_AT_ONCE + ) + ).thenReturn(Single.just(listOf())) + + runBlocking { + viewModel.loadInitial() + } + + verify(timelineRepository).getStatuses( + null, + null, + null, + LOAD_AT_ONCE, + TimelineRequestMode.ANY + ) + } + + @Test + fun `loadInitial, home, without cache, single item in response`() { + setCachedResponse(listOf()) + + val status = makeStatus("1") + whenever( + timelineRepository.getStatuses( + isNull(), + isNull(), + isNull(), + eq(LOAD_AT_ONCE), + eq(TimelineRequestMode.ANY) + ) + ).thenReturn( + Single.just( + listOf( + Either.Right(status) + ) + ) + ) + + val updates = viewModel.viewUpdates.test() + + runBlocking { + viewModel.loadInitial() + } + + verify(timelineRepository).getStatuses( + isNull(), + isNull(), + isNull(), + eq(LOAD_AT_ONCE), + eq(TimelineRequestMode.ANY) + ) + + assertViewUpdated(updates) + + assertHasList(listOf(status).toViewData()) + } + + @Test + fun `loadInitial, list`() { + val listId = "listId" + viewModel.init(TimelineViewModel.Kind.LIST, listId, listOf()) + val status = makeStatus("1") + + whenever( + mastodonApi.listTimeline( + listId, + null, + null, + LOAD_AT_ONCE, + ) + ).thenReturn( + Single.just( + Response.success( + listOf( + status + ) + ) + ) + ) + + val updates = viewModel.viewUpdates.test() + + runBlocking { + viewModel.loadInitial().join() + } + assertViewUpdated(updates) + + assertHasList(listOf(status).toViewData()) + assertFalse("loading", viewModel.isLoadingInitially) + } + + @Test + fun `loadInitial, home, without cache, error on load`() { + setCachedResponse(listOf()) + + whenever( + timelineRepository.getStatuses( + maxId = null, + sinceId = null, + sincedIdMinusOne = null, + limit = LOAD_AT_ONCE, + TimelineRequestMode.ANY, + ) + ).thenReturn(Single.error(IOException("test"))) + + val updates = viewModel.viewUpdates.test() + + runBlocking { + viewModel.loadInitial() + } + + verify(timelineRepository).getStatuses( + isNull(), + isNull(), + isNull(), + eq(LOAD_AT_ONCE), + eq(TimelineRequestMode.ANY) + ) + + assertViewUpdated(updates) + + assertHasList(listOf()) + assertEquals(TimelineViewModel.FailureReason.NETWORK, viewModel.failure) + } + + @Test + fun `loadInitial, home, with cache, error on load above`() { + val statuses = (5 downTo 1).map { makeStatus(it.toString()) } + setCachedResponse(statuses) + setInitialRefresh("6", statuses) + + whenever( + timelineRepository.getStatuses( + maxId = null, + sinceId = "5", + sincedIdMinusOne = "4", + limit = LOAD_AT_ONCE, + TimelineRequestMode.NETWORK, + ) + ).thenReturn(Single.error(IOException("test"))) + + val updates = viewModel.viewUpdates.test() + + runBlocking { + viewModel.loadInitial() + } + + assertViewUpdated(updates) + + assertHasList(statuses.toViewData()) + // No failure set since we had statuses + assertNull(viewModel.failure) + } + + @Test + fun `loadInitial, home, with cache, error on refresh`() { + val statuses = (5 downTo 2).map { makeStatus(it.toString()) } + setCachedResponse(statuses) + + // Error on refreshing cached + whenever( + timelineRepository.getStatuses( + maxId = "6", + sinceId = null, + sincedIdMinusOne = null, + limit = LOAD_AT_ONCE, + TimelineRequestMode.NETWORK, + ) + ).thenReturn(Single.error(IOException("test"))) + + // Empty on loading above + setLoadAbove("5", "4", listOf()) + + val updates = viewModel.viewUpdates.test() + + runBlocking { + viewModel.loadInitial() + } + + assertViewUpdated(updates) + + assertHasList(statuses.toViewData()) + assertNull(viewModel.failure) + } + + @Test + fun `loads above cached`() { + val cachedStatuses = (5 downTo 1).map { makeStatus(it.toString()) } + setCachedResponse(cachedStatuses) + setInitialRefresh("6", cachedStatuses) + + val additionalStatuses = (10 downTo 6) + .map { makeStatus(it.toString()) } + + whenever( + timelineRepository.getStatuses( + null, + "5", + "4", + LOAD_AT_ONCE, + TimelineRequestMode.NETWORK + ) + ).thenReturn(Single.just(additionalStatuses.toEitherList())) + + runBlocking { + viewModel.loadInitial() + } + + // We could also check refresh progress here but it's a bit cumbersome + + assertHasList(additionalStatuses.plus(cachedStatuses).toViewData()) + } + + @Test + fun refresh() { + val cachedStatuses = (5 downTo 1).map { makeStatus(it.toString()) } + setCachedResponse(cachedStatuses) + setInitialRefresh("6", cachedStatuses) + + val additionalStatuses = listOf(makeStatus("6")) + + whenever( + timelineRepository.getStatuses( + null, + "5", + "4", + LOAD_AT_ONCE, + TimelineRequestMode.NETWORK + ) + ).thenReturn(Single.just(additionalStatuses.toEitherList())) + + runBlocking { + viewModel.loadInitial() + } + + clearInvocations(timelineRepository) + + val newStatuses = (8 downTo 7).map { makeStatus(it.toString()) } + + // Loading above the cached manually + whenever( + timelineRepository.getStatuses( + null, + "6", + "5", + LOAD_AT_ONCE, + TimelineRequestMode.NETWORK + ) + ).thenReturn(Single.just(newStatuses.toEitherList())) + + runBlocking { + viewModel.refresh() + } + + val allStatuses = newStatuses + additionalStatuses + cachedStatuses + assertHasList(allStatuses.toViewData()) + } + + @Test + fun `refresh failed`() { + val cachedStatuses = (5 downTo 1).map { makeStatus(it.toString()) } + setCachedResponse(cachedStatuses) + setInitialRefresh("6", cachedStatuses) + setLoadAbove("5", "4", listOf()) + + runBlocking { + viewModel.loadInitial() + } + + clearInvocations(timelineRepository) + + // Loading above the cached manually + whenever( + timelineRepository.getStatuses( + null, + "6", + "5", + LOAD_AT_ONCE, + TimelineRequestMode.NETWORK + ) + ).thenReturn(Single.error(IOException("test"))) + + runBlocking { + viewModel.refresh().join() + } + + assertHasList(cachedStatuses.map { it.toViewData(false, false) }) + assertFalse("refreshing", viewModel.isRefreshing) + assertNull("failure is not set", viewModel.failure) + } + + @Test + fun loadMore() { + val cachedStatuses = (10 downTo 5).map { makeStatus(it.toString()) } + setCachedResponse(cachedStatuses) + setInitialRefresh("11", cachedStatuses) + + // Nothing above + setLoadAbove("10", "9", listOf()) + + runBlocking { + viewModel.loadInitial().join() + } + + clearInvocations(timelineRepository) + + val oldStatuses = (4 downTo 1).map { makeStatus(it.toString()) } + + // Loading below the cached + whenever( + timelineRepository.getStatuses( + "5", + null, + null, + LOAD_AT_ONCE, + TimelineRequestMode.ANY + ) + ).thenReturn(Single.just(oldStatuses.toEitherList())) + + runBlocking { + viewModel.loadMore().join() + } + + val allStatuses = cachedStatuses + oldStatuses + assertHasList(allStatuses.toViewData()) + } + + @Test + fun `loadMore parallel`() { + val cachedStatuses = (10 downTo 5).map { makeStatus(it.toString()) } + setCachedResponse(cachedStatuses) + setInitialRefresh("11", cachedStatuses) + + // Nothing above + setLoadAbove("10", "9", listOf()) + + runBlocking { + viewModel.loadInitial().join() + } + + clearInvocations(timelineRepository) + + val oldStatuses = (4 downTo 1).map { makeStatus(it.toString()) } + + val responseSubject = PublishSubject.create>() + // Loading below the cached + whenever( + timelineRepository.getStatuses( + "5", + null, + null, + LOAD_AT_ONCE, + TimelineRequestMode.ANY + ) + ).thenReturn(responseSubject.firstOrError()) + + clearInvocations(timelineRepository) + + runBlocking { + // Trigger them in parallel + val job1 = viewModel.loadMore() + val job2 = viewModel.loadMore() + // Send the response + responseSubject.onNext(oldStatuses.toEitherList()) + // Wait for both + job1.join() + job2.join() + } + + val allStatuses = cachedStatuses + oldStatuses + assertHasList(allStatuses.toViewData()) + + verify(timelineRepository, times(1)).getStatuses( + "5", + null, + null, + LOAD_AT_ONCE, + TimelineRequestMode.ANY + ) + } + + @Test + fun `loadMore failed`() { + val cachedStatuses = (10 downTo 5).map { makeStatus(it.toString()) } + setCachedResponse(cachedStatuses) + setInitialRefresh("11", cachedStatuses) + + // Nothing above + setLoadAbove("10", "9", listOf()) + + runBlocking { + viewModel.loadInitial().join() + } + + clearInvocations(timelineRepository) + + // Loading below the cached + whenever( + timelineRepository.getStatuses( + "5", + null, + null, + LOAD_AT_ONCE, + TimelineRequestMode.ANY + ) + ).thenReturn(Single.error(IOException("test"))) + + runBlocking { + viewModel.loadMore().join() + } + + assertHasList(cachedStatuses.toViewData()) + + // Check that we can still load after that + + val oldStatuses = listOf(makeStatus("4")) + whenever( + timelineRepository.getStatuses( + "5", + null, + null, + LOAD_AT_ONCE, + TimelineRequestMode.ANY + ) + ).thenReturn(Single.just(oldStatuses.toEitherList())) + + runBlocking { + viewModel.loadMore().join() + } + assertHasList((cachedStatuses + oldStatuses).toViewData()) + } + + @Test + fun loadGap() { + val status5 = makeStatus("5") + val status4 = makeStatus("4") + val status3 = makeStatus("3") + val status1 = makeStatus("1") + + val cachedStatuses: List = listOf( + Either.Right(status5), + Either.Left(Placeholder("4")), + Either.Right(status1) + ) + val laterFetchedStatuses = listOf( + Either.Right(status4), + Either.Right(status3), + ) + + setCachedResponseWithGaps(cachedStatuses) + setInitialRefreshWithGaps("6", cachedStatuses) + + // Nothing above + setLoadAbove("5", items = listOf()) + + whenever( + timelineRepository.getStatuses( + "5", + "1", + null, + LOAD_AT_ONCE, + TimelineRequestMode.NETWORK + ) + ).thenReturn(Single.just(laterFetchedStatuses)) + + runBlocking { + viewModel.loadInitial().join() + + viewModel.loadGap(1).join() + } + + assertHasList( + listOf( + status5, + status4, + status3, + status1 + ).toViewData() + ) + } + + @Test + fun `loadGap failed`() { + val status5 = makeStatus("5") + val status1 = makeStatus("1") + + val cachedStatuses: List = listOf( + Either.Right(status5), + Either.Left(Placeholder("4")), + Either.Right(status1) + ) + setCachedResponseWithGaps(cachedStatuses) + setInitialRefreshWithGaps("6", cachedStatuses) + + setLoadAbove("5", items = listOf()) + + whenever( + timelineRepository.getStatuses( + "5", + "1", + null, + LOAD_AT_ONCE, + TimelineRequestMode.NETWORK + ) + ).thenReturn(Single.error(IOException("test"))) + + runBlocking { + viewModel.loadInitial().join() + + viewModel.loadGap(1).join() + } + + assertHasList( + listOf( + status5.toViewData(false, false), + StatusViewData.Placeholder("4", false), + status1.toViewData(false, false), + ) + ) + } + + @Test + fun favorite() { + val status5 = makeStatus("5") + val status4 = makeStatus("4") + val status3 = makeStatus("3") + val statuses = listOf(status5, status4, status3) + setCachedResponse(statuses) + setInitialRefresh("6", statuses) + setLoadAbove("5", "4", listOf()) + + runBlocking { viewModel.loadInitial() } + + whenever(timelineCases.favourite("4", true)) + .thenReturn(Single.just(status4.copy(favourited = true))) + + runBlocking { + viewModel.favorite(true, 1).join() + } + + verify(timelineCases).favourite("4", true) + + assertHasList(listOf(status5, status4.copy(favourited = true), status3).toViewData()) + } + + @Test + fun reblog() { + val status5 = makeStatus("5") + val status4 = makeStatus("4") + val status3 = makeStatus("3") + val statuses = listOf(status5, status4, status3) + setCachedResponse(statuses) + setInitialRefresh("6", statuses) + setLoadAbove("5", "4", listOf()) + + runBlocking { viewModel.loadInitial() } + + whenever(timelineCases.reblog("4", true)) + .thenReturn(Single.just(status4.copy(reblogged = true))) + + runBlocking { + viewModel.reblog(true, 1).join() + } + + verify(timelineCases).reblog("4", true) + + assertHasList(listOf(status5, status4.copy(reblogged = true), status3).toViewData()) + } + + @Test + fun bookmark() { + val status5 = makeStatus("5") + val status4 = makeStatus("4") + val status3 = makeStatus("3") + val statuses = listOf(status5, status4, status3) + setCachedResponse(statuses) + setInitialRefresh("6", statuses) + setLoadAbove("5", "4", listOf()) + + runBlocking { viewModel.loadInitial() } + + whenever(timelineCases.bookmark("4", true)) + .thenReturn(Single.just(status4.copy(bookmarked = true))) + + runBlocking { + viewModel.bookmark(true, 1).join() + } + + verify(timelineCases).bookmark("4", true) + + assertHasList(listOf(status5, status4.copy(bookmarked = true), status3).toViewData()) + } + + @Test + fun voteInPoll() { + val status5 = makeStatus("5") + val poll = Poll( + "1", + expiresAt = null, + expired = false, + multiple = false, + votersCount = 1, + votesCount = 1, + voted = false, + options = listOf(PollOption("1", 1), PollOption("2", 2)), + ) + val status4 = makeStatus("4").copy(poll = poll) + val status3 = makeStatus("3") + val statuses = listOf(status5, status4, status3) + setCachedResponse(statuses) + setInitialRefresh("6", statuses) + setLoadAbove("5", "4", listOf()) + + runBlocking { viewModel.loadInitial() } + + val votedPoll = poll.votedCopy(listOf(0)) + whenever(timelineCases.voteInPoll("4", poll.id, listOf(0))) + .thenReturn(Single.just(votedPoll)) + + runBlocking { + viewModel.voteInPoll(1, listOf(0)).join() + } + + verify(timelineCases).voteInPoll("4", poll.id, listOf(0)) + + assertHasList(listOf(status5, status4.copy(poll = votedPoll), status3).toViewData()) + } + + private fun setLoadAbove( + above: String, + aboveMinusOne: String? = null, + items: List + ) { + whenever( + timelineRepository.getStatuses( + null, + above, + aboveMinusOne, + LOAD_AT_ONCE, + TimelineRequestMode.NETWORK + ) + ).thenReturn(Single.just(items)) + } + + + private fun assertHasList(aList: List) { + assertEquals( + aList, + viewModel.statuses.toList() + ) + } + + private fun assertViewUpdated(updates: @NonNull TestObserver) { + assertTrue("There were view updates", updates.values().isNotEmpty()) + } + + private fun setInitialRefresh(maxId: String?, statuses: List) { + setInitialRefreshWithGaps(maxId, statuses.toEitherList()) + } + + private fun setCachedResponse(initialResponse: List) { + setCachedResponseWithGaps(initialResponse.toEitherList()) + } + + private fun setCachedResponseWithGaps(initialResponse: List) { + whenever( + timelineRepository.getStatuses( + isNull(), + isNull(), + isNull(), + eq(LOAD_AT_ONCE), + eq(TimelineRequestMode.DISK) + ) + ) + .thenReturn(Single.just(initialResponse)) + } + + private fun setInitialRefreshWithGaps(maxId: String?, statuses: List) { + whenever( + timelineRepository.getStatuses( + maxId, + null, + null, + LOAD_AT_ONCE, + TimelineRequestMode.NETWORK + ) + ).thenReturn(Single.just(statuses)) + } + + private fun List.toViewData(): List = map { + it.toViewData( + alwaysShowSensitiveMedia = false, + alwaysOpenSpoiler = false + ) + } + + private fun List.toEitherList() = map { Either.Right(it) } +} \ No newline at end of file From 6281e37aec085e5e88d25c504457126431ff86ec Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Fri, 11 Jun 2021 20:50:42 +0200 Subject: [PATCH 63/92] improve kotlin related proguard rules (#2190) --- app/proguard-rules.pro | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index a05994a10..af6372d4e 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -65,7 +65,14 @@ # remove some kotlin overhead -assumenosideeffects class kotlin.jvm.internal.Intrinsics { + static void checkNotNull(java.lang.Object); + static void checkNotNull(java.lang.Object, java.lang.String); static void checkParameterIsNotNull(java.lang.Object, java.lang.String); + static void checkParameterIsNotNull(java.lang.Object, java.lang.String); + static void checkNotNullParameter(java.lang.Object, java.lang.String); static void checkExpressionValueIsNotNull(java.lang.Object, java.lang.String); + static void checkNotNullExpressionValue(java.lang.Object, java.lang.String); + static void checkReturnedValueIsNotNull(java.lang.Object, java.lang.String); + static void checkReturnedValueIsNotNull(java.lang.Object, java.lang.String, java.lang.String); static void throwUninitializedPropertyAccessException(java.lang.String); } From 86002efc9707950d99b3d40e82a90acbc37e331e Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Fri, 11 Jun 2021 20:50:52 +0200 Subject: [PATCH 64/92] fix "show notifications filter" preference opening tabs preferences (#2193) --- .../tusky/components/preference/PreferencesFragment.kt | 8 -------- 1 file changed, 8 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt index e0a1b6836..fa4ac3b29 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt @@ -151,14 +151,6 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable { key = PrefKeys.SHOW_NOTIFICATIONS_FILTER setTitle(R.string.pref_title_show_notifications_filter) isSingleLineTitle = false - setOnPreferenceClickListener { - activity?.let { activity -> - val intent = PreferencesActivity.newIntent(activity, PreferencesActivity.TAB_FILTER_PREFERENCES) - activity.startActivity(intent) - activity.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left) - } - true - } } switchPreference { From b3e62f4e85fb3dae727fb72f4b80b45c3227e6f0 Mon Sep 17 00:00:00 2001 From: Ho Nhat Duy Date: Sat, 12 Jun 2021 01:40:23 +0000 Subject: [PATCH 65/92] Translated using Weblate (Vietnamese) Currently translated at 100.0% (14 of 14 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/vi/ --- fastlane/metadata/android/vi/changelogs/83.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 fastlane/metadata/android/vi/changelogs/83.txt diff --git a/fastlane/metadata/android/vi/changelogs/83.txt b/fastlane/metadata/android/vi/changelogs/83.txt new file mode 100644 index 000000000..68839be23 --- /dev/null +++ b/fastlane/metadata/android/vi/changelogs/83.txt @@ -0,0 +1,3 @@ +Tusky v15.1 + +Sửa lỗi crash khi ghi chú cho hình From b45adfcc1ae1e715169984bca94ba5255636daf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Mesk=C3=B3?= Date: Sat, 12 Jun 2021 01:40:24 +0000 Subject: [PATCH 66/92] Translated using Weblate (Hungarian) Currently translated at 100.0% (14 of 14 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/hu/ --- fastlane/metadata/android/hu/changelogs/83.txt | 2 +- fastlane/metadata/android/hu/full_description.txt | 12 ++++++------ fastlane/metadata/android/hu/short_description.txt | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/fastlane/metadata/android/hu/changelogs/83.txt b/fastlane/metadata/android/hu/changelogs/83.txt index 26718d870..a2b9d6c27 100644 --- a/fastlane/metadata/android/hu/changelogs/83.txt +++ b/fastlane/metadata/android/hu/changelogs/83.txt @@ -1,3 +1,3 @@ Tusky v15.1 -Ez a kiadás egy képek feliratozása közben jelentkező hibát javít +Ez a kiadás javít egy képek feliratozása közben jelentkező összeomlást diff --git a/fastlane/metadata/android/hu/full_description.txt b/fastlane/metadata/android/hu/full_description.txt index cd63195e1..dead1786d 100644 --- a/fastlane/metadata/android/hu/full_description.txt +++ b/fastlane/metadata/android/hu/full_description.txt @@ -1,12 +1,12 @@ -A Tusky egy kliens a Mastodonhoz, mely egy nyílt forráskódú szociális háló szerver. +A Tusky egy könnyűsúlyú kliens a Mastodonhoz, mely egy nyílt forráskódú közösségi hálózati kiszolgáló. • Material design -• A legtöbb Mastodon API-t implementáltuk +• A legtöbb Mastodon API-t megvalósításra került • Többfiókos támogatás • Sötét és világos téma, valamint automatikus váltási lehetőség napszak szerint -• Piszkozatok - készíts tülköket és mentsd el őket későbbre -• Emoji stílusok közötti választás +• Piszkozatok – tülkök készítése, és mentés későbbre +• Emodzsi stílusok közötti választás • Minden képernyőméretre optimalizálva -• Teljesen nyílt forráskód - nincsenek nem szabad függőségeink pl. Google Services +• Teljesen nyílt forráskód – nincsenek nem szabad függőségek, mint például a Google szolgáltatások -Többet a Mastodonról: https://joinmastodon.org/ +Több információ a Mastodonról: https://joinmastodon.org/ diff --git a/fastlane/metadata/android/hu/short_description.txt b/fastlane/metadata/android/hu/short_description.txt index 7c7b0a95b..130eac3b3 100644 --- a/fastlane/metadata/android/hu/short_description.txt +++ b/fastlane/metadata/android/hu/short_description.txt @@ -1 +1 @@ -Többfiókos kliens a Mastodon szociális hálóhoz +Többfiókos kliens a Mastodon közösségi hálóhoz From 46d4e8527853b1fcbe9329ed94072143cc189b67 Mon Sep 17 00:00:00 2001 From: XoseM Date: Sat, 12 Jun 2021 01:40:24 +0000 Subject: [PATCH 67/92] Translated using Weblate (Galician) Currently translated at 100.0% (14 of 14 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/gl/ --- fastlane/metadata/android/gl/changelogs/83.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 fastlane/metadata/android/gl/changelogs/83.txt diff --git a/fastlane/metadata/android/gl/changelogs/83.txt b/fastlane/metadata/android/gl/changelogs/83.txt new file mode 100644 index 000000000..60ed158d5 --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/83.txt @@ -0,0 +1,3 @@ +Tusky v15.1 + +Esta versión arranxa o problema coa descrición de imaxes From f695aae917ea1f468134edebbfa274ad794ebb43 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Sat, 12 Jun 2021 01:40:24 +0000 Subject: [PATCH 68/92] Translated using Weblate (Ukrainian) Currently translated at 100.0% (14 of 14 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/uk/ --- fastlane/metadata/android/uk/changelogs/83.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 fastlane/metadata/android/uk/changelogs/83.txt diff --git a/fastlane/metadata/android/uk/changelogs/83.txt b/fastlane/metadata/android/uk/changelogs/83.txt new file mode 100644 index 000000000..d8e3f5f02 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/83.txt @@ -0,0 +1,3 @@ +Tusky v15.1 + +У цьому випуску виправлено збої під час захоплення зображень From 9f40b283ee82048b1e9536ae2bbcfc74ed1ebbc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Mesk=C3=B3?= Date: Sat, 12 Jun 2021 01:40:22 +0000 Subject: [PATCH 69/92] Translated using Weblate (Hungarian) Currently translated at 100.0% (458 of 458 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/hu/ --- app/src/main/res/values-hu/strings.xml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 4596127b4..1a7579703 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -60,7 +60,7 @@ Kedvencnek jelölés Több Szerkesztés - Bejelentkezés Mastodon-nal + Bejelentkezés Mastodonnal Kijelentkezés Biztosan ki szeretnél jelentkezni a következőből: %1$s? Követés @@ -235,7 +235,7 @@ Média több betöltése Fiók hozzáadása - Új Mastodon fiók hozzáadása + Új Mastodon-fiók hozzáadása Listák Listák Törlés @@ -255,12 +255,12 @@ Keresés… Tülk megnyitása Az app újraindítása szükséges - A beállítások érvényesítéséhez újra kell indítani a Tusky-t + A beállítások érvényesítéséhez újra kell indítani a Tuskyt Később Újraindítás - Az eszközöd alapértelmezett emoji készlete - Az Android 4.4-7.1 Blob emoji-jai - Mastodon alapértelmezett emoji készlet + Az eszközöd alapértelmezett emodzsi készlete + Az Android 4.4–7.1 Blob emodzsijai + A Mastodon alapértelmezett emodzsi készlete Letöltés sikertelen Bot %1$s elköltözött: @@ -351,7 +351,7 @@ Cím beállítása Minden követődet külön engedélyezned kell Minden tülk kibontása/összecsukása - Google jelenlegi emoji készlete + A Google jelenlegi emodzsi készlete Megtolás az eredeti közönségnek Megtolás visszavonása Apache licensz alatt @@ -379,10 +379,10 @@ Törlés Szűrés Alkalmaz - Tülk Szerkesztése + Tülk szerkesztése Szerkesztés Biztos, hogy minden értesítésedet véglegesen törlöd\? - Műveletek a %s képpel + Műveletek a(z) %s képpel %1$s • %2$s %s szavazat @@ -456,7 +456,7 @@ Követési kérelmek Jóváhagyó ablak mutatása megtolás előtt Hivatkozás előnézetének mutatása idővonalakon - Tabok közötti váltás engedélyezése csúsztatással + Lapok közötti váltás engedélyezése csúsztatással %s személy %s személy @@ -484,7 +484,7 @@ Saját, mások számára nem látható megjegyzés erről a fiókról Nincsenek közlemények. Közlemények - A Tülköt, melyre válaszul piszkozatot készítettél törölték + A tülköt, melyre válaszul piszkozatot készítettél törölték Piszkozat törölve Nem sikerült a Válasz információit betölteni Ez a tülk nem küldődött el! From 1054aaf47f3d12d79a30fb33821c0ab4d85c40d1 Mon Sep 17 00:00:00 2001 From: Alberto Vacca Date: Sat, 12 Jun 2021 01:40:22 +0000 Subject: [PATCH 70/92] Translated using Weblate (Italian) Currently translated at 98.6% (452 of 458 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/it/ --- app/src/main/res/values-it/strings.xml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index c84301b13..ff60689a1 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -217,6 +217,7 @@ %1$s, %2$s e %3$s %1$s e %2$s + %d nuova interazione %d nuove interazioni Account bloccato @@ -341,6 +342,7 @@ %1$s e %2$s %1$s, %2$s ed altri %3$d + limite massimo di %1$d tab raggiunto limite massimo di %1$d tab raggiunto Media: %s @@ -499,6 +501,16 @@ qualcuno a cui sono iscritto ha pubblicato un nuovo toot %s appena pubblicato + Non puoi caricare più di %1$d allegato multimediale. Non puoi caricare più di %1$d allegati multimediali. + Il toot a cui hai scritto una risposta è stato rimosso + Bozza cancellata + L\'invio di questo toot è fallito! + Sei sicuro di voler cancellare la lista %s\? + Indefinita + Durata + Allegati + Audio + Mostra le animazioni delle emojis personalizzate \ No newline at end of file From 26ab5a22a8f808b3eefbfe6b34de66cc75d44b55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BF=BB=E8=A8=B3=E8=80=85X?= Date: Sat, 12 Jun 2021 01:40:23 +0000 Subject: [PATCH 71/92] Translated using Weblate (Japanese) Currently translated at 92.3% (423 of 458 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/ja/ --- app/src/main/res/values-ja/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index ec7e2a6a7..c2ba3f2e2 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -467,4 +467,5 @@ 本当に %s のすべてをブロックするのですか? そのドメインからのコンテンツは、公開タイムラインや通知に表示されなくなります。また、そのドメインのフォロワーは削除されます。 音声 ドメイン全体を非表示 + Tuskyによって提供されています \ No newline at end of file From ed001db6e418d2b54b2d6e20ff06634369a19f8a Mon Sep 17 00:00:00 2001 From: idontwanttohaveausername Date: Sat, 12 Jun 2021 01:40:23 +0000 Subject: [PATCH 72/92] Translated using Weblate (Ukrainian) Currently translated at 100.0% (458 of 458 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/uk/ --- app/src/main/res/values-uk/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 93910a137..3756c73f8 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -88,7 +88,7 @@ Заблокувати Відписатися Підписатися - Ви дійсно хочете вийти з облікового запису %1$s\? + Ви впевнені, що хочете вийти з облікового запису %1$s\? Написати Не подобається Додати в закладки From f04a2a1ee445cdfc52957bd5321781fb81527587 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Mon, 14 Jun 2021 10:22:08 +0200 Subject: [PATCH 73/92] fix reblog avatar (#2197) --- .../java/com/keylesspalace/tusky/viewdata/StatusViewData.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt index d02569d41..c3bf14183 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt @@ -60,7 +60,11 @@ sealed class StatusViewData private constructor() { get() = status.actionableStatus val rebloggedAvatar: String? - get() = status.reblog?.account?.avatar + get() = if (status.reblog != null) { + status.account.avatar + } else { + null + } val rebloggingStatus: Status? get() = if (status.reblog != null) status else null From e84dec29b215cccec72c29562edb0f285b2e5040 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Mon, 14 Jun 2021 11:00:25 +0200 Subject: [PATCH 74/92] update dependencies (#2198) --- app/build.gradle | 10 +++++----- build.gradle | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index ca917abdf..84ca536ac 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -90,8 +90,8 @@ ext.roomVersion = '2.3.0' ext.retrofitVersion = '2.9.0' ext.okhttpVersion = '4.9.1' ext.glideVersion = '4.12.0' -ext.daggerVersion = '2.35.1' -ext.materialdrawerVersion = '8.2.0' +ext.daggerVersion = '2.37' +ext.materialdrawerVersion = '8.4.1' // if libraries are changed here, they should also be changed in LicenseActivity dependencies { @@ -99,10 +99,10 @@ dependencies { implementation "androidx.core:core-ktx:1.5.0" implementation "androidx.appcompat:appcompat:1.3.0" - implementation "androidx.fragment:fragment-ktx:1.3.3" + implementation "androidx.fragment:fragment-ktx:1.3.4" implementation "androidx.browser:browser:1.3.0" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" - implementation "androidx.recyclerview:recyclerview:1.2.0" + implementation "androidx.recyclerview:recyclerview:1.2.1" implementation "androidx.exifinterface:exifinterface:1.3.2" implementation "androidx.cardview:cardview:1.0.0" implementation "androidx.preference:preference-ktx:1.1.1" @@ -132,7 +132,7 @@ dependencies { implementation "com.squareup.okhttp3:okhttp:$okhttpVersion" implementation "com.squareup.okhttp3:logging-interceptor:$okhttpVersion" - implementation "org.conscrypt:conscrypt-android:2.5.1" + implementation "org.conscrypt:conscrypt-android:2.5.2" implementation "com.github.bumptech.glide:glide:$glideVersion" implementation "com.github.bumptech.glide:okhttp3-integration:$glideVersion" diff --git a/build.gradle b/build.gradle index 0cd366037..3bc072dcd 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.5.0' + ext.kotlin_version = '1.5.10' repositories { google() mavenCentral() From 31da851f289c6f7cd15a552c59de2c2420af7fd6 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Mon, 14 Jun 2021 11:00:35 +0200 Subject: [PATCH 75/92] correctly serialize custom spans to html (#2199) --- .../com/keylesspalace/tusky/AboutActivity.kt | 4 +- .../tusky/util/ClickableSpanNoUnderline.kt | 11 ----- .../tusky/util/CustomURLSpan.java | 41 ------------------- .../keylesspalace/tusky/util/LinkHelper.java | 12 +++--- .../tusky/util/NoUnderlineURLSpan.kt | 34 +++++++++++++++ .../com/keylesspalace/tusky/util/SpanUtils.kt | 4 +- 6 files changed, 44 insertions(+), 62 deletions(-) delete mode 100644 app/src/main/java/com/keylesspalace/tusky/util/ClickableSpanNoUnderline.kt delete mode 100644 app/src/main/java/com/keylesspalace/tusky/util/CustomURLSpan.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/NoUnderlineURLSpan.kt diff --git a/app/src/main/java/com/keylesspalace/tusky/AboutActivity.kt b/app/src/main/java/com/keylesspalace/tusky/AboutActivity.kt index ada7af365..8246555b8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AboutActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/AboutActivity.kt @@ -11,7 +11,7 @@ import android.text.util.Linkify import android.widget.TextView import com.keylesspalace.tusky.databinding.ActivityAboutBinding import com.keylesspalace.tusky.di.Injectable -import com.keylesspalace.tusky.util.CustomURLSpan +import com.keylesspalace.tusky.util.NoUnderlineURLSpan import com.keylesspalace.tusky.util.hide class AboutActivity : BottomSheetActivity(), Injectable { @@ -63,7 +63,7 @@ private fun TextView.setClickableTextWithoutUnderlines(@StringRes textId: Int) { val end = builder.getSpanEnd(span) val flags = builder.getSpanFlags(span) - val customSpan = object : CustomURLSpan(span.url) {} + val customSpan = NoUnderlineURLSpan(span.url) builder.removeSpan(span) builder.setSpan(customSpan, start, end, flags) diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ClickableSpanNoUnderline.kt b/app/src/main/java/com/keylesspalace/tusky/util/ClickableSpanNoUnderline.kt deleted file mode 100644 index a9e7ba89a..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/util/ClickableSpanNoUnderline.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.keylesspalace.tusky.util - -import android.text.TextPaint -import android.text.style.ClickableSpan - -abstract class ClickableSpanNoUnderline : ClickableSpan() { - override fun updateDrawState(ds: TextPaint) { - super.updateDrawState(ds) - ds.isUnderlineText = false - } -} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/util/CustomURLSpan.java b/app/src/main/java/com/keylesspalace/tusky/util/CustomURLSpan.java deleted file mode 100644 index e772162e5..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/util/CustomURLSpan.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.keylesspalace.tusky.util; - -import android.os.Parcel; -import android.os.Parcelable; -import android.text.TextPaint; -import android.text.style.URLSpan; -import android.view.View; - -public class CustomURLSpan extends URLSpan { - public CustomURLSpan(String url) { - super(url); - } - - private CustomURLSpan(Parcel src) { - super(src); - } - - public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { - - @Override - public CustomURLSpan createFromParcel(Parcel source) { - return new CustomURLSpan(source); - } - - @Override - public CustomURLSpan[] newArray(int size) { - return new CustomURLSpan[size]; - } - - }; - - @Override - public void onClick(View view) { - LinkHelper.openLink(getURL(), view.getContext()); - } - - @Override public void updateDrawState(TextPaint ds) { - super.updateDrawState(ds); - ds.setUnderlineText(false); - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java b/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java index b0a475742..746bf5e69 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java @@ -82,7 +82,7 @@ public class LinkHelper { if (text.charAt(0) == '#') { final String tag = text.subSequence(1, text.length()).toString(); - customSpan = new ClickableSpanNoUnderline() { + customSpan = new NoUnderlineURLSpan(span.getURL()) { @Override public void onClick(@NonNull View widget) { listener.onViewTag(tag); } }; @@ -102,7 +102,7 @@ public class LinkHelper { } if (id != null) { final String accountId = id; - customSpan = new ClickableSpanNoUnderline() { + customSpan = new NoUnderlineURLSpan(span.getURL()) { @Override public void onClick(@NonNull View widget) { listener.onViewAccount(accountId); } }; @@ -110,9 +110,9 @@ public class LinkHelper { } if (customSpan == null) { - customSpan = new CustomURLSpan(span.getURL()) { + customSpan = new NoUnderlineURLSpan(span.getURL()) { @Override - public void onClick(View widget) { + public void onClick(@NonNull View widget) { listener.onViewUrl(getURL()); } }; @@ -155,7 +155,7 @@ public class LinkHelper { for (Status.Mention mention : mentions) { String accountUsername = mention.getLocalUsername(); final String accountId = mention.getId(); - ClickableSpan customSpan = new ClickableSpanNoUnderline() { + ClickableSpan customSpan = new NoUnderlineURLSpan(mention.getUrl()) { @Override public void onClick(@NonNull View widget) { listener.onViewAccount(accountId); } }; @@ -181,7 +181,7 @@ public class LinkHelper { } public static CharSequence createClickableText(String text, String link) { - URLSpan span = new CustomURLSpan(link); + URLSpan span = new NoUnderlineURLSpan(link); SpannableStringBuilder clickableText = new SpannableStringBuilder(text); clickableText.setSpan(span, 0, text.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); diff --git a/app/src/main/java/com/keylesspalace/tusky/util/NoUnderlineURLSpan.kt b/app/src/main/java/com/keylesspalace/tusky/util/NoUnderlineURLSpan.kt new file mode 100644 index 000000000..a9b56b894 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/NoUnderlineURLSpan.kt @@ -0,0 +1,34 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.util + +import android.text.TextPaint +import android.text.style.URLSpan +import android.view.View + +open class NoUnderlineURLSpan( + url: String +) : URLSpan(url) { + + override fun updateDrawState(ds: TextPaint) { + super.updateDrawState(ds) + ds.isUnderlineText = false + } + + override fun onClick(view: View) { + LinkHelper.openLink(url, view.context) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/SpanUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/SpanUtils.kt index 307fbeae7..b0c3850f7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/SpanUtils.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/SpanUtils.kt @@ -117,8 +117,8 @@ private fun findEndOfPattern(string: String, result: FindCharsResult, pattern: P private fun getSpan(matchType: FoundMatchType, string: String, colour: Int, start: Int, end: Int): CharacterStyle { return when(matchType) { - FoundMatchType.HTTP_URL -> CustomURLSpan(string.substring(start, end)) - FoundMatchType.HTTPS_URL -> CustomURLSpan(string.substring(start, end)) + FoundMatchType.HTTP_URL -> NoUnderlineURLSpan(string.substring(start, end)) + FoundMatchType.HTTPS_URL -> NoUnderlineURLSpan(string.substring(start, end)) else -> ForegroundColorSpan(colour) } } From 6d4f5ad0279e5ff51f143742fca060dd04e1c30c Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Thu, 17 Jun 2021 18:54:56 +0200 Subject: [PATCH 76/92] migrate to paging 3 (#2182) * migrate conversations and search to paging 3 * delete SearchRepository * remove unneeded executor from search * fix bugs in conversations * update license headers * fix conversations refreshing * fix search refresh indicators * show fullscreen loading while conversations are empty * search bugfixes * error handling * error handling * remove mastodon bug workaround * update ConversationsFragment * fix conversations more menu and deleting conversations * delete unused class * catch exceptions in ConversationsViewModel * fix bug where items are not diffed correctly / cleanup code * fix search progressbar display conditions --- app/build.gradle | 9 +- .../27.json | 753 ++++++++++++++++++ .../tusky/adapter/NetworkStateViewHolder.kt | 24 +- .../conversation/ConversationAdapter.kt | 111 +-- .../conversation/ConversationEntity.kt | 203 ++--- .../ConversationLoadStateAdapter.kt | 41 + .../ConversationsBoundaryCallback.kt | 98 --- .../conversation/ConversationsFragment.kt | 159 +++- .../ConversationsRemoteMediator.kt | 51 ++ .../conversation/ConversationsRepository.kt | 111 +-- .../conversation/ConversationsViewModel.kt | 215 +++-- .../tusky/components/search/SearchType.kt | 15 + .../components/search/SearchViewModel.kt | 172 ++-- .../search/adapter/SearchAccountsAdapter.kt | 17 +- .../search/adapter/SearchDataSource.kt | 126 --- .../search/adapter/SearchHashtagsAdapter.kt | 10 +- .../search/adapter/SearchPagingSource.kt | 83 ++ ...actory.kt => SearchPagingSourceFactory.kt} | 47 +- .../search/adapter/SearchRepository.kt | 56 -- .../search/adapter/SearchStatusesAdapter.kt | 31 +- .../fragments/SearchAccountsFragment.kt | 19 +- .../search/fragments/SearchFragment.kt | 88 +- .../fragments/SearchHashtagsFragment.kt | 20 +- .../fragments/SearchStatusesFragment.kt | 49 +- .../keylesspalace/tusky/db/AppDatabase.java | 9 +- .../tusky/db/ConversationsDao.kt | 18 +- .../com/keylesspalace/tusky/di/AppModule.kt | 1 + .../tusky/network/MastodonApi.kt | 45 +- .../com/keylesspalace/tusky/util/BiListing.kt | 2 +- .../com/keylesspalace/tusky/util/Listing.kt | 36 - app/src/main/res/menu/conversation_more.xml | 13 + app/src/main/res/values/strings.xml | 2 + 32 files changed, 1612 insertions(+), 1022 deletions(-) create mode 100644 app/schemas/com.keylesspalace.tusky.db.AppDatabase/27.json create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationLoadStateAdapter.kt delete mode 100644 app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsBoundaryCallback.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRemoteMediator.kt delete mode 100644 app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchDataSource.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchPagingSource.kt rename app/src/main/java/com/keylesspalace/tusky/components/search/adapter/{SearchDataSourceFactory.kt => SearchPagingSourceFactory.kt} (50%) delete mode 100644 app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchRepository.kt delete mode 100644 app/src/main/java/com/keylesspalace/tusky/util/Listing.kt create mode 100644 app/src/main/res/menu/conversation_more.xml diff --git a/app/build.gradle b/app/build.gradle index 84ca536ac..7f2dcc4ef 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -97,6 +97,9 @@ ext.materialdrawerVersion = '8.4.1' dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-rx3:1.5.0' + implementation "androidx.core:core-ktx:1.5.0" implementation "androidx.appcompat:appcompat:1.3.0" implementation "androidx.fragment:fragment-ktx:1.3.4" @@ -114,13 +117,11 @@ dependencies { implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-reactivestreams-ktx:$lifecycleVersion" implementation "androidx.constraintlayout:constraintlayout:2.0.4" - implementation "androidx.paging:paging-runtime-ktx:2.1.2" + implementation "androidx.paging:paging-runtime-ktx:3.0.0" implementation "androidx.viewpager2:viewpager2:1.0.0" implementation "androidx.work:work-runtime:2.5.0" - implementation "androidx.room:room-runtime:$roomVersion" + implementation "androidx.room:room-ktx:$roomVersion" implementation "androidx.room:room-rxjava3:$roomVersion" - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0' - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-rx3:1.5.0' kapt "androidx.room:room-compiler:$roomVersion" implementation "com.google.android.material:material:1.3.0" diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/27.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/27.json new file mode 100644 index 000000000..c83963093 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/27.json @@ -0,0 +1,753 @@ +{ + "formatVersion": 1, + "database": { + "version": 27, + "identityHash": "be914d4eb3f406b6970fef53a925afa1", + "entities": [ + { + "tableName": "DraftEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'be914d4eb3f406b6970fef53a925afa1')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/NetworkStateViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/NetworkStateViewHolder.kt index b45ca95f7..b991def5e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/NetworkStateViewHolder.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/NetworkStateViewHolder.kt @@ -15,30 +15,28 @@ package com.keylesspalace.tusky.adapter +import androidx.paging.LoadState import androidx.recyclerview.widget.RecyclerView -import android.view.ViewGroup import com.keylesspalace.tusky.databinding.ItemNetworkStateBinding -import com.keylesspalace.tusky.util.NetworkState -import com.keylesspalace.tusky.util.Status import com.keylesspalace.tusky.util.visible class NetworkStateViewHolder(private val binding: ItemNetworkStateBinding, private val retryCallback: () -> Unit) : RecyclerView.ViewHolder(binding.root) { - fun setUpWithNetworkState(state: NetworkState?, fullScreen: Boolean) { - binding.progressBar.visible(state?.status == Status.RUNNING) - binding.retryButton.visible(state?.status == Status.FAILED) - binding.errorMsg.visible(state?.msg != null) - binding.errorMsg.text = state?.msg + fun setUpWithNetworkState(state: LoadState) { + binding.progressBar.visible(state == LoadState.Loading) + binding.retryButton.visible(state is LoadState.Error) + val msg = if (state is LoadState.Error) { + state.error.message + } else { + null + } + binding.errorMsg.visible(msg != null) + binding.errorMsg.text = msg binding.retryButton.setOnClickListener { retryCallback() } - if(fullScreen) { - binding.root.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT - } else { - binding.root.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT - } } } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt index 376d3cd56..89c1ad0f1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt @@ -17,114 +17,39 @@ package com.keylesspalace.tusky.components.conversation import android.view.LayoutInflater import android.view.ViewGroup -import androidx.paging.AsyncPagedListDiffer -import androidx.paging.PagedList -import androidx.recyclerview.widget.AsyncDifferConfig +import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListUpdateCallback -import androidx.recyclerview.widget.RecyclerView import com.keylesspalace.tusky.R -import com.keylesspalace.tusky.adapter.NetworkStateViewHolder -import com.keylesspalace.tusky.databinding.ItemNetworkStateBinding import com.keylesspalace.tusky.interfaces.StatusActionListener -import com.keylesspalace.tusky.util.NetworkState import com.keylesspalace.tusky.util.StatusDisplayOptions class ConversationAdapter( - private val statusDisplayOptions: StatusDisplayOptions, - private val listener: StatusActionListener, - private val topLoadedCallback: () -> Unit, - private val retryCallback: () -> Unit -) : RecyclerView.Adapter() { + private val statusDisplayOptions: StatusDisplayOptions, + private val listener: StatusActionListener +) : PagingDataAdapter(CONVERSATION_COMPARATOR) { - private var networkState: NetworkState? = null - - private val differ: AsyncPagedListDiffer = AsyncPagedListDiffer(object : ListUpdateCallback { - override fun onInserted(position: Int, count: Int) { - notifyItemRangeInserted(position, count) - if (position == 0) { - topLoadedCallback() - } - } - - override fun onRemoved(position: Int, count: Int) { - notifyItemRangeRemoved(position, count) - } - - override fun onMoved(fromPosition: Int, toPosition: Int) { - notifyItemMoved(fromPosition, toPosition) - } - - override fun onChanged(position: Int, count: Int, payload: Any?) { - notifyItemRangeChanged(position, count, payload) - } - }, AsyncDifferConfig.Builder(CONVERSATION_COMPARATOR).build()) - - fun submitList(list: PagedList) { - differ.submitList(list) + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ConversationViewHolder { + val view = LayoutInflater.from(parent.context).inflate(R.layout.item_conversation, parent, false) + return ConversationViewHolder(view, statusDisplayOptions, listener) } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return when (viewType) { - R.layout.item_network_state -> { - val binding = ItemNetworkStateBinding.inflate(LayoutInflater.from(parent.context), parent, false) - NetworkStateViewHolder(binding, retryCallback) - } - R.layout.item_conversation -> { - val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false) - ConversationViewHolder(view, statusDisplayOptions, listener) - } - else -> throw IllegalArgumentException("unknown view type $viewType") - } + override fun onBindViewHolder(holder: ConversationViewHolder, position: Int) { + holder.setupWithConversation(getItem(position)) } - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - when (getItemViewType(position)) { - R.layout.item_network_state -> (holder as NetworkStateViewHolder).setUpWithNetworkState(networkState, differ.itemCount == 0) - R.layout.item_conversation -> (holder as ConversationViewHolder).setupWithConversation(differ.getItem(position)) - } - } - - private fun hasExtraRow() = networkState != null && networkState != NetworkState.LOADED - - override fun getItemViewType(position: Int): Int { - return if (hasExtraRow() && position == itemCount - 1) { - R.layout.item_network_state - } else { - R.layout.item_conversation - } - } - - override fun getItemCount(): Int { - return differ.itemCount + if (hasExtraRow()) 1 else 0 - } - - fun setNetworkState(newNetworkState: NetworkState?) { - val previousState = this.networkState - val hadExtraRow = hasExtraRow() - this.networkState = newNetworkState - val hasExtraRow = hasExtraRow() - if (hadExtraRow != hasExtraRow) { - if (hadExtraRow) { - notifyItemRemoved(differ.itemCount) - } else { - notifyItemInserted(differ.itemCount) - } - } else if (hasExtraRow && previousState != newNetworkState) { - notifyItemChanged(itemCount - 1) - } + fun item(position: Int): ConversationEntity? { + return getItem(position) } companion object { - val CONVERSATION_COMPARATOR = object : DiffUtil.ItemCallback() { - override fun areContentsTheSame(oldItem: ConversationEntity, newItem: ConversationEntity): Boolean = - oldItem == newItem + override fun areItemsTheSame(oldItem: ConversationEntity, newItem: ConversationEntity): Boolean { + return oldItem.id == newItem.id + } - override fun areItemsTheSame(oldItem: ConversationEntity, newItem: ConversationEntity): Boolean = - oldItem.id == newItem.id + override fun areContentsTheSame(oldItem: ConversationEntity, newItem: ConversationEntity): Boolean { + return oldItem == newItem + } } - } - -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt index 0ecfe3b5e..7caa91144 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt @@ -1,4 +1,4 @@ -/* Copyright 2019 Conny Duck +/* Copyright 2021 Tusky Contributors * * This file is a part of Tusky. * @@ -21,65 +21,70 @@ import androidx.room.Embedded import androidx.room.Entity import androidx.room.TypeConverters import com.keylesspalace.tusky.db.Converters -import com.keylesspalace.tusky.entity.* +import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.entity.Conversation +import com.keylesspalace.tusky.entity.Emoji +import com.keylesspalace.tusky.entity.Poll +import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.util.shouldTrimStatus -import java.util.* +import java.util.Date @Entity(primaryKeys = ["id","accountId"]) @TypeConverters(Converters::class) data class ConversationEntity( - val accountId: Long, - val id: String, - val accounts: List, - val unread: Boolean, - @Embedded(prefix = "s_") val lastStatus: ConversationStatusEntity + val accountId: Long, + val id: String, + val accounts: List, + val unread: Boolean, + @Embedded(prefix = "s_") val lastStatus: ConversationStatusEntity ) data class ConversationAccountEntity( - val id: String, - val username: String, - val displayName: String, - val avatar: String, - val emojis: List + val id: String, + val username: String, + val displayName: String, + val avatar: String, + val emojis: List ) { fun toAccount(): Account { return Account( - id = id, - username = username, - displayName = displayName, - avatar = avatar, - emojis = emojis, - url = "", - localUsername = "", - note = SpannedString(""), - header = "" + id = id, + username = username, + displayName = displayName, + avatar = avatar, + emojis = emojis, + url = "", + localUsername = "", + note = SpannedString(""), + header = "" ) } } @TypeConverters(Converters::class) data class ConversationStatusEntity( - val id: String, - val url: String?, - val inReplyToId: String?, - val inReplyToAccountId: String?, - val account: ConversationAccountEntity, - val content: Spanned, - val createdAt: Date, - val emojis: List, - val favouritesCount: Int, - val favourited: Boolean, - val bookmarked: Boolean, - val sensitive: Boolean, - val spoilerText: String, - val attachments: ArrayList, - val mentions: List, - val showingHiddenContent: Boolean, - val expanded: Boolean, - val collapsible: Boolean, - val collapsed: Boolean, - val poll: Poll? - + val id: String, + val url: String?, + val inReplyToId: String?, + val inReplyToAccountId: String?, + val account: ConversationAccountEntity, + val content: Spanned, + val createdAt: Date, + val emojis: List, + val favouritesCount: Int, + val favourited: Boolean, + val bookmarked: Boolean, + val sensitive: Boolean, + val spoilerText: String, + val attachments: ArrayList, + val mentions: List, + val showingHiddenContent: Boolean, + val expanded: Boolean, + val collapsible: Boolean, + val collapsed: Boolean, + val muted: Boolean, + val poll: Poll? ) { /** its necessary to override this because Spanned.equals does not work as expected */ override fun equals(other: Any?): Boolean { @@ -106,6 +111,7 @@ data class ConversationStatusEntity( if (expanded != other.expanded) return false if (collapsible != other.collapsible) return false if (collapsed != other.collapsed) return false + if (muted != other.muted) return false if (poll != other.poll) return false return true @@ -130,66 +136,79 @@ data class ConversationStatusEntity( result = 31 * result + expanded.hashCode() result = 31 * result + collapsible.hashCode() result = 31 * result + collapsed.hashCode() + result = 31 * result + muted.hashCode() result = 31 * result + poll.hashCode() return result } fun toStatus(): Status { return Status( - id = id, - url = url, - account = account.toAccount(), - inReplyToId = inReplyToId, - inReplyToAccountId = inReplyToAccountId, - content = content, - reblog = null, - createdAt = createdAt, - emojis = emojis, - reblogsCount = 0, - favouritesCount = favouritesCount, - reblogged = false, - favourited = favourited, - bookmarked = bookmarked, - sensitive= sensitive, - spoilerText = spoilerText, - visibility = Status.Visibility.DIRECT, - attachments = attachments, - mentions = mentions, - application = null, - pinned = false, - muted = false, - poll = poll, - card = null) + id = id, + url = url, + account = account.toAccount(), + inReplyToId = inReplyToId, + inReplyToAccountId = inReplyToAccountId, + content = content, + reblog = null, + createdAt = createdAt, + emojis = emojis, + reblogsCount = 0, + favouritesCount = favouritesCount, + reblogged = false, + favourited = favourited, + bookmarked = bookmarked, + sensitive= sensitive, + spoilerText = spoilerText, + visibility = Status.Visibility.DIRECT, + attachments = attachments, + mentions = mentions, + application = null, + pinned = false, + muted = muted, + poll = poll, + card = null) } } fun Account.toEntity() = - ConversationAccountEntity( - id, - username, - name, - avatar, - emojis ?: emptyList() - ) + ConversationAccountEntity( + id = id, + username = username, + displayName = name, + avatar = avatar, + emojis = emojis ?: emptyList() + ) fun Status.toEntity() = - ConversationStatusEntity( - id, url, inReplyToId, inReplyToAccountId, account.toEntity(), content, - createdAt, emojis, favouritesCount, favourited, bookmarked, sensitive, - spoilerText, attachments, mentions, - false, - false, - shouldTrimStatus(content), - true, - poll - ) - + ConversationStatusEntity( + id = id, + url = url, + inReplyToId = inReplyToId, + inReplyToAccountId = inReplyToAccountId, + account = account.toEntity(), + content = content, + createdAt = createdAt, + emojis = emojis, + favouritesCount = favouritesCount, + favourited = favourited, + bookmarked = bookmarked, + sensitive = sensitive, + spoilerText = spoilerText, + attachments = attachments, + mentions = mentions, + showingHiddenContent = false, + expanded = false, + collapsible = shouldTrimStatus(content), + collapsed = true, + muted = muted ?: false, + poll = poll + ) fun Conversation.toEntity(accountId: Long) = - ConversationEntity( - accountId, - id, - accounts.map { it.toEntity() }, - unread, - lastStatus!!.toEntity() - ) + ConversationEntity( + accountId = accountId, + id = id, + accounts = accounts.map { it.toEntity() }, + unread = unread, + lastStatus = lastStatus!!.toEntity() + ) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationLoadStateAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationLoadStateAdapter.kt new file mode 100644 index 000000000..d5c0983a0 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationLoadStateAdapter.kt @@ -0,0 +1,41 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.conversation + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.paging.LoadState +import androidx.paging.LoadStateAdapter +import com.keylesspalace.tusky.adapter.NetworkStateViewHolder +import com.keylesspalace.tusky.databinding.ItemNetworkStateBinding + +class ConversationLoadStateAdapter( + private val retryCallback: () -> Unit +) : LoadStateAdapter() { + + override fun onBindViewHolder(holder: NetworkStateViewHolder, loadState: LoadState) { + holder.setUpWithNetworkState(loadState) + } + + override fun onCreateViewHolder( + parent: ViewGroup, + loadState: LoadState + ): NetworkStateViewHolder { + val binding = ItemNetworkStateBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return NetworkStateViewHolder(binding, retryCallback) + } + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsBoundaryCallback.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsBoundaryCallback.kt deleted file mode 100644 index 5d3590157..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsBoundaryCallback.kt +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.keylesspalace.tusky.components.conversation - -import androidx.annotation.MainThread -import androidx.paging.PagedList -import com.keylesspalace.tusky.entity.Conversation -import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.util.PagingRequestHelper -import com.keylesspalace.tusky.util.createStatusLiveData -import retrofit2.Call -import retrofit2.Callback -import retrofit2.Response -import java.util.concurrent.Executor - -/** - * This boundary callback gets notified when user reaches to the edges of the list such that the - * database cannot provide any more data. - *

- * The boundary callback might be called multiple times for the same direction so it does its own - * rate limiting using the PagingRequestHelper class. - */ -class ConversationsBoundaryCallback( - private val accountId: Long, - private val mastodonApi: MastodonApi, - private val handleResponse: (Long, List?) -> Unit, - private val ioExecutor: Executor, - private val networkPageSize: Int) - : PagedList.BoundaryCallback() { - - val helper = PagingRequestHelper(ioExecutor) - val networkState = helper.createStatusLiveData() - - /** - * Database returned 0 items. We should query the backend for more items. - */ - @MainThread - override fun onZeroItemsLoaded() { - helper.runIfNotRunning(PagingRequestHelper.RequestType.INITIAL) { - mastodonApi.getConversations(null, networkPageSize) - .enqueue(createWebserviceCallback(it)) - } - } - - /** - * User reached to the end of the list. - */ - @MainThread - override fun onItemAtEndLoaded(itemAtEnd: ConversationEntity) { - helper.runIfNotRunning(PagingRequestHelper.RequestType.AFTER) { - mastodonApi.getConversations(itemAtEnd.lastStatus.id, networkPageSize) - .enqueue(createWebserviceCallback(it)) - } - } - - /** - * every time it gets new items, boundary callback simply inserts them into the database and - * paging library takes care of refreshing the list if necessary. - */ - private fun insertItemsIntoDb( - response: Response>, - it: PagingRequestHelper.Request.Callback) { - ioExecutor.execute { - handleResponse(accountId, response.body()) - it.recordSuccess() - } - } - - override fun onItemAtFrontLoaded(itemAtFront: ConversationEntity) { - // ignored, since we only ever append to what's in the DB - } - - private fun createWebserviceCallback(it: PagingRequestHelper.Request.Callback): Callback> { - return object : Callback> { - override fun onFailure(call: Call>, t: Throwable) { - it.recordFailure(t) - } - - override fun onResponse(call: Call>, response: Response>) { - insertItemsIntoDb(response, it) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt index 43f250c79..a484c6d06 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt @@ -1,4 +1,4 @@ -/* Copyright 2019 Conny Duck +/* Copyright 2021 Tusky Contributors * * This file is a part of Tusky. * @@ -20,7 +20,12 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.PopupMenu import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.paging.ExperimentalPagingApi +import androidx.paging.LoadState import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager @@ -35,8 +40,11 @@ import com.keylesspalace.tusky.fragment.SFragment import com.keylesspalace.tusky.interfaces.ReselectableFragment import com.keylesspalace.tusky.interfaces.StatusActionListener import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.util.* +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import java.io.IOException import com.keylesspalace.tusky.util.CardViewMode -import com.keylesspalace.tusky.util.NetworkState import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.viewBinding @@ -53,34 +61,39 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res private val binding by viewBinding(FragmentTimelineBinding::bind) private lateinit var adapter: ConversationAdapter + private lateinit var loadStateAdapter: ConversationLoadStateAdapter private var layoutManager: LinearLayoutManager? = null + private var initialRefreshDone: Boolean = false + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.fragment_timeline, container, false) } + @ExperimentalPagingApi override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val preferences = PreferenceManager.getDefaultSharedPreferences(view.context) val statusDisplayOptions = StatusDisplayOptions( - animateAvatars = preferences.getBoolean("animateGifAvatars", false), - mediaPreviewEnabled = accountManager.activeAccount?.mediaPreviewEnabled ?: true, - useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false), - showBotOverlay = preferences.getBoolean("showBotOverlay", true), - useBlurhash = preferences.getBoolean("useBlurhash", true), - cardViewMode = CardViewMode.NONE, - confirmReblogs = preferences.getBoolean("confirmReblogs", true), - hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false), - animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) + animateAvatars = preferences.getBoolean("animateGifAvatars", false), + mediaPreviewEnabled = accountManager.activeAccount?.mediaPreviewEnabled ?: true, + useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false), + showBotOverlay = preferences.getBoolean("showBotOverlay", true), + useBlurhash = preferences.getBoolean("useBlurhash", true), + cardViewMode = CardViewMode.NONE, + confirmReblogs = preferences.getBoolean("confirmReblogs", true), + hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false), + animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) ) - adapter = ConversationAdapter(statusDisplayOptions, this, ::onTopLoaded, viewModel::retry) + adapter = ConversationAdapter(statusDisplayOptions, this) + loadStateAdapter = ConversationLoadStateAdapter(adapter::retry) binding.recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)) layoutManager = LinearLayoutManager(view.context) binding.recyclerView.layoutManager = layoutManager - binding.recyclerView.adapter = adapter + binding.recyclerView.adapter = adapter.withLoadStateFooter(loadStateAdapter) (binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false binding.progressBar.hide() @@ -88,59 +101,101 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res initSwipeToRefresh() - viewModel.conversations.observe(viewLifecycleOwner) { - adapter.submitList(it) - } - viewModel.networkState.observe(viewLifecycleOwner) { - adapter.setNetworkState(it) + lifecycleScope.launch { + viewModel.conversationFlow.collectLatest { pagingData -> + adapter.submitData(pagingData) + } } - viewModel.load() + adapter.addLoadStateListener { loadStates -> + loadStates.refresh.let { refreshState -> + if (refreshState is LoadState.Error) { + binding.statusView.show() + if (refreshState.error is IOException) { + binding.statusView.setup(R.drawable.elephant_offline, R.string.error_network) { + adapter.refresh() + } + } else { + binding.statusView.setup(R.drawable.elephant_error, R.string.error_generic) { + adapter.refresh() + } + } + } else { + binding.statusView.hide() + } + + binding.progressBar.visible(refreshState == LoadState.Loading && adapter.itemCount == 0) + + if (refreshState is LoadState.NotLoading && !initialRefreshDone) { + // jump to top after the initial refresh finished + binding.recyclerView.scrollToPosition(0) + initialRefreshDone = true + } + + if (refreshState != LoadState.Loading) { + binding.swipeRefreshLayout.isRefreshing = false + } + } + } } private fun initSwipeToRefresh() { - viewModel.refreshState.observe(viewLifecycleOwner) { - binding.swipeRefreshLayout.isRefreshing = it == NetworkState.LOADING - } binding.swipeRefreshLayout.setOnRefreshListener { - viewModel.refresh() + adapter.refresh() } binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) } - private fun onTopLoaded() { - binding.recyclerView.scrollToPosition(0) - } - override fun onReblog(reblog: Boolean, position: Int) { // its impossible to reblog private messages } override fun onFavourite(favourite: Boolean, position: Int) { - viewModel.favourite(favourite, position) + adapter.item(position)?.let { conversation -> + viewModel.favourite(favourite, conversation) + } } override fun onBookmark(favourite: Boolean, position: Int) { - viewModel.bookmark(favourite, position) + adapter.item(position)?.let { conversation -> + viewModel.bookmark(favourite, conversation) + } } override fun onMore(view: View, position: Int) { - viewModel.conversations.value?.getOrNull(position)?.lastStatus?.let { - more(it.toStatus(), view, position) + adapter.item(position)?.let { conversation -> + + val popup = PopupMenu(requireContext(), view) + popup.inflate(R.menu.conversation_more) + + if (conversation.lastStatus.muted) { + popup.menu.removeItem(R.id.status_mute_conversation) + } else { + popup.menu.removeItem(R.id.status_unmute_conversation) + } + + popup.setOnMenuItemClickListener { item -> + when (item.itemId) { + R.id.status_mute_conversation -> viewModel.muteConversation(conversation) + R.id.status_unmute_conversation -> viewModel.muteConversation(conversation) + R.id.conversation_delete -> deleteConversation(conversation) + } + true + } + popup.show() } } override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) { - viewModel.conversations.value?.getOrNull(position)?.lastStatus?.let { - viewMedia(attachmentIndex, AttachmentViewData.list(it.toStatus()), view) + adapter.item(position)?.let { conversation -> + viewMedia(attachmentIndex, AttachmentViewData.list(conversation.lastStatus.toStatus()), view) } } override fun onViewThread(position: Int) { - viewModel.conversations.value?.getOrNull(position)?.lastStatus?.let { - val status = it.toStatus() - viewThread(status.actionableId, status.actionableStatus.url) + adapter.item(position)?.let { conversation -> + viewThread(conversation.lastStatus.id, conversation.lastStatus.url) } } @@ -149,11 +204,15 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res } override fun onExpandedChange(expanded: Boolean, position: Int) { - viewModel.expandHiddenStatus(expanded, position) + adapter.item(position)?.let { conversation -> + viewModel.expandHiddenStatus(expanded, conversation) + } } override fun onContentHiddenChange(isShowing: Boolean, position: Int) { - viewModel.showContent(isShowing, position) + adapter.item(position)?.let { conversation -> + viewModel.showContent(isShowing, conversation) + } } override fun onLoadMore(position: Int) { @@ -161,7 +220,9 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res } override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) { - viewModel.collapseLongStatus(isCollapsed, position) + adapter.item(position)?.let { conversation -> + viewModel.collapseLongStatus(isCollapsed, conversation) + } } override fun onViewAccount(id: String) { @@ -176,15 +237,25 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res } override fun removeItem(position: Int) { - viewModel.remove(position) + // not needed } override fun onReply(position: Int) { - viewModel.conversations.value?.getOrNull(position)?.lastStatus?.let { - reply(it.toStatus()) + adapter.item(position)?.let { conversation -> + reply(conversation.lastStatus.toStatus()) } } + private fun deleteConversation(conversation: ConversationEntity) { + AlertDialog.Builder(requireContext()) + .setMessage(R.string.dialog_delete_conversation_warning) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(android.R.string.ok) { _, _ -> + viewModel.remove(conversation) + } + .show() + } + private fun jumpToTop() { if (isAdded) { layoutManager?.scrollToPosition(0) @@ -197,7 +268,9 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res } override fun onVoteInPoll(position: Int, choices: MutableList) { - viewModel.voteInPoll(position, choices) + adapter.item(position)?.let { conversation -> + viewModel.voteInPoll(choices, conversation) + } } companion object { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRemoteMediator.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRemoteMediator.kt new file mode 100644 index 000000000..7418c3b08 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRemoteMediator.kt @@ -0,0 +1,51 @@ +package com.keylesspalace.tusky.components.conversation + +import androidx.paging.ExperimentalPagingApi +import androidx.paging.LoadType +import androidx.paging.PagingState +import androidx.paging.RemoteMediator +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.network.MastodonApi + +@ExperimentalPagingApi +class ConversationsRemoteMediator( + private val accountId: Long, + private val api: MastodonApi, + private val db: AppDatabase +) : RemoteMediator() { + + override suspend fun load( + loadType: LoadType, + state: PagingState + ): MediatorResult { + + try { + val conversationsResult = when (loadType) { + LoadType.REFRESH -> { + api.getConversations(limit = state.config.initialLoadSize) + } + LoadType.PREPEND -> { + return MediatorResult.Success(endOfPaginationReached = true) + } + LoadType.APPEND -> { + val maxId = state.pages.findLast { it.data.isNotEmpty() }?.data?.lastOrNull()?.lastStatus?.id + api.getConversations(maxId = maxId, limit = state.config.pageSize) + } + } + + if (loadType == LoadType.REFRESH) { + db.conversationDao().deleteForAccount(accountId) + } + db.conversationDao().insert( + conversationsResult + .filterNot { it.lastStatus == null } + .map { it.toEntity(accountId) } + ) + return MediatorResult.Success(endOfPaginationReached = conversationsResult.isEmpty()) + } catch (e: Exception) { + return MediatorResult.Error(e) + } + } + + override suspend fun initialize() = InitializeAction.LAUNCH_INITIAL_REFRESH +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRepository.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRepository.kt index e3703cbfe..2156b0189 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRepository.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRepository.kt @@ -1,99 +1,32 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + package com.keylesspalace.tusky.components.conversation -import androidx.annotation.MainThread -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.Transformations -import androidx.paging.Config -import androidx.paging.toLiveData import com.keylesspalace.tusky.db.AppDatabase -import com.keylesspalace.tusky.entity.Conversation import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.util.Listing -import com.keylesspalace.tusky.util.NetworkState import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.schedulers.Schedulers -import retrofit2.Call -import retrofit2.Callback -import retrofit2.Response -import java.util.concurrent.Executors import javax.inject.Inject import javax.inject.Singleton @Singleton -class ConversationsRepository @Inject constructor(val mastodonApi: MastodonApi, val db: AppDatabase) { - - private val ioExecutor = Executors.newSingleThreadExecutor() - - companion object { - private const val DEFAULT_PAGE_SIZE = 20 - } - - @MainThread - fun refresh(accountId: Long, showLoadingIndicator: Boolean): LiveData { - val networkState = MutableLiveData() - if(showLoadingIndicator) { - networkState.value = NetworkState.LOADING - } - - mastodonApi.getConversations(limit = DEFAULT_PAGE_SIZE).enqueue( - object : Callback> { - override fun onFailure(call: Call>, t: Throwable) { - // retrofit calls this on main thread so safe to call set value - networkState.value = NetworkState.error(t.message) - } - - override fun onResponse(call: Call>, response: Response>) { - ioExecutor.execute { - db.runInTransaction { - db.conversationDao().deleteForAccount(accountId) - insertResultIntoDb(accountId, response.body()) - } - // since we are in bg thread now, post the result. - networkState.postValue(NetworkState.LOADED) - } - } - } - ) - return networkState - } - - @MainThread - fun conversations(accountId: Long): Listing { - // create a boundary callback which will observe when the user reaches to the edges of - // the list and update the database with extra data. - val boundaryCallback = ConversationsBoundaryCallback( - accountId = accountId, - mastodonApi = mastodonApi, - handleResponse = this::insertResultIntoDb, - ioExecutor = ioExecutor, - networkPageSize = DEFAULT_PAGE_SIZE) - // we are using a mutable live data to trigger refresh requests which eventually calls - // refresh method and gets a new live data. Each refresh request by the user becomes a newly - // dispatched data in refreshTrigger - val refreshTrigger = MutableLiveData() - val refreshState = Transformations.switchMap(refreshTrigger) { - refresh(accountId, true) - } - - // We use toLiveData Kotlin extension function here, you could also use LivePagedListBuilder - val livePagedList = db.conversationDao().conversationsForAccount(accountId).toLiveData( - config = Config(pageSize = DEFAULT_PAGE_SIZE, prefetchDistance = DEFAULT_PAGE_SIZE / 2, enablePlaceholders = false), - boundaryCallback = boundaryCallback - ) - - return Listing( - pagedList = livePagedList, - networkState = boundaryCallback.networkState, - retry = { - boundaryCallback.helper.retryAllFailed() - }, - refresh = { - refreshTrigger.value = null - }, - refreshState = refreshState - ) - } +class ConversationsRepository @Inject constructor( + val mastodonApi: MastodonApi, + val db: AppDatabase +) { fun deleteCacheForAccount(accountId: Long) { Single.fromCallable { @@ -102,10 +35,4 @@ class ConversationsRepository @Inject constructor(val mastodonApi: MastodonApi, .subscribe() } - private fun insertResultIntoDb(accountId: Long, result: List?) { - result?.filter { it.lastStatus != null } - ?.map{ it.toEntity(accountId) } - ?.let { db.conversationDao().insert(it) } - - } } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt index 5f2b9cdb8..eafdbdf27 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt @@ -1,129 +1,100 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + package com.keylesspalace.tusky.components.conversation import android.util.Log -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.Transformations -import androidx.paging.PagedList +import androidx.lifecycle.viewModelScope +import androidx.paging.ExperimentalPagingApi +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.cachedIn import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.TimelineCases -import com.keylesspalace.tusky.util.Listing -import com.keylesspalace.tusky.util.NetworkState import com.keylesspalace.tusky.util.RxAwareViewModel -import io.reactivex.rxjava3.schedulers.Schedulers +import kotlinx.coroutines.launch +import kotlinx.coroutines.rx3.await import javax.inject.Inject class ConversationsViewModel @Inject constructor( - private val repository: ConversationsRepository, private val timelineCases: TimelineCases, private val database: AppDatabase, - private val accountManager: AccountManager + private val accountManager: AccountManager, + private val api: MastodonApi ) : RxAwareViewModel() { - private val repoResult = MutableLiveData>() + @ExperimentalPagingApi + val conversationFlow = Pager( + config = PagingConfig(pageSize = 10, enablePlaceholders = false, initialLoadSize = 20), + remoteMediator = ConversationsRemoteMediator(accountManager.activeAccount!!.id, api, database), + pagingSourceFactory = { database.conversationDao().conversationsForAccount(accountManager.activeAccount!!.id) } + ) + .flow + .cachedIn(viewModelScope) - val conversations: LiveData> = - Transformations.switchMap(repoResult) { it.pagedList } - val networkState: LiveData = - Transformations.switchMap(repoResult) { it.networkState } - val refreshState: LiveData = - Transformations.switchMap(repoResult) { it.refreshState } + fun favourite(favourite: Boolean, conversation: ConversationEntity) { + viewModelScope.launch { + try { + timelineCases.favourite(conversation.lastStatus.id, favourite).await() - fun load() { - val accountId = accountManager.activeAccount?.id ?: return - if (repoResult.value == null) { - repository.refresh(accountId, false) + val newConversation = conversation.copy( + lastStatus = conversation.lastStatus.copy(favourited = favourite) + ) + + database.conversationDao().insert(newConversation) + } catch (e: Exception) { + Log.w(TAG, "failed to favourite status", e) + } } - repoResult.value = repository.conversations(accountId) } - fun refresh() { - repoResult.value?.refresh?.invoke() - } + fun bookmark(bookmark: Boolean, conversation: ConversationEntity) { + viewModelScope.launch { + try { + timelineCases.bookmark(conversation.lastStatus.id, bookmark).await() - fun retry() { - repoResult.value?.retry?.invoke() - } + val newConversation = conversation.copy( + lastStatus = conversation.lastStatus.copy(bookmarked = bookmark) + ) - fun favourite(favourite: Boolean, position: Int) { - conversations.value?.getOrNull(position)?.let { conversation -> - timelineCases.favourite(conversation.lastStatus.id, favourite) - .flatMap { - val newConversation = conversation.copy( - lastStatus = conversation.lastStatus.copy(favourited = favourite) - ) - - database.conversationDao().insert(newConversation) - } - .subscribeOn(Schedulers.io()) - .doOnError { t -> - Log.w( - "ConversationViewModel", - "Failed to favourite conversation", - t - ) - } - .onErrorReturnItem(0) - .subscribe() - .autoDispose() + database.conversationDao().insert(newConversation) + } catch (e: Exception) { + Log.w(TAG, "failed to bookmark status", e) + } } - } - fun bookmark(bookmark: Boolean, position: Int) { - conversations.value?.getOrNull(position)?.let { conversation -> - timelineCases.bookmark(conversation.lastStatus.id, bookmark) - .flatMap { - val newConversation = conversation.copy( - lastStatus = conversation.lastStatus.copy(bookmarked = bookmark) - ) + fun voteInPoll(choices: List, conversation: ConversationEntity) { + viewModelScope.launch { + try { + val poll = timelineCases.voteInPoll(conversation.lastStatus.id, conversation.lastStatus.poll?.id!!, choices).await() + val newConversation = conversation.copy( + lastStatus = conversation.lastStatus.copy(poll = poll) + ) - database.conversationDao().insert(newConversation) - } - .subscribeOn(Schedulers.io()) - .doOnError { t -> - Log.w( - "ConversationViewModel", - "Failed to bookmark conversation", - t - ) - } - .onErrorReturnItem(0) - .subscribe() - .autoDispose() + database.conversationDao().insert(newConversation) + } catch (e: Exception) { + Log.w(TAG, "failed to vote in poll", e) + } } - } - fun voteInPoll(position: Int, choices: MutableList) { - conversations.value?.getOrNull(position)?.let { conversation -> - val poll = conversation.lastStatus.poll ?: return - timelineCases.voteInPoll(conversation.lastStatus.id, poll.id, choices) - .flatMap { newPoll -> - val newConversation = conversation.copy( - lastStatus = conversation.lastStatus.copy(poll = newPoll) - ) - - database.conversationDao().insert(newConversation) - } - .subscribeOn(Schedulers.io()) - .doOnError { t -> - Log.w( - "ConversationViewModel", - "Failed to favourite conversation", - t - ) - } - .onErrorReturnItem(0) - .subscribe() - .autoDispose() - } - - } - - fun expandHiddenStatus(expanded: Boolean, position: Int) { - conversations.value?.getOrNull(position)?.let { conversation -> + fun expandHiddenStatus(expanded: Boolean, conversation: ConversationEntity) { + viewModelScope.launch { val newConversation = conversation.copy( lastStatus = conversation.lastStatus.copy(expanded = expanded) ) @@ -131,8 +102,8 @@ class ConversationsViewModel @Inject constructor( } } - fun collapseLongStatus(collapsed: Boolean, position: Int) { - conversations.value?.getOrNull(position)?.let { conversation -> + fun collapseLongStatus(collapsed: Boolean, conversation: ConversationEntity) { + viewModelScope.launch { val newConversation = conversation.copy( lastStatus = conversation.lastStatus.copy(collapsed = collapsed) ) @@ -140,8 +111,8 @@ class ConversationsViewModel @Inject constructor( } } - fun showContent(showing: Boolean, position: Int) { - conversations.value?.getOrNull(position)?.let { conversation -> + fun showContent(showing: Boolean, conversation: ConversationEntity) { + viewModelScope.launch { val newConversation = conversation.copy( lastStatus = conversation.lastStatus.copy(showingHiddenContent = showing) ) @@ -149,16 +120,42 @@ class ConversationsViewModel @Inject constructor( } } - fun remove(position: Int) { - conversations.value?.getOrNull(position)?.let { - refresh() + fun remove(conversation: ConversationEntity) { + viewModelScope.launch { + try { + api.deleteConversation(conversationId = conversation.id) + + database.conversationDao().delete(conversation) + } catch (e: Exception) { + Log.w(TAG, "failed to delete conversation", e) + } } } - private fun saveConversationToDb(conversation: ConversationEntity) { - database.conversationDao().insert(conversation) - .subscribeOn(Schedulers.io()) - .subscribe() + fun muteConversation(conversation: ConversationEntity) { + viewModelScope.launch { + try { + val newStatus = timelineCases.muteConversation( + conversation.lastStatus.id, + !conversation.lastStatus.muted + ).await() + + val newConversation = conversation.copy( + lastStatus = newStatus.toEntity() + ) + + database.conversationDao().insert(newConversation) + } catch (e: Exception) { + Log.w(TAG, "failed to mute conversation", e) + } + } } + suspend fun saveConversationToDb(conversation: ConversationEntity) { + database.conversationDao().insert(conversation) + } + + companion object { + private const val TAG = "ConversationsViewModel" + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchType.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchType.kt index 5df657449..df98b9ab0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchType.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchType.kt @@ -1,3 +1,18 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + package com.keylesspalace.tusky.components.search enum class SearchType(val apiParameter: String) { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt index 2fdb76293..4ec51413b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt @@ -1,17 +1,35 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + package com.keylesspalace.tusky.components.search import android.util.Log -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.paging.PagedList -import com.keylesspalace.tusky.components.search.adapter.SearchRepository +import androidx.lifecycle.viewModelScope +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.cachedIn +import com.keylesspalace.tusky.components.search.adapter.SearchPagingSourceFactory import com.keylesspalace.tusky.db.AccountEntity import com.keylesspalace.tusky.db.AccountManager -import com.keylesspalace.tusky.entity.* +import com.keylesspalace.tusky.entity.DeletedStatus +import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.TimelineCases -import com.keylesspalace.tusky.util.* +import com.keylesspalace.tusky.util.RxAwareViewModel +import com.keylesspalace.tusky.util.toViewData import com.keylesspalace.tusky.viewdata.StatusViewData import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Single @@ -35,82 +53,62 @@ class SearchViewModel @Inject constructor( val alwaysShowSensitiveMedia = activeAccount?.alwaysShowSensitiveMedia ?: false val alwaysOpenSpoiler = activeAccount?.alwaysOpenSpoiler ?: false - private val statusesRepository = - SearchRepository>(mastodonApi) - private val accountsRepository = SearchRepository(mastodonApi) - private val hashtagsRepository = SearchRepository(mastodonApi) + private val loadedStatuses: MutableList> = mutableListOf() - private val repoResultStatus = MutableLiveData>>() - val statuses: LiveData>> = - repoResultStatus.switchMap { it.pagedList } - val networkStateStatus: LiveData = repoResultStatus.switchMap { it.networkState } - val networkStateStatusRefresh: LiveData = - repoResultStatus.switchMap { it.refreshState } + private val statusesPagingSourceFactory = SearchPagingSourceFactory(mastodonApi, SearchType.Status, loadedStatuses) { + it.statuses.map { status -> Pair(status, status.toViewData(alwaysShowSensitiveMedia, alwaysOpenSpoiler)) } + .apply { + loadedStatuses.addAll(this) + } + } + private val accountsPagingSourceFactory = SearchPagingSourceFactory(mastodonApi, SearchType.Account) { + it.accounts + } + private val hashtagsPagingSourceFactory = SearchPagingSourceFactory(mastodonApi, SearchType.Hashtag) { + it.hashtags + } - private val repoResultAccount = MutableLiveData>() - val accounts: LiveData> = repoResultAccount.switchMap { it.pagedList } - val networkStateAccount: LiveData = - repoResultAccount.switchMap { it.networkState } - val networkStateAccountRefresh: LiveData = - repoResultAccount.switchMap { it.refreshState } + val statusesFlow = Pager( + config = PagingConfig(pageSize = DEFAULT_LOAD_SIZE, initialLoadSize = DEFAULT_LOAD_SIZE), + pagingSourceFactory = statusesPagingSourceFactory + ).flow + .cachedIn(viewModelScope) - private val repoResultHashTag = MutableLiveData>() - val hashtags: LiveData> = repoResultHashTag.switchMap { it.pagedList } - val networkStateHashTag: LiveData = - repoResultHashTag.switchMap { it.networkState } - val networkStateHashTagRefresh: LiveData = - repoResultHashTag.switchMap { it.refreshState } + val accountsFlow = Pager( + config = PagingConfig(pageSize = DEFAULT_LOAD_SIZE, initialLoadSize = DEFAULT_LOAD_SIZE), + pagingSourceFactory = accountsPagingSourceFactory + ).flow + .cachedIn(viewModelScope) + + val hashtagsFlow = Pager( + config = PagingConfig(pageSize = DEFAULT_LOAD_SIZE, initialLoadSize = DEFAULT_LOAD_SIZE), + pagingSourceFactory = hashtagsPagingSourceFactory + ).flow + .cachedIn(viewModelScope) - private val loadedStatuses = ArrayList>() fun search(query: String) { loadedStatuses.clear() - repoResultStatus.value = statusesRepository.getSearchData( - SearchType.Status, - query, - disposables, - initialItems = loadedStatuses - ) { - it?.statuses?.map { status -> - Pair( - status, - status.toViewData(alwaysShowSensitiveMedia, alwaysOpenSpoiler) - ) - } - .orEmpty() - .apply { - loadedStatuses.addAll(this) - } - } - repoResultAccount.value = - accountsRepository.getSearchData(SearchType.Account, query, disposables) { - it?.accounts.orEmpty() - } - val hashtagQuery = if (query.startsWith("#")) query else "#$query" - repoResultHashTag.value = - hashtagsRepository.getSearchData(SearchType.Hashtag, hashtagQuery, disposables) { - it?.hashtags.orEmpty() - } - + statusesPagingSourceFactory.newSearch(query) + accountsPagingSourceFactory.newSearch(query) + hashtagsPagingSourceFactory.newSearch(query) } fun removeItem(status: Pair) { timelineCases.delete(status.first.id) - .subscribe({ - if (loadedStatuses.remove(status)) - repoResultStatus.value?.refresh?.invoke() - }, { err -> - Log.d(TAG, "Failed to delete status", err) - }) - .autoDispose() - + .subscribe({ + if (loadedStatuses.remove(status)) + statusesPagingSourceFactory.invalidate() + }, { + err -> Log.d(TAG, "Failed to delete status", err) + }) + .autoDispose() } fun expandedChange(status: Pair, expanded: Boolean) { val idx = loadedStatuses.indexOf(status) if (idx >= 0) { - val newPair = Pair(status.first, status.second.copy(isExpanded = expanded)) - loadedStatuses[idx] = newPair - repoResultStatus.value?.refresh?.invoke() + loadedStatuses[idx] = Pair(status.first, status.second.copy(isExpanded = expanded)) + statusesPagingSourceFactory.invalidate() } } @@ -119,36 +117,30 @@ class SearchViewModel @Inject constructor( .observeOn(AndroidSchedulers.mainThread()) .subscribe( { setRebloggedForStatus(status, reblog) }, - { err -> Log.d(TAG, "Failed to reblog status ${status.first.id}", err) } + { t -> Log.d(TAG, "Failed to reblog status ${status.first.id}", t) } ) .autoDispose() } - private fun setRebloggedForStatus( - status: Pair, - reblog: Boolean - ) { + private fun setRebloggedForStatus(status: Pair, reblog: Boolean) { status.first.reblogged = reblog status.first.reblog?.reblogged = reblog - - repoResultStatus.value?.refresh?.invoke() + statusesPagingSourceFactory.invalidate() } fun contentHiddenChange(status: Pair, isShowing: Boolean) { val idx = loadedStatuses.indexOf(status) if (idx >= 0) { - val newPair = Pair(status.first, status.second.copy(isShowingContent = isShowing)) - loadedStatuses[idx] = newPair - repoResultStatus.value?.refresh?.invoke() + loadedStatuses[idx] = Pair(status.first, status.second.copy(isShowingContent = isShowing)) + statusesPagingSourceFactory.invalidate() } } fun collapsedChange(status: Pair, collapsed: Boolean) { val idx = loadedStatuses.indexOf(status) if (idx >= 0) { - val newPair = Pair(status.first, status.second.copy(isCollapsed = collapsed)) - loadedStatuses[idx] = newPair - repoResultStatus.value?.refresh?.invoke() + loadedStatuses[idx] = Pair(status.first, status.second.copy(isCollapsed = collapsed)) + statusesPagingSourceFactory.invalidate() } } @@ -159,12 +151,7 @@ class SearchViewModel @Inject constructor( .observeOn(AndroidSchedulers.mainThread()) .subscribe( { newPoll -> updateStatus(status, newPoll) }, - { t -> - Log.d( - TAG, - "Failed to vote in poll: ${status.first.id}", t - ) - } + { t -> Log.d(TAG, "Failed to vote in poll: ${status.first.id}", t) } ) .autoDispose() } @@ -175,13 +162,13 @@ class SearchViewModel @Inject constructor( val newStatus = status.first.copy(poll = newPoll) val newViewData = status.second.copy(status = newStatus) loadedStatuses[idx] = Pair(newStatus, newViewData) - repoResultStatus.value?.refresh?.invoke() + statusesPagingSourceFactory.invalidate() } } fun favorite(status: Pair, isFavorited: Boolean) { status.first.favourited = isFavorited - repoResultStatus.value?.refresh?.invoke() + statusesPagingSourceFactory.invalidate() timelineCases.favourite(status.first.id, isFavorited) .onErrorReturnItem(status.first) .subscribe() @@ -190,7 +177,7 @@ class SearchViewModel @Inject constructor( fun bookmark(status: Pair, isBookmarked: Boolean) { status.first.bookmarked = isBookmarked - repoResultStatus.value?.refresh?.invoke() + statusesPagingSourceFactory.invalidate() timelineCases.bookmark(status.first.id, isBookmarked) .onErrorReturnItem(status.first) .subscribe() @@ -217,10 +204,6 @@ class SearchViewModel @Inject constructor( return timelineCases.delete(id) } - fun retryAllSearches() { - search(currentQuery) - } - fun muteConversation(status: Pair, mute: Boolean) { val idx = loadedStatuses.indexOf(status) if (idx >= 0) { @@ -230,7 +213,7 @@ class SearchViewModel @Inject constructor( status.second.copy(status = newStatus) ) loadedStatuses[idx] = newPair - repoResultStatus.value?.refresh?.invoke() + statusesPagingSourceFactory.invalidate() } timelineCases.muteConversation(status.first.id, mute) .onErrorReturnItem(status.first) @@ -240,5 +223,6 @@ class SearchViewModel @Inject constructor( companion object { private const val TAG = "SearchViewModel" + private const val DEFAULT_LOAD_SIZE = 20 } } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchAccountsAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchAccountsAdapter.kt index b6bc95681..7056d5e29 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchAccountsAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchAccountsAdapter.kt @@ -1,4 +1,4 @@ -/* Copyright 2019 Joel Pyska +/* Copyright 2021 Tusky Contributors * * This file is a part of Tusky. * @@ -17,26 +17,25 @@ package com.keylesspalace.tusky.components.search.adapter import android.view.LayoutInflater import android.view.ViewGroup -import androidx.paging.PagedListAdapter +import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.RecyclerView import com.keylesspalace.tusky.R import com.keylesspalace.tusky.adapter.AccountViewHolder import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.interfaces.LinkListener class SearchAccountsAdapter(private val linkListener: LinkListener, private val animateAvatars: Boolean, private val animateEmojis: Boolean) - : PagedListAdapter(ACCOUNT_COMPARATOR) { + : PagingDataAdapter(ACCOUNT_COMPARATOR) { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AccountViewHolder { val view = LayoutInflater.from(parent.context) .inflate(R.layout.item_account, parent, false) return AccountViewHolder(view) } - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + override fun onBindViewHolder(holder: AccountViewHolder, position: Int) { getItem(position)?.let { item -> - (holder as AccountViewHolder).apply { + holder.apply { setupWithAccount(item, animateAvatars, animateEmojis) setupLinkListener(linkListener) } @@ -52,7 +51,5 @@ class SearchAccountsAdapter(private val linkListener: LinkListener, private val override fun areItemsTheSame(oldItem: Account, newItem: Account): Boolean = oldItem.id == newItem.id } - } - -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchDataSource.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchDataSource.kt deleted file mode 100644 index a1ed9454a..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchDataSource.kt +++ /dev/null @@ -1,126 +0,0 @@ -/* Copyright 2019 Joel Pyska - * - * This file is a part of Tusky. - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky; if not, - * see . */ - -package com.keylesspalace.tusky.components.search.adapter - -import androidx.lifecycle.MutableLiveData -import androidx.paging.PositionalDataSource -import com.keylesspalace.tusky.components.search.SearchType -import com.keylesspalace.tusky.entity.SearchResult -import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.util.NetworkState -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.addTo -import java.util.concurrent.Executor - -class SearchDataSource( - private val mastodonApi: MastodonApi, - private val searchType: SearchType, - private val searchRequest: String, - private val disposables: CompositeDisposable, - private val retryExecutor: Executor, - private val initialItems: List? = null, - private val parser: (SearchResult?) -> List, - private val source: SearchDataSourceFactory) : PositionalDataSource() { - - val networkState = MutableLiveData() - - private var retry: (() -> Any)? = null - - val initialLoad = MutableLiveData() - - fun retry() { - retry?.let { - retryExecutor.execute { - it.invoke() - } - } - } - - override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback) { - if (!initialItems.isNullOrEmpty()) { - callback.onResult(initialItems.toList(), 0) - } else { - networkState.postValue(NetworkState.LOADED) - retry = null - initialLoad.postValue(NetworkState.LOADING) - mastodonApi.searchObservable( - query = searchRequest, - type = searchType.apiParameter, - resolve = true, - limit = params.requestedLoadSize, - offset = 0, - following = false) - .subscribe( - { data -> - val res = parser(data) - callback.onResult(res, params.requestedStartPosition) - initialLoad.postValue(NetworkState.LOADED) - - }, - { error -> - retry = { - loadInitial(params, callback) - } - initialLoad.postValue(NetworkState.error(error.message)) - } - ).addTo(disposables) - } - - } - - override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback) { - networkState.postValue(NetworkState.LOADING) - retry = null - if (source.exhausted) { - return callback.onResult(emptyList()) - } - mastodonApi.searchObservable( - query = searchRequest, - type = searchType.apiParameter, - resolve = true, - limit = params.loadSize, - offset = params.startPosition, - following = false) - .subscribe( - { data -> - // Working around Mastodon bug where exact match is returned no matter - // which offset is requested (so if we search for a full username, it's - // infinite) - // see https://github.com/tootsuite/mastodon/issues/11365 - // see https://github.com/tootsuite/mastodon/issues/13083 - val res = if ((data.accounts.size == 1 && data.accounts[0].username.equals(searchRequest, ignoreCase = true)) - || (data.statuses.size == 1 && data.statuses[0].url.equals(searchRequest))) { - listOf() - } else { - parser(data) - } - if (res.isEmpty()) { - source.exhausted = true - } - callback.onResult(res) - networkState.postValue(NetworkState.LOADED) - }, - { error -> - retry = { - loadRange(params, callback) - } - networkState.postValue(NetworkState.error(error.message)) - } - ).addTo(disposables) - - - } -} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchHashtagsAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchHashtagsAdapter.kt index ebc021602..cf7e7c7c5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchHashtagsAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchHashtagsAdapter.kt @@ -1,4 +1,4 @@ -/* Copyright 2019 Joel Pyska +/* Copyright 2021 Tusky Contributors * * This file is a part of Tusky. * @@ -17,7 +17,7 @@ package com.keylesspalace.tusky.components.search.adapter import android.view.LayoutInflater import android.view.ViewGroup -import androidx.paging.PagedListAdapter +import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.DiffUtil import com.keylesspalace.tusky.databinding.ItemHashtagBinding import com.keylesspalace.tusky.entity.HashTag @@ -25,7 +25,7 @@ import com.keylesspalace.tusky.interfaces.LinkListener import com.keylesspalace.tusky.util.BindingHolder class SearchHashtagsAdapter(private val linkListener: LinkListener) - : PagedListAdapter>(HASHTAG_COMPARATOR) { + : PagingDataAdapter>(HASHTAG_COMPARATOR) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder { val binding = ItemHashtagBinding.inflate(LayoutInflater.from(parent.context), parent, false) @@ -48,7 +48,5 @@ class SearchHashtagsAdapter(private val linkListener: LinkListener) override fun areItemsTheSame(oldItem: HashTag, newItem: HashTag): Boolean = oldItem.name == newItem.name } - } - -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchPagingSource.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchPagingSource.kt new file mode 100644 index 000000000..315edba69 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchPagingSource.kt @@ -0,0 +1,83 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.search.adapter + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import com.keylesspalace.tusky.components.search.SearchType +import com.keylesspalace.tusky.entity.SearchResult +import com.keylesspalace.tusky.network.MastodonApi +import kotlinx.coroutines.rx3.await + +class SearchPagingSource( + private val mastodonApi: MastodonApi, + private val searchType: SearchType, + private val searchRequest: String, + private val initialItems: List?, + private val parser: (SearchResult) -> List) : PagingSource() { + + override fun getRefreshKey(state: PagingState): Int? { + return null + } + + override suspend fun load(params: LoadParams): LoadResult { + if (searchRequest.isEmpty()) { + return LoadResult.Page( + data = emptyList(), + prevKey = null, + nextKey = null + ) + } + + if (params.key == null && !initialItems.isNullOrEmpty()) { + return LoadResult.Page( + data = initialItems.toList(), + prevKey = null, + nextKey = initialItems.size + ) + } + + val currentKey = params.key ?: 0 + + try { + + val data = mastodonApi.searchObservable( + query = searchRequest, + type = searchType.apiParameter, + resolve = true, + limit = params.loadSize, + offset = currentKey, + following = false + ).await() + + val res = parser(data) + + val nextKey = if (res.isEmpty()) { + null + } else { + currentKey + res.size + } + + return LoadResult.Page( + data = res, + prevKey = null, + nextKey = nextKey + ) + } catch (e: Exception) { + return LoadResult.Error(e) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchDataSourceFactory.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchPagingSourceFactory.kt similarity index 50% rename from app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchDataSourceFactory.kt rename to app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchPagingSourceFactory.kt index b19976706..fb3760ca4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchDataSourceFactory.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchPagingSourceFactory.kt @@ -1,4 +1,4 @@ -/* Copyright 2019 Joel Pyska +/* Copyright 2021 Tusky Contributors * * This file is a part of Tusky. * @@ -15,30 +15,39 @@ package com.keylesspalace.tusky.components.search.adapter -import androidx.lifecycle.MutableLiveData -import androidx.paging.DataSource import com.keylesspalace.tusky.components.search.SearchType import com.keylesspalace.tusky.entity.SearchResult import com.keylesspalace.tusky.network.MastodonApi -import io.reactivex.rxjava3.disposables.CompositeDisposable -import java.util.concurrent.Executor -class SearchDataSourceFactory( - private val mastodonApi: MastodonApi, - private val searchType: SearchType, - private val searchRequest: String, - private val disposables: CompositeDisposable, - private val retryExecutor: Executor, - private val cacheData: List? = null, - private val parser: (SearchResult?) -> List) : DataSource.Factory() { +class SearchPagingSourceFactory( + private val mastodonApi: MastodonApi, + private val searchType: SearchType, + private val initialItems: List? = null, + private val parser: (SearchResult) -> List +) : () -> SearchPagingSource { - val sourceLiveData = MutableLiveData>() + private var searchRequest: String = "" - var exhausted = false + private var currentSource: SearchPagingSource? = null - override fun create(): DataSource { - val source = SearchDataSource(mastodonApi, searchType, searchRequest, disposables, retryExecutor, cacheData, parser, this) - sourceLiveData.postValue(source) - return source + override fun invoke(): SearchPagingSource { + return SearchPagingSource( + mastodonApi = mastodonApi, + searchType = searchType, + searchRequest = searchRequest, + initialItems = initialItems, + parser = parser + ).also { source -> + currentSource = source + } + } + + fun newSearch(newSearchRequest: String) { + this.searchRequest = newSearchRequest + currentSource?.invalidate() + } + + fun invalidate() { + currentSource?.invalidate() } } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchRepository.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchRepository.kt deleted file mode 100644 index 4425542e6..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchRepository.kt +++ /dev/null @@ -1,56 +0,0 @@ -/* Copyright 2019 Joel Pyska - * - * This file is a part of Tusky. - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky; if not, - * see . */ - -package com.keylesspalace.tusky.components.search.adapter - -import androidx.lifecycle.Transformations -import androidx.paging.Config -import androidx.paging.toLiveData -import com.keylesspalace.tusky.components.search.SearchType -import com.keylesspalace.tusky.entity.SearchResult -import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.util.Listing -import io.reactivex.rxjava3.disposables.CompositeDisposable -import java.util.concurrent.Executors - -class SearchRepository(private val mastodonApi: MastodonApi) { - - private val executor = Executors.newSingleThreadExecutor() - - fun getSearchData(searchType: SearchType, searchRequest: String, disposables: CompositeDisposable, pageSize: Int = 20, - initialItems: List? = null, parser: (SearchResult?) -> List): Listing { - val sourceFactory = SearchDataSourceFactory(mastodonApi, searchType, searchRequest, disposables, executor, initialItems, parser) - val livePagedList = sourceFactory.toLiveData( - config = Config(pageSize = pageSize, enablePlaceholders = false, initialLoadSizeHint = pageSize * 2), - fetchExecutor = executor - ) - return Listing( - pagedList = livePagedList, - networkState = Transformations.switchMap(sourceFactory.sourceLiveData) { - it.networkState - }, - retry = { - sourceFactory.sourceLiveData.value?.retry() - }, - refresh = { - sourceFactory.sourceLiveData.value?.invalidate() - }, - refreshState = Transformations.switchMap(sourceFactory.sourceLiveData) { - it.initialLoad - } - - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchStatusesAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchStatusesAdapter.kt index a40414f93..d1ad35864 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchStatusesAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchStatusesAdapter.kt @@ -1,4 +1,4 @@ -/* Copyright 2019 Joel Pyska +/* Copyright 2021 Tusky Contributors * * This file is a part of Tusky. * @@ -17,9 +17,8 @@ package com.keylesspalace.tusky.components.search.adapter import android.view.LayoutInflater import android.view.ViewGroup -import androidx.paging.PagedListAdapter +import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.RecyclerView import com.keylesspalace.tusky.R import com.keylesspalace.tusky.adapter.StatusViewHolder import com.keylesspalace.tusky.entity.Status @@ -28,36 +27,34 @@ import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.viewdata.StatusViewData class SearchStatusesAdapter( - private val statusDisplayOptions: StatusDisplayOptions, - private val statusListener: StatusActionListener -) : PagedListAdapter, RecyclerView.ViewHolder>(STATUS_COMPARATOR) { + private val statusDisplayOptions: StatusDisplayOptions, + private val statusListener: StatusActionListener +) : PagingDataAdapter, StatusViewHolder>(STATUS_COMPARATOR) { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StatusViewHolder { val view = LayoutInflater.from(parent.context) - .inflate(R.layout.item_status, parent, false) + .inflate(R.layout.item_status, parent, false) return StatusViewHolder(view) } - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + override fun onBindViewHolder(holder: StatusViewHolder, position: Int) { getItem(position)?.let { item -> - (holder as StatusViewHolder).setupWithStatus(item.second, statusListener, statusDisplayOptions) + holder.setupWithStatus(item.second, statusListener, statusDisplayOptions) } } - public override fun getItem(position: Int): Pair? { - return super.getItem(position) + fun item(position: Int): Pair? { + return getItem(position) } companion object { val STATUS_COMPARATOR = object : DiffUtil.ItemCallback>() { override fun areContentsTheSame(oldItem: Pair, newItem: Pair): Boolean = - oldItem.second == newItem.second + oldItem == newItem override fun areItemsTheSame(oldItem: Pair, newItem: Pair): Boolean = - oldItem.second.id == newItem.second.id + oldItem.second.id == newItem.second.id } - } - -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchAccountsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchAccountsFragment.kt index 8715e1ab2..8a0d54162 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchAccountsFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchAccountsFragment.kt @@ -1,4 +1,4 @@ -/* Copyright 2019 Joel Pyska +/* Copyright 2021 Tusky Contributors * * This file is a part of Tusky. * @@ -15,17 +15,16 @@ package com.keylesspalace.tusky.components.search.fragments -import androidx.lifecycle.LiveData -import androidx.paging.PagedList -import androidx.paging.PagedListAdapter +import androidx.paging.PagingData +import androidx.paging.PagingDataAdapter import androidx.preference.PreferenceManager import com.keylesspalace.tusky.components.search.adapter.SearchAccountsAdapter import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.settings.PrefKeys -import com.keylesspalace.tusky.util.NetworkState +import kotlinx.coroutines.flow.Flow class SearchAccountsFragment : SearchFragment() { - override fun createAdapter(): PagedListAdapter { + override fun createAdapter(): PagingDataAdapter { val preferences = PreferenceManager.getDefaultSharedPreferences(binding.searchRecyclerView.context) return SearchAccountsAdapter( @@ -35,12 +34,8 @@ class SearchAccountsFragment : SearchFragment() { ) } - override val networkStateRefresh: LiveData - get() = viewModel.networkStateAccountRefresh - override val networkState: LiveData - get() = viewModel.networkStateAccount - override val data: LiveData> - get() = viewModel.accounts + override val data: Flow> + get() = viewModel.accountsFlow companion object { fun newInstance() = SearchAccountsFragment() diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt index 32475c78c..e18cd5cb1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt @@ -4,9 +4,10 @@ import android.os.Bundle import android.view.View import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels -import androidx.lifecycle.LiveData -import androidx.paging.PagedList -import androidx.paging.PagedListAdapter +import androidx.lifecycle.lifecycleScope +import androidx.paging.LoadState +import androidx.paging.PagingData +import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.SimpleItemAnimator @@ -21,10 +22,14 @@ import com.keylesspalace.tusky.databinding.FragmentSearchBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.interfaces.LinkListener -import com.keylesspalace.tusky.util.* +import com.keylesspalace.tusky.util.viewBinding +import com.keylesspalace.tusky.util.visible +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch import javax.inject.Inject -abstract class SearchFragment : Fragment(R.layout.fragment_search), +abstract class SearchFragment : Fragment(R.layout.fragment_search), LinkListener, Injectable, SwipeRefreshLayout.OnRefreshListener { @Inject @@ -36,12 +41,12 @@ abstract class SearchFragment : Fragment(R.layout.fragment_search), private var snackbarErrorRetry: Snackbar? = null - abstract fun createAdapter(): PagedListAdapter + abstract fun createAdapter(): PagingDataAdapter - abstract val networkStateRefresh: LiveData - abstract val networkState: LiveData - abstract val data: LiveData> - protected lateinit var adapter: PagedListAdapter + abstract val data: Flow> + protected lateinit var adapter: PagingDataAdapter + + private var currentQuery: String = "" override fun onViewCreated(view: View, savedInstanceState: Bundle?) { initAdapter() @@ -55,32 +60,32 @@ abstract class SearchFragment : Fragment(R.layout.fragment_search), } private fun subscribeObservables() { - data.observe(viewLifecycleOwner) { - adapter.submitList(it) - } - - networkStateRefresh.observe(viewLifecycleOwner) { - - binding.searchProgressBar.visible(it == NetworkState.LOADING) - - if (it.status == Status.FAILED) { - showError() - } - checkNoData() - } - - networkState.observe(viewLifecycleOwner) { - - binding.progressBarBottom.visible(it == NetworkState.LOADING) - - if (it.status == Status.FAILED) { - showError() + viewLifecycleOwner.lifecycleScope.launch { + data.collectLatest { pagingData -> + adapter.submitData(pagingData) } } - } - private fun checkNoData() { - showNoData(adapter.itemCount == 0) + adapter.addLoadStateListener { loadState -> + + if (loadState.refresh is LoadState.Error) { + showError() + } + + val isNewSearch = currentQuery != viewModel.currentQuery + + binding.searchProgressBar.visible(loadState.refresh == LoadState.Loading && isNewSearch && !binding.swipeRefreshLayout.isRefreshing) + binding.searchRecyclerView.visible(loadState.refresh is LoadState.NotLoading || !isNewSearch || binding.swipeRefreshLayout.isRefreshing) + + if (loadState.refresh != LoadState.Loading) { + binding.swipeRefreshLayout.isRefreshing = false + currentQuery = viewModel.currentQuery + } + + binding.progressBarBottom.visible(loadState.append == LoadState.Loading) + + binding.searchNoResultsText.visible(loadState.refresh is LoadState.NotLoading && adapter.itemCount == 0 && viewModel.currentQuery.isNotEmpty()) + } } private fun initAdapter() { @@ -92,20 +97,12 @@ abstract class SearchFragment : Fragment(R.layout.fragment_search), (binding.searchRecyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false } - private fun showNoData(isEmpty: Boolean) { - if (isEmpty && networkStateRefresh.value == NetworkState.LOADED) { - binding.searchNoResultsText.show() - } else { - binding.searchNoResultsText.hide() - } - } - private fun showError() { if (snackbarErrorRetry?.isShown != true) { snackbarErrorRetry = Snackbar.make(binding.root, R.string.failed_search, Snackbar.LENGTH_INDEFINITE) snackbarErrorRetry?.setAction(R.string.action_retry) { snackbarErrorRetry = null - viewModel.retryAllSearches() + adapter.retry() } snackbarErrorRetry?.show() } @@ -123,11 +120,6 @@ abstract class SearchFragment : Fragment(R.layout.fragment_search), get() = (activity as? BottomSheetActivity) override fun onRefresh() { - - // Dismissed here because the RecyclerView bottomProgressBar is shown as soon as the retry begins. - binding.swipeRefreshLayout.post { - binding.swipeRefreshLayout.isRefreshing = false - } - viewModel.retryAllSearches() + adapter.refresh() } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchHashtagsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchHashtagsFragment.kt index 15310d3c1..d0b7e8fa9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchHashtagsFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchHashtagsFragment.kt @@ -1,4 +1,4 @@ -/* Copyright 2019 Joel Pyska +/* Copyright 2021 Tusky Contributors * * This file is a part of Tusky. * @@ -15,22 +15,18 @@ package com.keylesspalace.tusky.components.search.fragments -import androidx.lifecycle.LiveData -import androidx.paging.PagedList -import androidx.paging.PagedListAdapter +import androidx.paging.PagingData +import androidx.paging.PagingDataAdapter import com.keylesspalace.tusky.components.search.adapter.SearchHashtagsAdapter import com.keylesspalace.tusky.entity.HashTag -import com.keylesspalace.tusky.util.NetworkState +import kotlinx.coroutines.flow.Flow class SearchHashtagsFragment : SearchFragment() { - override val networkStateRefresh: LiveData - get() = viewModel.networkStateHashTagRefresh - override val networkState: LiveData - get() = viewModel.networkStateHashTag - override val data: LiveData> - get() = viewModel.hashtags - override fun createAdapter(): PagedListAdapter = SearchHashtagsAdapter(this) + override val data: Flow> + get() = viewModel.hashtagsFlow + + override fun createAdapter(): PagingDataAdapter = SearchHashtagsAdapter(this) companion object { fun newInstance() = SearchHashtagsFragment() diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt index 06013ce42..c6fe2c4e0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt @@ -1,4 +1,4 @@ -/* Copyright 2019 Joel Pyska +/* Copyright 2021 Tusky Contributors * * This file is a part of Tusky. * @@ -32,9 +32,8 @@ import androidx.appcompat.widget.PopupMenu import androidx.core.app.ActivityOptionsCompat import androidx.core.view.ViewCompat import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LiveData -import androidx.paging.PagedList -import androidx.paging.PagedListAdapter +import androidx.paging.PagingData +import androidx.paging.PagingDataAdapter import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager @@ -57,26 +56,22 @@ import com.keylesspalace.tusky.interfaces.StatusActionListener import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.util.CardViewMode import com.keylesspalace.tusky.util.LinkHelper -import com.keylesspalace.tusky.util.NetworkState import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.view.showMuteAccountDialog import com.keylesspalace.tusky.viewdata.AttachmentViewData import com.keylesspalace.tusky.viewdata.StatusViewData import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import kotlinx.coroutines.flow.Flow class SearchStatusesFragment : SearchFragment>(), StatusActionListener { - override val networkStateRefresh: LiveData - get() = viewModel.networkStateStatusRefresh - override val networkState: LiveData - get() = viewModel.networkStateStatus - override val data: LiveData>> - get() = viewModel.statuses + override val data: Flow>> + get() = viewModel.statusesFlow private val searchAdapter get() = super.adapter as SearchStatusesAdapter - override fun createAdapter(): PagedListAdapter, *> { + override fun createAdapter(): PagingDataAdapter, *> { val preferences = PreferenceManager.getDefaultSharedPreferences(binding.searchRecyclerView.context) val statusDisplayOptions = StatusDisplayOptions( animateAvatars = preferences.getBoolean("animateGifAvatars", false), @@ -96,37 +91,37 @@ class SearchStatusesFragment : SearchFragment + searchAdapter.item(position)?.first?.let { status -> reply(status) } } override fun onFavourite(favourite: Boolean, position: Int) { - searchAdapter.getItem(position)?.let { status -> + searchAdapter.item(position)?.let { status -> viewModel.favorite(status, favourite) } } override fun onBookmark(bookmark: Boolean, position: Int) { - searchAdapter.getItem(position)?.let { status -> + searchAdapter.item(position)?.let { status -> viewModel.bookmark(status, bookmark) } } override fun onMore(view: View, position: Int) { - searchAdapter.getItem(position)?.first?.let { + searchAdapter.item(position)?.first?.let { more(it, view, position) } } override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) { - searchAdapter.getItem(position)?.first?.actionableStatus?.let { actionable -> + searchAdapter.item(position)?.first?.actionableStatus?.let { actionable -> when (actionable.attachments[attachmentIndex].type) { Attachment.Type.GIFV, Attachment.Type.VIDEO, Attachment.Type.IMAGE, Attachment.Type.AUDIO -> { val attachments = AttachmentViewData.list(actionable) @@ -146,26 +141,24 @@ class SearchStatusesFragment : SearchFragment + searchAdapter.item(position)?.first?.let { status -> val actionableStatus = status.actionableStatus bottomSheetActivity?.viewThread(actionableStatus.id, actionableStatus.url) } } override fun onOpenReblog(position: Int) { - searchAdapter.getItem(position)?.first?.let { status -> + searchAdapter.item(position)?.first?.let { status -> bottomSheetActivity?.viewAccount(status.account.id) } } override fun onExpandedChange(expanded: Boolean, position: Int) { - searchAdapter.getItem(position)?.let { + searchAdapter.item(position)?.let { viewModel.expandedChange(it, expanded) } } @@ -175,25 +168,25 @@ class SearchStatusesFragment : SearchFragment) { - searchAdapter.getItem(position)?.let { + searchAdapter.item(position)?.let { viewModel.voteInPoll(it, choices) } } private fun removeItem(position: Int) { - searchAdapter.getItem(position)?.let { + searchAdapter.item(position)?.let { viewModel.removeItem(it) } } override fun onReblog(reblog: Boolean, position: Int) { - searchAdapter.getItem(position)?.let { status -> + searchAdapter.item(position)?.let { status -> viewModel.reblog(status, reblog) } } @@ -323,7 +316,7 @@ class SearchStatusesFragment : SearchFragment { - searchAdapter.getItem(position)?.let { foundStatus -> + searchAdapter.item(position)?.let { foundStatus -> viewModel.muteConversation(foundStatus, status.muted != true) } return@setOnMenuItemClickListener true diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java index 1a950feec..624c15ac1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java +++ b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java @@ -32,7 +32,7 @@ import java.io.File; @Database(entities = { DraftEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class, TimelineAccountEntity.class, ConversationEntity.class - }, version = 26) + }, version = 27) public abstract class AppDatabase extends RoomDatabase { public abstract AccountDao accountDao(); @@ -393,4 +393,11 @@ public abstract class AppDatabase extends RoomDatabase { } } } + + public static final Migration MIGRATION_26_27 = new Migration(26, 27) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `ConversationEntity` ADD COLUMN `s_muted` INTEGER NOT NULL DEFAULT 0"); + } + }; } diff --git a/app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt index aae30826f..2d54e6746 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt @@ -15,27 +15,29 @@ package com.keylesspalace.tusky.db -import androidx.paging.DataSource -import androidx.room.* +import androidx.paging.PagingSource +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query import com.keylesspalace.tusky.components.conversation.ConversationEntity -import io.reactivex.rxjava3.core.Single @Dao interface ConversationsDao { @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insert(conversations: List) + suspend fun insert(conversations: List) @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insert(conversation: ConversationEntity): Single + suspend fun insert(conversation: ConversationEntity): Long @Delete - fun delete(conversation: ConversationEntity): Single + suspend fun delete(conversation: ConversationEntity): Int @Query("SELECT * FROM ConversationEntity WHERE accountId = :accountId ORDER BY s_createdAt DESC") - fun conversationsForAccount(accountId: Long) : DataSource.Factory + fun conversationsForAccount(accountId: Long) : PagingSource @Query("DELETE FROM ConversationEntity WHERE accountId = :accountId") fun deleteForAccount(accountId: Long) - } diff --git a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt index 2a699ee33..faf0a3863 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt @@ -83,6 +83,7 @@ class AppModule { AppDatabase.MIGRATION_16_17, AppDatabase.MIGRATION_17_18, AppDatabase.MIGRATION_18_19, AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21, AppDatabase.MIGRATION_21_22, AppDatabase.MIGRATION_22_23, AppDatabase.MIGRATION_23_24, AppDatabase.MIGRATION_24_25, + AppDatabase.MIGRATION_26_27, AppDatabase.Migration25_26(appContext.getExternalFilesDir("Tusky")) ) .build() diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt index dfa681f57..6aadc9b76 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -15,7 +15,27 @@ package com.keylesspalace.tusky.network -import com.keylesspalace.tusky.entity.* +import com.keylesspalace.tusky.entity.AccessToken +import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.entity.Announcement +import com.keylesspalace.tusky.entity.AppCredentials +import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.entity.Conversation +import com.keylesspalace.tusky.entity.DeletedStatus +import com.keylesspalace.tusky.entity.Emoji +import com.keylesspalace.tusky.entity.Filter +import com.keylesspalace.tusky.entity.IdentityProof +import com.keylesspalace.tusky.entity.Instance +import com.keylesspalace.tusky.entity.Marker +import com.keylesspalace.tusky.entity.MastoList +import com.keylesspalace.tusky.entity.NewStatus +import com.keylesspalace.tusky.entity.Notification +import com.keylesspalace.tusky.entity.Poll +import com.keylesspalace.tusky.entity.Relationship +import com.keylesspalace.tusky.entity.ScheduledStatus +import com.keylesspalace.tusky.entity.SearchResult +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.entity.StatusContext import io.reactivex.rxjava3.core.Completable import io.reactivex.rxjava3.core.Single import okhttp3.MultipartBody @@ -23,8 +43,20 @@ import okhttp3.RequestBody import okhttp3.ResponseBody import retrofit2.Call import retrofit2.Response -import retrofit2.http.* +import retrofit2.http.Body +import retrofit2.http.DELETE import retrofit2.http.Field +import retrofit2.http.FormUrlEncoded +import retrofit2.http.GET +import retrofit2.http.HTTP +import retrofit2.http.Header +import retrofit2.http.Multipart +import retrofit2.http.PATCH +import retrofit2.http.POST +import retrofit2.http.PUT +import retrofit2.http.Part +import retrofit2.http.Path +import retrofit2.http.Query /** * for documentation of the Mastodon REST API see https://docs.joinmastodon.org/api/ @@ -466,10 +498,15 @@ interface MastodonApi { ): Completable @GET("/api/v1/conversations") - fun getConversations( + suspend fun getConversations( @Query("max_id") maxId: String? = null, @Query("limit") limit: Int - ): Call> + ): List + + @DELETE("/api/v1/conversations/{id}") + suspend fun deleteConversation( + @Path("id") conversationId: String + ) @FormUrlEncoded @POST("api/v1/filters") diff --git a/app/src/main/java/com/keylesspalace/tusky/util/BiListing.kt b/app/src/main/java/com/keylesspalace/tusky/util/BiListing.kt index dad6d552d..268631cb7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/BiListing.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/BiListing.kt @@ -22,7 +22,7 @@ import androidx.paging.PagedList /** * Data class that is necessary for a UI to show a listing and interact w/ the rest of the system */ -data class BiListing( +data class BiListing( // the LiveData of paged lists for the UI to observe val pagedList: LiveData>, // represents the network request status for load data before first to show to the user diff --git a/app/src/main/java/com/keylesspalace/tusky/util/Listing.kt b/app/src/main/java/com/keylesspalace/tusky/util/Listing.kt deleted file mode 100644 index 3d4234c59..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/util/Listing.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.keylesspalace.tusky.util - -import androidx.lifecycle.LiveData -import androidx.paging.PagedList - -/** - * Data class that is necessary for a UI to show a listing and interact w/ the rest of the system - */ -data class Listing( - // the LiveData of paged lists for the UI to observe - val pagedList: LiveData>, - // represents the network request status to show to the user - val networkState: LiveData, - // represents the refresh status to show to the user. Separate from networkState, this - // value is importantly only when refresh is requested. - val refreshState: LiveData, - // refreshes the whole data and fetches it from scratch. - val refresh: () -> Unit, - // retries any failed requests. - val retry: () -> Unit) \ No newline at end of file diff --git a/app/src/main/res/menu/conversation_more.xml b/app/src/main/res/menu/conversation_more.xml new file mode 100644 index 000000000..2f5dedd93 --- /dev/null +++ b/app/src/main/res/menu/conversation_more.xml @@ -0,0 +1,13 @@ + +

+ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6dd78e760..d9fefae27 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -88,6 +88,7 @@ Report Edit Delete + Delete conversation Delete and re-draft TOOT TOOT! @@ -200,6 +201,7 @@ Unfollow this account? Delete this toot? Delete and re-draft this toot? + Delete this conversation? Are you sure you want to block all of %s? You will not see content from that domain in any public timelines or in your notifications. Your followers from that domain will be removed. Hide entire domain Block @%s? From 837ee2e40d44b83baa007bd70a0b45e6828905c0 Mon Sep 17 00:00:00 2001 From: Ivan Kupalov Date: Sun, 20 Jun 2021 10:18:40 +0200 Subject: [PATCH 77/92] Convert some adapters to Kotlin (#2187) * Rename .java adapters to .kt * Convert Account adapters to Kotlin * Apply feedback for adapter refactoring --- .../tusky/adapter/AccountAdapter.java | 116 ------------- .../tusky/adapter/AccountAdapter.kt | 125 +++++++++++++ .../tusky/adapter/BlocksAdapter.java | 106 ----------- .../tusky/adapter/BlocksAdapter.kt | 80 +++++++++ .../tusky/adapter/FollowAdapter.java | 61 ------- .../tusky/adapter/FollowAdapter.kt | 39 +++++ .../tusky/adapter/FollowRequestsAdapter.java | 60 ------- .../tusky/adapter/FollowRequestsAdapter.kt | 41 +++++ .../tusky/adapter/MutesAdapter.java | 131 -------------- .../tusky/adapter/MutesAdapter.kt | 132 ++++++++++++++ .../tusky/adapter/NotificationsAdapter.java | 2 +- .../tusky/adapter/PlaceholderViewHolder.java | 49 ------ .../tusky/adapter/PlaceholderViewHolder.kt | 41 +++++ .../tusky/adapter/ThreadAdapter.java | 164 ------------------ .../tusky/adapter/ThreadAdapter.kt | 129 ++++++++++++++ .../tusky/fragment/AccountListFragment.kt | 2 +- 16 files changed, 589 insertions(+), 689 deletions(-) delete mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/AccountAdapter.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/AccountAdapter.kt delete mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.kt delete mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/FollowAdapter.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/FollowAdapter.kt delete mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.kt delete mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.kt delete mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/PlaceholderViewHolder.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/PlaceholderViewHolder.kt delete mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.kt diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountAdapter.java deleted file mode 100644 index 24430dcec..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountAdapter.java +++ /dev/null @@ -1,116 +0,0 @@ -/* Copyright 2017 Andrew Dawson - * - * This file is a part of Tusky. - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky; if not, - * see . */ - -package com.keylesspalace.tusky.adapter; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.recyclerview.widget.RecyclerView; - -import com.keylesspalace.tusky.entity.Account; -import com.keylesspalace.tusky.interfaces.AccountActionListener; -import com.keylesspalace.tusky.util.ListUtils; - -import java.util.ArrayList; -import java.util.List; - -public abstract class AccountAdapter extends RecyclerView.Adapter { - static final int VIEW_TYPE_ACCOUNT = 0; - static final int VIEW_TYPE_FOOTER = 1; - - List accountList; - AccountActionListener accountActionListener; - private boolean bottomLoading; - protected final boolean animateEmojis; - protected final boolean animateAvatar; - - AccountAdapter(AccountActionListener accountActionListener, boolean animateAvatar, boolean animateEmojis) { - this.accountList = new ArrayList<>(); - this.accountActionListener = accountActionListener; - this.animateAvatar = animateAvatar; - this.animateEmojis = animateEmojis; - bottomLoading = false; - } - - @Override - public int getItemCount() { - return accountList.size() + (bottomLoading ? 1 : 0); - } - - @Override - public int getItemViewType(int position) { - if (position == accountList.size() && bottomLoading) { - return VIEW_TYPE_FOOTER; - } else { - return VIEW_TYPE_ACCOUNT; - } - } - - public void update(@NonNull List newAccounts) { - accountList = ListUtils.removeDuplicates(newAccounts); - notifyDataSetChanged(); - } - - public void addItems(@NonNull List newAccounts) { - int end = accountList.size(); - Account last = accountList.get(end - 1); - if (last != null && !findAccount(newAccounts, last.getId())) { - accountList.addAll(newAccounts); - notifyItemRangeInserted(end, newAccounts.size()); - } - } - - public void setBottomLoading(boolean loading) { - boolean wasLoading = bottomLoading; - if(wasLoading == loading) { - return; - } - bottomLoading = loading; - if(loading) { - notifyItemInserted(accountList.size()); - } else { - notifyItemRemoved(accountList.size()); - } - } - - private static boolean findAccount(@NonNull List accounts, String id) { - for (Account account : accounts) { - if (account.getId().equals(id)) { - return true; - } - } - return false; - } - - @Nullable - public Account removeItem(int position) { - if (position < 0 || position >= accountList.size()) { - return null; - } - Account account = accountList.remove(position); - notifyItemRemoved(position); - return account; - } - - public void addItem(@NonNull Account account, int position) { - if (position < 0 || position > accountList.size()) { - return; - } - accountList.add(position, account); - notifyItemInserted(position); - } - - -} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountAdapter.kt new file mode 100644 index 000000000..f68d022f1 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountAdapter.kt @@ -0,0 +1,125 @@ +/* Copyright 2021 Tusky Contributors. + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ +package com.keylesspalace.tusky.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.interfaces.AccountActionListener +import com.keylesspalace.tusky.util.removeDuplicates + +/** Generic adapter with bottom loading indicator. */ +abstract class AccountAdapter internal constructor( + var accountActionListener: AccountActionListener, + protected val animateAvatar: Boolean, + protected val animateEmojis: Boolean +) : RecyclerView.Adapter() { + var accountList = mutableListOf() + private var bottomLoading: Boolean = false + + override fun getItemCount(): Int { + return accountList.size + if (bottomLoading) 1 else 0 + } + + abstract fun createAccountViewHolder(parent: ViewGroup): AVH + + abstract fun onBindAccountViewHolder(viewHolder: AVH, position: Int) + + final override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) { + @Suppress("UNCHECKED_CAST") + this.onBindAccountViewHolder(holder as AVH, position) + } + } + + final override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): RecyclerView.ViewHolder { + return when (viewType) { + VIEW_TYPE_ACCOUNT -> this.createAccountViewHolder(parent) + VIEW_TYPE_FOOTER -> this.createFooterViewHolder(parent) + else -> error("Unknown item type: $viewType") + } + } + + private fun createFooterViewHolder( + parent: ViewGroup, + ): RecyclerView.ViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_footer, parent, false) + return LoadingFooterViewHolder(view) + } + + override fun getItemViewType(position: Int): Int { + return if (position == accountList.size && bottomLoading) { + VIEW_TYPE_FOOTER + } else { + VIEW_TYPE_ACCOUNT + } + } + + fun update(newAccounts: List) { + accountList = removeDuplicates(newAccounts) + notifyDataSetChanged() + } + + fun addItems(newAccounts: List) { + val end = accountList.size + val last = accountList[end - 1] + if (newAccounts.none { it.id == last.id }) { + accountList.addAll(newAccounts) + notifyItemRangeInserted(end, newAccounts.size) + } + } + + fun setBottomLoading(loading: Boolean) { + val wasLoading = bottomLoading + if (wasLoading == loading) { + return + } + bottomLoading = loading + if (loading) { + notifyItemInserted(accountList.size) + } else { + notifyItemRemoved(accountList.size) + } + } + + fun removeItem(position: Int): Account? { + if (position < 0 || position >= accountList.size) { + return null + } + val account = accountList.removeAt(position) + notifyItemRemoved(position) + return account + } + + fun addItem(account: Account, position: Int) { + if (position < 0 || position > accountList.size) { + return + } + accountList.add(position, account) + notifyItemInserted(position) + } + + companion object { + const val VIEW_TYPE_ACCOUNT = 0 + const val VIEW_TYPE_FOOTER = 1 + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.java deleted file mode 100644 index 57cc90359..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.java +++ /dev/null @@ -1,106 +0,0 @@ -/* Copyright 2017 Andrew Dawson - * - * This file is a part of Tusky. - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky; if not, - * see . */ - -package com.keylesspalace.tusky.adapter; - -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.preference.PreferenceManager; -import androidx.recyclerview.widget.RecyclerView; - -import com.keylesspalace.tusky.R; -import com.keylesspalace.tusky.entity.Account; -import com.keylesspalace.tusky.interfaces.AccountActionListener; -import com.keylesspalace.tusky.util.CustomEmojiHelper; -import com.keylesspalace.tusky.util.ImageLoadingHelper; - -public class BlocksAdapter extends AccountAdapter { - - public BlocksAdapter(AccountActionListener accountActionListener, boolean animateAvatar, boolean animateEmojis) { - super(accountActionListener, animateAvatar, animateEmojis); - } - - @NonNull - @Override - public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - switch (viewType) { - default: - case VIEW_TYPE_ACCOUNT: { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.item_blocked_user, parent, false); - return new BlockedUserViewHolder(view); - } - case VIEW_TYPE_FOOTER: { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.item_footer, parent, false); - return new LoadingFooterViewHolder(view); - } - } - } - - @Override - public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { - if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) { - BlockedUserViewHolder holder = (BlockedUserViewHolder) viewHolder; - holder.setupWithAccount(accountList.get(position), animateAvatar, animateEmojis); - holder.setupActionListener(accountActionListener); - } - } - - static class BlockedUserViewHolder extends RecyclerView.ViewHolder { - private ImageView avatar; - private TextView username; - private TextView displayName; - private ImageButton unblock; - private String id; - - BlockedUserViewHolder(View itemView) { - super(itemView); - avatar = itemView.findViewById(R.id.blocked_user_avatar); - username = itemView.findViewById(R.id.blocked_user_username); - displayName = itemView.findViewById(R.id.blocked_user_display_name); - unblock = itemView.findViewById(R.id.blocked_user_unblock); - - } - - void setupWithAccount(Account account, boolean animateAvatar, boolean animateEmojis) { - id = account.getId(); - CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName, animateEmojis); - displayName.setText(emojifiedName); - String format = username.getContext().getString(R.string.status_username_format); - String formattedUsername = String.format(format, account.getUsername()); - username.setText(formattedUsername); - int avatarRadius = avatar.getContext().getResources() - .getDimensionPixelSize(R.dimen.avatar_radius_48dp); - ImageLoadingHelper.loadAvatar(account.getAvatar(), avatar, avatarRadius, animateAvatar); - } - - void setupActionListener(final AccountActionListener listener) { - unblock.setOnClickListener(v -> { - int position = getBindingAdapterPosition(); - if (position != RecyclerView.NO_POSITION) { - listener.onBlock(false, id, position); - } - }); - itemView.setOnClickListener(v -> listener.onViewAccount(id)); - } - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.kt new file mode 100644 index 000000000..fc7cec24d --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.kt @@ -0,0 +1,80 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ +package com.keylesspalace.tusky.adapter + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageButton +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.interfaces.AccountActionListener +import com.keylesspalace.tusky.util.emojify +import com.keylesspalace.tusky.util.loadAvatar + +/** Displays a list of blocked accounts. */ +class BlocksAdapter( + accountActionListener: AccountActionListener, + animateAvatar: Boolean, + animateEmojis: Boolean +) : AccountAdapter( + accountActionListener, + animateAvatar, + animateEmojis +) { + override fun createAccountViewHolder(parent: ViewGroup): BlockedUserViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_blocked_user, parent, false) + return BlockedUserViewHolder(view) + } + + override fun onBindAccountViewHolder(viewHolder: BlockedUserViewHolder, position: Int) { + viewHolder.setupWithAccount(accountList[position], animateAvatar, animateEmojis) + viewHolder.setupActionListener(accountActionListener) + } + + class BlockedUserViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + private val avatar: ImageView = itemView.findViewById(R.id.blocked_user_avatar) + private val username: TextView = itemView.findViewById(R.id.blocked_user_username) + private val displayName: TextView = itemView.findViewById(R.id.blocked_user_display_name) + private val unblock: ImageButton = itemView.findViewById(R.id.blocked_user_unblock) + private var id: String? = null + + fun setupWithAccount(account: Account, animateAvatar: Boolean, animateEmojis: Boolean) { + id = account.id + val emojifiedName = account.name.emojify(account.emojis, displayName, animateEmojis) + displayName.text = emojifiedName + val format = username.context.getString(R.string.status_username_format) + val formattedUsername = String.format(format, account.username) + username.text = formattedUsername + val avatarRadius = avatar.context.resources + .getDimensionPixelSize(R.dimen.avatar_radius_48dp) + loadAvatar(account.avatar, avatar, avatarRadius, animateAvatar) + } + + fun setupActionListener(listener: AccountActionListener) { + unblock.setOnClickListener { + val position = bindingAdapterPosition + if (position != RecyclerView.NO_POSITION) { + listener.onBlock(false, id, position) + } + } + itemView.setOnClickListener { listener.onViewAccount(id) } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowAdapter.java deleted file mode 100644 index 98cb9e4df..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowAdapter.java +++ /dev/null @@ -1,61 +0,0 @@ -/* Copyright 2017 Andrew Dawson - * - * This file is a part of Tusky. - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky; if not, - * see . */ - -package com.keylesspalace.tusky.adapter; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import com.keylesspalace.tusky.R; -import com.keylesspalace.tusky.interfaces.AccountActionListener; - -/** Both for follows and following lists. */ -public class FollowAdapter extends AccountAdapter { - - public FollowAdapter(AccountActionListener accountActionListener, boolean animateAvatar, boolean animateEmojis) { - super(accountActionListener, animateAvatar, animateEmojis); - } - - @NonNull - @Override - public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - switch (viewType) { - default: - case VIEW_TYPE_ACCOUNT: { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.item_account, parent, false); - return new AccountViewHolder(view); - } - case VIEW_TYPE_FOOTER: { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.item_footer, parent, false); - return new LoadingFooterViewHolder(view); - } - } - } - - @Override - public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { - if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) { - AccountViewHolder holder = (AccountViewHolder) viewHolder; - holder.setupWithAccount(accountList.get(position), animateAvatar, animateEmojis); - holder.setupActionListener(accountActionListener); - } - } - -} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowAdapter.kt new file mode 100644 index 000000000..74797b3a7 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowAdapter.kt @@ -0,0 +1,39 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ +package com.keylesspalace.tusky.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.interfaces.AccountActionListener + +/** Displays either a follows or following list. */ +class FollowAdapter( + accountActionListener: AccountActionListener, + animateAvatar: Boolean, + animateEmojis: Boolean +) : AccountAdapter(accountActionListener, animateAvatar, animateEmojis) { + override fun createAccountViewHolder(parent: ViewGroup): AccountViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_account, parent, false) + return AccountViewHolder(view) + } + + override fun onBindAccountViewHolder(viewHolder: AccountViewHolder, position: Int) { + viewHolder.setupWithAccount(accountList[position], animateAvatar, animateEmojis) + viewHolder.setupActionListener(accountActionListener) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java deleted file mode 100644 index ef14618e6..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java +++ /dev/null @@ -1,60 +0,0 @@ -/* Copyright 2017 Andrew Dawson - * - * This file is a part of Tusky. - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky; if not, - * see . */ - -package com.keylesspalace.tusky.adapter; - -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - -import com.keylesspalace.tusky.R; -import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding; -import com.keylesspalace.tusky.interfaces.AccountActionListener; - -public class FollowRequestsAdapter extends AccountAdapter { - - public FollowRequestsAdapter(AccountActionListener accountActionListener, boolean animateAvatar, boolean animateEmojis) { - super(accountActionListener, animateAvatar, animateEmojis); - } - - @NonNull - @Override - public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - switch (viewType) { - default: - case VIEW_TYPE_ACCOUNT: { - ItemFollowRequestBinding binding = ItemFollowRequestBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); - return new FollowRequestViewHolder(binding, false); - } - case VIEW_TYPE_FOOTER: { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.item_footer, parent, false); - return new LoadingFooterViewHolder(view); - } - } - } - - @Override - public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { - if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) { - FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder; - holder.setupWithAccount(accountList.get(position), animateAvatar, animateEmojis); - holder.setupActionListener(accountActionListener, accountList.get(position).getId()); - } - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.kt new file mode 100644 index 000000000..11089cd42 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.kt @@ -0,0 +1,41 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ +package com.keylesspalace.tusky.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding +import com.keylesspalace.tusky.interfaces.AccountActionListener + +/** Displays a list of follow requests with accept/reject buttons. */ +class FollowRequestsAdapter( + accountActionListener: AccountActionListener, + animateAvatar: Boolean, + animateEmojis: Boolean +) : AccountAdapter(accountActionListener, animateAvatar, animateEmojis) { + override fun createAccountViewHolder(parent: ViewGroup): FollowRequestViewHolder { + val binding = ItemFollowRequestBinding.inflate( + LayoutInflater.from(parent.context), parent, false + ) + return FollowRequestViewHolder(binding, false) + } + + override fun onBindAccountViewHolder(viewHolder: FollowRequestViewHolder, position: Int) { + viewHolder.setupWithAccount(accountList[position], animateAvatar, animateEmojis) + viewHolder.setupActionListener(accountActionListener, accountList[position].id) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.java deleted file mode 100644 index f63af6ca6..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.java +++ /dev/null @@ -1,131 +0,0 @@ -package com.keylesspalace.tusky.adapter; - -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.core.view.ViewCompat; -import androidx.recyclerview.widget.RecyclerView; - -import com.keylesspalace.tusky.R; -import com.keylesspalace.tusky.entity.Account; -import com.keylesspalace.tusky.interfaces.AccountActionListener; -import com.keylesspalace.tusky.util.CustomEmojiHelper; -import com.keylesspalace.tusky.util.ImageLoadingHelper; - -import java.util.HashMap; - -public class MutesAdapter extends AccountAdapter { - private HashMap mutingNotificationsMap; - - public MutesAdapter(AccountActionListener accountActionListener, boolean animateAvatar, boolean animateEmojis) { - super(accountActionListener, animateAvatar, animateEmojis); - mutingNotificationsMap = new HashMap(); - } - - @NonNull - @Override - public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - switch (viewType) { - default: - case VIEW_TYPE_ACCOUNT: { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.item_muted_user, parent, false); - return new MutesAdapter.MutedUserViewHolder(view); - } - case VIEW_TYPE_FOOTER: { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.item_footer, parent, false); - return new LoadingFooterViewHolder(view); - } - } - } - - @Override - public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { - if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) { - MutedUserViewHolder holder = (MutedUserViewHolder) viewHolder; - Account account = accountList.get(position); - holder.setupWithAccount(account, mutingNotificationsMap.get(account.getId()), animateAvatar, animateEmojis); - holder.setupActionListener(accountActionListener); - } - } - - public void updateMutingNotifications(String id, boolean mutingNotifications, int position) { - mutingNotificationsMap.put(id, mutingNotifications); - notifyItemChanged(position); - } - - public void updateMutingNotificationsMap(HashMap newMutingNotificationsMap) { - mutingNotificationsMap.putAll(newMutingNotificationsMap); - notifyDataSetChanged(); - } - - static class MutedUserViewHolder extends RecyclerView.ViewHolder { - private ImageView avatar; - private TextView username; - private TextView displayName; - private ImageButton unmute; - private ImageButton muteNotifications; - private String id; - private boolean notifications; - - MutedUserViewHolder(View itemView) { - super(itemView); - avatar = itemView.findViewById(R.id.muted_user_avatar); - username = itemView.findViewById(R.id.muted_user_username); - displayName = itemView.findViewById(R.id.muted_user_display_name); - unmute = itemView.findViewById(R.id.muted_user_unmute); - muteNotifications = itemView.findViewById(R.id.muted_user_mute_notifications); - } - - void setupWithAccount(Account account, Boolean mutingNotifications, boolean animateAvatar, boolean animateEmojis) { - id = account.getId(); - CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName, animateEmojis); - displayName.setText(emojifiedName); - String format = username.getContext().getString(R.string.status_username_format); - String formattedUsername = String.format(format, account.getUsername()); - username.setText(formattedUsername); - int avatarRadius = avatar.getContext().getResources() - .getDimensionPixelSize(R.dimen.avatar_radius_48dp); - ImageLoadingHelper.loadAvatar(account.getAvatar(), avatar, avatarRadius, animateAvatar); - - String unmuteString = unmute.getContext().getString(R.string.action_unmute_desc, formattedUsername); - unmute.setContentDescription(unmuteString); - ViewCompat.setTooltipText(unmute, unmuteString); - - if (mutingNotifications == null) { - muteNotifications.setEnabled(false); - notifications = true; - } else { - muteNotifications.setEnabled(true); - notifications = mutingNotifications; - } - - if (notifications) { - muteNotifications.setImageResource(R.drawable.ic_notifications_24dp); - String unmuteNotificationsString = muteNotifications.getContext() - .getString(R.string.action_unmute_notifications_desc, formattedUsername); - muteNotifications.setContentDescription(unmuteNotificationsString); - ViewCompat.setTooltipText(muteNotifications, unmuteNotificationsString); - } else { - muteNotifications.setImageResource(R.drawable.ic_notifications_off_24dp); - String muteNotificationsString = muteNotifications.getContext() - .getString(R.string.action_mute_notifications_desc, formattedUsername); - muteNotifications.setContentDescription(muteNotificationsString); - ViewCompat.setTooltipText(muteNotifications, muteNotificationsString); - } - } - - void setupActionListener(final AccountActionListener listener) { - unmute.setOnClickListener(v -> listener.onMute(false, id, getBindingAdapterPosition(), false)); - muteNotifications.setOnClickListener( - v -> listener.onMute(true, id, getBindingAdapterPosition(), !notifications)); - itemView.setOnClickListener(v -> listener.onViewAccount(id)); - } - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.kt new file mode 100644 index 000000000..d20f783ce --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.kt @@ -0,0 +1,132 @@ +package com.keylesspalace.tusky.adapter + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageButton +import android.widget.ImageView +import android.widget.TextView +import androidx.core.view.ViewCompat +import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.interfaces.AccountActionListener +import com.keylesspalace.tusky.util.emojify +import com.keylesspalace.tusky.util.loadAvatar +import java.util.* + +/** + * Displays a list of muted accounts with mute/unmute account and mute/unmute notifications + * buttons. + * */ +class MutesAdapter( + accountActionListener: AccountActionListener, + animateAvatar: Boolean, + animateEmojis: Boolean +) : AccountAdapter( + accountActionListener, + animateAvatar, + animateEmojis +) { + private val mutingNotificationsMap = HashMap() + + override fun createAccountViewHolder(parent: ViewGroup): MutedUserViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_muted_user, parent, false) + return MutedUserViewHolder(view) + } + + override fun onBindAccountViewHolder(viewHolder: MutedUserViewHolder, position: Int) { + val account = accountList[position] + viewHolder.setupWithAccount( + account, + mutingNotificationsMap[account.id], + animateAvatar, + animateEmojis + ) + viewHolder.setupActionListener(accountActionListener) + } + + fun updateMutingNotifications(id: String, mutingNotifications: Boolean, position: Int) { + mutingNotificationsMap[id] = mutingNotifications + notifyItemChanged(position) + } + + fun updateMutingNotificationsMap(newMutingNotificationsMap: HashMap?) { + mutingNotificationsMap.putAll(newMutingNotificationsMap!!) + notifyDataSetChanged() + } + + class MutedUserViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + private val avatar: ImageView = itemView.findViewById(R.id.muted_user_avatar) + private val username: TextView = itemView.findViewById(R.id.muted_user_username) + private val displayName: TextView = itemView.findViewById(R.id.muted_user_display_name) + private val unmute: ImageButton = itemView.findViewById(R.id.muted_user_unmute) + private val muteNotifications: ImageButton = + itemView.findViewById(R.id.muted_user_mute_notifications) + + private var id: String? = null + private var notifications = false + + fun setupWithAccount( + account: Account, + mutingNotifications: Boolean?, + animateAvatar: Boolean, + animateEmojis: Boolean + ) { + id = account.id + val emojifiedName = account.name.emojify(account.emojis, displayName, animateEmojis) + displayName.text = emojifiedName + val format = username.context.getString(R.string.status_username_format) + val formattedUsername = String.format(format, account.username) + username.text = formattedUsername + val avatarRadius = avatar.context.resources + .getDimensionPixelSize(R.dimen.avatar_radius_48dp) + loadAvatar(account.avatar, avatar, avatarRadius, animateAvatar) + val unmuteString = + unmute.context.getString(R.string.action_unmute_desc, formattedUsername) + unmute.contentDescription = unmuteString + ViewCompat.setTooltipText(unmute, unmuteString) + if (mutingNotifications == null) { + muteNotifications.isEnabled = false + notifications = true + } else { + muteNotifications.isEnabled = true + notifications = mutingNotifications + } + if (notifications) { + muteNotifications.setImageResource(R.drawable.ic_notifications_24dp) + val unmuteNotificationsString = muteNotifications.context + .getString(R.string.action_unmute_notifications_desc, formattedUsername) + muteNotifications.contentDescription = unmuteNotificationsString + ViewCompat.setTooltipText(muteNotifications, unmuteNotificationsString) + } else { + muteNotifications.setImageResource(R.drawable.ic_notifications_off_24dp) + val muteNotificationsString = muteNotifications.context + .getString(R.string.action_mute_notifications_desc, formattedUsername) + muteNotifications.contentDescription = muteNotificationsString + ViewCompat.setTooltipText(muteNotifications, muteNotificationsString) + } + } + + fun setupActionListener(listener: AccountActionListener) { + unmute.setOnClickListener { + listener.onMute( + false, + id, + bindingAdapterPosition, + false + ) + } + muteNotifications.setOnClickListener { + listener.onMute( + true, + id, + bindingAdapterPosition, + !notifications + ) + } + itemView.setOnClickListener { listener.onViewAccount(id) } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java index b42f42782..e0d8e8983 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java @@ -1,4 +1,4 @@ -/* Copyright 2017 Andrew Dawson +/* Copyright 2021 Tusky Contributors * * This file is a part of Tusky. * diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/PlaceholderViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/PlaceholderViewHolder.java deleted file mode 100644 index 9f85d9817..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/PlaceholderViewHolder.java +++ /dev/null @@ -1,49 +0,0 @@ -/* Copyright 2017 Andrew Dawson - * - * This file is a part of Tusky. - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky; if not, - * see . */ - -package com.keylesspalace.tusky.adapter; - -import androidx.recyclerview.widget.RecyclerView; -import android.view.View; -import android.widget.Button; -import android.widget.ProgressBar; - -import com.keylesspalace.tusky.R; -import com.keylesspalace.tusky.interfaces.StatusActionListener; - -public final class PlaceholderViewHolder extends RecyclerView.ViewHolder { - - private Button loadMoreButton; - private ProgressBar progressBar; - - public PlaceholderViewHolder(View itemView) { - super(itemView); - loadMoreButton = itemView.findViewById(R.id.button_load_more); - progressBar = itemView.findViewById(R.id.progressBar); - } - - public void setup(final StatusActionListener listener, boolean progress) { - loadMoreButton.setVisibility(progress ? View.GONE : View.VISIBLE); - progressBar.setVisibility(progress ? View.VISIBLE : View.GONE); - - loadMoreButton.setEnabled(true); - loadMoreButton.setOnClickListener(v -> { - loadMoreButton.setEnabled(false); - listener.onLoadMore(getBindingAdapterPosition()); - }); - - } - -} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/PlaceholderViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/PlaceholderViewHolder.kt new file mode 100644 index 000000000..df05b6ae0 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/PlaceholderViewHolder.kt @@ -0,0 +1,41 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ +package com.keylesspalace.tusky.adapter + +import android.view.View +import android.widget.Button +import android.widget.ProgressBar +import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.interfaces.StatusActionListener + +/** + * Placeholder for different timelines. + * Either displays "load more" button or a progress indicator. + **/ +class PlaceholderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + private val loadMoreButton: Button = itemView.findViewById(R.id.button_load_more) + private val progressBar: ProgressBar = itemView.findViewById(R.id.progressBar) + + fun setup(listener: StatusActionListener, progress: Boolean) { + loadMoreButton.visibility = if (progress) View.GONE else View.VISIBLE + progressBar.visibility = if (progress) View.VISIBLE else View.GONE + loadMoreButton.isEnabled = true + loadMoreButton.setOnClickListener { v: View? -> + loadMoreButton.isEnabled = false + listener.onLoadMore(bindingAdapterPosition) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.java deleted file mode 100644 index 0143cb435..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.java +++ /dev/null @@ -1,164 +0,0 @@ -/* Copyright 2017 Andrew Dawson - * - * This file is a part of Tusky. - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky; if not, - * see . */ - -package com.keylesspalace.tusky.adapter; - -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.recyclerview.widget.RecyclerView; - -import com.keylesspalace.tusky.R; -import com.keylesspalace.tusky.interfaces.StatusActionListener; -import com.keylesspalace.tusky.util.StatusDisplayOptions; -import com.keylesspalace.tusky.viewdata.StatusViewData; - -import java.util.ArrayList; -import java.util.List; - -public class ThreadAdapter extends RecyclerView.Adapter { - private static final int VIEW_TYPE_STATUS = 0; - private static final int VIEW_TYPE_STATUS_DETAILED = 1; - - private List statuses; - private StatusDisplayOptions statusDisplayOptions; - private StatusActionListener statusActionListener; - private int detailedStatusPosition; - - public ThreadAdapter(StatusDisplayOptions statusDisplayOptions, StatusActionListener listener) { - this.statusDisplayOptions = statusDisplayOptions; - this.statusActionListener = listener; - this.statuses = new ArrayList<>(); - detailedStatusPosition = RecyclerView.NO_POSITION; - } - - @NonNull - @Override - public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - switch (viewType) { - default: - case VIEW_TYPE_STATUS: { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.item_status, parent, false); - return new StatusViewHolder(view); - } - case VIEW_TYPE_STATUS_DETAILED: { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.item_status_detailed, parent, false); - return new StatusDetailedViewHolder(view); - } - } - } - - @Override - public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { - StatusViewData.Concrete status = statuses.get(position); - if (position == detailedStatusPosition) { - StatusDetailedViewHolder holder = (StatusDetailedViewHolder) viewHolder; - holder.setupWithStatus(status, statusActionListener, statusDisplayOptions); - } else { - StatusViewHolder holder = (StatusViewHolder) viewHolder; - holder.setupWithStatus(status, statusActionListener, statusDisplayOptions); - } - } - - @Override - public int getItemViewType(int position) { - if (position == detailedStatusPosition) { - return VIEW_TYPE_STATUS_DETAILED; - } else { - return VIEW_TYPE_STATUS; - } - } - - @Override - public int getItemCount() { - return statuses.size(); - } - - public void setStatuses(List statuses) { - this.statuses.clear(); - this.statuses.addAll(statuses); - notifyDataSetChanged(); - } - - public void addItem(int position, StatusViewData.Concrete statusViewData) { - statuses.add(position, statusViewData); - notifyItemInserted(position); - } - - public void clearItems() { - int oldSize = statuses.size(); - statuses.clear(); - detailedStatusPosition = RecyclerView.NO_POSITION; - notifyItemRangeRemoved(0, oldSize); - } - - public void addAll(int position, List statuses) { - this.statuses.addAll(position, statuses); - notifyItemRangeInserted(position, statuses.size()); - } - - public void addAll(List statuses) { - int end = statuses.size(); - this.statuses.addAll(statuses); - notifyItemRangeInserted(end, statuses.size()); - } - - public void removeItem(int position) { - statuses.remove(position); - notifyItemRemoved(position); - } - - public void clear() { - statuses.clear(); - detailedStatusPosition = RecyclerView.NO_POSITION; - notifyDataSetChanged(); - } - - public void setItem(int position, StatusViewData.Concrete status, boolean notifyAdapter) { - statuses.set(position, status); - if (notifyAdapter) { - notifyItemChanged(position); - } - } - - @Nullable - public StatusViewData.Concrete getItem(int position) { - if (position >= 0 && position < statuses.size()) { - return statuses.get(position); - } else { - return null; - } - } - - public void setDetailedStatusPosition(int position) { - if (position != detailedStatusPosition - && detailedStatusPosition != RecyclerView.NO_POSITION) { - int prior = detailedStatusPosition; - detailedStatusPosition = position; - notifyItemChanged(prior); - } else { - detailedStatusPosition = position; - } - } - - public int getDetailedStatusPosition() { - return detailedStatusPosition; - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.kt new file mode 100644 index 000000000..1d05dff11 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.kt @@ -0,0 +1,129 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ +package com.keylesspalace.tusky.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.interfaces.StatusActionListener +import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.viewdata.StatusViewData + +class ThreadAdapter( + private val statusDisplayOptions: StatusDisplayOptions, + private val statusActionListener: StatusActionListener +) : RecyclerView.Adapter() { + private val statuses = mutableListOf() + var detailedStatusPosition: Int = RecyclerView.NO_POSITION + private set + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StatusBaseViewHolder { + return when (viewType) { + VIEW_TYPE_STATUS -> { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_status, parent, false) + StatusViewHolder(view) + } + VIEW_TYPE_STATUS_DETAILED -> { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_status_detailed, parent, false) + StatusDetailedViewHolder(view) + } + else -> error("Unknown item type: $viewType") + } + } + + override fun onBindViewHolder(viewHolder: StatusBaseViewHolder, position: Int) { + val status = statuses[position] + viewHolder.setupWithStatus(status, statusActionListener, statusDisplayOptions) + } + + override fun getItemViewType(position: Int): Int { + return if (position == detailedStatusPosition) { + VIEW_TYPE_STATUS_DETAILED + } else { + VIEW_TYPE_STATUS + } + } + + override fun getItemCount(): Int = statuses.size + + fun setStatuses(statuses: List?) { + this.statuses.clear() + this.statuses.addAll(statuses!!) + notifyDataSetChanged() + } + + fun addItem(position: Int, statusViewData: StatusViewData.Concrete) { + statuses.add(position, statusViewData) + notifyItemInserted(position) + } + + fun clearItems() { + val oldSize = statuses.size + statuses.clear() + detailedStatusPosition = RecyclerView.NO_POSITION + notifyItemRangeRemoved(0, oldSize) + } + + fun addAll(position: Int, statuses: List) { + this.statuses.addAll(position, statuses) + notifyItemRangeInserted(position, statuses.size) + } + + fun addAll(statuses: List) { + val end = statuses.size + this.statuses.addAll(statuses) + notifyItemRangeInserted(end, statuses.size) + } + + fun removeItem(position: Int) { + statuses.removeAt(position) + notifyItemRemoved(position) + } + + fun clear() { + statuses.clear() + detailedStatusPosition = RecyclerView.NO_POSITION + notifyDataSetChanged() + } + + fun setItem(position: Int, status: StatusViewData.Concrete, notifyAdapter: Boolean) { + statuses[position] = status + if (notifyAdapter) { + notifyItemChanged(position) + } + } + + fun getItem(position: Int): StatusViewData.Concrete? = statuses.getOrNull(position) + + fun setDetailedStatusPosition(position: Int) { + if (position != detailedStatusPosition + && detailedStatusPosition != RecyclerView.NO_POSITION + ) { + val prior = detailedStatusPosition + detailedStatusPosition = position + notifyItemChanged(prior) + } else { + detailedStatusPosition = position + } + } + + companion object { + private const val VIEW_TYPE_STATUS = 0 + private const val VIEW_TYPE_STATUS_DETAILED = 1 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt index dc49fbc22..cd37f8425 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt @@ -67,7 +67,7 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct private var id: String? = null private lateinit var scrollListener: EndlessOnScrollListener - private lateinit var adapter: AccountAdapter + private lateinit var adapter: AccountAdapter<*> private var fetching = false private var bottomId: String? = null From 920c71560b5200d14daaf43a87f8c9032f437812 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sun, 20 Jun 2021 10:19:03 +0200 Subject: [PATCH 78/92] throw HttpException instead of generic exception in TimelineViewModel (#2202) --- .../tusky/components/timeline/TimelineViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineViewModel.kt index 49655ad87..74ff7163d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineViewModel.kt @@ -429,7 +429,7 @@ class TimelineViewModel @Inject constructor( } response.body()?.map { Either.Right(it) } ?: listOf() } else { - throw Exception(response.message()) + throw HttpException(response) } } From 554820de5ff53bf2aa58d299b11a041c453f0880 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sun, 20 Jun 2021 10:58:19 +0200 Subject: [PATCH 79/92] migrate reporting to paging 3 (#2205) * migrate reporting to paging 3 * apply PR feedback --- .../tusky/components/report/ReportActivity.kt | 1 - .../components/report/ReportViewModel.kt | 58 ++- .../report/adapter/StatusesAdapter.kt | 4 +- .../report/adapter/StatusesDataSource.kt | 150 ------ .../adapter/StatusesDataSourceFactory.kt | 36 -- .../report/adapter/StatusesPagingSource.kt | 89 ++++ .../report/adapter/StatusesRepository.kt | 60 --- .../report/fragments/ReportNoteFragment.kt | 17 +- .../fragments/ReportStatusesFragment.kt | 68 +-- .../tusky/network/MastodonApi.kt | 1 + .../com/keylesspalace/tusky/util/BiListing.kt | 38 -- .../tusky/util/PagingRequestHelper.java | 491 ------------------ .../tusky/util/getErrorMessage.kt | 23 - 13 files changed, 162 insertions(+), 874 deletions(-) delete mode 100644 app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesDataSource.kt delete mode 100644 app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesDataSourceFactory.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesPagingSource.kt delete mode 100644 app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesRepository.kt delete mode 100644 app/src/main/java/com/keylesspalace/tusky/util/BiListing.kt delete mode 100644 app/src/main/java/com/keylesspalace/tusky/util/PagingRequestHelper.java delete mode 100644 app/src/main/java/com/keylesspalace/tusky/util/getErrorMessage.kt diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt index 57c9214cf..4c53588a3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt @@ -51,7 +51,6 @@ class ReportActivity : BottomSheetActivity(), HasAndroidInjector { viewModel.init(accountId, accountUserName, intent?.getStringExtra(STATUS_ID)) - setContentView(binding.root) setSupportActionBar(binding.includedToolbar.toolbar) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt index e5691473d..7b51e91ce 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt @@ -17,25 +17,35 @@ package com.keylesspalace.tusky.components.report import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.Transformations -import androidx.paging.PagedList +import androidx.lifecycle.viewModelScope +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.cachedIn import com.keylesspalace.tusky.appstore.BlockEvent import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.MuteEvent -import com.keylesspalace.tusky.components.report.adapter.StatusesRepository +import com.keylesspalace.tusky.components.report.adapter.StatusesPagingSource import com.keylesspalace.tusky.components.report.model.StatusViewState import com.keylesspalace.tusky.entity.Relationship import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.util.* +import com.keylesspalace.tusky.util.Error +import com.keylesspalace.tusky.util.Loading +import com.keylesspalace.tusky.util.Resource +import com.keylesspalace.tusky.util.RxAwareViewModel +import com.keylesspalace.tusky.util.Success import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.schedulers.Schedulers +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.launch import javax.inject.Inject class ReportViewModel @Inject constructor( private val mastodonApi: MastodonApi, - private val eventHub: EventHub, - private val statusesRepository: StatusesRepository) : RxAwareViewModel() { + private val eventHub: EventHub +) : RxAwareViewModel() { private val navigationMutable = MutableLiveData() val navigation: LiveData = navigationMutable @@ -52,11 +62,19 @@ class ReportViewModel @Inject constructor( private val checkUrlMutable = MutableLiveData() val checkUrl: LiveData = checkUrlMutable - private val repoResult = MutableLiveData>() - val statuses: LiveData> = Transformations.switchMap(repoResult) { it.pagedList } - val networkStateAfter: LiveData = Transformations.switchMap(repoResult) { it.networkStateAfter } - val networkStateBefore: LiveData = Transformations.switchMap(repoResult) { it.networkStateBefore } - val networkStateRefresh: LiveData = Transformations.switchMap(repoResult) { it.refreshState } + private val accountIdFlow = MutableSharedFlow( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + + val statusesFlow = accountIdFlow.flatMapLatest { accountId -> + Pager( + initialKey = statusId, + config = PagingConfig(pageSize = 20, initialLoadSize = 20), + pagingSourceFactory = { StatusesPagingSource(accountId, mastodonApi) } + ).flow + } + .cachedIn(viewModelScope) private val selectedIds = HashSet() val statusViewState = StatusViewState() @@ -84,7 +102,10 @@ class ReportViewModel @Inject constructor( } obtainRelationship() - repoResult.value = statusesRepository.getStatuses(accountId, statusId, disposables) + + viewModelScope.launch { + accountIdFlow.emit(accountId) + } } fun navigateTo(screen: Screen) { @@ -95,7 +116,6 @@ class ReportViewModel @Inject constructor( navigationMutable.value = null } - private fun obtainRelationship() { val ids = listOf(accountId) muteStateMutable.value = Loading() @@ -115,7 +135,6 @@ class ReportViewModel @Inject constructor( .autoDispose() } - private fun updateRelationship(relationship: Relationship?) { if (relationship != null) { muteStateMutable.value = Success(relationship.muting) @@ -194,14 +213,6 @@ class ReportViewModel @Inject constructor( } - fun retryStatusLoad() { - repoResult.value?.retry?.invoke() - } - - fun refreshStatuses() { - repoResult.value?.refresh?.invoke() - } - fun checkClickedUrl(url: String?) { checkUrlMutable.value = url } @@ -221,5 +232,4 @@ class ReportViewModel @Inject constructor( fun isStatusChecked(id: String): Boolean { return selectedIds.contains(id) } - -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesAdapter.kt index b66ac4f3c..d472995d4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesAdapter.kt @@ -17,7 +17,7 @@ package com.keylesspalace.tusky.components.report.adapter import android.view.LayoutInflater import android.view.ViewGroup -import androidx.paging.PagedListAdapter +import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.keylesspalace.tusky.components.report.model.StatusViewState @@ -29,7 +29,7 @@ class StatusesAdapter( private val statusDisplayOptions: StatusDisplayOptions, private val statusViewState: StatusViewState, private val adapterHandler: AdapterHandler -) : PagedListAdapter(STATUS_COMPARATOR) { +) : PagingDataAdapter(STATUS_COMPARATOR) { private val statusForPosition: (Int) -> Status? = { position: Int -> if (position != RecyclerView.NO_POSITION) getItem(position) else null diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesDataSource.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesDataSource.kt deleted file mode 100644 index 9566214c5..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesDataSource.kt +++ /dev/null @@ -1,150 +0,0 @@ -/* Copyright 2019 Joel Pyska - * - * This file is a part of Tusky. - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky; if not, - * see . */ - -package com.keylesspalace.tusky.components.report.adapter - -import android.annotation.SuppressLint -import androidx.lifecycle.MutableLiveData -import androidx.paging.ItemKeyedDataSource -import com.keylesspalace.tusky.entity.Status -import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.util.NetworkState -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.functions.BiFunction -import java.util.concurrent.Executor - -class StatusesDataSource(private val accountId: String, - private val mastodonApi: MastodonApi, - private val disposables: CompositeDisposable, - private val retryExecutor: Executor) : ItemKeyedDataSource() { - - val networkStateAfter = MutableLiveData() - val networkStateBefore = MutableLiveData() - - private var retryAfter: (() -> Any)? = null - private var retryBefore: (() -> Any)? = null - private var retryInitial: (() -> Any)? = null - - val initialLoad = MutableLiveData() - fun retryAllFailed() { - var prevRetry = retryInitial - retryInitial = null - prevRetry?.let { - retryExecutor.execute { - it.invoke() - } - } - - prevRetry = retryAfter - retryAfter = null - prevRetry?.let { - retryExecutor.execute { - it.invoke() - } - } - - prevRetry = retryBefore - retryBefore = null - prevRetry?.let { - retryExecutor.execute { - it.invoke() - } - } - } - - @SuppressLint("CheckResult") - override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback) { - networkStateAfter.postValue(NetworkState.LOADED) - networkStateBefore.postValue(NetworkState.LOADED) - retryAfter = null - retryBefore = null - retryInitial = null - initialLoad.postValue(NetworkState.LOADING) - val initialKey = params.requestedInitialKey - if (initialKey == null) { - mastodonApi.accountStatusesObservable(accountId, null, null, params.requestedLoadSize, true) - } else { - mastodonApi.statusObservable(initialKey).zipWith( - mastodonApi.accountStatusesObservable(accountId, params.requestedInitialKey, null, params.requestedLoadSize - 1, true), - BiFunction { status: Status, list: List -> - val ret = ArrayList() - ret.add(status) - ret.addAll(list) - return@BiFunction ret - }) - } - .doOnSubscribe { - disposables.add(it) - } - .subscribe( - { - callback.onResult(it) - initialLoad.postValue(NetworkState.LOADED) - }, - { - retryInitial = { - loadInitial(params, callback) - } - initialLoad.postValue(NetworkState.error(it.message)) - } - ) - } - - @SuppressLint("CheckResult") - override fun loadAfter(params: LoadParams, callback: LoadCallback) { - networkStateAfter.postValue(NetworkState.LOADING) - retryAfter = null - mastodonApi.accountStatusesObservable(accountId, params.key, null, params.requestedLoadSize, true) - .doOnSubscribe { - disposables.add(it) - } - .subscribe( - { - callback.onResult(it) - networkStateAfter.postValue(NetworkState.LOADED) - }, - { - retryAfter = { - loadAfter(params, callback) - } - networkStateAfter.postValue(NetworkState.error(it.message)) - } - ) - } - - @SuppressLint("CheckResult") - override fun loadBefore(params: LoadParams, callback: LoadCallback) { - networkStateBefore.postValue(NetworkState.LOADING) - retryBefore = null - mastodonApi.accountStatusesObservable(accountId, null, params.key, params.requestedLoadSize, true) - .doOnSubscribe { - disposables.add(it) - } - .subscribe( - { - callback.onResult(it) - networkStateBefore.postValue(NetworkState.LOADED) - }, - { - retryBefore = { - loadBefore(params, callback) - } - networkStateBefore.postValue(NetworkState.error(it.message)) - } - ) - } - - override fun getKey(item: Status): String = item.id -} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesDataSourceFactory.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesDataSourceFactory.kt deleted file mode 100644 index 1afdc3c01..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesDataSourceFactory.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* Copyright 2019 Joel Pyska - * - * This file is a part of Tusky. - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky; if not, - * see . */ - -package com.keylesspalace.tusky.components.report.adapter - -import androidx.lifecycle.MutableLiveData -import androidx.paging.DataSource -import com.keylesspalace.tusky.entity.Status -import com.keylesspalace.tusky.network.MastodonApi -import io.reactivex.rxjava3.disposables.CompositeDisposable -import java.util.concurrent.Executor - -class StatusesDataSourceFactory( - private val accountId: String, - private val mastodonApi: MastodonApi, - private val disposables: CompositeDisposable, - private val retryExecutor: Executor) : DataSource.Factory() { - val sourceLiveData = MutableLiveData() - override fun create(): DataSource { - val source = StatusesDataSource(accountId, mastodonApi, disposables, retryExecutor) - sourceLiveData.postValue(source) - return source - } -} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesPagingSource.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesPagingSource.kt new file mode 100644 index 000000000..964e23e27 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesPagingSource.kt @@ -0,0 +1,89 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.report.adapter + +import android.util.Log +import androidx.paging.PagingSource +import androidx.paging.PagingState +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.network.MastodonApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.rx3.await +import kotlinx.coroutines.withContext + +class StatusesPagingSource( + private val accountId: String, + private val mastodonApi: MastodonApi +) : PagingSource() { + + override fun getRefreshKey(state: PagingState): String? { + return state.anchorPosition?.let { anchorPosition -> + state.closestItemToPosition(anchorPosition)?.id + } + } + + override suspend fun load(params: LoadParams): LoadResult { + val key = params.key + try { + val result = if (params is LoadParams.Refresh && key != null) { + withContext(Dispatchers.IO) { + val initialStatus = async { getSingleStatus(key) } + val additionalStatuses = async { getStatusList(maxId = key, limit = params.loadSize - 1) } + listOf(initialStatus.await()) + additionalStatuses.await() + } + } else { + val maxId = if (params is LoadParams.Refresh || params is LoadParams.Append) { + params.key + } else { + null + } + + val minId = if (params is LoadParams.Prepend) { + params.key + } else { + null + } + + getStatusList(minId = minId, maxId = maxId, limit = params.loadSize) + } + return LoadResult.Page( + data = result, + prevKey = result.firstOrNull()?.id, + nextKey = result.lastOrNull()?.id + ) + + } catch (e: Exception) { + Log.w("StatusesPagingSource", "failed to load statuses", e) + return LoadResult.Error(e) + } + } + + private suspend fun getSingleStatus(statusId: String): Status { + return mastodonApi.statusObservable(statusId).await() + } + + private suspend fun getStatusList(minId: String? = null, maxId: String? = null, limit: Int): List { + return mastodonApi.accountStatusesObservable( + accountId = accountId, + maxId = maxId, + sinceId = null, + minId = minId, + limit = limit, + excludeReblogs = true + ).await() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesRepository.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesRepository.kt deleted file mode 100644 index eb7866ac3..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesRepository.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* Copyright 2019 Joel Pyska - * - * This file is a part of Tusky. - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky; if not, - * see . */ - -package com.keylesspalace.tusky.components.report.adapter - -import androidx.lifecycle.Transformations -import androidx.paging.Config -import androidx.paging.toLiveData -import com.keylesspalace.tusky.entity.Status -import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.util.BiListing -import io.reactivex.rxjava3.disposables.CompositeDisposable -import java.util.concurrent.Executors -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class StatusesRepository @Inject constructor(private val mastodonApi: MastodonApi) { - - private val executor = Executors.newSingleThreadExecutor() - - fun getStatuses(accountId: String, initialStatus: String?, disposables: CompositeDisposable, pageSize: Int = 20): BiListing { - val sourceFactory = StatusesDataSourceFactory(accountId, mastodonApi, disposables, executor) - val livePagedList = sourceFactory.toLiveData( - config = Config(pageSize = pageSize, enablePlaceholders = false, initialLoadSizeHint = pageSize * 2), - fetchExecutor = executor, initialLoadKey = initialStatus - ) - return BiListing( - pagedList = livePagedList, - networkStateBefore = Transformations.switchMap(sourceFactory.sourceLiveData) { - it.networkStateBefore - }, - networkStateAfter = Transformations.switchMap(sourceFactory.sourceLiveData) { - it.networkStateAfter - }, - retry = { - sourceFactory.sourceLiveData.value?.retryAllFailed() - }, - refresh = { - sourceFactory.sourceLiveData.value?.invalidate() - }, - refreshState = Transformations.switchMap(sourceFactory.sourceLiveData) { - it.initialLoad - } - - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt index b47b586a5..aa3559355 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt @@ -27,7 +27,12 @@ import com.keylesspalace.tusky.components.report.Screen import com.keylesspalace.tusky.databinding.FragmentReportNoteBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory -import com.keylesspalace.tusky.util.* +import com.keylesspalace.tusky.util.Error +import com.keylesspalace.tusky.util.Loading +import com.keylesspalace.tusky.util.Success +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.viewBinding import java.io.IOException import javax.inject.Inject @@ -92,12 +97,10 @@ class ReportNoteFragment : Fragment(R.layout.fragment_report_note), Injectable { binding.progressBar.hide() Snackbar.make(binding.buttonBack, if (error is IOException) R.string.error_network else R.string.error_generic, Snackbar.LENGTH_LONG) - .apply { - setAction(R.string.action_retry) { - sendReport() - } - } - .show() + .setAction(R.string.action_retry) { + sendReport() + } + .show() } private fun sendReport() { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt index 01a12c23c..33cd2ece2 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt @@ -21,6 +21,8 @@ import androidx.core.app.ActivityOptionsCompat import androidx.core.view.ViewCompat import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope +import androidx.paging.LoadState import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager @@ -43,10 +45,11 @@ import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.util.CardViewMode import com.keylesspalace.tusky.util.StatusDisplayOptions -import com.keylesspalace.tusky.util.hide -import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.viewBinding +import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.viewdata.AttachmentViewData +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch import javax.inject.Inject class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Injectable, AdapterHandler { @@ -70,13 +73,11 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje when (actionable.attachments[idx].type) { Attachment.Type.GIFV, Attachment.Type.VIDEO, Attachment.Type.IMAGE, Attachment.Type.AUDIO -> { val attachments = AttachmentViewData.list(actionable) - val intent = ViewMediaActivity.newIntent(context, attachments, - idx) + val intent = ViewMediaActivity.newIntent(context, attachments, idx) if (v != null) { val url = actionable.attachments[idx].url ViewCompat.setTransitionName(v, url) - val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(), - v, url) + val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(), v, url) startActivity(intent, options.toBundle()) } else { startActivity(intent) @@ -85,7 +86,6 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje Attachment.Type.UNKNOWN -> { } } - } } @@ -100,7 +100,7 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje binding.swipeRefreshLayout.setOnRefreshListener { snackbarErrorRetry?.dismiss() - viewModel.refreshStatuses() + adapter.refresh() } } @@ -118,62 +118,46 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) ) - adapter = StatusesAdapter(statusDisplayOptions, - viewModel.statusViewState, this) + adapter = StatusesAdapter(statusDisplayOptions, viewModel.statusViewState, this) binding.recyclerView.addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL)) binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) binding.recyclerView.adapter = adapter (binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false - viewModel.statuses.observe(viewLifecycleOwner) { - adapter.submitList(it) + lifecycleScope.launch { + viewModel.statusesFlow.collectLatest { pagingData -> + adapter.submitData(pagingData) + } } - viewModel.networkStateAfter.observe(viewLifecycleOwner) { - if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING) - binding.progressBarBottom.show() - else - binding.progressBarBottom.hide() + adapter.addLoadStateListener { loadState -> + if (loadState.refresh is LoadState.Error + || loadState.append is LoadState.Error + || loadState.prepend is LoadState.Error) { + showError() + } - if (it?.status == com.keylesspalace.tusky.util.Status.FAILED) - showError(it.msg) - } + binding.progressBarBottom.visible(loadState.append == LoadState.Loading) + binding.progressBarTop.visible(loadState.prepend == LoadState.Loading) + binding.progressBarLoading.visible(loadState.refresh == LoadState.Loading && !binding.swipeRefreshLayout.isRefreshing) - viewModel.networkStateBefore.observe(viewLifecycleOwner) { - if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING) - binding.progressBarTop.show() - else - binding.progressBarTop.hide() - - if (it?.status == com.keylesspalace.tusky.util.Status.FAILED) - showError(it.msg) - } - - viewModel.networkStateRefresh.observe(viewLifecycleOwner) { - if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING && !binding.swipeRefreshLayout.isRefreshing) - binding.progressBarLoading.show() - else - binding.progressBarLoading.hide() - - if (it?.status != com.keylesspalace.tusky.util.Status.RUNNING) + if (loadState.refresh != LoadState.Loading) { binding.swipeRefreshLayout.isRefreshing = false - if (it?.status == com.keylesspalace.tusky.util.Status.FAILED) - showError(it.msg) + } } } - private fun showError(@Suppress("UNUSED_PARAMETER") msg: String?) { + private fun showError() { if (snackbarErrorRetry?.isShown != true) { snackbarErrorRetry = Snackbar.make(binding.swipeRefreshLayout, R.string.failed_fetch_statuses, Snackbar.LENGTH_INDEFINITE) snackbarErrorRetry?.setAction(R.string.action_retry) { - viewModel.retryStatusLoad() + adapter.retry() } snackbarErrorRetry?.show() } } - private fun handleClicks() { binding.buttonCancel.setOnClickListener { viewModel.navigateTo(Screen.Back) diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt index 6aadc9b76..96f05349d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -577,6 +577,7 @@ interface MastodonApi { @Path("id") accountId: String, @Query("max_id") maxId: String?, @Query("since_id") sinceId: String?, + @Query("min_id") minId: String?, @Query("limit") limit: Int?, @Query("exclude_reblogs") excludeReblogs: Boolean? ): Single> diff --git a/app/src/main/java/com/keylesspalace/tusky/util/BiListing.kt b/app/src/main/java/com/keylesspalace/tusky/util/BiListing.kt deleted file mode 100644 index 268631cb7..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/util/BiListing.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.keylesspalace.tusky.util - -import androidx.lifecycle.LiveData -import androidx.paging.PagedList - -/** - * Data class that is necessary for a UI to show a listing and interact w/ the rest of the system - */ -data class BiListing( - // the LiveData of paged lists for the UI to observe - val pagedList: LiveData>, - // represents the network request status for load data before first to show to the user - val networkStateBefore: LiveData, - // represents the network request status for load data after last to show to the user - val networkStateAfter: LiveData, - // represents the refresh status to show to the user. Separate from networkState, this - // value is importantly only when refresh is requested. - val refreshState: LiveData, - // refreshes the whole data and fetches it from scratch. - val refresh: () -> Unit, - // retries any failed requests. - val retry: () -> Unit) \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/util/PagingRequestHelper.java b/app/src/main/java/com/keylesspalace/tusky/util/PagingRequestHelper.java deleted file mode 100644 index 4f7d3effb..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/util/PagingRequestHelper.java +++ /dev/null @@ -1,491 +0,0 @@ -/* - * Copyright 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.keylesspalace.tusky.util; - -import androidx.annotation.AnyThread; -import androidx.annotation.GuardedBy; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import java.util.Arrays; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.Executor; -import java.util.concurrent.atomic.AtomicBoolean; -/** - * A helper class for {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}s and - * {@link androidx.paging.DataSource}s to help with tracking network requests. - *

- * It is designed to support 3 types of requests, {@link RequestType#INITIAL INITIAL}, - * {@link RequestType#BEFORE BEFORE} and {@link RequestType#AFTER AFTER} and runs only 1 request - * for each of them via {@link #runIfNotRunning(RequestType, Request)}. - *

- * It tracks a {@link Status} and an {@code error} for each {@link RequestType}. - *

- * A sample usage of this class to limit requests looks like this: - *

- * class PagingBoundaryCallback extends PagedList.BoundaryCallback<MyItem> {
- *     // TODO replace with an executor from your application
- *     Executor executor = Executors.newSingleThreadExecutor();
- *     PagingRequestHelper helper = new PagingRequestHelper(executor);
- *     // imaginary API service, using Retrofit
- *     MyApi api;
- *
- *     {@literal @}Override
- *     public void onItemAtFrontLoaded({@literal @}NonNull MyItem itemAtFront) {
- *         helper.runIfNotRunning(PagingRequestHelper.RequestType.BEFORE,
- *                 helperCallback -> api.getTopBefore(itemAtFront.getName(), 10).enqueue(
- *                         new Callback<ApiResponse>() {
- *                             {@literal @}Override
- *                             public void onResponse(Call<ApiResponse> call,
- *                                     Response<ApiResponse> response) {
- *                                 // TODO insert new records into database
- *                                 helperCallback.recordSuccess();
- *                             }
- *
- *                             {@literal @}Override
- *                             public void onFailure(Call<ApiResponse> call, Throwable t) {
- *                                 helperCallback.recordFailure(t);
- *                             }
- *                         }));
- *     }
- *
- *     {@literal @}Override
- *     public void onItemAtEndLoaded({@literal @}NonNull MyItem itemAtEnd) {
- *         helper.runIfNotRunning(PagingRequestHelper.RequestType.AFTER,
- *                 helperCallback -> api.getTopBefore(itemAtEnd.getName(), 10).enqueue(
- *                         new Callback<ApiResponse>() {
- *                             {@literal @}Override
- *                             public void onResponse(Call<ApiResponse> call,
- *                                     Response<ApiResponse> response) {
- *                                 // TODO insert new records into database
- *                                 helperCallback.recordSuccess();
- *                             }
- *
- *                             {@literal @}Override
- *                             public void onFailure(Call<ApiResponse> call, Throwable t) {
- *                                 helperCallback.recordFailure(t);
- *                             }
- *                         }));
- *     }
- * }
- * 
- *

- * The helper provides an API to observe combined request status, which can be reported back to the - * application based on your business rules. - *

- * MutableLiveData<PagingRequestHelper.Status> combined = new MutableLiveData<>();
- * helper.addListener(status -> {
- *     // merge multiple states per request type into one, or dispatch separately depending on
- *     // your application logic.
- *     if (status.hasRunning()) {
- *         combined.postValue(PagingRequestHelper.Status.RUNNING);
- *     } else if (status.hasError()) {
- *         // can also obtain the error via {@link StatusReport#getErrorFor(RequestType)}
- *         combined.postValue(PagingRequestHelper.Status.FAILED);
- *     } else {
- *         combined.postValue(PagingRequestHelper.Status.SUCCESS);
- *     }
- * });
- * 
- */ -// THIS class is likely to be moved into the library in a future release. Feel free to copy it -// from this sample. -public class PagingRequestHelper { - private final Object mLock = new Object(); - private final Executor mRetryService; - @GuardedBy("mLock") - private final RequestQueue[] mRequestQueues = new RequestQueue[] - {new RequestQueue(RequestType.INITIAL), - new RequestQueue(RequestType.BEFORE), - new RequestQueue(RequestType.AFTER)}; - @NonNull - final CopyOnWriteArrayList mListeners = new CopyOnWriteArrayList<>(); - /** - * Creates a new PagingRequestHelper with the given {@link Executor} which is used to run - * retry actions. - * - * @param retryService The {@link Executor} that can run the retry actions. - */ - public PagingRequestHelper(@NonNull Executor retryService) { - mRetryService = retryService; - } - /** - * Adds a new listener that will be notified when any request changes {@link Status state}. - * - * @param listener The listener that will be notified each time a request's status changes. - * @return True if it is added, false otherwise (e.g. it already exists in the list). - */ - @AnyThread - public boolean addListener(@NonNull Listener listener) { - return mListeners.add(listener); - } - /** - * Removes the given listener from the listeners list. - * - * @param listener The listener that will be removed. - * @return True if the listener is removed, false otherwise (e.g. it never existed) - */ - public boolean removeListener(@NonNull Listener listener) { - return mListeners.remove(listener); - } - /** - * Runs the given {@link Request} if no other requests in the given request type is already - * running. - *

- * If run, the request will be run in the current thread. - * - * @param type The type of the request. - * @param request The request to run. - * @return True if the request is run, false otherwise. - */ - @SuppressWarnings("WeakerAccess") - @AnyThread - public boolean runIfNotRunning(@NonNull RequestType type, @NonNull Request request) { - boolean hasListeners = !mListeners.isEmpty(); - StatusReport report = null; - synchronized (mLock) { - RequestQueue queue = mRequestQueues[type.ordinal()]; - if (queue.mRunning != null) { - return false; - } - queue.mRunning = request; - queue.mStatus = Status.RUNNING; - queue.mFailed = null; - queue.mLastError = null; - if (hasListeners) { - report = prepareStatusReportLocked(); - } - } - if (report != null) { - dispatchReport(report); - } - final RequestWrapper wrapper = new RequestWrapper(request, this, type); - wrapper.run(); - return true; - } - @GuardedBy("mLock") - private StatusReport prepareStatusReportLocked() { - Throwable[] errors = new Throwable[]{ - mRequestQueues[0].mLastError, - mRequestQueues[1].mLastError, - mRequestQueues[2].mLastError - }; - return new StatusReport( - getStatusForLocked(RequestType.INITIAL), - getStatusForLocked(RequestType.BEFORE), - getStatusForLocked(RequestType.AFTER), - errors - ); - } - @GuardedBy("mLock") - private Status getStatusForLocked(RequestType type) { - return mRequestQueues[type.ordinal()].mStatus; - } - @AnyThread - @VisibleForTesting - void recordResult(@NonNull RequestWrapper wrapper, @Nullable Throwable throwable) { - StatusReport report = null; - final boolean success = throwable == null; - boolean hasListeners = !mListeners.isEmpty(); - synchronized (mLock) { - RequestQueue queue = mRequestQueues[wrapper.mType.ordinal()]; - queue.mRunning = null; - queue.mLastError = throwable; - if (success) { - queue.mFailed = null; - queue.mStatus = Status.SUCCESS; - } else { - queue.mFailed = wrapper; - queue.mStatus = Status.FAILED; - } - if (hasListeners) { - report = prepareStatusReportLocked(); - } - } - if (report != null) { - dispatchReport(report); - } - } - private void dispatchReport(StatusReport report) { - for (Listener listener : mListeners) { - listener.onStatusChange(report); - } - } - /** - * Retries all failed requests. - * - * @return True if any request is retried, false otherwise. - */ - public boolean retryAllFailed() { - final RequestWrapper[] toBeRetried = new RequestWrapper[RequestType.values().length]; - boolean retried = false; - synchronized (mLock) { - for (int i = 0; i < RequestType.values().length; i++) { - toBeRetried[i] = mRequestQueues[i].mFailed; - mRequestQueues[i].mFailed = null; - } - } - for (RequestWrapper failed : toBeRetried) { - if (failed != null) { - failed.retry(mRetryService); - retried = true; - } - } - return retried; - } - static class RequestWrapper implements Runnable { - @NonNull - final Request mRequest; - @NonNull - final PagingRequestHelper mHelper; - @NonNull - final RequestType mType; - RequestWrapper(@NonNull Request request, @NonNull PagingRequestHelper helper, - @NonNull RequestType type) { - mRequest = request; - mHelper = helper; - mType = type; - } - @Override - public void run() { - mRequest.run(new Request.Callback(this, mHelper)); - } - void retry(Executor service) { - service.execute(new Runnable() { - @Override - public void run() { - mHelper.runIfNotRunning(mType, mRequest); - } - }); - } - } - /** - * Runner class that runs a request tracked by the {@link PagingRequestHelper}. - *

- * When a request is invoked, it must call one of {@link Callback#recordFailure(Throwable)} - * or {@link Callback#recordSuccess()} once and only once. This call - * can be made any time. Until that method call is made, {@link PagingRequestHelper} will - * consider the request is running. - */ - @FunctionalInterface - public interface Request { - /** - * Should run the request and call the given {@link Callback} with the result of the - * request. - * - * @param callback The callback that should be invoked with the result. - */ - void run(Callback callback); - /** - * Callback class provided to the {@link #run(Callback)} method to report the result. - */ - class Callback { - private final AtomicBoolean mCalled = new AtomicBoolean(); - private final RequestWrapper mWrapper; - private final PagingRequestHelper mHelper; - Callback(RequestWrapper wrapper, PagingRequestHelper helper) { - mWrapper = wrapper; - mHelper = helper; - } - /** - * Call this method when the request succeeds and new data is fetched. - */ - @SuppressWarnings("unused") - public final void recordSuccess() { - if (mCalled.compareAndSet(false, true)) { - mHelper.recordResult(mWrapper, null); - } else { - throw new IllegalStateException( - "already called recordSuccess or recordFailure"); - } - } - /** - * Call this method with the failure message and the request can be retried via - * {@link #retryAllFailed()}. - * - * @param throwable The error that occured while carrying out the request. - */ - @SuppressWarnings("unused") - public final void recordFailure(@NonNull Throwable throwable) { - //noinspection ConstantConditions - if (throwable == null) { - throw new IllegalArgumentException("You must provide a throwable describing" - + " the error to record the failure"); - } - if (mCalled.compareAndSet(false, true)) { - mHelper.recordResult(mWrapper, throwable); - } else { - throw new IllegalStateException( - "already called recordSuccess or recordFailure"); - } - } - } - } - /** - * Data class that holds the information about the current status of the ongoing requests - * using this helper. - */ - public static final class StatusReport { - /** - * Status of the latest request that were submitted with {@link RequestType#INITIAL}. - */ - @NonNull - public final Status initial; - /** - * Status of the latest request that were submitted with {@link RequestType#BEFORE}. - */ - @NonNull - public final Status before; - /** - * Status of the latest request that were submitted with {@link RequestType#AFTER}. - */ - @NonNull - public final Status after; - @NonNull - private final Throwable[] mErrors; - StatusReport(@NonNull Status initial, @NonNull Status before, @NonNull Status after, - @NonNull Throwable[] errors) { - this.initial = initial; - this.before = before; - this.after = after; - this.mErrors = errors; - } - /** - * Convenience method to check if there are any running requests. - * - * @return True if there are any running requests, false otherwise. - */ - public boolean hasRunning() { - return initial == Status.RUNNING - || before == Status.RUNNING - || after == Status.RUNNING; - } - /** - * Convenience method to check if there are any requests that resulted in an error. - * - * @return True if there are any requests that finished with error, false otherwise. - */ - public boolean hasError() { - return initial == Status.FAILED - || before == Status.FAILED - || after == Status.FAILED; - } - /** - * Returns the error for the given request type. - * - * @param type The request type for which the error should be returned. - * @return The {@link Throwable} returned by the failing request with the given type or - * {@code null} if the request for the given type did not fail. - */ - @Nullable - public Throwable getErrorFor(@NonNull RequestType type) { - return mErrors[type.ordinal()]; - } - @Override - public String toString() { - return "StatusReport{" - + "initial=" + initial - + ", before=" + before - + ", after=" + after - + ", mErrors=" + Arrays.toString(mErrors) - + '}'; - } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - StatusReport that = (StatusReport) o; - if (initial != that.initial) return false; - if (before != that.before) return false; - if (after != that.after) return false; - // Probably incorrect - comparing Object[] arrays with Arrays.equals - return Arrays.equals(mErrors, that.mErrors); - } - @Override - public int hashCode() { - int result = initial.hashCode(); - result = 31 * result + before.hashCode(); - result = 31 * result + after.hashCode(); - result = 31 * result + Arrays.hashCode(mErrors); - return result; - } - } - /** - * Listener interface to get notified by request status changes. - */ - public interface Listener { - /** - * Called when the status for any of the requests has changed. - * - * @param report The current status report that has all the information about the requests. - */ - void onStatusChange(@NonNull StatusReport report); - } - /** - * Represents the status of a Request for each {@link RequestType}. - */ - public enum Status { - /** - * There is current a running request. - */ - RUNNING, - /** - * The last request has succeeded or no such requests have ever been run. - */ - SUCCESS, - /** - * The last request has failed. - */ - FAILED - } - /** - * Available request types. - */ - public enum RequestType { - /** - * Corresponds to an initial request made to a {@link androidx.paging.DataSource} or the empty state for - * a {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}. - */ - INITIAL, - /** - * Corresponds to the {@code loadBefore} calls in {@link androidx.paging.DataSource} or - * {@code onItemAtFrontLoaded} in - * {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}. - */ - BEFORE, - /** - * Corresponds to the {@code loadAfter} calls in {@link androidx.paging.DataSource} or - * {@code onItemAtEndLoaded} in - * {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}. - */ - AFTER - } - class RequestQueue { - @NonNull - final RequestType mRequestType; - @Nullable - RequestWrapper mFailed; - @Nullable - Request mRunning; - @Nullable - Throwable mLastError; - @NonNull - Status mStatus = Status.SUCCESS; - RequestQueue(@NonNull RequestType requestType) { - mRequestType = requestType; - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/util/getErrorMessage.kt b/app/src/main/java/com/keylesspalace/tusky/util/getErrorMessage.kt deleted file mode 100644 index b003cb2d5..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/util/getErrorMessage.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.keylesspalace.tusky.util - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData - -private fun getErrorMessage(report: PagingRequestHelper.StatusReport): String { - return PagingRequestHelper.RequestType.values().mapNotNull { - report.getErrorFor(it)?.message - }.first() -} - -fun PagingRequestHelper.createStatusLiveData(): LiveData { - val liveData = MutableLiveData() - addListener { report -> - when { - report.hasRunning() -> liveData.postValue(NetworkState.LOADING) - report.hasError() -> liveData.postValue( - NetworkState.error(getErrorMessage(report))) - else -> liveData.postValue(NetworkState.LOADED) - } - } - return liveData -} \ No newline at end of file From bb0f529127463185e86c91c95a629aff27688bc0 Mon Sep 17 00:00:00 2001 From: XoseM Date: Mon, 21 Jun 2021 02:17:56 +0000 Subject: [PATCH 80/92] Translated using Weblate (Galician) Currently translated at 100.0% (460 of 460 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/gl/ --- app/src/main/res/values-gl/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 150a40785..d63b34135 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -508,4 +508,6 @@ Programar Toot Teclado Emoji Aínda que a túa conta non está bloqueada, a administración de %1$s opina que debes revisar manualmente as peticións de seguimento destas contas. + Eliminar esta conversa\? + Eliminar conversa \ No newline at end of file From cee22e70d1a81a3e72144f37835eb902a40094bb Mon Sep 17 00:00:00 2001 From: Vancha Date: Mon, 21 Jun 2021 02:17:56 +0000 Subject: [PATCH 81/92] Translated using Weblate (Dutch) Currently translated at 99.5% (458 of 460 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/nl/ --- app/src/main/res/values-nl/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index af3a53f8a..e4e965ea7 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -515,4 +515,6 @@ Laden van reactie-informatie mislukt Kwantitatieve statistieken in profielen verbergen Hoofd navigatiepositie + Dit gesprek verwijderen\? + Gesprek verwijderen \ No newline at end of file From 70681cbad9f5508792d35b9369d45a2ac0d108c4 Mon Sep 17 00:00:00 2001 From: Daniele Lira Mereb Date: Mon, 21 Jun 2021 02:17:56 +0000 Subject: [PATCH 82/92] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (460 of 460 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/pt_BR/ Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (460 of 460 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/pt_BR/ --- app/src/main/res/values-pt-rBR/strings.xml | 40 +++++++++++++--------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 685049d0b..6d1753dec 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -58,7 +58,7 @@ Compor Entrar com Mastodon Sair - Você tem certeza de que deseja sair da conta %1$s\? + Tem certeza de que deseja sair da conta %1$s\? Seguir Deixar de seguir Bloquear @@ -131,9 +131,9 @@ Conectando… O domínio de qualquer instância pode ser inserido aqui, como mastodon.social, masto.donte.com.br, colorid.es ou qualquer outro! \n -\n Se você não tem uma conta ainda, você pode inserir o nome da instância a qual você gostaria de participar e criar uma conta lá. +\n Se não tem uma conta ainda, insira o nome da instância que gostaria de participar e crie uma conta lá. \n -\n Uma instância é um lugar onde sua conta é hospedada, mas você pode facilmente se comunicar e seguir pessoas de outras instâncias como se vocês estivessem no mesmo site. +\n Uma instância é um lugar onde sua conta é hospedada, mas é fácil se comunicar e seguir pessoas de outras instâncias como se todos estivessem no mesmo site. \n \n Mais informações podem ser encontradas em joinmastodon.org. Envio de mídia terminando @@ -203,14 +203,13 @@ %1$s, %2$s, e %3$s %1$s e %2$s + %d nova interação %d novas interações - Conta trancada + Perfil trancado Sobre Tusky %s - Tusky é um software livre e de código aberto. - Ele é licenciado sob a versão 3 da Licença Pública Geral GNU. - Você pode ler a licença aqui: https://www.gnu.org/licenses/gpl-3.0.pt-br.html + Tusky é um software livre e de código aberto. Ele é licenciado sob a versão 3 da Licença Pública Geral GNU. Leia a licença aqui: https://www.gnu.org/licenses/gpl-3.0.pt-br.html %1$s • %2$s @@ -382,19 +382,19 @@ Sua enquete terminou %d dia restante - %d dias restante + %d dias restantes %d hora restante - %d horas restante + %d horas restantes %d minuto restante - %d minutos restante + %d minutos restantes %d segundo restante - %d segundos restante + %d segundos restantes Reproduzir GIFs Enquete com as opções: %1$s, %2$s, %3$s, %4$s; %5$s @@ -407,13 +407,13 @@ Encaminhar para %s Erro ao denunciar Erro ao carregar toots - A denúncia será enviada aos moderadores da instância. Você pode explicar por que você denunciou a conta: + A denúncia será enviada aos moderadores da instância. Explique por que denunciou a conta: A conta está em outra instância. Enviar uma cópia anônima da denúncia para lá\? Instâncias bloqueadas Instâncias bloqueadas Bloquear %s %s desbloqueada - Você tem certeza de que deseja bloquear tudo de %s\? Você não verá mais o conteúdo desta instância em nenhuma linha do tempo pública ou nas suas notificações. Seus seguidores desta instância serão removidos. + Tem certeza de que deseja bloquear tudo de %s\? Você não verá mais o conteúdo desta instância em nenhuma linha do tempo pública ou nas suas notificações. Seus seguidores desta instância serão removidos. Bloquear instância Mostrar filtro de notificações Toda palavra @@ -485,6 +485,7 @@ O toot em que se rascunhou uma resposta foi excluído Rascunho excluído + Não é possível anexar mais de %1$d arquivo de mídia. Não é possível anexar mais de %1$d arquivos de mídia. Ocultar status dos perfis @@ -499,7 +500,7 @@ \n \nNotificações push não serão afetadas, mas é possível revisar sua preferência manualmente. Salvo! - Nota pessoal sobre esta conta aqui + Nota pessoal sobre este perfil aqui Bem-estar Sem comunicados. Indefinido @@ -509,4 +510,11 @@ Novos toots %s recém tootou Comunicados + Apesar do seu perfil não ser trancado, %1$s exige que você revise a solicitação para te seguir destes perfis manualmente. + Cancelar + Notificar + Animar emojis personalizados + Excluir esta conversa\? + Excluir conversa + Deseja excluir a lista %s\? \ No newline at end of file From 3fa209926171eadc0ebf4ed4446e706eaf17f8ac Mon Sep 17 00:00:00 2001 From: Vegard Skjefstad Date: Mon, 21 Jun 2021 02:17:56 +0000 Subject: [PATCH 83/92] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegian?= =?UTF-8?q?=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (460 of 460 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/nb_NO/ --- app/src/main/res/values-no-rNB/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/res/values-no-rNB/strings.xml b/app/src/main/res/values-no-rNB/strings.xml index 40a7607c4..ac123ac25 100644 --- a/app/src/main/res/values-no-rNB/strings.xml +++ b/app/src/main/res/values-no-rNB/strings.xml @@ -508,4 +508,6 @@ Avslutt abonnementet Abonner Selv om kontoen din ikke er låst, har %1$s administratorer markert disse følgeforespørsler for manuell godkjenning. + Slette denne samtalen\? + Slett samtale \ No newline at end of file From a3aaef75f75da6f88fd1b71b2f1a48b61bd63197 Mon Sep 17 00:00:00 2001 From: Connyduck Date: Mon, 21 Jun 2021 02:17:56 +0000 Subject: [PATCH 84/92] Added translation using Weblate (Finnish) --- app/src/main/res/values-fi/strings.xml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 app/src/main/res/values-fi/strings.xml diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml new file mode 100644 index 000000000..a6b3daec9 --- /dev/null +++ b/app/src/main/res/values-fi/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file From 6d09544d70e3a6c8043c20cca34625804b435012 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Mon, 21 Jun 2021 02:17:57 +0000 Subject: [PATCH 85/92] Translated using Weblate (Ukrainian) Currently translated at 100.0% (460 of 460 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/uk/ --- app/src/main/res/values-uk/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 3756c73f8..37f2ab257 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -530,4 +530,6 @@ Поточний набір емодзі Google Стандартний набір емодзі Mastodon Навіть попри те, що ваш обліковий запис загальнодоступний, співробітники %1$s вважають, що ви, можливо, захочете переглянути запити від цих облікових записів власноруч. + Видалити цю бесіду\? + Видалити бесіду \ No newline at end of file From 4173750cbf284099489943df057df5a82d077a20 Mon Sep 17 00:00:00 2001 From: majava Date: Mon, 21 Jun 2021 02:17:57 +0000 Subject: [PATCH 86/92] Translated using Weblate (Finnish) Currently translated at 33.4% (154 of 460 strings) Translation: Tusky/Tusky Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/fi/ --- app/src/main/res/values-fi/strings.xml | 170 ++++++++++++++++++++++++- 1 file changed, 169 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index a6b3daec9..3638aeafc 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -1,2 +1,170 @@ - \ No newline at end of file + + Animoi mukautetut emojit + Animoi GIF-avatarit + Seuraa laitteen teemaa + Lopeta tilin seuraaminen\? + Poista tuuttaus\? + Mikä on instanssi\? + Kopioi linkki + Avaa selaimessa + Kirjaudu Mastodonilla + Muokkaa profiilia + Luonnos poistettu + Vaihtoehto %d + Monivalinta + Lisää vaihtoehto + 7 päivää + 3 päivää + 1 päivä + 6 tuntia + 1 tunti + 30 minuuttia + 5 minuuttia + Lisää hashtag + Ei kuvausta + CC-BY-SA 4.0 + CC-BY 4.0 + Lataus epäonnistui + Avaa tuuttaus + Järjestelmän oletus + Emojien tyyli + Lähetetään tuuttausta… + Tallennetaanko luonnoksena\? + Lukitse tili + Lisää tili + Seuraa sinua + Tuskyn profiili + Tusky %s + Lukittu tili + Uusia tuuttauksia + Seuraamispyynnöt + Uusia seuraajia + Uusia mainintoja + HTTP-välityspalvelin + Näytä vastaukset + Teema + Ei tuloksia + Sisältövaroitus + Mitä tapahtuu\? + Mikä instanssi\? + Ladataan mediaa + Lataa media + Näytä suosikit + Lisää välilehti + Ajasta tuuttaus + Emoji-näppäimistö + Sisältövaroitus + Ajastetut tuuttaukset + Muokkaa profiilia + Piilota media + Ota kuva + Lisää kysely + Lisää media + Seuraamispyynnöt + Estetyt tilit + Tiliasetukset + Kirjaudu ulos + Näytä vähemmän + Näytä lisää + Media piilotettu + Seuraamispyynnöt + Estetyt tilit + Mykistetyt tilit + Viestit + Tallennettu! + Muokkaa + Kysely + Tilit + Valmis + Takaisin + Jatka + Äänestä + suljettu + Suodatin + Lista + Hastagit + Julkinen + Kiinnitä + Poista kiinnitys + Botti + Poista + Listat + Listat + Päivitä + Poista + Audio + Video + Kuvat + Tietoja + Välityspalvelin + Vain seuraajat + Julkinen + Välilehdet + Kieli + Selain + Musta + Vaalea + Tumma + Suodattimet + Aikajanat + seurasi + mainitsi + Ilmoitukset + Ilmoitukset + Lataa + Profiilikuva + Vastaa… + Hae… + Kuvaus + Linkit + Maininnat + Hastagit + Hastagit + Maininnat + Linkit + Nollaa + Luonnokset + Hae + Älä hyväksy + Hyväksy + Peruuta + Muokkaa + Tallenna + Mainitse + Poista mykistys + Mykistä + Jaa + Media + Kirjanmerkit + Suosikit + Asetukset + Profiili + Sulje + Yritä uudelleen + TUUTTAUS + Poista + Muokkaa + Ilmianna + Poista esto + Estä + Seurataan + Seuraa + TUUTAA! + Tuuttaus + Ajastetut tuuttaukset + Vastaa + \@%s + Lisenssit + Luonnokset + Suosikit + Kirjanmerkit + Seuraajat + Seurataan + Kiinnitetty + Julkaisut + Välilehdet + Paikallinen + Ilmoitukset + Koti + \ No newline at end of file From 74a2943032bf0c587c99eccced6e208543be2612 Mon Sep 17 00:00:00 2001 From: Daniele Lira Mereb Date: Sat, 19 Jun 2021 02:44:42 +0000 Subject: [PATCH 87/92] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (14 of 14 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/pt_BR/ --- fastlane/metadata/android/pt-BR/changelogs/77.txt | 10 ++++++++++ fastlane/metadata/android/pt-BR/changelogs/80.txt | 7 +++++++ fastlane/metadata/android/pt-BR/changelogs/82.txt | 5 +++++ fastlane/metadata/android/pt-BR/changelogs/83.txt | 3 +++ 4 files changed, 25 insertions(+) create mode 100644 fastlane/metadata/android/pt-BR/changelogs/77.txt create mode 100644 fastlane/metadata/android/pt-BR/changelogs/80.txt create mode 100644 fastlane/metadata/android/pt-BR/changelogs/82.txt create mode 100644 fastlane/metadata/android/pt-BR/changelogs/83.txt diff --git a/fastlane/metadata/android/pt-BR/changelogs/77.txt b/fastlane/metadata/android/pt-BR/changelogs/77.txt new file mode 100644 index 000000000..48cb81de9 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/77.txt @@ -0,0 +1,10 @@ +Tusky v13.0 + +- Suporte à função de nota pessoal sobre o perfil (novidade do Mastodon 3.2.0) +- Suporte à função de Comunicados da administração (novidade do Mastodon 3.1.0) + +- O avatar de sua conta selecionada agora ficará visível no cantinho da barra de títulos +- Tocar no nome de exibição na linha do tempo abrirá o perfil em questão + +- O desenvolvedor pegou o mata-moscas e fez um monte de pequenas melhorias e correções +- Tradução atualizada diff --git a/fastlane/metadata/android/pt-BR/changelogs/80.txt b/fastlane/metadata/android/pt-BR/changelogs/80.txt new file mode 100644 index 000000000..7ffefce5f --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/80.txt @@ -0,0 +1,7 @@ +Tusky v14.0 + +- Receba notificação quando a pessoa amada tootar - toca no sininho e aproveita! (novidade do Mastodon 3.3.0) +- Rascunhos no Tusky foi completamente redesenhado e agora está mais chique! +- Foi adicionado funções de bem-estar que permite limitar certas coisinhas no Tusky. +- Tusky consegue animar os emojis personalizados +Para ver mais: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/pt-BR/changelogs/82.txt b/fastlane/metadata/android/pt-BR/changelogs/82.txt new file mode 100644 index 000000000..5451dee94 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/82.txt @@ -0,0 +1,5 @@ +Tusky v15.0 + +- Seguidores Pendentes agora ficará fixo no menu principal! +- O relógio para agendar toots ficou mais bonitinho e combina melhor com o resto do Tusky +Para ver mais: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/pt-BR/changelogs/83.txt b/fastlane/metadata/android/pt-BR/changelogs/83.txt new file mode 100644 index 000000000..e49556d05 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/83.txt @@ -0,0 +1,3 @@ +Tusky v15.1 + +Esta atualização corrige aquele inconveniente ao descrever imagens From f6dd131b95f1b91cceea7765e38b6731d562b295 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Thu, 24 Jun 2021 21:23:29 +0200 Subject: [PATCH 88/92] migrate drafts to paging 3 (#2206) * migrate drafts to paging 3 * migrate DraftHelper to coroutines --- .../com/keylesspalace/tusky/MainActivity.kt | 513 +++++++++--------- .../components/compose/ComposeViewModel.kt | 28 +- .../tusky/components/drafts/DraftHelper.kt | 53 +- .../tusky/components/drafts/DraftsActivity.kt | 22 +- .../tusky/components/drafts/DraftsAdapter.kt | 7 +- .../components/drafts/DraftsViewModel.kt | 33 +- .../com/keylesspalace/tusky/db/DraftDao.kt | 14 +- .../tusky/service/SendTootService.kt | 24 +- 8 files changed, 357 insertions(+), 337 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt index 96eb1e34c..bb0ac6589 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt @@ -35,6 +35,7 @@ import androidx.core.content.pm.ShortcutManagerCompat import androidx.emoji.text.EmojiCompat import androidx.emoji.text.EmojiCompat.InitCallback import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceManager import androidx.viewpager2.widget.MarginPageTransformer import autodispose2.androidx.lifecycle.autoDispose @@ -61,7 +62,6 @@ import com.keylesspalace.tusky.components.search.SearchActivity import com.keylesspalace.tusky.databinding.ActivityMainBinding import com.keylesspalace.tusky.db.AccountEntity import com.keylesspalace.tusky.entity.Account -import com.keylesspalace.tusky.fragment.SFragment import com.keylesspalace.tusky.interfaces.AccountSelectionListener import com.keylesspalace.tusky.interfaces.ActionButtonActivity import com.keylesspalace.tusky.interfaces.ReselectableFragment @@ -84,6 +84,7 @@ import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.schedulers.Schedulers +import kotlinx.coroutines.launch import javax.inject.Inject class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInjector { @@ -218,18 +219,18 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje NotificationHelper.disablePullNotifications(this) } eventHub.events - .observeOn(AndroidSchedulers.mainThread()) - .autoDispose(this, Lifecycle.Event.ON_DESTROY) - .subscribe { event: Event? -> - when (event) { - is ProfileEditedEvent -> onFetchUserInfoSuccess(event.newProfileData) - is MainTabsChangedEvent -> setupTabs(false) - is AnnouncementReadEvent -> { - unreadAnnouncementsCount-- - updateAnnouncementsBadge() - } + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this, Lifecycle.Event.ON_DESTROY) + .subscribe { event: Event? -> + when (event) { + is ProfileEditedEvent -> onFetchUserInfoSuccess(event.newProfileData) + is MainTabsChangedEvent -> setupTabs(false) + is AnnouncementReadEvent -> { + unreadAnnouncementsCount-- + updateAnnouncementsBadge() } } + } Schedulers.io().scheduleDirect { // Flush old media that was cached for sharing @@ -341,13 +342,13 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje override fun set(imageView: ImageView, uri: Uri, placeholder: Drawable, tag: String?) { if (animateAvatars) { glide.load(uri) - .placeholder(placeholder) - .into(imageView) + .placeholder(placeholder) + .into(imageView) } else { glide.asBitmap() - .load(uri) - .placeholder(placeholder) - .into(imageView) + .load(uri) + .placeholder(placeholder) + .into(imageView) } } @@ -367,114 +368,114 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje binding.mainDrawer.apply { tintStatusBar = true addItems( - primaryDrawerItem { - nameRes = R.string.action_edit_profile - iconicsIcon = GoogleMaterial.Icon.gmd_person - onClick = { - val intent = Intent(context, EditProfileActivity::class.java) - startActivityWithSlideInAnimation(intent) - } - }, - primaryDrawerItem { - nameRes = R.string.action_view_favourites - isSelectable = false - iconicsIcon = GoogleMaterial.Icon.gmd_star - onClick = { - val intent = StatusListActivity.newFavouritesIntent(context) - startActivityWithSlideInAnimation(intent) - } - }, - primaryDrawerItem { - nameRes = R.string.action_view_bookmarks - iconicsIcon = GoogleMaterial.Icon.gmd_bookmark - onClick = { - val intent = StatusListActivity.newBookmarksIntent(context) - startActivityWithSlideInAnimation(intent) - } - }, - primaryDrawerItem { - nameRes = R.string.action_view_follow_requests - iconicsIcon = GoogleMaterial.Icon.gmd_person_add - onClick = { - val intent = AccountListActivity.newIntent(context, AccountListActivity.Type.FOLLOW_REQUESTS, accountLocked = accountLocked) - startActivityWithSlideInAnimation(intent) - } - }, - primaryDrawerItem { - nameRes = R.string.action_lists - iconicsIcon = GoogleMaterial.Icon.gmd_list - onClick = { - startActivityWithSlideInAnimation(ListsActivity.newIntent(context)) - } - }, - primaryDrawerItem { - nameRes = R.string.action_access_drafts - iconRes = R.drawable.ic_notebook - onClick = { - val intent = DraftsActivity.newIntent(context) - startActivityWithSlideInAnimation(intent) - } - }, - primaryDrawerItem { - nameRes = R.string.action_access_scheduled_toot - iconRes = R.drawable.ic_access_time - onClick = { - startActivityWithSlideInAnimation(ScheduledTootActivity.newIntent(context)) - } - }, - primaryDrawerItem { - identifier = DRAWER_ITEM_ANNOUNCEMENTS - nameRes = R.string.title_announcements - iconRes = R.drawable.ic_bullhorn_24dp - onClick = { - startActivityWithSlideInAnimation(AnnouncementsActivity.newIntent(context)) - } - badgeStyle = BadgeStyle().apply { - textColor = ColorHolder.fromColor(ThemeUtils.getColor(this@MainActivity, R.attr.colorOnPrimary)) - color = ColorHolder.fromColor(ThemeUtils.getColor(this@MainActivity, R.attr.colorPrimary)) - } - }, - DividerDrawerItem(), - secondaryDrawerItem { - nameRes = R.string.action_view_account_preferences - iconRes = R.drawable.ic_account_settings - onClick = { - val intent = PreferencesActivity.newIntent(context, PreferencesActivity.ACCOUNT_PREFERENCES) - startActivityWithSlideInAnimation(intent) - } - }, - secondaryDrawerItem { - nameRes = R.string.action_view_preferences - iconicsIcon = GoogleMaterial.Icon.gmd_settings - onClick = { - val intent = PreferencesActivity.newIntent(context, PreferencesActivity.GENERAL_PREFERENCES) - startActivityWithSlideInAnimation(intent) - } - }, - secondaryDrawerItem { - nameRes = R.string.about_title_activity - iconicsIcon = GoogleMaterial.Icon.gmd_info - onClick = { - val intent = Intent(context, AboutActivity::class.java) - startActivityWithSlideInAnimation(intent) - } - }, - secondaryDrawerItem { - nameRes = R.string.action_logout - iconRes = R.drawable.ic_logout - onClick = ::logout + primaryDrawerItem { + nameRes = R.string.action_edit_profile + iconicsIcon = GoogleMaterial.Icon.gmd_person + onClick = { + val intent = Intent(context, EditProfileActivity::class.java) + startActivityWithSlideInAnimation(intent) } + }, + primaryDrawerItem { + nameRes = R.string.action_view_favourites + isSelectable = false + iconicsIcon = GoogleMaterial.Icon.gmd_star + onClick = { + val intent = StatusListActivity.newFavouritesIntent(context) + startActivityWithSlideInAnimation(intent) + } + }, + primaryDrawerItem { + nameRes = R.string.action_view_bookmarks + iconicsIcon = GoogleMaterial.Icon.gmd_bookmark + onClick = { + val intent = StatusListActivity.newBookmarksIntent(context) + startActivityWithSlideInAnimation(intent) + } + }, + primaryDrawerItem { + nameRes = R.string.action_view_follow_requests + iconicsIcon = GoogleMaterial.Icon.gmd_person_add + onClick = { + val intent = AccountListActivity.newIntent(context, AccountListActivity.Type.FOLLOW_REQUESTS, accountLocked = accountLocked) + startActivityWithSlideInAnimation(intent) + } + }, + primaryDrawerItem { + nameRes = R.string.action_lists + iconicsIcon = GoogleMaterial.Icon.gmd_list + onClick = { + startActivityWithSlideInAnimation(ListsActivity.newIntent(context)) + } + }, + primaryDrawerItem { + nameRes = R.string.action_access_drafts + iconRes = R.drawable.ic_notebook + onClick = { + val intent = DraftsActivity.newIntent(context) + startActivityWithSlideInAnimation(intent) + } + }, + primaryDrawerItem { + nameRes = R.string.action_access_scheduled_toot + iconRes = R.drawable.ic_access_time + onClick = { + startActivityWithSlideInAnimation(ScheduledTootActivity.newIntent(context)) + } + }, + primaryDrawerItem { + identifier = DRAWER_ITEM_ANNOUNCEMENTS + nameRes = R.string.title_announcements + iconRes = R.drawable.ic_bullhorn_24dp + onClick = { + startActivityWithSlideInAnimation(AnnouncementsActivity.newIntent(context)) + } + badgeStyle = BadgeStyle().apply { + textColor = ColorHolder.fromColor(ThemeUtils.getColor(this@MainActivity, R.attr.colorOnPrimary)) + color = ColorHolder.fromColor(ThemeUtils.getColor(this@MainActivity, R.attr.colorPrimary)) + } + }, + DividerDrawerItem(), + secondaryDrawerItem { + nameRes = R.string.action_view_account_preferences + iconRes = R.drawable.ic_account_settings + onClick = { + val intent = PreferencesActivity.newIntent(context, PreferencesActivity.ACCOUNT_PREFERENCES) + startActivityWithSlideInAnimation(intent) + } + }, + secondaryDrawerItem { + nameRes = R.string.action_view_preferences + iconicsIcon = GoogleMaterial.Icon.gmd_settings + onClick = { + val intent = PreferencesActivity.newIntent(context, PreferencesActivity.GENERAL_PREFERENCES) + startActivityWithSlideInAnimation(intent) + } + }, + secondaryDrawerItem { + nameRes = R.string.about_title_activity + iconicsIcon = GoogleMaterial.Icon.gmd_info + onClick = { + val intent = Intent(context, AboutActivity::class.java) + startActivityWithSlideInAnimation(intent) + } + }, + secondaryDrawerItem { + nameRes = R.string.action_logout + iconRes = R.drawable.ic_logout + onClick = ::logout + } ) if (addSearchButton) { binding.mainDrawer.addItemsAtPosition(4, - primaryDrawerItem { - nameRes = R.string.action_search - iconicsIcon = GoogleMaterial.Icon.gmd_search - onClick = { - startActivityWithSlideInAnimation(SearchActivity.getIntent(context)) - } - }) + primaryDrawerItem { + nameRes = R.string.action_search + iconicsIcon = GoogleMaterial.Icon.gmd_search + onClick = { + startActivityWithSlideInAnimation(SearchActivity.getIntent(context)) + } + }) } setSavedInstance(savedInstanceState) @@ -482,11 +483,11 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje if (BuildConfig.DEBUG) { binding.mainDrawer.addItems( - secondaryDrawerItem { - nameText = "debug" - isEnabled = false - textColor = ColorStateList.valueOf(Color.GREEN) - } + secondaryDrawerItem { + nameText = "debug" + isEnabled = false + textColor = ColorStateList.valueOf(Color.GREEN) + } ) } EmojiCompat.get().registerInitCallback(emojiInitCallback) @@ -519,7 +520,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje activeTabLayout.removeAllTabs() for (i in tabs.indices) { val tab = activeTabLayout.newTab() - .setIcon(tabs[i].icon) + .setIcon(tabs[i].icon) if (tabs[i].id == LIST) { tab.contentDescription = tabs[i].arguments[1] } else { @@ -611,168 +612,174 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje private fun logout() { accountManager.activeAccount?.let { activeAccount -> AlertDialog.Builder(this) - .setTitle(R.string.action_logout) - .setMessage(getString(R.string.action_logout_confirm, activeAccount.fullName)) - .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> - NotificationHelper.deleteNotificationChannelsForAccount(activeAccount, this) + .setTitle(R.string.action_logout) + .setMessage(getString(R.string.action_logout_confirm, activeAccount.fullName)) + .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> + lifecycleScope.launch { + NotificationHelper.deleteNotificationChannelsForAccount(activeAccount, this@MainActivity) cacheUpdater.clearForUser(activeAccount.id) conversationRepository.deleteCacheForAccount(activeAccount.id) draftHelper.deleteAllDraftsAndAttachmentsForAccount(activeAccount.id) - removeShortcut(this, activeAccount) + removeShortcut(this@MainActivity, activeAccount) val newAccount = accountManager.logActiveAccountOut() - if (!NotificationHelper.areNotificationsEnabled(this, accountManager)) { - NotificationHelper.disablePullNotifications(this) + if (!NotificationHelper.areNotificationsEnabled( + this@MainActivity, + accountManager + ) + ) { + NotificationHelper.disablePullNotifications(this@MainActivity) } val intent = if (newAccount == null) { - LoginActivity.getIntent(this, false) + LoginActivity.getIntent(this@MainActivity, false) } else { - Intent(this, MainActivity::class.java) + Intent(this@MainActivity, MainActivity::class.java) } startActivity(intent) finishWithoutSlideOutAnimation() } - .setNegativeButton(android.R.string.cancel, null) - .show() - } - } - - private fun fetchUserInfo() { - mastodonApi.accountVerifyCredentials() - .observeOn(AndroidSchedulers.mainThread()) - .autoDispose(this, Lifecycle.Event.ON_DESTROY) - .subscribe( - { userInfo -> - onFetchUserInfoSuccess(userInfo) - }, - { throwable -> - Log.e(TAG, "Failed to fetch user info. " + throwable.message) - } - ) - } - - private fun onFetchUserInfoSuccess(me: Account) { - glide.asBitmap() - .load(me.header) - .into(header.accountHeaderBackground) - - loadDrawerAvatar(me.avatar, false) - - accountManager.updateActiveAccount(me) - NotificationHelper.createNotificationChannelsForAccount(accountManager.activeAccount!!, this) - - accountLocked = me.locked - - updateProfiles() - updateShortcut(this, accountManager.activeAccount!!) - } - - private fun loadDrawerAvatar(avatarUrl: String, showPlaceholder: Boolean) { - val navIconSize = resources.getDimensionPixelSize(R.dimen.avatar_toolbar_nav_icon_size) - - glide.asDrawable() - .load(avatarUrl) - .transform( - RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp)) - ) - .apply { - if (showPlaceholder) { - placeholder(R.drawable.avatar_default) } + .setNegativeButton(android.R.string.cancel, null) + .show() + } +} + +private fun fetchUserInfo() { + mastodonApi.accountVerifyCredentials() + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this, Lifecycle.Event.ON_DESTROY) + .subscribe( + { userInfo -> + onFetchUserInfoSuccess(userInfo) + }, + { throwable -> + Log.e(TAG, "Failed to fetch user info. " + throwable.message) } - .into(object : CustomTarget(navIconSize, navIconSize) { + ) +} - override fun onLoadStarted(placeholder: Drawable?) { - if (placeholder != null) { - binding.mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize) - } - } +private fun onFetchUserInfoSuccess(me: Account) { + glide.asBitmap() + .load(me.header) + .into(header.accountHeaderBackground) - override fun onResourceReady(resource: Drawable, transition: Transition?) { - binding.mainToolbar.navigationIcon = FixedSizeDrawable(resource, navIconSize, navIconSize) - } + loadDrawerAvatar(me.avatar, false) - override fun onLoadCleared(placeholder: Drawable?) { - if (placeholder != null) { - binding.mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize) - } - } - }) - } + accountManager.updateActiveAccount(me) + NotificationHelper.createNotificationChannelsForAccount(accountManager.activeAccount!!, this) - private fun fetchAnnouncements() { - mastodonApi.listAnnouncements(false) - .observeOn(AndroidSchedulers.mainThread()) - .autoDispose(this, Lifecycle.Event.ON_DESTROY) - .subscribe( - { announcements -> - unreadAnnouncementsCount = announcements.count { !it.read } - updateAnnouncementsBadge() - }, - { - Log.w(TAG, "Failed to fetch announcements.", it) - } - ) - } + accountLocked = me.locked - private fun updateAnnouncementsBadge() { - binding.mainDrawer.updateBadge(DRAWER_ITEM_ANNOUNCEMENTS, StringHolder(if (unreadAnnouncementsCount <= 0) null else unreadAnnouncementsCount.toString())) - } + updateProfiles() + updateShortcut(this, accountManager.activeAccount!!) +} - private fun updateProfiles() { - val animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) - val profiles: MutableList = accountManager.getAllAccountsOrderedByActive().map { acc -> - val emojifiedName = EmojiCompat.get().process(acc.displayName.emojify(acc.emojis, header, animateEmojis)) +private fun loadDrawerAvatar(avatarUrl: String, showPlaceholder: Boolean) { + val navIconSize = resources.getDimensionPixelSize(R.dimen.avatar_toolbar_nav_icon_size) - ProfileDrawerItem().apply { - isSelected = acc.isActive - nameText = emojifiedName - iconUrl = acc.profilePictureUrl - isNameShown = true - identifier = acc.id - descriptionText = acc.fullName - } - }.toMutableList() - - // reuse the already existing "add account" item - for (profile in header.profiles.orEmpty()) { - if (profile.identifier == DRAWER_ITEM_ADD_ACCOUNT) { - profiles.add(profile) - break + glide.asDrawable() + .load(avatarUrl) + .transform( + RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp)) + ) + .apply { + if (showPlaceholder) { + placeholder(R.drawable.avatar_default) } } - header.clear() - header.profiles = profiles - header.setActiveProfile(accountManager.activeAccount!!.id) + .into(object : CustomTarget(navIconSize, navIconSize) { + + override fun onLoadStarted(placeholder: Drawable?) { + if (placeholder != null) { + binding.mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize) + } + } + + override fun onResourceReady(resource: Drawable, transition: Transition?) { + binding.mainToolbar.navigationIcon = FixedSizeDrawable(resource, navIconSize, navIconSize) + } + + override fun onLoadCleared(placeholder: Drawable?) { + if (placeholder != null) { + binding.mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize) + } + } + }) +} + +private fun fetchAnnouncements() { + mastodonApi.listAnnouncements(false) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this, Lifecycle.Event.ON_DESTROY) + .subscribe( + { announcements -> + unreadAnnouncementsCount = announcements.count { !it.read } + updateAnnouncementsBadge() + }, + { + Log.w(TAG, "Failed to fetch announcements.", it) + } + ) +} + +private fun updateAnnouncementsBadge() { + binding.mainDrawer.updateBadge(DRAWER_ITEM_ANNOUNCEMENTS, StringHolder(if (unreadAnnouncementsCount <= 0) null else unreadAnnouncementsCount.toString())) +} + +private fun updateProfiles() { + val animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) + val profiles: MutableList = accountManager.getAllAccountsOrderedByActive().map { acc -> + val emojifiedName = EmojiCompat.get().process(acc.displayName.emojify(acc.emojis, header, animateEmojis)) + + ProfileDrawerItem().apply { + isSelected = acc.isActive + nameText = emojifiedName + iconUrl = acc.profilePictureUrl + isNameShown = true + identifier = acc.id + descriptionText = acc.fullName + } + }.toMutableList() + + // reuse the already existing "add account" item + for (profile in header.profiles.orEmpty()) { + if (profile.identifier == DRAWER_ITEM_ADD_ACCOUNT) { + profiles.add(profile) + break + } } + header.clear() + header.profiles = profiles + header.setActiveProfile(accountManager.activeAccount!!.id) +} - override fun getActionButton() = binding.composeButton +override fun getActionButton() = binding.composeButton - override fun androidInjector() = androidInjector +override fun androidInjector() = androidInjector - companion object { - private const val TAG = "MainActivity" // logging tag - private const val DRAWER_ITEM_ADD_ACCOUNT: Long = -13 - private const val DRAWER_ITEM_ANNOUNCEMENTS: Long = 14 - const val STATUS_URL = "statusUrl" - } +companion object { + private const val TAG = "MainActivity" // logging tag + private const val DRAWER_ITEM_ADD_ACCOUNT: Long = -13 + private const val DRAWER_ITEM_ANNOUNCEMENTS: Long = 14 + const val STATUS_URL = "statusUrl" +} } private inline fun primaryDrawerItem(block: PrimaryDrawerItem.() -> Unit): PrimaryDrawerItem { return PrimaryDrawerItem() - .apply { - isSelectable = false - isIconTinted = true - } - .apply(block) + .apply { + isSelectable = false + isIconTinted = true + } + .apply(block) } private inline fun secondaryDrawerItem(block: SecondaryDrawerItem.() -> Unit): SecondaryDrawerItem { return SecondaryDrawerItem() - .apply { - isSelectable = false - isIconTinted = true - } - .apply(block) + .apply { + isSelectable = false + isIconTinted = true + } + .apply(block) } private var AbstractDrawerItem<*, *>.onClick: () -> Unit diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt index 2e38fbe84..7fffa68d2 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt @@ -21,6 +21,7 @@ import androidx.core.net.toUri import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer +import androidx.lifecycle.viewModelScope import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia import com.keylesspalace.tusky.components.drafts.DraftHelper import com.keylesspalace.tusky.components.search.SearchType @@ -36,6 +37,7 @@ import com.keylesspalace.tusky.util.* import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.disposables.Disposable +import kotlinx.coroutines.launch import java.util.* import javax.inject.Inject @@ -214,22 +216,23 @@ class ComposeViewModel @Inject constructor( } fun deleteDraft() { - if (draftId != 0) { - draftHelper.deleteDraftAndAttachments(draftId) - .subscribe() + viewModelScope.launch { + if (draftId != 0) { + draftHelper.deleteDraftAndAttachments(draftId) + } } } fun saveDraft(content: String, contentWarning: String) { + viewModelScope.launch { + val mediaUris: MutableList = mutableListOf() + val mediaDescriptions: MutableList = mutableListOf() + media.value?.forEach { item -> + mediaUris.add(item.uri.toString()) + mediaDescriptions.add(item.description) + } - val mediaUris: MutableList = mutableListOf() - val mediaDescriptions: MutableList = mutableListOf() - media.value?.forEach { item -> - mediaUris.add(item.uri.toString()) - mediaDescriptions.add(item.description) - } - - draftHelper.saveDraft( + draftHelper.saveDraft( draftId = draftId, accountId = accountManager.activeAccount?.id!!, inReplyToId = inReplyToId, @@ -241,7 +244,8 @@ class ComposeViewModel @Inject constructor( mediaDescriptions = mediaDescriptions, poll = poll.value, failedToSend = false - ).subscribe() + ) + } } /** diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt index e9fe72383..82a7dae50 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt @@ -28,13 +28,12 @@ import com.keylesspalace.tusky.db.DraftEntity import com.keylesspalace.tusky.entity.NewPoll import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.util.IOUtils -import io.reactivex.rxjava3.core.Completable -import io.reactivex.rxjava3.core.Observable -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.schedulers.Schedulers +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import java.io.File import java.text.SimpleDateFormat -import java.util.* +import java.util.Date +import java.util.Locale import javax.inject.Inject class DraftHelper @Inject constructor( @@ -44,7 +43,7 @@ class DraftHelper @Inject constructor( private val draftDao = db.draftDao() - fun saveDraft( + suspend fun saveDraft( draftId: Int, accountId: Long, inReplyToId: String?, @@ -56,9 +55,7 @@ class DraftHelper @Inject constructor( mediaDescriptions: List, poll: NewPoll?, failedToSend: Boolean - ): Completable { - return Single.fromCallable { - + ) = withContext(Dispatchers.IO) { val externalFilesDir = context.getExternalFilesDir("Tusky") if (externalFilesDir == null || !(externalFilesDir.exists())) { @@ -103,7 +100,7 @@ class DraftHelper @Inject constructor( ) } - DraftEntity( + val draft = DraftEntity( id = draftId, accountId = accountId, inReplyToId = inReplyToId, @@ -116,42 +113,34 @@ class DraftHelper @Inject constructor( failedToSend = failedToSend ) - }.flatMapCompletable { draft -> draftDao.insertOrReplace(draft) - }.subscribeOn(Schedulers.io()) } - fun deleteDraftAndAttachments(draftId: Int): Completable { - return draftDao.find(draftId) - .flatMapCompletable { draft -> - draft?.let { - deleteDraftAndAttachments(it) - } - } + suspend fun deleteDraftAndAttachments(draftId: Int) { + draftDao.find(draftId)?.let { draft -> + deleteDraftAndAttachments(draft) + } } - fun deleteDraftAndAttachments(draft: DraftEntity): Completable { - return deleteAttachments(draft) - .andThen(draftDao.delete(draft.id)) + suspend fun deleteDraftAndAttachments(draft: DraftEntity) { + deleteAttachments(draft) + draftDao.delete(draft.id) } - fun deleteAllDraftsAndAttachmentsForAccount(accountId: Long) { - draftDao.loadDraftsSingle(accountId) - .flatMapObservable { Observable.fromIterable(it) } - .flatMapCompletable { draft -> - deleteDraftAndAttachments(draft) - }.subscribeOn(Schedulers.io()) - .subscribe() + suspend fun deleteAllDraftsAndAttachmentsForAccount(accountId: Long) { + draftDao.loadDrafts(accountId).forEach { draft -> + deleteDraftAndAttachments(draft) + } } - fun deleteAttachments(draft: DraftEntity): Completable { - return Completable.fromCallable { + suspend fun deleteAttachments(draft: DraftEntity) { + withContext(Dispatchers.IO) { draft.attachments.forEach { attachment -> if (context.contentResolver.delete(attachment.uri, null, null) == 0) { Log.e("DraftHelper", "Did not delete file ${attachment.uriString}") } } - }.subscribeOn(Schedulers.io()) + } } private fun Uri.isNotInFolder(folder: File): Boolean { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt index e70050160..8ca00491a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt @@ -22,6 +22,7 @@ import android.util.Log import android.widget.LinearLayout import android.widget.Toast import androidx.activity.viewModels +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from @@ -34,9 +35,10 @@ import com.keylesspalace.tusky.components.compose.ComposeActivity import com.keylesspalace.tusky.databinding.ActivityDraftsBinding import com.keylesspalace.tusky.db.DraftEntity import com.keylesspalace.tusky.di.ViewModelFactory -import com.keylesspalace.tusky.util.hide -import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.visible import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch import retrofit2.HttpException import javax.inject.Inject @@ -51,7 +53,6 @@ class DraftsActivity : BaseActivity(), DraftActionListener { private lateinit var bottomSheet: BottomSheetBehavior override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) binding = ActivityDraftsBinding.inflate(layoutInflater) @@ -74,16 +75,15 @@ class DraftsActivity : BaseActivity(), DraftActionListener { bottomSheet = BottomSheetBehavior.from(binding.bottomSheet.root) - viewModel.drafts.observe(this) { draftList -> - if (draftList.isEmpty()) { - binding.draftsRecyclerView.hide() - binding.draftsErrorMessageView.show() - } else { - binding.draftsRecyclerView.show() - binding.draftsErrorMessageView.hide() - adapter.submitList(draftList) + lifecycleScope.launch { + viewModel.drafts.collectLatest { draftData -> + adapter.submitData(draftData) } } + + adapter.addLoadStateListener { + binding.draftsErrorMessageView.visible(adapter.itemCount == 0) + } } override fun onOpenDraft(draft: DraftEntity) { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsAdapter.kt index 5ba3716eb..42c93b0b0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsAdapter.kt @@ -17,7 +17,7 @@ package com.keylesspalace.tusky.components.drafts import android.view.LayoutInflater import android.view.ViewGroup -import androidx.paging.PagedListAdapter +import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -35,7 +35,7 @@ interface DraftActionListener { class DraftsAdapter( private val listener: DraftActionListener -) : PagedListAdapter>( +) : PagingDataAdapter>( object : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: DraftEntity, newItem: DraftEntity): Boolean { return oldItem.id == newItem.id @@ -87,6 +87,5 @@ class DraftsAdapter( holder.binding.draftPoll.hide() } } - } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsViewModel.kt index aaf878153..78853d1e5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsViewModel.kt @@ -16,13 +16,17 @@ package com.keylesspalace.tusky.components.drafts import androidx.lifecycle.ViewModel -import androidx.paging.toLiveData +import androidx.lifecycle.viewModelScope +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.cachedIn import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.db.DraftEntity import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.network.MastodonApi import io.reactivex.rxjava3.core.Single +import kotlinx.coroutines.launch import javax.inject.Inject class DraftsViewModel @Inject constructor( @@ -32,22 +36,28 @@ class DraftsViewModel @Inject constructor( val draftHelper: DraftHelper ) : ViewModel() { - val drafts = database.draftDao().loadDrafts(accountManager.activeAccount?.id!!).toLiveData(pageSize = 20) + val drafts = Pager( + config = PagingConfig(pageSize = 20), + pagingSourceFactory = { database.draftDao().draftsPagingSource(accountManager.activeAccount?.id!!) } + ).flow + .cachedIn(viewModelScope) private val deletedDrafts: MutableList = mutableListOf() fun deleteDraft(draft: DraftEntity) { // this does not immediately delete media files to avoid unnecessary file operations // in case the user decides to restore the draft - database.draftDao().delete(draft.id) - .subscribe() - deletedDrafts.add(draft) + viewModelScope.launch { + database.draftDao().delete(draft.id) + deletedDrafts.add(draft) + } } fun restoreDraft(draft: DraftEntity) { - database.draftDao().insertOrReplace(draft) - .subscribe() - deletedDrafts.remove(draft) + viewModelScope.launch { + database.draftDao().insertOrReplace(draft) + deletedDrafts.remove(draft) + } } fun getToot(tootId: String): Single { @@ -55,9 +65,10 @@ class DraftsViewModel @Inject constructor( } override fun onCleared() { - deletedDrafts.forEach { - draftHelper.deleteAttachments(it).subscribe() + viewModelScope.launch { + deletedDrafts.forEach { + draftHelper.deleteAttachments(it) + } } } - } diff --git a/app/src/main/java/com/keylesspalace/tusky/db/DraftDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/DraftDao.kt index 5e6f21b4c..88f683d8a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/DraftDao.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/DraftDao.kt @@ -15,30 +15,28 @@ package com.keylesspalace.tusky.db -import androidx.paging.DataSource +import androidx.paging.PagingSource import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query -import io.reactivex.rxjava3.core.Completable -import io.reactivex.rxjava3.core.Single @Dao interface DraftDao { @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insertOrReplace(draft: DraftEntity): Completable + suspend fun insertOrReplace(draft: DraftEntity) @Query("SELECT * FROM DraftEntity WHERE accountId = :accountId ORDER BY id ASC") - fun loadDrafts(accountId: Long): DataSource.Factory + fun draftsPagingSource(accountId: Long): PagingSource @Query("SELECT * FROM DraftEntity WHERE accountId = :accountId") - fun loadDraftsSingle(accountId: Long): Single> + suspend fun loadDrafts(accountId: Long): List @Query("DELETE FROM DraftEntity WHERE id = :id") - fun delete(id: Int): Completable + suspend fun delete(id: Int) @Query("SELECT * FROM DraftEntity WHERE id = :id") - fun find(id: Int): Single + suspend fun find(id: Int): DraftEntity? } diff --git a/app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt b/app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt index 460c83e5e..20ae8f476 100644 --- a/app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt +++ b/app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt @@ -27,6 +27,10 @@ import com.keylesspalace.tusky.entity.NewStatus import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.network.MastodonApi import dagger.android.AndroidInjection +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize import retrofit2.Call import retrofit2.Callback @@ -49,6 +53,9 @@ class SendTootService : Service(), Injectable { @Inject lateinit var draftHelper: DraftHelper + private val supervisorJob = SupervisorJob() + private val serviceScope = CoroutineScope(Dispatchers.Main + supervisorJob) + private val tootsToSend = ConcurrentHashMap() private val sendCalls = ConcurrentHashMap>() @@ -148,7 +155,6 @@ class SendTootService : Service(), Injectable { newStatus ) - sendCalls[tootId] = sendCall val callback = object : Callback { @@ -160,8 +166,9 @@ class SendTootService : Service(), Injectable { if (response.isSuccessful) { // If the status was loaded from a draft, delete the draft and associated media files. if (tootToSend.draftId != 0) { - draftHelper.deleteDraftAndAttachments(tootToSend.draftId) - .subscribe() + serviceScope.launch { + draftHelper.deleteDraftAndAttachments(tootToSend.draftId) + } } if (scheduled) { @@ -244,8 +251,8 @@ class SendTootService : Service(), Injectable { } private fun saveTootToDrafts(toot: TootToSend) { - - draftHelper.saveDraft( + serviceScope.launch { + draftHelper.saveDraft( draftId = toot.draftId, accountId = toot.accountId, inReplyToId = toot.inReplyToId, @@ -257,7 +264,8 @@ class SendTootService : Service(), Injectable { mediaDescriptions = toot.mediaDescriptions, poll = toot.poll, failedToSend = true - ).subscribe() + ) + } } private fun cancelSendingIntent(tootId: Int): PendingIntent { @@ -269,6 +277,10 @@ class SendTootService : Service(), Injectable { return PendingIntent.getService(this, tootId, intent, PendingIntent.FLAG_UPDATE_CURRENT) } + override fun onDestroy() { + super.onDestroy() + supervisorJob.cancel() + } companion object { From 955267199ef53e70413bc22e2501ec75d097eb8a Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Thu, 24 Jun 2021 21:24:04 +0200 Subject: [PATCH 89/92] migrate scheduled toots to paging 3 (#2208) --- .../scheduled/ScheduledTootActivity.kt | 93 ++++++++-------- .../scheduled/ScheduledTootAdapter.kt | 4 +- .../scheduled/ScheduledTootDataSource.kt | 102 ------------------ .../scheduled/ScheduledTootPagingSource.kt | 79 ++++++++++++++ .../scheduled/ScheduledTootViewModel.kt | 60 ++++------- 5 files changed, 154 insertions(+), 184 deletions(-) delete mode 100644 app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootDataSource.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootPagingSource.kt diff --git a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt index 4af5771c1..0df191f81 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt @@ -19,18 +19,25 @@ import android.content.Context import android.content.Intent import android.os.Bundle import androidx.activity.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.paging.LoadState import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager +import autodispose2.androidx.lifecycle.autoDispose import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.StatusScheduledEvent import com.keylesspalace.tusky.components.compose.ComposeActivity import com.keylesspalace.tusky.databinding.ActivityScheduledTootBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.entity.ScheduledStatus -import com.keylesspalace.tusky.util.Status import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.show +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch import javax.inject.Inject class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injectable { @@ -38,6 +45,9 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec @Inject lateinit var viewModelFactory: ViewModelFactory + @Inject + lateinit var eventHub: EventHub + private val viewModel: ScheduledTootViewModel by viewModels { viewModelFactory } private val adapter = ScheduledTootAdapter(this) @@ -64,58 +74,58 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec binding.scheduledTootList.addItemDecoration(divider) binding.scheduledTootList.adapter = adapter - viewModel.data.observe(this) { - adapter.submitList(it) + lifecycleScope.launch { + viewModel.data.collectLatest { pagingData -> + adapter.submitData(pagingData) + } } - viewModel.networkState.observe(this) { (status) -> - when(status) { - Status.SUCCESS -> { - binding.progressBar.hide() - binding.swipeRefreshLayout.isRefreshing = false - if(viewModel.data.value?.loadedCount == 0) { - binding.errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_scheduled_status) - binding.errorMessageView.show() - } else { - binding.errorMessageView.hide() - } + adapter.addLoadStateListener { loadState -> + if (loadState.refresh is Error) { + binding.progressBar.hide() + binding.errorMessageView.setup(R.drawable.elephant_error, R.string.error_generic) { + refreshStatuses() } - Status.RUNNING -> { + binding.errorMessageView.show() + } + if (loadState.refresh != LoadState.Loading) { + binding.swipeRefreshLayout.isRefreshing = false + } + if (loadState.refresh is LoadState.NotLoading) { + binding.progressBar.hide() + if(adapter.itemCount == 0) { + binding.errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_scheduled_status) + binding.errorMessageView.show() + } else { binding.errorMessageView.hide() - if(viewModel.data.value?.loadedCount ?: 0 > 0) { - binding.swipeRefreshLayout.isRefreshing = true - } else { - binding.progressBar.show() - } - } - Status.FAILED -> { - if(viewModel.data.value?.loadedCount ?: 0 >= 0) { - binding.progressBar.hide() - binding.swipeRefreshLayout.isRefreshing = false - binding.errorMessageView.setup(R.drawable.elephant_error, R.string.error_generic) { - refreshStatuses() - } - binding.errorMessageView.show() - } } } } + + eventHub.events + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this) + .subscribe { event -> + if (event is StatusScheduledEvent) { + adapter.refresh() + } + } } private fun refreshStatuses() { - viewModel.reload() + adapter.refresh() } override fun edit(item: ScheduledStatus) { val intent = ComposeActivity.startIntent(this, ComposeActivity.ComposeOptions( - scheduledTootId = item.id, - tootText = item.params.text, - contentWarning = item.params.spoilerText, - mediaAttachments = item.mediaAttachments, - inReplyToId = item.params.inReplyToId, - visibility = item.params.visibility, - scheduledAt = item.scheduledAt, - sensitive = item.params.sensitive + scheduledTootId = item.id, + tootText = item.params.text, + contentWarning = item.params.spoilerText, + mediaAttachments = item.mediaAttachments, + inReplyToId = item.params.inReplyToId, + visibility = item.params.visibility, + scheduledAt = item.scheduledAt, + sensitive = item.params.sensitive )) startActivity(intent) } @@ -125,9 +135,6 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec } companion object { - @JvmStatic - fun newIntent(context: Context): Intent { - return Intent(context, ScheduledTootActivity::class.java) - } + fun newIntent(context: Context) = Intent(context, ScheduledTootActivity::class.java) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootAdapter.kt index 414130ddb..e21019ee7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootAdapter.kt @@ -18,7 +18,7 @@ package com.keylesspalace.tusky.components.scheduled import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.paging.PagedListAdapter +import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.DiffUtil import com.keylesspalace.tusky.databinding.ItemScheduledTootBinding import com.keylesspalace.tusky.entity.ScheduledStatus @@ -31,7 +31,7 @@ interface ScheduledTootActionListener { class ScheduledTootAdapter( val listener: ScheduledTootActionListener -) : PagedListAdapter>( +) : PagingDataAdapter>( object: DiffUtil.ItemCallback(){ override fun areItemsTheSame(oldItem: ScheduledStatus, newItem: ScheduledStatus): Boolean { return oldItem.id == newItem.id diff --git a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootDataSource.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootDataSource.kt deleted file mode 100644 index 09d05bcc5..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootDataSource.kt +++ /dev/null @@ -1,102 +0,0 @@ -/* Copyright 2019 Tusky Contributors - * - * This file is a part of Tusky. - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky; if not, - * see . */ - -package com.keylesspalace.tusky.components.scheduled - -import android.util.Log -import androidx.lifecycle.MutableLiveData -import androidx.paging.DataSource -import androidx.paging.ItemKeyedDataSource -import com.keylesspalace.tusky.entity.ScheduledStatus -import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.util.NetworkState -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.addTo - -class ScheduledTootDataSourceFactory( - private val mastodonApi: MastodonApi, - private val disposables: CompositeDisposable -): DataSource.Factory() { - - private val scheduledTootsCache = mutableListOf() - - private var dataSource: ScheduledTootDataSource? = null - - val networkState = MutableLiveData() - - override fun create(): DataSource { - return ScheduledTootDataSource(mastodonApi, disposables, scheduledTootsCache, networkState).also { - dataSource = it - } - } - - fun reload() { - scheduledTootsCache.clear() - dataSource?.invalidate() - } - - fun remove(status: ScheduledStatus) { - scheduledTootsCache.remove(status) - dataSource?.invalidate() - } - -} - - -class ScheduledTootDataSource( - private val mastodonApi: MastodonApi, - private val disposables: CompositeDisposable, - private val scheduledTootsCache: MutableList, - private val networkState: MutableLiveData -): ItemKeyedDataSource() { - override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback) { - if(scheduledTootsCache.isNotEmpty()) { - callback.onResult(scheduledTootsCache.toList()) - } else { - networkState.postValue(NetworkState.LOADING) - mastodonApi.scheduledStatuses(limit = params.requestedLoadSize) - .subscribe({ newData -> - scheduledTootsCache.addAll(newData) - callback.onResult(newData) - networkState.postValue(NetworkState.LOADED) - }, { throwable -> - Log.w("ScheduledTootDataSource", "Error loading scheduled statuses", throwable) - networkState.postValue(NetworkState.error(throwable.message)) - }) - .addTo(disposables) - } - } - - override fun loadAfter(params: LoadParams, callback: LoadCallback) { - mastodonApi.scheduledStatuses(limit = params.requestedLoadSize, maxId = params.key) - .subscribe({ newData -> - scheduledTootsCache.addAll(newData) - callback.onResult(newData) - }, { throwable -> - Log.w("ScheduledTootDataSource", "Error loading scheduled statuses", throwable) - networkState.postValue(NetworkState.error(throwable.message)) - }) - .addTo(disposables) - } - - override fun loadBefore(params: LoadParams, callback: LoadCallback) { - // we are always loading from beginning to end - } - - override fun getKey(item: ScheduledStatus): String { - return item.id - } - -} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootPagingSource.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootPagingSource.kt new file mode 100644 index 000000000..0578c5b12 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootPagingSource.kt @@ -0,0 +1,79 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.scheduled + +import android.util.Log +import androidx.paging.PagingSource +import androidx.paging.PagingState +import com.keylesspalace.tusky.entity.ScheduledStatus +import com.keylesspalace.tusky.network.MastodonApi +import kotlinx.coroutines.rx3.await + +class ScheduledTootPagingSourceFactory( + private val mastodonApi: MastodonApi +): () -> ScheduledTootPagingSource { + + private val scheduledTootsCache = mutableListOf() + + private var pagingSource: ScheduledTootPagingSource? = null + + override fun invoke(): ScheduledTootPagingSource { + return ScheduledTootPagingSource(mastodonApi, scheduledTootsCache).also { + pagingSource = it + } + } + + fun remove(status: ScheduledStatus) { + scheduledTootsCache.remove(status) + pagingSource?.invalidate() + } +} + +class ScheduledTootPagingSource( + private val mastodonApi: MastodonApi, + private val scheduledTootsCache: MutableList +): PagingSource() { + + override fun getRefreshKey(state: PagingState): String? { + return null + } + + override suspend fun load(params: LoadParams): LoadResult { + return if (params is LoadParams.Refresh && scheduledTootsCache.isNotEmpty()) { + LoadResult.Page( + data = scheduledTootsCache, + prevKey = null, + nextKey = scheduledTootsCache.lastOrNull()?.id + ) + } else { + try { + val result = mastodonApi.scheduledStatuses( + maxId = params.key, + limit = params.loadSize + ).await() + + LoadResult.Page( + data = result, + prevKey = null, + nextKey = result.lastOrNull()?.id + ) + } catch (e: Exception) { + Log.w("ScheduledTootPgngSrc", "Error loading scheduled statuses", e) + LoadResult.Error(e) + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootViewModel.kt index f07ca2b97..6890b15b3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootViewModel.kt @@ -16,53 +16,39 @@ package com.keylesspalace.tusky.components.scheduled import android.util.Log -import androidx.paging.Config -import androidx.paging.toLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.cachedIn import com.keylesspalace.tusky.appstore.EventHub -import com.keylesspalace.tusky.appstore.StatusScheduledEvent import com.keylesspalace.tusky.entity.ScheduledStatus import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.util.RxAwareViewModel -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import kotlinx.coroutines.launch +import kotlinx.coroutines.rx3.await import javax.inject.Inject class ScheduledTootViewModel @Inject constructor( val mastodonApi: MastodonApi, val eventHub: EventHub -): RxAwareViewModel() { +): ViewModel() { - private val dataSourceFactory = ScheduledTootDataSourceFactory(mastodonApi, disposables) + private val pagingSourceFactory = ScheduledTootPagingSourceFactory(mastodonApi) - val data = dataSourceFactory.toLiveData( - config = Config(pageSize = 20, initialLoadSizeHint = 20, enablePlaceholders = false) - ) - - val networkState = dataSourceFactory.networkState - - init { - eventHub.events - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { event -> - if (event is StatusScheduledEvent) { - reload() - } - } - .autoDispose() - } - - fun reload() { - dataSourceFactory.reload() - } + val data = Pager( + config = PagingConfig(pageSize = 20, initialLoadSize = 20), + pagingSourceFactory = pagingSourceFactory + ).flow + .cachedIn(viewModelScope) fun deleteScheduledStatus(status: ScheduledStatus) { - mastodonApi.deleteScheduledStatus(status.id) - .subscribe({ - dataSourceFactory.remove(status) - },{ throwable -> - Log.w("ScheduledTootViewModel", "Error deleting scheduled status", throwable) - }) - .autoDispose() - + viewModelScope.launch { + try { + mastodonApi.deleteScheduledStatus(status.id).await() + pagingSourceFactory.remove(status) + } catch (throwable: Throwable) { + Log.w("ScheduledTootViewModel", "Error deleting scheduled status", throwable) + } + } } - -} \ No newline at end of file +} From 16ffcca748a14c770088f5d9dcbebc0b5cccd27a Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Mon, 28 Jun 2021 21:13:24 +0200 Subject: [PATCH 90/92] add ktlint plugin to project and apply default code style (#2209) * add ktlint plugin to project and apply default code style * some manual adjustments, fix wildcard imports * update CONTRIBUTING.md * fix formatting --- CONTRIBUTING.md | 16 +- .../com/keylesspalace/tusky/MigrationsTest.kt | 26 +- .../keylesspalace/tusky/TimelineDAOTest.kt | 214 +++++------ .../com/keylesspalace/tusky/AboutActivity.kt | 4 +- .../keylesspalace/tusky/AccountActivity.kt | 131 ++++--- .../tusky/AccountListActivity.kt | 6 +- .../tusky/AccountsInListFragment.kt | 48 ++- .../tusky/BottomSheetActivity.kt | 35 +- .../tusky/EditProfileActivity.kt | 176 +++++---- .../keylesspalace/tusky/FiltersActivity.kt | 75 ++-- .../keylesspalace/tusky/LicenseActivity.kt | 3 +- .../com/keylesspalace/tusky/ListsActivity.kt | 121 +++--- .../com/keylesspalace/tusky/LoginActivity.kt | 89 ++--- .../tusky/ModalTimelineActivity.kt | 16 +- .../com/keylesspalace/tusky/SplashActivity.kt | 4 +- .../keylesspalace/tusky/StatusListActivity.kt | 23 +- .../java/com/keylesspalace/tusky/TabData.kt | 93 ++--- .../tusky/TabPreferenceActivity.kt | 84 ++--- .../keylesspalace/tusky/TuskyApplication.kt | 14 +- .../keylesspalace/tusky/ViewMediaActivity.kt | 78 ++-- .../tusky/adapter/AccountAdapter.kt | 3 +- .../tusky/adapter/AccountFieldAdapter.kt | 22 +- .../tusky/adapter/AccountFieldEditAdapter.kt | 10 +- .../tusky/adapter/AccountSelectionAdapter.kt | 6 +- .../tusky/adapter/BlocksAdapter.kt | 2 +- .../tusky/adapter/EmojiAdapter.kt | 14 +- .../tusky/adapter/FollowAdapter.kt | 3 +- .../tusky/adapter/FollowRequestViewHolder.kt | 9 +- .../tusky/adapter/FollowRequestsAdapter.kt | 4 +- .../adapter/FollowRequestsHeaderAdapter.kt | 3 +- .../tusky/adapter/LoadingFooterViewHolder.kt | 4 +- .../tusky/adapter/MutesAdapter.kt | 4 +- .../tusky/adapter/NetworkStateViewHolder.kt | 10 +- .../tusky/adapter/PlaceholderViewHolder.kt | 2 +- .../tusky/adapter/PollAdapter.kt | 29 +- .../adapter/PreviewPollOptionsAdapter.kt | 5 +- .../keylesspalace/tusky/adapter/TabAdapter.kt | 29 +- .../tusky/adapter/ThreadAdapter.kt | 6 +- .../tusky/appstore/CacheUpdater.kt | 16 +- .../keylesspalace/tusky/appstore/Events.kt | 6 +- .../keylesspalace/tusky/appstore/EventsHub.kt | 2 +- .../announcements/AnnouncementAdapter.kt | 44 +-- .../announcements/AnnouncementsActivity.kt | 19 +- .../announcements/AnnouncementsViewModel.kt | 241 ++++++------ .../components/compose/ComposeActivity.kt | 104 ++++-- .../components/compose/MediaPreviewAdapter.kt | 43 ++- .../tusky/components/compose/MediaUploader.kt | 79 ++-- .../compose/dialog/AddPollDialog.kt | 50 +-- .../compose/dialog/AddPollOptionsAdapter.kt | 12 +- .../compose/dialog/CaptionDialog.kt | 56 +-- .../compose/view/ComposeOptionsView.kt | 2 - .../components/compose/view/EditTextTyped.kt | 27 +- .../compose/view/PollPreviewView.kt | 13 +- .../components/compose/view/TootButton.kt | 13 +- .../conversation/ConversationEntity.kt | 9 +- .../ConversationLoadStateAdapter.kt | 1 - .../conversation/ConversationsFragment.kt | 9 +- .../conversation/ConversationsRepository.kt | 5 +- .../components/drafts/DraftMediaAdapter.kt | 39 +- .../tusky/components/drafts/DraftsActivity.kt | 58 +-- .../instancemute/InstanceListActivity.kt | 11 +- .../adapter/DomainMutesAdapter.kt | 4 +- .../fragment/InstanceListFragment.kt | 35 +- .../interfaces/InstanceActionListener.kt | 2 +- .../notifications/NotificationFetcher.kt | 20 +- .../notifications/NotificationWorker.kt | 14 +- .../components/notifications/Notifier.kt | 4 +- .../preference/AccountPreferencesFragment.kt | 93 +++-- .../components/preference/EmojiPreference.kt | 112 +++--- .../NotificationPreferencesFragment.kt | 3 +- .../preference/PreferencesActivity.kt | 53 ++- .../preference/PreferencesFragment.kt | 13 +- .../preference/ProxyPreferencesFragment.kt | 1 - .../tusky/components/report/ReportActivity.kt | 12 +- .../components/report/ReportViewModel.kt | 108 +++--- .../tusky/components/report/Screen.kt | 2 +- .../report/adapter/AdapterHandler.kt | 4 +- .../report/adapter/ReportPagerAdapter.kt | 2 +- .../report/adapter/StatusViewHolder.kt | 53 ++- .../report/adapter/StatusesAdapter.kt | 16 +- .../report/adapter/StatusesPagingSource.kt | 5 +- .../report/fragments/ReportDoneFragment.kt | 24 +- .../report/fragments/ReportNoteFragment.kt | 6 +- .../fragments/ReportStatusesFragment.kt | 25 +- .../report/model/StatusViewState.kt | 4 +- .../scheduled/ScheduledTootViewModel.kt | 6 +- .../tusky/components/search/SearchActivity.kt | 2 +- .../tusky/components/search/SearchType.kt | 2 +- .../components/search/SearchViewModel.kt | 16 +- .../search/adapter/SearchAccountsAdapter.kt | 10 +- .../search/adapter/SearchHashtagsAdapter.kt | 8 +- .../search/adapter/SearchPagerAdapter.kt | 3 +- .../search/adapter/SearchPagingSource.kt | 7 +- .../adapter/SearchPagingSourceFactory.kt | 2 +- .../fragments/SearchAccountsFragment.kt | 6 +- .../search/fragments/SearchFragment.kt | 7 +- .../fragments/SearchStatusesFragment.kt | 162 ++++---- .../components/timeline/TimelineFragment.kt | 48 ++- .../components/timeline/TimelineRepository.kt | 56 ++- .../components/timeline/TimelineViewModel.kt | 52 ++- .../com/keylesspalace/tusky/db/AccountDao.kt | 7 +- .../keylesspalace/tusky/db/AccountEntity.kt | 71 ++-- .../keylesspalace/tusky/db/AccountManager.kt | 12 +- .../tusky/db/ConversationsDao.kt | 3 +- .../com/keylesspalace/tusky/db/Converters.kt | 23 +- .../com/keylesspalace/tusky/db/DraftDao.kt | 1 - .../com/keylesspalace/tusky/db/DraftEntity.kt | 28 +- .../keylesspalace/tusky/db/InstanceEntity.kt | 12 +- .../com/keylesspalace/tusky/db/TimelineDao.kt | 65 ++-- .../tusky/db/TimelineStatusEntity.kt | 97 ++--- .../tusky/di/ActivitiesModule.kt | 18 +- .../keylesspalace/tusky/di/AppComponent.kt | 27 +- .../com/keylesspalace/tusky/di/AppInjector.kt | 19 +- .../com/keylesspalace/tusky/di/AppModule.kt | 37 +- .../tusky/di/BroadcastReceiverModule.kt | 8 +- .../tusky/di/FragmentBuildersModule.kt | 9 +- .../com/keylesspalace/tusky/di/Injectable.kt | 3 +- .../tusky/di/MediaUploaderModule.kt | 4 +- .../keylesspalace/tusky/di/NetworkModule.kt | 37 +- .../tusky/di/RepositoryModule.kt | 14 +- .../keylesspalace/tusky/di/ServicesModule.kt | 2 +- .../tusky/di/ViewModelFactory.kt | 4 +- .../keylesspalace/tusky/entity/AccessToken.kt | 2 +- .../com/keylesspalace/tusky/entity/Account.kt | 88 ++--- .../tusky/entity/Announcement.kt | 38 +- .../tusky/entity/AppCredentials.kt | 4 +- .../keylesspalace/tusky/entity/Attachment.kt | 26 +- .../com/keylesspalace/tusky/entity/Card.kt | 20 +- .../tusky/entity/Conversation.kt | 10 +- .../tusky/entity/DeletedStatus.kt | 21 +- .../com/keylesspalace/tusky/entity/Emoji.kt | 8 +- .../com/keylesspalace/tusky/entity/Filter.kt | 3 +- .../com/keylesspalace/tusky/entity/HashTag.kt | 2 +- .../tusky/entity/IdentityProof.kt | 6 +- .../keylesspalace/tusky/entity/Instance.kt | 34 +- .../com/keylesspalace/tusky/entity/Marker.kt | 14 +- .../keylesspalace/tusky/entity/MastoList.kt | 6 +- .../keylesspalace/tusky/entity/NewStatus.kt | 24 +- .../tusky/entity/Notification.kt | 6 +- .../com/keylesspalace/tusky/entity/Poll.kt | 45 ++- .../tusky/entity/Relationship.kt | 2 +- .../tusky/entity/ScheduledStatus.kt | 8 +- .../tusky/entity/SearchResult.kt | 2 +- .../com/keylesspalace/tusky/entity/Status.kt | 72 ++-- .../tusky/entity/StatusContext.kt | 2 +- .../tusky/entity/StatusParams.kt | 12 +- .../tusky/fragment/AccountListFragment.kt | 104 +++--- .../tusky/fragment/AccountMediaFragment.kt | 51 ++- .../tusky/fragment/ViewImageFragment.kt | 121 +++--- .../tusky/fragment/ViewMediaFragment.kt | 16 +- .../tusky/fragment/ViewVideoFragment.kt | 30 +- .../interfaces/AccountSelectionListener.kt | 2 +- .../tusky/interfaces/RefreshableFragment.kt | 2 +- .../tusky/interfaces/ReselectableFragment.kt | 2 +- .../tusky/json/SpannedTypeAdapter.kt | 10 +- .../tusky/network/FilterModel.kt | 10 +- .../tusky/network/MastodonApi.kt | 351 +++++++++--------- .../tusky/network/TimelineCases.kt | 41 +- .../tusky/pager/AccountPagerAdapter.kt | 11 +- .../tusky/pager/ImagePagerAdapter.kt | 12 +- .../tusky/pager/MainPagerAdapter.kt | 1 - .../tusky/pager/SingleImagePagerAdapter.kt | 4 +- .../NotificationClearBroadcastReceiver.kt | 4 +- .../receiver/SendStatusBroadcastReceiver.kt | 66 ++-- .../tusky/service/SendTootService.kt | 147 ++++---- .../tusky/service/ServiceClient.kt | 2 +- .../tusky/settings/SettingsDSL.kt | 27 +- .../keylesspalace/tusky/util/BindingHolder.kt | 2 +- .../tusky/util/BlurHashDecoder.kt | 29 +- .../keylesspalace/tusky/util/CardViewMode.kt | 2 +- .../tusky/util/ComposeTokenizer.kt | 27 +- .../tusky/util/CustomEmojiHelper.kt | 29 +- .../tusky/util/CustomFragmentStateAdapter.kt | 8 +- .../com/keylesspalace/tusky/util/Either.kt | 2 +- .../tusky/util/EmojiCompatFont.kt | 107 +++--- .../tusky/util/FocalPointUtil.kt | 41 +- .../tusky/util/ImageLoadingHelper.kt | 39 +- .../util/ListStatusAccessibilityDelegate.kt | 11 +- .../com/keylesspalace/tusky/util/ListUtils.kt | 5 +- .../keylesspalace/tusky/util/LiveDataUtil.kt | 22 +- .../keylesspalace/tusky/util/LocaleManager.kt | 2 +- .../keylesspalace/tusky/util/MediaUtils.kt | 18 +- .../keylesspalace/tusky/util/NetworkState.kt | 7 +- .../tusky/util/NotificationTypeConverter.kt | 2 +- .../tusky/util/PickMediaFiles.kt | 2 +- .../com/keylesspalace/tusky/util/Resource.kt | 11 +- .../com/keylesspalace/tusky/util/RickRoll.kt | 6 +- .../tusky/util/RxAwareViewModel.kt | 2 +- .../tusky/util/ShareShortcutHelper.kt | 52 ++- .../tusky/util/SmartLengthInputFilter.kt | 98 ++--- .../com/keylesspalace/tusky/util/SpanUtils.kt | 39 +- .../tusky/util/StatusDisplayOptions.kt | 38 +- .../tusky/util/StatusViewHelper.kt | 102 ++--- .../keylesspalace/tusky/util/StringUtils.kt | 6 +- .../tusky/util/ViewBindingExtensions.kt | 36 +- .../keylesspalace/tusky/util/ViewDataUtils.kt | 4 +- .../tusky/util/ViewExtensions.kt | 8 +- .../tusky/view/BackgroundMessageView.kt | 14 +- .../view/ConversationLineItemDecoration.kt | 7 +- .../keylesspalace/tusky/view/EmojiPicker.kt | 4 +- .../tusky/view/ExposedPlayPauseVideoView.kt | 11 +- .../keylesspalace/tusky/view/LicenseCard.kt | 11 +- .../tusky/view/MediaPreviewImageView.kt | 18 +- .../tusky/view/MuteAccountDialog.kt | 30 +- .../tusky/view/SquareImageView.kt | 8 +- .../tusky/viewdata/AttachmentViewData.kt | 8 +- .../tusky/viewdata/PollViewData.kt | 48 +-- .../tusky/viewmodel/AccountViewModel.kt | 136 ++++--- .../viewmodel/AccountsInListViewModel.kt | 60 +-- .../tusky/viewmodel/EditProfileViewModel.kt | 135 +++---- .../tusky/viewmodel/ListsViewModel.kt | 70 ++-- ...eSpannableString.kt => SpannableString.kt} | 3 +- .../tusky/BottomSheetActivityTest.kt | 95 ++--- .../tusky/ComposeActivityTest.kt | 123 +++--- .../tusky/ComposeTokenizerTest.kt | 108 +++--- .../com/keylesspalace/tusky/FilterTest.kt | 8 +- .../keylesspalace/tusky/FocalPointUtilTest.kt | 98 +++-- .../com/keylesspalace/tusky/SpanUtilsTest.kt | 40 +- .../keylesspalace/tusky/StringUtilsTest.kt | 36 +- .../keylesspalace/tusky/TuskyApplication.kt | 2 +- .../timeline/TimelineRepositoryTest.kt | 8 +- .../timeline/TimelineViewModelTest.kt | 18 +- .../tusky/util/EmojiCompatFontTest.kt | 54 +-- .../keylesspalace/tusky/util/RickRollTest.kt | 6 +- .../tusky/util/SmartLengthInputFilterTest.kt | 103 +++-- .../tusky/util/VersionUtilsTest.kt | 27 +- build.gradle | 6 + 227 files changed, 3933 insertions(+), 3371 deletions(-) rename app/src/test/java/android/text/{FakeSpannableString.kt => SpannableString.kt} (99%) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0e4f81ab2..428f50eab 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,17 +11,23 @@ All English text that will be visible to users should be put in ```app/src/main/res/values/strings.xml```. Any text that is missing in a translation will fall back to the version in this file. Be aware that anything added to this file will need to be translated, so be very concise with wording and try to add as few things as possible. Look for existing strings to use first. If there is untranslatable text that you don't want to keep as a string constant in a Java class, you can use the string resource file ```app/src/main/res/values/donottranslate.xml```. ### Translation -Translations are done through https://weblate.tusky.app/projects/tusky/tusky/ . -To add a new language, clic on the 'Start a new translation' button on at the bottom of the page. +Translations are done through our [Weblate](https://weblate.tusky.app/projects/tusky/tusky/). +To add a new language, click on the 'Start a new translation' button on at the bottom of the page. ### Kotlin -This project is in the process of migrating to Kotlin, we prefer new code to be written in Kotlin. We try to follow the [Kotlin Style Guide](https://android.github.io/kotlin-guides/style.html) and make use of the [Kotlin Android Extensions](https://kotlinlang.org/docs/tutorials/android-plugin.html). +This project is in the process of migrating to Kotlin, all new code must be written in Kotlin. +We try to follow the [Kotlin Style Guide](https://developer.android.com/kotlin/style-guide) and make format the code according to the default [ktlint codestyle](https://github.com/pinterest/ktlint). +You can check the codestyle by running `./gradlew ktlintCheck`. ### Java -Existing code in Java should follow the [Android Style Guide](https://source.android.com/source/code-style), which is what Android uses for their own source code. ```@Nullable``` and ```@NotNull``` annotations are really helpful for Kotlin interoperability. +Existing code in Java should follow the [Android Style Guide](https://source.android.com/source/code-style), which is what Android uses for their own source code. ```@Nullable``` and ```@NotNull``` annotations are really helpful for Kotlin interoperability. Please don't submit new features written in Kotlin. + +### Viewbinding +We use [Viewbinding](https://developer.android.com/topic/libraries/view-binding) to reference views. No contribution using another mechanism will be accepted. +There are useful extensions in `src/main/java/com/keylesspalace/tusky/util/ViewExtensions.kt` that make working with viewbinding easier. ### Visuals -There are three themes in the app, so any visual changes should be checked with each of them to ensure they look appropriate no matter which theme is selected. Usually, you can use existing color attributes like ```?attr/colorPrimary``` and ```?attr/textColorSecondary```. For icons and drawables, use a white drawable and tint it at runtime using ```ThemeUtils``` and specify an attribute that references different colours depending on the theme. +There are three themes in the app, so any visual changes should be checked with each of them to ensure they look appropriate no matter which theme is selected. Usually, you can use existing color attributes like ```?attr/colorPrimary``` and ```?attr/textColorSecondary```. ### Saving Any time you get a good chunk of work done it's good to make a commit. You can either uses Android Studio's built-in UI for doing this or running the commands: diff --git a/app/src/androidTest/java/com/keylesspalace/tusky/MigrationsTest.kt b/app/src/androidTest/java/com/keylesspalace/tusky/MigrationsTest.kt index 9c65aebf3..69641cc41 100644 --- a/app/src/androidTest/java/com/keylesspalace/tusky/MigrationsTest.kt +++ b/app/src/androidTest/java/com/keylesspalace/tusky/MigrationsTest.kt @@ -2,8 +2,8 @@ package com.keylesspalace.tusky import androidx.room.testing.MigrationTestHelper import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry import com.keylesspalace.tusky.db.AppDatabase import org.junit.Assert.assertEquals import org.junit.Rule @@ -18,9 +18,9 @@ class MigrationsTest { @JvmField @Rule var helper: MigrationTestHelper = MigrationTestHelper( - InstrumentationRegistry.getInstrumentation(), - AppDatabase::class.java.canonicalName, - FrameworkSQLiteOpenHelperFactory() + InstrumentationRegistry.getInstrumentation(), + AppDatabase::class.java.canonicalName, + FrameworkSQLiteOpenHelperFactory() ) @Test @@ -33,12 +33,15 @@ class MigrationsTest { val active = true val accountId = "accountId" val username = "username" - val values = arrayOf(id, domain, token, active, accountId, username, "Display Name", - "https://picture.url", true, true, true, true, true, true, true, - true, "1000", "[]", "[{\"shortcode\": \"emoji\", \"url\": \"yes\"}]", 0, false, - false, true) + val values = arrayOf( + id, domain, token, active, accountId, username, "Display Name", + "https://picture.url", true, true, true, true, true, true, true, + true, "1000", "[]", "[{\"shortcode\": \"emoji\", \"url\": \"yes\"}]", 0, false, + false, true + ) - db.execSQL("INSERT OR REPLACE INTO `AccountEntity`(`id`,`domain`,`accessToken`,`isActive`," + + db.execSQL( + "INSERT OR REPLACE INTO `AccountEntity`(`id`,`domain`,`accessToken`,`isActive`," + "`accountId`,`username`,`displayName`,`profilePictureUrl`,`notificationsEnabled`," + "`notificationsMentioned`,`notificationsFollowed`,`notificationsReblogged`," + "`notificationsFavorited`,`notificationSound`,`notificationVibration`," + @@ -46,7 +49,8 @@ class MigrationsTest { "`defaultPostPrivacy`,`defaultMediaSensitivity`,`alwaysShowSensitiveMedia`," + "`mediaPreviewEnabled`) " + "VALUES (nullif(?, 0),?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", - values) + values + ) db.close() @@ -61,4 +65,4 @@ class MigrationsTest { assertEquals(accountId, cursor.getString(4)) assertEquals(username, cursor.getString(5)) } -} \ No newline at end of file +} diff --git a/app/src/androidTest/java/com/keylesspalace/tusky/TimelineDAOTest.kt b/app/src/androidTest/java/com/keylesspalace/tusky/TimelineDAOTest.kt index 92288bba2..c4959b3ab 100644 --- a/app/src/androidTest/java/com/keylesspalace/tusky/TimelineDAOTest.kt +++ b/app/src/androidTest/java/com/keylesspalace/tusky/TimelineDAOTest.kt @@ -3,9 +3,13 @@ package com.keylesspalace.tusky import androidx.room.Room import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry -import com.keylesspalace.tusky.db.* -import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.components.timeline.TimelineRepository +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.db.TimelineAccountEntity +import com.keylesspalace.tusky.db.TimelineDao +import com.keylesspalace.tusky.db.TimelineStatusEntity +import com.keylesspalace.tusky.db.TimelineStatusWithAccount +import com.keylesspalace.tusky.entity.Status import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertNull @@ -41,9 +45,11 @@ class TimelineDAOTest { timelineDao.insertInTransaction(status, author, reblogger) } - val resultsFromDb = timelineDao.getStatusesForAccount(setOne.first.timelineUserId, - maxId = "21", sinceId = ignoredOne.first.serverId, limit = 10) - .blockingGet() + val resultsFromDb = timelineDao.getStatusesForAccount( + setOne.first.timelineUserId, + maxId = "21", sinceId = ignoredOne.first.serverId, limit = 10 + ) + .blockingGet() assertEquals(2, resultsFromDb.size) for ((set, fromDb) in listOf(setTwo, setOne).zip(resultsFromDb)) { @@ -64,14 +70,13 @@ class TimelineDAOTest { timelineDao.insertStatusIfNotThere(placeholder) val fromDb = timelineDao.getStatusesForAccount(status.timelineUserId, null, null, 10) - .blockingGet() + .blockingGet() val result = fromDb.first() assertEquals(1, fromDb.size) assertEquals(author, result.account) assertEquals(status, result.status) assertNull(result.reblogAccount) - } @Test @@ -79,22 +84,22 @@ class TimelineDAOTest { val now = System.currentTimeMillis() val oldDate = now - TimelineRepository.CLEANUP_INTERVAL - 20_000 val oldThisAccount = makeStatus( - statusId = 5, - createdAt = oldDate + statusId = 5, + createdAt = oldDate ) val oldAnotherAccount = makeStatus( - statusId = 10, - createdAt = oldDate, - accountId = 2 + statusId = 10, + createdAt = oldDate, + accountId = 2 ) val recentThisAccount = makeStatus( - statusId = 30, - createdAt = System.currentTimeMillis() + statusId = 30, + createdAt = System.currentTimeMillis() ) val recentAnotherAccount = makeStatus( - statusId = 60, - createdAt = System.currentTimeMillis(), - accountId = 2 + statusId = 60, + createdAt = System.currentTimeMillis(), + accountId = 2 ) for ((status, author, reblogAuthor) in listOf(oldThisAccount, oldAnotherAccount, recentThisAccount, recentAnotherAccount)) { @@ -104,15 +109,15 @@ class TimelineDAOTest { timelineDao.cleanup(now - TimelineRepository.CLEANUP_INTERVAL) assertEquals( - listOf(recentThisAccount), - timelineDao.getStatusesForAccount(1, null, null, 100).blockingGet() - .map { it.toTriple() } + listOf(recentThisAccount), + timelineDao.getStatusesForAccount(1, null, null, 100).blockingGet() + .map { it.toTriple() } ) assertEquals( - listOf(recentAnotherAccount), - timelineDao.getStatusesForAccount(2, null, null, 100).blockingGet() - .map { it.toTriple() } + listOf(recentAnotherAccount), + timelineDao.getStatusesForAccount(2, null, null, 100).blockingGet() + .map { it.toTriple() } ) } @@ -120,9 +125,9 @@ class TimelineDAOTest { fun overwriteDeletedStatus() { val oldStatuses = listOf( - makeStatus(statusId = 3), - makeStatus(statusId = 2), - makeStatus(statusId = 1) + makeStatus(statusId = 3), + makeStatus(statusId = 2), + makeStatus(statusId = 1) ) timelineDao.deleteRange(1, oldStatuses.last().first.serverId, oldStatuses.first().first.serverId) @@ -133,8 +138,8 @@ class TimelineDAOTest { // status 2 gets deleted, newly loaded status contain only 1 + 3 val newStatuses = listOf( - makeStatus(statusId = 3), - makeStatus(statusId = 1) + makeStatus(statusId = 3), + makeStatus(statusId = 1) ) timelineDao.deleteRange(1, newStatuses.last().first.serverId, newStatuses.first().first.serverId) @@ -143,107 +148,106 @@ class TimelineDAOTest { timelineDao.insertInTransaction(status, author, reblogAuthor) } - //make sure status 2 is no longer in db + // make sure status 2 is no longer in db assertEquals( - newStatuses, - timelineDao.getStatusesForAccount(1, null, null, 100).blockingGet() - .map { it.toTriple() } + newStatuses, + timelineDao.getStatusesForAccount(1, null, null, 100).blockingGet() + .map { it.toTriple() } ) } private fun makeStatus( - accountId: Long = 1, - statusId: Long = 10, - reblog: Boolean = false, - createdAt: Long = statusId, - authorServerId: String = "20" + accountId: Long = 1, + statusId: Long = 10, + reblog: Boolean = false, + createdAt: Long = statusId, + authorServerId: String = "20" ): Triple { val author = TimelineAccountEntity( - authorServerId, - accountId, - "localUsername", - "username", - "displayName", - "blah", - "avatar", - "[\"tusky\": \"http://tusky.cool/emoji.jpg\"]", - false + authorServerId, + accountId, + "localUsername", + "username", + "displayName", + "blah", + "avatar", + "[\"tusky\": \"http://tusky.cool/emoji.jpg\"]", + false ) val reblogAuthor = if (reblog) { TimelineAccountEntity( - "R$authorServerId", - accountId, - "RlocalUsername", - "Rusername", - "RdisplayName", - "Rblah", - "Ravatar", - "[]", - false + "R$authorServerId", + accountId, + "RlocalUsername", + "Rusername", + "RdisplayName", + "Rblah", + "Ravatar", + "[]", + false ) } else null - val even = accountId % 2 == 0L val status = TimelineStatusEntity( - serverId = statusId.toString(), - url = "url$statusId", - timelineUserId = accountId, - authorServerId = authorServerId, - inReplyToId = "inReplyToId$statusId", - inReplyToAccountId = "inReplyToAccountId$statusId", - content = "Content!$statusId", - createdAt = createdAt, - emojis = "emojis$statusId", - reblogsCount = 1 * statusId.toInt(), - favouritesCount = 2 * statusId.toInt(), - reblogged = even, - favourited = !even, - bookmarked = false, - sensitive = even, - spoilerText = "spoier$statusId", - visibility = Status.Visibility.PRIVATE, - attachments = "attachments$accountId", - mentions = "mentions$accountId", - application = "application$accountId", - reblogServerId = if (reblog) (statusId * 100).toString() else null, - reblogAccountId = reblogAuthor?.serverId, - poll = null, - muted = false + serverId = statusId.toString(), + url = "url$statusId", + timelineUserId = accountId, + authorServerId = authorServerId, + inReplyToId = "inReplyToId$statusId", + inReplyToAccountId = "inReplyToAccountId$statusId", + content = "Content!$statusId", + createdAt = createdAt, + emojis = "emojis$statusId", + reblogsCount = 1 * statusId.toInt(), + favouritesCount = 2 * statusId.toInt(), + reblogged = even, + favourited = !even, + bookmarked = false, + sensitive = even, + spoilerText = "spoier$statusId", + visibility = Status.Visibility.PRIVATE, + attachments = "attachments$accountId", + mentions = "mentions$accountId", + application = "application$accountId", + reblogServerId = if (reblog) (statusId * 100).toString() else null, + reblogAccountId = reblogAuthor?.serverId, + poll = null, + muted = false ) return Triple(status, author, reblogAuthor) } private fun createPlaceholder(serverId: String, timelineUserId: Long): TimelineStatusEntity { return TimelineStatusEntity( - serverId = serverId, - url = null, - timelineUserId = timelineUserId, - authorServerId = null, - inReplyToId = null, - inReplyToAccountId = null, - content = null, - createdAt = 0L, - emojis = null, - reblogsCount = 0, - favouritesCount = 0, - reblogged = false, - favourited = false, - bookmarked = false, - sensitive = false, - spoilerText = null, - visibility = null, - attachments = null, - mentions = null, - application = null, - reblogServerId = null, - reblogAccountId = null, - poll = null, - muted = false + serverId = serverId, + url = null, + timelineUserId = timelineUserId, + authorServerId = null, + inReplyToId = null, + inReplyToAccountId = null, + content = null, + createdAt = 0L, + emojis = null, + reblogsCount = 0, + favouritesCount = 0, + reblogged = false, + favourited = false, + bookmarked = false, + sensitive = false, + spoilerText = null, + visibility = null, + attachments = null, + mentions = null, + application = null, + reblogServerId = null, + reblogAccountId = null, + poll = null, + muted = false ) } private fun TimelineStatusWithAccount.toTriple() = Triple(status, account, reblogAccount) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/AboutActivity.kt b/app/src/main/java/com/keylesspalace/tusky/AboutActivity.kt index 8246555b8..fc8b3db2b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AboutActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/AboutActivity.kt @@ -2,13 +2,13 @@ package com.keylesspalace.tusky import android.content.Intent import android.os.Bundle -import androidx.annotation.StringRes import android.text.SpannableString import android.text.SpannableStringBuilder import android.text.method.LinkMovementMethod import android.text.style.URLSpan import android.text.util.Linkify import android.widget.TextView +import androidx.annotation.StringRes import com.keylesspalace.tusky.databinding.ActivityAboutBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.util.NoUnderlineURLSpan @@ -32,7 +32,7 @@ class AboutActivity : BottomSheetActivity(), Injectable { binding.versionTextView.text = getString(R.string.about_app_version, getString(R.string.app_name), BuildConfig.VERSION_NAME) - if(BuildConfig.CUSTOM_INSTANCE.isBlank()) { + if (BuildConfig.CUSTOM_INSTANCE.isBlank()) { binding.aboutPoweredByTusky.hide() } diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt index a408bc43f..adcdcac6e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt @@ -62,7 +62,16 @@ import com.keylesspalace.tusky.interfaces.LinkListener import com.keylesspalace.tusky.interfaces.ReselectableFragment import com.keylesspalace.tusky.pager.AccountPagerAdapter import com.keylesspalace.tusky.settings.PrefKeys -import com.keylesspalace.tusky.util.* +import com.keylesspalace.tusky.util.DefaultTextWatcher +import com.keylesspalace.tusky.util.LinkHelper +import com.keylesspalace.tusky.util.Success +import com.keylesspalace.tusky.util.ThemeUtils +import com.keylesspalace.tusky.util.emojify +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.loadAvatar +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.viewBinding +import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.view.showMuteAccountDialog import com.keylesspalace.tusky.viewmodel.AccountViewModel import dagger.android.DispatchingAndroidInjector @@ -82,7 +91,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI private val binding: ActivityAccountBinding by viewBinding(ActivityAccountBinding::inflate) - private lateinit var accountFieldAdapter : AccountFieldAdapter + private lateinit var accountFieldAdapter: AccountFieldAdapter private var followState: FollowState = FollowState.NOT_FOLLOWING private var blocking: Boolean = false @@ -233,7 +242,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI override fun onTabUnselected(tab: TabLayout.Tab?) {} override fun onTabSelected(tab: TabLayout.Tab?) {} - }) } @@ -266,8 +274,8 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI fillColor = ColorStateList.valueOf(toolbarColor) elevation = appBarElevation shapeAppearanceModel = ShapeAppearanceModel.builder() - .setAllCornerSizes(resources.getDimension(R.dimen.account_avatar_background_radius)) - .build() + .setAllCornerSizes(resources.getDimension(R.dimen.account_avatar_background_radius)) + .build() } binding.accountAvatarImageView.background = avatarBackground @@ -314,7 +322,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI binding.swipeToRefreshLayout.isEnabled = verticalOffset == 0 } }) - } private fun makeNotificationBarTransparent() { @@ -331,8 +338,8 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI is Success -> onAccountChanged(it.data) is Error -> { Snackbar.make(binding.accountCoordinatorLayout, R.string.error_generic, Snackbar.LENGTH_LONG) - .setAction(R.string.action_retry) { viewModel.refresh() } - .show() + .setAction(R.string.action_retry) { viewModel.refresh() } + .show() } } } @@ -344,15 +351,17 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI if (it is Error) { Snackbar.make(binding.accountCoordinatorLayout, R.string.error_generic, Snackbar.LENGTH_LONG) - .setAction(R.string.action_retry) { viewModel.refresh() } - .show() + .setAction(R.string.action_retry) { viewModel.refresh() } + .show() } - } - viewModel.accountFieldData.observe(this, { - accountFieldAdapter.fields = it - accountFieldAdapter.notifyDataSetChanged() - }) + viewModel.accountFieldData.observe( + this, + { + accountFieldAdapter.fields = it + accountFieldAdapter.notifyDataSetChanged() + } + ) viewModel.noteSaved.observe(this) { binding.saveNoteInfo.visible(it, View.INVISIBLE) } @@ -366,9 +375,12 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI viewModel.refresh() adapter.refreshContent() } - viewModel.isRefreshing.observe(this, { isRefreshing -> - binding.swipeToRefreshLayout.isRefreshing = isRefreshing == true - }) + viewModel.isRefreshing.observe( + this, + { isRefreshing -> + binding.swipeToRefreshLayout.isRefreshing = isRefreshing == true + } + ) binding.swipeToRefreshLayout.setColorSchemeResources(R.color.tusky_blue) } @@ -382,7 +394,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI val emojifiedNote = account.note.emojify(account.emojis, binding.accountNoteTextView, animateEmojis) LinkHelper.setClickableText(binding.accountNoteTextView, emojifiedNote, null, this) - // accountFieldAdapter.fields = account.fields ?: emptyList() + // accountFieldAdapter.fields = account.fields ?: emptyList() accountFieldAdapter.emojis = account.emojis ?: emptyList() accountFieldAdapter.notifyDataSetChanged() @@ -409,18 +421,17 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI loadedAccount?.let { account -> loadAvatar( - account.avatar, - binding.accountAvatarImageView, - resources.getDimensionPixelSize(R.dimen.avatar_radius_94dp), - animateAvatar + account.avatar, + binding.accountAvatarImageView, + resources.getDimensionPixelSize(R.dimen.avatar_radius_94dp), + animateAvatar ) Glide.with(this) - .asBitmap() - .load(account.header) - .centerCrop() - .into(binding.accountHeaderImageView) - + .asBitmap() + .load(account.header) + .centerCrop() + .into(binding.accountHeaderImageView) binding.accountAvatarImageView.setOnClickListener { avatarView -> val intent = ViewMediaActivity.newSingleImageIntent(avatarView.context, account.avatar) @@ -478,7 +489,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI binding.accountMovedText.setCompoundDrawablesRelativeWithIntrinsicBounds(movedIcon, null, null, null) } - } /** @@ -554,15 +564,16 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI // because subscribing is Pleroma extension, enable it __only__ when we have non-null subscribing field // it's also now supported in Mastodon 3.3.0rc but called notifying and use different API call - if(!viewModel.isSelf && followState == FollowState.FOLLOWING - && (relation.subscribing != null || relation.notifying != null)) { + if (!viewModel.isSelf && followState == FollowState.FOLLOWING && + (relation.subscribing != null || relation.notifying != null) + ) { binding.accountSubscribeButton.show() binding.accountSubscribeButton.setOnClickListener { viewModel.changeSubscribingState() } - if(relation.notifying != null) + if (relation.notifying != null) subscribing = relation.notifying - else if(relation.subscribing != null) + else if (relation.subscribing != null) subscribing = relation.subscribing } @@ -577,7 +588,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI updateButtons() } - private val noteWatcher = object: DefaultTextWatcher() { + private val noteWatcher = object : DefaultTextWatcher() { override fun afterTextChanged(s: Editable) { viewModel.noteChanged(s.toString()) } @@ -615,11 +626,11 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI } private fun updateSubscribeButton() { - if(followState != FollowState.FOLLOWING) { + if (followState != FollowState.FOLLOWING) { binding.accountSubscribeButton.hide() } - if(subscribing) { + if (subscribing) { binding.accountSubscribeButton.setIconResource(R.drawable.ic_notifications_active_24dp) binding.accountSubscribeButton.contentDescription = getString(R.string.action_unsubscribe_account) } else { @@ -648,7 +659,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI binding.accountMuteButton.hide() updateMuteButton() } - } else { binding.accountFloatingActionButton.hide() binding.accountFollowButton.hide() @@ -698,11 +708,9 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI } else { getString(R.string.action_show_reblogs) } - } else { menu.removeItem(R.id.action_show_reblogs) } - } else { // It shouldn't be possible to block, mute or report yourself. menu.removeItem(R.id.action_block) @@ -717,39 +725,39 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI private fun showFollowRequestPendingDialog() { AlertDialog.Builder(this) - .setMessage(R.string.dialog_message_cancel_follow_request) - .setPositiveButton(android.R.string.ok) { _, _ -> viewModel.changeFollowState() } - .setNegativeButton(android.R.string.cancel, null) - .show() + .setMessage(R.string.dialog_message_cancel_follow_request) + .setPositiveButton(android.R.string.ok) { _, _ -> viewModel.changeFollowState() } + .setNegativeButton(android.R.string.cancel, null) + .show() } private fun showUnfollowWarningDialog() { AlertDialog.Builder(this) - .setMessage(R.string.dialog_unfollow_warning) - .setPositiveButton(android.R.string.ok) { _, _ -> viewModel.changeFollowState() } - .setNegativeButton(android.R.string.cancel, null) - .show() + .setMessage(R.string.dialog_unfollow_warning) + .setPositiveButton(android.R.string.ok) { _, _ -> viewModel.changeFollowState() } + .setNegativeButton(android.R.string.cancel, null) + .show() } private fun toggleBlockDomain(instance: String) { - if(blockingDomain) { + if (blockingDomain) { viewModel.unblockDomain(instance) } else { AlertDialog.Builder(this) - .setMessage(getString(R.string.mute_domain_warning, instance)) - .setPositiveButton(getString(R.string.mute_domain_warning_dialog_ok)) { _, _ -> viewModel.blockDomain(instance) } - .setNegativeButton(android.R.string.cancel, null) - .show() + .setMessage(getString(R.string.mute_domain_warning, instance)) + .setPositiveButton(getString(R.string.mute_domain_warning_dialog_ok)) { _, _ -> viewModel.blockDomain(instance) } + .setNegativeButton(android.R.string.cancel, null) + .show() } } private fun toggleBlock() { if (viewModel.relationshipData.value?.data?.blocking != true) { AlertDialog.Builder(this) - .setMessage(getString(R.string.dialog_block_warning, loadedAccount?.username)) - .setPositiveButton(android.R.string.ok) { _, _ -> viewModel.changeBlockState() } - .setNegativeButton(android.R.string.cancel, null) - .show() + .setMessage(getString(R.string.dialog_block_warning, loadedAccount?.username)) + .setPositiveButton(android.R.string.ok) { _, _ -> viewModel.changeBlockState() } + .setNegativeButton(android.R.string.cancel, null) + .show() } else { viewModel.changeBlockState() } @@ -759,8 +767,8 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI if (viewModel.relationshipData.value?.data?.muting != true) { loadedAccount?.let { showMuteAccountDialog( - this, - it.username + this, + it.username ) { notifications, duration -> viewModel.muteAccount(notifications, duration) } @@ -772,8 +780,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI private fun mention() { loadedAccount?.let { - val intent = ComposeActivity.startIntent(this, - ComposeActivity.ComposeOptions(mentionedUsernames = setOf(it.username))) + val intent = ComposeActivity.startIntent( + this, + ComposeActivity.ComposeOptions(mentionedUsernames = setOf(it.username)) + ) startActivity(intent) } } @@ -849,5 +859,4 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI return intent } } - } diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountListActivity.kt b/app/src/main/java/com/keylesspalace/tusky/AccountListActivity.kt index 7f00150f4..ca23f7912 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountListActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/AccountListActivity.kt @@ -64,9 +64,9 @@ class AccountListActivity : BaseActivity(), HasAndroidInjector { } supportFragmentManager - .beginTransaction() - .replace(R.id.fragment_container, AccountListFragment.newInstance(type, id, accountLocked)) - .commit() + .beginTransaction() + .replace(R.id.fragment_container, AccountListFragment.newInstance(type, id, accountLocked)) + .commit() } override fun androidInjector() = dispatchingAndroidInjector diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt b/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt index 59859b313..02fa07382 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt @@ -36,7 +36,13 @@ import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.settings.PrefKeys -import com.keylesspalace.tusky.util.* +import com.keylesspalace.tusky.util.BindingHolder +import com.keylesspalace.tusky.util.Either +import com.keylesspalace.tusky.util.emojify +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.loadAvatar +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel import com.keylesspalace.tusky.viewmodel.State import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers @@ -93,19 +99,19 @@ class AccountsInListFragment : DialogFragment(), Injectable { binding.accountsSearchRecycler.adapter = searchAdapter viewModel.state - .observeOn(AndroidSchedulers.mainThread()) - .autoDispose(from(this)) - .subscribe { state -> - adapter.submitList(state.accounts.asRightOrNull() ?: listOf()) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(from(this)) + .subscribe { state -> + adapter.submitList(state.accounts.asRightOrNull() ?: listOf()) - when (state.accounts) { - is Either.Right -> binding.messageView.hide() - is Either.Left -> handleError(state.accounts.value) - } - - setupSearchView(state) + when (state.accounts) { + is Either.Right -> binding.messageView.hide() + is Either.Left -> handleError(state.accounts.value) } + setupSearchView(state) + } + binding.searchView.isSubmitButtonEnabled = true binding.searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String?): Boolean { @@ -146,11 +152,15 @@ class AccountsInListFragment : DialogFragment(), Injectable { viewModel.load(listId) } if (error is IOException) { - binding.messageView.setup(R.drawable.elephant_offline, - R.string.error_network, retryAction) + binding.messageView.setup( + R.drawable.elephant_offline, + R.string.error_network, retryAction + ) } else { - binding.messageView.setup(R.drawable.elephant_error, - R.string.error_generic, retryAction) + binding.messageView.setup( + R.drawable.elephant_error, + R.string.error_generic, retryAction + ) } } @@ -184,7 +194,7 @@ class AccountsInListFragment : DialogFragment(), Injectable { onRemoveFromList(getItem(holder.bindingAdapterPosition).id) } binding.rejectButton.contentDescription = - binding.root.context.getString(R.string.action_remove_from_list) + binding.root.context.getString(R.string.action_remove_from_list) return holder } @@ -203,8 +213,8 @@ class AccountsInListFragment : DialogFragment(), Injectable { } override fun areContentsTheSame(oldItem: AccountInfo, newItem: AccountInfo): Boolean { - return oldItem.second == newItem.second - && oldItem.first.deepEquals(newItem.first) + return oldItem.second == newItem.second && + oldItem.first.deepEquals(newItem.first) } } @@ -260,4 +270,4 @@ class AccountsInListFragment : DialogFragment(), Injectable { return AccountsInListFragment().apply { arguments = args } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/BottomSheetActivity.kt b/app/src/main/java/com/keylesspalace/tusky/BottomSheetActivity.kt index 6fd42b8d4..99903e32d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/BottomSheetActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/BottomSheetActivity.kt @@ -60,7 +60,6 @@ abstract class BottomSheetActivity : BaseActivity() { override fun onSlide(bottomSheet: View, slideOffset: Float) {} }) - } open fun viewUrl(url: String, lookupFallbackBehavior: PostLookupFallbackBehavior = PostLookupFallbackBehavior.OPEN_IN_BROWSER) { @@ -70,11 +69,12 @@ abstract class BottomSheetActivity : BaseActivity() { } mastodonApi.searchObservable( - query = url, - resolve = true + query = url, + resolve = true ).observeOn(AndroidSchedulers.mainThread()) - .autoDispose(AndroidLifecycleScopeProvider.from(this, Lifecycle.Event.ON_DESTROY)) - .subscribe({ (accounts, statuses) -> + .autoDispose(AndroidLifecycleScopeProvider.from(this, Lifecycle.Event.ON_DESTROY)) + .subscribe( + { (accounts, statuses) -> if (getCancelSearchRequested(url)) { return@subscribe } @@ -90,12 +90,14 @@ abstract class BottomSheetActivity : BaseActivity() { } performUrlFallbackAction(url, lookupFallbackBehavior) - }, { + }, + { if (!getCancelSearchRequested(url)) { onEndSearch(url) performUrlFallbackAction(url, lookupFallbackBehavior) } - }) + } + ) onBeginSearch(url) } @@ -186,20 +188,21 @@ fun looksLikeMastodonUrl(urlString: String): Boolean { } if (uri.query != null || - uri.fragment != null || - uri.path == null) { + uri.fragment != null || + uri.path == null + ) { return false } val path = uri.path return path.matches("^/@[^/]+$".toRegex()) || - path.matches("^/@[^/]+/\\d+$".toRegex()) || - path.matches("^/users/\\w+$".toRegex()) || - path.matches("^/notice/[a-zA-Z0-9]+$".toRegex()) || - path.matches("^/objects/[-a-f0-9]+$".toRegex()) || - path.matches("^/notes/[a-z0-9]+$".toRegex()) || - path.matches("^/display/[-a-f0-9]+$".toRegex()) || - path.matches("^/profile/\\w+$".toRegex()) + path.matches("^/@[^/]+/\\d+$".toRegex()) || + path.matches("^/users/\\w+$".toRegex()) || + path.matches("^/notice/[a-zA-Z0-9]+$".toRegex()) || + path.matches("^/objects/[-a-f0-9]+$".toRegex()) || + path.matches("^/notes/[a-z0-9]+$".toRegex()) || + path.matches("^/display/[-a-f0-9]+$".toRegex()) || + path.matches("^/profile/\\w+$".toRegex()) } enum class PostLookupFallbackBehavior { diff --git a/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt index e1d9768ef..1115cd41e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt @@ -36,18 +36,24 @@ import androidx.recyclerview.widget.LinearLayoutManager import com.bumptech.glide.Glide import com.bumptech.glide.load.resource.bitmap.FitCenter import com.bumptech.glide.load.resource.bitmap.RoundedCorners +import com.canhub.cropper.CropImage import com.google.android.material.snackbar.Snackbar import com.keylesspalace.tusky.adapter.AccountFieldEditAdapter import com.keylesspalace.tusky.databinding.ActivityEditProfileBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory -import com.keylesspalace.tusky.util.* +import com.keylesspalace.tusky.util.Error +import com.keylesspalace.tusky.util.Loading +import com.keylesspalace.tusky.util.Resource +import com.keylesspalace.tusky.util.Success +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.viewmodel.EditProfileViewModel import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.sizeDp -import com.canhub.cropper.CropImage import javax.inject.Inject class EditProfileActivity : BaseActivity(), Injectable { @@ -110,11 +116,11 @@ class EditProfileActivity : BaseActivity(), Injectable { binding.addFieldButton.setOnClickListener { accountFieldEditAdapter.addField() - if(accountFieldEditAdapter.itemCount >= MAX_ACCOUNT_FIELDS) { + if (accountFieldEditAdapter.itemCount >= MAX_ACCOUNT_FIELDS) { it.isVisible = false } - binding.scrollView.post{ + binding.scrollView.post { binding.scrollView.smoothScrollTo(0, it.bottom) } } @@ -134,23 +140,22 @@ class EditProfileActivity : BaseActivity(), Injectable { accountFieldEditAdapter.setFields(me.source?.fields ?: emptyList()) binding.addFieldButton.isEnabled = me.source?.fields?.size ?: 0 < MAX_ACCOUNT_FIELDS - if(viewModel.avatarData.value == null) { + if (viewModel.avatarData.value == null) { Glide.with(this) - .load(me.avatar) - .placeholder(R.drawable.avatar_default) - .transform( - FitCenter(), - RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp)) - ) - .into(binding.avatarPreview) + .load(me.avatar) + .placeholder(R.drawable.avatar_default) + .transform( + FitCenter(), + RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp)) + ) + .into(binding.avatarPreview) } - if(viewModel.headerData.value == null) { + if (viewModel.headerData.value == null) { Glide.with(this) - .load(me.header) - .into(binding.headerPreview) + .load(me.header) + .into(binding.headerPreview) } - } } is Error -> { @@ -159,7 +164,6 @@ class EditProfileActivity : BaseActivity(), Injectable { viewModel.obtainProfile() } snackbar.show() - } } } @@ -179,20 +183,22 @@ class EditProfileActivity : BaseActivity(), Injectable { observeImage(viewModel.avatarData, binding.avatarPreview, binding.avatarProgressBar, true) observeImage(viewModel.headerData, binding.headerPreview, binding.headerProgressBar, false) - viewModel.saveData.observe(this, { - when(it) { - is Success -> { - finish() - } - is Loading -> { - binding.saveProgressBar.visibility = View.VISIBLE - } - is Error -> { - onSaveFailure(it.errorMessage) + viewModel.saveData.observe( + this, + { + when (it) { + is Success -> { + finish() + } + is Loading -> { + binding.saveProgressBar.visibility = View.VISIBLE + } + is Error -> { + onSaveFailure(it.errorMessage) + } } } - }) - + ) } override fun onSaveInstanceState(outState: Bundle) { @@ -202,50 +208,56 @@ class EditProfileActivity : BaseActivity(), Injectable { override fun onStop() { super.onStop() - if(!isFinishing) { - viewModel.updateProfile(binding.displayNameEditText.text.toString(), - binding.noteEditText.text.toString(), - binding.lockedCheckBox.isChecked, - accountFieldEditAdapter.getFieldData()) + if (!isFinishing) { + viewModel.updateProfile( + binding.displayNameEditText.text.toString(), + binding.noteEditText.text.toString(), + binding.lockedCheckBox.isChecked, + accountFieldEditAdapter.getFieldData() + ) } } - private fun observeImage(liveData: LiveData>, - imageView: ImageView, - progressBar: View, - roundedCorners: Boolean) { - liveData.observe(this, { + private fun observeImage( + liveData: LiveData>, + imageView: ImageView, + progressBar: View, + roundedCorners: Boolean + ) { + liveData.observe( + this, + { - when (it) { - is Success -> { - val glide = Glide.with(imageView) + when (it) { + is Success -> { + val glide = Glide.with(imageView) .load(it.data) - if (roundedCorners) { - glide.transform( - FitCenter(), - RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp)) - ) - } + if (roundedCorners) { + glide.transform( + FitCenter(), + RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp)) + ) + } - glide.into(imageView) + glide.into(imageView) - imageView.show() - progressBar.hide() - } - is Loading -> { - progressBar.show() - } - is Error -> { - progressBar.hide() - if(!it.consumed) { - onResizeFailure() - it.consumed = true + imageView.show() + progressBar.hide() + } + is Loading -> { + progressBar.show() + } + is Error -> { + progressBar.hide() + if (!it.consumed) { + onResizeFailure() + it.consumed = true + } } - } } - }) + ) } private fun onMediaPick(pickType: PickType) { @@ -261,8 +273,11 @@ class EditProfileActivity : BaseActivity(), Injectable { } } - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, - grantResults: IntArray) { + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { when (requestCode) { PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE -> { if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { @@ -307,14 +322,16 @@ class EditProfileActivity : BaseActivity(), Injectable { private fun save() { if (currentlyPicking != PickType.NOTHING) { - return + return } - viewModel.save(binding.displayNameEditText.text.toString(), - binding.noteEditText.text.toString(), - binding.lockedCheckBox.isChecked, - accountFieldEditAdapter.getFieldData(), - this) + viewModel.save( + binding.displayNameEditText.text.toString(), + binding.noteEditText.text.toString(), + binding.lockedCheckBox.isChecked, + accountFieldEditAdapter.getFieldData(), + this + ) } private fun onSaveFailure(msg: String?) { @@ -352,10 +369,10 @@ class EditProfileActivity : BaseActivity(), Injectable { AVATAR_PICK_RESULT -> { if (resultCode == Activity.RESULT_OK && data != null) { CropImage.activity(data.data) - .setInitialCropWindowPaddingRatio(0f) - .setOutputCompressFormat(Bitmap.CompressFormat.PNG) - .setAspectRatio(AVATAR_SIZE, AVATAR_SIZE) - .start(this) + .setInitialCropWindowPaddingRatio(0f) + .setOutputCompressFormat(Bitmap.CompressFormat.PNG) + .setAspectRatio(AVATAR_SIZE, AVATAR_SIZE) + .start(this) } else { endMediaPicking() } @@ -363,10 +380,10 @@ class EditProfileActivity : BaseActivity(), Injectable { HEADER_PICK_RESULT -> { if (resultCode == Activity.RESULT_OK && data != null) { CropImage.activity(data.data) - .setInitialCropWindowPaddingRatio(0f) - .setOutputCompressFormat(Bitmap.CompressFormat.PNG) - .setAspectRatio(HEADER_WIDTH, HEADER_HEIGHT) - .start(this) + .setInitialCropWindowPaddingRatio(0f) + .setOutputCompressFormat(Bitmap.CompressFormat.PNG) + .setAspectRatio(HEADER_WIDTH, HEADER_HEIGHT) + .start(this) } else { endMediaPicking() } @@ -383,7 +400,7 @@ class EditProfileActivity : BaseActivity(), Injectable { } private fun beginResize(uri: Uri?) { - if(uri == null) { + if (uri == null) { currentlyPicking = PickType.NOTHING return } @@ -409,5 +426,4 @@ class EditProfileActivity : BaseActivity(), Injectable { Snackbar.make(binding.avatarButton, R.string.error_media_upload_sending, Snackbar.LENGTH_LONG).show() endMediaPicking() } - } diff --git a/app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt b/app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt index 1fe165b74..d6de5d8ec 100644 --- a/app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt @@ -22,10 +22,9 @@ import retrofit2.Call import retrofit2.Callback import retrofit2.Response import java.io.IOException -import java.lang.Exception import javax.inject.Inject -class FiltersActivity: BaseActivity() { +class FiltersActivity : BaseActivity() { @Inject lateinit var api: MastodonApi @@ -34,7 +33,7 @@ class FiltersActivity: BaseActivity() { private val binding by viewBinding(ActivityFiltersBinding::inflate) - private lateinit var context : String + private lateinit var context: String private lateinit var filters: MutableList override fun onCreate(savedInstanceState: Bundle?) { @@ -58,7 +57,7 @@ class FiltersActivity: BaseActivity() { private fun updateFilter(filter: Filter, itemIndex: Int) { api.updateFilter(filter.id, filter.phrase, filter.context, filter.irreversible, filter.wholeWord, filter.expiresAt) - .enqueue(object: Callback{ + .enqueue(object : Callback { override fun onFailure(call: Call, t: Throwable) { Toast.makeText(this@FiltersActivity, "Error updating filter '${filter.phrase}'", Toast.LENGTH_SHORT).show() } @@ -80,7 +79,7 @@ class FiltersActivity: BaseActivity() { val filter = filters[itemIndex] if (filter.context.size == 1) { // This is the only context for this filter; delete it - api.deleteFilter(filters[itemIndex].id).enqueue(object: Callback { + api.deleteFilter(filters[itemIndex].id).enqueue(object : Callback { override fun onFailure(call: Call, t: Throwable) { Toast.makeText(this@FiltersActivity, "Error updating filter '${filters[itemIndex].phrase}'", Toast.LENGTH_SHORT).show() } @@ -94,17 +93,19 @@ class FiltersActivity: BaseActivity() { } else { // Keep the filter, but remove it from this context val oldFilter = filters[itemIndex] - val newFilter = Filter(oldFilter.id, oldFilter.phrase, oldFilter.context.filter { c -> c != context }, - oldFilter.expiresAt, oldFilter.irreversible, oldFilter.wholeWord) + val newFilter = Filter( + oldFilter.id, oldFilter.phrase, oldFilter.context.filter { c -> c != context }, + oldFilter.expiresAt, oldFilter.irreversible, oldFilter.wholeWord + ) updateFilter(newFilter, itemIndex) } } private fun createFilter(phrase: String, wholeWord: Boolean) { - api.createFilter(phrase, listOf(context), false, wholeWord, "").enqueue(object: Callback { + api.createFilter(phrase, listOf(context), false, wholeWord, "").enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { val filterResponse = response.body() - if(response.isSuccessful && filterResponse != null) { + if (response.isSuccessful && filterResponse != null) { filters.add(filterResponse) refreshFilterDisplay() eventHub.dispatch(PreferenceChangedEvent(context)) @@ -123,13 +124,13 @@ class FiltersActivity: BaseActivity() { val binding = DialogFilterBinding.inflate(layoutInflater) binding.phraseWholeWord.isChecked = true AlertDialog.Builder(this@FiltersActivity) - .setTitle(R.string.filter_addition_dialog_title) - .setView(binding.root) - .setPositiveButton(android.R.string.ok){ _, _ -> - createFilter(binding.phraseEditText.text.toString(), binding.phraseWholeWord.isChecked) - } - .setNeutralButton(android.R.string.cancel, null) - .show() + .setTitle(R.string.filter_addition_dialog_title) + .setView(binding.root) + .setPositiveButton(android.R.string.ok) { _, _ -> + createFilter(binding.phraseEditText.text.toString(), binding.phraseWholeWord.isChecked) + } + .setNeutralButton(android.R.string.cancel, null) + .show() } private fun setupEditDialogForItem(itemIndex: Int) { @@ -139,19 +140,21 @@ class FiltersActivity: BaseActivity() { binding.phraseWholeWord.isChecked = filter.wholeWord AlertDialog.Builder(this@FiltersActivity) - .setTitle(R.string.filter_edit_dialog_title) - .setView(binding.root) - .setPositiveButton(R.string.filter_dialog_update_button) { _, _ -> - val oldFilter = filters[itemIndex] - val newFilter = Filter(oldFilter.id, binding.phraseEditText.text.toString(), oldFilter.context, - oldFilter.expiresAt, oldFilter.irreversible, binding.phraseWholeWord.isChecked) - updateFilter(newFilter, itemIndex) - } - .setNegativeButton(R.string.filter_dialog_remove_button) { _, _ -> - deleteFilter(itemIndex) - } - .setNeutralButton(android.R.string.cancel, null) - .show() + .setTitle(R.string.filter_edit_dialog_title) + .setView(binding.root) + .setPositiveButton(R.string.filter_dialog_update_button) { _, _ -> + val oldFilter = filters[itemIndex] + val newFilter = Filter( + oldFilter.id, binding.phraseEditText.text.toString(), oldFilter.context, + oldFilter.expiresAt, oldFilter.irreversible, binding.phraseWholeWord.isChecked + ) + updateFilter(newFilter, itemIndex) + } + .setNegativeButton(R.string.filter_dialog_remove_button) { _, _ -> + deleteFilter(itemIndex) + } + .setNeutralButton(android.R.string.cancel, null) + .show() } private fun refreshFilterDisplay() { @@ -173,11 +176,15 @@ class FiltersActivity: BaseActivity() { binding.filterProgressBar.hide() binding.filterMessageView.show() if (t is IOException) { - binding.filterMessageView.setup(R.drawable.elephant_offline, - R.string.error_network) { loadFilters() } + binding.filterMessageView.setup( + R.drawable.elephant_offline, + R.string.error_network + ) { loadFilters() } } else { - binding.filterMessageView.setup(R.drawable.elephant_error, - R.string.error_generic) { loadFilters() } + binding.filterMessageView.setup( + R.drawable.elephant_error, + R.string.error_generic + ) { loadFilters() } } return@launch } @@ -195,4 +202,4 @@ class FiltersActivity: BaseActivity() { const val FILTERS_CONTEXT = "filters_context" const val FILTERS_TITLE = "filters_title" } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/LicenseActivity.kt b/app/src/main/java/com/keylesspalace/tusky/LicenseActivity.kt index 406a4aafb..ed3aed3a5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/LicenseActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/LicenseActivity.kt @@ -16,9 +16,9 @@ package com.keylesspalace.tusky import android.os.Bundle -import androidx.annotation.RawRes import android.util.Log import android.widget.TextView +import androidx.annotation.RawRes import com.keylesspalace.tusky.databinding.ActivityLicenseBinding import com.keylesspalace.tusky.util.IOUtils import java.io.BufferedReader @@ -41,7 +41,6 @@ class LicenseActivity : BaseActivity() { setTitle(R.string.title_licenses) loadFileIntoTextView(R.raw.apache, binding.licenseApacheTextView) - } private fun loadFileIntoTextView(@RawRes fileId: Int, textView: TextView) { diff --git a/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt index cb6acd866..e816a0a55 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt @@ -23,25 +23,41 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.* +import android.widget.EditText +import android.widget.FrameLayout +import android.widget.ImageButton +import android.widget.PopupMenu +import android.widget.TextView import androidx.activity.viewModels import androidx.annotation.StringRes import androidx.appcompat.app.AlertDialog -import androidx.recyclerview.widget.* +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView import at.connyduck.sparkbutton.helpers.Utils import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from import autodispose2.autoDispose import com.google.android.material.snackbar.Snackbar +import com.keylesspalace.tusky.components.timeline.TimelineViewModel import com.keylesspalace.tusky.databinding.ActivityListsBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.entity.MastoList -import com.keylesspalace.tusky.components.timeline.TimelineViewModel -import com.keylesspalace.tusky.util.* +import com.keylesspalace.tusky.util.ThemeUtils +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.onTextChanged +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.viewBinding +import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.viewmodel.ListsViewModel -import com.keylesspalace.tusky.viewmodel.ListsViewModel.Event.* -import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.* +import com.keylesspalace.tusky.viewmodel.ListsViewModel.Event +import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.ERROR_NETWORK +import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.ERROR_OTHER +import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.INITIAL +import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.LOADED +import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.LOADING import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.utils.colorInt @@ -84,12 +100,13 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector { binding.listsRecycler.adapter = adapter binding.listsRecycler.layoutManager = LinearLayoutManager(this) binding.listsRecycler.addItemDecoration( - DividerItemDecoration(this, DividerItemDecoration.VERTICAL)) + DividerItemDecoration(this, DividerItemDecoration.VERTICAL) + ) viewModel.state - .observeOn(AndroidSchedulers.mainThread()) - .autoDispose(from(this)) - .subscribe(this::update) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(from(this)) + .subscribe(this::update) viewModel.retryLoading() binding.addListButton.setOnClickListener { @@ -97,15 +114,15 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector { } viewModel.events.observeOn(AndroidSchedulers.mainThread()) - .autoDispose(from(this)) - .subscribe { event -> - @Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA") - when (event) { - CREATE_ERROR -> showMessage(R.string.error_create_list) - RENAME_ERROR -> showMessage(R.string.error_rename_list) - DELETE_ERROR -> showMessage(R.string.error_delete_list) - } + .autoDispose(from(this)) + .subscribe { event -> + @Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA") + when (event) { + Event.CREATE_ERROR -> showMessage(R.string.error_create_list) + Event.RENAME_ERROR -> showMessage(R.string.error_rename_list) + Event.DELETE_ERROR -> showMessage(R.string.error_delete_list) } + } } private fun showlistNameDialog(list: MastoList?) { @@ -115,17 +132,18 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector { layout.addView(editText) val margin = Utils.dpToPx(this, 8) (editText.layoutParams as ViewGroup.MarginLayoutParams) - .setMargins(margin, margin, margin, 0) + .setMargins(margin, margin, margin, 0) val dialog = AlertDialog.Builder(this) - .setView(layout) - .setPositiveButton( - if (list == null) R.string.action_create_list - else R.string.action_rename_list) { _, _ -> - onPickedDialogName(editText.text, list?.id) - } - .setNegativeButton(android.R.string.cancel, null) - .show() + .setView(layout) + .setPositiveButton( + if (list == null) R.string.action_create_list + else R.string.action_rename_list + ) { _, _ -> + onPickedDialogName(editText.text, list?.id) + } + .setNegativeButton(android.R.string.cancel, null) + .show() val positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE) editText.onTextChanged { s, _, _, _ -> @@ -137,15 +155,14 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector { private fun showListDeleteDialog(list: MastoList) { AlertDialog.Builder(this) - .setMessage(getString(R.string.dialog_delete_list_warning, list.title)) - .setPositiveButton(R.string.action_delete){ _, _ -> - viewModel.deleteList(list.id) - } - .setNegativeButton(android.R.string.cancel, null) - .show() + .setMessage(getString(R.string.dialog_delete_list_warning, list.title)) + .setPositiveButton(R.string.action_delete) { _, _ -> + viewModel.deleteList(list.id) + } + .setNegativeButton(android.R.string.cancel, null) + .show() } - private fun update(state: ListsViewModel.State) { adapter.submitList(state.lists) binding.progressBar.visible(state.loadingState == LOADING) @@ -166,8 +183,10 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector { LOADED -> if (state.lists.isEmpty()) { binding.messageView.show() - binding.messageView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, - null) + binding.messageView.setup( + R.drawable.elephant_friend_empty, R.string.message_empty, + null + ) } else { binding.messageView.hide() } @@ -176,13 +195,14 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector { private fun showMessage(@StringRes messageId: Int) { Snackbar.make( - binding.listsRecycler, messageId, Snackbar.LENGTH_SHORT + binding.listsRecycler, messageId, Snackbar.LENGTH_SHORT ).show() } private fun onListSelected(listId: String) { startActivityWithSlideInAnimation( - ModalTimelineActivity.newIntent(this, TimelineViewModel.Kind.LIST, listId)) + ModalTimelineActivity.newIntent(this, TimelineViewModel.Kind.LIST, listId) + ) } private fun openListSettings(list: MastoList) { @@ -219,27 +239,28 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector { } } - private inner class ListsAdapter - : ListAdapter(ListsDiffer) { + private inner class ListsAdapter : + ListAdapter(ListsDiffer) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder { return LayoutInflater.from(parent.context).inflate(R.layout.item_list, parent, false) - .let(this::ListViewHolder) - .apply { - val context = nameTextView.context - val iconColor = ThemeUtils.getColor(context, android.R.attr.textColorTertiary) - val icon = IconicsDrawable(context, GoogleMaterial.Icon.gmd_list).apply { sizeDp = 20; colorInt = iconColor } + .let(this::ListViewHolder) + .apply { + val context = nameTextView.context + val iconColor = ThemeUtils.getColor(context, android.R.attr.textColorTertiary) + val icon = IconicsDrawable(context, GoogleMaterial.Icon.gmd_list).apply { sizeDp = 20; colorInt = iconColor } - nameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null) - } + nameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null) + } } override fun onBindViewHolder(holder: ListViewHolder, position: Int) { holder.nameTextView.text = getItem(position).title } - private inner class ListViewHolder(view: View) : RecyclerView.ViewHolder(view), - View.OnClickListener { + private inner class ListViewHolder(view: View) : + RecyclerView.ViewHolder(view), + View.OnClickListener { val nameTextView: TextView = view.findViewById(R.id.list_name_textview) val moreButton: ImageButton = view.findViewById(R.id.editListButton) @@ -271,4 +292,4 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector { companion object { fun newIntent(context: Context) = Intent(context, ListsActivity::class.java) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/LoginActivity.kt b/app/src/main/java/com/keylesspalace/tusky/LoginActivity.kt index 2ba797983..08140b63d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/LoginActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/LoginActivity.kt @@ -34,7 +34,11 @@ import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.entity.AccessToken import com.keylesspalace.tusky.entity.AppCredentials import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.util.* +import com.keylesspalace.tusky.util.ThemeUtils +import com.keylesspalace.tusky.util.getNonNullString +import com.keylesspalace.tusky.util.rickRoll +import com.keylesspalace.tusky.util.shouldRickRoll +import com.keylesspalace.tusky.util.viewBinding import okhttp3.HttpUrl import retrofit2.Call import retrofit2.Callback @@ -62,28 +66,29 @@ class LoginActivity : BaseActivity(), Injectable { setContentView(binding.root) - if(savedInstanceState == null && BuildConfig.CUSTOM_INSTANCE.isNotBlank() && !isAdditionalLogin()) { + if (savedInstanceState == null && BuildConfig.CUSTOM_INSTANCE.isNotBlank() && !isAdditionalLogin()) { binding.domainEditText.setText(BuildConfig.CUSTOM_INSTANCE) binding.domainEditText.setSelection(BuildConfig.CUSTOM_INSTANCE.length) } - if(BuildConfig.CUSTOM_LOGO_URL.isNotBlank()) { + if (BuildConfig.CUSTOM_LOGO_URL.isNotBlank()) { Glide.with(binding.loginLogo) - .load(BuildConfig.CUSTOM_LOGO_URL) - .placeholder(null) - .into(binding.loginLogo) + .load(BuildConfig.CUSTOM_LOGO_URL) + .placeholder(null) + .into(binding.loginLogo) } preferences = getSharedPreferences( - getString(R.string.preferences_file_key), Context.MODE_PRIVATE) + getString(R.string.preferences_file_key), Context.MODE_PRIVATE + ) binding.loginButton.setOnClickListener { onButtonClick() } binding.whatsAnInstanceTextView.setOnClickListener { val dialog = AlertDialog.Builder(this) - .setMessage(R.string.dialog_whats_an_instance) - .setPositiveButton(R.string.action_close, null) - .show() + .setMessage(R.string.dialog_whats_an_instance) + .setPositiveButton(R.string.action_close, null) + .show() val textView = dialog.findViewById(android.R.id.message) textView?.movementMethod = LinkMovementMethod.getInstance() } @@ -95,7 +100,6 @@ class LoginActivity : BaseActivity(), Injectable { } else { binding.toolbar.visibility = View.GONE } - } override fun requiresLogin(): Boolean { @@ -104,7 +108,7 @@ class LoginActivity : BaseActivity(), Injectable { override fun finish() { super.finish() - if(isAdditionalLogin()) { + if (isAdditionalLogin()) { overridePendingTransition(R.anim.slide_from_left, R.anim.slide_to_right) } } @@ -134,8 +138,10 @@ class LoginActivity : BaseActivity(), Injectable { } val callback = object : Callback { - override fun onResponse(call: Call, - response: Response) { + override fun onResponse( + call: Call, + response: Response + ) { if (!response.isSuccessful) { binding.loginButton.isEnabled = true binding.domainTextInputLayout.error = getString(R.string.error_failed_app_registration) @@ -148,10 +154,10 @@ class LoginActivity : BaseActivity(), Injectable { val clientSecret = credentials.clientSecret preferences.edit() - .putString("domain", domain) - .putString("clientId", clientId) - .putString("clientSecret", clientSecret) - .apply() + .putString("domain", domain) + .putString("clientId", clientId) + .putString("clientSecret", clientSecret) + .apply() redirectUserToAuthorizeAndLogin(domain, clientId) } @@ -165,11 +171,12 @@ class LoginActivity : BaseActivity(), Injectable { } mastodonApi - .authenticateApp(domain, getString(R.string.app_name), oauthRedirectUri, - OAUTH_SCOPES, getString(R.string.tusky_website)) - .enqueue(callback) + .authenticateApp( + domain, getString(R.string.app_name), oauthRedirectUri, + OAUTH_SCOPES, getString(R.string.tusky_website) + ) + .enqueue(callback) setLoading(true) - } private fun redirectUserToAuthorizeAndLogin(domain: String, clientId: String) { @@ -177,10 +184,10 @@ class LoginActivity : BaseActivity(), Injectable { * login there, and the server will redirect back to the app with its response. */ val endpoint = MastodonApi.ENDPOINT_AUTHORIZE val parameters = mapOf( - "client_id" to clientId, - "redirect_uri" to oauthRedirectUri, - "response_type" to "code", - "scope" to OAUTH_SCOPES + "client_id" to clientId, + "redirect_uri" to oauthRedirectUri, + "response_type" to "code", + "scope" to OAUTH_SCOPES ) val url = "https://" + domain + endpoint + "?" + toQueryString(parameters) val uri = Uri.parse(url) @@ -224,31 +231,27 @@ class LoginActivity : BaseActivity(), Injectable { } else { setLoading(false) binding.domainTextInputLayout.error = getString(R.string.error_retrieving_oauth_token) - Log.e(TAG, String.format("%s %s", - getString(R.string.error_retrieving_oauth_token), - response.message())) + Log.e(TAG, "%s %s".format(getString(R.string.error_retrieving_oauth_token), response.message())) } } override fun onFailure(call: Call, t: Throwable) { setLoading(false) binding.domainTextInputLayout.error = getString(R.string.error_retrieving_oauth_token) - Log.e(TAG, String.format("%s %s", - getString(R.string.error_retrieving_oauth_token), - t.message)) + Log.e(TAG, "%s %s".format(getString(R.string.error_retrieving_oauth_token), t.message)) } } - mastodonApi.fetchOAuthToken(domain, clientId, clientSecret, redirectUri, code, - "authorization_code").enqueue(callback) + mastodonApi.fetchOAuthToken( + domain, clientId, clientSecret, redirectUri, code, + "authorization_code" + ).enqueue(callback) } else if (error != null) { /* Authorization failed. Put the error response where the user can read it and they * can try again. */ setLoading(false) binding.domainTextInputLayout.error = getString(R.string.error_authorization_denied) - Log.e(TAG, String.format("%s %s", - getString(R.string.error_authorization_denied), - error)) + Log.e(TAG, "%s %s".format(getString(R.string.error_authorization_denied), error)) } else { // This case means a junk response was received somehow. setLoading(false) @@ -340,14 +343,14 @@ class LoginActivity : BaseActivity(), Injectable { val navigationbarDividerColor = ThemeUtils.getColor(context, R.attr.dividerColor) val colorSchemeParams = CustomTabColorSchemeParams.Builder() - .setToolbarColor(toolbarColor) - .setNavigationBarColor(navigationbarColor) - .setNavigationBarDividerColor(navigationbarDividerColor) - .build() + .setToolbarColor(toolbarColor) + .setNavigationBarColor(navigationbarColor) + .setNavigationBarDividerColor(navigationbarDividerColor) + .build() val customTabsIntent = CustomTabsIntent.Builder() - .setDefaultColorSchemeParams(colorSchemeParams) - .build() + .setDefaultColorSchemeParams(colorSchemeParams) + .build() try { customTabsIntent.launchUrl(context, uri) diff --git a/app/src/main/java/com/keylesspalace/tusky/ModalTimelineActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ModalTimelineActivity.kt index 000cf3a9f..dbcde89dc 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ModalTimelineActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/ModalTimelineActivity.kt @@ -4,9 +4,9 @@ import android.content.Context import android.content.Intent import android.os.Bundle import com.google.android.material.floatingactionbutton.FloatingActionButton -import com.keylesspalace.tusky.databinding.ActivityModalTimelineBinding import com.keylesspalace.tusky.components.timeline.TimelineFragment import com.keylesspalace.tusky.components.timeline.TimelineViewModel +import com.keylesspalace.tusky.databinding.ActivityModalTimelineBinding import com.keylesspalace.tusky.interfaces.ActionButtonActivity import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector @@ -31,11 +31,11 @@ class ModalTimelineActivity : BottomSheetActivity(), ActionButtonActivity, HasAn if (supportFragmentManager.findFragmentById(R.id.contentFrame) == null) { val kind = intent?.getSerializableExtra(ARG_KIND) as? TimelineViewModel.Kind - ?: TimelineViewModel.Kind.HOME + ?: TimelineViewModel.Kind.HOME val argument = intent?.getStringExtra(ARG_ARG) supportFragmentManager.beginTransaction() - .replace(R.id.contentFrame, TimelineFragment.newInstance(kind, argument)) - .commit() + .replace(R.id.contentFrame, TimelineFragment.newInstance(kind, argument)) + .commit() } } @@ -48,13 +48,15 @@ class ModalTimelineActivity : BottomSheetActivity(), ActionButtonActivity, HasAn private const val ARG_ARG = "arg" @JvmStatic - fun newIntent(context: Context, kind: TimelineViewModel.Kind, - argument: String?): Intent { + fun newIntent( + context: Context, + kind: TimelineViewModel.Kind, + argument: String? + ): Intent { val intent = Intent(context, ModalTimelineActivity::class.java) intent.putExtra(ARG_KIND, kind) intent.putExtra(ARG_ARG, argument) return intent } - } } diff --git a/app/src/main/java/com/keylesspalace/tusky/SplashActivity.kt b/app/src/main/java/com/keylesspalace/tusky/SplashActivity.kt index 07c54e973..1b7e29947 100644 --- a/app/src/main/java/com/keylesspalace/tusky/SplashActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/SplashActivity.kt @@ -18,10 +18,9 @@ package com.keylesspalace.tusky import android.content.Intent import android.os.Bundle import androidx.appcompat.app.AppCompatActivity +import com.keylesspalace.tusky.components.notifications.NotificationHelper import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.di.Injectable - -import com.keylesspalace.tusky.components.notifications.NotificationHelper import javax.inject.Inject class SplashActivity : AppCompatActivity(), Injectable { @@ -46,5 +45,4 @@ class SplashActivity : AppCompatActivity(), Injectable { startActivity(intent) finish() } - } diff --git a/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt b/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt index 219d47df5..ac9dba6bd 100644 --- a/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt @@ -19,15 +19,12 @@ import android.content.Context import android.content.Intent import android.os.Bundle import androidx.fragment.app.commit -import com.keylesspalace.tusky.databinding.ActivityStatuslistBinding - import com.keylesspalace.tusky.components.timeline.TimelineFragment import com.keylesspalace.tusky.components.timeline.TimelineViewModel.Kind - -import javax.inject.Inject - +import com.keylesspalace.tusky.databinding.ActivityStatuslistBinding import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector +import javax.inject.Inject class StatusListActivity : BottomSheetActivity(), HasAndroidInjector { @@ -44,7 +41,7 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector { setSupportActionBar(binding.includedToolbar.toolbar) - val title = if(kind == Kind.FAVOURITES) { + val title = if (kind == Kind.FAVOURITES) { R.string.title_favourites } else { R.string.title_bookmarks @@ -60,7 +57,6 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector { val fragment = TimelineFragment.newInstance(kind) replace(R.id.fragment_container, fragment) } - } override fun androidInjector() = dispatchingAndroidInjector @@ -71,15 +67,14 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector { @JvmStatic fun newFavouritesIntent(context: Context) = - Intent(context, StatusListActivity::class.java).apply { - putExtra(EXTRA_KIND, Kind.FAVOURITES.name) - } + Intent(context, StatusListActivity::class.java).apply { + putExtra(EXTRA_KIND, Kind.FAVOURITES.name) + } @JvmStatic fun newBookmarksIntent(context: Context) = - Intent(context, StatusListActivity::class.java).apply { - putExtra(EXTRA_KIND, Kind.BOOKMARKS.name) - } + Intent(context, StatusListActivity::class.java).apply { + putExtra(EXTRA_KIND, Kind.BOOKMARKS.name) + } } - } diff --git a/app/src/main/java/com/keylesspalace/tusky/TabData.kt b/app/src/main/java/com/keylesspalace/tusky/TabData.kt index 5eabec5f4..fa1876ce3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TabData.kt +++ b/app/src/main/java/com/keylesspalace/tusky/TabData.kt @@ -20,9 +20,9 @@ import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.fragment.app.Fragment import com.keylesspalace.tusky.components.conversation.ConversationsFragment -import com.keylesspalace.tusky.fragment.NotificationsFragment import com.keylesspalace.tusky.components.timeline.TimelineFragment import com.keylesspalace.tusky.components.timeline.TimelineViewModel +import com.keylesspalace.tusky.fragment.NotificationsFragment /** this would be a good case for a sealed class, but that does not work nice with Room */ @@ -34,71 +34,72 @@ const val DIRECT = "Direct" const val HASHTAG = "Hashtag" const val LIST = "List" -data class TabData(val id: String, - @StringRes val text: Int, - @DrawableRes val icon: Int, - val fragment: (List) -> Fragment, - val arguments: List = emptyList(), - val title: (Context) -> String = { context -> context.getString(text)} - ) +data class TabData( + val id: String, + @StringRes val text: Int, + @DrawableRes val icon: Int, + val fragment: (List) -> Fragment, + val arguments: List = emptyList(), + val title: (Context) -> String = { context -> context.getString(text) } +) fun createTabDataFromId(id: String, arguments: List = emptyList()): TabData { return when (id) { HOME -> TabData( - HOME, - R.string.title_home, - R.drawable.ic_home_24dp, - { TimelineFragment.newInstance(TimelineViewModel.Kind.HOME) } + HOME, + R.string.title_home, + R.drawable.ic_home_24dp, + { TimelineFragment.newInstance(TimelineViewModel.Kind.HOME) } ) NOTIFICATIONS -> TabData( - NOTIFICATIONS, - R.string.title_notifications, - R.drawable.ic_notifications_24dp, - { NotificationsFragment.newInstance() } + NOTIFICATIONS, + R.string.title_notifications, + R.drawable.ic_notifications_24dp, + { NotificationsFragment.newInstance() } ) LOCAL -> TabData( - LOCAL, - R.string.title_public_local, - R.drawable.ic_local_24dp, - { TimelineFragment.newInstance(TimelineViewModel.Kind.PUBLIC_LOCAL) } + LOCAL, + R.string.title_public_local, + R.drawable.ic_local_24dp, + { TimelineFragment.newInstance(TimelineViewModel.Kind.PUBLIC_LOCAL) } ) FEDERATED -> TabData( - FEDERATED, - R.string.title_public_federated, - R.drawable.ic_public_24dp, - { TimelineFragment.newInstance(TimelineViewModel.Kind.PUBLIC_FEDERATED) } + FEDERATED, + R.string.title_public_federated, + R.drawable.ic_public_24dp, + { TimelineFragment.newInstance(TimelineViewModel.Kind.PUBLIC_FEDERATED) } ) DIRECT -> TabData( - DIRECT, - R.string.title_direct_messages, - R.drawable.ic_reblog_direct_24dp, - { ConversationsFragment.newInstance() } + DIRECT, + R.string.title_direct_messages, + R.drawable.ic_reblog_direct_24dp, + { ConversationsFragment.newInstance() } ) HASHTAG -> TabData( - HASHTAG, - R.string.hashtags, - R.drawable.ic_hashtag, - { args -> TimelineFragment.newHashtagInstance(args) }, - arguments, - { context -> arguments.joinToString(separator = " ") { context.getString(R.string.title_tag, it) }} + HASHTAG, + R.string.hashtags, + R.drawable.ic_hashtag, + { args -> TimelineFragment.newHashtagInstance(args) }, + arguments, + { context -> arguments.joinToString(separator = " ") { context.getString(R.string.title_tag, it) } } ) LIST -> TabData( - LIST, - R.string.list, - R.drawable.ic_list, - { args -> TimelineFragment.newInstance(TimelineViewModel.Kind.LIST, args.getOrNull(0).orEmpty()) }, - arguments, - { arguments.getOrNull(1).orEmpty() } - ) + LIST, + R.string.list, + R.drawable.ic_list, + { args -> TimelineFragment.newInstance(TimelineViewModel.Kind.LIST, args.getOrNull(0).orEmpty()) }, + arguments, + { arguments.getOrNull(1).orEmpty() } + ) else -> throw IllegalArgumentException("unknown tab type") } } fun defaultTabs(): List { return listOf( - createTabDataFromId(HOME), - createTabDataFromId(NOTIFICATIONS), - createTabDataFromId(LOCAL), - createTabDataFromId(FEDERATED) + createTabDataFromId(HOME), + createTabDataFromId(NOTIFICATIONS), + createTabDataFromId(LOCAL), + createTabDataFromId(FEDERATED) ) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt b/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt index acb5529e8..720664bde 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt @@ -221,26 +221,26 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene frameLayout.addView(editText) val dialog = AlertDialog.Builder(this) - .setTitle(R.string.add_hashtag_title) - .setView(frameLayout) - .setNegativeButton(android.R.string.cancel, null) - .setPositiveButton(R.string.action_save) { _, _ -> - val input = editText.text.toString().trim() - if (tab == null) { - val newTab = createTabDataFromId(HASHTAG, listOf(input)) - currentTabs.add(newTab) - currentTabsAdapter.notifyItemInserted(currentTabs.size - 1) - } else { - val newTab = tab.copy(arguments = tab.arguments + input) - currentTabs[tabPosition] = newTab + .setTitle(R.string.add_hashtag_title) + .setView(frameLayout) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(R.string.action_save) { _, _ -> + val input = editText.text.toString().trim() + if (tab == null) { + val newTab = createTabDataFromId(HASHTAG, listOf(input)) + currentTabs.add(newTab) + currentTabsAdapter.notifyItemInserted(currentTabs.size - 1) + } else { + val newTab = tab.copy(arguments = tab.arguments + input) + currentTabs[tabPosition] = newTab - currentTabsAdapter.notifyItemChanged(tabPosition) - } - - updateAvailableTabs() - saveTabs() + currentTabsAdapter.notifyItemChanged(tabPosition) } - .create() + + updateAvailableTabs() + saveTabs() + } + .create() editText.onTextChanged { s, _, _, _ -> dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = validateHashtag(s) @@ -254,28 +254,28 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene private fun showSelectListDialog() { val adapter = ListSelectionAdapter(this) mastodonApi.getLists() - .observeOn(AndroidSchedulers.mainThread()) - .autoDispose(from(this, Lifecycle.Event.ON_DESTROY)) - .subscribe ( - { lists -> - adapter.addAll(lists) - }, - { throwable -> - Log.e("TabPreferenceActivity", "failed to load lists", throwable) - } - ) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(from(this, Lifecycle.Event.ON_DESTROY)) + .subscribe( + { lists -> + adapter.addAll(lists) + }, + { throwable -> + Log.e("TabPreferenceActivity", "failed to load lists", throwable) + } + ) AlertDialog.Builder(this) - .setTitle(R.string.select_list_title) - .setAdapter(adapter) { _, position -> - val list = adapter.getItem(position) - val newTab = createTabDataFromId(LIST, listOf(list!!.id, list.title)) - currentTabs.add(newTab) - currentTabsAdapter.notifyItemInserted(currentTabs.size - 1) - updateAvailableTabs() - saveTabs() - } - .show() + .setTitle(R.string.select_list_title) + .setAdapter(adapter) { _, position -> + val list = adapter.getItem(position) + val newTab = createTabDataFromId(LIST, listOf(list!!.id, list.title)) + currentTabs.add(newTab) + currentTabsAdapter.notifyItemInserted(currentTabs.size - 1) + updateAvailableTabs() + saveTabs() + } + .show() } private fun validateHashtag(input: CharSequence?): Boolean { @@ -330,10 +330,9 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene it.tabPreferences = currentTabs accountManager.saveAccount(it) } - .subscribeOn(Schedulers.io()) - .autoDispose(from(this, Lifecycle.Event.ON_DESTROY)) - .subscribe() - + .subscribeOn(Schedulers.io()) + .autoDispose(from(this, Lifecycle.Event.ON_DESTROY)) + .subscribe() } tabsChanged = true } @@ -357,5 +356,4 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene private const val MIN_TAB_COUNT = 2 private const val MAX_TAB_COUNT = 5 } - } diff --git a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt index 629696bfc..0339a7bcc 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt +++ b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt @@ -68,8 +68,8 @@ class TuskyApplication : Application(), HasAndroidInjector { // init the custom emoji fonts val emojiSelection = preferences.getInt(PrefKeys.EMOJI, 0) val emojiConfig = EmojiCompatFont.byId(emojiSelection) - .getConfig(this) - .setReplaceAll(true) + .getConfig(this) + .setReplaceAll(true) EmojiCompat.init(emojiConfig) // init night mode @@ -81,10 +81,10 @@ class TuskyApplication : Application(), HasAndroidInjector { } WorkManager.initialize( - this, - androidx.work.Configuration.Builder() - .setWorkerFactory(notificationWorkerFactory) - .build() + this, + androidx.work.Configuration.Builder() + .setWorkerFactory(notificationWorkerFactory) + .build() ) } @@ -104,4 +104,4 @@ class TuskyApplication : Application(), HasAndroidInjector { @JvmStatic lateinit var localeManager: LocaleManager } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt index b266444e3..2598b5e22 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt @@ -61,7 +61,7 @@ import java.io.File import java.io.FileNotFoundException import java.io.FileOutputStream import java.io.IOException -import java.util.* +import java.util.Locale typealias ToolbarVisibilityListener = (isVisible: Boolean) -> Unit @@ -102,17 +102,16 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener val realAttachs = attachments!!.map(AttachmentViewData::attachment) // Setup the view pager. ImagePagerAdapter(this, realAttachs, initialPosition) - } else { imageUrl = intent.getStringExtra(EXTRA_SINGLE_IMAGE_URL) - ?: throw IllegalArgumentException("attachment list or image url has to be set") + ?: throw IllegalArgumentException("attachment list or image url has to be set") SingleImagePagerAdapter(this, imageUrl!!) } binding.viewPager.adapter = adapter binding.viewPager.setCurrentItem(initialPosition, false) - binding.viewPager.registerOnPageChangeCallback(object: ViewPager2.OnPageChangeCallback() { + binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { override fun onPageSelected(position: Int) { binding.toolbar.title = getPageTitle(position) } @@ -183,17 +182,17 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener } binding.toolbar.animate().alpha(alpha) - .setListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - binding.toolbar.visibility = visibility - animation.removeListener(this) - } - }) - .start() + .setListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + binding.toolbar.visibility = visibility + animation.removeListener(this) + } + }) + .start() } private fun getPageTitle(position: Int): CharSequence { - if(attachments == null) { + if (attachments == null) { return "" } return String.format(Locale.getDefault(), "%d/%d", position + 1, attachments?.size) @@ -206,8 +205,10 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager val request = DownloadManager.Request(Uri.parse(url)) - request.setDestinationInExternalPublicDir(Environment.DIRECTORY_PICTURES, - getString(R.string.app_name) + "/" + filename) + request.setDestinationInExternalPublicDir( + Environment.DIRECTORY_PICTURES, + getString(R.string.app_name) + "/" + filename + ) downloadManager.enqueue(request) } @@ -261,7 +262,6 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_media_to))) } - private var isCreating: Boolean = false private fun shareImage(directory: File, url: String) { @@ -270,7 +270,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener invalidateOptionsMenu() val file = File(directory, getTemporaryMediaFilename("png")) val futureTask: FutureTarget = - Glide.with(applicationContext).asBitmap().load(Uri.parse(url)).submit() + Glide.with(applicationContext).asBitmap().load(Uri.parse(url)).submit() Single.fromCallable { val bitmap = futureTask.get() try { @@ -284,32 +284,30 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener Log.e(TAG, "Error writing temporary media.") } return@fromCallable false - } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .doOnDispose { - futureTask.cancel(true) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnDispose { + futureTask.cancel(true) + } + .autoDispose(AndroidLifecycleScopeProvider.from(this, Lifecycle.Event.ON_DESTROY)) + .subscribe( + { result -> + Log.d(TAG, "Download image result: $result") + isCreating = false + invalidateOptionsMenu() + binding.progressBarShare.visibility = View.GONE + if (result) + shareFile(file, "image/png") + }, + { error -> + isCreating = false + invalidateOptionsMenu() + binding.progressBarShare.visibility = View.GONE + Log.e(TAG, "Failed to download image", error) } - .autoDispose(AndroidLifecycleScopeProvider.from(this, Lifecycle.Event.ON_DESTROY)) - .subscribe( - { result -> - Log.d(TAG, "Download image result: $result") - isCreating = false - invalidateOptionsMenu() - binding.progressBarShare.visibility = View.GONE - if (result) - shareFile(file, "image/png") - }, - { error -> - isCreating = false - invalidateOptionsMenu() - binding.progressBarShare.visibility = View.GONE - Log.e(TAG, "Failed to download image", error) - } - ) - + ) } private fun shareMediaFile(directory: File, url: String) { @@ -352,7 +350,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener } } -abstract class ViewMediaAdapter(activity: FragmentActivity): FragmentStateAdapter(activity) { +abstract class ViewMediaAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) { abstract fun onTransitionEnd(position: Int) } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountAdapter.kt index f68d022f1..320f8126f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountAdapter.kt @@ -121,5 +121,4 @@ abstract class AccountAdapter internal constructo const val VIEW_TYPE_ACCOUNT = 0 const val VIEW_TYPE_FOOTER = 1 } - -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldAdapter.kt index fe3b15f82..d6dd668a5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldAdapter.kt @@ -16,20 +16,23 @@ package com.keylesspalace.tusky.adapter import android.text.method.LinkMovementMethod -import androidx.recyclerview.widget.RecyclerView import android.view.LayoutInflater import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView import com.keylesspalace.tusky.R import com.keylesspalace.tusky.databinding.ItemAccountFieldBinding import com.keylesspalace.tusky.entity.Emoji import com.keylesspalace.tusky.entity.Field import com.keylesspalace.tusky.entity.IdentityProof import com.keylesspalace.tusky.interfaces.LinkListener -import com.keylesspalace.tusky.util.* +import com.keylesspalace.tusky.util.BindingHolder +import com.keylesspalace.tusky.util.Either +import com.keylesspalace.tusky.util.LinkHelper +import com.keylesspalace.tusky.util.emojify class AccountFieldAdapter( - private val linkListener: LinkListener, - private val animateEmojis: Boolean + private val linkListener: LinkListener, + private val animateEmojis: Boolean ) : RecyclerView.Adapter>() { var emojis: List = emptyList() @@ -47,7 +50,7 @@ class AccountFieldAdapter( val nameTextView = holder.binding.accountFieldName val valueTextView = holder.binding.accountFieldValue - if(proofOrField.isLeft()) { + if (proofOrField.isLeft()) { val identityProof = proofOrField.asLeft() nameTextView.text = identityProof.provider @@ -55,7 +58,7 @@ class AccountFieldAdapter( valueTextView.movementMethod = LinkMovementMethod.getInstance() - valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0) + valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0) } else { val field = proofOrField.asRight() val emojifiedName = field.name.emojify(emojis, nameTextView, animateEmojis) @@ -64,12 +67,11 @@ class AccountFieldAdapter( val emojifiedValue = field.value.emojify(emojis, valueTextView, animateEmojis) LinkHelper.setClickableText(valueTextView, emojifiedValue, null, linkListener) - if(field.verifiedAt != null) { - valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0) + if (field.verifiedAt != null) { + valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0) } else { - valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0 ) + valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0) } } - } } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldEditAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldEditAdapter.kt index f7f4553a3..7ba5537b8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldEditAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldEditAdapter.kt @@ -34,7 +34,7 @@ class AccountFieldEditAdapter : RecyclerView.Adapter fieldData.add(MutableStringPair(field.name, field.value)) } - if(fieldData.isEmpty()) { + if (fieldData.isEmpty()) { fieldData.add(MutableStringPair("", "")) } @@ -63,7 +63,7 @@ class AccountFieldEditAdapter : RecyclerView.Adapter(context, R.layout.item_autocomplete_account) { @@ -48,9 +49,8 @@ class AccountSelectionAdapter(context: Context) : ArrayAdapter(co val animateAvatar = pm.getBoolean("animateGifAvatars", false) loadAvatar(account.profilePictureUrl, binding.avatar, avatarRadius, animateAvatar) - } return binding.root } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.kt index fc7cec24d..33a236056 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.kt @@ -77,4 +77,4 @@ class BlocksAdapter( itemView.setOnClickListener { listener.onViewAccount(id) } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt index c5656115f..dc9ec70dc 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt @@ -22,15 +22,15 @@ import com.bumptech.glide.Glide import com.keylesspalace.tusky.databinding.ItemEmojiButtonBinding import com.keylesspalace.tusky.entity.Emoji import com.keylesspalace.tusky.util.BindingHolder -import java.util.* +import java.util.Locale class EmojiAdapter( - emojiList: List, - private val onEmojiSelectedListener: OnEmojiSelectedListener + emojiList: List, + private val onEmojiSelectedListener: OnEmojiSelectedListener ) : RecyclerView.Adapter>() { - private val emojiList : List = emojiList.filter { emoji -> emoji.visibleInPicker == null || emoji.visibleInPicker } - .sortedBy { it.shortcode.lowercase(Locale.ROOT) } + private val emojiList: List = emojiList.filter { emoji -> emoji.visibleInPicker == null || emoji.visibleInPicker } + .sortedBy { it.shortcode.lowercase(Locale.ROOT) } override fun getItemCount() = emojiList.size @@ -44,8 +44,8 @@ class EmojiAdapter( val emojiImageView = holder.binding.root Glide.with(emojiImageView) - .load(emoji.url) - .into(emojiImageView) + .load(emoji.url) + .into(emojiImageView) emojiImageView.setOnClickListener { onEmojiSelectedListener.onEmojiSelected(emoji.shortcode) diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowAdapter.kt index 74797b3a7..672f1fcac 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowAdapter.kt @@ -16,7 +16,6 @@ package com.keylesspalace.tusky.adapter import android.view.LayoutInflater import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView import com.keylesspalace.tusky.R import com.keylesspalace.tusky.interfaces.AccountActionListener @@ -36,4 +35,4 @@ class FollowAdapter( viewHolder.setupWithAccount(accountList[position], animateAvatar, animateEmojis) viewHolder.setupActionListener(accountActionListener) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt index a7e927433..2be8b7621 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt @@ -24,11 +24,14 @@ import com.keylesspalace.tusky.R import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.interfaces.AccountActionListener -import com.keylesspalace.tusky.util.* +import com.keylesspalace.tusky.util.emojify +import com.keylesspalace.tusky.util.loadAvatar +import com.keylesspalace.tusky.util.unicodeWrap +import com.keylesspalace.tusky.util.visible class FollowRequestViewHolder( - private val binding: ItemFollowRequestBinding, - private val showHeader: Boolean + private val binding: ItemFollowRequestBinding, + private val showHeader: Boolean ) : RecyclerView.ViewHolder(binding.root) { fun setupWithAccount(account: Account, animateAvatar: Boolean, animateEmojis: Boolean) { diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.kt index 11089cd42..9b0a5dd90 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.kt @@ -16,8 +16,6 @@ package com.keylesspalace.tusky.adapter import android.view.LayoutInflater import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView -import com.keylesspalace.tusky.R import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding import com.keylesspalace.tusky.interfaces.AccountActionListener @@ -38,4 +36,4 @@ class FollowRequestsAdapter( viewHolder.setupWithAccount(accountList[position], animateAvatar, animateEmojis) viewHolder.setupActionListener(accountActionListener, accountList[position].id) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsHeaderAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsHeaderAdapter.kt index 60ab40086..2480086e4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsHeaderAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsHeaderAdapter.kt @@ -25,7 +25,7 @@ class FollowRequestsHeaderAdapter(private val instanceName: String, private val override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HeaderViewHolder { val view = LayoutInflater.from(parent.context) - .inflate(R.layout.item_follow_requests_header, parent, false) as TextView + .inflate(R.layout.item_follow_requests_header, parent, false) as TextView return HeaderViewHolder(view) } @@ -34,7 +34,6 @@ class FollowRequestsHeaderAdapter(private val instanceName: String, private val } override fun getItemCount() = if (accountLocked) 0 else 1 - } class HeaderViewHolder(var textView: TextView) : RecyclerView.ViewHolder(textView) diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/LoadingFooterViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/LoadingFooterViewHolder.kt index ebff5c5f1..6d5ddbd81 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/LoadingFooterViewHolder.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/LoadingFooterViewHolder.kt @@ -15,7 +15,7 @@ package com.keylesspalace.tusky.adapter -import androidx.recyclerview.widget.RecyclerView import android.view.View +import androidx.recyclerview.widget.RecyclerView -class LoadingFooterViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) \ No newline at end of file +class LoadingFooterViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.kt index d20f783ce..9fca33e8f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.kt @@ -13,7 +13,7 @@ import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.interfaces.AccountActionListener import com.keylesspalace.tusky.util.emojify import com.keylesspalace.tusky.util.loadAvatar -import java.util.* +import java.util.HashMap /** * Displays a list of muted accounts with mute/unmute account and mute/unmute notifications @@ -129,4 +129,4 @@ class MutesAdapter( itemView.setOnClickListener { listener.onViewAccount(id) } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/NetworkStateViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/NetworkStateViewHolder.kt index b991def5e..cf7559908 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/NetworkStateViewHolder.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/NetworkStateViewHolder.kt @@ -20,9 +20,10 @@ import androidx.recyclerview.widget.RecyclerView import com.keylesspalace.tusky.databinding.ItemNetworkStateBinding import com.keylesspalace.tusky.util.visible -class NetworkStateViewHolder(private val binding: ItemNetworkStateBinding, - private val retryCallback: () -> Unit) -: RecyclerView.ViewHolder(binding.root) { +class NetworkStateViewHolder( + private val binding: ItemNetworkStateBinding, + private val retryCallback: () -> Unit +) : RecyclerView.ViewHolder(binding.root) { fun setUpWithNetworkState(state: LoadState) { binding.progressBar.visible(state == LoadState.Loading) @@ -38,5 +39,4 @@ class NetworkStateViewHolder(private val binding: ItemNetworkStateBinding, retryCallback() } } - -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/PlaceholderViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/PlaceholderViewHolder.kt index df05b6ae0..e80e3746d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/PlaceholderViewHolder.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/PlaceholderViewHolder.kt @@ -38,4 +38,4 @@ class PlaceholderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) listener.onLoadMore(bindingAdapterPosition) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt index 1f57cc4e0..89b3915e6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt @@ -29,7 +29,7 @@ import com.keylesspalace.tusky.viewdata.PollOptionViewData import com.keylesspalace.tusky.viewdata.buildDescription import com.keylesspalace.tusky.viewdata.calculatePercent -class PollAdapter: RecyclerView.Adapter>() { +class PollAdapter : RecyclerView.Adapter>() { private var pollOptions: List = emptyList() private var voteCount: Int = 0 @@ -40,13 +40,14 @@ class PollAdapter: RecyclerView.Adapter>() { private var animateEmojis = false fun setup( - options: List, - voteCount: Int, - votersCount: Int?, - emojis: List, - mode: Int, - resultClickListener: View.OnClickListener?, - animateEmojis: Boolean) { + options: List, + voteCount: Int, + votersCount: Int?, + emojis: List, + mode: Int, + resultClickListener: View.OnClickListener?, + animateEmojis: Boolean + ) { this.pollOptions = options this.voteCount = voteCount this.votersCount = votersCount @@ -57,12 +58,11 @@ class PollAdapter: RecyclerView.Adapter>() { notifyDataSetChanged() } - fun getSelected() : List { + fun getSelected(): List { return pollOptions.filter { it.selected } - .map { pollOptions.indexOf(it) } + .map { pollOptions.indexOf(it) } } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder { val binding = ItemPollBinding.inflate(LayoutInflater.from(parent.context), parent, false) return BindingHolder(binding) @@ -82,12 +82,12 @@ class PollAdapter: RecyclerView.Adapter>() { radioButton.visible(mode == SINGLE) checkBox.visible(mode == MULTIPLE) - when(mode) { + when (mode) { RESULT -> { val percent = calculatePercent(option.votesCount, votersCount, voteCount) val emojifiedPollOptionText = buildDescription(option.title, percent, resultTextView.context) - .emojify(emojis, resultTextView, animateEmojis) - resultTextView.text = EmojiCompat.get().process(emojifiedPollOptionText) + .emojify(emojis, resultTextView, animateEmojis) + resultTextView.text = EmojiCompat.get().process(emojifiedPollOptionText) val level = percent * 100 @@ -114,7 +114,6 @@ class PollAdapter: RecyclerView.Adapter>() { } } } - } companion object { diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/PreviewPollOptionsAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/PreviewPollOptionsAdapter.kt index 4206f7cfe..6b59672d1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/PreviewPollOptionsAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/PreviewPollOptionsAdapter.kt @@ -23,7 +23,7 @@ import androidx.core.widget.TextViewCompat import androidx.recyclerview.widget.RecyclerView import com.keylesspalace.tusky.R -class PreviewPollOptionsAdapter: RecyclerView.Adapter() { +class PreviewPollOptionsAdapter : RecyclerView.Adapter() { private var options: List = emptyList() private var multiple: Boolean = false @@ -60,7 +60,6 @@ class PreviewPollOptionsAdapter: RecyclerView.Adapter() { textView.setOnClickListener(clickListener) } - } -class PreviewViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) +class PreviewViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt index bec07f067..994630a14 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt @@ -43,10 +43,11 @@ interface ItemInteractionListener { fun onChipClicked(tab: TabData, tabPosition: Int, chipPosition: Int) } -class TabAdapter(private var data: List, - private val small: Boolean, - private val listener: ItemInteractionListener, - private var removeButtonEnabled: Boolean = false +class TabAdapter( + private var data: List, + private val small: Boolean, + private val listener: ItemInteractionListener, + private var removeButtonEnabled: Boolean = false ) : RecyclerView.Adapter>() { fun updateData(newData: List) { @@ -77,7 +78,6 @@ class TabAdapter(private var data: List, binding.textView.setOnClickListener { listener.onTabAdded(tab) } - } else { val binding = holder.binding as ItemTabPreferenceBinding @@ -102,9 +102,9 @@ class TabAdapter(private var data: List, } binding.removeButton.isEnabled = removeButtonEnabled ThemeUtils.setDrawableTint( - holder.itemView.context, - binding.removeButton.drawable, - (if (removeButtonEnabled) android.R.attr.textColorTertiary else R.attr.textColorDisabled) + holder.itemView.context, + binding.removeButton.drawable, + (if (removeButtonEnabled) android.R.attr.textColorTertiary else R.attr.textColorDisabled) ) if (tab.id == HASHTAG) { @@ -118,14 +118,14 @@ class TabAdapter(private var data: List, tab.arguments.forEachIndexed { i, arg -> val chip = binding.chipGroup.getChildAt(i).takeUnless { it.id == R.id.actionChip } as Chip? - ?: Chip(context).apply { - binding.chipGroup.addView(this, binding.chipGroup.size - 1) - chipIconTint = ColorStateList.valueOf(ThemeUtils.getColor(context, android.R.attr.textColorPrimary)) - } + ?: Chip(context).apply { + binding.chipGroup.addView(this, binding.chipGroup.size - 1) + chipIconTint = ColorStateList.valueOf(ThemeUtils.getColor(context, android.R.attr.textColorPrimary)) + } chip.text = arg - if(tab.arguments.size <= 1) { + if (tab.arguments.size <= 1) { chip.chipIcon = null chip.setOnClickListener(null) } else { @@ -136,14 +136,13 @@ class TabAdapter(private var data: List, } } - while(binding.chipGroup.size - 1 > tab.arguments.size) { + while (binding.chipGroup.size - 1 > tab.arguments.size) { binding.chipGroup.removeViewAt(tab.arguments.size) } binding.actionChip.setOnClickListener { listener.onActionChipClicked(tab, holder.bindingAdapterPosition) } - } else { binding.chipGroup.hide() } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.kt index 1d05dff11..8abbbd5f6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.kt @@ -111,8 +111,8 @@ class ThreadAdapter( fun getItem(position: Int): StatusViewData.Concrete? = statuses.getOrNull(position) fun setDetailedStatusPosition(position: Int) { - if (position != detailedStatusPosition - && detailedStatusPosition != RecyclerView.NO_POSITION + if (position != detailedStatusPosition && + detailedStatusPosition != RecyclerView.NO_POSITION ) { val prior = detailedStatusPosition detailedStatusPosition = position @@ -126,4 +126,4 @@ class ThreadAdapter( private const val VIEW_TYPE_STATUS = 0 private const val VIEW_TYPE_STATUS_DETAILED = 1 } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/appstore/CacheUpdater.kt b/app/src/main/java/com/keylesspalace/tusky/appstore/CacheUpdater.kt index d418321fe..7b5ecf77a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/appstore/CacheUpdater.kt +++ b/app/src/main/java/com/keylesspalace/tusky/appstore/CacheUpdater.kt @@ -9,10 +9,10 @@ import io.reactivex.rxjava3.schedulers.Schedulers import javax.inject.Inject class CacheUpdater @Inject constructor( - eventHub: EventHub, - accountManager: AccountManager, - private val appDatabase: AppDatabase, - gson: Gson + eventHub: EventHub, + accountManager: AccountManager, + private val appDatabase: AppDatabase, + gson: Gson ) { private val disposable: Disposable @@ -27,7 +27,7 @@ class CacheUpdater @Inject constructor( is ReblogEvent -> timelineDao.setReblogged(accountId, event.statusId, event.reblog) is BookmarkEvent -> - timelineDao.setBookmarked(accountId, event.statusId, event.bookmark ) + timelineDao.setBookmarked(accountId, event.statusId, event.bookmark) is UnfollowEvent -> timelineDao.removeAllByUser(accountId, event.accountId) is StatusDeletedEvent -> @@ -49,7 +49,7 @@ class CacheUpdater @Inject constructor( appDatabase.timelineDao().removeAllForAccount(accountId) appDatabase.timelineDao().removeAllUsersForAccount(accountId) } - .subscribeOn(Schedulers.io()) - .subscribe() + .subscribeOn(Schedulers.io()) + .subscribe() } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt b/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt index 13baf07f0..aef4525cb 100644 --- a/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt +++ b/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt @@ -19,6 +19,6 @@ data class ProfileEditedEvent(val newProfileData: Account) : Dispatchable data class PreferenceChangedEvent(val preferenceKey: String) : Dispatchable data class MainTabsChangedEvent(val newTabs: List) : Dispatchable data class PollVoteEvent(val statusId: String, val poll: Poll) : Dispatchable -data class DomainMuteEvent(val instance: String): Dispatchable -data class AnnouncementReadEvent(val announcementId: String): Dispatchable -data class PinEvent(val statusId: String, val pinned: Boolean): Dispatchable +data class DomainMuteEvent(val instance: String) : Dispatchable +data class AnnouncementReadEvent(val announcementId: String) : Dispatchable +data class PinEvent(val statusId: String, val pinned: Boolean) : Dispatchable diff --git a/app/src/main/java/com/keylesspalace/tusky/appstore/EventsHub.kt b/app/src/main/java/com/keylesspalace/tusky/appstore/EventsHub.kt index d2f182d81..316974935 100644 --- a/app/src/main/java/com/keylesspalace/tusky/appstore/EventsHub.kt +++ b/app/src/main/java/com/keylesspalace/tusky/appstore/EventsHub.kt @@ -19,4 +19,4 @@ object EventHubImpl : EventHub { override fun dispatch(event: Dispatchable) { eventsSubject.onNext(event) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt index 5014b52e2..2b05ee088 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt @@ -31,17 +31,17 @@ import com.keylesspalace.tusky.util.BindingHolder import com.keylesspalace.tusky.util.LinkHelper import com.keylesspalace.tusky.util.emojify -interface AnnouncementActionListener: LinkListener { +interface AnnouncementActionListener : LinkListener { fun openReactionPicker(announcementId: String, target: View) fun addReaction(announcementId: String, name: String) fun removeReaction(announcementId: String, name: String) } class AnnouncementAdapter( - private var items: List = emptyList(), - private val listener: AnnouncementActionListener, - private val wellbeingEnabled: Boolean = false, - private val animateEmojis: Boolean = false + private var items: List = emptyList(), + private val listener: AnnouncementActionListener, + private val wellbeingEnabled: Boolean = false, + private val animateEmojis: Boolean = false ) : RecyclerView.Adapter>() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder { @@ -67,12 +67,12 @@ class AnnouncementAdapter( } item.reactions.forEachIndexed { i, reaction -> - (chips.getChildAt(i)?.takeUnless { it.id == R.id.addReactionChip } as Chip? - ?: Chip(ContextThemeWrapper(chips.context, R.style.Widget_MaterialComponents_Chip_Choice)).apply { - isCheckable = true - checkedIcon = null - chips.addView(this, i) - }) + chips.getChildAt(i)?.takeUnless { it.id == R.id.addReactionChip } as Chip? + ?: Chip(ContextThemeWrapper(chips.context, R.style.Widget_MaterialComponents_Chip_Choice)).apply { + isCheckable = true + checkedIcon = null + chips.addView(this, i) + } .apply { val emojiText = if (reaction.url == null) { reaction.name @@ -80,16 +80,18 @@ class AnnouncementAdapter( context.getString(R.string.emoji_shortcode_format, reaction.name) } this.text = ("$emojiText ${reaction.count}") - .emojify( - listOf(Emoji( - reaction.name, - reaction.url ?: "", - reaction.staticUrl ?: "", - null - )), - this, - animateEmojis - ) + .emojify( + listOf( + Emoji( + reaction.name, + reaction.url ?: "", + reaction.staticUrl ?: "", + null + ) + ), + this, + animateEmojis + ) isChecked = reaction.me diff --git a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt index 9f61bea86..738476f5d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt @@ -34,7 +34,12 @@ import com.keylesspalace.tusky.databinding.ActivityAnnouncementsBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.settings.PrefKeys -import com.keylesspalace.tusky.util.* +import com.keylesspalace.tusky.util.Error +import com.keylesspalace.tusky.util.Loading +import com.keylesspalace.tusky.util.Success +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.view.EmojiPicker import javax.inject.Inject @@ -52,13 +57,13 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener, private val picker by lazy { EmojiPicker(this) } private val pickerDialog by lazy { PopupWindow(this) - .apply { - contentView = picker - isFocusable = true - setOnDismissListener { - currentAnnouncementId = null - } + .apply { + contentView = picker + isFocusable = true + setOnDismissListener { + currentAnnouncementId = null } + } } private var currentAnnouncementId: String? = null diff --git a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt index 2b4d61a40..fef8f9326 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt @@ -27,15 +27,20 @@ import com.keylesspalace.tusky.entity.Announcement import com.keylesspalace.tusky.entity.Emoji import com.keylesspalace.tusky.entity.Instance import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.util.* +import com.keylesspalace.tusky.util.Either +import com.keylesspalace.tusky.util.Error +import com.keylesspalace.tusky.util.Loading +import com.keylesspalace.tusky.util.Resource +import com.keylesspalace.tusky.util.RxAwareViewModel +import com.keylesspalace.tusky.util.Success import io.reactivex.rxjava3.core.Single import javax.inject.Inject class AnnouncementsViewModel @Inject constructor( - accountManager: AccountManager, - private val appDatabase: AppDatabase, - private val mastodonApi: MastodonApi, - private val eventHub: EventHub + accountManager: AccountManager, + private val appDatabase: AppDatabase, + private val mastodonApi: MastodonApi, + private val eventHub: EventHub ) : RxAwareViewModel() { private val announcementsMutable = MutableLiveData>>() @@ -45,139 +50,153 @@ class AnnouncementsViewModel @Inject constructor( val emojis: LiveData> = emojisMutable init { - Single.zip(mastodonApi.getCustomEmojis(), - appDatabase.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!) - .map> { Either.Left(it) } - .onErrorResumeNext { - mastodonApi.getInstance() - .map { Either.Right(it) } - }, - { emojis, either -> - either.asLeftOrNull()?.copy(emojiList = emojis) - ?: InstanceEntity( - accountManager.activeAccount?.domain!!, - emojis, - either.asRight().maxTootChars, - either.asRight().pollLimits?.maxOptions, - either.asRight().pollLimits?.maxOptionChars, - either.asRight().version - ) - }) - .doOnSuccess { - appDatabase.instanceDao().insertOrReplace(it) - } - .subscribe({ + Single.zip( + mastodonApi.getCustomEmojis(), + appDatabase.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!) + .map> { Either.Left(it) } + .onErrorResumeNext { + mastodonApi.getInstance() + .map { Either.Right(it) } + }, + { emojis, either -> + either.asLeftOrNull()?.copy(emojiList = emojis) + ?: InstanceEntity( + accountManager.activeAccount?.domain!!, + emojis, + either.asRight().maxTootChars, + either.asRight().pollLimits?.maxOptions, + either.asRight().pollLimits?.maxOptionChars, + either.asRight().version + ) + } + ) + .doOnSuccess { + appDatabase.instanceDao().insertOrReplace(it) + } + .subscribe( + { emojisMutable.postValue(it.emojiList.orEmpty()) - }, { + }, + { Log.w(TAG, "Failed to get custom emojis.", it) - }) - .autoDispose() + } + ) + .autoDispose() } fun load() { announcementsMutable.postValue(Loading()) mastodonApi.listAnnouncements() - .subscribe({ + .subscribe( + { announcementsMutable.postValue(Success(it)) it.filter { announcement -> !announcement.read } - .forEach { announcement -> - mastodonApi.dismissAnnouncement(announcement.id) - .subscribe( - { - eventHub.dispatch(AnnouncementReadEvent(announcement.id)) - }, - { throwable -> - Log.d(TAG, "Failed to mark announcement as read.", throwable) - } - ) - .autoDispose() - } - }, { + .forEach { announcement -> + mastodonApi.dismissAnnouncement(announcement.id) + .subscribe( + { + eventHub.dispatch(AnnouncementReadEvent(announcement.id)) + }, + { throwable -> + Log.d(TAG, "Failed to mark announcement as read.", throwable) + } + ) + .autoDispose() + } + }, + { announcementsMutable.postValue(Error(cause = it)) - }) - .autoDispose() + } + ) + .autoDispose() } fun addReaction(announcementId: String, name: String) { mastodonApi.addAnnouncementReaction(announcementId, name) - .subscribe({ + .subscribe( + { announcementsMutable.postValue( - Success( - announcements.value!!.data!!.map { announcement -> - if (announcement.id == announcementId) { - announcement.copy( - reactions = if (announcement.reactions.find { reaction -> reaction.name == name } != null) { - announcement.reactions.map { reaction -> - if (reaction.name == name) { - reaction.copy( - count = reaction.count + 1, - me = true - ) - } else { - reaction - } - } - } else { - listOf( - *announcement.reactions.toTypedArray(), - emojis.value!!.find { emoji -> emoji.shortcode == name } - !!.run { - Announcement.Reaction( - name, - 1, - true, - url, - staticUrl - ) - } - ) - } - ) + Success( + announcements.value!!.data!!.map { announcement -> + if (announcement.id == announcementId) { + announcement.copy( + reactions = if (announcement.reactions.find { reaction -> reaction.name == name } != null) { + announcement.reactions.map { reaction -> + if (reaction.name == name) { + reaction.copy( + count = reaction.count + 1, + me = true + ) + } else { + reaction + } + } } else { - announcement + listOf( + *announcement.reactions.toTypedArray(), + emojis.value!!.find { emoji -> emoji.shortcode == name } + !!.run { + Announcement.Reaction( + name, + 1, + true, + url, + staticUrl + ) + } + ) } - } - ) + ) + } else { + announcement + } + } + ) ) - }, { + }, + { Log.w(TAG, "Failed to add reaction to the announcement.", it) - }) - .autoDispose() + } + ) + .autoDispose() } fun removeReaction(announcementId: String, name: String) { mastodonApi.removeAnnouncementReaction(announcementId, name) - .subscribe({ + .subscribe( + { announcementsMutable.postValue( - Success( - announcements.value!!.data!!.map { announcement -> - if (announcement.id == announcementId) { - announcement.copy( - reactions = announcement.reactions.mapNotNull { reaction -> - if (reaction.name == name) { - if (reaction.count > 1) { - reaction.copy( - count = reaction.count - 1, - me = false - ) - } else { - null - } - } else { - reaction - } - } - ) - } else { - announcement + Success( + announcements.value!!.data!!.map { announcement -> + if (announcement.id == announcementId) { + announcement.copy( + reactions = announcement.reactions.mapNotNull { reaction -> + if (reaction.name == name) { + if (reaction.count > 1) { + reaction.copy( + count = reaction.count - 1, + me = false + ) + } else { + null + } + } else { + reaction + } } - } - ) + ) + } else { + announcement + } + } + ) ) - }, { + }, + { Log.w(TAG, "Failed to remove reaction from the announcement.", it) - }) - .autoDispose() + } + ) + .autoDispose() } companion object { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt index 9458b26a3..bfce01304 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt @@ -32,7 +32,10 @@ import android.view.KeyEvent import android.view.MenuItem import android.view.View import android.view.ViewGroup -import android.widget.* +import android.widget.ImageButton +import android.widget.LinearLayout +import android.widget.PopupMenu +import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.annotation.ColorInt @@ -70,7 +73,20 @@ import com.keylesspalace.tusky.entity.Emoji import com.keylesspalace.tusky.entity.NewPoll import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.settings.PrefKeys -import com.keylesspalace.tusky.util.* +import com.keylesspalace.tusky.util.ComposeTokenizer +import com.keylesspalace.tusky.util.PickMediaFiles +import com.keylesspalace.tusky.util.ThemeUtils +import com.keylesspalace.tusky.util.afterTextChanged +import com.keylesspalace.tusky.util.combineLiveData +import com.keylesspalace.tusky.util.combineOptionalLiveData +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.highlightSpans +import com.keylesspalace.tusky.util.loadAvatar +import com.keylesspalace.tusky.util.onTextChanged +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.viewBinding +import com.keylesspalace.tusky.util.visible +import com.keylesspalace.tusky.util.withLifecycleContext import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.utils.colorInt @@ -83,7 +99,8 @@ import javax.inject.Inject import kotlin.math.max import kotlin.math.min -class ComposeActivity : BaseActivity(), +class ComposeActivity : + BaseActivity(), ComposeOptionsListener, ComposeAutoCompleteAdapter.AutocompletionProvider, OnEmojiSelectedListener, @@ -288,8 +305,9 @@ class ComposeActivity : BaseActivity(), } // work around Android platform bug -> https://issuetracker.google.com/issues/67102093 - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.O - || Build.VERSION.SDK_INT == Build.VERSION_CODES.O_MR1) { + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.O || + Build.VERSION.SDK_INT == Build.VERSION_CODES.O_MR1 + ) { binding.composeEditField.setLayerType(View.LAYER_TYPE_SOFTWARE, null) } } @@ -330,9 +348,9 @@ class ComposeActivity : BaseActivity(), updateScheduleButton() } combineOptionalLiveData(viewModel.media, viewModel.poll) { media, poll -> - val active = poll == null - && media!!.size != 4 - && (media.isEmpty() || media.first().type == QueuedMedia.Type.IMAGE) + val active = poll == null && + media!!.size != 4 && + (media.isEmpty() || media.first().type == QueuedMedia.Type.IMAGE) enableButton(binding.composeAddMediaButton, active, active) enablePollButton(media.isNullOrEmpty()) }.subscribe() @@ -393,7 +411,6 @@ class ComposeActivity : BaseActivity(), setDisplayShowHomeEnabled(true) setHomeAsUpIndicator(R.drawable.ic_close_24dp) } - } private fun setupAvatar(preferences: SharedPreferences, activeAccount: AccountEntity) { @@ -409,8 +426,10 @@ class ComposeActivity : BaseActivity(), avatarSize / 8, animateAvatars ) - binding.composeAvatar.contentDescription = getString(R.string.compose_active_account_description, - activeAccount.fullName) + binding.composeAvatar.contentDescription = getString( + R.string.compose_active_account_description, + activeAccount.fullName + ) } private fun replaceTextAtCaret(text: CharSequence) { @@ -468,7 +487,6 @@ class ComposeActivity : BaseActivity(), } } - private fun atButtonClicked() { prependSelectedWordsWith("@") } @@ -484,7 +502,7 @@ class ComposeActivity : BaseActivity(), private fun displayTransientError(@StringRes stringId: Int) { val bar = Snackbar.make(binding.activityCompose, stringId, Snackbar.LENGTH_LONG) - //necessary so snackbar is shown over everything + // necessary so snackbar is shown over everything bar.view.elevation = resources.getDimension(R.dimen.compose_activity_snackbar_elevation) bar.show() } @@ -502,7 +520,6 @@ class ComposeActivity : BaseActivity(), binding.composeHideMediaButton.setImageResource(R.drawable.ic_hide_media_24dp) binding.composeHideMediaButton.isClickable = false ContextCompat.getColor(this, R.color.transparent_tusky_blue) - } else { binding.composeHideMediaButton.isClickable = true if (markMediaSensitive) { @@ -611,13 +628,15 @@ class ComposeActivity : BaseActivity(), private fun onMediaPick() { addMediaBehavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { override fun onStateChanged(bottomSheet: View, newState: Int) { - //Wait until bottom sheet is not collapsed and show next screen after + // Wait until bottom sheet is not collapsed and show next screen after if (newState == BottomSheetBehavior.STATE_COLLAPSED) { addMediaBehavior.removeBottomSheetCallback(this) if (ContextCompat.checkSelfPermission(this@ComposeActivity, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { - ActivityCompat.requestPermissions(this@ComposeActivity, + ActivityCompat.requestPermissions( + this@ComposeActivity, arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), - PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE) + PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE + ) } else { pickMediaFile.launch(true) } @@ -633,8 +652,10 @@ class ComposeActivity : BaseActivity(), private fun openPollDialog() { addMediaBehavior.state = BottomSheetBehavior.STATE_COLLAPSED val instanceParams = viewModel.instanceParams.value!! - showAddPollDialog(this, viewModel.poll.value, instanceParams.pollMaxOptions, - instanceParams.pollMaxLength, viewModel::updatePoll) + showAddPollDialog( + this, viewModel.poll.value, instanceParams.pollMaxOptions, + instanceParams.pollMaxLength, viewModel::updatePoll + ) } private fun setupPollView() { @@ -755,14 +776,17 @@ class ComposeActivity : BaseActivity(), if (viewModel.media.value!!.isNotEmpty()) { finishingUploadDialog = ProgressDialog.show( this, getString(R.string.dialog_title_finishing_media_upload), - getString(R.string.dialog_message_uploading_media), true, true) + getString(R.string.dialog_message_uploading_media), true, true + ) } - viewModel.sendStatus(contentText, spoilerText).observe(this, { - finishingUploadDialog?.dismiss() - deleteDraftAndFinish() - }) - + viewModel.sendStatus(contentText, spoilerText).observe( + this, + { + finishingUploadDialog?.dismiss() + deleteDraftAndFinish() + } + ) } else { binding.composeEditField.error = getString(R.string.error_compose_character_limit) enableButtons(true) @@ -776,10 +800,12 @@ class ComposeActivity : BaseActivity(), if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { pickMediaFile.launch(true) } else { - Snackbar.make(binding.activityCompose, R.string.error_media_upload_permission, - Snackbar.LENGTH_SHORT).apply { + Snackbar.make( + binding.activityCompose, R.string.error_media_upload_permission, + Snackbar.LENGTH_SHORT + ).apply { setAction(R.string.action_retry) { onMediaPick() } - //necessary so snackbar is shown over everything + // necessary so snackbar is shown over everything view.elevation = resources.getDimension(R.dimen.compose_activity_snackbar_elevation) show() } @@ -798,24 +824,30 @@ class ComposeActivity : BaseActivity(), } // Continue only if the File was successfully created - photoUploadUri = FileProvider.getUriForFile(this, + photoUploadUri = FileProvider.getUriForFile( + this, BuildConfig.APPLICATION_ID + ".fileprovider", - photoFile) + photoFile + ) takePicture.launch(photoUploadUri) } private fun enableButton(button: ImageButton, clickable: Boolean, colorActive: Boolean) { button.isEnabled = clickable - ThemeUtils.setDrawableTint(this, button.drawable, + ThemeUtils.setDrawableTint( + this, button.drawable, if (colorActive) android.R.attr.textColorTertiary - else R.attr.textColorDisabled) + else R.attr.textColorDisabled + ) } private fun enablePollButton(enable: Boolean) { binding.addPollTextActionTextView.isEnabled = enable - val textColor = ThemeUtils.getColor(this, + val textColor = ThemeUtils.getColor( + this, if (enable) android.R.attr.textColorTertiary - else R.attr.textColorDisabled) + else R.attr.textColorDisabled + ) binding.addPollTextActionTextView.setTextColor(textColor) binding.addPollTextActionTextView.compoundDrawablesRelative[0].colorFilter = PorterDuffColorFilter(textColor, PorterDuff.Mode.SRC_IN) } @@ -847,7 +879,6 @@ class ComposeActivity : BaseActivity(), } displayTransientError(errorId) } - } } } @@ -881,7 +912,8 @@ class ComposeActivity : BaseActivity(), if (composeOptionsBehavior.state == BottomSheetBehavior.STATE_EXPANDED || addMediaBehavior.state == BottomSheetBehavior.STATE_EXPANDED || emojiBehavior.state == BottomSheetBehavior.STATE_EXPANDED || - scheduleBehavior.state == BottomSheetBehavior.STATE_EXPANDED) { + scheduleBehavior.state == BottomSheetBehavior.STATE_EXPANDED + ) { composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaPreviewAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaPreviewAdapter.kt index a08aebc08..0b1fa8c41 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaPreviewAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaPreviewAdapter.kt @@ -30,9 +30,9 @@ import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.compose.view.ProgressImageView class MediaPreviewAdapter( - context: Context, - private val onAddCaption: (ComposeActivity.QueuedMedia) -> Unit, - private val onRemove: (ComposeActivity.QueuedMedia) -> Unit + context: Context, + private val onAddCaption: (ComposeActivity.QueuedMedia) -> Unit, + private val onRemove: (ComposeActivity.QueuedMedia) -> Unit ) : RecyclerView.Adapter() { fun submitList(list: List) { @@ -57,7 +57,7 @@ class MediaPreviewAdapter( } private val thumbnailViewSize = - context.resources.getDimensionPixelSize(R.dimen.compose_media_preview_size) + context.resources.getDimensionPixelSize(R.dimen.compose_media_preview_size) override fun getItemCount(): Int = differ.currentList.size @@ -74,31 +74,34 @@ class MediaPreviewAdapter( holder.progressImageView.setImageResource(R.drawable.ic_music_box_preview_24dp) } else { Glide.with(holder.itemView.context) - .load(item.uri) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .dontAnimate() - .into(holder.progressImageView) + .load(item.uri) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .dontAnimate() + .into(holder.progressImageView) } } - private val differ = AsyncListDiffer(this, object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: ComposeActivity.QueuedMedia, newItem: ComposeActivity.QueuedMedia): Boolean { - return oldItem.localId == newItem.localId - } + private val differ = AsyncListDiffer( + this, + object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: ComposeActivity.QueuedMedia, newItem: ComposeActivity.QueuedMedia): Boolean { + return oldItem.localId == newItem.localId + } - override fun areContentsTheSame(oldItem: ComposeActivity.QueuedMedia, newItem: ComposeActivity.QueuedMedia): Boolean { - return oldItem == newItem + override fun areContentsTheSame(oldItem: ComposeActivity.QueuedMedia, newItem: ComposeActivity.QueuedMedia): Boolean { + return oldItem == newItem + } } - }) + ) - inner class PreviewViewHolder(val progressImageView: ProgressImageView) - : RecyclerView.ViewHolder(progressImageView) { + inner class PreviewViewHolder(val progressImageView: ProgressImageView) : + RecyclerView.ViewHolder(progressImageView) { init { val layoutParams = ConstraintLayout.LayoutParams(thumbnailViewSize, thumbnailViewSize) val margin = itemView.context.resources - .getDimensionPixelSize(R.dimen.compose_media_preview_margin) + .getDimensionPixelSize(R.dimen.compose_media_preview_margin) val marginBottom = itemView.context.resources - .getDimensionPixelSize(R.dimen.compose_media_preview_margin_bottom) + .getDimensionPixelSize(R.dimen.compose_media_preview_margin_bottom) layoutParams.setMargins(margin, 0, margin, marginBottom) progressImageView.layoutParams = layoutParams progressImageView.scaleType = ImageView.ScaleType.CENTER_CROP @@ -107,4 +110,4 @@ class MediaPreviewAdapter( } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt index cf5da80d0..6ff361a9b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt @@ -28,7 +28,10 @@ import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.ProgressRequestBody -import com.keylesspalace.tusky.util.* +import com.keylesspalace.tusky.util.MEDIA_SIZE_UNKNOWN +import com.keylesspalace.tusky.util.getImageSquarePixels +import com.keylesspalace.tusky.util.getMediaSize +import com.keylesspalace.tusky.util.randomAlphanumericString import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.schedulers.Schedulers @@ -37,7 +40,7 @@ import okhttp3.MultipartBody import java.io.File import java.io.FileOutputStream import java.io.IOException -import java.util.* +import java.util.Date sealed class UploadEvent { data class ProgressEvent(val percentage: Int) : UploadEvent() @@ -50,9 +53,9 @@ fun createNewImageFile(context: Context): File { val imageFileName = "Tusky_${randomId}_" val storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) return File.createTempFile( - imageFileName, /* prefix */ - ".jpg", /* suffix */ - storageDir /* directory */ + imageFileName, /* prefix */ + ".jpg", /* suffix */ + storageDir /* directory */ ) } @@ -69,18 +72,18 @@ class MediaTypeException : Exception() class CouldNotOpenFileException : Exception() class MediaUploaderImpl( - private val context: Context, - private val mastodonApi: MastodonApi + private val context: Context, + private val mastodonApi: MastodonApi ) : MediaUploader { override fun uploadMedia(media: QueuedMedia): Observable { return Observable - .fromCallable { - if (shouldResizeMedia(media)) { - downsize(media) - } else media - } - .switchMap { upload(it) } - .subscribeOn(Schedulers.io()) + .fromCallable { + if (shouldResizeMedia(media)) { + downsize(media) + } else media + } + .switchMap { upload(it) } + .subscribeOn(Schedulers.io()) } override fun prepareMedia(inUri: Uri): Single { @@ -101,12 +104,13 @@ class MediaUploaderImpl( val file = File.createTempFile("randomTemp1", suffix, context.cacheDir) FileOutputStream(file.absoluteFile).use { out -> input.copyTo(out) - uri = FileProvider.getUriForFile(context, - BuildConfig.APPLICATION_ID + ".fileprovider", - file) + uri = FileProvider.getUriForFile( + context, + BuildConfig.APPLICATION_ID + ".fileprovider", + file + ) mediaSize = getMediaSize(contentResolver, uri) } - } } catch (e: IOException) { Log.w(TAG, e) @@ -151,20 +155,22 @@ class MediaUploaderImpl( var mimeType = contentResolver.getType(media.uri) val map = MimeTypeMap.getSingleton() val fileExtension = map.getExtensionFromMimeType(mimeType) - val filename = String.format("%s_%s_%s.%s", - context.getString(R.string.app_name), - Date().time.toString(), - randomAlphanumericString(10), - fileExtension) + val filename = "%s_%s_%s.%s".format( + context.getString(R.string.app_name), + Date().time.toString(), + randomAlphanumericString(10), + fileExtension + ) val stream = contentResolver.openInputStream(media.uri) if (mimeType == null) mimeType = "multipart/form-data" - var lastProgress = -1 - val fileBody = ProgressRequestBody(stream, media.mediaSize, - mimeType.toMediaTypeOrNull()) { percentage -> + val fileBody = ProgressRequestBody( + stream, media.mediaSize, + mimeType.toMediaTypeOrNull() + ) { percentage -> if (percentage != lastProgress) { emitter.onNext(UploadEvent.ProgressEvent(percentage)) } @@ -180,12 +186,15 @@ class MediaUploaderImpl( } val uploadDisposable = mastodonApi.uploadMedia(body, description) - .subscribe({ attachment -> + .subscribe( + { attachment -> emitter.onNext(UploadEvent.FinishedEvent(attachment)) emitter.onComplete() - }, { e -> + }, + { e -> emitter.onError(e) - }) + } + ) // Cancel the request when our observable is cancelled emitter.setDisposable(uploadDisposable) @@ -194,15 +203,16 @@ class MediaUploaderImpl( private fun downsize(media: QueuedMedia): QueuedMedia { val file = createNewImageFile(context) - DownsizeImageTask.resize(arrayOf(media.uri), - STATUS_IMAGE_SIZE_LIMIT, context.contentResolver, file) + DownsizeImageTask.resize( + arrayOf(media.uri), + STATUS_IMAGE_SIZE_LIMIT, context.contentResolver, file + ) return media.copy(uri = file.toUri(), mediaSize = file.length()) } private fun shouldResizeMedia(media: QueuedMedia): Boolean { - return media.type == QueuedMedia.Type.IMAGE - && (media.mediaSize > STATUS_IMAGE_SIZE_LIMIT - || getImageSquarePixels(context.contentResolver, media.uri) > STATUS_IMAGE_PIXEL_SIZE_LIMIT) + return media.type == QueuedMedia.Type.IMAGE && + (media.mediaSize > STATUS_IMAGE_SIZE_LIMIT || getImageSquarePixels(context.contentResolver, media.uri) > STATUS_IMAGE_PIXEL_SIZE_LIMIT) } private companion object { @@ -211,6 +221,5 @@ class MediaUploaderImpl( private const val STATUS_AUDIO_SIZE_LIMIT = 41943040 // 40MiB private const val STATUS_IMAGE_SIZE_LIMIT = 8388608 // 8MiB private const val STATUS_IMAGE_PIXEL_SIZE_LIMIT = 16777216 // 4096^2 Pixels - } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollDialog.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollDialog.kt index 6ace77bc3..7a4f73898 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollDialog.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollDialog.kt @@ -26,33 +26,33 @@ import com.keylesspalace.tusky.databinding.DialogAddPollBinding import com.keylesspalace.tusky.entity.NewPoll fun showAddPollDialog( - context: Context, - poll: NewPoll?, - maxOptionCount: Int, - maxOptionLength: Int, - onUpdatePoll: (NewPoll) -> Unit + context: Context, + poll: NewPoll?, + maxOptionCount: Int, + maxOptionLength: Int, + onUpdatePoll: (NewPoll) -> Unit ) { val binding = DialogAddPollBinding.inflate(LayoutInflater.from(context)) val dialog = AlertDialog.Builder(context) - .setIcon(R.drawable.ic_poll_24dp) - .setTitle(R.string.create_poll_title) - .setView(binding.root) - .setNegativeButton(android.R.string.cancel, null) - .setPositiveButton(android.R.string.ok, null) - .create() + .setIcon(R.drawable.ic_poll_24dp) + .setTitle(R.string.create_poll_title) + .setView(binding.root) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(android.R.string.ok, null) + .create() val adapter = AddPollOptionsAdapter( - options = poll?.options?.toMutableList() ?: mutableListOf("", ""), - maxOptionLength = maxOptionLength, - onOptionRemoved = { valid -> - binding.addChoiceButton.isEnabled = true - dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = valid - }, - onOptionChanged = { valid -> - dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = valid - } + options = poll?.options?.toMutableList() ?: mutableListOf("", ""), + maxOptionLength = maxOptionLength, + onOptionRemoved = { valid -> + binding.addChoiceButton.isEnabled = true + dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = valid + }, + onOptionChanged = { valid -> + dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = valid + } ) binding.pollChoices.adapter = adapter @@ -80,13 +80,15 @@ fun showAddPollDialog( val selectedPollDurationId = binding.pollDurationSpinner.selectedItemPosition val pollDuration = context.resources - .getIntArray(R.array.poll_duration_values)[selectedPollDurationId] + .getIntArray(R.array.poll_duration_values)[selectedPollDurationId] - onUpdatePoll(NewPoll( + onUpdatePoll( + NewPoll( options = adapter.pollOptions, expiresIn = pollDuration, multiple = binding.multipleChoicesCheckBox.isChecked - )) + ) + ) dialog.dismiss() } @@ -96,4 +98,4 @@ fun showAddPollDialog( // make the dialog focusable so the keyboard does not stay behind it dialog.window?.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollOptionsAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollOptionsAdapter.kt index c3da2c1c2..3640ffa97 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollOptionsAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollOptionsAdapter.kt @@ -27,11 +27,11 @@ import com.keylesspalace.tusky.util.onTextChanged import com.keylesspalace.tusky.util.visible class AddPollOptionsAdapter( - private var options: MutableList, - private val maxOptionLength: Int, - private val onOptionRemoved: (Boolean) -> Unit, - private val onOptionChanged: (Boolean) -> Unit -): RecyclerView.Adapter>() { + private var options: MutableList, + private val maxOptionLength: Int, + private val onOptionRemoved: (Boolean) -> Unit, + private val onOptionChanged: (Boolean) -> Unit +) : RecyclerView.Adapter>() { val pollOptions: List get() = options.toList() @@ -48,7 +48,7 @@ class AddPollOptionsAdapter( binding.optionEditText.onTextChanged { s, _, _, _ -> val pos = holder.bindingAdapterPosition - if(pos != RecyclerView.NO_POSITION) { + if (pos != RecyclerView.NO_POSITION) { options[pos] = s.toString() onOptionChanged(validateInput()) } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt index 13601c1a8..0c15eff0d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt @@ -40,9 +40,10 @@ import com.keylesspalace.tusky.util.withLifecycleContext // https://github.com/tootsuite/mastodon/blob/c6904c0d3766a2ea8a81ab025c127169ecb51373/app/models/media_attachment.rb#L32 private const val MEDIA_DESCRIPTION_CHARACTER_LIMIT = 1500 -fun T.makeCaptionDialog(existingDescription: String?, - previewUri: Uri, - onUpdateDescription: (String) -> LiveData +fun T.makeCaptionDialog( + existingDescription: String?, + previewUri: Uri, + onUpdateDescription: (String) -> LiveData ) where T : Activity, T : LifecycleOwner { val dialogLayout = LinearLayout(this) val padding = Utils.dpToPx(this, 8) @@ -60,14 +61,18 @@ fun T.makeCaptionDialog(existingDescription: String?, (imageView.layoutParams as LinearLayout.LayoutParams).setMargins(0, margin, 0, 0) val input = EditText(this) - input.hint = resources.getQuantityString(R.plurals.hint_describe_for_visually_impaired, - MEDIA_DESCRIPTION_CHARACTER_LIMIT, MEDIA_DESCRIPTION_CHARACTER_LIMIT) + input.hint = resources.getQuantityString( + R.plurals.hint_describe_for_visually_impaired, + MEDIA_DESCRIPTION_CHARACTER_LIMIT, MEDIA_DESCRIPTION_CHARACTER_LIMIT + ) dialogLayout.addView(input) (input.layoutParams as LinearLayout.LayoutParams).setMargins(margin, margin, margin, margin) input.setLines(2) - input.inputType = (InputType.TYPE_CLASS_TEXT - or InputType.TYPE_TEXT_FLAG_MULTI_LINE - or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES) + input.inputType = ( + InputType.TYPE_CLASS_TEXT + or InputType.TYPE_TEXT_FLAG_MULTI_LINE + or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES + ) input.setText(existingDescription) input.filters = arrayOf(InputFilter.LengthFilter(MEDIA_DESCRIPTION_CHARACTER_LIMIT)) @@ -75,41 +80,40 @@ fun T.makeCaptionDialog(existingDescription: String?, onUpdateDescription(input.text.toString()) withLifecycleContext { onUpdateDescription(input.text.toString()) - .observe { success -> if (!success) showFailedCaptionMessage() } - + .observe { success -> if (!success) showFailedCaptionMessage() } } dialog.dismiss() } val dialog = AlertDialog.Builder(this) - .setView(dialogLayout) - .setPositiveButton(android.R.string.ok, okListener) - .setNegativeButton(android.R.string.cancel, null) - .create() + .setView(dialogLayout) + .setPositiveButton(android.R.string.ok, okListener) + .setNegativeButton(android.R.string.cancel, null) + .create() val window = dialog.window window?.setSoftInputMode( - WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) + WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE + ) dialog.show() // Load the image and manually set it into the ImageView because it doesn't have a fixed size. Glide.with(this) - .load(previewUri) - .downsample(DownsampleStrategy.CENTER_INSIDE) - .into(object : CustomTarget(4096, 4096) { - override fun onLoadCleared(placeholder: Drawable?) { - imageView.setImageDrawable(placeholder) - } + .load(previewUri) + .downsample(DownsampleStrategy.CENTER_INSIDE) + .into(object : CustomTarget(4096, 4096) { + override fun onLoadCleared(placeholder: Drawable?) { + imageView.setImageDrawable(placeholder) + } - override fun onResourceReady(resource: Drawable, transition: Transition?) { - imageView.setImageDrawable(resource) - } - }) + override fun onResourceReady(resource: Drawable, transition: Transition?) { + imageView.setImageDrawable(resource) + } + }) } - private fun Activity.showFailedCaptionMessage() { Toast.makeText(this, R.string.error_failed_set_caption, Toast.LENGTH_SHORT).show() } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ComposeOptionsView.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ComposeOptionsView.kt index 8f80c76df..02ec9a9bc 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ComposeOptionsView.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ComposeOptionsView.kt @@ -57,12 +57,10 @@ class ComposeOptionsView @JvmOverloads constructor(context: Context, attrs: Attr R.id.directRadioButton else -> R.id.directRadioButton - } check(selectedButton) } - } interface ComposeOptionsListener { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/EditTextTyped.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/EditTextTyped.kt index 0a5e1c33a..a8403c954 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/EditTextTyped.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/EditTextTyped.kt @@ -16,25 +16,27 @@ package com.keylesspalace.tusky.components.compose.view import android.content.Context -import androidx.emoji.widget.EmojiEditTextHelper -import androidx.core.view.inputmethod.EditorInfoCompat -import androidx.core.view.inputmethod.InputConnectionCompat -import androidx.appcompat.widget.AppCompatMultiAutoCompleteTextView import android.text.InputType import android.text.method.KeyListener import android.util.AttributeSet import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputConnection +import androidx.appcompat.widget.AppCompatMultiAutoCompleteTextView +import androidx.core.view.inputmethod.EditorInfoCompat +import androidx.core.view.inputmethod.InputConnectionCompat +import androidx.emoji.widget.EmojiEditTextHelper -class EditTextTyped @JvmOverloads constructor(context: Context, - attributeSet: AttributeSet? = null) - : AppCompatMultiAutoCompleteTextView(context, attributeSet) { +class EditTextTyped @JvmOverloads constructor( + context: Context, + attributeSet: AttributeSet? = null +) : + AppCompatMultiAutoCompleteTextView(context, attributeSet) { private var onCommitContentListener: InputConnectionCompat.OnCommitContentListener? = null private val emojiEditTextHelper: EmojiEditTextHelper = EmojiEditTextHelper(this) init { - //fix a bug with autocomplete and some keyboards + // fix a bug with autocomplete and some keyboards val newInputType = inputType and (inputType xor InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE) inputType = newInputType super.setKeyListener(getEmojiEditTextHelper().getKeyListener(keyListener)) @@ -52,8 +54,13 @@ class EditTextTyped @JvmOverloads constructor(context: Context, val connection = super.onCreateInputConnection(editorInfo) return if (onCommitContentListener != null) { EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("image/*")) - getEmojiEditTextHelper().onCreateInputConnection(InputConnectionCompat.createWrapper(connection, editorInfo, - onCommitContentListener!!), editorInfo)!! + getEmojiEditTextHelper().onCreateInputConnection( + InputConnectionCompat.createWrapper( + connection, editorInfo, + onCommitContentListener!! + ), + editorInfo + )!! } else { connection } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/PollPreviewView.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/PollPreviewView.kt index 1126047d8..c55e8fce7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/PollPreviewView.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/PollPreviewView.kt @@ -25,10 +25,11 @@ import com.keylesspalace.tusky.databinding.ViewPollPreviewBinding import com.keylesspalace.tusky.entity.NewPoll class PollPreviewView @JvmOverloads constructor( - context: Context?, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0) - : LinearLayout(context, attrs, defStyleAttr) { + context: Context?, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : + LinearLayout(context, attrs, defStyleAttr) { private val adapter = PreviewPollOptionsAdapter() @@ -46,7 +47,7 @@ class PollPreviewView @JvmOverloads constructor( binding.pollPreviewOptions.adapter = adapter } - fun setPoll(poll: NewPoll){ + fun setPoll(poll: NewPoll) { adapter.update(poll.options, poll.multiple) val pollDurationId = resources.getIntArray(R.array.poll_duration_values).indexOfLast { @@ -59,4 +60,4 @@ class PollPreviewView @JvmOverloads constructor( super.setOnClickListener(l) adapter.setOnClickListener(l) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/TootButton.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/TootButton.kt index f7ba7ee69..24f4130b2 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/TootButton.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/TootButton.kt @@ -28,15 +28,15 @@ import com.mikepenz.iconics.utils.sizeDp class TootButton @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 ) : MaterialButton(context, attrs, defStyleAttr) { private val smallStyle: Boolean = context.resources.getBoolean(R.bool.show_small_toot_button) init { - if(smallStyle) { + if (smallStyle) { setIconResource(R.drawable.ic_send_24dp) } else { setText(R.string.action_send) @@ -47,7 +47,7 @@ class TootButton } fun setStatusVisibility(visibility: Status.Visibility) { - if(!smallStyle) { + if (!smallStyle) { icon = when (visibility) { Status.Visibility.PUBLIC -> { @@ -68,8 +68,5 @@ class TootButton } } } - } - } - diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt index 7caa91144..0a4698227 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt @@ -30,7 +30,7 @@ import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.util.shouldTrimStatus import java.util.Date -@Entity(primaryKeys = ["id","accountId"]) +@Entity(primaryKeys = ["id", "accountId"]) @TypeConverters(Converters::class) data class ConversationEntity( val accountId: Long, @@ -98,7 +98,7 @@ data class ConversationStatusEntity( if (inReplyToId != other.inReplyToId) return false if (inReplyToAccountId != other.inReplyToAccountId) return false if (account != other.account) return false - if (content.toString() != other.content.toString()) return false //TODO find a better method to compare two spanned strings + if (content.toString() != other.content.toString()) return false // TODO find a better method to compare two spanned strings if (createdAt != other.createdAt) return false if (emojis != other.emojis) return false if (favouritesCount != other.favouritesCount) return false @@ -157,7 +157,7 @@ data class ConversationStatusEntity( reblogged = false, favourited = favourited, bookmarked = bookmarked, - sensitive= sensitive, + sensitive = sensitive, spoilerText = spoilerText, visibility = Status.Visibility.DIRECT, attachments = attachments, @@ -166,7 +166,8 @@ data class ConversationStatusEntity( pinned = false, muted = muted, poll = poll, - card = null) + card = null + ) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationLoadStateAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationLoadStateAdapter.kt index d5c0983a0..c7224c4d2 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationLoadStateAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationLoadStateAdapter.kt @@ -37,5 +37,4 @@ class ConversationLoadStateAdapter( val binding = ItemNetworkStateBinding.inflate(LayoutInflater.from(parent.context), parent, false) return NetworkStateViewHolder(binding, retryCallback) } - } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt index a484c6d06..4272c1bd6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt @@ -40,15 +40,16 @@ import com.keylesspalace.tusky.fragment.SFragment import com.keylesspalace.tusky.interfaces.ReselectableFragment import com.keylesspalace.tusky.interfaces.StatusActionListener import com.keylesspalace.tusky.settings.PrefKeys -import com.keylesspalace.tusky.util.* -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch -import java.io.IOException import com.keylesspalace.tusky.util.CardViewMode import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.viewBinding +import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.viewdata.AttachmentViewData +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import java.io.IOException import javax.inject.Inject class ConversationsFragment : SFragment(), StatusActionListener, Injectable, ReselectableFragment { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRepository.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRepository.kt index 2156b0189..12c5eb0bb 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRepository.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRepository.kt @@ -32,7 +32,6 @@ class ConversationsRepository @Inject constructor( Single.fromCallable { db.conversationDao().deleteForAccount(accountId) }.subscribeOn(Schedulers.io()) - .subscribe() + .subscribe() } - -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftMediaAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftMediaAdapter.kt index 69403fdb5..acee683b6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftMediaAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftMediaAdapter.kt @@ -28,18 +28,17 @@ import com.keylesspalace.tusky.R import com.keylesspalace.tusky.db.DraftAttachment class DraftMediaAdapter( - private val attachmentClick: () -> Unit + private val attachmentClick: () -> Unit ) : ListAdapter( - object: DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: DraftAttachment, newItem: DraftAttachment): Boolean { - return oldItem == newItem - } - - override fun areContentsTheSame(oldItem: DraftAttachment, newItem: DraftAttachment): Boolean { - return oldItem == newItem - } - + object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: DraftAttachment, newItem: DraftAttachment): Boolean { + return oldItem == newItem } + + override fun areContentsTheSame(oldItem: DraftAttachment, newItem: DraftAttachment): Boolean { + return oldItem == newItem + } + } ) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DraftMediaViewHolder { @@ -52,24 +51,24 @@ class DraftMediaAdapter( holder.imageView.setImageResource(R.drawable.ic_music_box_preview_24dp) } else { Glide.with(holder.itemView.context) - .load(attachment.uri) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .dontAnimate() - .into(holder.imageView) + .load(attachment.uri) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .dontAnimate() + .into(holder.imageView) } } } - inner class DraftMediaViewHolder(val imageView: ImageView) - : RecyclerView.ViewHolder(imageView) { + inner class DraftMediaViewHolder(val imageView: ImageView) : + RecyclerView.ViewHolder(imageView) { init { val thumbnailViewSize = - imageView.context.resources.getDimensionPixelSize(R.dimen.compose_media_preview_size) + imageView.context.resources.getDimensionPixelSize(R.dimen.compose_media_preview_size) val layoutParams = ConstraintLayout.LayoutParams(thumbnailViewSize, thumbnailViewSize) val margin = itemView.context.resources - .getDimensionPixelSize(R.dimen.compose_media_preview_margin) + .getDimensionPixelSize(R.dimen.compose_media_preview_margin) val marginBottom = itemView.context.resources - .getDimensionPixelSize(R.dimen.compose_media_preview_margin_bottom) + .getDimensionPixelSize(R.dimen.compose_media_preview_margin_bottom) layoutParams.setMargins(margin, 0, margin, marginBottom) imageView.layoutParams = layoutParams imageView.scaleType = ImageView.ScaleType.CENTER_CROP @@ -78,4 +77,4 @@ class DraftMediaAdapter( } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt index 8ca00491a..ce0048011 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt @@ -91,27 +91,28 @@ class DraftsActivity : BaseActivity(), DraftActionListener { if (draft.inReplyToId != null) { bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED viewModel.getToot(draft.inReplyToId) - .observeOn(AndroidSchedulers.mainThread()) - .autoDispose(from(this)) - .subscribe({ status -> + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(from(this)) + .subscribe( + { status -> val composeOptions = ComposeActivity.ComposeOptions( - draftId = draft.id, - tootText = draft.content, - contentWarning = draft.contentWarning, - inReplyToId = draft.inReplyToId, - replyingStatusContent = status.content.toString(), - replyingStatusAuthor = status.account.localUsername, - draftAttachments = draft.attachments, - poll = draft.poll, - sensitive = draft.sensitive, - visibility = draft.visibility + draftId = draft.id, + tootText = draft.content, + contentWarning = draft.contentWarning, + inReplyToId = draft.inReplyToId, + replyingStatusContent = status.content.toString(), + replyingStatusAuthor = status.account.localUsername, + draftAttachments = draft.attachments, + poll = draft.poll, + sensitive = draft.sensitive, + visibility = draft.visibility ) bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN startActivity(ComposeActivity.startIntent(this, composeOptions)) - - }, { throwable -> + }, + { throwable -> bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN @@ -124,9 +125,10 @@ class DraftsActivity : BaseActivity(), DraftActionListener { openDraftWithoutReply(draft) } else { Snackbar.make(binding.root, getString(R.string.drafts_failed_loading_reply), Snackbar.LENGTH_SHORT) - .show() + .show() } - }) + } + ) } else { openDraftWithoutReply(draft) } @@ -134,13 +136,13 @@ class DraftsActivity : BaseActivity(), DraftActionListener { private fun openDraftWithoutReply(draft: DraftEntity) { val composeOptions = ComposeActivity.ComposeOptions( - draftId = draft.id, - tootText = draft.content, - contentWarning = draft.contentWarning, - draftAttachments = draft.attachments, - poll = draft.poll, - sensitive = draft.sensitive, - visibility = draft.visibility + draftId = draft.id, + tootText = draft.content, + contentWarning = draft.contentWarning, + draftAttachments = draft.attachments, + poll = draft.poll, + sensitive = draft.sensitive, + visibility = draft.visibility ) startActivity(ComposeActivity.startIntent(this, composeOptions)) @@ -149,10 +151,10 @@ class DraftsActivity : BaseActivity(), DraftActionListener { override fun onDeleteDraft(draft: DraftEntity) { viewModel.deleteDraft(draft) Snackbar.make(binding.root, getString(R.string.draft_deleted), Snackbar.LENGTH_LONG) - .setAction(R.string.action_undo) { - viewModel.restoreDraft(draft) - } - .show() + .setAction(R.string.action_undo) { + viewModel.restoreDraft(draft) + } + .show() } companion object { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/InstanceListActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/InstanceListActivity.kt index 7253112a1..83a2191fe 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/InstanceListActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/InstanceListActivity.kt @@ -9,7 +9,7 @@ import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector import javax.inject.Inject -class InstanceListActivity: BaseActivity(), HasAndroidInjector { +class InstanceListActivity : BaseActivity(), HasAndroidInjector { @Inject lateinit var androidInjector: DispatchingAndroidInjector @@ -27,11 +27,10 @@ class InstanceListActivity: BaseActivity(), HasAndroidInjector { } supportFragmentManager - .beginTransaction() - .replace(R.id.fragment_container, InstanceListFragment()) - .commit() + .beginTransaction() + .replace(R.id.fragment_container, InstanceListFragment()) + .commit() } override fun androidInjector() = androidInjector - -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/adapter/DomainMutesAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/adapter/DomainMutesAdapter.kt index f475f3942..509c9561d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/adapter/DomainMutesAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/adapter/DomainMutesAdapter.kt @@ -8,8 +8,8 @@ import com.keylesspalace.tusky.databinding.ItemMutedDomainBinding import com.keylesspalace.tusky.util.BindingHolder class DomainMutesAdapter( - private val actionListener: InstanceActionListener -): RecyclerView.Adapter>() { + private val actionListener: InstanceActionListener +) : RecyclerView.Adapter>() { var instances: MutableList = mutableListOf() var bottomLoading: Boolean = false diff --git a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/fragment/InstanceListFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/fragment/InstanceListFragment.kt index 1a1392d42..ccfe52b3c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/fragment/InstanceListFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/fragment/InstanceListFragment.kt @@ -29,7 +29,7 @@ import retrofit2.Response import java.io.IOException import javax.inject.Inject -class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectable, InstanceActionListener { +class InstanceListFragment : Fragment(R.layout.fragment_instance_list), Injectable, InstanceActionListener { @Inject lateinit var api: MastodonApi @@ -65,7 +65,7 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl override fun mute(mute: Boolean, instance: String, position: Int) { if (mute) { - api.blockDomain(instance).enqueue(object: Callback { + api.blockDomain(instance).enqueue(object : Callback { override fun onFailure(call: Call, t: Throwable) { Log.e(TAG, "Error muting domain $instance") } @@ -79,7 +79,7 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl } }) } else { - api.unblockDomain(instance).enqueue(object: Callback { + api.unblockDomain(instance).enqueue(object : Callback { override fun onFailure(call: Call, t: Throwable) { Log.e(TAG, "Error unmuting domain $instance") } @@ -88,10 +88,10 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl if (response.isSuccessful) { adapter.removeItem(position) Snackbar.make(binding.recyclerView, getString(R.string.confirmation_domain_unmuted, instance), Snackbar.LENGTH_LONG) - .setAction(R.string.action_undo) { - mute(true, instance, position) - } - .show() + .setAction(R.string.action_undo) { + mute(true, instance, position) + } + .show() } else { Log.e(TAG, "Error unmuting domain $instance") } @@ -112,9 +112,10 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl } api.domainBlocks(id, bottomId) - .observeOn(AndroidSchedulers.mainThread()) - .autoDispose(from(this, Lifecycle.Event.ON_DESTROY)) - .subscribe({ response -> + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(from(this, Lifecycle.Event.ON_DESTROY)) + .subscribe( + { response -> val instances = response.body() if (response.isSuccessful && instances != null) { @@ -122,9 +123,11 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl } else { onFetchInstancesFailure(Exception(response.message())) } - }, {throwable -> + }, + { throwable -> onFetchInstancesFailure(throwable) - }) + } + ) } private fun onFetchInstancesSuccess(instances: List, linkHeader: String?) { @@ -141,9 +144,9 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl if (adapter.itemCount == 0) { binding.messageView.show() binding.messageView.setup( - R.drawable.elephant_friend_empty, - R.string.message_empty, - null + R.drawable.elephant_friend_empty, + R.string.message_empty, + null ) } else { binding.messageView.hide() @@ -174,4 +177,4 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl companion object { private const val TAG = "InstanceList" // logging tag } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/interfaces/InstanceActionListener.kt b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/interfaces/InstanceActionListener.kt index 97d59cc96..9b88ad966 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/interfaces/InstanceActionListener.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/interfaces/InstanceActionListener.kt @@ -2,4 +2,4 @@ package com.keylesspalace.tusky.components.instancemute.interfaces interface InstanceActionListener { fun mute(mute: Boolean, instance: String, position: Int) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationFetcher.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationFetcher.kt index 394a68466..fe48a16b3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationFetcher.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationFetcher.kt @@ -10,9 +10,9 @@ import com.keylesspalace.tusky.util.isLessThan import javax.inject.Inject class NotificationFetcher @Inject constructor( - private val mastodonApi: MastodonApi, - private val accountManager: AccountManager, - private val notifier: Notifier + private val mastodonApi: MastodonApi, + private val accountManager: AccountManager, + private val notifier: Notifier ) { fun fetchAndShow() { for (account in accountManager.getAllAccountsOrderedByActive()) { @@ -39,9 +39,9 @@ class NotificationFetcher @Inject constructor( } Log.d(TAG, "getting Notifications for " + account.fullName) val notifications = mastodonApi.notificationsWithAuth( - authHeader, - account.domain, - account.lastNotificationId + authHeader, + account.domain, + account.lastNotificationId ).blockingGet() val newId = account.lastNotificationId @@ -63,9 +63,9 @@ class NotificationFetcher @Inject constructor( private fun fetchMarker(authHeader: String, account: AccountEntity): Marker? { return try { val allMarkers = mastodonApi.markersWithAuth( - authHeader, - account.domain, - listOf("notifications") + authHeader, + account.domain, + listOf("notifications") ).blockingGet() val notificationMarker = allMarkers["notifications"] Log.d(TAG, "Fetched marker: $notificationMarker") @@ -79,4 +79,4 @@ class NotificationFetcher @Inject constructor( companion object { const val TAG = "NotificationFetcher" } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationWorker.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationWorker.kt index ae7d4d3fb..42b9c869e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationWorker.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationWorker.kt @@ -23,9 +23,9 @@ import androidx.work.WorkerParameters import javax.inject.Inject class NotificationWorker( - context: Context, - params: WorkerParameters, - private val notificationsFetcher: NotificationFetcher + context: Context, + params: WorkerParameters, + private val notificationsFetcher: NotificationFetcher ) : Worker(context, params) { override fun doWork(): Result { @@ -35,13 +35,13 @@ class NotificationWorker( } class NotificationWorkerFactory @Inject constructor( - private val notificationsFetcher: NotificationFetcher + private val notificationsFetcher: NotificationFetcher ) : WorkerFactory() { override fun createWorker( - appContext: Context, - workerClassName: String, - workerParameters: WorkerParameters + appContext: Context, + workerClassName: String, + workerParameters: WorkerParameters ): ListenableWorker? { if (workerClassName == NotificationWorker::class.java.name) { return NotificationWorker(appContext, workerParameters, notificationsFetcher) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/Notifier.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/Notifier.kt index 35c33a9b8..5092530bd 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/Notifier.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/Notifier.kt @@ -12,9 +12,9 @@ interface Notifier { } class SystemNotifier( - private val context: Context + private val context: Context ) : Notifier { override fun show(notification: Notification, account: AccountEntity, isFirstInBatch: Boolean) { NotificationHelper.make(context, notification, account, isFirstInBatch) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/AccountPreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/AccountPreferencesFragment.kt index 286b49b56..e6bf83fbc 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/preference/AccountPreferencesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/AccountPreferencesFragment.kt @@ -22,7 +22,11 @@ import android.util.Log import androidx.annotation.DrawableRes import androidx.preference.PreferenceFragmentCompat import com.google.android.material.snackbar.Snackbar -import com.keylesspalace.tusky.* +import com.keylesspalace.tusky.AccountListActivity +import com.keylesspalace.tusky.BuildConfig +import com.keylesspalace.tusky.FiltersActivity +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.TabPreferenceActivity import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.PreferenceChangedEvent import com.keylesspalace.tusky.components.instancemute.InstanceListActivity @@ -33,7 +37,12 @@ import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Filter import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.settings.* +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.settings.listPreference +import com.keylesspalace.tusky.settings.makePreferenceScreen +import com.keylesspalace.tusky.settings.preference +import com.keylesspalace.tusky.settings.preferenceCategory +import com.keylesspalace.tusky.settings.switchPreference import com.keylesspalace.tusky.util.ThemeUtils import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial @@ -75,8 +84,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable { setOnPreferenceClickListener { val intent = Intent(context, TabPreferenceActivity::class.java) activity?.startActivity(intent) - activity?.overridePendingTransition(R.anim.slide_from_right, - R.anim.slide_to_left) + activity?.overridePendingTransition( + R.anim.slide_from_right, + R.anim.slide_to_left + ) true } } @@ -88,8 +99,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable { val intent = Intent(context, AccountListActivity::class.java) intent.putExtra("type", AccountListActivity.Type.MUTES) activity?.startActivity(intent) - activity?.overridePendingTransition(R.anim.slide_from_right, - R.anim.slide_to_left) + activity?.overridePendingTransition( + R.anim.slide_from_right, + R.anim.slide_to_left + ) true } } @@ -104,8 +117,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable { val intent = Intent(context, AccountListActivity::class.java) intent.putExtra("type", AccountListActivity.Type.BLOCKS) activity?.startActivity(intent) - activity?.overridePendingTransition(R.anim.slide_from_right, - R.anim.slide_to_left) + activity?.overridePendingTransition( + R.anim.slide_from_right, + R.anim.slide_to_left + ) true } } @@ -116,8 +131,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable { setOnPreferenceClickListener { val intent = Intent(context, InstanceListActivity::class.java) activity?.startActivity(intent) - activity?.overridePendingTransition(R.anim.slide_from_right, - R.anim.slide_to_left) + activity?.overridePendingTransition( + R.anim.slide_from_right, + R.anim.slide_to_left + ) true } } @@ -130,7 +147,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable { key = PrefKeys.DEFAULT_POST_PRIVACY setSummaryProvider { entry } val visibility = accountManager.activeAccount?.defaultPostPrivacy - ?: Status.Visibility.PUBLIC + ?: Status.Visibility.PUBLIC value = visibility.serverString() setIcon(getIconForVisibility(visibility)) setOnPreferenceChangeListener { _, newValue -> @@ -147,7 +164,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable { key = PrefKeys.DEFAULT_MEDIA_SENSITIVITY isSingleLineTitle = false val sensitivity = accountManager.activeAccount?.defaultMediaSensitivity - ?: false + ?: false setDefaultValue(sensitivity) setIcon(getIconForSensitivity(sensitivity)) setOnPreferenceChangeListener { _, newValue -> @@ -201,8 +218,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable { preference { setTitle(R.string.pref_title_public_filter_keywords) setOnPreferenceClickListener { - launchFilterActivity(Filter.PUBLIC, - R.string.pref_title_public_filter_keywords) + launchFilterActivity( + Filter.PUBLIC, + R.string.pref_title_public_filter_keywords + ) true } } @@ -226,8 +245,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable { preference { setTitle(R.string.pref_title_thread_filter_keywords) setOnPreferenceClickListener { - launchFilterActivity(Filter.THREAD, - R.string.pref_title_thread_filter_keywords) + launchFilterActivity( + Filter.THREAD, + R.string.pref_title_thread_filter_keywords + ) true } } @@ -255,7 +276,6 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable { it.startActivity(intent) it.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left) } - } } @@ -268,36 +288,35 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable { private fun syncWithServer(visibility: String? = null, sensitive: Boolean? = null) { mastodonApi.accountUpdateSource(visibility, sensitive) - .enqueue(object : Callback { - override fun onResponse(call: Call, response: Response) { - val account = response.body() - if (response.isSuccessful && account != null) { + .enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + val account = response.body() + if (response.isSuccessful && account != null) { - accountManager.activeAccount?.let { - it.defaultPostPrivacy = account.source?.privacy - ?: Status.Visibility.PUBLIC - it.defaultMediaSensitivity = account.source?.sensitive ?: false - accountManager.saveAccount(it) - } - } else { - Log.e("AccountPreferences", "failed updating settings on server") - showErrorSnackbar(visibility, sensitive) + accountManager.activeAccount?.let { + it.defaultPostPrivacy = account.source?.privacy + ?: Status.Visibility.PUBLIC + it.defaultMediaSensitivity = account.source?.sensitive ?: false + accountManager.saveAccount(it) } - } - - override fun onFailure(call: Call, t: Throwable) { - Log.e("AccountPreferences", "failed updating settings on server", t) + } else { + Log.e("AccountPreferences", "failed updating settings on server") showErrorSnackbar(visibility, sensitive) } + } - }) + override fun onFailure(call: Call, t: Throwable) { + Log.e("AccountPreferences", "failed updating settings on server", t) + showErrorSnackbar(visibility, sensitive) + } + }) } private fun showErrorSnackbar(visibility: String?, sensitive: Boolean?) { view?.let { view -> Snackbar.make(view, R.string.pref_failed_to_sync, Snackbar.LENGTH_LONG) - .setAction(R.string.action_retry) { syncWithServer(visibility, sensitive) } - .show() + .setAction(R.string.action_retry) { syncWithServer(visibility, sensitive) } + .show() } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt index d045350f3..e793f17f1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt @@ -34,8 +34,8 @@ import kotlin.system.exitProcess * This Preference lets the user select their preferred emoji font */ class EmojiPreference( - context: Context, - private val okHttpClient: OkHttpClient + context: Context, + private val okHttpClient: OkHttpClient ) : Preference(context) { private lateinit var selected: EmojiCompatFont @@ -51,7 +51,7 @@ class EmojiPreference( // Find out which font is currently active selected = EmojiCompatFont.byId( - PreferenceManager.getDefaultSharedPreferences(context).getInt(key, 0) + PreferenceManager.getDefaultSharedPreferences(context).getInt(key, 0) ) // We'll use this later to determine if anything has changed original = selected @@ -67,10 +67,10 @@ class EmojiPreference( setupItem(SYSTEM_DEFAULT, binding.itemNomoji) AlertDialog.Builder(context) - .setView(binding.root) - .setPositiveButton(android.R.string.ok) { _, _ -> onDialogOk() } - .setNegativeButton(android.R.string.cancel, null) - .show() + .setView(binding.root) + .setPositiveButton(android.R.string.ok) { _, _ -> onDialogOk() } + .setNegativeButton(android.R.string.cancel, null) + .show() } private fun setupItem(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) { @@ -100,32 +100,30 @@ class EmojiPreference( binding.emojiProgress.progress = 0 binding.emojiDownloadCancel.show() font.downloadFontFile(context, okHttpClient) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { progress -> - // The progress is returned as a float between 0 and 1, or -1 if it could not determined - if (progress >= 0) { - binding.emojiProgress.isIndeterminate = false - val max = binding.emojiProgress.max.toFloat() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - binding.emojiProgress.setProgress((max * progress).toInt(), true) - } else { - binding.emojiProgress.progress = (max * progress).toInt() - } - } else { - binding.emojiProgress.isIndeterminate = true - } - }, - { - Toast.makeText(context, R.string.download_failed, Toast.LENGTH_SHORT).show() - updateItem(font, binding) - }, - { - finishDownload(font, binding) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { progress -> + // The progress is returned as a float between 0 and 1, or -1 if it could not determined + if (progress >= 0) { + binding.emojiProgress.isIndeterminate = false + val max = binding.emojiProgress.max.toFloat() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + binding.emojiProgress.setProgress((max * progress).toInt(), true) + } else { + binding.emojiProgress.progress = (max * progress).toInt() } - ).also { downloadDisposables[font.id] = it } - - + } else { + binding.emojiProgress.isIndeterminate = true + } + }, + { + Toast.makeText(context, R.string.download_failed, Toast.LENGTH_SHORT).show() + updateItem(font, binding) + }, + { + finishDownload(font, binding) + } + ).also { downloadDisposables[font.id] = it } } private fun cancelDownload(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) { @@ -197,10 +195,10 @@ class EmojiPreference( val index = selected.id Log.i(TAG, "saveSelectedFont: Font ID: $index") PreferenceManager - .getDefaultSharedPreferences(context) - .edit() - .putInt(key, index) - .apply() + .getDefaultSharedPreferences(context) + .edit() + .putInt(key, index) + .apply() summary = selected.getDisplay(context) } @@ -211,29 +209,31 @@ class EmojiPreference( saveSelectedFont() if (selected !== original || updated) { AlertDialog.Builder(context) - .setTitle(R.string.restart_required) - .setMessage(R.string.restart_emoji) - .setNegativeButton(R.string.later, null) - .setPositiveButton(R.string.restart) { _, _ -> - // Restart the app - // From https://stackoverflow.com/a/17166729/5070653 - val launchIntent = Intent(context, SplashActivity::class.java) - val mPendingIntent = PendingIntent.getActivity( - context, - 0x1f973, // This is the codepoint of the party face emoji :D - launchIntent, - PendingIntent.FLAG_CANCEL_CURRENT) - val mgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager - mgr.set( - AlarmManager.RTC, - System.currentTimeMillis() + 100, - mPendingIntent) - exitProcess(0) - }.show() + .setTitle(R.string.restart_required) + .setMessage(R.string.restart_emoji) + .setNegativeButton(R.string.later, null) + .setPositiveButton(R.string.restart) { _, _ -> + // Restart the app + // From https://stackoverflow.com/a/17166729/5070653 + val launchIntent = Intent(context, SplashActivity::class.java) + val mPendingIntent = PendingIntent.getActivity( + context, + 0x1f973, // This is the codepoint of the party face emoji :D + launchIntent, + PendingIntent.FLAG_CANCEL_CURRENT + ) + val mgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager + mgr.set( + AlarmManager.RTC, + System.currentTimeMillis() + 100, + mPendingIntent + ) + exitProcess(0) + }.show() } } companion object { private const val TAG = "EmojiPreference" } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt index 1e90abc85..4d8ba84f3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt @@ -111,7 +111,7 @@ class NotificationPreferencesFragment : PreferenceFragmentCompat(), Injectable { true } } - + switchPreference { setTitle(R.string.pref_title_notification_filter_subscriptions) key = PrefKeys.NOTIFICATION_FILTER_SUBSCRIPTIONS @@ -176,5 +176,4 @@ class NotificationPreferencesFragment : PreferenceFragmentCompat(), Injectable { return NotificationPreferencesFragment() } } - } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt index f1a076159..8297fae48 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt @@ -36,8 +36,10 @@ import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector import javax.inject.Inject -class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreferenceChangeListener, - HasAndroidInjector { +class PreferencesActivity : + BaseActivity(), + SharedPreferences.OnSharedPreferenceChangeListener, + HasAndroidInjector { @Inject lateinit var eventHub: EventHub @@ -62,36 +64,35 @@ class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreference val fragmentTag = "preference_fragment_$EXTRA_PREFERENCE_TYPE" val fragment: Fragment = supportFragmentManager.findFragmentByTag(fragmentTag) - ?: when (intent.getIntExtra(EXTRA_PREFERENCE_TYPE, 0)) { - GENERAL_PREFERENCES -> { - setTitle(R.string.action_view_preferences) - PreferencesFragment.newInstance() - } - ACCOUNT_PREFERENCES -> { - setTitle(R.string.action_view_account_preferences) - AccountPreferencesFragment.newInstance() - } - NOTIFICATION_PREFERENCES -> { - setTitle(R.string.pref_title_edit_notification_settings) - NotificationPreferencesFragment.newInstance() - } - TAB_FILTER_PREFERENCES -> { - setTitle(R.string.pref_title_status_tabs) - TabFilterPreferencesFragment.newInstance() - } - PROXY_PREFERENCES -> { - setTitle(R.string.pref_title_http_proxy_settings) - ProxyPreferencesFragment.newInstance() - } - else -> throw IllegalArgumentException("preferenceType not known") + ?: when (intent.getIntExtra(EXTRA_PREFERENCE_TYPE, 0)) { + GENERAL_PREFERENCES -> { + setTitle(R.string.action_view_preferences) + PreferencesFragment.newInstance() } + ACCOUNT_PREFERENCES -> { + setTitle(R.string.action_view_account_preferences) + AccountPreferencesFragment.newInstance() + } + NOTIFICATION_PREFERENCES -> { + setTitle(R.string.pref_title_edit_notification_settings) + NotificationPreferencesFragment.newInstance() + } + TAB_FILTER_PREFERENCES -> { + setTitle(R.string.pref_title_status_tabs) + TabFilterPreferencesFragment.newInstance() + } + PROXY_PREFERENCES -> { + setTitle(R.string.pref_title_http_proxy_settings) + ProxyPreferencesFragment.newInstance() + } + else -> throw IllegalArgumentException("preferenceType not known") + } supportFragmentManager.commit { replace(R.id.fragment_container, fragment, fragmentTag) } restartActivitiesOnExit = intent.getBooleanExtra("restart", false) - } override fun onResume() { @@ -122,7 +123,6 @@ class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreference restartActivitiesOnExit = true this.restartCurrentActivity() - } "statusTextSize", "absoluteTimeView", "showBotOverlay", "animateGifAvatars", "useBlurhash", "showCardsInTimelines", "confirmReblogs", "enableSwipeForTabs", "mainNavPosition", PrefKeys.HIDE_TOP_TOOLBAR -> { @@ -179,5 +179,4 @@ class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreference return intent } } - } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt index fa4ac3b29..d3f44e322 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt @@ -22,7 +22,14 @@ import com.keylesspalace.tusky.R import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.entity.Notification -import com.keylesspalace.tusky.settings.* +import com.keylesspalace.tusky.settings.AppTheme +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.settings.emojiPreference +import com.keylesspalace.tusky.settings.listPreference +import com.keylesspalace.tusky.settings.makePreferenceScreen +import com.keylesspalace.tusky.settings.preference +import com.keylesspalace.tusky.settings.preferenceCategory +import com.keylesspalace.tusky.settings.switchPreference import com.keylesspalace.tusky.util.ThemeUtils import com.keylesspalace.tusky.util.deserialize import com.keylesspalace.tusky.util.getNonNullString @@ -122,7 +129,6 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable { setTitle(R.string.pref_title_bot_overlay) isSingleLineTitle = false setIcon(R.drawable.ic_bot_24dp) - } switchPreference { @@ -259,7 +265,6 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable { sizePx = iconSize colorInt = ThemeUtils.getColor(context, R.attr.iconColor) } - } override fun onResume() { @@ -274,7 +279,7 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable { try { val httpPort = sharedPreferences.getNonNullString(PrefKeys.HTTP_PROXY_PORT, "-1") - .toInt() + .toInt() if (httpProxyEnabled && httpServer.isNotBlank() && httpPort > 0 && httpPort < 65535) { httpProxyPref?.summary = "$httpServer:$httpPort" diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/ProxyPreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/ProxyPreferencesFragment.kt index 922d5a7a1..322b0c1da 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/preference/ProxyPreferencesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/ProxyPreferencesFragment.kt @@ -50,7 +50,6 @@ class ProxyPreferencesFragment : PreferenceFragmentCompat() { setSummaryProvider { text } } } - } override fun onPause() { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt index 4c53588a3..82526706e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt @@ -126,12 +126,12 @@ class ReportActivity : BottomSheetActivity(), HasAndroidInjector { @JvmStatic fun getIntent(context: Context, accountId: String, userName: String, statusId: String? = null) = - Intent(context, ReportActivity::class.java) - .apply { - putExtra(ACCOUNT_ID, accountId) - putExtra(ACCOUNT_USERNAME, userName) - putExtra(STATUS_ID, statusId) - } + Intent(context, ReportActivity::class.java) + .apply { + putExtra(ACCOUNT_ID, accountId) + putExtra(ACCOUNT_USERNAME, userName) + putExtra(STATUS_ID, statusId) + } } override fun androidInjector() = androidInjector diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt index 7b51e91ce..f8991282d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt @@ -43,8 +43,8 @@ import kotlinx.coroutines.launch import javax.inject.Inject class ReportViewModel @Inject constructor( - private val mastodonApi: MastodonApi, - private val eventHub: EventHub + private val mastodonApi: MastodonApi, + private val eventHub: EventHub ) : RxAwareViewModel() { private val navigationMutable = MutableLiveData() @@ -121,18 +121,17 @@ class ReportViewModel @Inject constructor( muteStateMutable.value = Loading() blockStateMutable.value = Loading() mastodonApi.relationships(ids) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { data -> - updateRelationship(data.getOrNull(0)) - - }, - { - updateRelationship(null) - } - ) - .autoDispose() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { data -> + updateRelationship(data.getOrNull(0)) + }, + { + updateRelationship(null) + } + ) + .autoDispose() } private fun updateRelationship(relationship: Relationship?) { @@ -152,20 +151,20 @@ class ReportViewModel @Inject constructor( } else { mastodonApi.muteAccount(accountId) } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { relationship -> - val muting = relationship?.muting == true - muteStateMutable.value = Success(muting) - if (muting) { - eventHub.dispatch(MuteEvent(accountId)) - } - }, - { error -> - muteStateMutable.value = Error(false, error.message) - } - ).autoDispose() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { relationship -> + val muting = relationship?.muting == true + muteStateMutable.value = Success(muting) + if (muting) { + eventHub.dispatch(MuteEvent(accountId)) + } + }, + { error -> + muteStateMutable.value = Error(false, error.message) + } + ).autoDispose() muteStateMutable.value = Loading() } @@ -177,21 +176,21 @@ class ReportViewModel @Inject constructor( } else { mastodonApi.blockAccount(accountId) } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { relationship -> - val blocking = relationship?.blocking == true - blockStateMutable.value = Success(blocking) - if (blocking) { - eventHub.dispatch(BlockEvent(accountId)) - } - }, - { error -> - blockStateMutable.value = Error(false, error.message) - } - ) - .autoDispose() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { relationship -> + val blocking = relationship?.blocking == true + blockStateMutable.value = Success(blocking) + if (blocking) { + eventHub.dispatch(BlockEvent(accountId)) + } + }, + { error -> + blockStateMutable.value = Error(false, error.message) + } + ) + .autoDispose() blockStateMutable.value = Loading() } @@ -199,18 +198,17 @@ class ReportViewModel @Inject constructor( fun doReport() { reportingStateMutable.value = Loading() mastodonApi.reportObservable(accountId, selectedIds.toList(), reportNote, if (isRemoteAccount) isRemoteNotify else null) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { - reportingStateMutable.value = Success(true) - }, - { error -> - reportingStateMutable.value = Error(cause = error) - } - ) - .autoDispose() - + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + reportingStateMutable.value = Success(true) + }, + { error -> + reportingStateMutable.value = Error(cause = error) + } + ) + .autoDispose() } fun checkClickedUrl(url: String?) { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/Screen.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/Screen.kt index 643c46c18..fb0b15cae 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/Screen.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/Screen.kt @@ -21,4 +21,4 @@ enum class Screen { Done, Back, Finish -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/AdapterHandler.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/AdapterHandler.kt index 957d5b325..fd150f91a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/AdapterHandler.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/AdapterHandler.kt @@ -19,8 +19,8 @@ import android.view.View import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.interfaces.LinkListener -interface AdapterHandler: LinkListener { +interface AdapterHandler : LinkListener { fun showMedia(v: View?, status: Status?, idx: Int) fun setStatusChecked(status: Status, isChecked: Boolean) fun isStatusChecked(id: String): Boolean -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/ReportPagerAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/ReportPagerAdapter.kt index 506d99afe..fa5acc2d6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/ReportPagerAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/ReportPagerAdapter.kt @@ -33,4 +33,4 @@ class ReportPagerAdapter(activity: FragmentActivity) : FragmentStateAdapter(acti } override fun getItemCount() = 3 -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt index 5ac3dd6a1..41486506c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt @@ -25,18 +25,25 @@ import com.keylesspalace.tusky.databinding.ItemReportStatusBinding import com.keylesspalace.tusky.entity.Emoji import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.interfaces.LinkListener -import com.keylesspalace.tusky.util.* +import com.keylesspalace.tusky.util.LinkHelper +import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.util.StatusViewHelper import com.keylesspalace.tusky.util.StatusViewHelper.Companion.COLLAPSE_INPUT_FILTER import com.keylesspalace.tusky.util.StatusViewHelper.Companion.NO_INPUT_FILTER +import com.keylesspalace.tusky.util.TimestampUtils +import com.keylesspalace.tusky.util.emojify +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.shouldTrimStatus +import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.viewdata.toViewData -import java.util.* +import java.util.Date class StatusViewHolder( - private val binding: ItemReportStatusBinding, - private val statusDisplayOptions: StatusDisplayOptions, - private val viewState: StatusViewState, - private val adapterHandler: AdapterHandler, - private val getStatusForPosition: (Int) -> Status? + private val binding: ItemReportStatusBinding, + private val statusDisplayOptions: StatusDisplayOptions, + private val viewState: StatusViewState, + private val adapterHandler: AdapterHandler, + private val getStatusForPosition: (Int) -> Status? ) : RecyclerView.ViewHolder(binding.root) { private val mediaViewHeight = itemView.context.resources.getDimensionPixelSize(R.dimen.status_media_preview_height) private val statusViewHelper = StatusViewHelper(itemView) @@ -71,9 +78,11 @@ class StatusViewHolder( val sensitive = status.sensitive - statusViewHelper.setMediasPreview(statusDisplayOptions, status.attachments, - sensitive, previewListener, viewState.isMediaShow(status.id, status.sensitive), - mediaViewHeight) + statusViewHelper.setMediasPreview( + statusDisplayOptions, status.attachments, + sensitive, previewListener, viewState.isMediaShow(status.id, status.sensitive), + mediaViewHeight + ) statusViewHelper.setupPollReadonly(status.poll.toViewData(), status.emojis, statusDisplayOptions) setCreatedAt(status.createdAt) @@ -81,8 +90,10 @@ class StatusViewHolder( private fun updateTextView() { status()?.let { status -> - setupCollapsedState(shouldTrimStatus(status.content), viewState.isCollapsed(status.id, true), - viewState.isContentShow(status.id, status.sensitive), status.spoilerText) + setupCollapsedState( + shouldTrimStatus(status.content), viewState.isCollapsed(status.id, true), + viewState.isContentShow(status.id, status.sensitive), status.spoilerText + ) if (status.spoilerText.isBlank()) { setTextVisible(true, status.content, status.mentions, status.emojis, adapterHandler) @@ -109,18 +120,20 @@ class StatusViewHolder( } private fun setContentWarningButtonText(contentShown: Boolean) { - if(contentShown) { + if (contentShown) { binding.statusContentWarningButton.setText(R.string.status_content_warning_show_less) } else { binding.statusContentWarningButton.setText(R.string.status_content_warning_show_more) } } - private fun setTextVisible(expanded: Boolean, - content: Spanned, - mentions: List?, - emojis: List, - listener: LinkListener) { + private fun setTextVisible( + expanded: Boolean, + content: Spanned, + mentions: List?, + emojis: List, + listener: LinkListener + ) { if (expanded) { val emojifiedText = content.emojify(emojis, binding.statusContent, statusDisplayOptions.animateEmojis) LinkHelper.setClickableText(binding.statusContent, emojifiedText, mentions, listener) @@ -152,7 +165,7 @@ class StatusViewHolder( private fun setupCollapsedState(collapsible: Boolean, collapsed: Boolean, expanded: Boolean, spoilerText: String) { /* input filter for TextViews have to be set before text */ if (collapsible && (expanded || TextUtils.isEmpty(spoilerText))) { - binding.buttonToggleContent.setOnClickListener{ + binding.buttonToggleContent.setOnClickListener { status()?.let { status -> viewState.setCollapsed(status.id, !collapsed) updateTextView() @@ -174,4 +187,4 @@ class StatusViewHolder( } private fun status() = getStatusForPosition(bindingAdapterPosition) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesAdapter.kt index d472995d4..76ed2ebea 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesAdapter.kt @@ -26,9 +26,9 @@ import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.util.StatusDisplayOptions class StatusesAdapter( - private val statusDisplayOptions: StatusDisplayOptions, - private val statusViewState: StatusViewState, - private val adapterHandler: AdapterHandler + private val statusDisplayOptions: StatusDisplayOptions, + private val statusViewState: StatusViewState, + private val adapterHandler: AdapterHandler ) : PagingDataAdapter(STATUS_COMPARATOR) { private val statusForPosition: (Int) -> Status? = { position: Int -> @@ -37,8 +37,10 @@ class StatusesAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StatusViewHolder { val binding = ItemReportStatusBinding.inflate(LayoutInflater.from(parent.context), parent, false) - return StatusViewHolder(binding, statusDisplayOptions, statusViewState, adapterHandler, - statusForPosition) + return StatusViewHolder( + binding, statusDisplayOptions, statusViewState, adapterHandler, + statusForPosition + ) } override fun onBindViewHolder(holder: StatusViewHolder, position: Int) { @@ -50,10 +52,10 @@ class StatusesAdapter( companion object { val STATUS_COMPARATOR = object : DiffUtil.ItemCallback() { override fun areContentsTheSame(oldItem: Status, newItem: Status): Boolean = - oldItem == newItem + oldItem == newItem override fun areItemsTheSame(oldItem: Status, newItem: Status): Boolean = - oldItem.id == newItem.id + oldItem.id == newItem.id } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesPagingSource.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesPagingSource.kt index 964e23e27..c007239d8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesPagingSource.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesPagingSource.kt @@ -32,7 +32,7 @@ class StatusesPagingSource( override fun getRefreshKey(state: PagingState): String? { return state.anchorPosition?.let { anchorPosition -> - state.closestItemToPosition(anchorPosition)?.id + state.closestItemToPosition(anchorPosition)?.id } } @@ -65,7 +65,6 @@ class StatusesPagingSource( prevKey = result.firstOrNull()?.id, nextKey = result.lastOrNull()?.id ) - } catch (e: Exception) { Log.w("StatusesPagingSource", "failed to load statuses", e) return LoadResult.Error(e) @@ -86,4 +85,4 @@ class StatusesPagingSource( excludeReblogs = true ).await() } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt index 794cb287b..0f8065776 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt @@ -56,27 +56,29 @@ class ReportDoneFragment : Fragment(R.layout.fragment_report_done), Injectable { binding.progressMute.hide() } - binding.buttonMute.setText(when (it.data) { - true -> R.string.action_unmute - else -> R.string.action_mute - }) + binding.buttonMute.setText( + when (it.data) { + true -> R.string.action_unmute + else -> R.string.action_mute + } + ) } viewModel.blockState.observe(viewLifecycleOwner) { if (it !is Loading) { binding.buttonBlock.show() binding.progressBlock.show() - } - else { + } else { binding.buttonBlock.hide() binding.progressBlock.hide() } - binding.buttonBlock.setText(when (it.data) { - true -> R.string.action_unblock - else -> R.string.action_block - }) + binding.buttonBlock.setText( + when (it.data) { + true -> R.string.action_unblock + else -> R.string.action_block + } + ) } - } private fun handleClicks() { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt index aa3559355..56f812a05 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt @@ -64,11 +64,10 @@ class ReportNoteFragment : Fragment(R.layout.fragment_report_note), Injectable { private fun fillViews() { binding.editNote.setText(viewModel.reportNote) - if (viewModel.isRemoteAccount){ + if (viewModel.isRemoteAccount) { binding.checkIsNotifyRemote.show() binding.reportDescriptionRemoteInstance.show() - } - else{ + } else { binding.checkIsNotifyRemote.hide() binding.reportDescriptionRemoteInstance.hide() } @@ -84,7 +83,6 @@ class ReportNoteFragment : Fragment(R.layout.fragment_report_note), Injectable { is Success -> viewModel.navigateTo(Screen.Done) is Loading -> showLoading() is Error -> showError(it.cause) - } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt index 33cd2ece2..98de3c94b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt @@ -107,15 +107,15 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje private fun initStatusesView() { val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) val statusDisplayOptions = StatusDisplayOptions( - animateAvatars = false, - mediaPreviewEnabled = accountManager.activeAccount?.mediaPreviewEnabled ?: true, - useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false), - showBotOverlay = false, - useBlurhash = preferences.getBoolean("useBlurhash", true), - cardViewMode = CardViewMode.NONE, - confirmReblogs = preferences.getBoolean("confirmReblogs", true), - hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false), - animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) + animateAvatars = false, + mediaPreviewEnabled = accountManager.activeAccount?.mediaPreviewEnabled ?: true, + useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false), + showBotOverlay = false, + useBlurhash = preferences.getBoolean("useBlurhash", true), + cardViewMode = CardViewMode.NONE, + confirmReblogs = preferences.getBoolean("confirmReblogs", true), + hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false), + animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) ) adapter = StatusesAdapter(statusDisplayOptions, viewModel.statusViewState, this) @@ -132,9 +132,10 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje } adapter.addLoadStateListener { loadState -> - if (loadState.refresh is LoadState.Error - || loadState.append is LoadState.Error - || loadState.prepend is LoadState.Error) { + if (loadState.refresh is LoadState.Error || + loadState.append is LoadState.Error || + loadState.prepend is LoadState.Error + ) { showError() } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/model/StatusViewState.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/model/StatusViewState.kt index 664ddc6a5..2bcade2fe 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/model/StatusViewState.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/model/StatusViewState.kt @@ -30,7 +30,7 @@ class StatusViewState { fun setCollapsed(id: String, isCollapsed: Boolean) = setStateEnabled(longContentCollapsedState, id, isCollapsed) private fun isStateEnabled(map: Map, id: String, def: Boolean): Boolean = map[id] - ?: def + ?: def private fun setStateEnabled(map: MutableMap, id: String, state: Boolean) = map.put(id, state) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootViewModel.kt index 6890b15b3..14f012ba6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootViewModel.kt @@ -29,9 +29,9 @@ import kotlinx.coroutines.rx3.await import javax.inject.Inject class ScheduledTootViewModel @Inject constructor( - val mastodonApi: MastodonApi, - val eventHub: EventHub -): ViewModel() { + val mastodonApi: MastodonApi, + val eventHub: EventHub +) : ViewModel() { private val pagingSourceFactory = ScheduledTootPagingSourceFactory(mastodonApi) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchActivity.kt index 7208b0388..2326bf17e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchActivity.kt @@ -76,7 +76,7 @@ class SearchActivity : BottomSheetActivity(), HasAndroidInjector { menuInflater.inflate(R.menu.search_toolbar, menu) val searchView = menu.findItem(R.id.action_search) - .actionView as SearchView + .actionView as SearchView setupSearchView(searchView) searchView.setQuery(viewModel.currentQuery, false) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchType.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchType.kt index df98b9ab0..235f8ce03 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchType.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchType.kt @@ -19,4 +19,4 @@ enum class SearchType(val apiParameter: String) { Status("statuses"), Account("accounts"), Hashtag("hashtags") -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt index 4ec51413b..8682b5a27 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt @@ -95,13 +95,17 @@ class SearchViewModel @Inject constructor( fun removeItem(status: Pair) { timelineCases.delete(status.first.id) - .subscribe({ + .subscribe( + { if (loadedStatuses.remove(status)) statusesPagingSourceFactory.invalidate() - }, { - err -> Log.d(TAG, "Failed to delete status", err) - }) - .autoDispose() + }, + { + err -> + Log.d(TAG, "Failed to delete status", err) + } + ) + .autoDispose() } fun expandedChange(status: Pair, expanded: Boolean) { @@ -225,4 +229,4 @@ class SearchViewModel @Inject constructor( private const val TAG = "SearchViewModel" private const val DEFAULT_LOAD_SIZE = 20 } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchAccountsAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchAccountsAdapter.kt index 7056d5e29..71d582680 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchAccountsAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchAccountsAdapter.kt @@ -24,12 +24,12 @@ import com.keylesspalace.tusky.adapter.AccountViewHolder import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.interfaces.LinkListener -class SearchAccountsAdapter(private val linkListener: LinkListener, private val animateAvatars: Boolean, private val animateEmojis: Boolean) - : PagingDataAdapter(ACCOUNT_COMPARATOR) { +class SearchAccountsAdapter(private val linkListener: LinkListener, private val animateAvatars: Boolean, private val animateEmojis: Boolean) : + PagingDataAdapter(ACCOUNT_COMPARATOR) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AccountViewHolder { val view = LayoutInflater.from(parent.context) - .inflate(R.layout.item_account, parent, false) + .inflate(R.layout.item_account, parent, false) return AccountViewHolder(view) } @@ -46,10 +46,10 @@ class SearchAccountsAdapter(private val linkListener: LinkListener, private val val ACCOUNT_COMPARATOR = object : DiffUtil.ItemCallback() { override fun areContentsTheSame(oldItem: Account, newItem: Account): Boolean = - oldItem.deepEquals(newItem) + oldItem.deepEquals(newItem) override fun areItemsTheSame(oldItem: Account, newItem: Account): Boolean = - oldItem.id == newItem.id + oldItem.id == newItem.id } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchHashtagsAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchHashtagsAdapter.kt index cf7e7c7c5..50bd0f933 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchHashtagsAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchHashtagsAdapter.kt @@ -24,8 +24,8 @@ import com.keylesspalace.tusky.entity.HashTag import com.keylesspalace.tusky.interfaces.LinkListener import com.keylesspalace.tusky.util.BindingHolder -class SearchHashtagsAdapter(private val linkListener: LinkListener) - : PagingDataAdapter>(HASHTAG_COMPARATOR) { +class SearchHashtagsAdapter(private val linkListener: LinkListener) : + PagingDataAdapter>(HASHTAG_COMPARATOR) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder { val binding = ItemHashtagBinding.inflate(LayoutInflater.from(parent.context), parent, false) @@ -43,10 +43,10 @@ class SearchHashtagsAdapter(private val linkListener: LinkListener) val HASHTAG_COMPARATOR = object : DiffUtil.ItemCallback() { override fun areContentsTheSame(oldItem: HashTag, newItem: HashTag): Boolean = - oldItem.name == newItem.name + oldItem.name == newItem.name override fun areItemsTheSame(oldItem: HashTag, newItem: HashTag): Boolean = - oldItem.name == newItem.name + oldItem.name == newItem.name } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchPagerAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchPagerAdapter.kt index 845abaf89..9f30f9c55 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchPagerAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchPagerAdapter.kt @@ -34,5 +34,4 @@ class SearchPagerAdapter(activity: FragmentActivity) : FragmentStateAdapter(acti } override fun getItemCount() = 3 - -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchPagingSource.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchPagingSource.kt index 315edba69..5ced44037 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchPagingSource.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchPagingSource.kt @@ -22,12 +22,13 @@ import com.keylesspalace.tusky.entity.SearchResult import com.keylesspalace.tusky.network.MastodonApi import kotlinx.coroutines.rx3.await -class SearchPagingSource( +class SearchPagingSource( private val mastodonApi: MastodonApi, private val searchType: SearchType, private val searchRequest: String, private val initialItems: List?, - private val parser: (SearchResult) -> List) : PagingSource() { + private val parser: (SearchResult) -> List +) : PagingSource() { override fun getRefreshKey(state: PagingState): Int? { return null @@ -80,4 +81,4 @@ class SearchPagingSource( return LoadResult.Error(e) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchPagingSourceFactory.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchPagingSourceFactory.kt index fb3760ca4..f995d029b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchPagingSourceFactory.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchPagingSourceFactory.kt @@ -50,4 +50,4 @@ class SearchPagingSourceFactory( fun invalidate() { currentSource?.invalidate() } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchAccountsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchAccountsFragment.kt index 8a0d54162..d5e2a7aba 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchAccountsFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchAccountsFragment.kt @@ -28,9 +28,9 @@ class SearchAccountsFragment : SearchFragment() { val preferences = PreferenceManager.getDefaultSharedPreferences(binding.searchRecyclerView.context) return SearchAccountsAdapter( - this, - preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false), - preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) + this, + preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false), + preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) ) } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt index e18cd5cb1..10ef2713f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt @@ -29,8 +29,11 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import javax.inject.Inject -abstract class SearchFragment : Fragment(R.layout.fragment_search), - LinkListener, Injectable, SwipeRefreshLayout.OnRefreshListener { +abstract class SearchFragment : + Fragment(R.layout.fragment_search), + LinkListener, + Injectable, + SwipeRefreshLayout.OnRefreshListener { @Inject lateinit var viewModelFactory: ViewModelFactory diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt index c6fe2c4e0..3ade751f1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt @@ -74,15 +74,15 @@ class SearchStatusesFragment : SearchFragment, *> { val preferences = PreferenceManager.getDefaultSharedPreferences(binding.searchRecyclerView.context) val statusDisplayOptions = StatusDisplayOptions( - animateAvatars = preferences.getBoolean("animateGifAvatars", false), - mediaPreviewEnabled = viewModel.mediaPreviewEnabled, - useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false), - showBotOverlay = preferences.getBoolean("showBotOverlay", true), - useBlurhash = preferences.getBoolean("useBlurhash", true), - cardViewMode = CardViewMode.NONE, - confirmReblogs = preferences.getBoolean("confirmReblogs", true), - hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false), - animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) + animateAvatars = preferences.getBoolean("animateGifAvatars", false), + mediaPreviewEnabled = viewModel.mediaPreviewEnabled, + useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false), + showBotOverlay = preferences.getBoolean("showBotOverlay", true), + useBlurhash = preferences.getBoolean("useBlurhash", true), + cardViewMode = CardViewMode.NONE, + confirmReblogs = preferences.getBoolean("confirmReblogs", true), + hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false), + animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) ) binding.searchRecyclerView.addItemDecoration(DividerItemDecoration(binding.searchRecyclerView.context, DividerItemDecoration.VERTICAL)) @@ -125,13 +125,17 @@ class SearchStatusesFragment : SearchFragment { val attachments = AttachmentViewData.list(actionable) - val intent = ViewMediaActivity.newIntent(context, attachments, - attachmentIndex) + val intent = ViewMediaActivity.newIntent( + context, attachments, + attachmentIndex + ) if (view != null) { val url = actionable.attachments[attachmentIndex].url ViewCompat.setTransitionName(view, url) - val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(), - view, url) + val options = ActivityOptionsCompat.makeSceneTransitionAnimation( + requireActivity(), + view, url + ) startActivity(intent, options.toBundle()) } else { startActivity(intent) @@ -198,20 +202,23 @@ class SearchStatusesFragment : SearchFragment { - } //Ignore + } // Ignore } } else { popup.inflate(R.menu.status_more) @@ -271,11 +278,12 @@ class SearchStatusesFragment : SearchFragment @@ -287,8 +295,8 @@ class SearchStatusesFragment : SearchFragment viewModel.blockAccount(accountId) } - .setNegativeButton(android.R.string.cancel, null) - .show() + .setMessage(getString(R.string.dialog_block_warning, accountUsername)) + .setPositiveButton(android.R.string.ok) { _, _ -> viewModel.blockAccount(accountId) } + .setNegativeButton(android.R.string.cancel, null) + .show() } private fun onMute(accountId: String, accountUsername: String) { @@ -383,11 +391,14 @@ class SearchStatusesFragment : SearchFragment - viewModel.deleteStatus(id) - removeItem(position) - } - .setNegativeButton(android.R.string.cancel, null) - .show() + .setMessage(R.string.dialog_delete_toot_warning) + .setPositiveButton(android.R.string.ok) { _, _ -> + viewModel.deleteStatus(id) + removeItem(position) + } + .setNegativeButton(android.R.string.cancel, null) + .show() } } private fun showConfirmEditDialog(id: String, position: Int, status: Status) { activity?.let { AlertDialog.Builder(it) - .setMessage(R.string.dialog_redraft_toot_warning) - .setPositiveButton(android.R.string.ok) { _, _ -> - viewModel.deleteStatus(id) - .observeOn(AndroidSchedulers.mainThread()) - .autoDispose(from(this, Lifecycle.Event.ON_DESTROY)) - .subscribe({ deletedStatus -> - removeItem(position) + .setMessage(R.string.dialog_redraft_toot_warning) + .setPositiveButton(android.R.string.ok) { _, _ -> + viewModel.deleteStatus(id) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(from(this, Lifecycle.Event.ON_DESTROY)) + .subscribe( + { deletedStatus -> + removeItem(position) - val redraftStatus = if (deletedStatus.isEmpty()) { - status.toDeletedStatus() - } else { - deletedStatus - } + val redraftStatus = if (deletedStatus.isEmpty()) { + status.toDeletedStatus() + } else { + deletedStatus + } - val intent = ComposeActivity.startIntent(requireContext(), ComposeOptions( - tootText = redraftStatus.text ?: "", - inReplyToId = redraftStatus.inReplyToId, - visibility = redraftStatus.visibility, - contentWarning = redraftStatus.spoilerText, - mediaAttachments = redraftStatus.attachments, - sensitive = redraftStatus.sensitive, - poll = redraftStatus.poll?.toNewPoll(status.createdAt) - )) - startActivity(intent) - }, { error -> - Log.w("SearchStatusesFragment", "error deleting status", error) - Toast.makeText(context, R.string.error_generic, Toast.LENGTH_SHORT).show() - }) - - } - .setNegativeButton(android.R.string.cancel, null) - .show() + val intent = ComposeActivity.startIntent( + requireContext(), + ComposeOptions( + tootText = redraftStatus.text ?: "", + inReplyToId = redraftStatus.inReplyToId, + visibility = redraftStatus.visibility, + contentWarning = redraftStatus.spoilerText, + mediaAttachments = redraftStatus.attachments, + sensitive = redraftStatus.sensitive, + poll = redraftStatus.poll?.toNewPoll(status.createdAt) + ) + ) + startActivity(intent) + }, + { error -> + Log.w("SearchStatusesFragment", "error deleting status", error) + Toast.makeText(context, R.string.error_generic, Toast.LENGTH_SHORT).show() + } + ) + } + .setNegativeButton(android.R.string.cancel, null) + .show() } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt index b0fc5d14d..50d6abcac 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt @@ -25,10 +25,16 @@ import androidx.core.content.ContextCompat import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle import androidx.preference.PreferenceManager -import androidx.recyclerview.widget.* +import androidx.recyclerview.widget.AsyncDifferConfig +import androidx.recyclerview.widget.AsyncListDiffer +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.ListUpdateCallback +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.SimpleItemAnimator import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener import at.connyduck.sparkbutton.helpers.Utils -import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.* import autodispose2.androidx.lifecycle.autoDispose import com.keylesspalace.tusky.AccountListActivity import com.keylesspalace.tusky.AccountListActivity.Companion.newIntent @@ -47,7 +53,13 @@ import com.keylesspalace.tusky.interfaces.RefreshableFragment import com.keylesspalace.tusky.interfaces.ReselectableFragment import com.keylesspalace.tusky.interfaces.StatusActionListener import com.keylesspalace.tusky.settings.PrefKeys -import com.keylesspalace.tusky.util.* +import com.keylesspalace.tusky.util.CardViewMode +import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate +import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.viewBinding +import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.view.EndlessOnScrollListener import com.keylesspalace.tusky.viewdata.AttachmentViewData import com.keylesspalace.tusky.viewdata.StatusViewData @@ -56,8 +68,13 @@ import io.reactivex.rxjava3.core.Observable import java.util.concurrent.TimeUnit import javax.inject.Inject -class TimelineFragment : SFragment(), OnRefreshListener, StatusActionListener, Injectable, - ReselectableFragment, RefreshableFragment { +class TimelineFragment : + SFragment(), + OnRefreshListener, + StatusActionListener, + Injectable, + ReselectableFragment, + RefreshableFragment { @Inject lateinit var viewModelFactory: ViewModelFactory @@ -161,8 +178,7 @@ class TimelineFragment : SFragment(), OnRefreshListener, StatusActionListener, I private fun setupRecyclerView() { binding.recyclerView.setAccessibilityDelegateCompat( - ListStatusAccessibilityDelegate(binding.recyclerView, this) - { pos -> viewModel.statuses.getOrNull(pos) } + ListStatusAccessibilityDelegate(binding.recyclerView, this) { pos -> viewModel.statuses.getOrNull(pos) } ) binding.recyclerView.setHasFixedSize(true) layoutManager = LinearLayoutManager(context) @@ -330,8 +346,10 @@ class TimelineFragment : SFragment(), OnRefreshListener, StatusActionListener, I } override fun onViewAccount(id: String) { - if ((viewModel.kind == TimelineViewModel.Kind.USER || - viewModel.kind == TimelineViewModel.Kind.USER_WITH_REPLIES) && + if (( + viewModel.kind == TimelineViewModel.Kind.USER || + viewModel.kind == TimelineViewModel.Kind.USER_WITH_REPLIES + ) && viewModel.id == id ) { /* If already viewing an account page, then any requests to view that account page @@ -369,9 +387,9 @@ class TimelineFragment : SFragment(), OnRefreshListener, StatusActionListener, I private fun actionButtonPresent(): Boolean { return viewModel.kind != TimelineViewModel.Kind.TAG && - viewModel.kind != TimelineViewModel.Kind.FAVOURITES && - viewModel.kind != TimelineViewModel.Kind.BOOKMARKS && - activity is ActionButtonActivity + viewModel.kind != TimelineViewModel.Kind.FAVOURITES && + viewModel.kind != TimelineViewModel.Kind.BOOKMARKS && + activity is ActionButtonActivity } private fun updateViews() { @@ -505,7 +523,6 @@ class TimelineFragment : SFragment(), OnRefreshListener, StatusActionListener, I private const val HASHTAGS_ARG = "hashtags" private const val ARG_ENABLE_SWIPE_TO_REFRESH = "enableSwipeToRefresh" - fun newInstance( kind: TimelineViewModel.Kind, hashtagOrId: String? = null, @@ -531,7 +548,6 @@ class TimelineFragment : SFragment(), OnRefreshListener, StatusActionListener, I return fragment } - private val diffCallback: DiffUtil.ItemCallback = object : DiffUtil.ItemCallback() { override fun areItemsTheSame( @@ -555,9 +571,9 @@ class TimelineFragment : SFragment(), OnRefreshListener, StatusActionListener, I return if (oldItem === newItem) { // If items are equal - update timestamp only listOf(StatusBaseViewHolder.Key.KEY_CREATED) - } else // If items are different - update the whole view holder + } else // If items are different - update the whole view holder null } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineRepository.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineRepository.kt index dac285593..1f7d32e74 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineRepository.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineRepository.kt @@ -5,11 +5,19 @@ import androidx.core.text.parseAsHtml import androidx.core.text.toHtml import com.google.gson.Gson import com.google.gson.reflect.TypeToken -import com.keylesspalace.tusky.db.* -import com.keylesspalace.tusky.entity.* -import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.components.timeline.TimelineRequestMode.DISK import com.keylesspalace.tusky.components.timeline.TimelineRequestMode.NETWORK +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.TimelineAccountEntity +import com.keylesspalace.tusky.db.TimelineDao +import com.keylesspalace.tusky.db.TimelineStatusEntity +import com.keylesspalace.tusky.db.TimelineStatusWithAccount +import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.entity.Emoji +import com.keylesspalace.tusky.entity.Poll +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.Either import com.keylesspalace.tusky.util.dec import com.keylesspalace.tusky.util.inc @@ -17,9 +25,8 @@ import com.keylesspalace.tusky.util.trimTrailingWhitespace import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.schedulers.Schedulers import java.io.IOException -import java.util.* +import java.util.Date import java.util.concurrent.TimeUnit -import kotlin.collections.ArrayList data class Placeholder(val id: String) @@ -31,7 +38,10 @@ enum class TimelineRequestMode { interface TimelineRepository { fun getStatuses( - maxId: String?, sinceId: String?, sincedIdMinusOne: String?, limit: Int, + maxId: String?, + sinceId: String?, + sincedIdMinusOne: String?, + limit: Int, requestMode: TimelineRequestMode ): Single> @@ -52,8 +62,11 @@ class TimelineRepositoryImpl( } override fun getStatuses( - maxId: String?, sinceId: String?, sincedIdMinusOne: String?, - limit: Int, requestMode: TimelineRequestMode + maxId: String?, + sinceId: String?, + sincedIdMinusOne: String?, + limit: Int, + requestMode: TimelineRequestMode ): Single> { val acc = accountManager.activeAccount ?: throw IllegalStateException() val accountId = acc.id @@ -66,9 +79,12 @@ class TimelineRepositoryImpl( } private fun getStatusesFromNetwork( - maxId: String?, sinceId: String?, - sinceIdMinusOne: String?, limit: Int, - accountId: Long, requestMode: TimelineRequestMode + maxId: String?, + sinceId: String?, + sinceIdMinusOne: String?, + limit: Int, + accountId: Long, + requestMode: TimelineRequestMode ): Single> { return mastodonApi.homeTimeline(maxId, sinceIdMinusOne, limit + 1) .map { response -> @@ -87,8 +103,11 @@ class TimelineRepositoryImpl( } private fun addFromDbIfNeeded( - accountId: Long, statuses: List>, - maxId: String?, sinceId: String?, limit: Int, + accountId: Long, + statuses: List>, + maxId: String?, + sinceId: String?, + limit: Int, requestMode: TimelineRequestMode ): Single> { return if (requestMode != NETWORK && statuses.size < 2) { @@ -113,7 +132,9 @@ class TimelineRepositoryImpl( } private fun getStatusesFromDb( - accountId: Long, maxId: String?, sinceId: String?, + accountId: Long, + maxId: String?, + sinceId: String?, limit: Int ): Single> { return timelineDao.getStatusesForAccount(accountId, maxId, sinceId, limit) @@ -124,8 +145,10 @@ class TimelineRepositoryImpl( } private fun saveStatusesToDb( - accountId: Long, statuses: List, - maxId: String?, sinceId: String? + accountId: Long, + statuses: List, + maxId: String?, + sinceId: String? ): List> { var placeholderToInsert: Placeholder? = null @@ -347,7 +370,6 @@ fun TimelineAccountEntity.toAccount(gson: Gson): Account { ) } - fun Placeholder.toEntity(timelineUserId: Long): TimelineStatusEntity { return TimelineStatusEntity( serverId = this.id, diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineViewModel.kt index 74ff7163d..5509b9294 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineViewModel.kt @@ -3,7 +3,20 @@ package com.keylesspalace.tusky.components.timeline import android.content.SharedPreferences import android.util.Log import androidx.lifecycle.viewModelScope -import com.keylesspalace.tusky.appstore.* +import com.keylesspalace.tusky.appstore.BlockEvent +import com.keylesspalace.tusky.appstore.BookmarkEvent +import com.keylesspalace.tusky.appstore.DomainMuteEvent +import com.keylesspalace.tusky.appstore.Event +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.FavoriteEvent +import com.keylesspalace.tusky.appstore.MuteConversationEvent +import com.keylesspalace.tusky.appstore.MuteEvent +import com.keylesspalace.tusky.appstore.PinEvent +import com.keylesspalace.tusky.appstore.PreferenceChangedEvent +import com.keylesspalace.tusky.appstore.ReblogEvent +import com.keylesspalace.tusky.appstore.StatusComposedEvent +import com.keylesspalace.tusky.appstore.StatusDeletedEvent +import com.keylesspalace.tusky.appstore.UnfollowEvent import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.entity.Filter import com.keylesspalace.tusky.entity.Poll @@ -12,7 +25,15 @@ import com.keylesspalace.tusky.network.FilterModel import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.TimelineCases import com.keylesspalace.tusky.settings.PrefKeys -import com.keylesspalace.tusky.util.* +import com.keylesspalace.tusky.util.Either +import com.keylesspalace.tusky.util.HttpHeaderLink +import com.keylesspalace.tusky.util.LinkHelper +import com.keylesspalace.tusky.util.RxAwareViewModel +import com.keylesspalace.tusky.util.dec +import com.keylesspalace.tusky.util.firstIsInstanceOrNull +import com.keylesspalace.tusky.util.inc +import com.keylesspalace.tusky.util.isLessThan +import com.keylesspalace.tusky.util.toViewData import com.keylesspalace.tusky.viewdata.StatusViewData import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.core.Single @@ -238,8 +259,8 @@ class TimelineViewModel @Inject constructor( private fun addStatusesBelow(statuses: MutableList>) { val fullFetch = isFullFetch(statuses) // Remove placeholder in the bottom if it's there - if (this.statuses.isNotEmpty() - && this.statuses.last() !is StatusViewData.Concrete + if (this.statuses.isNotEmpty() && + this.statuses.last() !is StatusViewData.Concrete ) { this.statuses.removeAt(this.statuses.lastIndex) } @@ -264,7 +285,7 @@ class TimelineViewModel @Inject constructor( fun loadGap(position: Int): Job { return viewModelScope.launch { - //check bounds before accessing list, + // check bounds before accessing list, if (statuses.size < position || position <= 0) { Log.e(TAG, "Wrong gap position: $position") return@launch @@ -318,7 +339,6 @@ class TimelineViewModel @Inject constructor( } catch (t: Exception) { ifExpected(t) { Log.d(TAG, "Failed to reblog status " + status.id, t) - } } } @@ -485,9 +505,9 @@ class TimelineViewModel @Inject constructor( } private fun shouldFilterStatus(status: Status): Boolean { - return status.inReplyToId != null && filterRemoveReplies - || status.reblog != null && filterRemoveReblogs - || filterModel.shouldFilterStatus(status.actionableStatus) + return status.inReplyToId != null && filterRemoveReplies || + status.reblog != null && filterRemoveReblogs || + filterModel.shouldFilterStatus(status.actionableStatus) } private fun extractNextId(response: Response<*>): String? { @@ -644,7 +664,8 @@ class TimelineViewModel @Inject constructor( private fun replacePlaceholderWithStatuses( newStatuses: MutableList>, - fullFetch: Boolean, pos: Int + fullFetch: Boolean, + pos: Int ) { val placeholder = statuses[pos] if (placeholder is StatusViewData.Placeholder) { @@ -873,9 +894,11 @@ class TimelineViewModel @Inject constructor( Log.e(TAG, "Failed to fetch filters", t) return@launch } - filterModel.initWithFilters(filters.filter { - filterContextMatchesKind(kind, it.context) - }) + filterModel.initWithFilters( + filters.filter { + filterContextMatchesKind(kind, it.context) + } + ) filterViewData(this@TimelineViewModel.statuses) } } @@ -891,7 +914,6 @@ class TimelineViewModel @Inject constructor( } } - companion object { private const val TAG = "TimelineVM" internal const val LOAD_AT_ONCE = 30 @@ -900,4 +922,4 @@ class TimelineViewModel @Inject constructor( enum class Kind { HOME, PUBLIC_LOCAL, PUBLIC_FEDERATED, TAG, USER, USER_PINNED, USER_WITH_REPLIES, FAVOURITES, LIST, BOOKMARKS } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AccountDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/AccountDao.kt index e1c64e28d..218c9b8f4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AccountDao.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/AccountDao.kt @@ -15,7 +15,11 @@ package com.keylesspalace.tusky.db -import androidx.room.* +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query @Dao interface AccountDao { @@ -27,5 +31,4 @@ interface AccountDao { @Query("SELECT * FROM AccountEntity ORDER BY id ASC") fun loadAll(): List - } diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt index ab6dbb7eb..0c25cbbc9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt @@ -21,42 +21,49 @@ import androidx.room.PrimaryKey import androidx.room.TypeConverters import com.keylesspalace.tusky.TabData import com.keylesspalace.tusky.defaultTabs - import com.keylesspalace.tusky.entity.Emoji import com.keylesspalace.tusky.entity.Status -@Entity(indices = [Index(value = ["domain", "accountId"], - unique = true)]) +@Entity( + indices = [ + Index( + value = ["domain", "accountId"], + unique = true + ) + ] +) @TypeConverters(Converters::class) -data class AccountEntity(@field:PrimaryKey(autoGenerate = true) var id: Long, - val domain: String, - var accessToken: String, - var isActive: Boolean, - var accountId: String = "", - var username: String = "", - var displayName: String = "", - var profilePictureUrl: String = "", - var notificationsEnabled: Boolean = true, - var notificationsMentioned: Boolean = true, - var notificationsFollowed: Boolean = true, - var notificationsFollowRequested: Boolean = false, - var notificationsReblogged: Boolean = true, - var notificationsFavorited: Boolean = true, - var notificationsPolls: Boolean = true, - var notificationsSubscriptions: Boolean = true, - var notificationSound: Boolean = true, - var notificationVibration: Boolean = true, - var notificationLight: Boolean = true, - var defaultPostPrivacy: Status.Visibility = Status.Visibility.PUBLIC, - var defaultMediaSensitivity: Boolean = false, - var alwaysShowSensitiveMedia: Boolean = false, - var alwaysOpenSpoiler: Boolean = false, - var mediaPreviewEnabled: Boolean = true, - var lastNotificationId: String = "0", - var activeNotifications: String = "[]", - var emojis: List = emptyList(), - var tabPreferences: List = defaultTabs(), - var notificationsFilter: String = "[\"follow_request\"]") { +data class AccountEntity( + @field:PrimaryKey(autoGenerate = true) var id: Long, + val domain: String, + var accessToken: String, + var isActive: Boolean, + var accountId: String = "", + var username: String = "", + var displayName: String = "", + var profilePictureUrl: String = "", + var notificationsEnabled: Boolean = true, + var notificationsMentioned: Boolean = true, + var notificationsFollowed: Boolean = true, + var notificationsFollowRequested: Boolean = false, + var notificationsReblogged: Boolean = true, + var notificationsFavorited: Boolean = true, + var notificationsPolls: Boolean = true, + var notificationsSubscriptions: Boolean = true, + var notificationSound: Boolean = true, + var notificationVibration: Boolean = true, + var notificationLight: Boolean = true, + var defaultPostPrivacy: Status.Visibility = Status.Visibility.PUBLIC, + var defaultMediaSensitivity: Boolean = false, + var alwaysShowSensitiveMedia: Boolean = false, + var alwaysOpenSpoiler: Boolean = false, + var mediaPreviewEnabled: Boolean = true, + var lastNotificationId: String = "0", + var activeNotifications: String = "[]", + var emojis: List = emptyList(), + var tabPreferences: List = defaultTabs(), + var notificationsFilter: String = "[\"follow_request\"]" +) { val identifier: String get() = "$domain:$accountId" diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt b/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt index 52650f77a..3de34f55e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt @@ -18,7 +18,7 @@ package com.keylesspalace.tusky.db import android.util.Log import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Status -import java.util.* +import java.util.Locale import javax.inject.Inject import javax.inject.Singleton @@ -66,7 +66,6 @@ class AccountManager @Inject constructor(db: AppDatabase) { val maxAccountId = accounts.maxByOrNull { it.id }?.id ?: 0 val newAccountId = maxAccountId + 1 activeAccount = AccountEntity(id = newAccountId, domain = domain.lowercase(Locale.ROOT), accessToken = accessToken, isActive = true) - } /** @@ -79,7 +78,6 @@ class AccountManager @Inject constructor(db: AppDatabase) { Log.d(TAG, "saveAccount: saving account with id " + account.id) accountDao.insertOrReplace(account) } - } /** @@ -103,9 +101,7 @@ class AccountManager @Inject constructor(db: AppDatabase) { activeAccount = null } return activeAccount - } - } /** @@ -129,13 +125,12 @@ class AccountManager @Inject constructor(db: AppDatabase) { val accountIndex = accounts.indexOf(it) if (accountIndex != -1) { - //in case the user was already logged in with this account, remove the old information + // in case the user was already logged in with this account, remove the old information accounts.removeAt(accountIndex) accounts.add(accountIndex, it) } else { accounts.add(it) } - } } @@ -194,5 +189,4 @@ class AccountManager @Inject constructor(db: AppDatabase) { id == accountId } } - -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt index 2d54e6746..393a23925 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt @@ -35,9 +35,8 @@ interface ConversationsDao { suspend fun delete(conversation: ConversationEntity): Int @Query("SELECT * FROM ConversationEntity WHERE accountId = :accountId ORDER BY s_createdAt DESC") - fun conversationsForAccount(accountId: Long) : PagingSource + fun conversationsForAccount(accountId: Long): PagingSource @Query("DELETE FROM ConversationEntity WHERE accountId = :accountId") fun deleteForAccount(accountId: Long) - } diff --git a/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt b/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt index 48792a1be..a59133dea 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt @@ -25,18 +25,23 @@ import com.google.gson.reflect.TypeToken import com.keylesspalace.tusky.TabData import com.keylesspalace.tusky.components.conversation.ConversationAccountEntity import com.keylesspalace.tusky.createTabDataFromId -import com.keylesspalace.tusky.entity.* +import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.entity.Emoji +import com.keylesspalace.tusky.entity.NewPoll +import com.keylesspalace.tusky.entity.Poll +import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.util.trimTrailingWhitespace import java.net.URLDecoder import java.net.URLEncoder -import java.util.* +import java.util.ArrayList +import java.util.Date import javax.inject.Inject import javax.inject.Singleton @ProvidedTypeConverter @Singleton class Converters @Inject constructor ( - private val gson: Gson + private val gson: Gson ) { @TypeConverter @@ -62,10 +67,10 @@ class Converters @Inject constructor ( @TypeConverter fun stringToTabData(str: String?): List? { return str?.split(";") - ?.map { - val data = it.split(":") - createTabDataFromId(data[0], data.drop(1).map { s -> URLDecoder.decode(s, "UTF-8") }) - } + ?.map { + val data = it.split(":") + createTabDataFromId(data[0], data.drop(1).map { s -> URLDecoder.decode(s, "UTF-8") }) + } } @TypeConverter @@ -126,7 +131,7 @@ class Converters @Inject constructor ( @TypeConverter fun spannedToString(spanned: Spanned?): String? { - if(spanned == null) { + if (spanned == null) { return null } return spanned.toHtml() @@ -134,7 +139,7 @@ class Converters @Inject constructor ( @TypeConverter fun stringToSpanned(spannedString: String?): Spanned? { - if(spannedString == null) { + if (spannedString == null) { return null } return spannedString.parseAsHtml().trimTrailingWhitespace() diff --git a/app/src/main/java/com/keylesspalace/tusky/db/DraftDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/DraftDao.kt index 88f683d8a..8029dd236 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/DraftDao.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/DraftDao.kt @@ -38,5 +38,4 @@ interface DraftDao { @Query("SELECT * FROM DraftEntity WHERE id = :id") suspend fun find(id: Int): DraftEntity? - } diff --git a/app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt index 184ff2c30..0df30040e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt @@ -28,24 +28,24 @@ import kotlinx.parcelize.Parcelize @Entity @TypeConverters(Converters::class) data class DraftEntity( - @PrimaryKey(autoGenerate = true) val id: Int = 0, - val accountId: Long, - val inReplyToId: String?, - val content: String?, - val contentWarning: String?, - val sensitive: Boolean, - val visibility: Status.Visibility, - val attachments: List, - val poll: NewPoll?, - val failedToSend: Boolean + @PrimaryKey(autoGenerate = true) val id: Int = 0, + val accountId: Long, + val inReplyToId: String?, + val content: String?, + val contentWarning: String?, + val sensitive: Boolean, + val visibility: Status.Visibility, + val attachments: List, + val poll: NewPoll?, + val failedToSend: Boolean ) @Parcelize data class DraftAttachment( - val uriString: String, - val description: String?, - val type: Type -): Parcelable { + val uriString: String, + val description: String?, + val type: Type +) : Parcelable { val uri: Uri get() = uriString.toUri() diff --git a/app/src/main/java/com/keylesspalace/tusky/db/InstanceEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/InstanceEntity.kt index 1e2adaf04..ac4464f2d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/InstanceEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/InstanceEntity.kt @@ -23,10 +23,10 @@ import com.keylesspalace.tusky.entity.Emoji @Entity @TypeConverters(Converters::class) data class InstanceEntity( - @field:PrimaryKey var instance: String, - val emojiList: List?, - val maximumTootCharacters: Int?, - val maxPollOptions: Int?, - val maxPollOptionLength: Int?, - val version: String? + @field:PrimaryKey var instance: String, + val emojiList: List?, + val maximumTootCharacters: Int?, + val maxPollOptions: Int?, + val maxPollOptionLength: Int?, + val version: String? ) diff --git a/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt index 82e97aed1..6bbc08047 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt @@ -17,11 +17,11 @@ abstract class TimelineDao { @Insert(onConflict = REPLACE) abstract fun insertStatus(timelineAccountEntity: TimelineStatusEntity): Long - @Insert(onConflict = IGNORE) abstract fun insertStatusIfNotThere(timelineAccountEntity: TimelineStatusEntity): Long - @Query(""" + @Query( + """ SELECT s.serverId, s.url, s.timelineUserId, s.authorServerId, s.inReplyToId, s.inReplyToAccountId, s.createdAt, s.emojis, s.reblogsCount, s.favouritesCount, s.reblogged, s.favourited, s.bookmarked, s.sensitive, @@ -46,47 +46,62 @@ AND (CASE WHEN :sinceId IS NOT NULL THEN (LENGTH(s.serverId) > LENGTH(:sinceId) OR LENGTH(s.serverId) == LENGTH(:sinceId) AND s.serverId > :sinceId) ELSE 1 END) ORDER BY LENGTH(s.serverId) DESC, s.serverId DESC -LIMIT :limit""") +LIMIT :limit""" + ) abstract fun getStatusesForAccount(account: Long, maxId: String?, sinceId: String?, limit: Int): Single> - @Transaction - open fun insertInTransaction(status: TimelineStatusEntity, account: TimelineAccountEntity, - reblogAccount: TimelineAccountEntity?) { + open fun insertInTransaction( + status: TimelineStatusEntity, + account: TimelineAccountEntity, + reblogAccount: TimelineAccountEntity? + ) { insertAccount(account) reblogAccount?.let(this::insertAccount) insertStatus(status) } - @Query("""DELETE FROM TimelineStatusEntity WHERE timelineUserId = :accountId AND + @Query( + """DELETE FROM TimelineStatusEntity WHERE timelineUserId = :accountId AND (LENGTH(serverId) < LENGTH(:maxId) OR LENGTH(serverId) == LENGTH(:maxId) AND serverId < :maxId) AND (LENGTH(serverId) > LENGTH(:minId) OR LENGTH(serverId) == LENGTH(:minId) AND serverId > :minId) - """) + """ + ) abstract fun deleteRange(accountId: Long, minId: String, maxId: String) - @Query("""DELETE FROM TimelineStatusEntity WHERE authorServerId = null + @Query( + """DELETE FROM TimelineStatusEntity WHERE authorServerId = null AND timelineUserId = :account AND (LENGTH(serverId) < LENGTH(:maxId) OR LENGTH(serverId) == LENGTH(:maxId) AND serverId < :maxId) AND (LENGTH(serverId) > LENGTH(:sinceId) OR LENGTH(serverId) == LENGTH(:sinceId) AND serverId > :sinceId) -""") +""" + ) abstract fun removeAllPlaceholdersBetween(account: Long, maxId: String, sinceId: String) - @Query("""UPDATE TimelineStatusEntity SET favourited = :favourited -WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId)""") + @Query( + """UPDATE TimelineStatusEntity SET favourited = :favourited +WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId)""" + ) abstract fun setFavourited(accountId: Long, statusId: String, favourited: Boolean) - @Query("""UPDATE TimelineStatusEntity SET bookmarked = :bookmarked -WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId)""") + @Query( + """UPDATE TimelineStatusEntity SET bookmarked = :bookmarked +WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId)""" + ) abstract fun setBookmarked(accountId: Long, statusId: String, bookmarked: Boolean) - @Query("""UPDATE TimelineStatusEntity SET reblogged = :reblogged -WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId)""") + @Query( + """UPDATE TimelineStatusEntity SET reblogged = :reblogged +WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId)""" + ) abstract fun setReblogged(accountId: Long, statusId: String, reblogged: Boolean) - @Query("""DELETE FROM TimelineStatusEntity WHERE timelineUserId = :accountId AND -(authorServerId = :userId OR reblogAccountId = :userId)""") + @Query( + """DELETE FROM TimelineStatusEntity WHERE timelineUserId = :accountId AND +(authorServerId = :userId OR reblogAccountId = :userId)""" + ) abstract fun removeAllByUser(accountId: Long, userId: String) @Query("DELETE FROM TimelineStatusEntity WHERE timelineUserId = :accountId") @@ -95,14 +110,18 @@ WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = @Query("DELETE FROM TimelineAccountEntity WHERE timelineUserId = :accountId") abstract fun removeAllUsersForAccount(accountId: Long) - @Query("""DELETE FROM TimelineStatusEntity WHERE timelineUserId = :accountId -AND serverId = :statusId""") + @Query( + """DELETE FROM TimelineStatusEntity WHERE timelineUserId = :accountId +AND serverId = :statusId""" + ) abstract fun delete(accountId: Long, statusId: String) @Query("""DELETE FROM TimelineStatusEntity WHERE createdAt < :olderThan""") abstract fun cleanup(olderThan: Long) - @Query("""UPDATE TimelineStatusEntity SET poll = :poll -WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId)""") + @Query( + """UPDATE TimelineStatusEntity SET poll = :poll +WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId)""" + ) abstract fun setVoted(accountId: Long, statusId: String, poll: String) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt index 296111d30..4e2db4ff3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt @@ -1,6 +1,10 @@ package com.keylesspalace.tusky.db -import androidx.room.* +import androidx.room.Embedded +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.TypeConverters import com.keylesspalace.tusky.entity.Status /** @@ -15,62 +19,63 @@ import com.keylesspalace.tusky.entity.Status * fields. */ @Entity( - primaryKeys = ["serverId", "timelineUserId"], - foreignKeys = ([ + primaryKeys = ["serverId", "timelineUserId"], + foreignKeys = ( + [ ForeignKey( - entity = TimelineAccountEntity::class, - parentColumns = ["serverId", "timelineUserId"], - childColumns = ["authorServerId", "timelineUserId"] + entity = TimelineAccountEntity::class, + parentColumns = ["serverId", "timelineUserId"], + childColumns = ["authorServerId", "timelineUserId"] ) - ]), - // Avoiding rescanning status table when accounts table changes. Recommended by Room(c). - indices = [Index("authorServerId", "timelineUserId")] + ] + ), + // Avoiding rescanning status table when accounts table changes. Recommended by Room(c). + indices = [Index("authorServerId", "timelineUserId")] ) @TypeConverters(Converters::class) data class TimelineStatusEntity( - val serverId: String, // id never flips: we need it for sorting so it's a real id - val url: String?, - // our local id for the logged in user in case there are multiple accounts per instance - val timelineUserId: Long, - val authorServerId: String?, - val inReplyToId: String?, - val inReplyToAccountId: String?, - val content: String?, - val createdAt: Long, - val emojis: String?, - val reblogsCount: Int, - val favouritesCount: Int, - val reblogged: Boolean, - val bookmarked: Boolean, - val favourited: Boolean, - val sensitive: Boolean, - val spoilerText: String?, - val visibility: Status.Visibility?, - val attachments: String?, - val mentions: String?, - val application: String?, - val reblogServerId: String?, // if it has a reblogged status, it's id is stored here - val reblogAccountId: String?, - val poll: String?, - val muted: Boolean? + val serverId: String, // id never flips: we need it for sorting so it's a real id + val url: String?, + // our local id for the logged in user in case there are multiple accounts per instance + val timelineUserId: Long, + val authorServerId: String?, + val inReplyToId: String?, + val inReplyToAccountId: String?, + val content: String?, + val createdAt: Long, + val emojis: String?, + val reblogsCount: Int, + val favouritesCount: Int, + val reblogged: Boolean, + val bookmarked: Boolean, + val favourited: Boolean, + val sensitive: Boolean, + val spoilerText: String?, + val visibility: Status.Visibility?, + val attachments: String?, + val mentions: String?, + val application: String?, + val reblogServerId: String?, // if it has a reblogged status, it's id is stored here + val reblogAccountId: String?, + val poll: String?, + val muted: Boolean? ) @Entity( - primaryKeys = ["serverId", "timelineUserId"] + primaryKeys = ["serverId", "timelineUserId"] ) data class TimelineAccountEntity( - val serverId: String, - val timelineUserId: Long, - val localUsername: String, - val username: String, - val displayName: String, - val url: String, - val avatar: String, - val emojis: String, - val bot: Boolean + val serverId: String, + val timelineUserId: Long, + val localUsername: String, + val username: String, + val displayName: String, + val url: String, + val avatar: String, + val emojis: String, + val bot: Boolean ) - class TimelineStatusWithAccount { @Embedded lateinit var status: TimelineStatusEntity @@ -78,4 +83,4 @@ class TimelineStatusWithAccount { lateinit var account: TimelineAccountEntity @Embedded(prefix = "rb_") var reblogAccount: TimelineAccountEntity? = null -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt index cdf10224b..cbeda2f41 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt @@ -15,7 +15,23 @@ package com.keylesspalace.tusky.di -import com.keylesspalace.tusky.* +import com.keylesspalace.tusky.AboutActivity +import com.keylesspalace.tusky.AccountActivity +import com.keylesspalace.tusky.AccountListActivity +import com.keylesspalace.tusky.BaseActivity +import com.keylesspalace.tusky.EditProfileActivity +import com.keylesspalace.tusky.FiltersActivity +import com.keylesspalace.tusky.LicenseActivity +import com.keylesspalace.tusky.ListsActivity +import com.keylesspalace.tusky.LoginActivity +import com.keylesspalace.tusky.MainActivity +import com.keylesspalace.tusky.ModalTimelineActivity +import com.keylesspalace.tusky.SplashActivity +import com.keylesspalace.tusky.StatusListActivity +import com.keylesspalace.tusky.TabPreferenceActivity +import com.keylesspalace.tusky.ViewMediaActivity +import com.keylesspalace.tusky.ViewTagActivity +import com.keylesspalace.tusky.ViewThreadActivity import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity import com.keylesspalace.tusky.components.compose.ComposeActivity import com.keylesspalace.tusky.components.drafts.DraftsActivity diff --git a/app/src/main/java/com/keylesspalace/tusky/di/AppComponent.kt b/app/src/main/java/com/keylesspalace/tusky/di/AppComponent.kt index ff3d02669..51596ac71 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/AppComponent.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/AppComponent.kt @@ -21,23 +21,24 @@ import dagger.Component import dagger.android.support.AndroidSupportInjectionModule import javax.inject.Singleton - /** * Created by charlag on 3/21/18. */ @Singleton -@Component(modules = [ - AppModule::class, - NetworkModule::class, - AndroidSupportInjectionModule::class, - ActivitiesModule::class, - ServicesModule::class, - BroadcastReceiverModule::class, - ViewModelModule::class, - RepositoryModule::class, - MediaUploaderModule::class -]) +@Component( + modules = [ + AppModule::class, + NetworkModule::class, + AndroidSupportInjectionModule::class, + ActivitiesModule::class, + ServicesModule::class, + BroadcastReceiverModule::class, + ViewModelModule::class, + RepositoryModule::class, + MediaUploaderModule::class + ] +) interface AppComponent { @Component.Builder interface Builder { @@ -48,4 +49,4 @@ interface AppComponent { } fun inject(app: TuskyApplication) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/di/AppInjector.kt b/app/src/main/java/com/keylesspalace/tusky/di/AppInjector.kt index 21fd184a6..6446a7357 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/AppInjector.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/AppInjector.kt @@ -34,7 +34,7 @@ import dagger.android.support.AndroidSupportInjection object AppInjector { fun init(app: TuskyApplication) { DaggerAppComponent.builder().application(app) - .build().inject(app) + .build().inject(app) app.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks { override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { @@ -58,7 +58,6 @@ object AppInjector { override fun onActivityStopped(activity: Activity) { } - }) } @@ -68,13 +67,15 @@ object AppInjector { } if (activity is FragmentActivity) { activity.supportFragmentManager.registerFragmentLifecycleCallbacks( - object : FragmentManager.FragmentLifecycleCallbacks() { - override fun onFragmentPreAttached(fm: FragmentManager, f: Fragment, context: Context) { - if (f is Injectable) { - AndroidSupportInjection.inject(f) - } + object : FragmentManager.FragmentLifecycleCallbacks() { + override fun onFragmentPreAttached(fm: FragmentManager, f: Fragment, context: Context) { + if (f is Injectable) { + AndroidSupportInjection.inject(f) } - }, true) + } + }, + true + ) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt index faf0a3863..4cce3f447 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt @@ -13,7 +13,6 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ - package com.keylesspalace.tusky.di import android.app.Application @@ -60,8 +59,10 @@ class AppModule { } @Provides - fun providesTimelineUseCases(api: MastodonApi, - eventHub: EventHub): TimelineCases { + fun providesTimelineUseCases( + api: MastodonApi, + eventHub: EventHub + ): TimelineCases { return TimelineCasesImpl(api, eventHub) } @@ -73,24 +74,24 @@ class AppModule { @Singleton fun providesDatabase(appContext: Context, converters: Converters): AppDatabase { return Room.databaseBuilder(appContext, AppDatabase::class.java, "tuskyDB") - .addTypeConverter(converters) - .allowMainThreadQueries() - .addMigrations(AppDatabase.MIGRATION_2_3, AppDatabase.MIGRATION_3_4, AppDatabase.MIGRATION_4_5, - AppDatabase.MIGRATION_5_6, AppDatabase.MIGRATION_6_7, AppDatabase.MIGRATION_7_8, - AppDatabase.MIGRATION_8_9, AppDatabase.MIGRATION_9_10, AppDatabase.MIGRATION_10_11, - AppDatabase.MIGRATION_11_12, AppDatabase.MIGRATION_12_13, AppDatabase.MIGRATION_10_13, - AppDatabase.MIGRATION_13_14, AppDatabase.MIGRATION_14_15, AppDatabase.MIGRATION_15_16, - AppDatabase.MIGRATION_16_17, AppDatabase.MIGRATION_17_18, AppDatabase.MIGRATION_18_19, - AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21, AppDatabase.MIGRATION_21_22, - AppDatabase.MIGRATION_22_23, AppDatabase.MIGRATION_23_24, AppDatabase.MIGRATION_24_25, - AppDatabase.MIGRATION_26_27, - AppDatabase.Migration25_26(appContext.getExternalFilesDir("Tusky")) - ) - .build() + .addTypeConverter(converters) + .allowMainThreadQueries() + .addMigrations( + AppDatabase.MIGRATION_2_3, AppDatabase.MIGRATION_3_4, AppDatabase.MIGRATION_4_5, + AppDatabase.MIGRATION_5_6, AppDatabase.MIGRATION_6_7, AppDatabase.MIGRATION_7_8, + AppDatabase.MIGRATION_8_9, AppDatabase.MIGRATION_9_10, AppDatabase.MIGRATION_10_11, + AppDatabase.MIGRATION_11_12, AppDatabase.MIGRATION_12_13, AppDatabase.MIGRATION_10_13, + AppDatabase.MIGRATION_13_14, AppDatabase.MIGRATION_14_15, AppDatabase.MIGRATION_15_16, + AppDatabase.MIGRATION_16_17, AppDatabase.MIGRATION_17_18, AppDatabase.MIGRATION_18_19, + AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21, AppDatabase.MIGRATION_21_22, + AppDatabase.MIGRATION_22_23, AppDatabase.MIGRATION_23_24, AppDatabase.MIGRATION_24_25, + AppDatabase.MIGRATION_26_27, + AppDatabase.Migration25_26(appContext.getExternalFilesDir("Tusky")) + ) + .build() } @Provides @Singleton fun notifier(context: Context): Notifier = SystemNotifier(context) - } diff --git a/app/src/main/java/com/keylesspalace/tusky/di/BroadcastReceiverModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/BroadcastReceiverModule.kt index edf95341c..b7213fa64 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/BroadcastReceiverModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/BroadcastReceiverModule.kt @@ -16,16 +16,16 @@ package com.keylesspalace.tusky.di -import com.keylesspalace.tusky.receiver.SendStatusBroadcastReceiver import com.keylesspalace.tusky.receiver.NotificationClearBroadcastReceiver +import com.keylesspalace.tusky.receiver.SendStatusBroadcastReceiver import dagger.Module import dagger.android.ContributesAndroidInjector @Module abstract class BroadcastReceiverModule { @ContributesAndroidInjector - abstract fun contributeSendStatusBroadcastReceiver() : SendStatusBroadcastReceiver + abstract fun contributeSendStatusBroadcastReceiver(): SendStatusBroadcastReceiver @ContributesAndroidInjector - abstract fun contributeNotificationClearBroadcastReceiver() : NotificationClearBroadcastReceiver -} \ No newline at end of file + abstract fun contributeNotificationClearBroadcastReceiver(): NotificationClearBroadcastReceiver +} diff --git a/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt index 16ed59cc2..704252aea 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt @@ -13,23 +13,25 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ - package com.keylesspalace.tusky.di import com.keylesspalace.tusky.AccountsInListFragment import com.keylesspalace.tusky.components.conversation.ConversationsFragment import com.keylesspalace.tusky.components.instancemute.fragment.InstanceListFragment -import com.keylesspalace.tusky.fragment.* import com.keylesspalace.tusky.components.preference.AccountPreferencesFragment import com.keylesspalace.tusky.components.preference.NotificationPreferencesFragment +import com.keylesspalace.tusky.components.preference.PreferencesFragment import com.keylesspalace.tusky.components.report.fragments.ReportDoneFragment import com.keylesspalace.tusky.components.report.fragments.ReportNoteFragment import com.keylesspalace.tusky.components.report.fragments.ReportStatusesFragment import com.keylesspalace.tusky.components.search.fragments.SearchAccountsFragment import com.keylesspalace.tusky.components.search.fragments.SearchHashtagsFragment import com.keylesspalace.tusky.components.search.fragments.SearchStatusesFragment -import com.keylesspalace.tusky.components.preference.PreferencesFragment import com.keylesspalace.tusky.components.timeline.TimelineFragment +import com.keylesspalace.tusky.fragment.AccountListFragment +import com.keylesspalace.tusky.fragment.AccountMediaFragment +import com.keylesspalace.tusky.fragment.NotificationsFragment +import com.keylesspalace.tusky.fragment.ViewThreadFragment import dagger.Module import dagger.android.ContributesAndroidInjector @@ -89,5 +91,4 @@ abstract class FragmentBuildersModule { @ContributesAndroidInjector abstract fun preferencesFragment(): PreferencesFragment - } diff --git a/app/src/main/java/com/keylesspalace/tusky/di/Injectable.kt b/app/src/main/java/com/keylesspalace/tusky/di/Injectable.kt index 1df715e70..f3b4e8105 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/Injectable.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/Injectable.kt @@ -13,11 +13,10 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ - package com.keylesspalace.tusky.di /** * Created by charlag on 3/24/18. */ -interface Injectable \ No newline at end of file +interface Injectable diff --git a/app/src/main/java/com/keylesspalace/tusky/di/MediaUploaderModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/MediaUploaderModule.kt index 66dc27110..00ec1b73e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/MediaUploaderModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/MediaUploaderModule.kt @@ -26,5 +26,5 @@ import dagger.Provides class MediaUploaderModule { @Provides fun providesMediaUploder(context: Context, mastodonApi: MastodonApi): MediaUploader = - MediaUploaderImpl(context, mastodonApi) -} \ No newline at end of file + MediaUploaderImpl(context, mastodonApi) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt index 89e28553c..7bda6ef74 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt @@ -53,16 +53,16 @@ class NetworkModule { @Singleton fun providesGson(): Gson { return GsonBuilder() - .registerTypeAdapter(Spanned::class.java, SpannedTypeAdapter()) - .create() + .registerTypeAdapter(Spanned::class.java, SpannedTypeAdapter()) + .create() } @Provides @Singleton fun providesHttpClient( - accountManager: AccountManager, - context: Context, - preferences: SharedPreferences + accountManager: AccountManager, + context: Context, + preferences: SharedPreferences ): OkHttpClient { val httpProxyEnabled = preferences.getBoolean("httpProxyEnabled", false) val httpServer = preferences.getNonNullString("httpProxyServer", "") @@ -92,30 +92,29 @@ class NetworkModule { builder.proxy(Proxy(Proxy.Type.HTTP, address)) } return builder - .apply { - addInterceptor(InstanceSwitchAuthInterceptor(accountManager)) - if (BuildConfig.DEBUG) { - addInterceptor(HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BASIC }) - } + .apply { + addInterceptor(InstanceSwitchAuthInterceptor(accountManager)) + if (BuildConfig.DEBUG) { + addInterceptor(HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BASIC }) } - .build() + } + .build() } @Provides @Singleton fun providesRetrofit( - httpClient: OkHttpClient, - gson: Gson + httpClient: OkHttpClient, + gson: Gson ): Retrofit { return Retrofit.Builder().baseUrl("https://" + MastodonApi.PLACEHOLDER_DOMAIN) - .client(httpClient) - .addConverterFactory(GsonConverterFactory.create(gson)) - .addCallAdapterFactory(RxJava3CallAdapterFactory.create()) - .build() - + .client(httpClient) + .addConverterFactory(GsonConverterFactory.create(gson)) + .addCallAdapterFactory(RxJava3CallAdapterFactory.create()) + .build() } @Provides @Singleton fun providesApi(retrofit: Retrofit): MastodonApi = retrofit.create() -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/di/RepositoryModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/RepositoryModule.kt index 5086c752d..e94c55d19 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/RepositoryModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/RepositoryModule.kt @@ -1,11 +1,11 @@ package com.keylesspalace.tusky.di import com.google.gson.Gson +import com.keylesspalace.tusky.components.timeline.TimelineRepository +import com.keylesspalace.tusky.components.timeline.TimelineRepositoryImpl import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.components.timeline.TimelineRepository -import com.keylesspalace.tusky.components.timeline.TimelineRepositoryImpl import dagger.Module import dagger.Provides @@ -13,11 +13,11 @@ import dagger.Provides class RepositoryModule { @Provides fun providesTimelineRepository( - db: AppDatabase, - mastodonApi: MastodonApi, - accountManager: AccountManager, - gson: Gson + db: AppDatabase, + mastodonApi: MastodonApi, + accountManager: AccountManager, + gson: Gson ): TimelineRepository { return TimelineRepositoryImpl(db.timelineDao(), mastodonApi, accountManager, gson) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/di/ServicesModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/ServicesModule.kt index 5f6495543..f34dc0750 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/ServicesModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/ServicesModule.kt @@ -36,4 +36,4 @@ abstract class ServicesModule { return ServiceClientImpl(context) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt b/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt index a71817ecb..8cb0a172d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt @@ -11,7 +11,6 @@ import com.keylesspalace.tusky.components.drafts.DraftsViewModel import com.keylesspalace.tusky.components.report.ReportViewModel import com.keylesspalace.tusky.components.scheduled.ScheduledTootViewModel import com.keylesspalace.tusky.components.search.SearchViewModel -import com.keylesspalace.tusky.components.timeline.TimelineFragment import com.keylesspalace.tusky.components.timeline.TimelineViewModel import com.keylesspalace.tusky.viewmodel.AccountViewModel import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel @@ -63,7 +62,6 @@ abstract class ViewModelModule { @ViewModelKey(ListsViewModel::class) internal abstract fun listsViewModel(viewModel: ListsViewModel): ViewModel - @Binds @IntoMap @ViewModelKey(AccountsInListViewModel::class) @@ -104,5 +102,5 @@ abstract class ViewModelModule { @ViewModelKey(TimelineViewModel::class) internal abstract fun timelineViewModel(viewModel: TimelineViewModel): ViewModel - //Add more ViewModels here + // Add more ViewModels here } diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/AccessToken.kt b/app/src/main/java/com/keylesspalace/tusky/entity/AccessToken.kt index 181078839..e974ce196 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/AccessToken.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/AccessToken.kt @@ -18,5 +18,5 @@ package com.keylesspalace.tusky.entity import com.google.gson.annotations.SerializedName data class AccessToken( - @SerializedName("access_token") val accessToken: String + @SerializedName("access_token") val accessToken: String ) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Account.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Account.kt index d4940fe0e..49df50734 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Account.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Account.kt @@ -20,23 +20,23 @@ import com.google.gson.annotations.SerializedName import java.util.Date data class Account( - val id: String, - @SerializedName("username") val localUsername: String, - @SerializedName("acct") val username: String, - @SerializedName("display_name") val displayName: String?, // should never be null per Api definition, but some servers break the contract - val note: Spanned, - val url: String, - val avatar: String, - val header: String, - val locked: Boolean = false, - @SerializedName("followers_count") val followersCount: Int = 0, - @SerializedName("following_count") val followingCount: Int = 0, - @SerializedName("statuses_count") val statusesCount: Int = 0, - val source: AccountSource? = null, - val bot: Boolean = false, - val emojis: List? = emptyList(), // nullable for backward compatibility - val fields: List? = emptyList(), //nullable for backward compatibility - val moved: Account? = null + val id: String, + @SerializedName("username") val localUsername: String, + @SerializedName("acct") val username: String, + @SerializedName("display_name") val displayName: String?, // should never be null per Api definition, but some servers break the contract + val note: Spanned, + val url: String, + val avatar: String, + val header: String, + val locked: Boolean = false, + @SerializedName("followers_count") val followersCount: Int = 0, + @SerializedName("following_count") val followingCount: Int = 0, + @SerializedName("statuses_count") val statusesCount: Int = 0, + val source: AccountSource? = null, + val bot: Boolean = false, + val emojis: List? = emptyList(), // nullable for backward compatibility + val fields: List? = emptyList(), // nullable for backward compatibility + val moved: Account? = null ) { @@ -57,41 +57,41 @@ data class Account( } fun deepEquals(other: Account): Boolean { - return id == other.id - && localUsername == other.localUsername - && displayName == other.displayName - && note == other.note - && url == other.url - && avatar == other.avatar - && header == other.header - && locked == other.locked - && followersCount == other.followersCount - && followingCount == other.followingCount - && statusesCount == other.statusesCount - && source == other.source - && bot == other.bot - && emojis == other.emojis - && fields == other.fields - && moved == other.moved + return id == other.id && + localUsername == other.localUsername && + displayName == other.displayName && + note == other.note && + url == other.url && + avatar == other.avatar && + header == other.header && + locked == other.locked && + followersCount == other.followersCount && + followingCount == other.followingCount && + statusesCount == other.statusesCount && + source == other.source && + bot == other.bot && + emojis == other.emojis && + fields == other.fields && + moved == other.moved } fun isRemote(): Boolean = this.username != this.localUsername } data class AccountSource( - val privacy: Status.Visibility, - val sensitive: Boolean, - val note: String, - val fields: List? + val privacy: Status.Visibility, + val sensitive: Boolean, + val note: String, + val fields: List? ) -data class Field ( - val name: String, - val value: Spanned, - @SerializedName("verified_at") val verifiedAt: Date? +data class Field( + val name: String, + val value: Spanned, + @SerializedName("verified_at") val verifiedAt: Date? ) -data class StringField ( - val name: String, - val value: String +data class StringField( + val name: String, + val value: String ) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Announcement.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Announcement.kt index 5cd32fe8d..400e9764d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Announcement.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Announcement.kt @@ -17,22 +17,22 @@ package com.keylesspalace.tusky.entity import android.text.Spanned import com.google.gson.annotations.SerializedName -import java.util.* +import java.util.Date data class Announcement( - val id: String, - val content: Spanned, - @SerializedName("starts_at") val startsAt: Date?, - @SerializedName("ends_at") val endsAt: Date?, - @SerializedName("all_day") val allDay: Boolean, - @SerializedName("published_at") val publishedAt: Date, - @SerializedName("updated_at") val updatedAt: Date, - val read: Boolean, - val mentions: List, - val statuses: List, - val tags: List, - val emojis: List, - val reactions: List + val id: String, + val content: Spanned, + @SerializedName("starts_at") val startsAt: Date?, + @SerializedName("ends_at") val endsAt: Date?, + @SerializedName("all_day") val allDay: Boolean, + @SerializedName("published_at") val publishedAt: Date, + @SerializedName("updated_at") val updatedAt: Date, + val read: Boolean, + val mentions: List, + val statuses: List, + val tags: List, + val emojis: List, + val reactions: List ) { override fun equals(other: Any?): Boolean { @@ -48,10 +48,10 @@ data class Announcement( } data class Reaction( - val name: String, - var count: Int, - var me: Boolean, - val url: String?, - @SerializedName("static_url") val staticUrl: String? + val name: String, + var count: Int, + var me: Boolean, + val url: String?, + @SerializedName("static_url") val staticUrl: String? ) } diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/AppCredentials.kt b/app/src/main/java/com/keylesspalace/tusky/entity/AppCredentials.kt index 95a829c14..fe6b0c3ce 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/AppCredentials.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/AppCredentials.kt @@ -18,6 +18,6 @@ package com.keylesspalace.tusky.entity import com.google.gson.annotations.SerializedName data class AppCredentials( - @SerializedName("client_id") val clientId: String, - @SerializedName("client_secret") val clientSecret: String + @SerializedName("client_id") val clientId: String, + @SerializedName("client_secret") val clientSecret: String ) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Attachment.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Attachment.kt index 3e14519ac..27fdc8be6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Attachment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Attachment.kt @@ -26,13 +26,13 @@ import kotlinx.parcelize.Parcelize @Parcelize data class Attachment( - val id: String, - val url: String, - @SerializedName("preview_url") val previewUrl: String?, // can be null for e.g. audio attachments - val meta: MetaData?, - val type: Type, - val description: String?, - val blurhash: String? + val id: String, + val url: String, + @SerializedName("preview_url") val previewUrl: String?, // can be null for e.g. audio attachments + val meta: MetaData?, + val type: Type, + val description: String?, + val blurhash: String? ) : Parcelable { @JsonAdapter(MediaTypeDeserializer::class) @@ -66,9 +66,9 @@ data class Attachment( * The meta data of an [Attachment]. */ @Parcelize - data class MetaData ( - val focus: Focus?, - val duration: Float? + data class MetaData( + val focus: Focus?, + val duration: Float? ) : Parcelable /** @@ -78,8 +78,8 @@ data class Attachment( * https://github.com/jonom/jquery-focuspoint#1-calculate-your-images-focus-point */ @Parcelize - data class Focus ( - val x: Float, - val y: Float + data class Focus( + val x: Float, + val y: Float ) : Parcelable } diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Card.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Card.kt index ada9ec205..52011f3d1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Card.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Card.kt @@ -19,16 +19,16 @@ import android.text.Spanned import com.google.gson.annotations.SerializedName data class Card( - val url: String, - val title: Spanned, - val description: Spanned, - @SerializedName("author_name") val authorName: String, - val image: String, - val type: String, - val width: Int, - val height: Int, - val blurhash: String?, - val embed_url: String? + val url: String, + val title: Spanned, + val description: Spanned, + @SerializedName("author_name") val authorName: String, + val image: String, + val type: String, + val width: Int, + val height: Int, + val blurhash: String?, + val embed_url: String? ) { override fun hashCode(): Int { diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Conversation.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Conversation.kt index 0e66385fd..cb09981db 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Conversation.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Conversation.kt @@ -18,8 +18,8 @@ package com.keylesspalace.tusky.entity import com.google.gson.annotations.SerializedName data class Conversation( - val id: String, - val accounts: List, - @SerializedName("last_status") val lastStatus: Status?, // should never be null, but apparently its possible https://github.com/tuskyapp/Tusky/issues/1038 - val unread: Boolean -) \ No newline at end of file + val id: String, + val accounts: List, + @SerializedName("last_status") val lastStatus: Status?, // should never be null, but apparently its possible https://github.com/tuskyapp/Tusky/issues/1038 + val unread: Boolean +) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/DeletedStatus.kt b/app/src/main/java/com/keylesspalace/tusky/entity/DeletedStatus.kt index 289a93fb7..92a35b69c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/DeletedStatus.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/DeletedStatus.kt @@ -16,19 +16,20 @@ package com.keylesspalace.tusky.entity import com.google.gson.annotations.SerializedName -import java.util.* +import java.util.ArrayList +import java.util.Date data class DeletedStatus( - var text: String?, - @SerializedName("in_reply_to_id") var inReplyToId: String?, - @SerializedName("spoiler_text") val spoilerText: String, - val visibility: Status.Visibility, - val sensitive: Boolean, - @SerializedName("media_attachments") var attachments: ArrayList?, - val poll: Poll?, - @SerializedName("created_at") val createdAt: Date + var text: String?, + @SerializedName("in_reply_to_id") var inReplyToId: String?, + @SerializedName("spoiler_text") val spoilerText: String, + val visibility: Status.Visibility, + val sensitive: Boolean, + @SerializedName("media_attachments") var attachments: ArrayList?, + val poll: Poll?, + @SerializedName("created_at") val createdAt: Date ) { fun isEmpty(): Boolean { return text == null && attachments == null } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Emoji.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Emoji.kt index 42bb99e93..130831a2d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Emoji.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Emoji.kt @@ -21,8 +21,8 @@ import kotlinx.parcelize.Parcelize @Parcelize data class Emoji( - val shortcode: String, - val url: String, - @SerializedName("static_url") val staticUrl: String, - @SerializedName("visible_in_picker") val visibleInPicker: Boolean? + val shortcode: String, + val url: String, + @SerializedName("static_url") val staticUrl: String, + @SerializedName("visible_in_picker") val visibleInPicker: Boolean? ) : Parcelable diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Filter.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Filter.kt index 58bdc79a9..34b80e83b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Filter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Filter.kt @@ -17,7 +17,7 @@ package com.keylesspalace.tusky.entity import com.google.gson.annotations.SerializedName -data class Filter ( +data class Filter( val id: String, val phrase: String, val context: List, @@ -45,4 +45,3 @@ data class Filter ( return filter?.id.equals(id) } } - diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/HashTag.kt b/app/src/main/java/com/keylesspalace/tusky/entity/HashTag.kt index 1eaaf68f9..a334257a1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/HashTag.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/HashTag.kt @@ -1,3 +1,3 @@ package com.keylesspalace.tusky.entity -data class HashTag(val name: String) \ No newline at end of file +data class HashTag(val name: String) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/IdentityProof.kt b/app/src/main/java/com/keylesspalace/tusky/entity/IdentityProof.kt index 9473f0372..98af734bf 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/IdentityProof.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/IdentityProof.kt @@ -3,7 +3,7 @@ package com.keylesspalace.tusky.entity import com.google.gson.annotations.SerializedName data class IdentityProof( - val provider: String, - @SerializedName("provider_username") val username: String, - @SerializedName("profile_url") val profileUrl: String + val provider: String, + @SerializedName("provider_username") val username: String, + @SerializedName("profile_url") val profileUrl: String ) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt index a9f6f499c..d1e2aca90 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt @@ -17,20 +17,20 @@ package com.keylesspalace.tusky.entity import com.google.gson.annotations.SerializedName -data class Instance ( - val uri: String, - val title: String, - val description: String, - val email: String, - val version: String, - val urls: Map, - val stats: Map?, - val thumbnail: String?, - val languages: List, - @SerializedName("contact_account") val contactAccount: Account, - @SerializedName("max_toot_chars") val maxTootChars: Int?, - @SerializedName("max_bio_chars") val maxBioChars: Int?, - @SerializedName("poll_limits") val pollLimits: PollLimits? +data class Instance( + val uri: String, + val title: String, + val description: String, + val email: String, + val version: String, + val urls: Map, + val stats: Map?, + val thumbnail: String?, + val languages: List, + @SerializedName("contact_account") val contactAccount: Account, + @SerializedName("max_toot_chars") val maxTootChars: Int?, + @SerializedName("max_bio_chars") val maxBioChars: Int?, + @SerializedName("poll_limits") val pollLimits: PollLimits? ) { override fun hashCode(): Int { return uri.hashCode() @@ -45,7 +45,7 @@ data class Instance ( } } -data class PollLimits ( - @SerializedName("max_options") val maxOptions: Int?, - @SerializedName("max_option_chars") val maxOptionChars: Int? +data class PollLimits( + @SerializedName("max_options") val maxOptions: Int?, + @SerializedName("max_option_chars") val maxOptionChars: Int? ) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Marker.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Marker.kt index 16fd9e318..78572054d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Marker.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Marker.kt @@ -1,15 +1,15 @@ package com.keylesspalace.tusky.entity import com.google.gson.annotations.SerializedName -import java.util.* +import java.util.Date /** * API type for saving the scroll position of a timeline. */ data class Marker( - @SerializedName("last_read_id") - val lastReadId: String, - val version: Int, - @SerializedName("updated_at") - val updatedAt: Date -) \ No newline at end of file + @SerializedName("last_read_id") + val lastReadId: String, + val version: Int, + @SerializedName("updated_at") + val updatedAt: Date +) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/MastoList.kt b/app/src/main/java/com/keylesspalace/tusky/entity/MastoList.kt index 2f8eecf3c..bfec7cc52 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/MastoList.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/MastoList.kt @@ -21,6 +21,6 @@ package com.keylesspalace.tusky.entity */ data class MastoList( - val id: String, - val title: String -) \ No newline at end of file + val id: String, + val title: String +) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/NewStatus.kt b/app/src/main/java/com/keylesspalace/tusky/entity/NewStatus.kt index 16cbc6a7c..83ed56e95 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/NewStatus.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/NewStatus.kt @@ -20,19 +20,19 @@ import com.google.gson.annotations.SerializedName import kotlinx.parcelize.Parcelize data class NewStatus( - val status: String, - @SerializedName("spoiler_text") val warningText: String, - @SerializedName("in_reply_to_id") val inReplyToId: String?, - val visibility: String, - val sensitive: Boolean, - @SerializedName("media_ids") val mediaIds: List?, - @SerializedName("scheduled_at") val scheduledAt: String?, - val poll: NewPoll? + val status: String, + @SerializedName("spoiler_text") val warningText: String, + @SerializedName("in_reply_to_id") val inReplyToId: String?, + val visibility: String, + val sensitive: Boolean, + @SerializedName("media_ids") val mediaIds: List?, + @SerializedName("scheduled_at") val scheduledAt: String?, + val poll: NewPoll? ) @Parcelize data class NewPoll( - val options: List, - @SerializedName("expires_in") val expiresIn: Int, - val multiple: Boolean -): Parcelable \ No newline at end of file + val options: List, + @SerializedName("expires_in") val expiresIn: Int, + val multiple: Boolean +) : Parcelable diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt index cb4ce3cbd..6198867d9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt @@ -79,7 +79,6 @@ data class Notification( ): Type { return Type.byString(json.asString) } - } /** Helper for Java */ @@ -89,8 +88,9 @@ data class Notification( fun rewriteToStatusTypeIfNeeded(accountId: String): Notification { if (type == Type.MENTION && status != null) { return if (status.mentions.any { - it.id == accountId - }) this else copy(type = Type.STATUS) + it.id == accountId + } + ) this else copy(type = Type.STATUS) } return this } diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Poll.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Poll.kt index 02c236c48..1a4c23548 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Poll.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Poll.kt @@ -1,22 +1,22 @@ package com.keylesspalace.tusky.entity import com.google.gson.annotations.SerializedName -import java.util.* +import java.util.Date data class Poll( - val id: String, - @SerializedName("expires_at") val expiresAt: Date?, - val expired: Boolean, - val multiple: Boolean, - @SerializedName("votes_count") val votesCount: Int, - @SerializedName("voters_count") val votersCount: Int?, // nullable for compatibility with Pleroma - val options: List, - val voted: Boolean + val id: String, + @SerializedName("expires_at") val expiresAt: Date?, + val expired: Boolean, + val multiple: Boolean, + @SerializedName("votes_count") val votesCount: Int, + @SerializedName("voters_count") val votersCount: Int?, // nullable for compatibility with Pleroma + val options: List, + val voted: Boolean ) { fun votedCopy(choices: List): Poll { val newOptions = options.mapIndexed { index, option -> - if(choices.contains(index)) { + if (choices.contains(index)) { option.copy(votesCount = option.votesCount + 1) } else { option @@ -24,24 +24,23 @@ data class Poll( } return copy( - options = newOptions, - votesCount = votesCount + choices.size, - votersCount = votersCount?.plus(1), - voted = true + options = newOptions, + votesCount = votesCount + choices.size, + votersCount = votersCount?.plus(1), + voted = true ) } fun toNewPoll(creationDate: Date) = NewPoll( - options.map { it.title }, - expiresAt?.let { - ((it.time - creationDate.time) / 1000).toInt() + 1 - }?: 3600, - multiple + options.map { it.title }, + expiresAt?.let { + ((it.time - creationDate.time) / 1000).toInt() + 1 + } ?: 3600, + multiple ) - } data class PollOption( - val title: String, - @SerializedName("votes_count") val votesCount: Int -) \ No newline at end of file + val title: String, + @SerializedName("votes_count") val votesCount: Int +) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Relationship.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Relationship.kt index e25a3d10d..17bddccaf 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Relationship.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Relationship.kt @@ -17,7 +17,7 @@ package com.keylesspalace.tusky.entity import com.google.gson.annotations.SerializedName -data class Relationship ( +data class Relationship( val id: String, val following: Boolean, @SerializedName("followed_by") val followedBy: Boolean, diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/ScheduledStatus.kt b/app/src/main/java/com/keylesspalace/tusky/entity/ScheduledStatus.kt index 2621bd5ed..dfaeb499c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/ScheduledStatus.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/ScheduledStatus.kt @@ -18,8 +18,8 @@ package com.keylesspalace.tusky.entity import com.google.gson.annotations.SerializedName data class ScheduledStatus( - val id: String, - @SerializedName("scheduled_at") val scheduledAt: String, - val params: StatusParams, - @SerializedName("media_attachments") val mediaAttachments: ArrayList + val id: String, + @SerializedName("scheduled_at") val scheduledAt: String, + val params: StatusParams, + @SerializedName("media_attachments") val mediaAttachments: ArrayList ) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/SearchResult.kt b/app/src/main/java/com/keylesspalace/tusky/entity/SearchResult.kt index 4307380ca..18e3d71b0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/SearchResult.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/SearchResult.kt @@ -15,7 +15,7 @@ package com.keylesspalace.tusky.entity -data class SearchResult ( +data class SearchResult( val accounts: List, val statuses: List, val hashtags: List diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt index 728cc1b40..1be5c2cb9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt @@ -19,33 +19,34 @@ import android.text.SpannableStringBuilder import android.text.Spanned import android.text.style.URLSpan import com.google.gson.annotations.SerializedName -import java.util.* +import java.util.ArrayList +import java.util.Date data class Status( - val id: String, - val url: String?, // not present if it's reblog - val account: Account, - @SerializedName("in_reply_to_id") var inReplyToId: String?, - @SerializedName("in_reply_to_account_id") val inReplyToAccountId: String?, - val reblog: Status?, - val content: Spanned, - @SerializedName("created_at") val createdAt: Date, - val emojis: List, - @SerializedName("reblogs_count") val reblogsCount: Int, - @SerializedName("favourites_count") val favouritesCount: Int, - var reblogged: Boolean, - var favourited: Boolean, - var bookmarked: Boolean, - var sensitive: Boolean, - @SerializedName("spoiler_text") val spoilerText: String, - val visibility: Visibility, - @SerializedName("media_attachments") var attachments: ArrayList, - val mentions: List, - val application: Application?, - val pinned: Boolean?, - val muted: Boolean?, - val poll: Poll?, - val card: Card? + val id: String, + val url: String?, // not present if it's reblog + val account: Account, + @SerializedName("in_reply_to_id") var inReplyToId: String?, + @SerializedName("in_reply_to_account_id") val inReplyToAccountId: String?, + val reblog: Status?, + val content: Spanned, + @SerializedName("created_at") val createdAt: Date, + val emojis: List, + @SerializedName("reblogs_count") val reblogsCount: Int, + @SerializedName("favourites_count") val favouritesCount: Int, + var reblogged: Boolean, + var favourited: Boolean, + var bookmarked: Boolean, + var sensitive: Boolean, + @SerializedName("spoiler_text") val spoilerText: String, + val visibility: Visibility, + @SerializedName("media_attachments") var attachments: ArrayList, + val mentions: List, + val application: Application?, + val pinned: Boolean?, + val muted: Boolean?, + val poll: Poll?, + val card: Card? ) { val actionableId: String @@ -119,14 +120,14 @@ data class Status( fun toDeletedStatus(): DeletedStatus { return DeletedStatus( - text = getEditableText(), - inReplyToId = inReplyToId, - spoilerText = spoilerText, - visibility = visibility, - sensitive = sensitive, - attachments = attachments, - poll = poll, - createdAt = createdAt + text = getEditableText(), + inReplyToId = inReplyToId, + spoilerText = spoilerText, + visibility = visibility, + sensitive = sensitive, + attachments = attachments, + poll = poll, + createdAt = createdAt ) } @@ -158,15 +159,14 @@ data class Status( return id.hashCode() } - - data class Mention ( + data class Mention( val id: String, val url: String, @SerializedName("acct") val username: String, @SerializedName("username") val localUsername: String ) - data class Application ( + data class Application( val name: String, val website: String? ) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/StatusContext.kt b/app/src/main/java/com/keylesspalace/tusky/entity/StatusContext.kt index 1287619b9..ce5bb1440 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/StatusContext.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/StatusContext.kt @@ -15,7 +15,7 @@ package com.keylesspalace.tusky.entity -data class StatusContext ( +data class StatusContext( val ancestors: List, val descendants: List ) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/StatusParams.kt b/app/src/main/java/com/keylesspalace/tusky/entity/StatusParams.kt index 0e25e6c16..d3235337b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/StatusParams.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/StatusParams.kt @@ -18,9 +18,9 @@ package com.keylesspalace.tusky.entity import com.google.gson.annotations.SerializedName data class StatusParams( - val text: String, - val sensitive: Boolean, - val visibility: Status.Visibility, - @SerializedName("spoiler_text") val spoilerText: String, - @SerializedName("in_reply_to_id") val inReplyToId: String? -) \ No newline at end of file + val text: String, + val sensitive: Boolean, + val visibility: Status.Visibility, + @SerializedName("spoiler_text") val spoilerText: String, + @SerializedName("in_reply_to_id") val inReplyToId: String? +) diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt index cd37f8425..16e6523e4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt @@ -33,9 +33,14 @@ import com.keylesspalace.tusky.AccountActivity import com.keylesspalace.tusky.AccountListActivity.Type import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.R -import com.keylesspalace.tusky.adapter.* -import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.adapter.AccountAdapter +import com.keylesspalace.tusky.adapter.BlocksAdapter +import com.keylesspalace.tusky.adapter.FollowAdapter +import com.keylesspalace.tusky.adapter.FollowRequestsAdapter +import com.keylesspalace.tusky.adapter.FollowRequestsHeaderAdapter +import com.keylesspalace.tusky.adapter.MutesAdapter import com.keylesspalace.tusky.databinding.FragmentAccountListBinding +import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Relationship @@ -51,7 +56,7 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Single import retrofit2.Response import java.io.IOException -import java.util.* +import java.util.HashMap import javax.inject.Inject class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountActionListener, Injectable { @@ -133,12 +138,15 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct } else { api.muteAccount(id, notifications) } - .autoDispose(from(this)) - .subscribe({ + .autoDispose(from(this)) + .subscribe( + { onMuteSuccess(mute, id, position, notifications) - }, { + }, + { onMuteFailure(mute, id, notifications) - }) + } + ) } private fun onMuteSuccess(muted: Boolean, id: String, position: Int, notifications: Boolean) { @@ -151,11 +159,11 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct if (unmutedUser != null) { Snackbar.make(binding.recyclerView, R.string.confirmation_unmuted, Snackbar.LENGTH_LONG) - .setAction(R.string.action_undo) { - mutesAdapter.addItem(unmutedUser, position) - onMute(true, id, position, notifications) - } - .show() + .setAction(R.string.action_undo) { + mutesAdapter.addItem(unmutedUser, position) + onMute(true, id, position, notifications) + } + .show() } } @@ -178,12 +186,15 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct } else { api.blockAccount(id) } - .autoDispose(from(this)) - .subscribe({ + .autoDispose(from(this)) + .subscribe( + { onBlockSuccess(block, id, position) - }, { + }, + { onBlockFailure(block, id) - }) + } + ) } private fun onBlockSuccess(blocked: Boolean, id: String, position: Int) { @@ -195,11 +206,11 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct if (unblockedUser != null) { Snackbar.make(binding.recyclerView, R.string.confirmation_unblocked, Snackbar.LENGTH_LONG) - .setAction(R.string.action_undo) { - blocksAdapter.addItem(unblockedUser, position) - onBlock(true, id, position) - } - .show() + .setAction(R.string.action_undo) { + blocksAdapter.addItem(unblockedUser, position) + onBlock(true, id, position) + } + .show() } } @@ -212,26 +223,31 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct Log.e(TAG, "Failed to $verb account accountId $accountId") } - override fun onRespondToFollowRequest(accept: Boolean, accountId: String, - position: Int) { + override fun onRespondToFollowRequest( + accept: Boolean, + accountId: String, + position: Int + ) { if (accept) { api.authorizeFollowRequest(accountId) } else { api.rejectFollowRequest(accountId) }.observeOn(AndroidSchedulers.mainThread()) - .autoDispose(from(this, Lifecycle.Event.ON_DESTROY)) - .subscribe({ + .autoDispose(from(this, Lifecycle.Event.ON_DESTROY)) + .subscribe( + { onRespondToFollowRequestSuccess(position) - }, { throwable -> + }, + { throwable -> val verb = if (accept) { "accept" } else { "reject" } Log.e(TAG, "Failed to $verb account id $accountId.", throwable) - }) - + } + ) } private fun onRespondToFollowRequestSuccess(position: Int) { @@ -264,7 +280,7 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct } private fun requireId(type: Type, id: String?): String { - return requireNotNull(id) { "id must not be null for type "+type.name } + return requireNotNull(id) { "id must not be null for type " + type.name } } private fun fetchAccounts(fromId: String? = null) { @@ -278,9 +294,10 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct } getFetchCallByListType(fromId) - .observeOn(AndroidSchedulers.mainThread()) - .autoDispose(from(this, Lifecycle.Event.ON_DESTROY)) - .subscribe({ response -> + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(from(this, Lifecycle.Event.ON_DESTROY)) + .subscribe( + { response -> val accountList = response.body() if (response.isSuccessful && accountList != null) { @@ -289,10 +306,11 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct } else { onFetchAccountsFailure(Exception(response.message())) } - }, {throwable -> + }, + { throwable -> onFetchAccountsFailure(throwable) - }) - + } + ) } private fun onFetchAccountsSuccess(accounts: List, linkHeader: String?) { @@ -319,9 +337,9 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct if (adapter.itemCount == 0) { binding.messageView.show() binding.messageView.setup( - R.drawable.elephant_friend_empty, - R.string.message_empty, - null + R.drawable.elephant_friend_empty, + R.string.message_empty, + null ) } else { binding.messageView.hide() @@ -330,11 +348,11 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct private fun fetchRelationships(ids: List) { api.relationships(ids) - .observeOn(AndroidSchedulers.mainThread()) - .autoDispose(from(this)) - .subscribe(::onFetchRelationshipsSuccess) { - onFetchRelationshipsFailure(ids) - } + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(from(this)) + .subscribe(::onFetchRelationshipsSuccess) { + onFetchRelationshipsFailure(ids) + } } private fun onFetchRelationshipsSuccess(relationships: List) { diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt index 588e22b6a..053299a53 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt @@ -49,7 +49,7 @@ import io.reactivex.rxjava3.core.SingleObserver import io.reactivex.rxjava3.disposables.Disposable import retrofit2.Response import java.io.IOException -import java.util.* +import java.util.Random import javax.inject.Inject /** @@ -156,18 +156,17 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - isSwipeToRefreshEnabled = arguments?.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH,true) == true - accountId = arguments?.getString(ACCOUNT_ID_ARG)!! + isSwipeToRefreshEnabled = arguments?.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true) == true + accountId = arguments?.getString(ACCOUNT_ID_ARG)!! } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val columnCount = view.context.resources.getInteger(R.integer.profile_media_column_count) val layoutManager = GridLayoutManager(view.context, columnCount) - adapter.baseItemColor = ThemeUtils.getColor(view.context, android.R.attr.windowBackground) + adapter.baseItemColor = ThemeUtils.getColor(view.context, android.R.attr.windowBackground) binding.recyclerView.layoutManager = layoutManager binding.recyclerView.adapter = adapter @@ -188,12 +187,12 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr val lastItem = layoutManager.findLastCompletelyVisibleItemPosition() if (itemCount <= lastItem + 3 && fetchingStatus == FetchingStatus.NOT_FETCHING) { statuses.lastOrNull()?.let { (id) -> - Log.d(TAG, "Requesting statuses with max_id: ${id}, (bottom)") + Log.d(TAG, "Requesting statuses with max_id: $id, (bottom)") fetchingStatus = FetchingStatus.FETCHING_BOTTOM api.accountStatuses(accountId, id, null, null, null, true, null) - .observeOn(AndroidSchedulers.mainThread()) - .autoDispose(this@AccountMediaFragment, Lifecycle.Event.ON_DESTROY) - .subscribe(bottomCallback) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this@AccountMediaFragment, Lifecycle.Event.ON_DESTROY) + .subscribe(bottomCallback) } } } @@ -213,8 +212,8 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr fetchingStatus = FetchingStatus.REFRESHING api.accountStatuses(accountId, null, statuses[0].id, null, null, true, null) }.observeOn(AndroidSchedulers.mainThread()) - .autoDispose(this, Lifecycle.Event.ON_DESTROY) - .subscribe(callback) + .autoDispose(this, Lifecycle.Event.ON_DESTROY) + .subscribe(callback) if (!isSwipeToRefreshEnabled) binding.topProgressBar.show() @@ -227,11 +226,10 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr if (fetchingStatus == FetchingStatus.NOT_FETCHING && statuses.isEmpty()) { fetchingStatus = FetchingStatus.INITIAL_FETCHING api.accountStatuses(accountId, null, null, null, null, true, null) - .observeOn(AndroidSchedulers.mainThread()) - .autoDispose(this@AccountMediaFragment, Lifecycle.Event.ON_DESTROY) - .subscribe(callback) - } - else if (needToRefresh) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this@AccountMediaFragment, Lifecycle.Event.ON_DESTROY) + .subscribe(callback) + } else if (needToRefresh) refresh() needToRefresh = false } @@ -264,7 +262,7 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr } inner class MediaGridAdapter : - RecyclerView.Adapter() { + RecyclerView.Adapter() { var baseItemColor = Color.BLACK @@ -305,15 +303,14 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr val item = items[position] Glide.with(holder.imageView) - .load(item.attachment.previewUrl) - .centerInside() - .into(holder.imageView) + .load(item.attachment.previewUrl) + .centerInside() + .into(holder.imageView) } - - inner class MediaViewHolder(val imageView: ImageView) - : RecyclerView.ViewHolder(imageView), - View.OnClickListener { + inner class MediaViewHolder(val imageView: ImageView) : + RecyclerView.ViewHolder(imageView), + View.OnClickListener { init { itemView.setOnClickListener(this) } @@ -334,11 +331,11 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr companion object { @JvmStatic - fun newInstance(accountId: String, enableSwipeToRefresh:Boolean=true): AccountMediaFragment { + fun newInstance(accountId: String, enableSwipeToRefresh: Boolean = true): AccountMediaFragment { val fragment = AccountMediaFragment() val args = Bundle() args.putString(ACCOUNT_ID_ARG, accountId) - args.putBoolean(ARG_ENABLE_SWIPE_TO_REFRESH,enableSwipeToRefresh) + args.putBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, enableSwipeToRefresh) fragment.arguments = args return fragment } @@ -347,4 +344,4 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr private const val TAG = "AccountMediaFragment" private const val ARG_ENABLE_SWIPE_TO_REFRESH = "arg.enable.swipe.to.refresh" } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt index ceb2f365d..0362da9c7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt @@ -66,12 +66,11 @@ class ViewImageFragment : ViewMediaFragment() { photoActionsListener = context as PhotoActionsListener } - override fun setupMediaView( - url: String, - previewUrl: String?, - description: String?, - showingDescription: Boolean + url: String, + previewUrl: String?, + description: String?, + showingDescription: Boolean ) { binding.photoView.transitionName = url binding.mediaDescription.text = description @@ -136,9 +135,9 @@ class ViewImageFragment : ViewMediaFragment() { if (event.action == MotionEvent.ACTION_DOWN) { lastY = event.rawY - } else if (event.pointerCount == 1 - && attacher.scale == 1f - && event.action == MotionEvent.ACTION_MOVE + } else if (event.pointerCount == 1 && + attacher.scale == 1f && + event.action == MotionEvent.ACTION_MOVE ) { val diff = event.rawY - lastY // This code is to prevent transformations during page scrolling @@ -176,21 +175,21 @@ class ViewImageFragment : ViewMediaFragment() { } override fun onToolbarVisibilityChange(visible: Boolean) { - if (_binding == null || !userVisibleHint ) { + if (_binding == null || !userVisibleHint) { return } isDescriptionVisible = showingDescription && visible val alpha = if (isDescriptionVisible) 1.0f else 0.0f binding.captionSheet.animate().alpha(alpha) - .setListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - if (_binding != null) { - binding.captionSheet.visible(isDescriptionVisible) - } - animation.removeListener(this) + .setListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + if (_binding != null) { + binding.captionSheet.visible(isDescriptionVisible) } - }) - .start() + animation.removeListener(this) + } + }) + .start() } override fun onDestroyView() { @@ -204,27 +203,30 @@ class ViewImageFragment : ViewMediaFragment() { val glide = Glide.with(this) // Request image from the any cache glide - .load(url) - .dontAnimate() - .onlyRetrieveFromCache(true) - .let { - if (previewUrl != null) - it.thumbnail(glide - .load(previewUrl) - .dontAnimate() - .onlyRetrieveFromCache(true) - .centerInside() - .addListener(ImageRequestListener(true, isThumnailRequest = true))) - else it - } - //Request image from the network on fail load image from cache - .error(glide.load(url) - .centerInside() - .addListener(ImageRequestListener(false, isThumnailRequest = false)) - ) - .centerInside() - .addListener(ImageRequestListener(true, isThumnailRequest = false)) - .into(photoView) + .load(url) + .dontAnimate() + .onlyRetrieveFromCache(true) + .let { + if (previewUrl != null) + it.thumbnail( + glide + .load(previewUrl) + .dontAnimate() + .onlyRetrieveFromCache(true) + .centerInside() + .addListener(ImageRequestListener(true, isThumnailRequest = true)) + ) + else it + } + // Request image from the network on fail load image from cache + .error( + glide.load(url) + .centerInside() + .addListener(ImageRequestListener(false, isThumnailRequest = false)) + ) + .centerInside() + .addListener(ImageRequestListener(true, isThumnailRequest = false)) + .into(photoView) } /** @@ -248,14 +250,20 @@ class ViewImageFragment : ViewMediaFragment() { * @param isCacheRequest - is this listener for request image from cache or from the network */ private inner class ImageRequestListener( - private val isCacheRequest: Boolean, - private val isThumnailRequest: Boolean) : RequestListener { + private val isCacheRequest: Boolean, + private val isThumnailRequest: Boolean + ) : RequestListener { - override fun onLoadFailed(e: GlideException?, model: Any, target: Target, - isFirstResource: Boolean): Boolean { + override fun onLoadFailed( + e: GlideException?, + model: Any, + target: Target, + isFirstResource: Boolean + ): Boolean { // If cache for full image failed complete transition - if (isCacheRequest && !isThumnailRequest && shouldStartTransition - && !startedTransition) { + if (isCacheRequest && !isThumnailRequest && shouldStartTransition && + !startedTransition + ) { photoActionsListener.onBringUp() } // Hide progress bar only on fail request from internet @@ -265,8 +273,13 @@ class ViewImageFragment : ViewMediaFragment() { } @SuppressLint("CheckResult") - override fun onResourceReady(resource: Drawable, model: Any, target: Target, - dataSource: DataSource, isFirstResource: Boolean): Boolean { + override fun onResourceReady( + resource: Drawable, + model: Any, + target: Target, + dataSource: DataSource, + isFirstResource: Boolean + ): Boolean { if (_binding != null) { binding.progressBar.hide() // Always hide the progress bar on success } @@ -284,14 +297,14 @@ class ViewImageFragment : ViewMediaFragment() { // This wait for transition. If there's no transition then we should hit // another branch. take() will unsubscribe after we have it to not leak menmory transition - .take(1) - .subscribe { - target.onResourceReady(resource, null) - // It's needed. Don't ask why, I don't know, setImageDrawable() should - // do it by itself but somehow it doesn't work automatically. - // Just do it. If you don't, image will jump around when touched. - attacher.update() - } + .take(1) + .subscribe { + target.onResourceReady(resource, null) + // It's needed. Don't ask why, I don't know, setImageDrawable() should + // do it by itself but somehow it doesn't work automatically. + // Just do it. If you don't, image will jump around when touched. + attacher.update() + } } return true } diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewMediaFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewMediaFragment.kt index b25fec26f..89c65e10a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewMediaFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewMediaFragment.kt @@ -25,10 +25,10 @@ abstract class ViewMediaFragment : Fragment() { private var toolbarVisibiltyDisposable: Function0? = null abstract fun setupMediaView( - url: String, - previewUrl: String?, - description: String?, - showingDescription: Boolean + url: String, + previewUrl: String?, + description: String?, + showingDescription: Boolean ) abstract fun onToolbarVisibilityChange(visible: Boolean) @@ -56,7 +56,7 @@ abstract class ViewMediaFragment : Fragment() { Attachment.Type.VIDEO, Attachment.Type.GIFV, Attachment.Type.AUDIO -> ViewVideoFragment() - else -> ViewImageFragment() // it probably won't show anything, but its better than crashing + else -> ViewImageFragment() // it probably won't show anything, but its better than crashing } fragment.arguments = arguments return fragment @@ -84,9 +84,9 @@ abstract class ViewMediaFragment : Fragment() { setupMediaView(url, previewUrl, description, showingDescription && mediaActivity.isToolbarVisible) toolbarVisibiltyDisposable = (activity as ViewMediaActivity) - .addToolbarVisibilityListener { isVisible -> - onToolbarVisibilityChange(isVisible) - } + .addToolbarVisibilityListener { isVisible -> + onToolbarVisibilityChange(isVisible) + } } override fun onDestroyView() { diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt index a0912837d..35b98ef71 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt @@ -48,7 +48,7 @@ class ViewVideoFragment : ViewMediaFragment() { } private lateinit var mediaActivity: ViewMediaActivity private val TOOLBAR_HIDE_DELAY_MS = 3000L - private lateinit var mediaController : MediaController + private lateinit var mediaController: MediaController private var isAudio = false override fun setUserVisibleHint(isVisibleToUser: Boolean) { @@ -72,10 +72,10 @@ class ViewVideoFragment : ViewMediaFragment() { @SuppressLint("ClickableViewAccessibility") override fun setupMediaView( - url: String, - previewUrl: String?, - description: String?, - showingDescription: Boolean + url: String, + previewUrl: String?, + description: String?, + showingDescription: Boolean ) { binding.mediaDescription.text = description binding.mediaDescription.visible(showingDescription) @@ -105,7 +105,7 @@ class ViewVideoFragment : ViewMediaFragment() { mediaController.setMediaPlayer(binding.videoView) binding.videoView.setMediaController(mediaController) binding.videoView.requestFocus() - binding.videoView.setPlayPauseListener(object: ExposedPlayPauseVideoView.PlayPauseListener { + binding.videoView.setPlayPauseListener(object : ExposedPlayPauseVideoView.PlayPauseListener { override fun onPause() { handler.removeCallbacks(hideToolbar) } @@ -125,7 +125,7 @@ class ViewVideoFragment : ViewMediaFragment() { val videoWidth = mp.videoWidth.toFloat() val videoHeight = mp.videoHeight.toFloat() - if(containerWidth/containerHeight > videoWidth/videoHeight) { + if (containerWidth / containerHeight > videoWidth / videoHeight) { binding.videoView.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT binding.videoView.layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT } else { @@ -190,15 +190,15 @@ class ViewVideoFragment : ViewMediaFragment() { } binding.mediaDescription.animate().alpha(alpha) - .setListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - if (_binding != null) { - binding.mediaDescription.visible(isDescriptionVisible) - } - animation.removeListener(this) + .setListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + if (_binding != null) { + binding.mediaDescription.visible(isDescriptionVisible) } - }) - .start() + animation.removeListener(this) + } + }) + .start() if (visible && binding.videoView.isPlaying && !isAudio) { hideToolbarAfterDelay(TOOLBAR_HIDE_DELAY_MS) diff --git a/app/src/main/java/com/keylesspalace/tusky/interfaces/AccountSelectionListener.kt b/app/src/main/java/com/keylesspalace/tusky/interfaces/AccountSelectionListener.kt index 04b1ebd2a..b86c55c76 100644 --- a/app/src/main/java/com/keylesspalace/tusky/interfaces/AccountSelectionListener.kt +++ b/app/src/main/java/com/keylesspalace/tusky/interfaces/AccountSelectionListener.kt @@ -19,4 +19,4 @@ import com.keylesspalace.tusky.db.AccountEntity interface AccountSelectionListener { fun onAccountSelected(account: AccountEntity) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/interfaces/RefreshableFragment.kt b/app/src/main/java/com/keylesspalace/tusky/interfaces/RefreshableFragment.kt index 5032774f4..83fc20c93 100644 --- a/app/src/main/java/com/keylesspalace/tusky/interfaces/RefreshableFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/interfaces/RefreshableFragment.kt @@ -8,4 +8,4 @@ interface RefreshableFragment { * Call this method to refresh fragment content */ fun refreshContent() -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/interfaces/ReselectableFragment.kt b/app/src/main/java/com/keylesspalace/tusky/interfaces/ReselectableFragment.kt index c50178c10..598894f6b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/interfaces/ReselectableFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/interfaces/ReselectableFragment.kt @@ -8,4 +8,4 @@ interface ReselectableFragment { * Call this method when tab reselected */ fun onReselect() -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/json/SpannedTypeAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/json/SpannedTypeAdapter.kt index 6eabea524..ceb96f4a2 100644 --- a/app/src/main/java/com/keylesspalace/tusky/json/SpannedTypeAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/json/SpannedTypeAdapter.kt @@ -19,7 +19,13 @@ import android.text.Spanned import android.text.SpannedString import androidx.core.text.HtmlCompat import androidx.core.text.parseAsHtml -import com.google.gson.* +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.google.gson.JsonParseException +import com.google.gson.JsonPrimitive +import com.google.gson.JsonSerializationContext +import com.google.gson.JsonSerializer import com.keylesspalace.tusky.util.trimTrailingWhitespace import java.lang.reflect.Type @@ -34,4 +40,4 @@ class SpannedTypeAdapter : JsonDeserializer, JsonSerializer { override fun serialize(src: Spanned?, typeOfSrc: Type, context: JsonSerializationContext): JsonElement { return JsonPrimitive(HtmlCompat.toHtml(src!!, HtmlCompat.TO_HTML_PARAGRAPH_LINES_INDIVIDUAL)) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/network/FilterModel.kt b/app/src/main/java/com/keylesspalace/tusky/network/FilterModel.kt index 3be20c1a9..d4c7464f1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/FilterModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/FilterModel.kt @@ -29,8 +29,10 @@ class FilterModel @Inject constructor() { } val spoilerText = status.actionableStatus.spoilerText - return (matcher.reset(status.actionableStatus.content).find() || - spoilerText.isNotEmpty() && matcher.reset(spoilerText).find()) + return ( + matcher.reset(status.actionableStatus.content).find() || + spoilerText.isNotEmpty() && matcher.reset(spoilerText).find() + ) } private fun filterToRegexToken(filter: Filter): String? { @@ -47,10 +49,10 @@ class FilterModel @Inject constructor() { if (filters.isEmpty()) return null val tokens = filters.map { filterToRegexToken(it) } - return Pattern.compile(TextUtils.join("|", tokens), Pattern.CASE_INSENSITIVE); + return Pattern.compile(TextUtils.join("|", tokens), Pattern.CASE_INSENSITIVE) } companion object { private val ALPHANUMERIC = Pattern.compile("^\\w+$") } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt index 96f05349d..dc5fd8f38 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -85,57 +85,57 @@ interface MastodonApi { @GET("api/v1/timelines/home") fun homeTimeline( - @Query("max_id") maxId: String?, - @Query("since_id") sinceId: String?, - @Query("limit") limit: Int? + @Query("max_id") maxId: String?, + @Query("since_id") sinceId: String?, + @Query("limit") limit: Int? ): Single>> @GET("api/v1/timelines/public") fun publicTimeline( - @Query("local") local: Boolean?, - @Query("max_id") maxId: String?, - @Query("since_id") sinceId: String?, - @Query("limit") limit: Int? + @Query("local") local: Boolean?, + @Query("max_id") maxId: String?, + @Query("since_id") sinceId: String?, + @Query("limit") limit: Int? ): Single>> @GET("api/v1/timelines/tag/{hashtag}") fun hashtagTimeline( - @Path("hashtag") hashtag: String, - @Query("any[]") any: List?, - @Query("local") local: Boolean?, - @Query("max_id") maxId: String?, - @Query("since_id") sinceId: String?, - @Query("limit") limit: Int? + @Path("hashtag") hashtag: String, + @Query("any[]") any: List?, + @Query("local") local: Boolean?, + @Query("max_id") maxId: String?, + @Query("since_id") sinceId: String?, + @Query("limit") limit: Int? ): Single>> @GET("api/v1/timelines/list/{listId}") fun listTimeline( - @Path("listId") listId: String, - @Query("max_id") maxId: String?, - @Query("since_id") sinceId: String?, - @Query("limit") limit: Int? + @Path("listId") listId: String, + @Query("max_id") maxId: String?, + @Query("since_id") sinceId: String?, + @Query("limit") limit: Int? ): Single>> @GET("api/v1/notifications") fun notifications( - @Query("max_id") maxId: String?, - @Query("since_id") sinceId: String?, - @Query("limit") limit: Int?, - @Query("exclude_types[]") excludes: Set? + @Query("max_id") maxId: String?, + @Query("since_id") sinceId: String?, + @Query("limit") limit: Int?, + @Query("exclude_types[]") excludes: Set? ): Single>> @GET("api/v1/markers") fun markersWithAuth( - @Header("Authorization") auth: String, - @Header(DOMAIN_HEADER) domain: String, - @Query("timeline[]") timelines: List + @Header("Authorization") auth: String, + @Header(DOMAIN_HEADER) domain: String, + @Query("timeline[]") timelines: List ): Single> @GET("api/v1/notifications") fun notificationsWithAuth( - @Header("Authorization") auth: String, - @Header(DOMAIN_HEADER) domain: String, - @Query("since_id") sinceId: String? + @Header("Authorization") auth: String, + @Header(DOMAIN_HEADER) domain: String, + @Query("since_id") sinceId: String? ): Single> @POST("api/v1/notifications/clear") @@ -144,111 +144,111 @@ interface MastodonApi { @Multipart @POST("api/v1/media") fun uploadMedia( - @Part file: MultipartBody.Part, - @Part description: MultipartBody.Part? = null + @Part file: MultipartBody.Part, + @Part description: MultipartBody.Part? = null ): Single @FormUrlEncoded @PUT("api/v1/media/{mediaId}") fun updateMedia( - @Path("mediaId") mediaId: String, - @Field("description") description: String + @Path("mediaId") mediaId: String, + @Field("description") description: String ): Single @POST("api/v1/statuses") fun createStatus( - @Header("Authorization") auth: String, - @Header(DOMAIN_HEADER) domain: String, - @Header("Idempotency-Key") idempotencyKey: String, - @Body status: NewStatus + @Header("Authorization") auth: String, + @Header(DOMAIN_HEADER) domain: String, + @Header("Idempotency-Key") idempotencyKey: String, + @Body status: NewStatus ): Call @GET("api/v1/statuses/{id}") fun status( - @Path("id") statusId: String + @Path("id") statusId: String ): Single @GET("api/v1/statuses/{id}/context") fun statusContext( - @Path("id") statusId: String + @Path("id") statusId: String ): Single @GET("api/v1/statuses/{id}/reblogged_by") fun statusRebloggedBy( - @Path("id") statusId: String, - @Query("max_id") maxId: String? + @Path("id") statusId: String, + @Query("max_id") maxId: String? ): Single>> @GET("api/v1/statuses/{id}/favourited_by") fun statusFavouritedBy( - @Path("id") statusId: String, - @Query("max_id") maxId: String? + @Path("id") statusId: String, + @Query("max_id") maxId: String? ): Single>> @DELETE("api/v1/statuses/{id}") fun deleteStatus( - @Path("id") statusId: String + @Path("id") statusId: String ): Single @POST("api/v1/statuses/{id}/reblog") fun reblogStatus( - @Path("id") statusId: String + @Path("id") statusId: String ): Single @POST("api/v1/statuses/{id}/unreblog") fun unreblogStatus( - @Path("id") statusId: String + @Path("id") statusId: String ): Single @POST("api/v1/statuses/{id}/favourite") fun favouriteStatus( - @Path("id") statusId: String + @Path("id") statusId: String ): Single @POST("api/v1/statuses/{id}/unfavourite") fun unfavouriteStatus( - @Path("id") statusId: String + @Path("id") statusId: String ): Single @POST("api/v1/statuses/{id}/bookmark") fun bookmarkStatus( - @Path("id") statusId: String + @Path("id") statusId: String ): Single @POST("api/v1/statuses/{id}/unbookmark") fun unbookmarkStatus( - @Path("id") statusId: String + @Path("id") statusId: String ): Single @POST("api/v1/statuses/{id}/pin") fun pinStatus( - @Path("id") statusId: String + @Path("id") statusId: String ): Single @POST("api/v1/statuses/{id}/unpin") fun unpinStatus( - @Path("id") statusId: String + @Path("id") statusId: String ): Single @POST("api/v1/statuses/{id}/mute") fun muteConversation( - @Path("id") statusId: String + @Path("id") statusId: String ): Single @POST("api/v1/statuses/{id}/unmute") fun unmuteConversation( - @Path("id") statusId: String + @Path("id") statusId: String ): Single @GET("api/v1/scheduled_statuses") fun scheduledStatuses( - @Query("limit") limit: Int? = null, - @Query("max_id") maxId: String? = null + @Query("limit") limit: Int? = null, + @Query("max_id") maxId: String? = null ): Single> @DELETE("api/v1/scheduled_statuses/{id}") fun deleteScheduledStatus( - @Path("id") scheduledStatusId: String + @Path("id") scheduledStatusId: String ): Single @GET("api/v1/accounts/verify_credentials") @@ -257,39 +257,39 @@ interface MastodonApi { @FormUrlEncoded @PATCH("api/v1/accounts/update_credentials") fun accountUpdateSource( - @Field("source[privacy]") privacy: String?, - @Field("source[sensitive]") sensitive: Boolean? + @Field("source[privacy]") privacy: String?, + @Field("source[sensitive]") sensitive: Boolean? ): Call @Multipart @PATCH("api/v1/accounts/update_credentials") fun accountUpdateCredentials( - @Part(value = "display_name") displayName: RequestBody?, - @Part(value = "note") note: RequestBody?, - @Part(value = "locked") locked: RequestBody?, - @Part avatar: MultipartBody.Part?, - @Part header: MultipartBody.Part?, - @Part(value = "fields_attributes[0][name]") fieldName0: RequestBody?, - @Part(value = "fields_attributes[0][value]") fieldValue0: RequestBody?, - @Part(value = "fields_attributes[1][name]") fieldName1: RequestBody?, - @Part(value = "fields_attributes[1][value]") fieldValue1: RequestBody?, - @Part(value = "fields_attributes[2][name]") fieldName2: RequestBody?, - @Part(value = "fields_attributes[2][value]") fieldValue2: RequestBody?, - @Part(value = "fields_attributes[3][name]") fieldName3: RequestBody?, - @Part(value = "fields_attributes[3][value]") fieldValue3: RequestBody? + @Part(value = "display_name") displayName: RequestBody?, + @Part(value = "note") note: RequestBody?, + @Part(value = "locked") locked: RequestBody?, + @Part avatar: MultipartBody.Part?, + @Part header: MultipartBody.Part?, + @Part(value = "fields_attributes[0][name]") fieldName0: RequestBody?, + @Part(value = "fields_attributes[0][value]") fieldValue0: RequestBody?, + @Part(value = "fields_attributes[1][name]") fieldName1: RequestBody?, + @Part(value = "fields_attributes[1][value]") fieldValue1: RequestBody?, + @Part(value = "fields_attributes[2][name]") fieldName2: RequestBody?, + @Part(value = "fields_attributes[2][value]") fieldValue2: RequestBody?, + @Part(value = "fields_attributes[3][name]") fieldName3: RequestBody?, + @Part(value = "fields_attributes[3][value]") fieldValue3: RequestBody? ): Call @GET("api/v1/accounts/search") fun searchAccounts( - @Query("q") query: String, - @Query("resolve") resolve: Boolean? = null, - @Query("limit") limit: Int? = null, - @Query("following") following: Boolean? = null + @Query("q") query: String, + @Query("resolve") resolve: Boolean? = null, + @Query("limit") limit: Int? = null, + @Query("following") following: Boolean? = null ): Single> @GET("api/v1/accounts/{id}") fun account( - @Path("id") accountId: String + @Path("id") accountId: String ): Single /** @@ -303,71 +303,71 @@ interface MastodonApi { */ @GET("api/v1/accounts/{id}/statuses") fun accountStatuses( - @Path("id") accountId: String, - @Query("max_id") maxId: String?, - @Query("since_id") sinceId: String?, - @Query("limit") limit: Int?, - @Query("exclude_replies") excludeReplies: Boolean?, - @Query("only_media") onlyMedia: Boolean?, - @Query("pinned") pinned: Boolean? + @Path("id") accountId: String, + @Query("max_id") maxId: String?, + @Query("since_id") sinceId: String?, + @Query("limit") limit: Int?, + @Query("exclude_replies") excludeReplies: Boolean?, + @Query("only_media") onlyMedia: Boolean?, + @Query("pinned") pinned: Boolean? ): Single>> @GET("api/v1/accounts/{id}/followers") fun accountFollowers( - @Path("id") accountId: String, - @Query("max_id") maxId: String? + @Path("id") accountId: String, + @Query("max_id") maxId: String? ): Single>> @GET("api/v1/accounts/{id}/following") fun accountFollowing( - @Path("id") accountId: String, - @Query("max_id") maxId: String? + @Path("id") accountId: String, + @Query("max_id") maxId: String? ): Single>> @FormUrlEncoded @POST("api/v1/accounts/{id}/follow") fun followAccount( - @Path("id") accountId: String, - @Field("reblogs") showReblogs: Boolean? = null, - @Field("notify") notify: Boolean? = null + @Path("id") accountId: String, + @Field("reblogs") showReblogs: Boolean? = null, + @Field("notify") notify: Boolean? = null ): Single @POST("api/v1/accounts/{id}/unfollow") fun unfollowAccount( - @Path("id") accountId: String + @Path("id") accountId: String ): Single @POST("api/v1/accounts/{id}/block") fun blockAccount( - @Path("id") accountId: String + @Path("id") accountId: String ): Single @POST("api/v1/accounts/{id}/unblock") fun unblockAccount( - @Path("id") accountId: String + @Path("id") accountId: String ): Single @FormUrlEncoded @POST("api/v1/accounts/{id}/mute") fun muteAccount( - @Path("id") accountId: String, - @Field("notifications") notifications: Boolean? = null, - @Field("duration") duration: Int? = null + @Path("id") accountId: String, + @Field("notifications") notifications: Boolean? = null, + @Field("duration") duration: Int? = null ): Single @POST("api/v1/accounts/{id}/unmute") fun unmuteAccount( - @Path("id") accountId: String + @Path("id") accountId: String ): Single @GET("api/v1/accounts/relationships") fun relationships( - @Query("id[]") accountIds: List + @Query("id[]") accountIds: List ): Single> @GET("api/v1/accounts/{id}/identity_proofs") fun identityProofs( - @Path("id") accountId: String + @Path("id") accountId: String ): Single> @POST("api/v1/pleroma/accounts/{id}/subscribe") @@ -382,25 +382,25 @@ interface MastodonApi { @GET("api/v1/blocks") fun blocks( - @Query("max_id") maxId: String? + @Query("max_id") maxId: String? ): Single>> @GET("api/v1/mutes") fun mutes( - @Query("max_id") maxId: String? + @Query("max_id") maxId: String? ): Single>> @GET("api/v1/domain_blocks") fun domainBlocks( - @Query("max_id") maxId: String? = null, - @Query("since_id") sinceId: String? = null, - @Query("limit") limit: Int? = null + @Query("max_id") maxId: String? = null, + @Query("since_id") sinceId: String? = null, + @Query("limit") limit: Int? = null ): Single>> @FormUrlEncoded @POST("api/v1/domain_blocks") fun blockDomain( - @Field("domain") domain: String + @Field("domain") domain: String ): Call @FormUrlEncoded @@ -410,97 +410,97 @@ interface MastodonApi { @GET("api/v1/favourites") fun favourites( - @Query("max_id") maxId: String?, - @Query("since_id") sinceId: String?, - @Query("limit") limit: Int? + @Query("max_id") maxId: String?, + @Query("since_id") sinceId: String?, + @Query("limit") limit: Int? ): Single>> @GET("api/v1/bookmarks") fun bookmarks( - @Query("max_id") maxId: String?, - @Query("since_id") sinceId: String?, - @Query("limit") limit: Int? + @Query("max_id") maxId: String?, + @Query("since_id") sinceId: String?, + @Query("limit") limit: Int? ): Single>> @GET("api/v1/follow_requests") fun followRequests( - @Query("max_id") maxId: String? + @Query("max_id") maxId: String? ): Single>> @POST("api/v1/follow_requests/{id}/authorize") fun authorizeFollowRequest( - @Path("id") accountId: String + @Path("id") accountId: String ): Single @POST("api/v1/follow_requests/{id}/reject") fun rejectFollowRequest( - @Path("id") accountId: String + @Path("id") accountId: String ): Single @FormUrlEncoded @POST("api/v1/apps") fun authenticateApp( - @Header(DOMAIN_HEADER) domain: String, - @Field("client_name") clientName: String, - @Field("redirect_uris") redirectUris: String, - @Field("scopes") scopes: String, - @Field("website") website: String + @Header(DOMAIN_HEADER) domain: String, + @Field("client_name") clientName: String, + @Field("redirect_uris") redirectUris: String, + @Field("scopes") scopes: String, + @Field("website") website: String ): Call @FormUrlEncoded @POST("oauth/token") fun fetchOAuthToken( - @Header(DOMAIN_HEADER) domain: String, - @Field("client_id") clientId: String, - @Field("client_secret") clientSecret: String, - @Field("redirect_uri") redirectUri: String, - @Field("code") code: String, - @Field("grant_type") grantType: String + @Header(DOMAIN_HEADER) domain: String, + @Field("client_id") clientId: String, + @Field("client_secret") clientSecret: String, + @Field("redirect_uri") redirectUri: String, + @Field("code") code: String, + @Field("grant_type") grantType: String ): Call @FormUrlEncoded @POST("api/v1/lists") fun createList( - @Field("title") title: String + @Field("title") title: String ): Single @FormUrlEncoded @PUT("api/v1/lists/{listId}") fun updateList( - @Path("listId") listId: String, - @Field("title") title: String + @Path("listId") listId: String, + @Field("title") title: String ): Single @DELETE("api/v1/lists/{listId}") fun deleteList( - @Path("listId") listId: String + @Path("listId") listId: String ): Completable @GET("api/v1/lists/{listId}/accounts") fun getAccountsInList( - @Path("listId") listId: String, - @Query("limit") limit: Int + @Path("listId") listId: String, + @Query("limit") limit: Int ): Single> @FormUrlEncoded // @DELETE doesn't support fields @HTTP(method = "DELETE", path = "api/v1/lists/{listId}/accounts", hasBody = true) fun deleteAccountFromList( - @Path("listId") listId: String, - @Field("account_ids[]") accountIds: List + @Path("listId") listId: String, + @Field("account_ids[]") accountIds: List ): Completable @FormUrlEncoded @POST("api/v1/lists/{listId}/accounts") fun addCountToList( - @Path("listId") listId: String, - @Field("account_ids[]") accountIds: List + @Path("listId") listId: String, + @Field("account_ids[]") accountIds: List ): Completable @GET("/api/v1/conversations") suspend fun getConversations( - @Query("max_id") maxId: String? = null, - @Query("limit") limit: Int + @Query("max_id") maxId: String? = null, + @Query("limit") limit: Int ): List @DELETE("/api/v1/conversations/{id}") @@ -511,97 +511,96 @@ interface MastodonApi { @FormUrlEncoded @POST("api/v1/filters") fun createFilter( - @Field("phrase") phrase: String, - @Field("context[]") context: List, - @Field("irreversible") irreversible: Boolean?, - @Field("whole_word") wholeWord: Boolean?, - @Field("expires_in") expiresIn: String? + @Field("phrase") phrase: String, + @Field("context[]") context: List, + @Field("irreversible") irreversible: Boolean?, + @Field("whole_word") wholeWord: Boolean?, + @Field("expires_in") expiresIn: String? ): Call @FormUrlEncoded @PUT("api/v1/filters/{id}") fun updateFilter( - @Path("id") id: String, - @Field("phrase") phrase: String, - @Field("context[]") context: List, - @Field("irreversible") irreversible: Boolean?, - @Field("whole_word") wholeWord: Boolean?, - @Field("expires_in") expiresIn: String? + @Path("id") id: String, + @Field("phrase") phrase: String, + @Field("context[]") context: List, + @Field("irreversible") irreversible: Boolean?, + @Field("whole_word") wholeWord: Boolean?, + @Field("expires_in") expiresIn: String? ): Call @DELETE("api/v1/filters/{id}") fun deleteFilter( - @Path("id") id: String + @Path("id") id: String ): Call @FormUrlEncoded @POST("api/v1/polls/{id}/votes") fun voteInPoll( - @Path("id") id: String, - @Field("choices[]") choices: List + @Path("id") id: String, + @Field("choices[]") choices: List ): Single @GET("api/v1/announcements") fun listAnnouncements( - @Query("with_dismissed") withDismissed: Boolean = true + @Query("with_dismissed") withDismissed: Boolean = true ): Single> @POST("api/v1/announcements/{id}/dismiss") fun dismissAnnouncement( - @Path("id") announcementId: String + @Path("id") announcementId: String ): Single @PUT("api/v1/announcements/{id}/reactions/{name}") fun addAnnouncementReaction( - @Path("id") announcementId: String, - @Path("name") name: String + @Path("id") announcementId: String, + @Path("name") name: String ): Single @DELETE("api/v1/announcements/{id}/reactions/{name}") fun removeAnnouncementReaction( - @Path("id") announcementId: String, - @Path("name") name: String + @Path("id") announcementId: String, + @Path("name") name: String ): Single @FormUrlEncoded @POST("api/v1/reports") fun reportObservable( - @Field("account_id") accountId: String, - @Field("status_ids[]") statusIds: List, - @Field("comment") comment: String, - @Field("forward") isNotifyRemote: Boolean? + @Field("account_id") accountId: String, + @Field("status_ids[]") statusIds: List, + @Field("comment") comment: String, + @Field("forward") isNotifyRemote: Boolean? ): Single @GET("api/v1/accounts/{id}/statuses") fun accountStatusesObservable( - @Path("id") accountId: String, - @Query("max_id") maxId: String?, - @Query("since_id") sinceId: String?, - @Query("min_id") minId: String?, - @Query("limit") limit: Int?, - @Query("exclude_reblogs") excludeReblogs: Boolean? + @Path("id") accountId: String, + @Query("max_id") maxId: String?, + @Query("since_id") sinceId: String?, + @Query("min_id") minId: String?, + @Query("limit") limit: Int?, + @Query("exclude_reblogs") excludeReblogs: Boolean? ): Single> @GET("api/v1/statuses/{id}") fun statusObservable( - @Path("id") statusId: String + @Path("id") statusId: String ): Single @GET("api/v2/search") fun searchObservable( - @Query("q") query: String?, - @Query("type") type: String? = null, - @Query("resolve") resolve: Boolean? = null, - @Query("limit") limit: Int? = null, - @Query("offset") offset: Int? = null, - @Query("following") following: Boolean? = null + @Query("q") query: String?, + @Query("type") type: String? = null, + @Query("resolve") resolve: Boolean? = null, + @Query("limit") limit: Int? = null, + @Query("offset") offset: Int? = null, + @Query("following") following: Boolean? = null ): Single @FormUrlEncoded @POST("api/v1/accounts/{id}/note") fun updateAccountNote( - @Path("id") accountId: String, - @Field("comment") note: String + @Path("id") accountId: String, + @Field("comment") note: String ): Single - } diff --git a/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt b/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt index 96fff6f80..ea51d8c3e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt @@ -16,14 +16,22 @@ package com.keylesspalace.tusky.network import android.util.Log -import com.keylesspalace.tusky.appstore.* +import com.keylesspalace.tusky.appstore.BlockEvent +import com.keylesspalace.tusky.appstore.BookmarkEvent +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.FavoriteEvent +import com.keylesspalace.tusky.appstore.MuteConversationEvent +import com.keylesspalace.tusky.appstore.MuteEvent +import com.keylesspalace.tusky.appstore.PinEvent +import com.keylesspalace.tusky.appstore.PollVoteEvent +import com.keylesspalace.tusky.appstore.ReblogEvent +import com.keylesspalace.tusky.appstore.StatusDeletedEvent import com.keylesspalace.tusky.entity.DeletedStatus import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.entity.Status import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.kotlin.addTo -import java.lang.IllegalStateException /** * Created by charlag on 3/24/18. @@ -98,21 +106,27 @@ class TimelineCasesImpl( override fun mute(statusId: String, notifications: Boolean, duration: Int?) { mastodonApi.muteAccount(statusId, notifications, duration) - .subscribe({ - eventHub.dispatch(MuteEvent(statusId)) - }, { t -> - Log.w("Failed to mute account", t) - }) + .subscribe( + { + eventHub.dispatch(MuteEvent(statusId)) + }, + { t -> + Log.w("Failed to mute account", t) + } + ) .addTo(cancelDisposable) } override fun block(statusId: String) { mastodonApi.blockAccount(statusId) - .subscribe({ - eventHub.dispatch(BlockEvent(statusId)) - }, { t -> - Log.w("Failed to block account", t) - }) + .subscribe( + { + eventHub.dispatch(BlockEvent(statusId)) + }, + { t -> + Log.w("Failed to block account", t) + } + ) .addTo(cancelDisposable) } @@ -140,5 +154,4 @@ class TimelineCasesImpl( eventHub.dispatch(PollVoteEvent(statusId, it)) } } - -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/pager/AccountPagerAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/pager/AccountPagerAdapter.kt index e2e13c66b..2d01a32db 100644 --- a/app/src/main/java/com/keylesspalace/tusky/pager/AccountPagerAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/pager/AccountPagerAdapter.kt @@ -15,18 +15,17 @@ package com.keylesspalace.tusky.pager -import androidx.fragment.app.* - -import com.keylesspalace.tusky.fragment.AccountMediaFragment +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity import com.keylesspalace.tusky.components.timeline.TimelineFragment import com.keylesspalace.tusky.components.timeline.TimelineViewModel +import com.keylesspalace.tusky.fragment.AccountMediaFragment import com.keylesspalace.tusky.interfaces.RefreshableFragment - import com.keylesspalace.tusky.util.CustomFragmentStateAdapter class AccountPagerAdapter( - activity: FragmentActivity, - private val accountId: String + activity: FragmentActivity, + private val accountId: String ) : CustomFragmentStateAdapter(activity) { override fun getItemCount() = TAB_COUNT diff --git a/app/src/main/java/com/keylesspalace/tusky/pager/ImagePagerAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/pager/ImagePagerAdapter.kt index 4f813d8b1..26c5fc05a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/pager/ImagePagerAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/pager/ImagePagerAdapter.kt @@ -8,9 +8,9 @@ import com.keylesspalace.tusky.fragment.ViewMediaFragment import java.lang.ref.WeakReference class ImagePagerAdapter( - activity: FragmentActivity, - private val attachments: List, - private val initialPosition: Int + activity: FragmentActivity, + private val attachments: List, + private val initialPosition: Int ) : ViewMediaAdapter(activity) { private var didTransition = false @@ -25,8 +25,8 @@ class ImagePagerAdapter( // forth photo and then back to the first. The first fragment will try to start the // transition and wait until it's over and it will never take place. val fragment = ViewMediaFragment.newInstance( - attachment = attachments[position], - shouldStartPostponedTransition = !didTransition && position == initialPosition + attachment = attachments[position], + shouldStartPostponedTransition = !didTransition && position == initialPosition ) fragments[position] = WeakReference(fragment) return fragment @@ -35,7 +35,7 @@ class ImagePagerAdapter( } } - override fun onTransitionEnd(position: Int) { + override fun onTransitionEnd(position: Int) { this.didTransition = true fragments[position]?.get()?.onTransitionEnd() } diff --git a/app/src/main/java/com/keylesspalace/tusky/pager/MainPagerAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/pager/MainPagerAdapter.kt index 1e1029410..4fe92660c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/pager/MainPagerAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/pager/MainPagerAdapter.kt @@ -28,5 +28,4 @@ class MainPagerAdapter(val tabs: List, activity: FragmentActivity) : Cu } override fun getItemCount() = tabs.size - } diff --git a/app/src/main/java/com/keylesspalace/tusky/pager/SingleImagePagerAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/pager/SingleImagePagerAdapter.kt index c8306f70e..c1f5342a3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/pager/SingleImagePagerAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/pager/SingleImagePagerAdapter.kt @@ -6,8 +6,8 @@ import com.keylesspalace.tusky.ViewMediaAdapter import com.keylesspalace.tusky.fragment.ViewMediaFragment class SingleImagePagerAdapter( - activity: FragmentActivity, - private val imageUrl: String + activity: FragmentActivity, + private val imageUrl: String ) : ViewMediaAdapter(activity) { override fun createFragment(position: Int): Fragment { diff --git a/app/src/main/java/com/keylesspalace/tusky/receiver/NotificationClearBroadcastReceiver.kt b/app/src/main/java/com/keylesspalace/tusky/receiver/NotificationClearBroadcastReceiver.kt index d9b94857c..6d4e97193 100644 --- a/app/src/main/java/com/keylesspalace/tusky/receiver/NotificationClearBroadcastReceiver.kt +++ b/app/src/main/java/com/keylesspalace/tusky/receiver/NotificationClearBroadcastReceiver.kt @@ -18,9 +18,8 @@ package com.keylesspalace.tusky.receiver import android.content.BroadcastReceiver import android.content.Context import android.content.Intent - -import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.components.notifications.NotificationHelper +import com.keylesspalace.tusky.db.AccountManager import dagger.android.AndroidInjection import javax.inject.Inject @@ -40,5 +39,4 @@ class NotificationClearBroadcastReceiver : BroadcastReceiver() { accountManager.saveAccount(account) } } - } diff --git a/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt b/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt index c69de81c3..fb1d78673 100644 --- a/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt +++ b/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt @@ -26,11 +26,11 @@ import androidx.core.content.ContextCompat import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.compose.ComposeActivity import com.keylesspalace.tusky.components.compose.ComposeActivity.ComposeOptions +import com.keylesspalace.tusky.components.notifications.NotificationHelper import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.service.SendTootService import com.keylesspalace.tusky.service.TootToSend -import com.keylesspalace.tusky.components.notifications.NotificationHelper import com.keylesspalace.tusky.util.randomAlphanumericString import dagger.android.AndroidInjection import javax.inject.Inject @@ -68,10 +68,10 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() { Log.w(TAG, "Account \"$senderId\" not found in database. Aborting quick reply!") val builder = NotificationCompat.Builder(context, NotificationHelper.CHANNEL_MENTION + senderIdentifier) - .setSmallIcon(R.drawable.ic_notify) - .setColor(ContextCompat.getColor(context, R.color.tusky_blue)) - .setGroup(senderFullName) - .setDefaults(0) // So it doesn't ring twice, notify only in Target callback + .setSmallIcon(R.drawable.ic_notify) + .setColor(ContextCompat.getColor(context, R.color.tusky_blue)) + .setGroup(senderFullName) + .setDefaults(0) // So it doesn't ring twice, notify only in Target callback builder.setContentTitle(context.getString(R.string.error_generic)) builder.setContentText(context.getString(R.string.error_sender_account_gone)) @@ -86,34 +86,34 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() { val text = mentions.joinToString(" ", postfix = " ") { "@$it" } + message.toString() val sendIntent = SendTootService.sendTootIntent( - context, - TootToSend( - text = text, - warningText = spoiler, - visibility = visibility.serverString(), - sensitive = false, - mediaIds = emptyList(), - mediaUris = emptyList(), - mediaDescriptions = emptyList(), - scheduledAt = null, - inReplyToId = citedStatusId, - poll = null, - replyingStatusContent = null, - replyingStatusAuthorUsername = null, - accountId = account.id, - draftId = -1, - idempotencyKey = randomAlphanumericString(16), - retries = 0 - ) + context, + TootToSend( + text = text, + warningText = spoiler, + visibility = visibility.serverString(), + sensitive = false, + mediaIds = emptyList(), + mediaUris = emptyList(), + mediaDescriptions = emptyList(), + scheduledAt = null, + inReplyToId = citedStatusId, + poll = null, + replyingStatusContent = null, + replyingStatusAuthorUsername = null, + accountId = account.id, + draftId = -1, + idempotencyKey = randomAlphanumericString(16), + retries = 0 + ) ) context.startService(sendIntent) val builder = NotificationCompat.Builder(context, NotificationHelper.CHANNEL_MENTION + senderIdentifier) - .setSmallIcon(R.drawable.ic_notify) - .setColor(ContextCompat.getColor(context, (R.color.tusky_blue))) - .setGroup(senderFullName) - .setDefaults(0) // So it doesn't ring twice, notify only in Target callback + .setSmallIcon(R.drawable.ic_notify) + .setColor(ContextCompat.getColor(context, (R.color.tusky_blue))) + .setGroup(senderFullName) + .setDefaults(0) // So it doesn't ring twice, notify only in Target callback builder.setContentTitle(context.getString(R.string.status_sent)) builder.setContentText(context.getString(R.string.status_sent_long)) @@ -133,14 +133,17 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() { accountManager.setActiveAccount(senderId) - val composeIntent = ComposeActivity.startIntent(context, ComposeOptions( + val composeIntent = ComposeActivity.startIntent( + context, + ComposeOptions( inReplyToId = citedStatusId, replyVisibility = visibility, contentWarning = spoiler, mentionedUsernames = mentions.toSet(), replyingStatusAuthor = localAuthorId, replyingStatusContent = citedText - )) + ) + ) composeIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) @@ -153,5 +156,4 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() { return remoteInput.getCharSequence(NotificationHelper.KEY_REPLY, "") } - -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt b/app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt index 20ae8f476..ed69a49d4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt +++ b/app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt @@ -35,7 +35,8 @@ import kotlinx.parcelize.Parcelize import retrofit2.Call import retrofit2.Callback import retrofit2.Response -import java.util.* +import java.util.Timer +import java.util.TimerTask import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -76,12 +77,11 @@ class SendTootService : Service(), Injectable { if (intent.hasExtra(KEY_TOOT)) { val tootToSend = intent.getParcelableExtra(KEY_TOOT) - ?: throw IllegalStateException("SendTootService started without $KEY_TOOT extra") + ?: throw IllegalStateException("SendTootService started without $KEY_TOOT extra") if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel(CHANNEL_ID, getString(R.string.send_toot_notification_channel_name), NotificationManager.IMPORTANCE_LOW) notificationManager.createNotificationChannel(channel) - } var notificationText = tootToSend.warningText @@ -90,13 +90,13 @@ class SendTootService : Service(), Injectable { } val builder = NotificationCompat.Builder(this, CHANNEL_ID) - .setSmallIcon(R.drawable.ic_notify) - .setContentTitle(getString(R.string.send_toot_notification_title)) - .setContentText(notificationText) - .setProgress(1, 0, true) - .setOngoing(true) - .setColor(ContextCompat.getColor(this, R.color.tusky_blue)) - .addAction(0, getString(android.R.string.cancel), cancelSendingIntent(sendingNotificationId)) + .setSmallIcon(R.drawable.ic_notify) + .setContentTitle(getString(R.string.send_toot_notification_title)) + .setContentText(notificationText) + .setProgress(1, 0, true) + .setOngoing(true) + .setColor(ContextCompat.getColor(this, R.color.tusky_blue)) + .addAction(0, getString(android.R.string.cancel), cancelSendingIntent(sendingNotificationId)) if (tootsToSend.size == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_DETACH) @@ -107,17 +107,14 @@ class SendTootService : Service(), Injectable { tootsToSend[sendingNotificationId] = tootToSend sendToot(sendingNotificationId--) - } else { if (intent.hasExtra(KEY_CANCEL)) { cancelSending(intent.getIntExtra(KEY_CANCEL, 0)) } - } return START_NOT_STICKY - } private fun sendToot(tootId: Int) { @@ -138,21 +135,21 @@ class SendTootService : Service(), Injectable { tootToSend.retries++ val newStatus = NewStatus( - tootToSend.text, - tootToSend.warningText, - tootToSend.inReplyToId, - tootToSend.visibility, - tootToSend.sensitive, - tootToSend.mediaIds, - tootToSend.scheduledAt, - tootToSend.poll + tootToSend.text, + tootToSend.warningText, + tootToSend.inReplyToId, + tootToSend.visibility, + tootToSend.sensitive, + tootToSend.mediaIds, + tootToSend.scheduledAt, + tootToSend.poll ) val sendCall = mastodonApi.createStatus( - "Bearer " + account.accessToken, - account.domain, - tootToSend.idempotencyKey, - newStatus + "Bearer " + account.accessToken, + account.domain, + tootToSend.idempotencyKey, + newStatus ) sendCalls[tootId] = sendCall @@ -178,24 +175,21 @@ class SendTootService : Service(), Injectable { } notificationManager.cancel(tootId) - } else { // the server refused to accept the toot, save toot & show error message saveTootToDrafts(tootToSend) val builder = NotificationCompat.Builder(this@SendTootService, CHANNEL_ID) - .setSmallIcon(R.drawable.ic_notify) - .setContentTitle(getString(R.string.send_toot_notification_error_title)) - .setContentText(getString(R.string.send_toot_notification_saved_content)) - .setColor(ContextCompat.getColor(this@SendTootService, R.color.tusky_blue)) + .setSmallIcon(R.drawable.ic_notify) + .setContentTitle(getString(R.string.send_toot_notification_error_title)) + .setContentText(getString(R.string.send_toot_notification_saved_content)) + .setColor(ContextCompat.getColor(this@SendTootService, R.color.tusky_blue)) notificationManager.cancel(tootId) notificationManager.notify(errorNotificationId--, builder.build()) - } stopSelfWhenDone() - } override fun onFailure(call: Call, t: Throwable) { @@ -204,16 +198,18 @@ class SendTootService : Service(), Injectable { backoff = MAX_RETRY_INTERVAL } - timer.schedule(object : TimerTask() { - override fun run() { - sendToot(tootId) - } - }, backoff) + timer.schedule( + object : TimerTask() { + override fun run() { + sendToot(tootId) + } + }, + backoff + ) } } sendCall.enqueue(callback) - } private fun stopSelfWhenDone() { @@ -233,20 +229,22 @@ class SendTootService : Service(), Injectable { saveTootToDrafts(tootToCancel) val builder = NotificationCompat.Builder(this@SendTootService, CHANNEL_ID) - .setSmallIcon(R.drawable.ic_notify) - .setContentTitle(getString(R.string.send_toot_notification_cancel_title)) - .setContentText(getString(R.string.send_toot_notification_saved_content)) - .setColor(ContextCompat.getColor(this@SendTootService, R.color.tusky_blue)) + .setSmallIcon(R.drawable.ic_notify) + .setContentTitle(getString(R.string.send_toot_notification_cancel_title)) + .setContentText(getString(R.string.send_toot_notification_saved_content)) + .setColor(ContextCompat.getColor(this@SendTootService, R.color.tusky_blue)) notificationManager.notify(tootId, builder.build()) - timer.schedule(object : TimerTask() { - override fun run() { - notificationManager.cancel(tootId) - stopSelfWhenDone() - } - }, 5000) - + timer.schedule( + object : TimerTask() { + override fun run() { + notificationManager.cancel(tootId) + stopSelfWhenDone() + } + }, + 5000 + ) } } @@ -294,8 +292,9 @@ class SendTootService : Service(), Injectable { private var errorNotificationId = Int.MIN_VALUE // use even more negative ids to not clash with other notis @JvmStatic - fun sendTootIntent(context: Context, - tootToSend: TootToSend + fun sendTootIntent( + context: Context, + tootToSend: TootToSend ): Intent { val intent = Intent(context, SendTootService::class.java) intent.putExtra(KEY_TOOT, tootToSend) @@ -304,41 +303,39 @@ class SendTootService : Service(), Injectable { // forward uri permissions intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) val uriClip = ClipData( - ClipDescription("Toot Media", arrayOf("image/*", "video/*")), - ClipData.Item(tootToSend.mediaUris[0]) + ClipDescription("Toot Media", arrayOf("image/*", "video/*")), + ClipData.Item(tootToSend.mediaUris[0]) ) tootToSend.mediaUris - .drop(1) - .forEach { mediaUri -> - uriClip.addItem(ClipData.Item(mediaUri)) - } + .drop(1) + .forEach { mediaUri -> + uriClip.addItem(ClipData.Item(mediaUri)) + } intent.clipData = uriClip - } return intent } - } } @Parcelize data class TootToSend( - val text: String, - val warningText: String, - val visibility: String, - val sensitive: Boolean, - val mediaIds: List, - val mediaUris: List, - val mediaDescriptions: List, - val scheduledAt: String?, - val inReplyToId: String?, - val poll: NewPoll?, - val replyingStatusContent: String?, - val replyingStatusAuthorUsername: String?, - val accountId: Long, - val draftId: Int, - val idempotencyKey: String, - var retries: Int + val text: String, + val warningText: String, + val visibility: String, + val sensitive: Boolean, + val mediaIds: List, + val mediaUris: List, + val mediaDescriptions: List, + val scheduledAt: String?, + val inReplyToId: String?, + val poll: NewPoll?, + val replyingStatusContent: String?, + val replyingStatusAuthorUsername: String?, + val accountId: Long, + val draftId: Int, + val idempotencyKey: String, + var retries: Int ) : Parcelable diff --git a/app/src/main/java/com/keylesspalace/tusky/service/ServiceClient.kt b/app/src/main/java/com/keylesspalace/tusky/service/ServiceClient.kt index b60377f52..5b9e3298d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/service/ServiceClient.kt +++ b/app/src/main/java/com/keylesspalace/tusky/service/ServiceClient.kt @@ -31,4 +31,4 @@ class ServiceClientImpl(private val context: Context) : ServiceClient { context.startService(intent) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsDSL.kt b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsDSL.kt index 82dfa14e2..1569cb151 100644 --- a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsDSL.kt +++ b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsDSL.kt @@ -2,13 +2,20 @@ package com.keylesspalace.tusky.settings import android.content.Context import androidx.annotation.StringRes -import androidx.preference.* +import androidx.preference.CheckBoxPreference +import androidx.preference.EditTextPreference +import androidx.preference.ListPreference +import androidx.preference.Preference +import androidx.preference.PreferenceCategory +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.PreferenceScreen +import androidx.preference.SwitchPreference import com.keylesspalace.tusky.components.preference.EmojiPreference import okhttp3.OkHttpClient class PreferenceParent( - val context: Context, - val addPref: (pref: Preference) -> Unit + val context: Context, + val addPref: (pref: Preference) -> Unit ) inline fun PreferenceParent.preference(builder: Preference.() -> Unit): Preference { @@ -33,7 +40,7 @@ inline fun PreferenceParent.emojiPreference(okHttpClient: OkHttpClient, builder: } inline fun PreferenceParent.switchPreference( - builder: SwitchPreference.() -> Unit + builder: SwitchPreference.() -> Unit ): SwitchPreference { val pref = SwitchPreference(context) builder(pref) @@ -42,7 +49,7 @@ inline fun PreferenceParent.switchPreference( } inline fun PreferenceParent.editTextPreference( - builder: EditTextPreference.() -> Unit + builder: EditTextPreference.() -> Unit ): EditTextPreference { val pref = EditTextPreference(context) builder(pref) @@ -51,7 +58,7 @@ inline fun PreferenceParent.editTextPreference( } inline fun PreferenceParent.checkBoxPreference( - builder: CheckBoxPreference.() -> Unit + builder: CheckBoxPreference.() -> Unit ): CheckBoxPreference { val pref = CheckBoxPreference(context) builder(pref) @@ -60,8 +67,8 @@ inline fun PreferenceParent.checkBoxPreference( } inline fun PreferenceParent.preferenceCategory( - @StringRes title: Int, - builder: PreferenceParent.(PreferenceCategory) -> Unit + @StringRes title: Int, + builder: PreferenceParent.(PreferenceCategory) -> Unit ) { val category = PreferenceCategory(context) addPref(category) @@ -71,7 +78,7 @@ inline fun PreferenceParent.preferenceCategory( } inline fun PreferenceFragmentCompat.makePreferenceScreen( - builder: PreferenceParent.() -> Unit + builder: PreferenceParent.() -> Unit ): PreferenceScreen { val context = requireContext() val screen = preferenceManager.createPreferenceScreen(context) @@ -81,4 +88,4 @@ inline fun PreferenceFragmentCompat.makePreferenceScreen( preferenceScreen = screen builder(parent) return screen -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/BindingHolder.kt b/app/src/main/java/com/keylesspalace/tusky/util/BindingHolder.kt index a7a4c9720..62167ee6a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/BindingHolder.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/BindingHolder.kt @@ -4,5 +4,5 @@ import androidx.recyclerview.widget.RecyclerView import androidx.viewbinding.ViewBinding class BindingHolder( - val binding: T + val binding: T ) : RecyclerView.ViewHolder(binding.root) diff --git a/app/src/main/java/com/keylesspalace/tusky/util/BlurHashDecoder.kt b/app/src/main/java/com/keylesspalace/tusky/util/BlurHashDecoder.kt index bd5f9007c..117f59c09 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/BlurHashDecoder.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/BlurHashDecoder.kt @@ -74,18 +74,20 @@ object BlurHashDecoder { val g = (value / 19) % 19 val b = value % 19 return floatArrayOf( - signedPow2((r - 9) / 9.0f) * maxAc, - signedPow2((g - 9) / 9.0f) * maxAc, - signedPow2((b - 9) / 9.0f) * maxAc + signedPow2((r - 9) / 9.0f) * maxAc, + signedPow2((g - 9) / 9.0f) * maxAc, + signedPow2((b - 9) / 9.0f) * maxAc ) } private fun signedPow2(value: Float) = value.pow(2f).withSign(value) private fun composeBitmap( - width: Int, height: Int, - numCompX: Int, numCompY: Int, - colors: Array + width: Int, + height: Int, + numCompX: Int, + numCompY: Int, + colors: Array ): Bitmap { val imageArray = IntArray(width * height) for (y in 0 until height) { @@ -118,13 +120,12 @@ object BlurHashDecoder { } private val charMap = listOf( - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', - 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', - 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', - 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '#', '$', '%', '*', '+', ',', - '-', '.', ':', ';', '=', '?', '@', '[', ']', '^', '_', '{', '|', '}', '~' + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', + 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', + 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', + 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '#', '$', '%', '*', '+', ',', + '-', '.', ':', ';', '=', '?', '@', '[', ']', '^', '_', '{', '|', '}', '~' ) - .mapIndexed { i, c -> c to i } - .toMap() - + .mapIndexed { i, c -> c to i } + .toMap() } diff --git a/app/src/main/java/com/keylesspalace/tusky/util/CardViewMode.kt b/app/src/main/java/com/keylesspalace/tusky/util/CardViewMode.kt index 2cf2348cd..81c2216b0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/CardViewMode.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/CardViewMode.kt @@ -4,4 +4,4 @@ enum class CardViewMode { NONE, FULL_WIDTH, INDENTED -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ComposeTokenizer.kt b/app/src/main/java/com/keylesspalace/tusky/util/ComposeTokenizer.kt index c0da4275e..6fee42edf 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ComposeTokenizer.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/ComposeTokenizer.kt @@ -22,10 +22,10 @@ import android.widget.MultiAutoCompleteTextView class ComposeTokenizer : MultiAutoCompleteTextView.Tokenizer { - private fun isMentionOrHashtagAllowedCharacter(character: Char) : Boolean { - return Character.isLetterOrDigit(character) || character == '_' // simple usernames - || character == '-' // extended usernames - || character == '.' // domain dot + private fun isMentionOrHashtagAllowedCharacter(character: Char): Boolean { + return Character.isLetterOrDigit(character) || character == '_' || // simple usernames + character == '-' || // extended usernames + character == '.' // domain dot } override fun findTokenStart(text: CharSequence, cursor: Int): Int { @@ -36,8 +36,8 @@ class ComposeTokenizer : MultiAutoCompleteTextView.Tokenizer { var character = text[i - 1] // go up to first illegal character or character we're looking for (@, # or :) - while(i > 0 && !(character == '@' || character == '#' || character == ':')) { - if(!isMentionOrHashtagAllowedCharacter(character)) { + while (i > 0 && !(character == '@' || character == '#' || character == ':')) { + if (!isMentionOrHashtagAllowedCharacter(character)) { return cursor } @@ -46,13 +46,13 @@ class ComposeTokenizer : MultiAutoCompleteTextView.Tokenizer { } // maybe caught domain name? try search username - if(i > 2 && character == '@') { + if (i > 2 && character == '@') { var j = i - 1 var character2 = text[i - 2] // again go up to first illegal character or tag "@" - while(j > 0 && character2 != '@') { - if(!isMentionOrHashtagAllowedCharacter(character2)) { + while (j > 0 && character2 != '@') { + if (!isMentionOrHashtagAllowedCharacter(character2)) { break } @@ -61,15 +61,16 @@ class ComposeTokenizer : MultiAutoCompleteTextView.Tokenizer { } // found mention symbol, override cursor - if(character2 == '@') { + if (character2 == '@') { i = j character = character2 } } - if (i < 1 - || (character != '@' && character != '#' && character != ':') - || i > 1 && !Character.isWhitespace(text[i - 2])) { + if (i < 1 || + (character != '@' && character != '#' && character != ':') || + i > 1 && !Character.isWhitespace(text[i - 2]) + ) { return cursor } return i - 1 diff --git a/app/src/main/java/com/keylesspalace/tusky/util/CustomEmojiHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/CustomEmojiHelper.kt index f63c44b84..2aee7384b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/CustomEmojiHelper.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/CustomEmojiHelper.kt @@ -18,17 +18,16 @@ package com.keylesspalace.tusky.util import android.graphics.Canvas import android.graphics.Paint -import android.graphics.drawable.* +import android.graphics.drawable.Animatable +import android.graphics.drawable.Drawable import android.text.SpannableStringBuilder import android.text.style.ReplacementSpan import android.view.View - import com.bumptech.glide.Glide import com.bumptech.glide.request.target.CustomTarget import com.bumptech.glide.request.target.Target import com.bumptech.glide.request.transition.Transition import com.keylesspalace.tusky.entity.Emoji - import java.lang.ref.WeakReference import java.util.regex.Pattern @@ -39,8 +38,8 @@ import java.util.regex.Pattern * @param view a reference to the a view the emojis will be shown in (should be the TextView, but parents of the TextView are also acceptable) * @return the text with the shortcodes replaced by EmojiSpans */ -fun CharSequence.emojify(emojis: List?, view: View, animate: Boolean) : CharSequence { - if(emojis.isNullOrEmpty()) +fun CharSequence.emojify(emojis: List?, view: View, animate: Boolean): CharSequence { + if (emojis.isNullOrEmpty()) return this val builder = SpannableStringBuilder.valueOf(this) @@ -49,7 +48,7 @@ fun CharSequence.emojify(emojis: List?, view: View, animate: Boolean) : C val matcher = Pattern.compile(":$shortcode:", Pattern.LITERAL) .matcher(this) - while(matcher.find()) { + while (matcher.find()) { val span = EmojiSpan(WeakReference(view)) builder.setSpan(span, matcher.start(), matcher.end(), 0) @@ -64,8 +63,8 @@ fun CharSequence.emojify(emojis: List?, view: View, animate: Boolean) : C class EmojiSpan(val viewWeakReference: WeakReference) : ReplacementSpan() { var imageDrawable: Drawable? = null - - override fun getSize(paint: Paint, text: CharSequence, start: Int, end: Int, fm: Paint.FontMetricsInt?) : Int { + + override fun getSize(paint: Paint, text: CharSequence, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int { if (fm != null) { /* update FontMetricsInt or otherwise span does not get drawn when * it covers the whole text */ @@ -75,10 +74,10 @@ class EmojiSpan(val viewWeakReference: WeakReference) : ReplacementSpan() fm.descent = metrics.descent fm.bottom = metrics.bottom } - + return (paint.textSize * 1.2).toInt() } - + override fun draw(canvas: Canvas, text: CharSequence, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) { imageDrawable?.let { drawable -> canvas.save() @@ -94,15 +93,15 @@ class EmojiSpan(val viewWeakReference: WeakReference) : ReplacementSpan() canvas.restore() } } - - fun getTarget(animate : Boolean): Target { + + fun getTarget(animate: Boolean): Target { return object : CustomTarget() { override fun onResourceReady(resource: Drawable, transition: Transition?) { viewWeakReference.get()?.let { view -> - if(animate && resource is Animatable) { + if (animate && resource is Animatable) { val callback = resource.callback - resource.callback = object: Drawable.Callback { + resource.callback = object : Drawable.Callback { override fun unscheduleDrawable(p0: Drawable, p1: Runnable) { callback?.unscheduleDrawable(p0, p1) } @@ -121,7 +120,7 @@ class EmojiSpan(val viewWeakReference: WeakReference) : ReplacementSpan() view.invalidate() } } - + override fun onLoadCleared(placeholder: Drawable?) {} } } diff --git a/app/src/main/java/com/keylesspalace/tusky/util/CustomFragmentStateAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/util/CustomFragmentStateAdapter.kt index bda206144..eb31032f5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/CustomFragmentStateAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/CustomFragmentStateAdapter.kt @@ -20,9 +20,9 @@ import androidx.fragment.app.FragmentActivity import androidx.viewpager2.adapter.FragmentStateAdapter abstract class CustomFragmentStateAdapter( - private val activity: FragmentActivity -): FragmentStateAdapter(activity) { + private val activity: FragmentActivity +) : FragmentStateAdapter(activity) { - fun getFragment(position: Int): Fragment? - = activity.supportFragmentManager.findFragmentByTag("f" + getItemId(position)) + fun getFragment(position: Int): Fragment? = + activity.supportFragmentManager.findFragmentByTag("f" + getItemId(position)) } diff --git a/app/src/main/java/com/keylesspalace/tusky/util/Either.kt b/app/src/main/java/com/keylesspalace/tusky/util/Either.kt index f0955cfa8..728ccd0e0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/Either.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/Either.kt @@ -44,4 +44,4 @@ sealed class Either { Right(mapper(this.asRight())) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.kt b/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.kt index 4bce7b8f4..f513feeef 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.kt @@ -29,13 +29,14 @@ import kotlin.math.max * This class bundles information about an emoji font as well as many convenient actions. */ class EmojiCompatFont( - val name: String, - private val display: String, - @StringRes val caption: Int, - @DrawableRes val img: Int, - val url: String, - // The version is stored as a String in the x.xx.xx format (to be able to compare versions) - val version: String) { + val name: String, + private val display: String, + @StringRes val caption: Int, + @DrawableRes val img: Int, + val url: String, + // The version is stored as a String in the x.xx.xx format (to be able to compare versions) + val version: String +) { private val versionCode = getVersionCode(version) @@ -102,8 +103,13 @@ class EmojiCompatFont( if (compareVersions(fileExists.second, versionCode) < 0) { val file = fileExists.first // Uses side effects! - Log.d(TAG, String.format("Deleted %s successfully: %s", file.absolutePath, - file.delete())) + Log.d( + TAG, + String.format( + "Deleted %s successfully: %s", file.absolutePath, + file.delete() + ) + ) } } } @@ -131,8 +137,13 @@ class EmojiCompatFont( val fontRegex = "$name(\\d+(\\.\\d+)*)?\\.ttf".toPattern() val ttfFilter = FilenameFilter { _, name: String -> name.endsWith(".ttf") } val foundFontFiles = directory.listFiles(ttfFilter).orEmpty() - Log.d(TAG, String.format("loadExistingFontFiles: %d other font files found", - foundFontFiles.size)) + Log.d( + TAG, + String.format( + "loadExistingFontFiles: %d other font files found", + foundFontFiles.size + ) + ) return foundFontFiles.map { file -> val matcher = fontRegex.matcher(file.name) @@ -170,8 +181,10 @@ class EmojiCompatFont( } } - fun downloadFontFile(context: Context, - okHttpClient: OkHttpClient): Observable { + fun downloadFontFile( + context: Context, + okHttpClient: OkHttpClient + ): Observable { return Observable.create { emitter: ObservableEmitter -> // It is possible (and very likely) that the file does not exist yet val downloadFile = getFontFile(context)!! @@ -180,7 +193,7 @@ class EmojiCompatFont( downloadFile.createNewFile() } val request = Request.Builder().url(url) - .build() + .build() val sink = downloadFile.sink().buffer() var source: Source? = null @@ -197,7 +210,7 @@ class EmojiCompatFont( while (!emitter.isDisposed) { sink.write(source, CHUNK_SIZE) progress += CHUNK_SIZE.toFloat() - if(size > 0) { + if (size > 0) { emitter.onNext(progress / size) } else { emitter.onNext(-1f) @@ -213,7 +226,6 @@ class EmojiCompatFont( Log.e(TAG, "Downloading $url failed. Status code: ${response.code}") emitter.tryOnError(Exception()) } - } catch (ex: IOException) { Log.e(TAG, "Downloading $url failed.", ex) downloadFile.deleteIfExists() @@ -228,10 +240,8 @@ class EmojiCompatFont( emitter.onComplete() } } - } - .subscribeOn(Schedulers.io()) - + .subscribeOn(Schedulers.io()) } /** @@ -256,32 +266,37 @@ class EmojiCompatFont( private const val CHUNK_SIZE = 4096L // The system font gets some special behavior... - val SYSTEM_DEFAULT = EmojiCompatFont("system-default", - "System Default", - R.string.caption_systememoji, - R.drawable.ic_emoji_34dp, - "", - "0") - val BLOBMOJI = EmojiCompatFont("Blobmoji", - "Blobmoji", - R.string.caption_blobmoji, - R.drawable.ic_blobmoji, - "https://tusky.app/hosted/emoji/BlobmojiCompat.ttf", - "12.0.0" + val SYSTEM_DEFAULT = EmojiCompatFont( + "system-default", + "System Default", + R.string.caption_systememoji, + R.drawable.ic_emoji_34dp, + "", + "0" ) - val TWEMOJI = EmojiCompatFont("Twemoji", - "Twemoji", - R.string.caption_twemoji, - R.drawable.ic_twemoji, - "https://tusky.app/hosted/emoji/TwemojiCompat.ttf", - "12.0.0" + val BLOBMOJI = EmojiCompatFont( + "Blobmoji", + "Blobmoji", + R.string.caption_blobmoji, + R.drawable.ic_blobmoji, + "https://tusky.app/hosted/emoji/BlobmojiCompat.ttf", + "12.0.0" ) - val NOTOEMOJI = EmojiCompatFont("NotoEmoji", - "Noto Emoji", - R.string.caption_notoemoji, - R.drawable.ic_notoemoji, - "https://tusky.app/hosted/emoji/NotoEmojiCompat.ttf", - "11.0.0" + val TWEMOJI = EmojiCompatFont( + "Twemoji", + "Twemoji", + R.string.caption_twemoji, + R.drawable.ic_twemoji, + "https://tusky.app/hosted/emoji/TwemojiCompat.ttf", + "12.0.0" + ) + val NOTOEMOJI = EmojiCompatFont( + "NotoEmoji", + "Noto Emoji", + R.string.caption_notoemoji, + R.drawable.ic_notoemoji, + "https://tusky.app/hosted/emoji/NotoEmojiCompat.ttf", + "11.0.0" ) /** @@ -341,11 +356,9 @@ class EmojiCompatFont( } private fun File.deleteIfExists() { - if(exists() && !delete()) { + if (exists() && !delete()) { Log.e(TAG, "Could not delete file $this") } } - } - -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/FocalPointUtil.kt b/app/src/main/java/com/keylesspalace/tusky/util/FocalPointUtil.kt index 6f2542b50..41d1034c7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/FocalPointUtil.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/FocalPointUtil.kt @@ -16,7 +16,6 @@ package com.keylesspalace.tusky.util import android.graphics.Matrix - import com.keylesspalace.tusky.entity.Attachment.Focus /** @@ -54,12 +53,14 @@ object FocalPointUtil { * * @return The matrix which correctly crops the image */ - fun updateFocalPointMatrix(viewWidth: Float, - viewHeight: Float, - imageWidth: Float, - imageHeight: Float, - focus: Focus, - mat: Matrix) { + fun updateFocalPointMatrix( + viewWidth: Float, + viewHeight: Float, + imageWidth: Float, + imageHeight: Float, + focus: Focus, + mat: Matrix + ) { // Reset the cached matrix: mat.reset() @@ -84,11 +85,15 @@ object FocalPointUtil { * * The scaling used depends on if we need a vertical of horizontal crop. */ - fun calculateScaling(viewWidth: Float, viewHeight: Float, - imageWidth: Float, imageHeight: Float): Float { + fun calculateScaling( + viewWidth: Float, + viewHeight: Float, + imageWidth: Float, + imageHeight: Float + ): Float { return if (isVerticalCrop(viewWidth, viewHeight, imageWidth, imageHeight)) { viewWidth / imageWidth - } else { // horizontal crop: + } else { // horizontal crop: viewHeight / imageHeight } } @@ -96,8 +101,12 @@ object FocalPointUtil { /** * Return true if we need a vertical crop, false for a horizontal crop. */ - fun isVerticalCrop(viewWidth: Float, viewHeight: Float, - imageWidth: Float, imageHeight: Float): Boolean { + fun isVerticalCrop( + viewWidth: Float, + viewHeight: Float, + imageWidth: Float, + imageHeight: Float + ): Boolean { val viewRatio = viewWidth / viewHeight val imageRatio = imageWidth / imageHeight @@ -135,8 +144,12 @@ object FocalPointUtil { * the image. So it won't put the very edge of the image in center, because that would * leave part of the view empty. */ - fun focalOffset(view: Float, image: Float, - scale: Float, focal: Float): Float { + fun focalOffset( + view: Float, + image: Float, + scale: Float, + focal: Float + ): Float { // The fraction of the image that will be in view: val inView = view / (scale * image) var offset = 0f diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ImageLoadingHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/ImageLoadingHelper.kt index 9daf16f80..1cd9b99ac 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ImageLoadingHelper.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/ImageLoadingHelper.kt @@ -11,41 +11,38 @@ import com.bumptech.glide.load.resource.bitmap.CenterCrop import com.bumptech.glide.load.resource.bitmap.RoundedCorners import com.keylesspalace.tusky.R - private val centerCropTransformation = CenterCrop() fun loadAvatar(url: String?, imageView: ImageView, @Px radius: Int, animate: Boolean) { if (url.isNullOrBlank()) { Glide.with(imageView) - .load(R.drawable.avatar_default) - .into(imageView) + .load(R.drawable.avatar_default) + .into(imageView) } else { if (animate) { Glide.with(imageView) - .load(url) - .transform( - centerCropTransformation, - RoundedCorners(radius) - ) - .placeholder(R.drawable.avatar_default) - .into(imageView) - + .load(url) + .transform( + centerCropTransformation, + RoundedCorners(radius) + ) + .placeholder(R.drawable.avatar_default) + .into(imageView) } else { Glide.with(imageView) - .asBitmap() - .load(url) - .transform( - centerCropTransformation, - RoundedCorners(radius) - ) - .placeholder(R.drawable.avatar_default) - .into(imageView) + .asBitmap() + .load(url) + .transform( + centerCropTransformation, + RoundedCorners(radius) + ) + .placeholder(R.drawable.avatar_default) + .into(imageView) } - } } fun decodeBlurHash(context: Context, blurhash: String): BitmapDrawable { return BitmapDrawable(context.resources, BlurHashDecoder.decode(blurhash, 32, 32, 1f)) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ListStatusAccessibilityDelegate.kt b/app/src/main/java/com/keylesspalace/tusky/util/ListStatusAccessibilityDelegate.kt index 93e0c67bb..879fccc30 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ListStatusAccessibilityDelegate.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/ListStatusAccessibilityDelegate.kt @@ -32,7 +32,7 @@ class ListStatusAccessibilityDelegate( private val statusProvider: StatusProvider ) : RecyclerViewAccessibilityDelegate(recyclerView) { private val a11yManager = context.getSystemService(Context.ACCESSIBILITY_SERVICE) - as AccessibilityManager + as AccessibilityManager override fun getItemDelegate(): AccessibilityDelegateCompat = itemDelegate @@ -92,11 +92,11 @@ class ListStatusAccessibilityDelegate( info.addAction(moreAction) } - } override fun performAccessibilityAction( - host: View, action: Int, + host: View, + action: Int, args: Bundle? ): Boolean { val pos = recyclerView.getChildAdapterPosition(host) @@ -170,7 +170,6 @@ class ListStatusAccessibilityDelegate( return true } - private fun showLinksDialog(host: View) { val status = getStatus(host) as? StatusViewData.Concrete ?: return val links = getLinks(status).toList() @@ -228,7 +227,6 @@ class ListStatusAccessibilityDelegate( } } - private fun getLinks(status: StatusViewData.Concrete): Sequence { val content = status.content return if (content is Spannable) { @@ -268,7 +266,6 @@ class ListStatusAccessibilityDelegate( a11yManager.interrupt() } - private fun isHashtag(text: CharSequence) = text.startsWith("#") private val collapseCwAction = AccessibilityActionCompat( @@ -357,4 +354,4 @@ class ListStatusAccessibilityDelegate( ) private data class LinkSpanInfo(val text: String, val link: String) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ListUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/ListUtils.kt index 28ef0c63a..7cdc12e83 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ListUtils.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/ListUtils.kt @@ -17,9 +17,8 @@ package com.keylesspalace.tusky.util -import java.util.LinkedHashSet import java.util.ArrayList - +import java.util.LinkedHashSet /** * @return true if list is null or else return list.isEmpty() @@ -56,4 +55,4 @@ inline fun List.replacedFirstWhich(replacement: T, predicate: (T) -> Bool inline fun Iterable<*>.firstIsInstanceOrNull(): R? { return firstOrNull { it is R }?.let { it as R } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/LiveDataUtil.kt b/app/src/main/java/com/keylesspalace/tusky/util/LiveDataUtil.kt index 822a8da6b..21c4307c6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/LiveDataUtil.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/LiveDataUtil.kt @@ -15,17 +15,21 @@ package com.keylesspalace.tusky.util -import androidx.lifecycle.* +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LiveData +import androidx.lifecycle.LiveDataReactiveStreams +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.Observer +import androidx.lifecycle.Transformations import io.reactivex.rxjava3.core.BackpressureStrategy import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.core.Single - inline fun LiveData.map(crossinline mapFunction: (X) -> Y): LiveData = - Transformations.map(this) { input -> mapFunction(input) } + Transformations.map(this) { input -> mapFunction(input) } inline fun LiveData.switchMap( - crossinline switchMapFunction: (X) -> LiveData + crossinline switchMapFunction: (X) -> LiveData ): LiveData = Transformations.switchMap(this) { input -> switchMapFunction(input) } inline fun LiveData.filter(crossinline predicate: (X) -> Boolean): LiveData { @@ -39,17 +43,17 @@ inline fun LiveData.filter(crossinline predicate: (X) -> Boolean): LiveDa } fun LifecycleOwner.withLifecycleContext(body: LifecycleContext.() -> Unit) = - LifecycleContext(this).apply(body) + LifecycleContext(this).apply(body) class LifecycleContext(val lifecycleOwner: LifecycleOwner) { inline fun LiveData.observe(crossinline observer: (T) -> Unit) = - this.observe(lifecycleOwner, Observer { observer(it) }) + this.observe(lifecycleOwner, Observer { observer(it) }) /** * Just hold a subscription, */ fun LiveData.subscribe() = - this.observe(lifecycleOwner, Observer { }) + this.observe(lifecycleOwner, Observer { }) } /** @@ -90,5 +94,5 @@ fun combineOptionalLiveData(a: LiveData, b: LiveData, combiner: fun Single.toLiveData() = LiveDataReactiveStreams.fromPublisher(this.toFlowable()) fun Observable.toLiveData( - backpressureStrategy: BackpressureStrategy = BackpressureStrategy.LATEST -) = LiveDataReactiveStreams.fromPublisher(this.toFlowable(BackpressureStrategy.LATEST)) \ No newline at end of file + backpressureStrategy: BackpressureStrategy = BackpressureStrategy.LATEST +) = LiveDataReactiveStreams.fromPublisher(this.toFlowable(BackpressureStrategy.LATEST)) diff --git a/app/src/main/java/com/keylesspalace/tusky/util/LocaleManager.kt b/app/src/main/java/com/keylesspalace/tusky/util/LocaleManager.kt index 4a80bca20..45f3ab371 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/LocaleManager.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/LocaleManager.kt @@ -19,7 +19,7 @@ import android.content.Context import android.content.SharedPreferences import android.content.res.Configuration import androidx.preference.PreferenceManager -import java.util.* +import java.util.Locale class LocaleManager(context: Context) { diff --git a/app/src/main/java/com/keylesspalace/tusky/util/MediaUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/MediaUtils.kt index 43f05e9cb..5482b292b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/MediaUtils.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/MediaUtils.kt @@ -22,11 +22,13 @@ import android.graphics.BitmapFactory import android.graphics.Matrix import android.net.Uri import android.provider.OpenableColumns +import android.util.Log import androidx.annotation.Px import androidx.exifinterface.media.ExifInterface -import android.util.Log -import java.io.* - +import java.io.File +import java.io.FileNotFoundException +import java.io.IOException +import java.io.InputStream import java.text.SimpleDateFormat import java.util.Calendar import java.util.Date @@ -46,7 +48,7 @@ const val MEDIA_SIZE_UNKNOWN = -1L * @return the size of the media in bytes or {@link MediaUtils#MEDIA_SIZE_UNKNOWN} */ fun getMediaSize(contentResolver: ContentResolver, uri: Uri?): Long { - if(uri == null) { + if (uri == null) { return MEDIA_SIZE_UNKNOWN } @@ -165,8 +167,10 @@ fun reorientBitmap(bitmap: Bitmap?, orientation: Int): Bitmap? { } return try { - val result = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, - bitmap.height, matrix, true) + val result = Bitmap.createBitmap( + bitmap, 0, 0, bitmap.width, + bitmap.height, matrix, true + ) if (!bitmap.sameAs(result)) { bitmap.recycle() } @@ -210,7 +214,7 @@ fun deleteStaleCachedMedia(mediaDirectory: File?) { twentyfourHoursAgo.add(Calendar.HOUR, -24) val unixTime = twentyfourHoursAgo.timeInMillis - val files = mediaDirectory.listFiles{ file -> unixTime > file.lastModified() && file.name.contains(MEDIA_TEMP_PREFIX) } + val files = mediaDirectory.listFiles { file -> unixTime > file.lastModified() && file.name.contains(MEDIA_TEMP_PREFIX) } if (files == null || files.isEmpty()) { // Nothing to do return diff --git a/app/src/main/java/com/keylesspalace/tusky/util/NetworkState.kt b/app/src/main/java/com/keylesspalace/tusky/util/NetworkState.kt index 09a00339a..c7eea3a65 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/NetworkState.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/NetworkState.kt @@ -24,11 +24,12 @@ enum class Status { @Suppress("DataClassPrivateConstructor") data class NetworkState private constructor( - val status: Status, - val msg: String? = null) { + val status: Status, + val msg: String? = null +) { companion object { val LOADED = NetworkState(Status.SUCCESS) val LOADING = NetworkState(Status.RUNNING) fun error(msg: String?) = NetworkState(Status.FAILED, msg) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/NotificationTypeConverter.kt b/app/src/main/java/com/keylesspalace/tusky/util/NotificationTypeConverter.kt index 65c8f6c08..34e8924f8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/NotificationTypeConverter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/NotificationTypeConverter.kt @@ -42,4 +42,4 @@ fun deserialize(data: String?): Set { } } return ret -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/PickMediaFiles.kt b/app/src/main/java/com/keylesspalace/tusky/util/PickMediaFiles.kt index ae09d9e4f..4d3fcd5b4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/PickMediaFiles.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/PickMediaFiles.kt @@ -49,4 +49,4 @@ class PickMediaFiles : ActivityResultContract>() { } return emptyList() } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/Resource.kt b/app/src/main/java/com/keylesspalace/tusky/util/Resource.kt index 1f9f35d20..ddc88f45d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/Resource.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/Resource.kt @@ -6,8 +6,9 @@ class Loading (override val data: T? = null) : Resource(data) class Success (override val data: T? = null) : Resource(data) -class Error (override val data: T? = null, - val errorMessage: String? = null, - var consumed: Boolean = false, - val cause: Throwable? = null -): Resource(data) \ No newline at end of file +class Error ( + override val data: T? = null, + val errorMessage: String? = null, + var consumed: Boolean = false, + val cause: Throwable? = null +) : Resource(data) diff --git a/app/src/main/java/com/keylesspalace/tusky/util/RickRoll.kt b/app/src/main/java/com/keylesspalace/tusky/util/RickRoll.kt index 03c3339fb..788786ed0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/RickRoll.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/RickRoll.kt @@ -6,9 +6,9 @@ import android.net.Uri import com.keylesspalace.tusky.R fun shouldRickRoll(context: Context, domain: String) = - context.resources.getStringArray(R.array.rick_roll_domains).any { candidate -> - domain.equals(candidate, true) || domain.endsWith(".$candidate", true) - } + context.resources.getStringArray(R.array.rick_roll_domains).any { candidate -> + domain.equals(candidate, true) || domain.endsWith(".$candidate", true) + } fun rickRoll(context: Context) { val uri = Uri.parse(context.getString(R.string.rick_roll_url)) diff --git a/app/src/main/java/com/keylesspalace/tusky/util/RxAwareViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/util/RxAwareViewModel.kt index 62af3ba56..0f3267436 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/RxAwareViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/RxAwareViewModel.kt @@ -15,4 +15,4 @@ open class RxAwareViewModel : ViewModel() { super.onCleared() disposables.clear() } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ShareShortcutHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/ShareShortcutHelper.kt index 11b7e2ccd..ee6874964 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ShareShortcutHelper.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/ShareShortcutHelper.kt @@ -43,17 +43,17 @@ fun updateShortcut(context: Context, account: AccountEntity) { val bmp = if (TextUtils.isEmpty(account.profilePictureUrl)) { Glide.with(context) - .asBitmap() - .load(R.drawable.avatar_default) - .submit(innerSize, innerSize) - .get() + .asBitmap() + .load(R.drawable.avatar_default) + .submit(innerSize, innerSize) + .get() } else { Glide.with(context) - .asBitmap() - .load(account.profilePictureUrl) - .error(R.drawable.avatar_default) - .submit(innerSize, innerSize) - .get() + .asBitmap() + .load(account.profilePictureUrl) + .error(R.drawable.avatar_default) + .submit(innerSize, innerSize) + .get() } // inset the loaded bitmap inside a 108dp transparent canvas so it looks good as adaptive icon @@ -65,10 +65,10 @@ fun updateShortcut(context: Context, account: AccountEntity) { val icon = IconCompat.createWithAdaptiveBitmap(outBmp) val person = Person.Builder() - .setIcon(icon) - .setName(account.displayName) - .setKey(account.identifier) - .build() + .setIcon(icon) + .setName(account.displayName) + .setKey(account.identifier) + .build() // This intent will be sent when the user clicks on one of the launcher shortcuts. Intent from share sheet will be different val intent = Intent(context, MainActivity::class.java).apply { @@ -78,26 +78,22 @@ fun updateShortcut(context: Context, account: AccountEntity) { } val shortcutInfo = ShortcutInfoCompat.Builder(context, account.id.toString()) - .setIntent(intent) - .setCategories(setOf("com.keylesspalace.tusky.Share")) - .setShortLabel(account.displayName) - .setPerson(person) - .setLongLived(true) - .setIcon(icon) - .build() + .setIntent(intent) + .setCategories(setOf("com.keylesspalace.tusky.Share")) + .setShortLabel(account.displayName) + .setPerson(person) + .setLongLived(true) + .setIcon(icon) + .build() ShortcutManagerCompat.addDynamicShortcuts(context, listOf(shortcutInfo)) - } - .subscribeOn(Schedulers.io()) - .onErrorReturnItem(false) - .subscribe() - - + .subscribeOn(Schedulers.io()) + .onErrorReturnItem(false) + .subscribe() } fun removeShortcut(context: Context, account: AccountEntity) { ShortcutManagerCompat.removeDynamicShortcuts(context, listOf(account.id.toString())) - -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/SmartLengthInputFilter.kt b/app/src/main/java/com/keylesspalace/tusky/util/SmartLengthInputFilter.kt index ba9c42039..078639acd 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/SmartLengthInputFilter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/SmartLengthInputFilter.kt @@ -35,10 +35,10 @@ private const val LENGTH_DEFAULT = 500 * be hidden will not be enough to justify the operation. * * @param message The message to trim. - * @return Whether the message should be trimmed or not. + * @return Whether the message should be trimmed or not. */ fun shouldTrimStatus(message: Spanned): Boolean { - return message.isNotEmpty() && LENGTH_DEFAULT.toFloat() / message.length < 0.75 + return message.isNotEmpty() && LENGTH_DEFAULT.toFloat() / message.length < 0.75 } /** @@ -53,59 +53,59 @@ fun shouldTrimStatus(message: Spanned): Boolean { * */ object SmartLengthInputFilter : InputFilter { - /** {@inheritDoc} */ - override fun filter(source: CharSequence, start: Int, end: Int, dest: Spanned, dstart: Int, dend: Int): CharSequence? { - // Code originally imported from InputFilter.LengthFilter but heavily customized and converted to Kotlin. - // https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/text/InputFilter.java#175 + /** {@inheritDoc} */ + override fun filter(source: CharSequence, start: Int, end: Int, dest: Spanned, dstart: Int, dend: Int): CharSequence? { + // Code originally imported from InputFilter.LengthFilter but heavily customized and converted to Kotlin. + // https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/text/InputFilter.java#175 - val sourceLength = source.length - var keep = LENGTH_DEFAULT - (dest.length - (dend - dstart)) - if (keep <= 0) return "" - if (keep >= end - start) return null // Keep original + val sourceLength = source.length + var keep = LENGTH_DEFAULT - (dest.length - (dend - dstart)) + if (keep <= 0) return "" + if (keep >= end - start) return null // Keep original - keep += start + keep += start - // Skip trimming if the ratio doesn't warrant it - if (keep.toDouble() / sourceLength > 0.75) return null + // Skip trimming if the ratio doesn't warrant it + if (keep.toDouble() / sourceLength > 0.75) return null - // Enable trimming at the end of the closest word if possible - if (source[keep].isLetterOrDigit()) { - var boundary: Int + // Enable trimming at the end of the closest word if possible + if (source[keep].isLetterOrDigit()) { + var boundary: Int - // Android N+ offer a clone of the ICU APIs in Java for better internationalization and - // unicode support. Using the ICU version of BreakIterator grants better support for - // those without having to add the ICU4J library at a minimum Api trade-off. - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { - val iterator = android.icu.text.BreakIterator.getWordInstance() - iterator.setText(source.toString()) - boundary = iterator.following(keep) - if (keep - boundary > RUNWAY) boundary = iterator.preceding(keep) - } else { - val iterator = java.text.BreakIterator.getWordInstance() - iterator.setText(source.toString()) - boundary = iterator.following(keep) - if (keep - boundary > RUNWAY) boundary = iterator.preceding(keep) - } + // Android N+ offer a clone of the ICU APIs in Java for better internationalization and + // unicode support. Using the ICU version of BreakIterator grants better support for + // those without having to add the ICU4J library at a minimum Api trade-off. + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + val iterator = android.icu.text.BreakIterator.getWordInstance() + iterator.setText(source.toString()) + boundary = iterator.following(keep) + if (keep - boundary > RUNWAY) boundary = iterator.preceding(keep) + } else { + val iterator = java.text.BreakIterator.getWordInstance() + iterator.setText(source.toString()) + boundary = iterator.following(keep) + if (keep - boundary > RUNWAY) boundary = iterator.preceding(keep) + } - keep = boundary - } else { + keep = boundary + } else { - // If no runway is allowed simply remove whitespaces if present - while(source[keep - 1].isWhitespace()) { - --keep - if (keep == start) return "" - } - } + // If no runway is allowed simply remove whitespaces if present + while (source[keep - 1].isWhitespace()) { + --keep + if (keep == start) return "" + } + } - if (source[keep - 1].isHighSurrogate()) { - --keep - if (keep == start) return "" - } + if (source[keep - 1].isHighSurrogate()) { + --keep + if (keep == start) return "" + } - return if (source is Spanned) { - SpannableStringBuilder(source, start, keep).append("…") - } else { - "${source.subSequence(start, keep)}…" - } - } -} \ No newline at end of file + return if (source is Spanned) { + SpannableStringBuilder(source, start, keep).append("…") + } else { + "${source.subSequence(start, keep)}…" + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/SpanUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/SpanUtils.kt index b0c3850f7..7734d9d7f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/SpanUtils.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/SpanUtils.kt @@ -49,13 +49,17 @@ private class FindCharsResult { var end: Int = -1 } -private class PatternFinder(val searchCharacter: Char, regex: String, val searchPrefixWidth: Int, - val prefixValidator: (Int) -> Boolean) { +private class PatternFinder( + val searchCharacter: Char, + regex: String, + val searchPrefixWidth: Int, + val prefixValidator: (Int) -> Boolean +) { val pattern: Pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE) } private fun clearSpans(text: Spannable, spanClass: Class) { - for(span in text.getSpans(0, text.length, spanClass)) { + for (span in text.getSpans(0, text.length, spanClass)) { text.removeSpan(span) } } @@ -66,14 +70,18 @@ private fun findPattern(string: String, fromIndex: Int): FindCharsResult { val c = string[i] for (matchType in FoundMatchType.values()) { val finder = finders[matchType] - if (finder!!.searchCharacter == c - && ((i - fromIndex) < finder.searchPrefixWidth || - finder.prefixValidator(string.codePointAt(i - finder.searchPrefixWidth)))) { + if (finder!!.searchCharacter == c && + ( + (i - fromIndex) < finder.searchPrefixWidth || + finder.prefixValidator(string.codePointAt(i - finder.searchPrefixWidth)) + ) + ) { result.matchType = matchType result.start = max(0, i - finder.searchPrefixWidth) findEndOfPattern(string, result, finder.pattern) if (result.start + finder.searchPrefixWidth <= i + 1 && // The found result is actually triggered by the correct search character - result.end >= result.start) { // ...and we actually found a valid result + result.end >= result.start + ) { // ...and we actually found a valid result return result } } @@ -92,7 +100,8 @@ private fun findEndOfPattern(string: String, result: FindCharsResult, pattern: P FoundMatchType.TAG -> { if (isValidForTagPrefix(string.codePointAt(result.start))) { if (string[result.start] != '#' || - (string[result.start] == '#' && string[result.start + 1] == '#')) { + (string[result.start] == '#' && string[result.start + 1] == '#') + ) { ++result.start } } @@ -116,7 +125,7 @@ private fun findEndOfPattern(string: String, result: FindCharsResult, pattern: P } private fun getSpan(matchType: FoundMatchType, string: String, colour: Int, start: Int, end: Int): CharacterStyle { - return when(matchType) { + return when (matchType) { FoundMatchType.HTTP_URL -> NoUnderlineURLSpan(string.substring(start, end)) FoundMatchType.HTTPS_URL -> NoUnderlineURLSpan(string.substring(start, end)) else -> ForegroundColorSpan(colour) @@ -149,13 +158,15 @@ fun highlightSpans(text: Spannable, colour: Int) { private fun isWordCharacters(codePoint: Int): Boolean { return (codePoint in 0x30..0x39) || // [0-9] - (codePoint in 0x41..0x5a) || // [A-Z] - (codePoint == 0x5f) || // _ - (codePoint in 0x61..0x7a) // [a-z] + (codePoint in 0x41..0x5a) || // [A-Z] + (codePoint == 0x5f) || // _ + (codePoint in 0x61..0x7a) // [a-z] } private fun isValidForTagPrefix(codePoint: Int): Boolean { - return !(isWordCharacters(codePoint) || // \w + return !( + isWordCharacters(codePoint) || // \w (codePoint == 0x2f) || // / - (codePoint == 0x29)) // ) + (codePoint == 0x29) + ) // ) } diff --git a/app/src/main/java/com/keylesspalace/tusky/util/StatusDisplayOptions.kt b/app/src/main/java/com/keylesspalace/tusky/util/StatusDisplayOptions.kt index ce19e00e8..9112919e2 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/StatusDisplayOptions.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/StatusDisplayOptions.kt @@ -1,22 +1,22 @@ package com.keylesspalace.tusky.util data class StatusDisplayOptions( - @get:JvmName("animateAvatars") - val animateAvatars: Boolean, - @get:JvmName("mediaPreviewEnabled") - val mediaPreviewEnabled: Boolean, - @get:JvmName("useAbsoluteTime") - val useAbsoluteTime: Boolean, - @get:JvmName("showBotOverlay") - val showBotOverlay: Boolean, - @get:JvmName("useBlurhash") - val useBlurhash: Boolean, - @get:JvmName("cardViewMode") - val cardViewMode: CardViewMode, - @get:JvmName("confirmReblogs") - val confirmReblogs: Boolean, - @get:JvmName("hideStats") - val hideStats: Boolean, - @get:JvmName("animateEmojis") - val animateEmojis: Boolean -) \ No newline at end of file + @get:JvmName("animateAvatars") + val animateAvatars: Boolean, + @get:JvmName("mediaPreviewEnabled") + val mediaPreviewEnabled: Boolean, + @get:JvmName("useAbsoluteTime") + val useAbsoluteTime: Boolean, + @get:JvmName("showBotOverlay") + val showBotOverlay: Boolean, + @get:JvmName("useBlurhash") + val useBlurhash: Boolean, + @get:JvmName("cardViewMode") + val cardViewMode: CardViewMode, + @get:JvmName("confirmReblogs") + val confirmReblogs: Boolean, + @get:JvmName("hideStats") + val hideStats: Boolean, + @get:JvmName("animateEmojis") + val animateEmojis: Boolean +) diff --git a/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt index 822100290..00f6699da 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt @@ -34,7 +34,8 @@ import com.keylesspalace.tusky.viewdata.buildDescription import com.keylesspalace.tusky.viewdata.calculatePercent import java.text.NumberFormat import java.text.SimpleDateFormat -import java.util.* +import java.util.Date +import java.util.Locale import kotlin.math.min class StatusViewHelper(private val itemView: View) { @@ -47,25 +48,28 @@ class StatusViewHelper(private val itemView: View) { private val longSdf = SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault()) fun setMediasPreview( - statusDisplayOptions: StatusDisplayOptions, - attachments: List, - sensitive: Boolean, - previewListener: MediaPreviewListener, - showingContent: Boolean, - mediaPreviewHeight: Int) { + statusDisplayOptions: StatusDisplayOptions, + attachments: List, + sensitive: Boolean, + previewListener: MediaPreviewListener, + showingContent: Boolean, + mediaPreviewHeight: Int + ) { val context = itemView.context val mediaPreviews = arrayOf( - itemView.findViewById(R.id.status_media_preview_0), - itemView.findViewById(R.id.status_media_preview_1), - itemView.findViewById(R.id.status_media_preview_2), - itemView.findViewById(R.id.status_media_preview_3)) + itemView.findViewById(R.id.status_media_preview_0), + itemView.findViewById(R.id.status_media_preview_1), + itemView.findViewById(R.id.status_media_preview_2), + itemView.findViewById(R.id.status_media_preview_3) + ) val mediaOverlays = arrayOf( - itemView.findViewById(R.id.status_media_overlay_0), - itemView.findViewById(R.id.status_media_overlay_1), - itemView.findViewById(R.id.status_media_overlay_2), - itemView.findViewById(R.id.status_media_overlay_3)) + itemView.findViewById(R.id.status_media_overlay_0), + itemView.findViewById(R.id.status_media_overlay_1), + itemView.findViewById(R.id.status_media_overlay_2), + itemView.findViewById(R.id.status_media_overlay_3) + ) val sensitiveMediaWarning = itemView.findViewById(R.id.status_sensitive_media_warning) val sensitiveMediaShow = itemView.findViewById(R.id.status_sensitive_media_button) @@ -85,7 +89,6 @@ class StatusViewHelper(private val itemView: View) { return } - val mediaPreviewUnloaded = ColorDrawable(ThemeUtils.getColor(context, R.attr.colorBackgroundAccent)) val n = min(attachments.size, Status.MAX_MEDIA_ATTACHMENTS) @@ -105,9 +108,9 @@ class StatusViewHelper(private val itemView: View) { if (TextUtils.isEmpty(previewUrl)) { Glide.with(mediaPreviews[i]) - .load(mediaPreviewUnloaded) - .centerInside() - .into(mediaPreviews[i]) + .load(mediaPreviewUnloaded) + .centerInside() + .into(mediaPreviews[i]) } else { val placeholder = if (attachment.blurhash != null) decodeBlurHash(context, attachment.blurhash) @@ -119,19 +122,19 @@ class StatusViewHelper(private val itemView: View) { mediaPreviews[i].setFocalPoint(focus) Glide.with(mediaPreviews[i]) - .load(previewUrl) - .placeholder(placeholder) - .centerInside() - .addListener(mediaPreviews[i]) - .into(mediaPreviews[i]) + .load(previewUrl) + .placeholder(placeholder) + .centerInside() + .addListener(mediaPreviews[i]) + .into(mediaPreviews[i]) } else { mediaPreviews[i].removeFocalPoint() Glide.with(mediaPreviews[i]) - .load(previewUrl) - .placeholder(placeholder) - .centerInside() - .into(mediaPreviews[i]) + .load(previewUrl) + .placeholder(placeholder) + .centerInside() + .into(mediaPreviews[i]) } } else { mediaPreviews[i].removeFocalPoint() @@ -145,8 +148,9 @@ class StatusViewHelper(private val itemView: View) { } val type = attachment.type - if (showingContent - && (type === Attachment.Type.VIDEO) or (type === Attachment.Type.GIFV)) { + if (showingContent && + (type === Attachment.Type.VIDEO) or (type === Attachment.Type.GIFV) + ) { mediaOverlays[i].visibility = View.VISIBLE } else { mediaOverlays[i].visibility = View.GONE @@ -170,7 +174,7 @@ class StatusViewHelper(private val itemView: View) { sensitiveMediaWarning.visibility = View.GONE sensitiveMediaShow.visibility = View.GONE } else { - sensitiveMediaWarning.text = if (sensitive) { + sensitiveMediaWarning.text = if (sensitive) { context.getString(R.string.status_sensitive_media_title) } else { context.getString(R.string.status_media_hidden_title) @@ -182,15 +186,19 @@ class StatusViewHelper(private val itemView: View) { previewListener.onContentHiddenChange(false) v.visibility = View.GONE sensitiveMediaWarning.visibility = View.VISIBLE - setMediasPreview(statusDisplayOptions, attachments, sensitive, previewListener, - false, mediaPreviewHeight) + setMediasPreview( + statusDisplayOptions, attachments, sensitive, previewListener, + false, mediaPreviewHeight + ) } sensitiveMediaWarning.setOnClickListener { v -> previewListener.onContentHiddenChange(true) v.visibility = View.GONE sensitiveMediaShow.visibility = View.VISIBLE - setMediasPreview(statusDisplayOptions, attachments, sensitive, previewListener, - true, mediaPreviewHeight) + setMediasPreview( + statusDisplayOptions, attachments, sensitive, previewListener, + true, mediaPreviewHeight + ) } } @@ -200,8 +208,12 @@ class StatusViewHelper(private val itemView: View) { } } - private fun setMediaLabel(mediaLabel: TextView, attachments: List, sensitive: Boolean, - listener: MediaPreviewListener) { + private fun setMediaLabel( + mediaLabel: TextView, + attachments: List, + sensitive: Boolean, + listener: MediaPreviewListener + ) { if (attachments.isEmpty()) { mediaLabel.visibility = View.GONE return @@ -245,10 +257,11 @@ class StatusViewHelper(private val itemView: View) { fun setupPollReadonly(poll: PollViewData?, emojis: List, statusDisplayOptions: StatusDisplayOptions) { val pollResults = listOf( - itemView.findViewById(R.id.status_poll_option_result_0), - itemView.findViewById(R.id.status_poll_option_result_1), - itemView.findViewById(R.id.status_poll_option_result_2), - itemView.findViewById(R.id.status_poll_option_result_3)) + itemView.findViewById(R.id.status_poll_option_result_0), + itemView.findViewById(R.id.status_poll_option_result_1), + itemView.findViewById(R.id.status_poll_option_result_2), + itemView.findViewById(R.id.status_poll_option_result_3) + ) val pollDescription = itemView.findViewById(R.id.status_poll_description) @@ -260,7 +273,6 @@ class StatusViewHelper(private val itemView: View) { } else { val timestamp = System.currentTimeMillis() - setupPollResult(poll, emojis, pollResults, statusDisplayOptions.animateEmojis) pollDescription.visibility = View.VISIBLE @@ -271,7 +283,7 @@ class StatusViewHelper(private val itemView: View) { private fun getPollInfoText(timestamp: Long, poll: PollViewData, pollDescription: TextView, useAbsoluteTime: Boolean): CharSequence { val context = pollDescription.context - val votesText = if(poll.votersCount == null) { + val votesText = if (poll.votersCount == null) { val votes = NumberFormat.getNumberInstance().format(poll.votesCount.toLong()) context.resources.getQuantityString(R.plurals.poll_info_votes, poll.votesCount, votes) } else { @@ -291,7 +303,6 @@ class StatusViewHelper(private val itemView: View) { return context.getString(R.string.poll_info_format, votesText, pollDurationInfo) } - private fun setupPollResult(poll: PollViewData, emojis: List, pollResults: List, animateEmojis: Boolean) { val options = poll.options @@ -306,7 +317,6 @@ class StatusViewHelper(private val itemView: View) { val level = percent * 100 pollResults[i].background.level = level - } else { pollResults[i].visibility = View.GONE } @@ -329,4 +339,4 @@ class StatusViewHelper(private val itemView: View) { val COLLAPSE_INPUT_FILTER = arrayOf(SmartLengthInputFilter) val NO_INPUT_FILTER = arrayOfNulls(0) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/StringUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/StringUtils.kt index 23423b591..57b87f92c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/StringUtils.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/StringUtils.kt @@ -3,8 +3,7 @@ package com.keylesspalace.tusky.util import android.text.Spanned -import java.util.* - +import java.util.Random private const val POSSIBLE_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" @@ -30,7 +29,6 @@ fun String.inc(): String { return String(builder) } - /** * "Decrement" string so that during sorting it's smaller than [this]. */ @@ -97,4 +95,4 @@ fun Spanned.trimTrailingWhitespace(): Spanned { */ fun CharSequence.unicodeWrap(): String { return "\u2068${this}\u2069" -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ViewBindingExtensions.kt b/app/src/main/java/com/keylesspalace/tusky/util/ViewBindingExtensions.kt index 5fa80fcd4..e2db79c6e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ViewBindingExtensions.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/ViewBindingExtensions.kt @@ -16,35 +16,35 @@ import kotlin.reflect.KProperty */ inline fun AppCompatActivity.viewBinding( - crossinline bindingInflater: (LayoutInflater) -> T + crossinline bindingInflater: (LayoutInflater) -> T ) = lazy(LazyThreadSafetyMode.NONE) { bindingInflater(layoutInflater) } class FragmentViewBindingDelegate( - val fragment: Fragment, - val viewBindingFactory: (View) -> T + val fragment: Fragment, + val viewBindingFactory: (View) -> T ) : ReadOnlyProperty { private var binding: T? = null init { fragment.lifecycle.addObserver( - object : DefaultLifecycleObserver { - override fun onCreate(owner: LifecycleOwner) { - fragment.viewLifecycleOwnerLiveData.observe( - fragment, - { t -> - t?.lifecycle?.addObserver( - object : DefaultLifecycleObserver { - override fun onDestroy(owner: LifecycleOwner) { - binding = null - } - } - ) + object : DefaultLifecycleObserver { + override fun onCreate(owner: LifecycleOwner) { + fragment.viewLifecycleOwnerLiveData.observe( + fragment, + { t -> + t?.lifecycle?.addObserver( + object : DefaultLifecycleObserver { + override fun onDestroy(owner: LifecycleOwner) { + binding = null + } } - ) - } + ) + } + ) } + } ) } @@ -64,4 +64,4 @@ class FragmentViewBindingDelegate( } fun Fragment.viewBinding(viewBindingFactory: (View) -> T) = - FragmentViewBindingDelegate(this, viewBindingFactory) + FragmentViewBindingDelegate(this, viewBindingFactory) diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.kt index 21e522f03..9b86db6d7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.kt @@ -20,8 +20,6 @@ import com.keylesspalace.tusky.entity.Notification import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.viewdata.NotificationViewData import com.keylesspalace.tusky.viewdata.StatusViewData -import com.keylesspalace.tusky.viewdata.toViewData -import java.util.* @JvmName("statusToViewData") fun Status.toViewData( @@ -50,4 +48,4 @@ fun Notification.toViewData( this.account, this.status?.toViewData(alwaysShowSensitiveData, alwaysOpenSpoiler) ) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ViewExtensions.kt b/app/src/main/java/com/keylesspalace/tusky/util/ViewExtensions.kt index 389995ae2..07a9539f5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ViewExtensions.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/ViewExtensions.kt @@ -45,7 +45,8 @@ open class DefaultTextWatcher : TextWatcher { } inline fun EditText.onTextChanged( - crossinline callback: (s: CharSequence, start: Int, before: Int, count: Int) -> Unit) { + crossinline callback: (s: CharSequence, start: Int, before: Int, count: Int) -> Unit +) { addTextChangedListener(object : DefaultTextWatcher() { override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { callback(s, start, before, count) @@ -54,10 +55,11 @@ inline fun EditText.onTextChanged( } inline fun EditText.afterTextChanged( - crossinline callback: (s: Editable) -> Unit) { + crossinline callback: (s: Editable) -> Unit +) { addTextChangedListener(object : DefaultTextWatcher() { override fun afterTextChanged(s: Editable) { callback(s) } }) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/view/BackgroundMessageView.kt b/app/src/main/java/com/keylesspalace/tusky/view/BackgroundMessageView.kt index 32a7d6b31..82860f9fc 100644 --- a/app/src/main/java/com/keylesspalace/tusky/view/BackgroundMessageView.kt +++ b/app/src/main/java/com/keylesspalace/tusky/view/BackgroundMessageView.kt @@ -17,9 +17,9 @@ import com.keylesspalace.tusky.util.visible * Can show an image, text and button below them. */ class BackgroundMessageView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 ) : LinearLayout(context, attrs, defStyleAttr) { private val binding = ViewBackgroundMessageBinding.inflate(LayoutInflater.from(context), this) @@ -38,13 +38,13 @@ class BackgroundMessageView @JvmOverloads constructor( * If [clickListener] is `null` then the button will be hidden. */ fun setup( - @DrawableRes imageRes: Int, - @StringRes messageRes: Int, - clickListener: ((v: View) -> Unit)? = null + @DrawableRes imageRes: Int, + @StringRes messageRes: Int, + clickListener: ((v: View) -> Unit)? = null ) { binding.messageTextView.setText(messageRes) binding.imageView.setImageResource(imageRes) binding.button.setOnClickListener(clickListener) binding.button.visible(clickListener != null) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/view/ConversationLineItemDecoration.kt b/app/src/main/java/com/keylesspalace/tusky/view/ConversationLineItemDecoration.kt index 7be8d06e5..c291bd019 100644 --- a/app/src/main/java/com/keylesspalace/tusky/view/ConversationLineItemDecoration.kt +++ b/app/src/main/java/com/keylesspalace/tusky/view/ConversationLineItemDecoration.kt @@ -18,10 +18,9 @@ package com.keylesspalace.tusky.view import android.content.Context import android.graphics.Canvas import android.graphics.drawable.Drawable -import androidx.recyclerview.widget.RecyclerView import android.view.View import androidx.core.content.ContextCompat - +import androidx.recyclerview.widget.RecyclerView import com.keylesspalace.tusky.R import com.keylesspalace.tusky.adapter.ThreadAdapter @@ -54,7 +53,8 @@ class ConversationLineItemDecoration(private val context: Context) : RecyclerVie } val below = adapter.getItem(position + 1) dividerBottom = if (below != null && current.id == below.status.inReplyToId && - adapter.detailedStatusPosition != position) { + adapter.detailedStatusPosition != position + ) { child.bottom } else { child.top + avatarMargin @@ -66,7 +66,6 @@ class ConversationLineItemDecoration(private val context: Context) : RecyclerVie divider.setBounds(canvas.width - dividerEnd, dividerTop, canvas.width - dividerStart, dividerBottom) } divider.draw(canvas) - } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/view/EmojiPicker.kt b/app/src/main/java/com/keylesspalace/tusky/view/EmojiPicker.kt index 09e648adf..0a9bfbf7e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/view/EmojiPicker.kt +++ b/app/src/main/java/com/keylesspalace/tusky/view/EmojiPicker.kt @@ -6,8 +6,8 @@ import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView class EmojiPicker @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null + context: Context, + attrs: AttributeSet? = null ) : RecyclerView(context, attrs) { init { diff --git a/app/src/main/java/com/keylesspalace/tusky/view/ExposedPlayPauseVideoView.kt b/app/src/main/java/com/keylesspalace/tusky/view/ExposedPlayPauseVideoView.kt index ec748e040..444e71dc1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/view/ExposedPlayPauseVideoView.kt +++ b/app/src/main/java/com/keylesspalace/tusky/view/ExposedPlayPauseVideoView.kt @@ -5,10 +5,11 @@ import android.util.AttributeSet import android.widget.VideoView class ExposedPlayPauseVideoView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0) - : VideoView(context, attrs, defStyleAttr) { + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : + VideoView(context, attrs, defStyleAttr) { private var listener: PlayPauseListener? = null @@ -30,4 +31,4 @@ class ExposedPlayPauseVideoView @JvmOverloads constructor( fun onPlay() fun onPause() } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/view/LicenseCard.kt b/app/src/main/java/com/keylesspalace/tusky/view/LicenseCard.kt index ad9ae52c2..116f01703 100644 --- a/app/src/main/java/com/keylesspalace/tusky/view/LicenseCard.kt +++ b/app/src/main/java/com/keylesspalace/tusky/view/LicenseCard.kt @@ -27,9 +27,9 @@ import com.keylesspalace.tusky.util.hide class LicenseCard @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 ) : MaterialCardView(context, attrs, defStyleAttr) { init { @@ -46,14 +46,11 @@ class LicenseCard binding.licenseCardName.text = name binding.licenseCardLicense.text = license - if(link.isNullOrBlank()) { + if (link.isNullOrBlank()) { binding.licenseCardLink.hide() } else { binding.licenseCardLink.text = link setOnClickListener { LinkHelper.openLink(link, context) } } - } - } - diff --git a/app/src/main/java/com/keylesspalace/tusky/view/MediaPreviewImageView.kt b/app/src/main/java/com/keylesspalace/tusky/view/MediaPreviewImageView.kt index 42bfc276c..8922fafd5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/view/MediaPreviewImageView.kt +++ b/app/src/main/java/com/keylesspalace/tusky/view/MediaPreviewImageView.kt @@ -24,7 +24,6 @@ import com.bumptech.glide.load.engine.GlideException import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.target.Target import com.keylesspalace.tusky.entity.Attachment - import com.keylesspalace.tusky.util.FocalPointUtil /** @@ -40,10 +39,10 @@ import com.keylesspalace.tusky.util.FocalPointUtil */ class MediaPreviewImageView @JvmOverloads constructor( -context: Context, -attrs: AttributeSet? = null, -defStyleAttr: Int = 0 -) : AppCompatImageView(context, attrs, defStyleAttr),RequestListener { + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : AppCompatImageView(context, attrs, defStyleAttr), RequestListener { private var focus: Attachment.Focus? = null private var focalMatrix: Matrix? = null @@ -106,7 +105,6 @@ defStyleAttr: Int = 0 return false } - /** * Called when the size of the view changes, it calls the FocalPointUtil to update the * matrix if we have a set focal point. It then reassigns the matrix to this imageView. @@ -120,9 +118,11 @@ defStyleAttr: Int = 0 private fun recalculateMatrix(width: Int, height: Int, drawable: Drawable?) { if (drawable != null && focus != null && focalMatrix != null) { scaleType = ScaleType.MATRIX - FocalPointUtil.updateFocalPointMatrix(width.toFloat(), height.toFloat(), - drawable.intrinsicWidth.toFloat(), drawable.intrinsicHeight.toFloat(), - focus as Attachment.Focus, focalMatrix as Matrix) + FocalPointUtil.updateFocalPointMatrix( + width.toFloat(), height.toFloat(), + drawable.intrinsicWidth.toFloat(), drawable.intrinsicHeight.toFloat(), + focus as Attachment.Focus, focalMatrix as Matrix + ) imageMatrix = focalMatrix } } diff --git a/app/src/main/java/com/keylesspalace/tusky/view/MuteAccountDialog.kt b/app/src/main/java/com/keylesspalace/tusky/view/MuteAccountDialog.kt index 022927e61..715fa6033 100644 --- a/app/src/main/java/com/keylesspalace/tusky/view/MuteAccountDialog.kt +++ b/app/src/main/java/com/keylesspalace/tusky/view/MuteAccountDialog.kt @@ -17,20 +17,20 @@ fun showMuteAccountDialog( binding.checkbox.isChecked = true AlertDialog.Builder(activity) - .setView(binding.root) - .setPositiveButton(android.R.string.ok) { _, _ -> - val durationValues = activity.resources.getIntArray(R.array.mute_duration_values) + .setView(binding.root) + .setPositiveButton(android.R.string.ok) { _, _ -> + val durationValues = activity.resources.getIntArray(R.array.mute_duration_values) - // workaround to make indefinite muting work with Mastodon 3.3.0 - // https://github.com/tuskyapp/Tusky/issues/2107 - val duration = if(binding.duration.selectedItemPosition == 0) { - null - } else { - durationValues[binding.duration.selectedItemPosition] - } - - onOk(binding.checkbox.isChecked, duration) + // workaround to make indefinite muting work with Mastodon 3.3.0 + // https://github.com/tuskyapp/Tusky/issues/2107 + val duration = if (binding.duration.selectedItemPosition == 0) { + null + } else { + durationValues[binding.duration.selectedItemPosition] } - .setNegativeButton(android.R.string.cancel, null) - .show() -} \ No newline at end of file + + onOk(binding.checkbox.isChecked, duration) + } + .setNegativeButton(android.R.string.cancel, null) + .show() +} diff --git a/app/src/main/java/com/keylesspalace/tusky/view/SquareImageView.kt b/app/src/main/java/com/keylesspalace/tusky/view/SquareImageView.kt index d0e730522..d7e753bbb 100644 --- a/app/src/main/java/com/keylesspalace/tusky/view/SquareImageView.kt +++ b/app/src/main/java/com/keylesspalace/tusky/view/SquareImageView.kt @@ -1,8 +1,8 @@ package com.keylesspalace.tusky.view import android.content.Context -import androidx.appcompat.widget.AppCompatImageView import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatImageView /** * Created by charlag on 26/10/2017. @@ -13,12 +13,12 @@ class SquareImageView : AppCompatImageView { constructor(context: Context, attributes: AttributeSet) : super(context, attributes) - constructor(context: Context, attributes: AttributeSet, defStyleAttr: Int) - : super(context, attributes, defStyleAttr) + constructor(context: Context, attributes: AttributeSet, defStyleAttr: Int) : + super(context, attributes, defStyleAttr) override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) val width = measuredWidth setMeasuredDimension(width, width) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/AttachmentViewData.kt b/app/src/main/java/com/keylesspalace/tusky/viewdata/AttachmentViewData.kt index f2e42e404..b0a8062f6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewdata/AttachmentViewData.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/AttachmentViewData.kt @@ -7,9 +7,9 @@ import kotlinx.parcelize.Parcelize @Parcelize data class AttachmentViewData( - val attachment: Attachment, - val statusId: String, - val statusUrl: String + val attachment: Attachment, + val statusId: String, + val statusUrl: String ) : Parcelable { companion object { @JvmStatic @@ -20,4 +20,4 @@ data class AttachmentViewData( } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/PollViewData.kt b/app/src/main/java/com/keylesspalace/tusky/viewdata/PollViewData.kt index b6eefd713..0cd73bc98 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewdata/PollViewData.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/PollViewData.kt @@ -22,24 +22,24 @@ import androidx.core.text.parseAsHtml import com.keylesspalace.tusky.R import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.entity.PollOption -import java.util.* +import java.util.Date import kotlin.math.roundToInt data class PollViewData( - val id: String, - val expiresAt: Date?, - val expired: Boolean, - val multiple: Boolean, - val votesCount: Int, - val votersCount: Int?, - val options: List, - var voted: Boolean + val id: String, + val expiresAt: Date?, + val expired: Boolean, + val multiple: Boolean, + val votesCount: Int, + val votersCount: Int?, + val options: List, + var voted: Boolean ) data class PollOptionViewData( - val title: String, - var votesCount: Int, - var selected: Boolean + val title: String, + var votesCount: Int, + var selected: Boolean ) fun calculatePercent(fraction: Int, totalVoters: Int?, totalVotes: Int): Int { @@ -60,21 +60,21 @@ fun buildDescription(title: String, percent: Int, context: Context): Spanned { fun Poll?.toViewData(): PollViewData? { if (this == null) return null return PollViewData( - id = id, - expiresAt = expiresAt, - expired = expired, - multiple = multiple, - votesCount = votesCount, - votersCount = votersCount, - options = options.map { it.toViewData() }, - voted = voted + id = id, + expiresAt = expiresAt, + expired = expired, + multiple = multiple, + votesCount = votesCount, + votersCount = votersCount, + options = options.map { it.toViewData() }, + voted = voted ) } fun PollOption.toViewData(): PollOptionViewData { return PollOptionViewData( - title = title, - votesCount = votesCount, - selected = false + title = title, + votesCount = votesCount, + selected = false ) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountViewModel.kt index 129959f92..5d8fcd32b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountViewModel.kt @@ -2,14 +2,25 @@ package com.keylesspalace.tusky.viewmodel import android.util.Log import androidx.lifecycle.MutableLiveData -import com.keylesspalace.tusky.appstore.* +import com.keylesspalace.tusky.appstore.BlockEvent +import com.keylesspalace.tusky.appstore.DomainMuteEvent +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.MuteEvent +import com.keylesspalace.tusky.appstore.ProfileEditedEvent +import com.keylesspalace.tusky.appstore.UnfollowEvent import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Field import com.keylesspalace.tusky.entity.IdentityProof import com.keylesspalace.tusky.entity.Relationship import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.util.* +import com.keylesspalace.tusky.util.Either +import com.keylesspalace.tusky.util.Error +import com.keylesspalace.tusky.util.Loading +import com.keylesspalace.tusky.util.Resource +import com.keylesspalace.tusky.util.RxAwareViewModel +import com.keylesspalace.tusky.util.Success +import com.keylesspalace.tusky.util.combineOptionalLiveData import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.disposables.Disposable import retrofit2.Call @@ -19,9 +30,9 @@ import java.util.concurrent.TimeUnit import javax.inject.Inject class AccountViewModel @Inject constructor( - private val mastodonApi: MastodonApi, - private val eventHub: EventHub, - private val accountManager: AccountManager + private val mastodonApi: MastodonApi, + private val eventHub: EventHub, + private val accountManager: AccountManager ) : RxAwareViewModel() { val accountData = MutableLiveData>() @@ -33,7 +44,7 @@ class AccountViewModel @Inject constructor( val accountFieldData = combineOptionalLiveData(accountData, identityProofData) { accountRes, identityProofs -> identityProofs.orEmpty().map { Either.Left(it) } - .plus(accountRes?.data?.fields.orEmpty().map { Either.Right(it) }) + .plus(accountRes?.data?.fields.orEmpty().map { Either.Right(it) }) } val isRefreshing = MutableLiveData().apply { value = false } @@ -46,11 +57,11 @@ class AccountViewModel @Inject constructor( init { eventHub.events - .subscribe { event -> - if (event is ProfileEditedEvent && event.newProfileData.id == accountData.value?.data?.id) { - accountData.postValue(Success(event.newProfileData)) - } - }.autoDispose() + .subscribe { event -> + if (event is ProfileEditedEvent && event.newProfileData.id == accountData.value?.data?.id) { + accountData.postValue(Success(event.newProfileData)) + } + }.autoDispose() } private fun obtainAccount(reload: Boolean = false) { @@ -59,17 +70,20 @@ class AccountViewModel @Inject constructor( accountData.postValue(Loading()) mastodonApi.account(accountId) - .subscribe({ account -> + .subscribe( + { account -> accountData.postValue(Success(account)) isDataLoading = false isRefreshing.postValue(false) - }, {t -> + }, + { t -> Log.w(TAG, "failed obtaining account", t) accountData.postValue(Error()) isDataLoading = false isRefreshing.postValue(false) - }) - .autoDispose() + } + ) + .autoDispose() } } @@ -79,13 +93,16 @@ class AccountViewModel @Inject constructor( relationshipData.postValue(Loading()) mastodonApi.relationships(listOf(accountId)) - .subscribe({ relationships -> + .subscribe( + { relationships -> relationshipData.postValue(Success(relationships[0])) - }, { t -> + }, + { t -> Log.w(TAG, "failed obtaining relationships", t) relationshipData.postValue(Error()) - }) - .autoDispose() + } + ) + .autoDispose() } } @@ -93,12 +110,15 @@ class AccountViewModel @Inject constructor( if (identityProofData.value == null || reload) { mastodonApi.identityProofs(accountId) - .subscribe({ proofs -> + .subscribe( + { proofs -> identityProofData.postValue(proofs) - }, { t -> + }, + { t -> Log.w(TAG, "failed obtaining identity proofs", t) - }) - .autoDispose() + } + ) + .autoDispose() } } @@ -126,11 +146,12 @@ class AccountViewModel @Inject constructor( fun unmuteAccount() { changeRelationship(RelationShipAction.UNMUTE) } - + fun changeSubscribingState() { val relationship = relationshipData.value?.data - if(relationship?.notifying == true /* Mastodon 3.3.0rc1 */ - || relationship?.subscribing == true /* Pleroma */ ) { + if (relationship?.notifying == true || /* Mastodon 3.3.0rc1 */ + relationship?.subscribing == true /* Pleroma */ + ) { changeRelationship(RelationShipAction.UNSUBSCRIBE) } else { changeRelationship(RelationShipAction.SUBSCRIBE) @@ -138,12 +159,12 @@ class AccountViewModel @Inject constructor( } fun blockDomain(instance: String) { - mastodonApi.blockDomain(instance).enqueue(object: Callback { + mastodonApi.blockDomain(instance).enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { if (response.isSuccessful) { eventHub.dispatch(DomainMuteEvent(instance)) val relation = relationshipData.value?.data - if(relation != null) { + if (relation != null) { relationshipData.postValue(Success(relation.copy(blockingDomain = true))) } } else { @@ -158,11 +179,11 @@ class AccountViewModel @Inject constructor( } fun unblockDomain(instance: String) { - mastodonApi.unblockDomain(instance).enqueue(object: Callback { + mastodonApi.unblockDomain(instance).enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { if (response.isSuccessful) { val relation = relationshipData.value?.data - if(relation != null) { + if (relation != null) { relationshipData.postValue(Success(relation.copy(blockingDomain = false))) } } else { @@ -209,12 +230,12 @@ class AccountViewModel @Inject constructor( RelationShipAction.MUTE -> relation.copy(muting = true) RelationShipAction.UNMUTE -> relation.copy(muting = false) RelationShipAction.SUBSCRIBE -> { - if(isMastodon) + if (isMastodon) relation.copy(notifying = true) else relation.copy(subscribing = true) } RelationShipAction.UNSUBSCRIBE -> { - if(isMastodon) + if (isMastodon) relation.copy(notifying = false) else relation.copy(subscribing = false) } @@ -230,50 +251,53 @@ class AccountViewModel @Inject constructor( RelationShipAction.MUTE -> mastodonApi.muteAccount(accountId, parameter ?: true, duration) RelationShipAction.UNMUTE -> mastodonApi.unmuteAccount(accountId) RelationShipAction.SUBSCRIBE -> { - if(isMastodon) + if (isMastodon) mastodonApi.followAccount(accountId, notify = true) else mastodonApi.subscribeAccount(accountId) } RelationShipAction.UNSUBSCRIBE -> { - if(isMastodon) + if (isMastodon) mastodonApi.followAccount(accountId, notify = false) else mastodonApi.unsubscribeAccount(accountId) } }.subscribe( - { relationship -> - relationshipData.postValue(Success(relationship)) + { relationship -> + relationshipData.postValue(Success(relationship)) - when (relationshipAction) { - RelationShipAction.UNFOLLOW -> eventHub.dispatch(UnfollowEvent(accountId)) - RelationShipAction.BLOCK -> eventHub.dispatch(BlockEvent(accountId)) - RelationShipAction.MUTE -> eventHub.dispatch(MuteEvent(accountId)) - else -> { - } + when (relationshipAction) { + RelationShipAction.UNFOLLOW -> eventHub.dispatch(UnfollowEvent(accountId)) + RelationShipAction.BLOCK -> eventHub.dispatch(BlockEvent(accountId)) + RelationShipAction.MUTE -> eventHub.dispatch(MuteEvent(accountId)) + else -> { } - }, - { - relationshipData.postValue(Error(relation)) } + }, + { + relationshipData.postValue(Error(relation)) + } ) - .autoDispose() + .autoDispose() } fun noteChanged(newNote: String) { noteSaved.postValue(false) noteDisposable?.dispose() noteDisposable = Single.timer(1500, TimeUnit.MILLISECONDS) - .flatMap { - mastodonApi.updateAccountNote(accountId, newNote) - } - .doOnSuccess { - noteSaved.postValue(true) - } - .delay(4, TimeUnit.SECONDS) - .subscribe({ + .flatMap { + mastodonApi.updateAccountNote(accountId, newNote) + } + .doOnSuccess { + noteSaved.postValue(true) + } + .delay(4, TimeUnit.SECONDS) + .subscribe( + { noteSaved.postValue(false) - }, { + }, + { Log.e(TAG, "Error updating note", it) - }) + } + ) } override fun onCleared() { diff --git a/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountsInListViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountsInListViewModel.kt index e65decae6..b02c2ac09 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountsInListViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountsInListViewModel.kt @@ -38,39 +38,52 @@ class AccountsInListViewModel @Inject constructor(private val api: MastodonApi) fun load(listId: String) { val state = _state.value!! if (state.accounts.isLeft() || state.accounts.asRight().isEmpty()) { - api.getAccountsInList(listId, 0).subscribe({ accounts -> - updateState { copy(accounts = Right(accounts)) } - }, { e -> - updateState { copy(accounts = Left(e)) } - }).autoDispose() + api.getAccountsInList(listId, 0).subscribe( + { accounts -> + updateState { copy(accounts = Right(accounts)) } + }, + { e -> + updateState { copy(accounts = Left(e)) } + } + ).autoDispose() } } fun addAccountToList(listId: String, account: Account) { api.addCountToList(listId, listOf(account.id)) - .subscribe({ + .subscribe( + { updateState { copy(accounts = accounts.map { it + account }) } - }, { - Log.i(javaClass.simpleName, - "Failed to add account to the list: ${account.username}") - }) - .autoDispose() + }, + { + Log.i( + javaClass.simpleName, + "Failed to add account to the list: ${account.username}" + ) + } + ) + .autoDispose() } fun deleteAccountFromList(listId: String, accountId: String) { api.deleteAccountFromList(listId, listOf(accountId)) - .subscribe({ + .subscribe( + { updateState { - copy(accounts = accounts.map { accounts -> - accounts.withoutFirstWhich { it.id == accountId } - }) + copy( + accounts = accounts.map { accounts -> + accounts.withoutFirstWhich { it.id == accountId } + } + ) } - }, { + }, + { Log.i(javaClass.simpleName, "Failed to remove account from thelist: $accountId") - }) - .autoDispose() + } + ) + .autoDispose() } fun search(query: String) { @@ -78,15 +91,18 @@ class AccountsInListViewModel @Inject constructor(private val api: MastodonApi) query.isEmpty() -> updateState { copy(searchResult = null) } query.isBlank() -> updateState { copy(searchResult = listOf()) } else -> api.searchAccounts(query, null, 10, true) - .subscribe({ result -> + .subscribe( + { result -> updateState { copy(searchResult = result) } - }, { + }, + { updateState { copy(searchResult = listOf()) } - }).autoDispose() + } + ).autoDispose() } } private inline fun updateState(crossinline fn: State.() -> State) { _state.onNext(fn(_state.value!!)) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt index f75dbad5d..a51fea0de 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt @@ -15,12 +15,12 @@ package com.keylesspalace.tusky.viewmodel -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel import android.content.Context import android.graphics.Bitmap import android.net.Uri import android.util.Log +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel import com.keylesspalace.tusky.EditProfileActivity.Companion.AVATAR_SIZE import com.keylesspalace.tusky.EditProfileActivity.Companion.HEADER_HEIGHT import com.keylesspalace.tusky.EditProfileActivity.Companion.HEADER_WIDTH @@ -30,16 +30,22 @@ import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Instance import com.keylesspalace.tusky.entity.StringField import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.util.* +import com.keylesspalace.tusky.util.Error +import com.keylesspalace.tusky.util.IOUtils +import com.keylesspalace.tusky.util.Loading +import com.keylesspalace.tusky.util.Resource +import com.keylesspalace.tusky.util.Success +import com.keylesspalace.tusky.util.getSampledBitmap +import com.keylesspalace.tusky.util.randomAlphanumericString import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.kotlin.addTo import io.reactivex.rxjava3.schedulers.Schedulers import okhttp3.MediaType.Companion.toMediaTypeOrNull -import okhttp3.RequestBody.Companion.asRequestBody -import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.MultipartBody import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.asRequestBody +import okhttp3.RequestBody.Companion.toRequestBody import org.json.JSONException import org.json.JSONObject import retrofit2.Call @@ -56,10 +62,10 @@ private const val AVATAR_FILE_NAME = "avatar.png" private const val TAG = "EditProfileViewModel" -class EditProfileViewModel @Inject constructor( - private val mastodonApi: MastodonApi, - private val eventHub: EventHub -): ViewModel() { +class EditProfileViewModel @Inject constructor( + private val mastodonApi: MastodonApi, + private val eventHub: EventHub +) : ViewModel() { val profileData = MutableLiveData>() val avatarData = MutableLiveData>() @@ -72,21 +78,21 @@ class EditProfileViewModel @Inject constructor( private val disposeables = CompositeDisposable() fun obtainProfile() { - if(profileData.value == null || profileData.value is Error) { + if (profileData.value == null || profileData.value is Error) { profileData.postValue(Loading()) mastodonApi.accountVerifyCredentials() - .subscribe( - {profile -> - oldProfileData = profile - profileData.postValue(Success(profile)) - }, - { - profileData.postValue(Error()) - }) - .addTo(disposeables) - + .subscribe( + { profile -> + oldProfileData = profile + profileData.postValue(Success(profile)) + }, + { + profileData.postValue(Error()) + } + ) + .addTo(disposeables) } } @@ -102,12 +108,14 @@ class EditProfileViewModel @Inject constructor( resizeImage(uri, context, HEADER_WIDTH, HEADER_HEIGHT, cacheFile, headerData) } - private fun resizeImage(uri: Uri, - context: Context, - resizeWidth: Int, - resizeHeight: Int, - cacheFile: File, - imageLiveData: MutableLiveData>) { + private fun resizeImage( + uri: Uri, + context: Context, + resizeWidth: Int, + resizeHeight: Int, + cacheFile: File, + imageLiveData: MutableLiveData> + ) { Single.fromCallable { val contentResolver = context.contentResolver @@ -117,13 +125,13 @@ class EditProfileViewModel @Inject constructor( throw Exception() } - //dont upscale image if its smaller than the desired size + // dont upscale image if its smaller than the desired size val bitmap = - if (sourceBitmap.width <= resizeWidth && sourceBitmap.height <= resizeHeight) { - sourceBitmap - } else { - Bitmap.createScaledBitmap(sourceBitmap, resizeWidth, resizeHeight, true) - } + if (sourceBitmap.width <= resizeWidth && sourceBitmap.height <= resizeHeight) { + sourceBitmap + } else { + Bitmap.createScaledBitmap(sourceBitmap, resizeWidth, resizeHeight, true) + } if (!saveBitmapToFile(bitmap, cacheFile)) { throw Exception() @@ -131,17 +139,20 @@ class EditProfileViewModel @Inject constructor( bitmap }.subscribeOn(Schedulers.io()) - .subscribe({ + .subscribe( + { imageLiveData.postValue(Success(it)) - }, { + }, + { imageLiveData.postValue(Error()) - }) - .addTo(disposeables) + } + ) + .addTo(disposeables) } fun save(newDisplayName: String, newNote: String, newLocked: Boolean, newFields: List, context: Context) { - if(saveData.value is Loading || profileData.value !is Success) { + if (saveData.value is Loading || profileData.value !is Success) { return } @@ -184,21 +195,23 @@ class EditProfileViewModel @Inject constructor( val field3 = calculateFieldToUpdate(newFields.getOrNull(2), fieldsUnchanged) val field4 = calculateFieldToUpdate(newFields.getOrNull(3), fieldsUnchanged) - if (displayName == null && note == null && locked == null && avatar == null && header == null - && field1 == null && field2 == null && field3 == null && field4 == null) { + if (displayName == null && note == null && locked == null && avatar == null && header == null && + field1 == null && field2 == null && field3 == null && field4 == null + ) { /** if nothing has changed, there is no need to make a network request */ saveData.postValue(Success()) return } - mastodonApi.accountUpdateCredentials(displayName, note, locked, avatar, header, - field1?.first, field1?.second, field2?.first, field2?.second, field3?.first, field3?.second, field4?.first, field4?.second + mastodonApi.accountUpdateCredentials( + displayName, note, locked, avatar, header, + field1?.first, field1?.second, field2?.first, field2?.second, field3?.first, field3?.second, field4?.first, field4?.second ).enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { val newProfileData = response.body() if (!response.isSuccessful || newProfileData == null) { val errorResponse = response.errorBody()?.string() - val errorMsg = if(!errorResponse.isNullOrBlank()) { + val errorMsg = if (!errorResponse.isNullOrBlank()) { try { JSONObject(errorResponse).optString("error", null) } catch (e: JSONException) { @@ -218,29 +231,28 @@ class EditProfileViewModel @Inject constructor( saveData.postValue(Error()) } }) - } // cache activity state for rotation change fun updateProfile(newDisplayName: String, newNote: String, newLocked: Boolean, newFields: List) { - if(profileData.value is Success) { + if (profileData.value is Success) { val newProfileSource = profileData.value?.data?.source?.copy(note = newNote, fields = newFields) - val newProfile = profileData.value?.data?.copy(displayName = newDisplayName, - locked = newLocked, source = newProfileSource) + val newProfile = profileData.value?.data?.copy( + displayName = newDisplayName, + locked = newLocked, source = newProfileSource + ) profileData.postValue(Success(newProfile)) } - } - private fun calculateFieldToUpdate(newField: StringField?, fieldsUnchanged: Boolean): Pair? { - if(fieldsUnchanged || newField == null) { + if (fieldsUnchanged || newField == null) { return null } return Pair( - newField.name.toRequestBody(MultipartBody.FORM), - newField.value.toRequestBody(MultipartBody.FORM) + newField.name.toRequestBody(MultipartBody.FORM), + newField.value.toRequestBody(MultipartBody.FORM) ) } @@ -270,19 +282,18 @@ class EditProfileViewModel @Inject constructor( } fun obtainInstance() { - if(instanceData.value == null || instanceData.value is Error) { + if (instanceData.value == null || instanceData.value is Error) { instanceData.postValue(Loading()) mastodonApi.getInstance().subscribe( - { instance -> - instanceData.postValue(Success(instance)) - }, - { - instanceData.postValue(Error()) - }) - .addTo(disposeables) + { instance -> + instanceData.postValue(Success(instance)) + }, + { + instanceData.postValue(Error()) + } + ) + .addTo(disposeables) } } - - -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/viewmodel/ListsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/viewmodel/ListsViewModel.kt index 650636e61..682631555 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewmodel/ListsViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewmodel/ListsViewModel.kt @@ -55,49 +55,63 @@ internal class ListsViewModel @Inject constructor(private val api: MastodonApi) copy(loadingState = LoadingState.LOADING) } - api.getLists().subscribe({ lists -> - updateState { - copy( + api.getLists().subscribe( + { lists -> + updateState { + copy( lists = lists, loadingState = LoadingState.LOADED - ) + ) + } + }, + { err -> + updateState { + copy( + loadingState = if (err is IOException || err is ConnectException) + LoadingState.ERROR_NETWORK else LoadingState.ERROR_OTHER + ) + } } - }, { err -> - updateState { - copy(loadingState = if (err is IOException || err is ConnectException) - LoadingState.ERROR_NETWORK else LoadingState.ERROR_OTHER) - } - }).autoDispose() + ).autoDispose() } fun createNewList(listName: String) { - api.createList(listName).subscribe({ list -> - updateState { - copy(lists = lists + list) + api.createList(listName).subscribe( + { list -> + updateState { + copy(lists = lists + list) + } + }, + { + sendEvent(Event.CREATE_ERROR) } - }, { - sendEvent(Event.CREATE_ERROR) - }).autoDispose() + ).autoDispose() } fun renameList(listId: String, listName: String) { - api.updateList(listId, listName).subscribe({ list -> - updateState { - copy(lists = lists.replacedFirstWhich(list) { it.id == listId }) + api.updateList(listId, listName).subscribe( + { list -> + updateState { + copy(lists = lists.replacedFirstWhich(list) { it.id == listId }) + } + }, + { + sendEvent(Event.RENAME_ERROR) } - }, { - sendEvent(Event.RENAME_ERROR) - }).autoDispose() + ).autoDispose() } fun deleteList(listId: String) { - api.deleteList(listId).subscribe({ - updateState { - copy(lists = lists.withoutFirstWhich { it.id == listId }) + api.deleteList(listId).subscribe( + { + updateState { + copy(lists = lists.withoutFirstWhich { it.id == listId }) + } + }, + { + sendEvent(Event.DELETE_ERROR) } - }, { - sendEvent(Event.DELETE_ERROR) - }).autoDispose() + ).autoDispose() } private inline fun updateState(crossinline fn: State.() -> State) { diff --git a/app/src/test/java/android/text/FakeSpannableString.kt b/app/src/test/java/android/text/SpannableString.kt similarity index 99% rename from app/src/test/java/android/text/FakeSpannableString.kt rename to app/src/test/java/android/text/SpannableString.kt index c4e4e4ccd..dc8cd831f 100644 --- a/app/src/test/java/android/text/FakeSpannableString.kt +++ b/app/src/test/java/android/text/SpannableString.kt @@ -23,7 +23,6 @@ class SpannableString(private val text: CharSequence) : Spannable { override val length: Int get() = text.length - override fun nextSpanTransition(start: Int, limit: Int, type: Class<*>?): Int { throw NotImplementedError() } @@ -47,4 +46,4 @@ class SpannableString(private val text: CharSequence) : Spannable { override fun getSpanStart(tag: Any?): Int { throw NotImplementedError() } -} \ No newline at end of file +} diff --git a/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt index 3b72d2ae2..5c77de765 100644 --- a/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt @@ -34,17 +34,20 @@ import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized import org.mockito.ArgumentMatchers -import org.mockito.Mockito.* -import java.util.* +import org.mockito.Mockito.`when` +import org.mockito.Mockito.eq +import org.mockito.Mockito.mock +import java.util.ArrayList +import java.util.Collections +import java.util.Date import java.util.concurrent.TimeUnit - class BottomSheetActivityTest { @get:Rule val instantTaskExecutorRule: InstantTaskExecutorRule = InstantTaskExecutorRule() - private lateinit var activity : FakeBottomSheetActivity + private lateinit var activity: FakeBottomSheetActivity private lateinit var apiMock: MastodonApi private val accountQuery = "http://mastodon.foo.bar/@User" private val statusQuery = "http://mastodon.foo.bar/@User/345678" @@ -52,51 +55,51 @@ class BottomSheetActivityTest { private val emptyCallback = Single.just(SearchResult(emptyList(), emptyList(), emptyList())) private val testScheduler = TestScheduler() - private val account = Account ( - "1", - "admin", - "admin", - "Ad Min", - SpannedString(""), - "http://mastodon.foo.bar", - "", - "", - false, - 0, - 0, - 0, - null, - false, - emptyList(), - emptyList() + private val account = Account( + "1", + "admin", + "admin", + "Ad Min", + SpannedString(""), + "http://mastodon.foo.bar", + "", + "", + false, + 0, + 0, + 0, + null, + false, + emptyList(), + emptyList() ) private val accountSingle = Single.just(SearchResult(listOf(account), emptyList(), emptyList())) private val status = Status( - "1", - statusQuery, - account, - null, - null, - null, - SpannedString("omgwat"), - Date(), - Collections.emptyList(), - 0, - 0, - false, - false, - false, - false, - "", - Status.Visibility.PUBLIC, - ArrayList(), - listOf(), - null, - pinned = false, - muted = false, - poll = null, - card = null + "1", + statusQuery, + account, + null, + null, + null, + SpannedString("omgwat"), + Date(), + Collections.emptyList(), + 0, + 0, + false, + false, + false, + false, + "", + Status.Visibility.PUBLIC, + ArrayList(), + listOf(), + null, + pinned = false, + muted = false, + poll = null, + card = null ) private val statusSingle = Single.just(SearchResult(emptyList(), listOf(status), emptyList())) @@ -119,7 +122,7 @@ class BottomSheetActivityTest { companion object { @Parameterized.Parameters(name = "match_{0}") @JvmStatic - fun data() : Iterable { + fun data(): Iterable { return listOf( arrayOf("https://mastodon.foo.bar/@User", true), arrayOf("http://mastodon.foo.bar/@abc123", true), diff --git a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt index 6bbdc1897..156a6b413 100644 --- a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt @@ -25,7 +25,11 @@ import com.keylesspalace.tusky.components.compose.ComposeViewModel import com.keylesspalace.tusky.components.compose.DEFAULT_CHARACTER_LIMIT import com.keylesspalace.tusky.components.compose.MediaUploader import com.keylesspalace.tusky.components.drafts.DraftHelper -import com.keylesspalace.tusky.db.* +import com.keylesspalace.tusky.db.AccountEntity +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.db.InstanceDao +import com.keylesspalace.tusky.db.InstanceEntity import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Instance @@ -34,7 +38,9 @@ import com.keylesspalace.tusky.service.ServiceClient import com.nhaarman.mockitokotlin2.any import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.core.SingleObserver -import org.junit.Assert.* +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -59,25 +65,25 @@ class ComposeActivityTest { private val instanceDomain = "example.domain" private val account = AccountEntity( - id = 1, - domain = instanceDomain, - accessToken = "token", - isActive = true, - accountId = "1", - username = "username", - displayName = "Display Name", - profilePictureUrl = "", - notificationsEnabled = true, - notificationsMentioned = true, - notificationsFollowed = true, - notificationsFollowRequested = false, - notificationsReblogged = true, - notificationsFavorited = true, - notificationSound = true, - notificationVibration = true, - notificationLight = true + id = 1, + domain = instanceDomain, + accessToken = "token", + isActive = true, + accountId = "1", + username = "username", + displayName = "Display Name", + profilePictureUrl = "", + notificationsEnabled = true, + notificationsMentioned = true, + notificationsFollowed = true, + notificationsFollowRequested = false, + notificationsReblogged = true, + notificationsFavorited = true, + notificationSound = true, + notificationVibration = true, + notificationLight = true ) - private var instanceResponseCallback: (()->Instance)? = null + private var instanceResponseCallback: (() -> Instance)? = null private var composeOptions: ComposeActivity.ComposeOptions? = null @Before @@ -90,7 +96,7 @@ class ComposeActivityTest { apiMock = mock(MastodonApi::class.java) `when`(apiMock.getCustomEmojis()).thenReturn(Single.just(emptyList())) - `when`(apiMock.getInstance()).thenReturn(object: Single() { + `when`(apiMock.getInstance()).thenReturn(object : Single() { override fun subscribeActual(observer: SingleObserver) { val instance = instanceResponseCallback?.invoke() if (instance == null) { @@ -103,19 +109,19 @@ class ComposeActivityTest { val instanceDaoMock = mock(InstanceDao::class.java) `when`(instanceDaoMock.loadMetadataForInstance(any())).thenReturn( - Single.just(InstanceEntity(instanceDomain, emptyList(),null, null, null, null)) + Single.just(InstanceEntity(instanceDomain, emptyList(), null, null, null, null)) ) val dbMock = mock(AppDatabase::class.java) `when`(dbMock.instanceDao()).thenReturn(instanceDaoMock) val viewModel = ComposeViewModel( - apiMock, - accountManagerMock, - mock(MediaUploader::class.java), - mock(ServiceClient::class.java), - mock(DraftHelper::class.java), - dbMock + apiMock, + accountManagerMock, + mock(MediaUploader::class.java), + mock(ServiceClient::class.java), + mock(DraftHelper::class.java), + dbMock ) activity.intent = Intent(activity, ComposeActivity::class.java).apply { putExtra(ComposeActivity.COMPOSE_OPTIONS_EXTRA, composeOptions) @@ -381,41 +387,38 @@ class ComposeActivityTest { activity.findViewById(R.id.composeEditField).setText(text ?: "Some text") } - private fun getInstanceWithMaximumTootCharacters(maximumTootCharacters: Int?): Instance - { + private fun getInstanceWithMaximumTootCharacters(maximumTootCharacters: Int?): Instance { return Instance( + "https://example.token", + "Example dot Token", + "Example instance for testing", + "admin@example.token", + "2.6.3", + HashMap(), + null, + null, + listOf("en"), + Account( + "1", + "admin", + "admin", + "admin", + SpannedString(""), "https://example.token", - "Example dot Token", - "Example instance for testing", - "admin@example.token", - "2.6.3", - HashMap(), + "", + "", + false, + 0, + 0, + 0, null, - null, - listOf("en"), - Account( - "1", - "admin", - "admin", - "admin", - SpannedString(""), - "https://example.token", - "", - "", - false, - 0, - 0, - 0, - null, - false, - emptyList(), - emptyList() - ), - maximumTootCharacters, - null, - null + false, + emptyList(), + emptyList() + ), + maximumTootCharacters, + null, + null ) } - } - diff --git a/app/src/test/java/com/keylesspalace/tusky/ComposeTokenizerTest.kt b/app/src/test/java/com/keylesspalace/tusky/ComposeTokenizerTest.kt index b603a4a7c..e203dde27 100644 --- a/app/src/test/java/com/keylesspalace/tusky/ComposeTokenizerTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/ComposeTokenizerTest.kt @@ -22,64 +22,66 @@ import org.junit.runner.RunWith import org.junit.runners.Parameterized @RunWith(Parameterized::class) -class ComposeTokenizerTest(private val text: CharSequence, - private val expectedStartIndex: Int, - private val expectedEndIndex: Int) { +class ComposeTokenizerTest( + private val text: CharSequence, + private val expectedStartIndex: Int, + private val expectedEndIndex: Int +) { companion object { @Parameterized.Parameters(name = "{0}") @JvmStatic fun data(): Iterable { return listOf( - arrayOf("@mention", 0, 8), - arrayOf("@ment10n", 0, 8), - arrayOf("@ment10n_", 0, 9), - arrayOf("@ment10n_n", 0, 10), - arrayOf("@ment10n_9", 0, 10), - arrayOf(" @mention", 1, 9), - arrayOf(" @ment10n", 1, 9), - arrayOf(" @ment10n_", 1, 10), - arrayOf(" @ment10n_ @", 11, 12), - arrayOf(" @ment10n_ @ment20n", 11, 19), - arrayOf(" @ment10n_ @ment20n_", 11, 20), - arrayOf(" @ment10n_ @ment20n_n", 11, 21), - arrayOf(" @ment10n_ @ment20n_9", 11, 21), - arrayOf(" @ment10n-", 1, 10), - arrayOf(" @ment10n- @", 11, 12), - arrayOf(" @ment10n- @ment20n", 11, 19), - arrayOf(" @ment10n- @ment20n-", 11, 20), - arrayOf(" @ment10n- @ment20n-n", 11, 21), - arrayOf(" @ment10n- @ment20n-9", 11, 21), - arrayOf("@ment10n@l0calhost", 0, 18), - arrayOf(" @ment10n@l0calhost", 1, 19), - arrayOf(" @ment10n_@l0calhost", 1, 20), - arrayOf(" @ment10n-@l0calhost", 1, 20), - arrayOf(" @ment10n_@l0calhost @ment20n@husky", 21, 35), - arrayOf(" @ment10n_@l0calhost @ment20n_@husky", 21, 36), - arrayOf(" @ment10n-@l0calhost @ment20n-@husky", 21, 36), - arrayOf(" @m@localhost", 1, 13), - arrayOf(" @m@localhost @a@localhost", 14, 26), - arrayOf("@m@", 0, 3), - arrayOf(" @m@ @a@asdf", 5, 12), - arrayOf(" @m@ @a@", 5, 8), - arrayOf(" @m@ @a@a", 5, 9), - arrayOf(" @m@a @a@m", 6, 10), - arrayOf("@m@m@", 5, 5), - arrayOf("#tusky@husky", 12, 12), - arrayOf(":tusky@husky", 12, 12), - arrayOf("mention", 7, 7), - arrayOf("ment10n", 7, 7), - arrayOf("mentio_", 7, 7), - arrayOf("#tusky", 0, 6), - arrayOf("#@tusky", 7, 7), - arrayOf("@#tusky", 7, 7), - arrayOf(" @#tusky", 8, 8), - arrayOf(":mastodon", 0, 9), - arrayOf(":@mastodon", 10, 10), - arrayOf("@:mastodon", 10, 10), - arrayOf(" @:mastodon", 11, 11), - arrayOf("#@:mastodon", 11, 11), - arrayOf(" #@:mastodon", 12, 12) + arrayOf("@mention", 0, 8), + arrayOf("@ment10n", 0, 8), + arrayOf("@ment10n_", 0, 9), + arrayOf("@ment10n_n", 0, 10), + arrayOf("@ment10n_9", 0, 10), + arrayOf(" @mention", 1, 9), + arrayOf(" @ment10n", 1, 9), + arrayOf(" @ment10n_", 1, 10), + arrayOf(" @ment10n_ @", 11, 12), + arrayOf(" @ment10n_ @ment20n", 11, 19), + arrayOf(" @ment10n_ @ment20n_", 11, 20), + arrayOf(" @ment10n_ @ment20n_n", 11, 21), + arrayOf(" @ment10n_ @ment20n_9", 11, 21), + arrayOf(" @ment10n-", 1, 10), + arrayOf(" @ment10n- @", 11, 12), + arrayOf(" @ment10n- @ment20n", 11, 19), + arrayOf(" @ment10n- @ment20n-", 11, 20), + arrayOf(" @ment10n- @ment20n-n", 11, 21), + arrayOf(" @ment10n- @ment20n-9", 11, 21), + arrayOf("@ment10n@l0calhost", 0, 18), + arrayOf(" @ment10n@l0calhost", 1, 19), + arrayOf(" @ment10n_@l0calhost", 1, 20), + arrayOf(" @ment10n-@l0calhost", 1, 20), + arrayOf(" @ment10n_@l0calhost @ment20n@husky", 21, 35), + arrayOf(" @ment10n_@l0calhost @ment20n_@husky", 21, 36), + arrayOf(" @ment10n-@l0calhost @ment20n-@husky", 21, 36), + arrayOf(" @m@localhost", 1, 13), + arrayOf(" @m@localhost @a@localhost", 14, 26), + arrayOf("@m@", 0, 3), + arrayOf(" @m@ @a@asdf", 5, 12), + arrayOf(" @m@ @a@", 5, 8), + arrayOf(" @m@ @a@a", 5, 9), + arrayOf(" @m@a @a@m", 6, 10), + arrayOf("@m@m@", 5, 5), + arrayOf("#tusky@husky", 12, 12), + arrayOf(":tusky@husky", 12, 12), + arrayOf("mention", 7, 7), + arrayOf("ment10n", 7, 7), + arrayOf("mentio_", 7, 7), + arrayOf("#tusky", 0, 6), + arrayOf("#@tusky", 7, 7), + arrayOf("@#tusky", 7, 7), + arrayOf(" @#tusky", 8, 8), + arrayOf(":mastodon", 0, 9), + arrayOf(":@mastodon", 10, 10), + arrayOf("@:mastodon", 10, 10), + arrayOf(" @:mastodon", 11, 11), + arrayOf("#@:mastodon", 11, 11), + arrayOf(" #@:mastodon", 12, 12) ) } } @@ -91,4 +93,4 @@ class ComposeTokenizerTest(private val text: CharSequence, Assert.assertEquals(expectedStartIndex, tokenizer.findTokenStart(text, text.length)) Assert.assertEquals(expectedEndIndex, tokenizer.findTokenEnd(text, text.length)) } -} \ No newline at end of file +} diff --git a/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt b/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt index 71f0377fd..1c6a19c68 100644 --- a/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt @@ -7,17 +7,14 @@ import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.entity.PollOption import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.network.FilterModel -import com.keylesspalace.tusky.network.MastodonApi -import com.nhaarman.mockitokotlin2.doReturn import com.nhaarman.mockitokotlin2.mock -import io.reactivex.rxjava3.core.Single import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.robolectric.annotation.Config -import java.util.* +import java.util.Date @Config(sdk = [28]) @RunWith(AndroidJUnit4::class) @@ -182,5 +179,4 @@ class FilterTest { card = null ) } - -} \ No newline at end of file +} diff --git a/app/src/test/java/com/keylesspalace/tusky/FocalPointUtilTest.kt b/app/src/test/java/com/keylesspalace/tusky/FocalPointUtilTest.kt index f3094ee93..444da0619 100644 --- a/app/src/test/java/com/keylesspalace/tusky/FocalPointUtilTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/FocalPointUtilTest.kt @@ -17,8 +17,8 @@ package com.keylesspalace.tusky import com.keylesspalace.tusky.util.FocalPointUtil import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Test class FocalPointUtilTest { @@ -45,66 +45,112 @@ class FocalPointUtilTest { // isVerticalCrop tests @Test fun isVerticalCropTest() { - assertTrue(FocalPointUtil.isVerticalCrop(2f, 1f, - 1f, 2f)) + assertTrue( + FocalPointUtil.isVerticalCrop( + 2f, 1f, + 1f, 2f + ) + ) } @Test fun isHorizontalCropTest() { - assertFalse(FocalPointUtil.isVerticalCrop(1f, 2f, - 2f,1f)) + assertFalse( + FocalPointUtil.isVerticalCrop( + 1f, 2f, + 2f, 1f + ) + ) } @Test fun isPerfectFitTest() { // Doesn't matter what it returns, just check it doesn't crash - FocalPointUtil.isVerticalCrop(3f, 1f, - 6f, 2f) + FocalPointUtil.isVerticalCrop( + 3f, 1f, + 6f, 2f + ) } // calculateScaling tests @Test fun perfectFitScaleDownTest() { - assertEquals(FocalPointUtil.calculateScaling(2f, 5f, - 5f, 12.5f), 0.4f, eps) + assertEquals( + FocalPointUtil.calculateScaling( + 2f, 5f, + 5f, 12.5f + ), + 0.4f, eps + ) } @Test fun perfectFitScaleUpTest() { - assertEquals(FocalPointUtil.calculateScaling(2f, 5f, - 1f, 2.5f), 2f, eps) + assertEquals( + FocalPointUtil.calculateScaling( + 2f, 5f, + 1f, 2.5f + ), + 2f, eps + ) } @Test fun verticalCropScaleUpTest() { - assertEquals(FocalPointUtil.calculateScaling(2f, 1f, - 1f, 2f), 2f, eps) + assertEquals( + FocalPointUtil.calculateScaling( + 2f, 1f, + 1f, 2f + ), + 2f, eps + ) } @Test fun verticalCropScaleDownTest() { - assertEquals(FocalPointUtil.calculateScaling(4f, 3f, - 8f, 24f), 0.5f, eps) + assertEquals( + FocalPointUtil.calculateScaling( + 4f, 3f, + 8f, 24f + ), + 0.5f, eps + ) } @Test fun horizontalCropScaleUpTest() { - assertEquals(FocalPointUtil.calculateScaling(1f, 2f, - 2f, 1f), 2f, eps) + assertEquals( + FocalPointUtil.calculateScaling( + 1f, 2f, + 2f, 1f + ), + 2f, eps + ) } @Test fun horizontalCropScaleDownTest() { - assertEquals(FocalPointUtil.calculateScaling(3f, 4f, - 24f, 8f), 0.5f, eps) + assertEquals( + FocalPointUtil.calculateScaling( + 3f, 4f, + 24f, 8f + ), + 0.5f, eps + ) } // focalOffset tests @Test fun toLowFocalOffsetTest() { - assertEquals(FocalPointUtil.focalOffset(2f, 8f, 1f, 0.05f), - 0f, eps) + assertEquals( + FocalPointUtil.focalOffset(2f, 8f, 1f, 0.05f), + 0f, eps + ) } @Test fun toHighFocalOffsetTest() { - assertEquals(FocalPointUtil.focalOffset(2f, 4f, 2f,0.95f), - -6f, eps) + assertEquals( + FocalPointUtil.focalOffset(2f, 4f, 2f, 0.95f), + -6f, eps + ) } @Test fun possibleFocalOffsetTest() { - assertEquals(FocalPointUtil.focalOffset(2f, 4f, 2f,0.7f), - -4.6f, eps) + assertEquals( + FocalPointUtil.focalOffset(2f, 4f, 2f, 0.7f), + -4.6f, eps + ) } -} \ No newline at end of file +} diff --git a/app/src/test/java/com/keylesspalace/tusky/SpanUtilsTest.kt b/app/src/test/java/com/keylesspalace/tusky/SpanUtilsTest.kt index 68bdaaa97..213405603 100644 --- a/app/src/test/java/com/keylesspalace/tusky/SpanUtilsTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/SpanUtilsTest.kt @@ -36,10 +36,10 @@ class SpanUtilsTest { @JvmStatic fun data(): Iterable { return listOf( - "@mention", - "#tag", - "https://thr.ee/meh?foo=bar&wat=@at#hmm", - "http://thr.ee/meh?foo=bar&wat=@at#hmm" + "@mention", + "#tag", + "https://thr.ee/meh?foo=bar&wat=@at#hmm", + "http://thr.ee/meh?foo=bar&wat=@at#hmm" ) } } @@ -94,21 +94,23 @@ class SpanUtilsTest { } @RunWith(Parameterized::class) - class HighlightingTestsForTag(private val text: String, - private val expectedStartIndex: Int, - private val expectedEndIndex: Int) { + class HighlightingTestsForTag( + private val text: String, + private val expectedStartIndex: Int, + private val expectedEndIndex: Int + ) { companion object { @Parameterized.Parameters(name = "{0}") @JvmStatic fun data(): Iterable { return listOf( - arrayOf("#test", 0, 5), - arrayOf(" #AfterSpace", 1, 12), - arrayOf("#BeforeSpace ", 0, 12), - arrayOf("@#after_at", 1, 10), - arrayOf("あいうえお#after_hiragana", 5, 20), - arrayOf("##DoubleHash", 1, 12), - arrayOf("###TripleHash", 2, 13) + arrayOf("#test", 0, 5), + arrayOf(" #AfterSpace", 1, 12), + arrayOf("#BeforeSpace ", 0, 12), + arrayOf("@#after_at", 1, 10), + arrayOf("あいうえお#after_hiragana", 5, 20), + arrayOf("##DoubleHash", 1, 12), + arrayOf("###TripleHash", 2, 13) ) } } @@ -133,13 +135,13 @@ class SpanUtilsTest { } override fun getSpans(start: Int, end: Int, type: Class): Array { - return spans.filter { it.start >= start && it.end <= end && type.isInstance(it)} - .map { it.span } - .toTypedArray() as Array + return spans.filter { it.start >= start && it.end <= end && type.isInstance(it) } + .map { it.span } + .toTypedArray() as Array } override fun removeSpan(what: Any?) { - spans.removeIf { span -> span.span == what} + spans.removeIf { span -> span.span == what } } override fun toString(): String { @@ -175,4 +177,4 @@ class SpanUtilsTest { throw NotImplementedError() } } -} \ No newline at end of file +} diff --git a/app/src/test/java/com/keylesspalace/tusky/StringUtilsTest.kt b/app/src/test/java/com/keylesspalace/tusky/StringUtilsTest.kt index 7b23297e8..5966cc39e 100644 --- a/app/src/test/java/com/keylesspalace/tusky/StringUtilsTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/StringUtilsTest.kt @@ -3,21 +3,23 @@ package com.keylesspalace.tusky import com.keylesspalace.tusky.util.dec import com.keylesspalace.tusky.util.inc import com.keylesspalace.tusky.util.isLessThan -import org.junit.Assert.* +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Test class StringUtilsTest { @Test fun isLessThan() { val lessList = listOf( - "abc" to "bcd", - "ab" to "abc", - "cb" to "abc", - "1" to "2" + "abc" to "bcd", + "ab" to "abc", + "cb" to "abc", + "1" to "2" ) lessList.forEach { (l, r) -> assertTrue("$l < $r", l.isLessThan(r)) } val notLessList = lessList.map { (l, r) -> r to l } + listOf( - "abc" to "abc" + "abc" to "abc" ) notLessList.forEach { (l, r) -> assertFalse("not $l < $r", l.isLessThan(r)) } } @@ -25,22 +27,22 @@ class StringUtilsTest { @Test fun inc() { listOf( - "122" to "123", - "12A" to "12B", - "1" to "2" + "122" to "123", + "12A" to "12B", + "1" to "2" ).forEach { (l, r) -> assertEquals("$l + 1 = $r", r, l.inc()) } } @Test fun dec() { listOf( - "123" to "122", - "12B" to "12A", - "120" to "11z", - "100" to "zz", - "0" to "", - "" to "", - "2" to "1" + "123" to "122", + "12B" to "12A", + "120" to "11z", + "100" to "zz", + "0" to "", + "" to "", + "2" to "1" ).forEach { (l, r) -> assertEquals("$l - 1 = $r", r, l.dec()) } } -} \ No newline at end of file +} diff --git a/app/src/test/java/com/keylesspalace/tusky/TuskyApplication.kt b/app/src/test/java/com/keylesspalace/tusky/TuskyApplication.kt index 7f82e2495..7724ba768 100644 --- a/app/src/test/java/com/keylesspalace/tusky/TuskyApplication.kt +++ b/app/src/test/java/com/keylesspalace/tusky/TuskyApplication.kt @@ -44,4 +44,4 @@ class TuskyApplication : Application() { @JvmStatic lateinit var localeManager: LocaleManager } -} \ No newline at end of file +} diff --git a/app/src/test/java/com/keylesspalace/tusky/components/timeline/TimelineRepositoryTest.kt b/app/src/test/java/com/keylesspalace/tusky/components/timeline/TimelineRepositoryTest.kt index a11fda67d..55861fd4e 100644 --- a/app/src/test/java/com/keylesspalace/tusky/components/timeline/TimelineRepositoryTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/components/timeline/TimelineRepositoryTest.kt @@ -23,14 +23,15 @@ import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.* +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyLong import org.mockito.Mock import org.mockito.MockitoAnnotations import org.robolectric.annotation.Config import retrofit2.Response -import java.util.* +import java.util.Date import java.util.concurrent.TimeUnit -import kotlin.collections.ArrayList @Config(sdk = [28]) @RunWith(AndroidJUnit4::class) @@ -50,7 +51,6 @@ class TimelineRepositoryTest { private lateinit var testScheduler: TestScheduler - private val limit = 30 private val account = AccountEntity( id = 2, diff --git a/app/src/test/java/com/keylesspalace/tusky/components/timeline/TimelineViewModelTest.kt b/app/src/test/java/com/keylesspalace/tusky/components/timeline/TimelineViewModelTest.kt index b70972dbb..a5665de92 100644 --- a/app/src/test/java/com/keylesspalace/tusky/components/timeline/TimelineViewModelTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/components/timeline/TimelineViewModelTest.kt @@ -14,14 +14,24 @@ import com.keylesspalace.tusky.network.TimelineCases import com.keylesspalace.tusky.util.Either import com.keylesspalace.tusky.util.toViewData import com.keylesspalace.tusky.viewdata.StatusViewData -import com.nhaarman.mockitokotlin2.* +import com.nhaarman.mockitokotlin2.clearInvocations +import com.nhaarman.mockitokotlin2.doReturn +import com.nhaarman.mockitokotlin2.eq +import com.nhaarman.mockitokotlin2.isNull +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.times +import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.whenever import io.reactivex.rxjava3.annotations.NonNull import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.observers.TestObserver import io.reactivex.rxjava3.subjects.PublishSubject import kotlinx.coroutines.runBlocking -import org.junit.Assert.* +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.robolectric.annotation.Config @@ -29,7 +39,6 @@ import org.robolectric.shadows.ShadowLog import retrofit2.Response import java.io.IOException - @Config(sdk = [29]) class TimelineViewModelTest { lateinit var timelineRepository: TimelineRepository @@ -727,7 +736,6 @@ class TimelineViewModelTest { ).thenReturn(Single.just(items)) } - private fun assertHasList(aList: List) { assertEquals( aList, @@ -780,4 +788,4 @@ class TimelineViewModelTest { } private fun List.toEitherList() = map { Either.Right(it) } -} \ No newline at end of file +} diff --git a/app/src/test/java/com/keylesspalace/tusky/util/EmojiCompatFontTest.kt b/app/src/test/java/com/keylesspalace/tusky/util/EmojiCompatFontTest.kt index badaa709a..5dd5ea84f 100644 --- a/app/src/test/java/com/keylesspalace/tusky/util/EmojiCompatFontTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/util/EmojiCompatFontTest.kt @@ -1,6 +1,6 @@ package com.keylesspalace.tusky.util -import org.junit.Assert.* +import org.junit.Assert.assertEquals import org.junit.Test class EmojiCompatFontTest { @@ -9,39 +9,39 @@ class EmojiCompatFontTest { fun testCompareVersions() { assertEquals( - -1, - EmojiCompatFont.compareVersions( - listOf(0), - listOf(1, 2, 3) - ) + -1, + EmojiCompatFont.compareVersions( + listOf(0), + listOf(1, 2, 3) + ) ) assertEquals( - 1, - EmojiCompatFont.compareVersions( - listOf(1, 2, 3), - listOf(0, 0, 0) - ) + 1, + EmojiCompatFont.compareVersions( + listOf(1, 2, 3), + listOf(0, 0, 0) + ) ) assertEquals( - -1, - EmojiCompatFont.compareVersions( - listOf(1, 0, 1), - listOf(1, 1, 0) - ) + -1, + EmojiCompatFont.compareVersions( + listOf(1, 0, 1), + listOf(1, 1, 0) + ) ) assertEquals( - 0, - EmojiCompatFont.compareVersions( - listOf(4, 5, 6), - listOf(4, 5, 6) - ) + 0, + EmojiCompatFont.compareVersions( + listOf(4, 5, 6), + listOf(4, 5, 6) + ) ) assertEquals( - 0, - EmojiCompatFont.compareVersions( - listOf(0, 0), - listOf(0) - ) + 0, + EmojiCompatFont.compareVersions( + listOf(0, 0), + listOf(0) + ) ) } -} \ No newline at end of file +} diff --git a/app/src/test/java/com/keylesspalace/tusky/util/RickRollTest.kt b/app/src/test/java/com/keylesspalace/tusky/util/RickRollTest.kt index b9b3a3740..c5bfad426 100644 --- a/app/src/test/java/com/keylesspalace/tusky/util/RickRollTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/util/RickRollTest.kt @@ -23,11 +23,13 @@ class RickRollTest { @Test fun testShouldRickRoll() { listOf("gab.Com", "social.gab.ai", "whatever.GAB.com").forEach { - rollableDomain -> assertTrue(shouldRickRoll(activity, rollableDomain)) + rollableDomain -> + assertTrue(shouldRickRoll(activity, rollableDomain)) } listOf("chaos.social", "notgab.com").forEach { - notRollableDomain -> assertFalse(shouldRickRoll(activity, notRollableDomain)) + notRollableDomain -> + assertFalse(shouldRickRoll(activity, notRollableDomain)) } } } diff --git a/app/src/test/java/com/keylesspalace/tusky/util/SmartLengthInputFilterTest.kt b/app/src/test/java/com/keylesspalace/tusky/util/SmartLengthInputFilterTest.kt index b85d60a1f..5b6f417b7 100644 --- a/app/src/test/java/com/keylesspalace/tusky/util/SmartLengthInputFilterTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/util/SmartLengthInputFilterTest.kt @@ -12,7 +12,6 @@ import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) class SmartLengthInputFilterTest { - @Test fun shouldNotTrimStatusWithLength0() { assertFalse(shouldTrimStatus(SpannableStringBuilder(""))) @@ -25,56 +24,80 @@ class SmartLengthInputFilterTest { @Test fun shouldNotTrimStatusWithLength500() { - assertFalse(shouldTrimStatus(SpannableStringBuilder("u1Pc5TbDVYFnzIdqlQkb3xuZ2S61fFD1K4u" + - "cb3q40dnELjAsWxnSH59jqly249Spr0Vod029zfwFHYQ0PkBCNQ7tuk90h6aY661RFC7vhIKJna4yDYOBFj" + - "RR9u0CsUa6vlgEE5yUrk5LKn3bmnnzRCXmU6HyT2bFu256qoUWbmMQ6GFXUXjO28tru8Q3UiXKLgrotKdSH" + - "mmqPwQgtatbMykTW4RZdKTE46nzlbD3mXHdWQkf4uVPYhVT1CMvVbCPMaimfQ0xuU8CpxyVqA8a6lCL3YX9" + - "pNnZjD7DoCg2FCejANnjXsTF6vuqPSHjQZDjy696nSAFy95p9kBeJkc70fHzX5TcfUqSaNtvx3LUtpIkwh4" + - "q2EYmKISPsxlANaspEMPuX6r9fSACiEwmHsitZkp4RMKZq5NqRsGPCiAXcNIN3jj9fCYVGxUwVxVeCescDG" + - "5naEEszIR3FT1RO4MSn9c2ZZi0UdLizd8ciJAIuwwmcVyYyyM4"))) + assertFalse( + shouldTrimStatus( + SpannableStringBuilder( + "u1Pc5TbDVYFnzIdqlQkb3xuZ2S61fFD1K4u" + + "cb3q40dnELjAsWxnSH59jqly249Spr0Vod029zfwFHYQ0PkBCNQ7tuk90h6aY661RFC7vhIKJna4yDYOBFj" + + "RR9u0CsUa6vlgEE5yUrk5LKn3bmnnzRCXmU6HyT2bFu256qoUWbmMQ6GFXUXjO28tru8Q3UiXKLgrotKdSH" + + "mmqPwQgtatbMykTW4RZdKTE46nzlbD3mXHdWQkf4uVPYhVT1CMvVbCPMaimfQ0xuU8CpxyVqA8a6lCL3YX9" + + "pNnZjD7DoCg2FCejANnjXsTF6vuqPSHjQZDjy696nSAFy95p9kBeJkc70fHzX5TcfUqSaNtvx3LUtpIkwh4" + + "q2EYmKISPsxlANaspEMPuX6r9fSACiEwmHsitZkp4RMKZq5NqRsGPCiAXcNIN3jj9fCYVGxUwVxVeCescDG" + + "5naEEszIR3FT1RO4MSn9c2ZZi0UdLizd8ciJAIuwwmcVyYyyM4" + ) + ) + ) } @Test fun shouldNotTrimStatusWithLength666() { - assertFalse(shouldTrimStatus(SpannableStringBuilder("hIAXqY7DYynQGcr3zxcjCjNZFcdwAzwnWv" + - "NHONtT55rO3r2faeMRZLTG3JlOshq8M1mtLRn0Ca8M9w82nIjJDm1jspxhFc4uLFpOjb9Gm2BokgRftA8ih" + - "pv6wvMwF5Fg8V4qa8GcXcqt1q7S9g09S3PszCXG4wnrR6dp8GGc9TqVArgmoLSc9EVREIRcLPdzkhV1WWM9" + - "ZWw7josT27BfBdMWk0ckQkClHAyqLtlKZ84WamxK2q3NtHR5gr7ohIjU8CZoKDjv1bA8ZI8wBesyOhqbmHf" + - "0Ltypq39WKZ63VTGSf5Dd9kuTEjlXJtxZD1DXH4FFplY45DH5WuQ61Ih5dGx0WFEEVb1L3aku3Ht8rKG7YU" + - "bOPeanGMBmeI9YRdiD4MmuTUkJfVLkA9rrpRtiEYw8RS3Jf9iqDkTpES9aLQODMip5xTsT4liIcUbLo0Z1d" + - "NhHk7YKubigNQIm1mmh2iU3Q0ZEm8TraDpKu2o27gIwSKbAnTllrOokprPxWQWDVrN9bIliwGHzgTKPI5z8" + - "gUybaqewxUYe12GvxnzqpfPFvvHricyZAC9i6Fkil5VmFdae75tLFWRBfE8Wfep0dSjL751m2yzvzZTc6uZ" + - "RTcUiipvl42DaY8Z5eG2b6xPVhvXshMORvHzwhJhPkHSbnwXX5K"))) + assertFalse( + shouldTrimStatus( + SpannableStringBuilder( + "hIAXqY7DYynQGcr3zxcjCjNZFcdwAzwnWv" + + "NHONtT55rO3r2faeMRZLTG3JlOshq8M1mtLRn0Ca8M9w82nIjJDm1jspxhFc4uLFpOjb9Gm2BokgRftA8ih" + + "pv6wvMwF5Fg8V4qa8GcXcqt1q7S9g09S3PszCXG4wnrR6dp8GGc9TqVArgmoLSc9EVREIRcLPdzkhV1WWM9" + + "ZWw7josT27BfBdMWk0ckQkClHAyqLtlKZ84WamxK2q3NtHR5gr7ohIjU8CZoKDjv1bA8ZI8wBesyOhqbmHf" + + "0Ltypq39WKZ63VTGSf5Dd9kuTEjlXJtxZD1DXH4FFplY45DH5WuQ61Ih5dGx0WFEEVb1L3aku3Ht8rKG7YU" + + "bOPeanGMBmeI9YRdiD4MmuTUkJfVLkA9rrpRtiEYw8RS3Jf9iqDkTpES9aLQODMip5xTsT4liIcUbLo0Z1d" + + "NhHk7YKubigNQIm1mmh2iU3Q0ZEm8TraDpKu2o27gIwSKbAnTllrOokprPxWQWDVrN9bIliwGHzgTKPI5z8" + + "gUybaqewxUYe12GvxnzqpfPFvvHricyZAC9i6Fkil5VmFdae75tLFWRBfE8Wfep0dSjL751m2yzvzZTc6uZ" + + "RTcUiipvl42DaY8Z5eG2b6xPVhvXshMORvHzwhJhPkHSbnwXX5K" + ) + ) + ) } @Test fun shouldTrimStatusWithLength667() { - assertTrue(shouldTrimStatus(SpannableStringBuilder("hIAXqY7DYynQGcr3zxcjCjNZFcdwAzwnWv" + - "NHONtT55rO3r2faeMRZLTG3JlOshq8M1mtLRn0Ca8M9w82nIjJDm1jspxhFc4uLFpOjb9Gm2BokgRftA8ih" + - "pv6wvMwF5Fg8V4qa8GcXcqt1q7S9g09S3PszCXG4wnrR6dp8GGc9TqVArgmoLSc9EVREIRcLPdzkhV1WWM9" + - "ZWw7josT27BfBdMWk0ckQkClHAyqLtlKZ84WamxK2q3NtHR5gr7ohIjU8CZoKDjv1bA8ZI8wBesyOhqbmHf" + - "0Ltypq39WKZ63VTGSf5Dd9kuTEjlXJtxZD1DXH4FFplY45DH5WuQ61Ih5dGx0WFEEVb1L3aku3Ht8rKG7YU" + - "bOPeanGMBmeI9YRdiD4MmuTUkJfVLkA9rrpRtiEYw8RS3Jf9iqDkTpES9aLQODMip5xTsT4liIcUbLo0Z1d" + - "NhHk7YKubigNQIm1mmh2iU3Q0ZEm8TraDpKu2o27gIwSKbAnTllrOokprPxWQWDVrN9bIliwGHzgTKPI5z8" + - "gUybaqewxUYe12GvxnzqpfPFvvHricyZAC9i6Fkil5VmFdae75tLFWRBfE8Wfep0dSjL751m2yzvzZTc6uZ" + - "RTcUiipvl42DaY8Z5eG2b6xPVhvXshMORvHzwhJhPkHSbnwXX5K1"))) + assertTrue( + shouldTrimStatus( + SpannableStringBuilder( + "hIAXqY7DYynQGcr3zxcjCjNZFcdwAzwnWv" + + "NHONtT55rO3r2faeMRZLTG3JlOshq8M1mtLRn0Ca8M9w82nIjJDm1jspxhFc4uLFpOjb9Gm2BokgRftA8ih" + + "pv6wvMwF5Fg8V4qa8GcXcqt1q7S9g09S3PszCXG4wnrR6dp8GGc9TqVArgmoLSc9EVREIRcLPdzkhV1WWM9" + + "ZWw7josT27BfBdMWk0ckQkClHAyqLtlKZ84WamxK2q3NtHR5gr7ohIjU8CZoKDjv1bA8ZI8wBesyOhqbmHf" + + "0Ltypq39WKZ63VTGSf5Dd9kuTEjlXJtxZD1DXH4FFplY45DH5WuQ61Ih5dGx0WFEEVb1L3aku3Ht8rKG7YU" + + "bOPeanGMBmeI9YRdiD4MmuTUkJfVLkA9rrpRtiEYw8RS3Jf9iqDkTpES9aLQODMip5xTsT4liIcUbLo0Z1d" + + "NhHk7YKubigNQIm1mmh2iU3Q0ZEm8TraDpKu2o27gIwSKbAnTllrOokprPxWQWDVrN9bIliwGHzgTKPI5z8" + + "gUybaqewxUYe12GvxnzqpfPFvvHricyZAC9i6Fkil5VmFdae75tLFWRBfE8Wfep0dSjL751m2yzvzZTc6uZ" + + "RTcUiipvl42DaY8Z5eG2b6xPVhvXshMORvHzwhJhPkHSbnwXX5K1" + ) + ) + ) } @Test fun shouldTrimStatusWithLength1000() { - assertTrue(shouldTrimStatus(SpannableStringBuilder("u1Pc5TbDVYFnzIdqlQkb3xuZ2S61fFD1K4u" + - "cb3q40dnELjAsWxnSH59jqly249Spr0Vod029zfwFHYQ0PkBCNQ7tuk90h6aY661RFC7vhIKJna4yDYOBFj" + - "RR9u0CsUa6vlgEE5yUrk5LKn3bmnnzRCXmU6HyT2bFu256qoUWbmMQ6GFXUXjO28tru8Q3UiXKLgrotKdSH" + - "mmqPwQgtatbMykTW4RZdKTE46nzlbD3mXHdWQkf4uVPYhVT1CMvVbCPMaimfQ0xuU8CpxyVqA8a6lCL3YX9" + - "pNnZjD7DoCg2FCejANnjXsTF6vuqPSHjQZDjy696nSAFy95p9kBeJkc70fHzX5TcfUqSaNtvx3LUtpIkwh4" + - "q2EYmKISPsxlANaspEMPuX6r9fSACiEwmHsitZkp4RMKZq5NqRsGPCiAXcNIN3jj9fCYVGxUwVxVeCescDG" + - "5naEEszIR3FT1RO4MSn9c2ZZi0UdLizd8ciJAIuwwmcVyYyyM4"+ - "u1Pc5TbDVYFnzIdqlQkb3xuZ2S61fFD1K4u" + - "cb3q40dnELjAsWxnSH59jqly249Spr0Vod029zfwFHYQ0PkBCNQ7tuk90h6aY661RFC7vhIKJna4yDYOBFj" + - "RR9u0CsUa6vlgEE5yUrk5LKn3bmnnzRCXmU6HyT2bFu256qoUWbmMQ6GFXUXjO28tru8Q3UiXKLgrotKdSH" + - "mmqPwQgtatbMykTW4RZdKTE46nzlbD3mXHdWQkf4uVPYhVT1CMvVbCPMaimfQ0xuU8CpxyVqA8a6lCL3YX9" + - "pNnZjD7DoCg2FCejANnjXsTF6vuqPSHjQZDjy696nSAFy95p9kBeJkc70fHzX5TcfUqSaNtvx3LUtpIkwh4" + - "q2EYmKISPsxlANaspEMPuX6r9fSACiEwmHsitZkp4RMKZq5NqRsGPCiAXcNIN3jj9fCYVGxUwVxVeCescDG" + - "5naEEszIR3FT1RO4MSn9c2ZZi0UdLizd8ciJAIuwwmcVyYyyM4"))) + assertTrue( + shouldTrimStatus( + SpannableStringBuilder( + "u1Pc5TbDVYFnzIdqlQkb3xuZ2S61fFD1K4u" + + "cb3q40dnELjAsWxnSH59jqly249Spr0Vod029zfwFHYQ0PkBCNQ7tuk90h6aY661RFC7vhIKJna4yDYOBFj" + + "RR9u0CsUa6vlgEE5yUrk5LKn3bmnnzRCXmU6HyT2bFu256qoUWbmMQ6GFXUXjO28tru8Q3UiXKLgrotKdSH" + + "mmqPwQgtatbMykTW4RZdKTE46nzlbD3mXHdWQkf4uVPYhVT1CMvVbCPMaimfQ0xuU8CpxyVqA8a6lCL3YX9" + + "pNnZjD7DoCg2FCejANnjXsTF6vuqPSHjQZDjy696nSAFy95p9kBeJkc70fHzX5TcfUqSaNtvx3LUtpIkwh4" + + "q2EYmKISPsxlANaspEMPuX6r9fSACiEwmHsitZkp4RMKZq5NqRsGPCiAXcNIN3jj9fCYVGxUwVxVeCescDG" + + "5naEEszIR3FT1RO4MSn9c2ZZi0UdLizd8ciJAIuwwmcVyYyyM4" + + "u1Pc5TbDVYFnzIdqlQkb3xuZ2S61fFD1K4u" + + "cb3q40dnELjAsWxnSH59jqly249Spr0Vod029zfwFHYQ0PkBCNQ7tuk90h6aY661RFC7vhIKJna4yDYOBFj" + + "RR9u0CsUa6vlgEE5yUrk5LKn3bmnnzRCXmU6HyT2bFu256qoUWbmMQ6GFXUXjO28tru8Q3UiXKLgrotKdSH" + + "mmqPwQgtatbMykTW4RZdKTE46nzlbD3mXHdWQkf4uVPYhVT1CMvVbCPMaimfQ0xuU8CpxyVqA8a6lCL3YX9" + + "pNnZjD7DoCg2FCejANnjXsTF6vuqPSHjQZDjy696nSAFy95p9kBeJkc70fHzX5TcfUqSaNtvx3LUtpIkwh4" + + "q2EYmKISPsxlANaspEMPuX6r9fSACiEwmHsitZkp4RMKZq5NqRsGPCiAXcNIN3jj9fCYVGxUwVxVeCescDG" + + "5naEEszIR3FT1RO4MSn9c2ZZi0UdLizd8ciJAIuwwmcVyYyyM4" + ) + ) + ) } } diff --git a/app/src/test/java/com/keylesspalace/tusky/util/VersionUtilsTest.kt b/app/src/test/java/com/keylesspalace/tusky/util/VersionUtilsTest.kt index 03ab3d947..2731228a0 100644 --- a/app/src/test/java/com/keylesspalace/tusky/util/VersionUtilsTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/util/VersionUtilsTest.kt @@ -7,24 +7,24 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) class VersionUtilsTest( - private val versionString: String, - private val supportsScheduledToots: Boolean + private val versionString: String, + private val supportsScheduledToots: Boolean ) { companion object { @JvmStatic @Parameterized.Parameters fun data() = listOf( - arrayOf("2.0.0", false), - arrayOf("2a9a0", false), - arrayOf("1.0", false), - arrayOf("error", false), - arrayOf("", false), - arrayOf("2.6.9", false), - arrayOf("2.7.0", true), - arrayOf("2.00008.0", true), - arrayOf("2.7.2 (compatible; Pleroma 1.0.0-1168-ge18c7866-pleroma-dot-site)", true), - arrayOf("3.0.1", true) + arrayOf("2.0.0", false), + arrayOf("2a9a0", false), + arrayOf("1.0", false), + arrayOf("error", false), + arrayOf("", false), + arrayOf("2.6.9", false), + arrayOf("2.7.0", true), + arrayOf("2.00008.0", true), + arrayOf("2.7.2 (compatible; Pleroma 1.0.0-1168-ge18c7866-pleroma-dot-site)", true), + arrayOf("3.0.1", true) ) } @@ -32,5 +32,4 @@ class VersionUtilsTest( fun testVersionUtils() { assertEquals(VersionUtils(versionString).supportsScheduledToots(), supportsScheduledToots) } - -} \ No newline at end of file +} diff --git a/build.gradle b/build.gradle index 3bc072dcd..ccc0c7faa 100644 --- a/build.gradle +++ b/build.gradle @@ -3,14 +3,20 @@ buildscript { repositories { google() mavenCentral() + gradlePluginPortal() } dependencies { classpath 'com.android.tools.build:gradle:4.2.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "org.jlleitschuh.gradle:ktlint-gradle:10.1.0" } } +plugins { + id "org.jlleitschuh.gradle.ktlint" version "10.1.0" +} allprojects { + apply plugin: "org.jlleitschuh.gradle.ktlint" repositories { google() mavenCentral() From 9ca7e708bd66febb400326565554f9597137def5 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Mon, 28 Jun 2021 21:41:18 +0200 Subject: [PATCH 91/92] fix liking/boosting/bookmarking/voting boosted statuses in timeline (#2212) --- .../components/timeline/TimelineViewModel.kt | 43 +++++++++++++------ .../tusky/viewdata/StatusViewData.kt | 7 ++- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineViewModel.kt index 5509b9294..798939d30 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineViewModel.kt @@ -335,10 +335,10 @@ class TimelineViewModel @Inject constructor( fun reblog(reblog: Boolean, position: Int): Job = viewModelScope.launch { val status = statuses[position].asStatusOrNull() ?: return@launch try { - timelineCases.reblog(status.id, reblog).await() + timelineCases.reblog(status.actionableId, reblog).await() } catch (t: Exception) { ifExpected(t) { - Log.d(TAG, "Failed to reblog status " + status.id, t) + Log.d(TAG, "Failed to reblog status " + status.actionableId, t) } } } @@ -347,10 +347,10 @@ class TimelineViewModel @Inject constructor( val status = statuses[position].asStatusOrNull() ?: return@launch try { - timelineCases.favourite(status.id, favorite).await() + timelineCases.favourite(status.actionableId, favorite).await() } catch (t: Exception) { ifExpected(t) { - Log.d(TAG, "Failed to favourite status " + status.id, t) + Log.d(TAG, "Failed to favourite status " + status.actionableId, t) } } } @@ -358,10 +358,10 @@ class TimelineViewModel @Inject constructor( fun bookmark(bookmark: Boolean, position: Int): Job = viewModelScope.launch { val status = statuses[position].asStatusOrNull() ?: return@launch try { - timelineCases.bookmark(status.id, bookmark).await() + timelineCases.bookmark(status.actionableId, bookmark).await() } catch (t: Exception) { ifExpected(t) { - Log.d(TAG, "Failed to favourite status " + status.id, t) + Log.d(TAG, "Failed to favourite status " + status.actionableId, t) } } } @@ -378,10 +378,10 @@ class TimelineViewModel @Inject constructor( updatePoll(status, votedPoll) try { - timelineCases.voteInPoll(status.id, poll.id, choices).await() + timelineCases.voteInPoll(status.actionableId, poll.id, choices).await() } catch (t: Exception) { ifExpected(t) { - Log.d(TAG, "Failed to vote in poll: " + status.id, t) + Log.d(TAG, "Failed to vote in poll: " + status.actionableId, t) } } } @@ -718,20 +718,20 @@ class TimelineViewModel @Inject constructor( } private fun handleFavEvent(favEvent: FavoriteEvent) { - updateStatusById(favEvent.statusId) { - it.copy(status = it.status.copy(favourited = favEvent.favourite)) + updateActionableStatusById(favEvent.statusId) { + it.copy(favourited = favEvent.favourite) } } private fun handleBookmarkEvent(bookmarkEvent: BookmarkEvent) { - updateStatusById(bookmarkEvent.statusId) { - it.copy(status = it.status.copy(bookmarked = bookmarkEvent.bookmark)) + updateActionableStatusById(bookmarkEvent.statusId) { + it.copy(bookmarked = bookmarkEvent.bookmark) } } private fun handlePinEvent(pinEvent: PinEvent) { - updateStatusById(pinEvent.statusId) { - it.copy(status = it.status.copy(pinned = pinEvent.pinned)) + updateActionableStatusById(pinEvent.statusId) { + it.copy(pinned = pinEvent.pinned) } } @@ -858,6 +858,21 @@ class TimelineViewModel @Inject constructor( } } + private inline fun updateActionableStatusById( + id: String, + updater: (Status) -> Status + ) { + val pos = statuses.indexOfFirst { it.asStatusOrNull()?.id == id } + if (pos == -1) return + updateStatusAt(pos) { + if (it.status.reblog != null) { + it.copy(status = it.status.copy(reblog = updater(it.status.reblog))) + } else { + it.copy(status = updater(it.status)) + } + } + } + private inline fun updateStatusById( id: String, updater: (StatusViewData.Concrete) -> StatusViewData.Concrete diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt index c3bf14183..92675eb61 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt @@ -59,6 +59,9 @@ sealed class StatusViewData private constructor() { val actionable: Status get() = status.actionableStatus + val actionableId: String + get() = status.actionableStatus.id + val rebloggedAvatar: String? get() = if (status.reblog != null) { status.account.avatar @@ -91,10 +94,10 @@ sealed class StatusViewData private constructor() { return replaceCrashingCharacters(content as CharSequence) as Spanned } - fun replaceCrashingCharacters(content: CharSequence?): CharSequence? { + fun replaceCrashingCharacters(content: CharSequence): CharSequence? { var replacing = false var builder: SpannableStringBuilder? = null - val length = content!!.length + val length = content.length for (index in 0 until length) { val character = content[index] From 2cc53d67725b935db084f2c88ec40cb63218f610 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Mon, 28 Jun 2021 22:04:34 +0200 Subject: [PATCH 92/92] fix codestyle --- .../com/keylesspalace/tusky/MainActivity.kt | 328 ++++++++++-------- .../components/compose/ComposeViewModel.kt | 256 ++++++++------ .../tusky/components/drafts/DraftHelper.kt | 129 ++++--- .../tusky/components/drafts/DraftsAdapter.kt | 18 +- .../scheduled/ScheduledTootActivity.kt | 25 +- .../scheduled/ScheduledTootAdapter.kt | 21 +- .../scheduled/ScheduledTootPagingSource.kt | 4 +- 7 files changed, 423 insertions(+), 358 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt index bb0ac6589..39239139a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt @@ -48,7 +48,12 @@ import com.bumptech.glide.request.transition.Transition import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout.OnTabSelectedListener import com.google.android.material.tabs.TabLayoutMediator -import com.keylesspalace.tusky.appstore.* +import com.keylesspalace.tusky.appstore.AnnouncementReadEvent +import com.keylesspalace.tusky.appstore.CacheUpdater +import com.keylesspalace.tusky.appstore.Event +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.MainTabsChangedEvent +import com.keylesspalace.tusky.appstore.ProfileEditedEvent import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity import com.keylesspalace.tusky.components.compose.ComposeActivity import com.keylesspalace.tusky.components.compose.ComposeActivity.Companion.canHandleMimeType @@ -67,7 +72,14 @@ import com.keylesspalace.tusky.interfaces.ActionButtonActivity import com.keylesspalace.tusky.interfaces.ReselectableFragment import com.keylesspalace.tusky.pager.MainPagerAdapter import com.keylesspalace.tusky.settings.PrefKeys -import com.keylesspalace.tusky.util.* +import com.keylesspalace.tusky.util.ThemeUtils +import com.keylesspalace.tusky.util.deleteStaleCachedMedia +import com.keylesspalace.tusky.util.emojify +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.removeShortcut +import com.keylesspalace.tusky.util.updateShortcut +import com.keylesspalace.tusky.util.viewBinding +import com.keylesspalace.tusky.util.visible import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.utils.colorInt @@ -76,9 +88,24 @@ import com.mikepenz.materialdrawer.holder.BadgeStyle import com.mikepenz.materialdrawer.holder.ColorHolder import com.mikepenz.materialdrawer.holder.StringHolder import com.mikepenz.materialdrawer.iconics.iconicsIcon -import com.mikepenz.materialdrawer.model.* -import com.mikepenz.materialdrawer.model.interfaces.* -import com.mikepenz.materialdrawer.util.* +import com.mikepenz.materialdrawer.model.AbstractDrawerItem +import com.mikepenz.materialdrawer.model.DividerDrawerItem +import com.mikepenz.materialdrawer.model.PrimaryDrawerItem +import com.mikepenz.materialdrawer.model.ProfileDrawerItem +import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem +import com.mikepenz.materialdrawer.model.SecondaryDrawerItem +import com.mikepenz.materialdrawer.model.interfaces.IProfile +import com.mikepenz.materialdrawer.model.interfaces.descriptionRes +import com.mikepenz.materialdrawer.model.interfaces.descriptionText +import com.mikepenz.materialdrawer.model.interfaces.iconRes +import com.mikepenz.materialdrawer.model.interfaces.iconUrl +import com.mikepenz.materialdrawer.model.interfaces.nameRes +import com.mikepenz.materialdrawer.model.interfaces.nameText +import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader +import com.mikepenz.materialdrawer.util.DrawerImageLoader +import com.mikepenz.materialdrawer.util.addItems +import com.mikepenz.materialdrawer.util.addItemsAtPosition +import com.mikepenz.materialdrawer.util.updateBadge import com.mikepenz.materialdrawer.widget.AccountHeaderView import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector @@ -156,19 +183,22 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje forwardShare(intent) } else { // No account was provided, show the chooser - showAccountChooserDialog(getString(R.string.action_share_as), true, object : AccountSelectionListener { - override fun onAccountSelected(account: AccountEntity) { - val requestedId = account.id - if (requestedId == activeAccount.id) { - // The correct account is already active - forwardShare(intent) - } else { - // A different account was requested, restart the activity - intent.putExtra(NotificationHelper.ACCOUNT_ID, requestedId) - changeAccount(requestedId, intent) + showAccountChooserDialog( + getString(R.string.action_share_as), true, + object : AccountSelectionListener { + override fun onAccountSelected(account: AccountEntity) { + val requestedId = account.id + if (requestedId == activeAccount.id) { + // The correct account is already active + forwardShare(intent) + } else { + // A different account was requested, restart the activity + intent.putExtra(NotificationHelper.ACCOUNT_ID, requestedId) + changeAccount(requestedId, intent) + } } } - }) + ) } } else if (accountRequested && savedInstanceState == null) { // user clicked a notification, show notification tab @@ -323,12 +353,15 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje headerBackgroundScaleType = ImageView.ScaleType.CENTER_CROP currentHiddenInList = true onAccountHeaderListener = { _: View?, profile: IProfile, current: Boolean -> handleProfileClick(profile, current) } - addProfile(ProfileSettingDrawerItem().apply { - identifier = DRAWER_ITEM_ADD_ACCOUNT - nameRes = R.string.add_account_name - descriptionRes = R.string.add_account_description - iconicsIcon = GoogleMaterial.Icon.gmd_add - }, 0) + addProfile( + ProfileSettingDrawerItem().apply { + identifier = DRAWER_ITEM_ADD_ACCOUNT + nameRes = R.string.add_account_name + descriptionRes = R.string.add_account_description + iconicsIcon = GoogleMaterial.Icon.gmd_add + }, + 0 + ) attachToSliderView(binding.mainDrawer) dividerBelowHeader = false closeDrawerOnProfileListClick = true @@ -468,14 +501,16 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje ) if (addSearchButton) { - binding.mainDrawer.addItemsAtPosition(4, + binding.mainDrawer.addItemsAtPosition( + 4, primaryDrawerItem { nameRes = R.string.action_search iconicsIcon = GoogleMaterial.Icon.gmd_search onClick = { startActivityWithSlideInAnimation(SearchActivity.getIntent(context)) } - }) + } + ) } setSavedInstance(savedInstanceState) @@ -572,24 +607,23 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje binding.mainToolbar.setOnClickListener { (adapter.getFragment(activeTabLayout.selectedTabPosition) as? ReselectableFragment)?.onReselect() } - } private fun handleProfileClick(profile: IProfile, current: Boolean): Boolean { val activeAccount = accountManager.activeAccount - //open profile when active image was clicked + // open profile when active image was clicked if (current && activeAccount != null) { val intent = AccountActivity.getIntent(this, activeAccount.accountId) startActivityWithSlideInAnimation(intent) return false } - //open LoginActivity to add new account + // open LoginActivity to add new account if (profile.identifier == DRAWER_ITEM_ADD_ACCOUNT) { startActivityWithSlideInAnimation(LoginActivity.getIntent(this, true)) return false } - //change Account + // change Account changeAccount(profile.identifier, null) return false } @@ -638,130 +672,130 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje finishWithoutSlideOutAnimation() } } - .setNegativeButton(android.R.string.cancel, null) - .show() - } -} - -private fun fetchUserInfo() { - mastodonApi.accountVerifyCredentials() - .observeOn(AndroidSchedulers.mainThread()) - .autoDispose(this, Lifecycle.Event.ON_DESTROY) - .subscribe( - { userInfo -> - onFetchUserInfoSuccess(userInfo) - }, - { throwable -> - Log.e(TAG, "Failed to fetch user info. " + throwable.message) - } - ) -} - -private fun onFetchUserInfoSuccess(me: Account) { - glide.asBitmap() - .load(me.header) - .into(header.accountHeaderBackground) - - loadDrawerAvatar(me.avatar, false) - - accountManager.updateActiveAccount(me) - NotificationHelper.createNotificationChannelsForAccount(accountManager.activeAccount!!, this) - - accountLocked = me.locked - - updateProfiles() - updateShortcut(this, accountManager.activeAccount!!) -} - -private fun loadDrawerAvatar(avatarUrl: String, showPlaceholder: Boolean) { - val navIconSize = resources.getDimensionPixelSize(R.dimen.avatar_toolbar_nav_icon_size) - - glide.asDrawable() - .load(avatarUrl) - .transform( - RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp)) - ) - .apply { - if (showPlaceholder) { - placeholder(R.drawable.avatar_default) - } - } - .into(object : CustomTarget(navIconSize, navIconSize) { - - override fun onLoadStarted(placeholder: Drawable?) { - if (placeholder != null) { - binding.mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize) - } - } - - override fun onResourceReady(resource: Drawable, transition: Transition?) { - binding.mainToolbar.navigationIcon = FixedSizeDrawable(resource, navIconSize, navIconSize) - } - - override fun onLoadCleared(placeholder: Drawable?) { - if (placeholder != null) { - binding.mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize) - } - } - }) -} - -private fun fetchAnnouncements() { - mastodonApi.listAnnouncements(false) - .observeOn(AndroidSchedulers.mainThread()) - .autoDispose(this, Lifecycle.Event.ON_DESTROY) - .subscribe( - { announcements -> - unreadAnnouncementsCount = announcements.count { !it.read } - updateAnnouncementsBadge() - }, - { - Log.w(TAG, "Failed to fetch announcements.", it) - } - ) -} - -private fun updateAnnouncementsBadge() { - binding.mainDrawer.updateBadge(DRAWER_ITEM_ANNOUNCEMENTS, StringHolder(if (unreadAnnouncementsCount <= 0) null else unreadAnnouncementsCount.toString())) -} - -private fun updateProfiles() { - val animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) - val profiles: MutableList = accountManager.getAllAccountsOrderedByActive().map { acc -> - val emojifiedName = EmojiCompat.get().process(acc.displayName.emojify(acc.emojis, header, animateEmojis)) - - ProfileDrawerItem().apply { - isSelected = acc.isActive - nameText = emojifiedName - iconUrl = acc.profilePictureUrl - isNameShown = true - identifier = acc.id - descriptionText = acc.fullName - } - }.toMutableList() - - // reuse the already existing "add account" item - for (profile in header.profiles.orEmpty()) { - if (profile.identifier == DRAWER_ITEM_ADD_ACCOUNT) { - profiles.add(profile) - break + .setNegativeButton(android.R.string.cancel, null) + .show() } } - header.clear() - header.profiles = profiles - header.setActiveProfile(accountManager.activeAccount!!.id) -} -override fun getActionButton() = binding.composeButton + private fun fetchUserInfo() { + mastodonApi.accountVerifyCredentials() + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this, Lifecycle.Event.ON_DESTROY) + .subscribe( + { userInfo -> + onFetchUserInfoSuccess(userInfo) + }, + { throwable -> + Log.e(TAG, "Failed to fetch user info. " + throwable.message) + } + ) + } -override fun androidInjector() = androidInjector + private fun onFetchUserInfoSuccess(me: Account) { + glide.asBitmap() + .load(me.header) + .into(header.accountHeaderBackground) -companion object { - private const val TAG = "MainActivity" // logging tag - private const val DRAWER_ITEM_ADD_ACCOUNT: Long = -13 - private const val DRAWER_ITEM_ANNOUNCEMENTS: Long = 14 - const val STATUS_URL = "statusUrl" -} + loadDrawerAvatar(me.avatar, false) + + accountManager.updateActiveAccount(me) + NotificationHelper.createNotificationChannelsForAccount(accountManager.activeAccount!!, this) + + accountLocked = me.locked + + updateProfiles() + updateShortcut(this, accountManager.activeAccount!!) + } + + private fun loadDrawerAvatar(avatarUrl: String, showPlaceholder: Boolean) { + val navIconSize = resources.getDimensionPixelSize(R.dimen.avatar_toolbar_nav_icon_size) + + glide.asDrawable() + .load(avatarUrl) + .transform( + RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp)) + ) + .apply { + if (showPlaceholder) { + placeholder(R.drawable.avatar_default) + } + } + .into(object : CustomTarget(navIconSize, navIconSize) { + + override fun onLoadStarted(placeholder: Drawable?) { + if (placeholder != null) { + binding.mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize) + } + } + + override fun onResourceReady(resource: Drawable, transition: Transition?) { + binding.mainToolbar.navigationIcon = FixedSizeDrawable(resource, navIconSize, navIconSize) + } + + override fun onLoadCleared(placeholder: Drawable?) { + if (placeholder != null) { + binding.mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize) + } + } + }) + } + + private fun fetchAnnouncements() { + mastodonApi.listAnnouncements(false) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this, Lifecycle.Event.ON_DESTROY) + .subscribe( + { announcements -> + unreadAnnouncementsCount = announcements.count { !it.read } + updateAnnouncementsBadge() + }, + { + Log.w(TAG, "Failed to fetch announcements.", it) + } + ) + } + + private fun updateAnnouncementsBadge() { + binding.mainDrawer.updateBadge(DRAWER_ITEM_ANNOUNCEMENTS, StringHolder(if (unreadAnnouncementsCount <= 0) null else unreadAnnouncementsCount.toString())) + } + + private fun updateProfiles() { + val animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) + val profiles: MutableList = accountManager.getAllAccountsOrderedByActive().map { acc -> + val emojifiedName = EmojiCompat.get().process(acc.displayName.emojify(acc.emojis, header, animateEmojis)) + + ProfileDrawerItem().apply { + isSelected = acc.isActive + nameText = emojifiedName + iconUrl = acc.profilePictureUrl + isNameShown = true + identifier = acc.id + descriptionText = acc.fullName + } + }.toMutableList() + + // reuse the already existing "add account" item + for (profile in header.profiles.orEmpty()) { + if (profile.identifier == DRAWER_ITEM_ADD_ACCOUNT) { + profiles.add(profile) + break + } + } + header.clear() + header.profiles = profiles + header.setActiveProfile(accountManager.activeAccount!!.id) + } + + override fun getActionButton() = binding.composeButton + + override fun androidInjector() = androidInjector + + companion object { + private const val TAG = "MainActivity" // logging tag + private const val DRAWER_ITEM_ADD_ACCOUNT: Long = -13 + private const val DRAWER_ITEM_ANNOUNCEMENTS: Long = 14 + const val STATUS_URL = "statusUrl" + } } private inline fun primaryDrawerItem(block: PrimaryDrawerItem.() -> Unit): PrimaryDrawerItem { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt index 7fffa68d2..f7969ac08 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt @@ -28,26 +28,36 @@ import com.keylesspalace.tusky.components.search.SearchType import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.db.InstanceEntity -import com.keylesspalace.tusky.entity.* +import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.entity.Emoji +import com.keylesspalace.tusky.entity.NewPoll import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.service.ServiceClient import com.keylesspalace.tusky.service.TootToSend -import com.keylesspalace.tusky.util.* +import com.keylesspalace.tusky.util.Either +import com.keylesspalace.tusky.util.RxAwareViewModel +import com.keylesspalace.tusky.util.VersionUtils +import com.keylesspalace.tusky.util.combineLiveData +import com.keylesspalace.tusky.util.filter +import com.keylesspalace.tusky.util.map +import com.keylesspalace.tusky.util.randomAlphanumericString +import com.keylesspalace.tusky.util.toLiveData +import com.keylesspalace.tusky.util.withoutFirstWhich import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.disposables.Disposable import kotlinx.coroutines.launch -import java.util.* +import java.util.Locale import javax.inject.Inject class ComposeViewModel @Inject constructor( - private val api: MastodonApi, - private val accountManager: AccountManager, - private val mediaUploader: MediaUploader, - private val serviceClient: ServiceClient, - private val draftHelper: DraftHelper, - private val db: AppDatabase + private val api: MastodonApi, + private val accountManager: AccountManager, + private val mediaUploader: MediaUploader, + private val serviceClient: ServiceClient, + private val draftHelper: DraftHelper, + private val db: AppDatabase ) : RxAwareViewModel() { private var replyingStatusAuthor: String? = null @@ -66,15 +76,15 @@ class ComposeViewModel @Inject constructor( val instanceParams: LiveData = instance.map { instance -> ComposeInstanceParams( - maxChars = instance?.maximumTootCharacters ?: DEFAULT_CHARACTER_LIMIT, - pollMaxOptions = instance?.maxPollOptions ?: DEFAULT_MAX_OPTION_COUNT, - pollMaxLength = instance?.maxPollOptionLength ?: DEFAULT_MAX_OPTION_LENGTH, - supportsScheduled = instance?.version?.let { VersionUtils(it).supportsScheduledToots() } ?: false + maxChars = instance?.maximumTootCharacters ?: DEFAULT_CHARACTER_LIMIT, + pollMaxOptions = instance?.maxPollOptions ?: DEFAULT_MAX_OPTION_COUNT, + pollMaxLength = instance?.maxPollOptionLength ?: DEFAULT_MAX_OPTION_LENGTH, + supportsScheduled = instance?.version?.let { VersionUtils(it).supportsScheduledToots() } ?: false ) } val emoji: MutableLiveData?> = MutableLiveData() val markMediaAsSensitive = - mutableLiveData(accountManager.activeAccount?.defaultMediaSensitivity ?: false) + mutableLiveData(accountManager.activeAccount?.defaultMediaSensitivity ?: false) val statusVisibility = mutableLiveData(Status.Visibility.UNKNOWN) val showContentWarning = mutableLiveData(false) @@ -91,30 +101,36 @@ class ComposeViewModel @Inject constructor( init { - Single.zip(api.getCustomEmojis(), api.getInstance(), { emojis, instance -> - InstanceEntity( + Single.zip( + api.getCustomEmojis(), api.getInstance(), + { emojis, instance -> + InstanceEntity( instance = accountManager.activeAccount?.domain!!, emojiList = emojis, maximumTootCharacters = instance.maxTootChars, maxPollOptions = instance.pollLimits?.maxOptions, maxPollOptionLength = instance.pollLimits?.maxOptionChars, version = instance.version - ) - }) - .doOnSuccess { - db.instanceDao().insertOrReplace(it) - } - .onErrorResumeNext { - db.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!) - } - .subscribe({ instanceEntity -> + ) + } + ) + .doOnSuccess { + db.instanceDao().insertOrReplace(it) + } + .onErrorResumeNext { + db.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!) + } + .subscribe( + { instanceEntity -> emoji.postValue(instanceEntity.emojiList) instance.postValue(instanceEntity) - }, { throwable -> + }, + { throwable -> // this can happen on network error when no cached data is available Log.w(TAG, "error loading instance data", throwable) - }) - .autoDispose() + } + ) + .autoDispose() } fun pickMedia(uri: Uri, description: String? = null): LiveData> { @@ -122,44 +138,49 @@ class ComposeViewModel @Inject constructor( // the Activity goes away temporarily (like on screen rotation). val liveData = MutableLiveData>() mediaUploader.prepareMedia(uri) - .map { (type, uri, size) -> - val mediaItems = media.value!! - if (type != QueuedMedia.Type.IMAGE - && mediaItems.isNotEmpty() - && mediaItems[0].type == QueuedMedia.Type.IMAGE) { - throw VideoOrImageException() - } else { - addMediaToQueue(type, uri, size, description) - } + .map { (type, uri, size) -> + val mediaItems = media.value!! + if (type != QueuedMedia.Type.IMAGE && + mediaItems.isNotEmpty() && + mediaItems[0].type == QueuedMedia.Type.IMAGE + ) { + throw VideoOrImageException() + } else { + addMediaToQueue(type, uri, size, description) } - .subscribe({ queuedMedia -> + } + .subscribe( + { queuedMedia -> liveData.postValue(Either.Right(queuedMedia)) - }, { error -> + }, + { error -> liveData.postValue(Either.Left(error)) - }) - .autoDispose() + } + ) + .autoDispose() return liveData } private fun addMediaToQueue( - type: QueuedMedia.Type, - uri: Uri, - mediaSize: Long, - description: String? = null + type: QueuedMedia.Type, + uri: Uri, + mediaSize: Long, + description: String? = null ): QueuedMedia { val mediaItem = QueuedMedia( - localId = System.currentTimeMillis(), - uri = uri, - type = type, - mediaSize = mediaSize, - description = description + localId = System.currentTimeMillis(), + uri = uri, + type = type, + mediaSize = mediaSize, + description = description ) media.value = media.value!! + mediaItem mediaToDisposable[mediaItem.localId] = mediaUploader - .uploadMedia(mediaItem) - .subscribe({ event -> + .uploadMedia(mediaItem) + .subscribe( + { event -> val item = media.value?.find { it.localId == mediaItem.localId } - ?: return@subscribe + ?: return@subscribe val newMediaItem = when (event) { is UploadEvent.ProgressEvent -> item.copy(uploadPercent = event.percentage) @@ -169,16 +190,20 @@ class ComposeViewModel @Inject constructor( synchronized(media) { val mediaValue = media.value!! val index = mediaValue.indexOfFirst { it.localId == newMediaItem.localId } - media.postValue(if (index == -1) { - mediaValue + newMediaItem - } else { - mediaValue.toMutableList().also { it[index] = newMediaItem } - }) + media.postValue( + if (index == -1) { + mediaValue + newMediaItem + } else { + mediaValue.toMutableList().also { it[index] = newMediaItem } + } + ) } - }, { error -> + }, + { error -> media.postValue(media.value?.filter { it.localId != mediaItem.localId } ?: emptyList()) uploadError.postValue(error) - }) + } + ) return mediaItem } @@ -198,12 +223,14 @@ class ComposeViewModel @Inject constructor( fun didChange(content: String?, contentWarning: String?): Boolean { - val textChanged = !(content.isNullOrEmpty() - || startingText?.startsWith(content.toString()) ?: false) + val textChanged = !( + content.isNullOrEmpty() || + startingText?.startsWith(content.toString()) ?: false + ) - val contentWarningChanged = showContentWarning.value!! - && !contentWarning.isNullOrEmpty() - && !startingContentWarning.startsWith(contentWarning.toString()) + val contentWarningChanged = showContentWarning.value!! && + !contentWarning.isNullOrEmpty() && + !startingContentWarning.startsWith(contentWarning.toString()) val mediaChanged = !media.value.isNullOrEmpty() val pollChanged = poll.value != null @@ -254,8 +281,8 @@ class ComposeViewModel @Inject constructor( * @return LiveData which will signal once the screen can be closed or null if there are errors */ fun sendStatus( - content: String, - spoilerText: String + content: String, + spoilerText: String ): LiveData { val deletionObservable = if (isEditingScheduledToot) { @@ -265,39 +292,39 @@ class ComposeViewModel @Inject constructor( }.toLiveData() val sendObservable = media - .filter { items -> items.all { it.uploadPercent == -1 } } - .map { - val mediaIds = ArrayList() - val mediaUris = ArrayList() - val mediaDescriptions = ArrayList() - for (item in media.value!!) { - mediaIds.add(item.id!!) - mediaUris.add(item.uri) - mediaDescriptions.add(item.description ?: "") - } - - val tootToSend = TootToSend( - text = content, - warningText = spoilerText, - visibility = statusVisibility.value!!.serverString(), - sensitive = mediaUris.isNotEmpty() && (markMediaAsSensitive.value!! || showContentWarning.value!!), - mediaIds = mediaIds, - mediaUris = mediaUris.map { it.toString() }, - mediaDescriptions = mediaDescriptions, - scheduledAt = scheduledAt.value, - inReplyToId = inReplyToId, - poll = poll.value, - replyingStatusContent = null, - replyingStatusAuthorUsername = null, - accountId = accountManager.activeAccount!!.id, - draftId = draftId, - idempotencyKey = randomAlphanumericString(16), - retries = 0 - ) - - serviceClient.sendToot(tootToSend) + .filter { items -> items.all { it.uploadPercent == -1 } } + .map { + val mediaIds = ArrayList() + val mediaUris = ArrayList() + val mediaDescriptions = ArrayList() + for (item in media.value!!) { + mediaIds.add(item.id!!) + mediaUris.add(item.uri) + mediaDescriptions.add(item.description ?: "") } + val tootToSend = TootToSend( + text = content, + warningText = spoilerText, + visibility = statusVisibility.value!!.serverString(), + sensitive = mediaUris.isNotEmpty() && (markMediaAsSensitive.value!! || showContentWarning.value!!), + mediaIds = mediaIds, + mediaUris = mediaUris.map { it.toString() }, + mediaDescriptions = mediaDescriptions, + scheduledAt = scheduledAt.value, + inReplyToId = inReplyToId, + poll = poll.value, + replyingStatusContent = null, + replyingStatusAuthorUsername = null, + accountId = accountManager.activeAccount!!.id, + draftId = draftId, + idempotencyKey = randomAlphanumericString(16), + retries = 0 + ) + + serviceClient.sendToot(tootToSend) + } + return combineLiveData(deletionObservable, sendObservable) { _, _ -> } } @@ -316,12 +343,15 @@ class ComposeViewModel @Inject constructor( media.removeObserver(this) } else if (updatedItem.id != null) { api.updateMedia(updatedItem.id, description) - .subscribe({ + .subscribe( + { completedCaptioningLiveData.postValue(true) - }, { + }, + { completedCaptioningLiveData.postValue(false) - }) - .autoDispose() + } + ) + .autoDispose() media.removeObserver(this) } } @@ -334,8 +364,8 @@ class ComposeViewModel @Inject constructor( '@' -> { return try { api.searchAccounts(query = token.substring(1), limit = 10) - .blockingGet() - .map { ComposeAutoCompleteAdapter.AccountResult(it) } + .blockingGet() + .map { ComposeAutoCompleteAdapter.AccountResult(it) } } catch (e: Throwable) { Log.e(TAG, String.format("Autocomplete search for %s failed.", token), e) emptyList() @@ -344,9 +374,9 @@ class ComposeViewModel @Inject constructor( '#' -> { return try { api.searchObservable(query = token, type = SearchType.Hashtag.apiParameter, limit = 10) - .blockingGet() - .hashtags - .map { ComposeAutoCompleteAdapter.HashtagResult(it) } + .blockingGet() + .hashtags + .map { ComposeAutoCompleteAdapter.HashtagResult(it) } } catch (e: Throwable) { Log.e(TAG, String.format("Autocomplete search for %s failed.", token), e) emptyList() @@ -389,7 +419,8 @@ class ComposeViewModel @Inject constructor( val replyVisibility = composeOptions?.replyVisibility ?: Status.Visibility.UNKNOWN startingVisibility = Status.Visibility.byNum( - preferredVisibility.num.coerceAtLeast(replyVisibility.num)) + preferredVisibility.num.coerceAtLeast(replyVisibility.num) + ) inReplyToId = composeOptions?.inReplyToId @@ -468,7 +499,6 @@ class ComposeViewModel @Inject constructor( private companion object { const val TAG = "ComposeViewModel" } - } fun mutableLiveData(default: T) = MutableLiveData().apply { value = default } @@ -478,10 +508,10 @@ private const val DEFAULT_MAX_OPTION_COUNT = 4 private const val DEFAULT_MAX_OPTION_LENGTH = 25 data class ComposeInstanceParams( - val maxChars: Int, - val pollMaxOptions: Int, - val pollMaxLength: Int, - val supportsScheduled: Boolean + val maxChars: Int, + val pollMaxOptions: Int, + val pollMaxLength: Int, + val supportsScheduled: Boolean ) /** diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt index 82a7dae50..7511dc3c3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt @@ -37,83 +37,83 @@ import java.util.Locale import javax.inject.Inject class DraftHelper @Inject constructor( - val context: Context, - db: AppDatabase + val context: Context, + db: AppDatabase ) { private val draftDao = db.draftDao() suspend fun saveDraft( - draftId: Int, - accountId: Long, - inReplyToId: String?, - content: String?, - contentWarning: String?, - sensitive: Boolean, - visibility: Status.Visibility, - mediaUris: List, - mediaDescriptions: List, - poll: NewPoll?, - failedToSend: Boolean + draftId: Int, + accountId: Long, + inReplyToId: String?, + content: String?, + contentWarning: String?, + sensitive: Boolean, + visibility: Status.Visibility, + mediaUris: List, + mediaDescriptions: List, + poll: NewPoll?, + failedToSend: Boolean ) = withContext(Dispatchers.IO) { - val externalFilesDir = context.getExternalFilesDir("Tusky") + val externalFilesDir = context.getExternalFilesDir("Tusky") - if (externalFilesDir == null || !(externalFilesDir.exists())) { - Log.e("DraftHelper", "Error obtaining directory to save media.") - throw Exception() + if (externalFilesDir == null || !(externalFilesDir.exists())) { + Log.e("DraftHelper", "Error obtaining directory to save media.") + throw Exception() + } + + val draftDirectory = File(externalFilesDir, "Drafts") + + if (!draftDirectory.exists()) { + draftDirectory.mkdir() + } + + val uris = mediaUris.map { uriString -> + uriString.toUri() + }.map { uri -> + if (uri.isNotInFolder(draftDirectory)) { + uri.copyToFolder(draftDirectory) + } else { + uri } + } - val draftDirectory = File(externalFilesDir, "Drafts") - - if (!draftDirectory.exists()) { - draftDirectory.mkdir() + val types = uris.map { uri -> + val mimeType = context.contentResolver.getType(uri) + when (mimeType?.substring(0, mimeType.indexOf('/'))) { + "video" -> DraftAttachment.Type.VIDEO + "image" -> DraftAttachment.Type.IMAGE + "audio" -> DraftAttachment.Type.AUDIO + else -> throw IllegalStateException("unknown media type") } + } - val uris = mediaUris.map { uriString -> - uriString.toUri() - }.map { uri -> - if (uri.isNotInFolder(draftDirectory)) { - uri.copyToFolder(draftDirectory) - } else { - uri - } - } - - val types = uris.map { uri -> - val mimeType = context.contentResolver.getType(uri) - when (mimeType?.substring(0, mimeType.indexOf('/'))) { - "video" -> DraftAttachment.Type.VIDEO - "image" -> DraftAttachment.Type.IMAGE - "audio" -> DraftAttachment.Type.AUDIO - else -> throw IllegalStateException("unknown media type") - } - } - - val attachments: MutableList = mutableListOf() - for (i in mediaUris.indices) { - attachments.add( - DraftAttachment( - uriString = uris[i].toString(), - description = mediaDescriptions[i], - type = types[i] - ) + val attachments: MutableList = mutableListOf() + for (i in mediaUris.indices) { + attachments.add( + DraftAttachment( + uriString = uris[i].toString(), + description = mediaDescriptions[i], + type = types[i] ) - } - - val draft = DraftEntity( - id = draftId, - accountId = accountId, - inReplyToId = inReplyToId, - content = content, - contentWarning = contentWarning, - sensitive = sensitive, - visibility = visibility, - attachments = attachments, - poll = poll, - failedToSend = failedToSend ) + } - draftDao.insertOrReplace(draft) + val draft = DraftEntity( + id = draftId, + accountId = accountId, + inReplyToId = inReplyToId, + content = content, + contentWarning = contentWarning, + sensitive = sensitive, + visibility = visibility, + attachments = attachments, + poll = poll, + failedToSend = failedToSend + ) + + draftDao.insertOrReplace(draft) } suspend fun deleteDraftAndAttachments(draftId: Int) { @@ -162,5 +162,4 @@ class DraftHelper @Inject constructor( IOUtils.copyToFile(contentResolver, this, file) return FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", file) } - -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsAdapter.kt index 42c93b0b0..18621fd3f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsAdapter.kt @@ -34,17 +34,17 @@ interface DraftActionListener { } class DraftsAdapter( - private val listener: DraftActionListener + private val listener: DraftActionListener ) : PagingDataAdapter>( - object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: DraftEntity, newItem: DraftEntity): Boolean { - return oldItem.id == newItem.id - } - - override fun areContentsTheSame(oldItem: DraftEntity, newItem: DraftEntity): Boolean { - return oldItem == newItem - } + object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: DraftEntity, newItem: DraftEntity): Boolean { + return oldItem.id == newItem.id } + + override fun areContentsTheSame(oldItem: DraftEntity, newItem: DraftEntity): Boolean { + return oldItem == newItem + } + } ) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt index 0df191f81..06a4dee00 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt @@ -93,7 +93,7 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec } if (loadState.refresh is LoadState.NotLoading) { binding.progressBar.hide() - if(adapter.itemCount == 0) { + if (adapter.itemCount == 0) { binding.errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_scheduled_status) binding.errorMessageView.show() } else { @@ -117,16 +117,19 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec } override fun edit(item: ScheduledStatus) { - val intent = ComposeActivity.startIntent(this, ComposeActivity.ComposeOptions( - scheduledTootId = item.id, - tootText = item.params.text, - contentWarning = item.params.spoilerText, - mediaAttachments = item.mediaAttachments, - inReplyToId = item.params.inReplyToId, - visibility = item.params.visibility, - scheduledAt = item.scheduledAt, - sensitive = item.params.sensitive - )) + val intent = ComposeActivity.startIntent( + this, + ComposeActivity.ComposeOptions( + scheduledTootId = item.id, + tootText = item.params.text, + contentWarning = item.params.spoilerText, + mediaAttachments = item.mediaAttachments, + inReplyToId = item.params.inReplyToId, + visibility = item.params.visibility, + scheduledAt = item.scheduledAt, + sensitive = item.params.sensitive + ) + ) startActivity(intent) } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootAdapter.kt index e21019ee7..75b83e5d2 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootAdapter.kt @@ -30,18 +30,17 @@ interface ScheduledTootActionListener { } class ScheduledTootAdapter( - val listener: ScheduledTootActionListener + val listener: ScheduledTootActionListener ) : PagingDataAdapter>( - object: DiffUtil.ItemCallback(){ - override fun areItemsTheSame(oldItem: ScheduledStatus, newItem: ScheduledStatus): Boolean { - return oldItem.id == newItem.id - } - - override fun areContentsTheSame(oldItem: ScheduledStatus, newItem: ScheduledStatus): Boolean { - return oldItem == newItem - } - + object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: ScheduledStatus, newItem: ScheduledStatus): Boolean { + return oldItem.id == newItem.id } + + override fun areContentsTheSame(oldItem: ScheduledStatus, newItem: ScheduledStatus): Boolean { + return oldItem == newItem + } + } ) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder { @@ -50,7 +49,7 @@ class ScheduledTootAdapter( } override fun onBindViewHolder(holder: BindingHolder, position: Int) { - getItem(position)?.let{ item -> + getItem(position)?.let { item -> holder.binding.edit.isEnabled = true holder.binding.delete.isEnabled = true holder.binding.text.text = item.params.text diff --git a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootPagingSource.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootPagingSource.kt index 0578c5b12..c4994cef6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootPagingSource.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootPagingSource.kt @@ -24,7 +24,7 @@ import kotlinx.coroutines.rx3.await class ScheduledTootPagingSourceFactory( private val mastodonApi: MastodonApi -): () -> ScheduledTootPagingSource { +) : () -> ScheduledTootPagingSource { private val scheduledTootsCache = mutableListOf() @@ -45,7 +45,7 @@ class ScheduledTootPagingSourceFactory( class ScheduledTootPagingSource( private val mastodonApi: MastodonApi, private val scheduledTootsCache: MutableList -): PagingSource() { +) : PagingSource() { override fun getRefreshKey(state: PagingState): String? { return null