From c78c06c30b4a4050029c3606a1b75183d3807ed3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 9 Jun 2023 14:19:05 +0200 Subject: [PATCH 001/128] Update dependency org.jetbrains.kotlin:kotlin-gradle-plugin to v1.8.22 (#3569) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 143f7b8c4..178f2bc2a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -33,7 +33,7 @@ glide = "4.15.1" # Deliberate downgrade, https://github.com/tuskyapp/Tusky/issues/3631 glide-animation-plugin = "2.23.0" gson = "2.10.1" -kotlin = "1.8.20" +kotlin = "1.8.22" image-cropper = "4.3.2" material = "1.8.0" material-drawer = "8.4.5" From 192c6979c625046eb1caad47c27f3e7ba369e9ff Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 9 Jun 2023 14:21:17 +0200 Subject: [PATCH 002/128] Update plugin ktlint to v11.4.0 (#3573) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 178f2bc2a..6b08e569f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -59,7 +59,7 @@ android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" } kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } -ktlint = "org.jlleitschuh.gradle.ktlint:11.3.1" +ktlint = "org.jlleitschuh.gradle.ktlint:11.4.0" [libraries] android-material = { module = "com.google.android.material:material", version.ref = "material" } From bf35c0e36fe15158017956989b8c6a9f4e752c91 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 9 Jun 2023 15:12:00 +0200 Subject: [PATCH 003/128] Update dagger to v2.46.1 (#3592) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6b08e569f..642d67b32 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,7 +24,7 @@ autodispose = "2.1.1" bouncycastle = "1.70" conscrypt = "2.5.2" coroutines = "1.6.4" -dagger = "2.45" +dagger = "2.46.1" diffx = "1.1.1" emoji2 = "1.3.0" espresso = "3.5.1" From cd2e3038aad80d2dce089f549106a6efdb5e2ab1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 9 Jun 2023 15:47:35 +0200 Subject: [PATCH 004/128] Update dependency org.robolectric:robolectric to v4.10.3 (#3613) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 642d67b32..417d327b4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -43,7 +43,7 @@ mockito-kotlin = "4.1.0" networkresult-calladapter = "1.0.0" okhttp = "4.11.0" retrofit = "2.9.0" -robolectric = "4.10" +robolectric = "4.10.3" rxandroid3 = "3.0.2" rxjava3 = "3.1.6" rxkotlin3 = "3.0.1" From 8a8c587979ab2d194b1a46ca81efa181774cca33 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 9 Jun 2023 17:23:09 +0200 Subject: [PATCH 005/128] Update dependency com.google.android.material:material to v1.9.0 (#3624) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 417d327b4..719bedb09 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -35,7 +35,7 @@ glide-animation-plugin = "2.23.0" gson = "2.10.1" kotlin = "1.8.22" image-cropper = "4.3.2" -material = "1.8.0" +material = "1.9.0" material-drawer = "8.4.5" material-typeface = "4.0.0.2-kotlin" mockito-inline = "5.2.0" From e4fc80db54017f17701be0547781bb13d938924f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 9 Jun 2023 19:54:03 +0200 Subject: [PATCH 006/128] Update coroutines to v1.7.1 (#3627) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 719bedb09..817886a24 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,7 +23,7 @@ androidx-room = "2.5.1" autodispose = "2.1.1" bouncycastle = "1.70" conscrypt = "2.5.2" -coroutines = "1.6.4" +coroutines = "1.7.1" dagger = "2.46.1" diffx = "1.1.1" emoji2 = "1.3.0" From b5c9fefda869484c51c5d0d5aa8b30ae07043904 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 9 Jun 2023 20:20:21 +0200 Subject: [PATCH 007/128] Update dependency app.cash.turbine:turbine to v0.13.0 (#3677) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 817886a24..ae26efbab 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -50,7 +50,7 @@ rxkotlin3 = "3.0.1" photoview = "2.3.0" sparkbutton = "4.1.0" truth = "1.1.3" -turbine = "0.12.3" +turbine = "0.13.0" unified-push = "2.1.1" xmlwriter = "1.0.4" From 129d07c49b164f32f1088de06318673df4439585 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 9 Jun 2023 21:14:15 +0200 Subject: [PATCH 008/128] Update dependency androidx.activity:activity-ktx to v1.7.2 (#3703) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ae26efbab..b3074e520 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] agp = "7.4.2" -androidx-activity = "1.7.1" +androidx-activity = "1.7.2" androidx-appcompat = "1.6.1" androidx-browser = "1.5.0" androidx-cardview = "1.0.0" From 1278c5e0ec5afcf1f77309b680be18173a6bf9b9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 9 Jun 2023 22:26:52 +0200 Subject: [PATCH 009/128] Update dependency androidx.core:core-ktx to v1.10.1 (#3704) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b3074e520..8e3f262db 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ androidx-appcompat = "1.6.1" androidx-browser = "1.5.0" androidx-cardview = "1.0.0" androidx-constraintlayout = "2.1.4" -androidx-core = "1.10.0" +androidx-core = "1.10.1" androidx-exifinterface = "1.3.6" androidx-fragment = "1.5.7" androidx-junit = "1.1.5" From 1d3e781f14e61a764ec8a63ce16a161566f38cf5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 10 Jun 2023 15:06:36 +0200 Subject: [PATCH 010/128] Update dependency com.google.truth:truth to v1.1.4 (#3721) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8e3f262db..bf501efe2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -49,7 +49,7 @@ rxjava3 = "3.1.6" rxkotlin3 = "3.0.1" photoview = "2.3.0" sparkbutton = "4.1.0" -truth = "1.1.3" +truth = "1.1.4" turbine = "0.13.0" unified-push = "2.1.1" xmlwriter = "1.0.4" From 020d427f0a38ac0f67291e3548df6ac63c8b40b5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 10 Jun 2023 15:44:25 +0200 Subject: [PATCH 011/128] Update dependency androidx.fragment:fragment-ktx to v1.6.0 (#3722) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bf501efe2..98861d138 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,7 +7,7 @@ androidx-cardview = "1.0.0" androidx-constraintlayout = "2.1.4" androidx-core = "1.10.1" androidx-exifinterface = "1.3.6" -androidx-fragment = "1.5.7" +androidx-fragment = "1.6.0" androidx-junit = "1.1.5" androidx-lifecycle = "2.6.1" androidx-paging = "3.1.1" From 291f0f5bd26561cf0e15b17f0653903e5d3064ee Mon Sep 17 00:00:00 2001 From: Weblate <42475313+nailyk-weblate@users.noreply.github.com> Date: Sat, 10 Jun 2023 16:17:51 +0200 Subject: [PATCH 012/128] Translations update from Weblate (#3729) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Translated using Weblate (German) Currently translated at 100.0% (28 of 28 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/de/ * Translated using Weblate (Persian) Currently translated at 100.0% (28 of 28 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/fa/ * Translated using Weblate (Vietnamese) Currently translated at 100.0% (28 of 28 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/vi/ --------- Co-authored-by: Deleted User Co-authored-by: Danial Behzadi Co-authored-by: Hồ Nhất Duy --- .../metadata/android/de/changelogs/109.txt | 6 ++++++ .../metadata/android/de/changelogs/110.txt | 16 +++++++++++++++ .../metadata/android/fa/changelogs/107.txt | 2 +- .../metadata/android/fa/changelogs/110.txt | 20 +++++++++++++++++++ .../metadata/android/fa/changelogs/89.txt | 10 +++++----- .../metadata/android/fa/full_description.txt | 2 +- .../metadata/android/vi/changelogs/107.txt | 6 ++++++ .../metadata/android/vi/changelogs/108.txt | 5 +++++ .../metadata/android/vi/changelogs/109.txt | 10 ++++++++++ .../metadata/android/vi/changelogs/110.txt | 18 +++++++++++++++++ 10 files changed, 88 insertions(+), 7 deletions(-) create mode 100644 fastlane/metadata/android/de/changelogs/109.txt create mode 100644 fastlane/metadata/android/de/changelogs/110.txt create mode 100644 fastlane/metadata/android/fa/changelogs/110.txt create mode 100644 fastlane/metadata/android/vi/changelogs/107.txt create mode 100644 fastlane/metadata/android/vi/changelogs/108.txt create mode 100644 fastlane/metadata/android/vi/changelogs/109.txt create mode 100644 fastlane/metadata/android/vi/changelogs/110.txt diff --git a/fastlane/metadata/android/de/changelogs/109.txt b/fastlane/metadata/android/de/changelogs/109.txt new file mode 100644 index 000000000..529a95dd6 --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/109.txt @@ -0,0 +1,6 @@ +Tusky 22.0 Beta 7 + +Behobene Fehler: +- Alle ausstehenden Mastodon-Benachrichtigungen werden nun beim Erstellen von Android-Benachrichtigungen abgerufen +- Beim Anlicken auf »Beitrag erstellen« in einer Benachrichtigung wurde das falsche Konto ausgewählt +- Die »ID der zuletzt gelesenen Benachrichtigung« wird nun im richtigen Konto gespeichert diff --git a/fastlane/metadata/android/de/changelogs/110.txt b/fastlane/metadata/android/de/changelogs/110.txt new file mode 100644 index 000000000..ddab5079c --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/110.txt @@ -0,0 +1,16 @@ +Tusky 22.0 + +Neu: +- Sieh dir angesagte Hashtags an +- Bessere Sortierung bei der Sprachauswahl +- Unterschiede zwischen den Versionen eines Beitrags ansehen +- Unterstützung für Filter aus Mastodon V4 +- Option zum Anzeigen von Beitragsstatistiken in der Timeline +- Und mehr … + +Behobene Fehler: +- Ausgewählter Tab und Position wird gespeichert +- Benachrichtigungen bleiben bis zum Lesen erhalten +- Korrekte Berechnung der Beitragslänge +- Bildbeschreibungen werden immer veröffentlicht +- Und mehr … diff --git a/fastlane/metadata/android/fa/changelogs/107.txt b/fastlane/metadata/android/fa/changelogs/107.txt index 59288ead2..98001e19f 100644 --- a/fastlane/metadata/android/fa/changelogs/107.txt +++ b/fastlane/metadata/android/fa/changelogs/107.txt @@ -3,4 +3,4 @@ رفع اشکال‌ها: - برگرداندن کتابخانهٔ APNG برای تعمیر اموجی‌های پویا -- ذخیرهٔ رونوشت محلی از علامت‌زن آگاهی در صورتی که کارساز از API پشتیبانی نکند +- ذخیرهٔ رونوشت محلی از علامت‌زن آگاهی در صورتی که کارساز از میانا پشتیبانی نکند diff --git a/fastlane/metadata/android/fa/changelogs/110.txt b/fastlane/metadata/android/fa/changelogs/110.txt new file mode 100644 index 000000000..8a372a59c --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/110.txt @@ -0,0 +1,20 @@ +تاسکی ۲۲٫۰ + +ویژگی‌های جدید: + +- دیدن برچسب‌های داغ +- پی‌گیری برچسب‌های جدید +- چینش بهتر هنگام گزینش زبان‌ها +- نمایش تفاوت بین نگارش‌های فرسته +- پشیبانی پالایه‌های ماستودون ن۴ +- گزینه برای نمایش آمار فرسته‌ها در خط زمانی +- و بیش‌تر… + +رفع اشکال‌ها: + +- به یاد سپردن مکان و زبانهٔ گزیده +- نگه داشتن آگاهی‌ها تا زمان خوانده شدن +- نمایش درست متن دوجهته در نمایه‌ها +- تصحیح محاسبهٔ طول فرسته +- نشر همیشگی متن تصویر +- و بیش‌تر… diff --git a/fastlane/metadata/android/fa/changelogs/89.txt b/fastlane/metadata/android/fa/changelogs/89.txt index 99c574794..971a841f3 100644 --- a/fastlane/metadata/android/fa/changelogs/89.txt +++ b/fastlane/metadata/android/fa/changelogs/89.txt @@ -1,7 +1,7 @@ تاسکی ن۱۷٫۰ -- "Open as..." is now also available in the menu on account profiles when using multiple accounts -- Login is now handled in a WebView within the app -- Support for Android 12 -- support for the new Mastodon instance configuration API -- and a lot of other small fixes and improvements +- اکنون هنگام استفاده از چندین حساب «گشودن به عنوان…» در فهرست روی نمایه‌های حساب نیز موجود است +- ورود اکنون در نمای وبی درون کاره مدیریت می‌شود +- پشتیبانی از اندروید ۱۲ +- پشتیبانی از میانای جدید پیکربندی نمونهٔ ماستودون +- و بسیاری از بهبودها و رفع مشکلات کوچک diff --git a/fastlane/metadata/android/fa/full_description.txt b/fastlane/metadata/android/fa/full_description.txt index f552647d8..5958cb5ab 100644 --- a/fastlane/metadata/android/fa/full_description.txt +++ b/fastlane/metadata/android/fa/full_description.txt @@ -1,7 +1,7 @@ تاسکی کارخواهی سبک برای ماستودون، یک کارساز شبکهٔ اجتماعی نرم‌افزار آزاد است. • طرّاحی متریال -• پیاده‌سازی اکثر APIهای ماستودون +• پیاده‌سازی بیش‌تر میاناهای ماستودون • پشتیبانی از چند حساب • پشتیبانی از زمینهٔ تاریک و روشن با امکان تغییر خودکار بر اساس زمان روز • پیش‌نویس‌ها - ایجاد بوق‌ها و ذخیره‌شان برای بعد diff --git a/fastlane/metadata/android/vi/changelogs/107.txt b/fastlane/metadata/android/vi/changelogs/107.txt new file mode 100644 index 000000000..7298707af --- /dev/null +++ b/fastlane/metadata/android/vi/changelogs/107.txt @@ -0,0 +1,6 @@ +Tusky 22.0 beta 5 + +Fixes: + +- Rolled back APNG library to fix broken animated emojis +- Save local copy of notification marker in case server does not support the API diff --git a/fastlane/metadata/android/vi/changelogs/108.txt b/fastlane/metadata/android/vi/changelogs/108.txt new file mode 100644 index 000000000..f4e1b0c62 --- /dev/null +++ b/fastlane/metadata/android/vi/changelogs/108.txt @@ -0,0 +1,5 @@ +Tusky 22.0 beta 6 + +Fixes: + +- Save reading position in the Notifications tab more frequently diff --git a/fastlane/metadata/android/vi/changelogs/109.txt b/fastlane/metadata/android/vi/changelogs/109.txt new file mode 100644 index 000000000..edc99c83c --- /dev/null +++ b/fastlane/metadata/android/vi/changelogs/109.txt @@ -0,0 +1,10 @@ +Tusky 22.0 beta 7 + +Fixes: + + +### Significant bug fixes + +- Fetch all outstanding Mastodon notifications when creating Android notifications +- Clicking "Compose" from a notification would set the wrong account +- Ensure "last read notification ID" is saved to the correct account diff --git a/fastlane/metadata/android/vi/changelogs/110.txt b/fastlane/metadata/android/vi/changelogs/110.txt new file mode 100644 index 000000000..330f7a3df --- /dev/null +++ b/fastlane/metadata/android/vi/changelogs/110.txt @@ -0,0 +1,18 @@ +Tusky 22.0 + +Tính năng mới: +- Xem hashtag xu hướng +- Theo dõi hashtag +- Chọn ngôn ngữ dễ hơn +- Xem lịch sử sửa tút +- Hỗ trợ bộ lọc Mastodon v4 +- Hiện số tương tác tút trên bảng tin +- Còn nữa... + +Sửa: +- Nhớ tab đã chọn và vị trí +- Giữ thông báo cho tới khi đọc +- Nội dung RTL và LTR trên hồ sơ +- Tính toán độ dài tút +- Luôn đăng mô tả ảnh +- Còn nữa... From dd1020e48a7b8032c6710f0777eec019a61a21bb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 10 Jun 2023 16:25:43 +0200 Subject: [PATCH 013/128] Update dependency org.mockito.kotlin:mockito-kotlin to v5 (#3724) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 98861d138..6bf83872a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -39,7 +39,7 @@ material = "1.9.0" material-drawer = "8.4.5" material-typeface = "4.0.0.2-kotlin" mockito-inline = "5.2.0" -mockito-kotlin = "4.1.0" +mockito-kotlin = "5.0.0" networkresult-calladapter = "1.0.0" okhttp = "4.11.0" retrofit = "2.9.0" From 071e00774e9e53357b17768195d8f27e885fca15 Mon Sep 17 00:00:00 2001 From: Nik Clayton Date: Sat, 10 Jun 2023 16:29:26 +0200 Subject: [PATCH 014/128] Replace shortNumber() with formatNumber() (#3519) formatNumber() was existing code to show numbers with suffixes like K, M, etc, so re-use that code and delete shortNumber(). Update the tests to (a) test formatNumber(), and (b) be parameterised. --- .../tusky/adapter/StatusBaseViewHolder.java | 2 +- .../tusky/adapter/StatusViewHolder.java | 4 +- .../tusky/adapter/TrendingTagViewHolder.kt | 25 +----- .../keylesspalace/tusky/util/NumberUtils.kt | 36 ++++---- .../tusky/util/NumberUtilsTest.kt | 89 ++++++++++++------- 5 files changed, 78 insertions(+), 78 deletions(-) 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 3bc1a26fd..de4d23802 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java @@ -397,7 +397,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { if (replyCountLabel == null) return; if (fullStats) { - replyCountLabel.setText(NumberUtils.shortNumber(repliesCount)); + replyCountLabel.setText(NumberUtils.formatNumber(repliesCount, 1000)); return; } 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 0763b96ae..304cf93a5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java @@ -114,11 +114,11 @@ public class StatusViewHolder extends StatusBaseViewHolder { } protected void setReblogsCount(int reblogsCount) { - reblogsCountLabel.setText(NumberUtils.shortNumber(reblogsCount)); + reblogsCountLabel.setText(NumberUtils.formatNumber(reblogsCount, 1000)); } protected void setFavouritedCount(int favouritedCount) { - favouritedCountLabel.setText(NumberUtils.shortNumber(favouritedCount)); + favouritedCountLabel.setText(NumberUtils.formatNumber(favouritedCount, 1000)); } protected void hideStatusInfo() { diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/TrendingTagViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/TrendingTagViewHolder.kt index f852c46b8..71a83c98c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/TrendingTagViewHolder.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/TrendingTagViewHolder.kt @@ -20,10 +20,8 @@ import com.keylesspalace.tusky.R import com.keylesspalace.tusky.databinding.ItemTrendingCellBinding import com.keylesspalace.tusky.entity.TrendingTagHistory import com.keylesspalace.tusky.interfaces.LinkListener +import com.keylesspalace.tusky.util.formatNumber import com.keylesspalace.tusky.viewdata.TrendingViewData -import java.text.NumberFormat -import kotlin.math.ln -import kotlin.math.pow class TrendingTagViewHolder( private val binding: ItemTrendingCellBinding @@ -70,25 +68,4 @@ class TrendingTagViewHolder( itemView.contentDescription = itemView.context.getString(R.string.accessibility_talking_about_tag, totalAccounts, tag) } - - companion object { - private val numberFormatter: NumberFormat = NumberFormat.getInstance() - private val ln_1k = ln(1000.0) - - /** - * Format numbers according to the current locale. Numbers < min have - * separators (',', '.', etc) inserted according to the locale. - * - * Numbers > min are scaled down to that by multiples of 1,000, and - * a suffix appropriate to the scaling is appended. - */ - private fun formatNumber(num: Long, min: Int = 100000): String { - if (num < min) return numberFormatter.format(num) - - val exp = (ln(num.toDouble()) / ln_1k).toInt() - - // TODO: is the choice of suffixes here locale-agnostic? - return String.format("%.1f %c", num / 1000.0.pow(exp.toDouble()), "KMGTPE"[exp - 1]) - } - } } diff --git a/app/src/main/java/com/keylesspalace/tusky/util/NumberUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/NumberUtils.kt index 6adb4d809..29a2ec67c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/NumberUtils.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/NumberUtils.kt @@ -2,25 +2,27 @@ package com.keylesspalace.tusky.util -import java.text.DecimalFormat +import java.text.NumberFormat import kotlin.math.abs -import kotlin.math.floor -import kotlin.math.log10 +import kotlin.math.ln import kotlin.math.pow -import kotlin.math.sign -val shortLetters = arrayOf(' ', 'K', 'M', 'B', 'T', 'P', 'E') +private val numberFormatter: NumberFormat = NumberFormat.getInstance() +private val ln_1k = ln(1000.0) -fun shortNumber(number: Number): String { - val numberAsDouble = number.toDouble() - val nonNegativeValue = abs(numberAsDouble) - var sign = "" - if (numberAsDouble.sign < 0) { sign = "-" } - val value = floor(log10(nonNegativeValue)).toInt() - val base = value / 3 - if (value >= 3 && base < shortLetters.size) { - return DecimalFormat("$sign#0.0").format(nonNegativeValue / 10.0.pow((base * 3).toDouble())) + shortLetters[base] - } else { - return DecimalFormat("$sign#,##0").format(nonNegativeValue) - } +/** + * Format numbers according to the current locale. Numbers < min have + * separators (',', '.', etc) inserted according to the locale. + * + * Numbers >= min are scaled down to that by multiples of 1,000, and + * a suffix appropriate to the scaling is appended. + */ +fun formatNumber(num: Long, min: Int = 100000): String { + val absNum = abs(num) + if (absNum < min) return numberFormatter.format(num) + + val exp = (ln(absNum.toDouble()) / ln_1k).toInt() + + // Suffixes here are locale-agnostic + return String.format("%.1f%c", num / 1000.0.pow(exp.toDouble()), "KMGTPE"[exp - 1]) } diff --git a/app/src/test/java/com/keylesspalace/tusky/util/NumberUtilsTest.kt b/app/src/test/java/com/keylesspalace/tusky/util/NumberUtilsTest.kt index 3654176b8..25ac69ee1 100644 --- a/app/src/test/java/com/keylesspalace/tusky/util/NumberUtilsTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/util/NumberUtilsTest.kt @@ -1,49 +1,70 @@ package com.keylesspalace.tusky.util +import org.junit.AfterClass import org.junit.Assert +import org.junit.BeforeClass import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import java.util.Locale import kotlin.math.pow -class NumberUtilsTest { +@RunWith(Parameterized::class) +class NumberUtilsTest(private val input: Long, private val want: String) { + companion object { + /** Default locale before this test started */ + private lateinit var locale: Locale - @Test - fun zeroShouldBeFormattedAsZero() { - val shortNumber = shortNumber(0) - Assert.assertEquals("0", shortNumber) - } + /** + * Ensure the Locale is ENGLISH so that tests against literal strings like + * "1.0M" later, even if the test host's locale is e.g. GERMAN which would + * normally report "1,0M". + */ + @BeforeClass + @JvmStatic + fun beforeClass() { + locale = Locale.getDefault() + Locale.setDefault(Locale.ENGLISH) + } - @Test - fun negativeValueShouldBeFormattedToNegativeValue() { - val shortNumber = shortNumber(-1) - Assert.assertEquals("-1", shortNumber) - } + @AfterClass + @JvmStatic + fun afterClass() { + Locale.setDefault(locale) + } - @Test - fun positiveValueShouldBeFormattedToPositiveValue() { - val shortNumber = shortNumber(1) - Assert.assertEquals("1", shortNumber) - } - - @Test - fun bigNumbersShouldBeShortened() { - var shortNumber = 1L - Assert.assertEquals("1", shortNumber(shortNumber)) - for (i in shortLetters.indices) { - if (i == 0) { - continue - } - shortNumber = 1000.0.pow(i.toDouble()).toLong() - Assert.assertEquals("1.0" + shortLetters[i], shortNumber(shortNumber)) + @Parameterized.Parameters(name = "formatNumber_{0}") + @JvmStatic + fun data(): Iterable { + return listOf( + arrayOf(0, "0"), + arrayOf(1, "1"), + arrayOf(-1, "-1"), + arrayOf(999, "999"), + arrayOf(1000, "1.0K"), + arrayOf(1500, "1.5K"), + arrayOf(-1500, "-1.5K"), + arrayOf(1000.0.pow(2).toLong(), "1.0M"), + arrayOf(1000.0.pow(3).toLong(), "1.0G"), + arrayOf(1000.0.pow(4).toLong(), "1.0T"), + arrayOf(1000.0.pow(5).toLong(), "1.0P"), + arrayOf(1000.0.pow(6).toLong(), "1.0E"), + arrayOf(3, "3"), + arrayOf(35, "35"), + arrayOf(350, "350"), + arrayOf(3500, "3.5K"), + arrayOf(-3500, "-3.5K"), + arrayOf(3500 * 1000, "3.5M"), + arrayOf(3500 * 1000.0.pow(2).toLong(), "3.5G"), + arrayOf(3500 * 1000.0.pow(3).toLong(), "3.5T"), + arrayOf(3500 * 1000.0.pow(4).toLong(), "3.5P"), + arrayOf(3500 * 1000.0.pow(5).toLong(), "3.5E") + ) } } @Test - fun roundingForNegativeAndPositiveValuesShouldBeTheSame() { - var value = 3492 - Assert.assertEquals("-3.5K", shortNumber(-value)) - Assert.assertEquals("3.5K", shortNumber(value)) - value = 1501 - Assert.assertEquals("-1.5K", shortNumber(-value)) - Assert.assertEquals("1.5K", shortNumber(value)) + fun test() { + Assert.assertEquals(want, formatNumber(input, 1000)) } } From f23c0cc634d28a64e276eaa42db188a4e3a1bf9f Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sat, 10 Jun 2023 19:47:07 +0200 Subject: [PATCH 015/128] Refactor "trending hashtags" code (#3595) - Fix codeformatting - Add new refreshing state - Disable LogConditional lint rule - Update lint-baseline --- app/lint-baseline.xml | 392 ++++++++---------- .../tusky/adapter/TrendingTagViewHolder.kt | 71 ---- .../components/trending/TrendingActivity.kt | 18 +- .../components/trending/TrendingAdapter.kt | 44 +- .../trending}/TrendingDateViewHolder.kt | 2 +- .../components/trending/TrendingFragment.kt | 84 +--- .../trending/TrendingTagViewHolder.kt | 57 +++ .../trending/viewmodel/TrendingViewModel.kt | 74 ++-- .../tusky/entity/TrendingTagsResult.kt | 8 +- .../tusky/network/MastodonApi.kt | 2 +- .../keylesspalace/tusky/util/ViewDataUtils.kt | 23 +- .../com/keylesspalace/tusky/view/GraphView.kt | 22 +- .../tusky/viewdata/TrendingViewData.kt | 18 +- 13 files changed, 335 insertions(+), 480 deletions(-) delete mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/TrendingTagViewHolder.kt rename app/src/main/java/com/keylesspalace/tusky/{adapter => components/trending}/TrendingDateViewHolder.kt (96%) create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/trending/TrendingTagViewHolder.kt diff --git a/app/lint-baseline.xml b/app/lint-baseline.xml index 03638736f..a02d2c01c 100644 --- a/app/lint-baseline.xml +++ b/app/lint-baseline.xml @@ -113,7 +113,7 @@ errorLine2=" ~~~~~~~"> @@ -311,7 +311,7 @@ errorLine2=" ^"> @@ -355,7 +355,7 @@ errorLine2=" ^"> @@ -366,7 +366,7 @@ errorLine2=" ^"> @@ -388,7 +388,7 @@ errorLine2=" ^"> @@ -399,7 +399,7 @@ errorLine2=" ^"> @@ -410,7 +410,7 @@ errorLine2=" ^"> @@ -432,7 +432,7 @@ errorLine2=" ^"> @@ -454,7 +454,7 @@ errorLine2=" ^"> @@ -465,7 +465,7 @@ errorLine2=" ^"> @@ -476,7 +476,7 @@ errorLine2=" ^"> @@ -487,7 +487,7 @@ errorLine2=" ^"> @@ -498,7 +498,7 @@ errorLine2=" ^"> @@ -509,7 +509,7 @@ errorLine2=" ^"> @@ -520,7 +520,7 @@ errorLine2=" ^"> @@ -531,7 +531,7 @@ errorLine2=" ^"> @@ -542,7 +542,7 @@ errorLine2=" ^"> @@ -553,7 +553,7 @@ errorLine2=" ^"> @@ -564,7 +564,7 @@ errorLine2=" ^"> @@ -575,7 +575,7 @@ errorLine2=" ^"> @@ -586,7 +586,7 @@ errorLine2=" ^"> @@ -597,7 +597,7 @@ errorLine2=" ^"> @@ -784,7 +784,7 @@ errorLine2=" ^"> @@ -795,7 +795,7 @@ errorLine2=" ^"> @@ -806,7 +806,7 @@ errorLine2=" ^"> @@ -817,7 +817,7 @@ errorLine2=" ^"> @@ -828,7 +828,7 @@ errorLine2=" ^"> @@ -839,7 +839,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -850,7 +850,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -894,7 +894,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -905,7 +905,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -960,7 +960,7 @@ errorLine2=" ^"> @@ -971,7 +971,7 @@ errorLine2=" ^"> @@ -982,7 +982,7 @@ errorLine2=" ^"> @@ -993,7 +993,7 @@ errorLine2=" ^"> @@ -1004,7 +1004,7 @@ errorLine2=" ^"> @@ -1015,7 +1015,7 @@ errorLine2=" ^"> @@ -1026,7 +1026,7 @@ errorLine2=" ^"> @@ -1037,7 +1037,7 @@ errorLine2=" ^"> @@ -1048,7 +1048,7 @@ errorLine2=" ^"> @@ -1059,7 +1059,7 @@ errorLine2=" ^"> @@ -1070,7 +1070,7 @@ errorLine2=" ^"> @@ -1081,7 +1081,7 @@ errorLine2=" ^"> @@ -1092,7 +1092,7 @@ errorLine2=" ^"> @@ -1103,7 +1103,7 @@ errorLine2=" ^"> @@ -1114,21 +1114,21 @@ errorLine2=" ^"> + + + + - - - - @@ -1147,7 +1147,7 @@ errorLine2=" ^"> @@ -1158,7 +1158,7 @@ errorLine2=" ^"> @@ -1169,7 +1169,7 @@ errorLine2=" ^"> @@ -1180,7 +1180,7 @@ errorLine2=" ^"> @@ -1191,7 +1191,7 @@ errorLine2=" ^"> @@ -1202,7 +1202,7 @@ errorLine2=" ^"> @@ -1213,7 +1213,7 @@ errorLine2=" ^"> @@ -1224,7 +1224,7 @@ errorLine2=" ^"> @@ -1235,7 +1235,7 @@ errorLine2=" ^"> @@ -1246,7 +1246,7 @@ errorLine2=" ^"> @@ -1257,7 +1257,7 @@ errorLine2=" ^"> @@ -1268,7 +1268,7 @@ errorLine2=" ^"> @@ -1279,7 +1279,7 @@ errorLine2=" ^"> @@ -1290,7 +1290,7 @@ errorLine2=" ^"> @@ -1301,7 +1301,7 @@ errorLine2=" ^"> @@ -1312,7 +1312,7 @@ errorLine2=" ^"> @@ -1323,7 +1323,7 @@ errorLine2=" ^"> @@ -1334,7 +1334,7 @@ errorLine2=" ^"> @@ -1345,7 +1345,7 @@ errorLine2=" ^"> @@ -1356,7 +1356,7 @@ errorLine2=" ^"> @@ -1367,7 +1367,7 @@ errorLine2=" ^"> @@ -1378,7 +1378,7 @@ errorLine2=" ^"> @@ -1389,7 +1389,7 @@ errorLine2=" ^"> @@ -1400,7 +1400,7 @@ errorLine2=" ^"> @@ -1411,7 +1411,7 @@ errorLine2=" ^"> @@ -1422,7 +1422,7 @@ errorLine2=" ^"> @@ -1433,7 +1433,7 @@ errorLine2=" ^"> @@ -2548,17 +2548,6 @@ column="13"/> - - - - @@ -2588,7 +2577,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> @@ -2599,7 +2588,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~"> @@ -2610,7 +2599,7 @@ errorLine2=" ~~~~~~~~~~~~"> @@ -2621,7 +2610,7 @@ errorLine2=" ~~~~~~~~~~~~~~"> @@ -2632,7 +2621,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -2643,7 +2632,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> @@ -2654,7 +2643,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> @@ -2665,7 +2654,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -2676,7 +2665,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> @@ -2687,7 +2676,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -2698,7 +2687,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -2709,7 +2698,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> @@ -2720,7 +2709,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -2731,7 +2720,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -2742,7 +2731,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -2753,7 +2742,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~"> @@ -2764,7 +2753,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -2775,7 +2764,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -2786,7 +2775,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -2797,7 +2786,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -4157,7 +4146,7 @@ + errorLine1=" binding.recyclerView.scrollBy(0, Utils.dpToPx(requireContext(), -30))" + errorLine2=" ~~~~~~~"> + line="215" + column="29"/> @@ -4645,29 +4634,29 @@ errorLine2=" ~~~~~~~"> @@ -4677,8 +4666,8 @@ errorLine1=" binding.totalAccounts.text = formatNumber(totalAccounts)" errorLine2=" ~~~~~~~~~~~~"> @@ -4688,8 +4677,8 @@ errorLine1=" binding.totalAccounts.text = formatNumber(totalAccounts)" errorLine2=" ~~~~~~~~~~~~"> @@ -4843,7 +4832,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -4854,7 +4843,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -4865,7 +4854,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -4876,7 +4865,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -4920,7 +4909,7 @@ errorLine2=" ~~~~~~~~~~"> @@ -5019,7 +5008,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> @@ -5030,7 +5019,7 @@ errorLine2=" ~~~~~~~~~"> @@ -5041,7 +5030,7 @@ errorLine2=" ~~~~~~~~~"> @@ -5052,7 +5041,7 @@ errorLine2=" ~~~~~~~~~"> @@ -5063,7 +5052,7 @@ errorLine2=" ~~~~~~~~~"> @@ -5074,18 +5063,18 @@ errorLine2=" ~~~~~~~~~"> @@ -5118,7 +5107,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> @@ -5129,7 +5118,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> @@ -5140,7 +5129,7 @@ errorLine2=" ^"> @@ -5151,7 +5140,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -5162,7 +5151,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> @@ -5173,7 +5162,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -5184,7 +5173,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~"> @@ -5195,17 +5184,10 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> - - - - @@ -5686,7 +5668,7 @@ errorLine2=" ~~~~~~~~"> @@ -5697,7 +5679,7 @@ errorLine2=" ~~~~~~~~"> @@ -5708,7 +5690,7 @@ errorLine2=" ~~~~~~~~"> @@ -6192,7 +6174,7 @@ errorLine2=" ~~~~~~~~"> @@ -6203,7 +6185,7 @@ errorLine2=" ~~~~~~~~"> @@ -6214,7 +6196,7 @@ errorLine2=" ~~~~~~~~"> @@ -6225,7 +6207,7 @@ errorLine2=" ~~~~~~~~"> @@ -6588,7 +6570,7 @@ errorLine2=" ~~~~~~~~~"> @@ -6702,28 +6684,6 @@ column="9"/> - - - - - - - - @@ -7369,7 +7329,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~"> @@ -7380,7 +7340,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~"> @@ -7391,7 +7351,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~"> @@ -7402,7 +7362,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~"> @@ -7413,7 +7373,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~"> @@ -7424,7 +7384,7 @@ errorLine2=" ~~~~~~"> @@ -7435,7 +7395,7 @@ errorLine2=" ~~~~~~"> @@ -7446,7 +7406,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~"> @@ -7457,7 +7417,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> @@ -7468,7 +7428,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~"> @@ -7479,7 +7439,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~"> @@ -7490,7 +7450,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~"> @@ -7501,7 +7461,7 @@ errorLine2=" ~~~~~~~~~~~~"> @@ -7512,7 +7472,7 @@ errorLine2=" ~~~~~~~"> @@ -7523,7 +7483,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~"> @@ -7534,7 +7494,7 @@ errorLine2=" ~~~~~~~~~~~~"> @@ -7545,7 +7505,7 @@ errorLine2=" ~~~~~~~"> @@ -7556,7 +7516,7 @@ errorLine2=" ~~~~~~~~~~~~"> @@ -7567,7 +7527,7 @@ errorLine2=" ~~~~~~~"> @@ -7578,7 +7538,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> @@ -7589,7 +7549,7 @@ errorLine2=" ~~~~~~~~~~~~"> @@ -7600,7 +7560,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~"> @@ -7611,7 +7571,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~"> diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/TrendingTagViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/TrendingTagViewHolder.kt deleted file mode 100644 index 71a83c98c..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/TrendingTagViewHolder.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* Copyright 2023 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 androidx.recyclerview.widget.RecyclerView -import com.keylesspalace.tusky.R -import com.keylesspalace.tusky.databinding.ItemTrendingCellBinding -import com.keylesspalace.tusky.entity.TrendingTagHistory -import com.keylesspalace.tusky.interfaces.LinkListener -import com.keylesspalace.tusky.util.formatNumber -import com.keylesspalace.tusky.viewdata.TrendingViewData - -class TrendingTagViewHolder( - private val binding: ItemTrendingCellBinding -) : RecyclerView.ViewHolder(binding.root) { - - fun setup( - tagViewData: TrendingViewData.Tag, - maxTrendingValue: Long, - trendingListener: LinkListener - ) { - val reversedHistory = tagViewData.tag.history.reversed() - setGraph(reversedHistory, maxTrendingValue) - setTag(tagViewData.tag.name) - - val totalUsage = tagViewData.tag.history.sumOf { it.uses.toLongOrNull() ?: 0 } - binding.totalUsage.text = formatNumber(totalUsage) - - val totalAccounts = tagViewData.tag.history.sumOf { it.accounts.toLongOrNull() ?: 0 } - binding.totalAccounts.text = formatNumber(totalAccounts) - - binding.currentUsage.text = reversedHistory.last().uses - binding.currentAccounts.text = reversedHistory.last().accounts - - itemView.setOnClickListener { - trendingListener.onViewTag(tagViewData.tag.name) - } - - setAccessibility(totalAccounts, tagViewData.tag.name) - } - - private fun setGraph(history: List, maxTrendingValue: Long) { - binding.graph.maxTrendingValue = maxTrendingValue - binding.graph.primaryLineData = history - .mapNotNull { it.uses.toLongOrNull() } - binding.graph.secondaryLineData = history - .mapNotNull { it.accounts.toLongOrNull() } - } - - private fun setTag(tag: String) { - binding.tag.text = binding.root.context.getString(R.string.title_tag, tag) - } - - private fun setAccessibility(totalAccounts: Long, tag: String) { - itemView.contentDescription = - itemView.context.getString(R.string.accessibility_talking_about_tag, totalAccounts, tag) - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/trending/TrendingActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/trending/TrendingActivity.kt index e9bd3b70c..3270e9c26 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/trending/TrendingActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/trending/TrendingActivity.kt @@ -19,23 +19,19 @@ import android.content.Context import android.content.Intent import android.os.Bundle import androidx.fragment.app.commit -import com.keylesspalace.tusky.BottomSheetActivity +import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.R -import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.databinding.ActivityTrendingBinding import com.keylesspalace.tusky.util.viewBinding import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector import javax.inject.Inject -class TrendingActivity : BottomSheetActivity(), HasAndroidInjector { +class TrendingActivity : BaseActivity(), HasAndroidInjector { @Inject lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector - @Inject - lateinit var eventHub: EventHub - private val binding: ActivityTrendingBinding by viewBinding(ActivityTrendingBinding::inflate) override fun onCreate(savedInstanceState: Bundle?) { @@ -44,10 +40,8 @@ class TrendingActivity : BottomSheetActivity(), HasAndroidInjector { setSupportActionBar(binding.includedToolbar.toolbar) - val title = getString(R.string.title_public_trending_hashtags) - supportActionBar?.run { - setTitle(title) + setTitle(R.string.title_public_trending_hashtags) setDisplayHomeAsUpEnabled(true) setDisplayShowHomeEnabled(true) } @@ -63,10 +57,6 @@ class TrendingActivity : BottomSheetActivity(), HasAndroidInjector { override fun androidInjector() = dispatchingAndroidInjector companion object { - const val TAG = "TrendingActivity" - - @JvmStatic - fun getIntent(context: Context) = - Intent(context, TrendingActivity::class.java) + fun getIntent(context: Context) = Intent(context, TrendingActivity::class.java) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/trending/TrendingAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/trending/TrendingAdapter.kt index 6f405e2f4..b40d67670 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/trending/TrendingAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/trending/TrendingAdapter.kt @@ -20,15 +20,12 @@ import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView -import com.keylesspalace.tusky.adapter.TrendingDateViewHolder -import com.keylesspalace.tusky.adapter.TrendingTagViewHolder import com.keylesspalace.tusky.databinding.ItemTrendingCellBinding import com.keylesspalace.tusky.databinding.ItemTrendingDateBinding -import com.keylesspalace.tusky.interfaces.LinkListener import com.keylesspalace.tusky.viewdata.TrendingViewData class TrendingAdapter( - private val trendingListener: LinkListener + private val onViewTag: (String) -> Unit ) : ListAdapter(TrendingDifferCallback) { init { @@ -42,7 +39,6 @@ class TrendingAdapter( ItemTrendingCellBinding.inflate(LayoutInflater.from(viewGroup.context)) TrendingTagViewHolder(binding) } - else -> { val binding = ItemTrendingDateBinding.inflate(LayoutInflater.from(viewGroup.context)) @@ -52,38 +48,15 @@ class TrendingAdapter( } override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) { - bindViewHolder(viewHolder, position, null) - } - - override fun onBindViewHolder( - viewHolder: RecyclerView.ViewHolder, - position: Int, - payloads: List<*> - ) { - bindViewHolder(viewHolder, position, payloads) - } - - private fun bindViewHolder( - viewHolder: RecyclerView.ViewHolder, - position: Int, - payloads: List<*>? - ) { - when (val header = getItem(position)) { + when (val viewData = getItem(position)) { is TrendingViewData.Tag -> { - val maxTrendingValue = currentList - .flatMap { trendingViewData -> - trendingViewData.asTagOrNull()?.tag?.history.orEmpty() - } - .mapNotNull { it.uses.toLongOrNull() } - .maxOrNull() ?: 1 - val holder = viewHolder as TrendingTagViewHolder - holder.setup(header, maxTrendingValue, trendingListener) + holder.setup(viewData, onViewTag) } is TrendingViewData.Header -> { val holder = viewHolder as TrendingDateViewHolder - holder.setup(header.start, header.end) + holder.setup(viewData.start, viewData.end) } } } @@ -112,14 +85,7 @@ class TrendingAdapter( oldItem: TrendingViewData, newItem: TrendingViewData ): Boolean { - return false - } - - override fun getChangePayload( - oldItem: TrendingViewData, - newItem: TrendingViewData - ): Any? { - return null + return oldItem == newItem } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/TrendingDateViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/components/trending/TrendingDateViewHolder.kt similarity index 96% rename from app/src/main/java/com/keylesspalace/tusky/adapter/TrendingDateViewHolder.kt rename to app/src/main/java/com/keylesspalace/tusky/components/trending/TrendingDateViewHolder.kt index 481573068..5d9e3c3af 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/TrendingDateViewHolder.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/trending/TrendingDateViewHolder.kt @@ -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.trending import androidx.recyclerview.widget.RecyclerView import com.keylesspalace.tusky.R diff --git a/app/src/main/java/com/keylesspalace/tusky/components/trending/TrendingFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/trending/TrendingFragment.kt index 4148a1086..bcb22d050 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/trending/TrendingFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/trending/TrendingFragment.kt @@ -15,17 +15,14 @@ package com.keylesspalace.tusky.components.trending -import android.content.Context import android.content.res.Configuration 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.Fragment -import androidx.lifecycle.ViewModelProvider +import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup @@ -33,18 +30,14 @@ import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.SimpleItemAnimator import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener import at.connyduck.sparkbutton.helpers.Utils -import com.keylesspalace.tusky.BottomSheetActivity -import com.keylesspalace.tusky.PostLookupFallbackBehavior +import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.R import com.keylesspalace.tusky.StatusListActivity -import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.components.trending.viewmodel.TrendingViewModel import com.keylesspalace.tusky.databinding.FragmentTrendingBinding -import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.interfaces.ActionButtonActivity -import com.keylesspalace.tusky.interfaces.LinkListener import com.keylesspalace.tusky.interfaces.RefreshableFragment import com.keylesspalace.tusky.interfaces.ReselectableFragment import com.keylesspalace.tusky.util.hide @@ -56,48 +49,20 @@ import kotlinx.coroutines.launch import javax.inject.Inject class TrendingFragment : - Fragment(), + Fragment(R.layout.fragment_trending), OnRefreshListener, - LinkListener, Injectable, ReselectableFragment, RefreshableFragment { - private lateinit var bottomSheetActivity: BottomSheetActivity - @Inject lateinit var viewModelFactory: ViewModelFactory - @Inject - lateinit var accountManager: AccountManager - - @Inject - lateinit var eventHub: EventHub - - private val viewModel: TrendingViewModel by lazy { - ViewModelProvider(this, viewModelFactory)[TrendingViewModel::class.java] - } + private val viewModel: TrendingViewModel by viewModels { viewModelFactory } private val binding by viewBinding(FragmentTrendingBinding::bind) - private lateinit var adapter: TrendingAdapter - - override fun onAttach(context: Context) { - super.onAttach(context) - bottomSheetActivity = if (context is BottomSheetActivity) { - context - } else { - throw IllegalStateException("Fragment must be attached to a BottomSheetActivity!") - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - adapter = TrendingAdapter( - this - ) - } + private val adapter = TrendingAdapter(::onViewTag) override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) @@ -106,14 +71,6 @@ class TrendingFragment : setupLayoutManager(columnCount) } - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - return inflater.inflate(R.layout.fragment_trending, container, false) - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { setupSwipeRefreshLayout() setupRecyclerView() @@ -175,25 +132,19 @@ class TrendingFragment : } override fun onRefresh() { - viewModel.invalidate() + viewModel.invalidate(true) } - override fun onViewUrl(url: String) { - bottomSheetActivity.viewUrl(url, PostLookupFallbackBehavior.OPEN_IN_BROWSER) - } - - override fun onViewTag(tag: String) { - bottomSheetActivity.startActivityWithSlideInAnimation(StatusListActivity.newHashtagIntent(requireContext(), tag)) - } - - override fun onViewAccount(id: String) { - bottomSheetActivity.viewAccount(id) + fun onViewTag(tag: String) { + (requireActivity() as BaseActivity).startActivityWithSlideInAnimation(StatusListActivity.newHashtagIntent(requireContext(), tag)) } private fun processViewState(uiState: TrendingViewModel.TrendingUiState) { + Log.d(TAG, uiState.loadingState.name) when (uiState.loadingState) { TrendingViewModel.LoadingState.INITIAL -> clearLoadingState() TrendingViewModel.LoadingState.LOADING -> applyLoadingState() + TrendingViewModel.LoadingState.REFRESHING -> applyRefreshingState() TrendingViewModel.LoadingState.LOADED -> applyLoadedState(uiState.trendingViewData) TrendingViewModel.LoadingState.ERROR_NETWORK -> networkError() TrendingViewModel.LoadingState.ERROR_OTHER -> otherError() @@ -203,8 +154,9 @@ class TrendingFragment : private fun applyLoadedState(viewData: List) { clearLoadingState() + adapter.submitList(viewData) + if (viewData.isEmpty()) { - adapter.submitList(emptyList()) binding.recyclerView.hide() binding.messageView.show() binding.messageView.setup( @@ -213,16 +165,16 @@ class TrendingFragment : null ) } else { - val viewDataWithDates = listOf(viewData.first().asHeaderOrNull()) + viewData - - adapter.submitList(viewDataWithDates) - binding.recyclerView.show() binding.messageView.hide() } binding.progressBar.hide() } + private fun applyRefreshingState() { + binding.swipeRefreshLayout.isRefreshing = true + } + private fun applyLoadingState() { binding.recyclerView.hide() binding.messageView.hide() @@ -297,8 +249,6 @@ class TrendingFragment : companion object { private const val TAG = "TrendingFragment" - fun newInstance(): TrendingFragment { - return TrendingFragment() - } + fun newInstance() = TrendingFragment() } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/trending/TrendingTagViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/components/trending/TrendingTagViewHolder.kt new file mode 100644 index 000000000..58aad9e84 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/trending/TrendingTagViewHolder.kt @@ -0,0 +1,57 @@ +/* Copyright 2023 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.trending + +import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ItemTrendingCellBinding +import com.keylesspalace.tusky.util.formatNumber +import com.keylesspalace.tusky.viewdata.TrendingViewData + +class TrendingTagViewHolder( + private val binding: ItemTrendingCellBinding +) : RecyclerView.ViewHolder(binding.root) { + + fun setup( + tagViewData: TrendingViewData.Tag, + onViewTag: (String) -> Unit + ) { + binding.tag.text = binding.root.context.getString(R.string.title_tag, tagViewData.name) + + binding.graph.maxTrendingValue = tagViewData.maxTrendingValue + binding.graph.primaryLineData = tagViewData.usage + binding.graph.secondaryLineData = tagViewData.accounts + + binding.totalUsage.text = formatNumber(tagViewData.usage.sum(), 1000) + + val totalAccounts = tagViewData.accounts.sum() + binding.totalAccounts.text = formatNumber(totalAccounts, 1000) + + binding.currentUsage.text = tagViewData.usage.last().toString() + binding.currentAccounts.text = tagViewData.usage.last().toString() + + itemView.setOnClickListener { + onViewTag(tagViewData.name) + } + + itemView.contentDescription = + itemView.context.getString( + R.string.accessibility_talking_about_tag, + totalAccounts, + tagViewData.name + ) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/trending/viewmodel/TrendingViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/trending/viewmodel/TrendingViewModel.kt index 7bf3aecfb..d1877a2fc 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/trending/viewmodel/TrendingViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/trending/viewmodel/TrendingViewModel.kt @@ -15,11 +15,15 @@ package com.keylesspalace.tusky.components.trending.viewmodel +import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import at.connyduck.calladapter.networkresult.fold import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.PreferenceChangedEvent import com.keylesspalace.tusky.entity.Filter +import com.keylesspalace.tusky.entity.end +import com.keylesspalace.tusky.entity.start import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.toViewData import com.keylesspalace.tusky.viewdata.TrendingViewData @@ -28,7 +32,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.launch -import okio.IOException +import java.io.IOException import javax.inject.Inject class TrendingViewModel @Inject constructor( @@ -36,7 +40,7 @@ class TrendingViewModel @Inject constructor( private val eventHub: EventHub ) : ViewModel() { enum class LoadingState { - INITIAL, LOADING, LOADED, ERROR_NETWORK, ERROR_OTHER + INITIAL, LOADING, REFRESHING, LOADED, ERROR_NETWORK, ERROR_OTHER } data class TrendingUiState( @@ -67,37 +71,43 @@ class TrendingViewModel @Inject constructor( * * A tag is excluded if it is filtered by the user on their home timeline. */ - fun invalidate() = viewModelScope.launch { - _uiState.value = TrendingUiState(emptyList(), LoadingState.LOADING) - - try { - val deferredFilters = async { mastodonApi.getFilters() } - val response = mastodonApi.trendingTags() - if (!response.isSuccessful) { - _uiState.value = TrendingUiState(emptyList(), LoadingState.ERROR_NETWORK) - return@launch - } - - val homeFilters = deferredFilters.await().getOrNull()?.filter { - it.context.contains(Filter.Kind.HOME.kind) - } - - val tags = response.body()!! - .filter { - homeFilters?.none { filter -> - filter.keywords.any { keyword -> keyword.keyword.equals(it.name, ignoreCase = true) } - } ?: false - } - .sortedBy { tag -> tag.history.sumOf { it.uses.toLongOrNull() ?: 0 } } - .map { it.toViewData() } - .asReversed() - - _uiState.value = TrendingUiState(tags, LoadingState.LOADED) - } catch (e: IOException) { - _uiState.value = TrendingUiState(emptyList(), LoadingState.ERROR_NETWORK) - } catch (e: Exception) { - _uiState.value = TrendingUiState(emptyList(), LoadingState.ERROR_OTHER) + fun invalidate(refresh: Boolean = false) = viewModelScope.launch { + if (refresh) { + _uiState.value = TrendingUiState(emptyList(), LoadingState.REFRESHING) + } else { + _uiState.value = TrendingUiState(emptyList(), LoadingState.LOADING) } + + val deferredFilters = async { mastodonApi.getFilters() } + + mastodonApi.trendingTags().fold( + { tagResponse -> + val homeFilters = deferredFilters.await().getOrNull()?.filter { filter -> + filter.context.contains(Filter.Kind.HOME.kind) + } + val tags = tagResponse + .filter { tag -> + homeFilters?.none { filter -> + filter.keywords.any { keyword -> keyword.keyword.equals(tag.name, ignoreCase = true) } + } ?: false + } + .sortedByDescending { tag -> tag.history.sumOf { it.uses.toLongOrNull() ?: 0 } } + .toViewData() + + val firstTag = tagResponse.first() + val header = TrendingViewData.Header(firstTag.start(), firstTag.end()) + + _uiState.value = TrendingUiState(listOf(header) + tags, LoadingState.LOADED) + }, + { error -> + Log.w(TAG, "failed loading trending tags", error) + if (error is IOException) { + _uiState.value = TrendingUiState(emptyList(), LoadingState.ERROR_NETWORK) + } else { + _uiState.value = TrendingUiState(emptyList(), LoadingState.ERROR_OTHER) + } + } + ) } companion object { diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/TrendingTagsResult.kt b/app/src/main/java/com/keylesspalace/tusky/entity/TrendingTagsResult.kt index 7baca6fe0..786695559 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/TrendingTagsResult.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/TrendingTagsResult.kt @@ -21,15 +21,13 @@ import java.util.Date * Mastodon API Documentation: https://docs.joinmastodon.org/methods/trends/#tags * * @param name The name of the hashtag (after the #). The "caturday" in "#caturday". - * @param url The URL to your mastodon instance list for this hashtag. + * (@param url The URL to your mastodon instance list for this hashtag.) * @param history A list of [TrendingTagHistory]. Each element contains metrics per day for this hashtag. - * @param following This is not listed in the APIs at the time of writing, but an instance is delivering it. + * (@param following This is not listed in the APIs at the time of writing, but an instance is delivering it.) */ data class TrendingTag( val name: String, - val url: String, - val history: List, - val following: Boolean + val history: List ) /** 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 3eb838406..a50ca7964 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -782,5 +782,5 @@ interface MastodonApi { suspend fun unfollowTag(@Path("name") name: String): NetworkResult @GET("api/v1/trends/tags") - suspend fun trendingTags(): Response> + suspend fun trendingTags(): NetworkResult> } 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 1d630f39b..f6ae9e7d7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.kt @@ -40,7 +40,6 @@ import com.keylesspalace.tusky.viewdata.NotificationViewData import com.keylesspalace.tusky.viewdata.StatusViewData import com.keylesspalace.tusky.viewdata.TrendingViewData -@JvmName("statusToViewData") fun Status.toViewData( isShowingContent: Boolean, isExpanded: Boolean, @@ -56,7 +55,6 @@ fun Status.toViewData( ) } -@JvmName("notificationToViewData") fun Notification.toViewData( isShowingContent: Boolean, isExpanded: Boolean, @@ -71,9 +69,20 @@ fun Notification.toViewData( ) } -@JvmName("tagToViewData") -fun TrendingTag.toViewData(): TrendingViewData.Tag { - return TrendingViewData.Tag( - tag = this - ) +fun List.toViewData(): List { + val maxTrendingValue = flatMap { tag -> tag.history } + .mapNotNull { it.uses.toLongOrNull() } + .maxOrNull() ?: 1 + + return map { tag -> + + val reversedHistory = tag.history.asReversed() + + TrendingViewData.Tag( + name = tag.name, + usage = reversedHistory.mapNotNull { it.uses.toLongOrNull() }, + accounts = reversedHistory.mapNotNull { it.accounts.toLongOrNull() }, + maxTrendingValue = maxTrendingValue + ) + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/view/GraphView.kt b/app/src/main/java/com/keylesspalace/tusky/view/GraphView.kt index 565d58a30..2aecad08d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/view/GraphView.kt +++ b/app/src/main/java/com/keylesspalace/tusky/view/GraphView.kt @@ -22,10 +22,9 @@ import android.graphics.Path import android.graphics.PathMeasure import android.graphics.Rect import android.util.AttributeSet +import android.view.View import androidx.annotation.ColorInt import androidx.annotation.Dimension -import androidx.appcompat.widget.AppCompatImageView -import androidx.core.content.ContextCompat import androidx.core.content.res.use import com.keylesspalace.tusky.R import kotlin.math.max @@ -33,9 +32,8 @@ import kotlin.math.max class GraphView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, - defStyleAttr: Int = 0, - defStyleRes: Int = 0 -) : AppCompatImageView(context, attrs, defStyleAttr) { + defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) { @get:ColorInt @ColorInt var primaryLineColor = 0 @@ -55,7 +53,7 @@ class GraphView @JvmOverloads constructor( @ColorInt var metaColor = 0 - var proportionalTrending = false + private var proportionalTrending = false private lateinit var primaryLinePaint: Paint private lateinit var secondaryLinePaint: Paint @@ -129,16 +127,14 @@ class GraphView @JvmOverloads constructor( private fun initFromXML(attr: AttributeSet?) { context.obtainStyledAttributes(attr, R.styleable.GraphView).use { a -> - primaryLineColor = ContextCompat.getColor( - context, + primaryLineColor = context.getColor( a.getResourceId( R.styleable.GraphView_primaryLineColor, R.color.tusky_blue ) ) - secondaryLineColor = ContextCompat.getColor( - context, + secondaryLineColor = context.getColor( a.getResourceId( R.styleable.GraphView_secondaryLineColor, R.color.tusky_red @@ -150,16 +146,14 @@ class GraphView @JvmOverloads constructor( R.dimen.graph_line_thickness ).toFloat() - graphColor = ContextCompat.getColor( - context, + graphColor = context.getColor( a.getResourceId( R.styleable.GraphView_graphColor, R.color.colorBackground ) ) - metaColor = ContextCompat.getColor( - context, + metaColor = context.getColor( a.getResourceId( R.styleable.GraphView_metaColor, R.color.dividerColor diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/TrendingViewData.kt b/app/src/main/java/com/keylesspalace/tusky/viewdata/TrendingViewData.kt index c018aebce..6eae73319 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewdata/TrendingViewData.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/TrendingViewData.kt @@ -15,9 +15,6 @@ package com.keylesspalace.tusky.viewdata -import com.keylesspalace.tusky.entity.TrendingTag -import com.keylesspalace.tusky.entity.end -import com.keylesspalace.tusky.entity.start import java.util.Date sealed class TrendingViewData { @@ -31,18 +28,13 @@ sealed class TrendingViewData { get() = start.toString() + end.toString() } - fun asHeaderOrNull(): Header? { - val tag = (this as? Tag)?.tag - ?: return null - return Header(tag.start(), tag.end()) - } - data class Tag( - val tag: TrendingTag + val name: String, + val usage: List, + val accounts: List, + val maxTrendingValue: Long ) : TrendingViewData() { override val id: String - get() = tag.name + get() = name } - - fun asTagOrNull() = this as? Tag } From 8e87b5d465c0a36ac65fd1a49fd5fcc4b75d8020 Mon Sep 17 00:00:00 2001 From: Nik Clayton Date: Sat, 10 Jun 2023 22:31:59 +0200 Subject: [PATCH 016/128] Downgrade Truth library to 1.1.3 (#3733) It bundles Guava 32.0.0 which has a bug on Windows where temporary directories can't be created, causing tests to fail. See https://github.com/google/truth/issues/1137 and https://github.com/google/guava/issues/6535 --- gradle/libs.versions.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6bf83872a..19b2f6c13 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -49,7 +49,8 @@ rxjava3 = "3.1.6" rxkotlin3 = "3.0.1" photoview = "2.3.0" sparkbutton = "4.1.0" -truth = "1.1.4" +# Deliberate downgrade from 1.1.4, see https://github.com/google/truth/issues/1137 +truth = "1.1.3" turbine = "0.13.0" unified-push = "2.1.1" xmlwriter = "1.0.4" From 85b7caa887cecce3464369344f1eb64acf8162a0 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sun, 11 Jun 2023 12:58:55 +0200 Subject: [PATCH 017/128] Replace deprecated getParcelable* methods with compat versions (#3633) --- app/lint-baseline.xml | 331 +++++++----------- .../keylesspalace/tusky/ViewMediaActivity.kt | 3 +- .../components/compose/ComposeActivity.kt | 10 +- .../compose/dialog/CaptionDialog.kt | 3 +- .../components/compose/dialog/FocusDialog.kt | 4 +- .../components/filters/EditFilterActivity.kt | 3 +- .../components/login/LoginWebViewActivity.kt | 7 +- .../tusky/fragment/ViewImageFragment.kt | 3 +- .../tusky/service/SendStatusService.kt | 3 +- 9 files changed, 148 insertions(+), 219 deletions(-) diff --git a/app/lint-baseline.xml b/app/lint-baseline.xml index a02d2c01c..e01146f54 100644 --- a/app/lint-baseline.xml +++ b/app/lint-baseline.xml @@ -1,39 +1,6 @@ - - - - - - - - - - - - - - - - @@ -1697,7 +1653,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -2258,7 +2214,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -2269,7 +2225,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -2973,7 +2929,7 @@ errorLine2=" ~~~~~~~~~~~~~"> @@ -3160,7 +3116,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~"> @@ -3171,7 +3127,7 @@ errorLine2=" ~~~~~~~~~"> @@ -3182,7 +3138,7 @@ errorLine2=" ~~~~~~~~~"> @@ -3270,7 +3226,7 @@ errorLine2=" ~~~~~~~~~"> @@ -3281,7 +3237,7 @@ errorLine2=" ~~~~~~~~~"> @@ -3292,7 +3248,7 @@ errorLine2=" ~~~~~~~~~"> @@ -3303,7 +3259,7 @@ errorLine2=" ~~~~~~~~~"> @@ -3666,7 +3622,7 @@ errorLine2=" ~~~~~~~"> @@ -3677,7 +3633,7 @@ errorLine2=" ~~~~~~~"> @@ -3688,7 +3644,7 @@ errorLine2=" ~~~~~~~~~~"> @@ -3699,7 +3655,7 @@ errorLine2=" ~~~~~~~~~~"> @@ -3710,7 +3666,7 @@ errorLine2=" ~~~~~~~~~~"> @@ -3719,17 +3675,6 @@ message="Access to `private` method `sendResult` of class `LoginWebViewActivity` requires synthetic accessor" errorLine1=" sendResult(LoginResult.Err(error))" errorLine2=" ~~~~~~~~~~"> - - - - + + + + @@ -4469,7 +4425,7 @@ errorLine2=" ~~~~~~~~~"> @@ -4480,7 +4436,7 @@ errorLine2=" ~~~~~~~"> @@ -4491,7 +4447,7 @@ errorLine2=" ~~~~~~~"> @@ -4579,7 +4535,7 @@ errorLine2=" ~~~~~~~"> @@ -4590,7 +4546,7 @@ errorLine2=" ~~~~~~~"> @@ -4601,7 +4557,7 @@ errorLine2=" ~~~~~~~"> @@ -4612,7 +4568,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -4638,50 +4594,6 @@ column="29"/> - - - - - - - - - - - - - - - - @@ -4700,7 +4612,7 @@ errorLine2=" ~~~~~~~"> @@ -4711,7 +4623,7 @@ errorLine2=" ~~~~~~~"> @@ -4722,7 +4634,7 @@ errorLine2=" ~~~~~~~"> @@ -4733,7 +4645,7 @@ errorLine2=" ~~~~~~~"> @@ -4744,7 +4656,7 @@ errorLine2=" ~~~~~~~~~~~~"> @@ -4755,7 +4667,7 @@ errorLine2=" ~~~~~~~~~~~~"> @@ -4766,7 +4678,7 @@ errorLine2=" ~~~~~~~"> @@ -4777,7 +4689,7 @@ errorLine2=" ~~~~~~~"> @@ -5188,6 +5100,13 @@ column="55"/> + + + + @@ -6845,7 +6764,7 @@ errorLine2=" ~~~~~~~~~~~"> @@ -6856,7 +6775,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~"> @@ -6867,7 +6786,7 @@ errorLine2=" ~~~~~~~~~~~"> @@ -6878,7 +6797,7 @@ errorLine2=" ~~~~~~~~"> @@ -6889,7 +6808,7 @@ errorLine2=" ~~~~"> @@ -6977,7 +6896,7 @@ errorLine2=" ~~~~~~~~"> @@ -6988,41 +6907,85 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> + errorLine1=" public static android.app.Notification make(final Context context, NotificationManager notificationManager, Notification body, AccountEntity account, boolean isFirstOfBatch) {" + errorLine2=" ~~~~~~~"> + line="151" + column="55"/> + errorLine1=" public static android.app.Notification make(final Context context, NotificationManager notificationManager, Notification body, AccountEntity account, boolean isFirstOfBatch) {" + errorLine2=" ~~~~~~~~~~~~~~~~~~~"> + line="151" + column="72"/> + errorLine1=" public static android.app.Notification make(final Context context, NotificationManager notificationManager, Notification body, AccountEntity account, boolean isFirstOfBatch) {" + errorLine2=" ~~~~~~~~~~~~"> + line="151" + column="113"/> + + + + + + + + + + + + + + + + @@ -7043,74 +7006,30 @@ errorLine2=" ~~~~~~~"> + errorLine1=" public static boolean filterNotification(NotificationManager notificationManager, AccountEntity account, @NonNull Notification notification) {" + errorLine2=" ~~~~~~~~~~~~~~~~~~~"> + errorLine1=" public static boolean filterNotification(NotificationManager notificationManager, AccountEntity account, @NonNull Notification notification) {" + errorLine2=" ~~~~~~~~~~~~~"> - - - - - - - - - - - - - - - - + line="620" + column="87"/> { - intent.getParcelableExtra(Intent.EXTRA_STREAM)?.let { uri -> + IntentCompat.getParcelableExtra(intent, Intent.EXTRA_STREAM, Uri::class.java)?.let { uri -> pickMedia(uri) } } Intent.ACTION_SEND_MULTIPLE -> { - intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM)?.forEach { uri -> + IntentCompat.getParcelableArrayListExtra(intent, Intent.EXTRA_STREAM, Uri::class.java)?.forEach { uri -> pickMedia(uri) } } 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 f6f8a495e..a1dc032cd 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 @@ -27,6 +27,7 @@ import android.view.ViewGroup import android.view.WindowManager import android.widget.EditText import androidx.appcompat.app.AlertDialog +import androidx.core.os.BundleCompat import androidx.core.os.bundleOf import androidx.fragment.app.DialogFragment import com.bumptech.glide.Glide @@ -73,7 +74,7 @@ class CaptionDialog : DialogFragment() { val window = dialog.window window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) - val previewUri = arguments?.getParcelable(PREVIEW_URI_ARG) ?: error("Preview Uri is null") + val previewUri = BundleCompat.getParcelable(requireArguments(), PREVIEW_URI_ARG, Uri::class.java) ?: error("Preview Uri is null") // Load the image and manually set it into the ImageView because it doesn't have a fixed size. Glide.with(this) .load(previewUri) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt index c159c41d6..93c99ee6f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt @@ -15,13 +15,13 @@ package com.keylesspalace.tusky.components.compose.dialog -import android.app.Activity import android.content.DialogInterface import android.graphics.drawable.Drawable import android.net.Uri import android.view.WindowManager import android.widget.FrameLayout import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import com.bumptech.glide.Glide @@ -38,7 +38,7 @@ fun T.makeFocusDialog( existingFocus: Focus?, previewUri: Uri, onUpdateFocus: suspend (Focus) -> Unit -) where T : Activity, T : LifecycleOwner { +) where T : AppCompatActivity, T : LifecycleOwner { val focus = existingFocus ?: Focus(0.0f, 0.0f) // Default to center val dialogBinding = DialogFocusBinding.inflate(layoutInflater) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/filters/EditFilterActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/filters/EditFilterActivity.kt index 3150d8d5d..664073949 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/filters/EditFilterActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/filters/EditFilterActivity.kt @@ -7,6 +7,7 @@ import android.widget.AdapterView import android.widget.ArrayAdapter import androidx.activity.viewModels import androidx.appcompat.app.AlertDialog +import androidx.core.content.IntentCompat import androidx.core.view.size import androidx.core.widget.doAfterTextChanged import androidx.lifecycle.lifecycleScope @@ -47,7 +48,7 @@ class EditFilterActivity : BaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - originalFilter = intent?.getParcelableExtra(FILTER_TO_EDIT) + originalFilter = IntentCompat.getParcelableExtra(intent, FILTER_TO_EDIT, Filter::class.java) filter = originalFilter ?: Filter("", "", listOf(), null, Filter.Action.WARN.action, listOf()) binding.apply { contextSwitches = mapOf( diff --git a/app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewActivity.kt index 367eab5b8..be7e323ad 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewActivity.kt @@ -33,6 +33,7 @@ import android.webkit.WebViewClient import androidx.activity.result.contract.ActivityResultContract import androidx.activity.viewModels import androidx.appcompat.app.AlertDialog +import androidx.core.content.IntentCompat import androidx.core.net.toUri import androidx.lifecycle.lifecycleScope import com.keylesspalace.tusky.BaseActivity @@ -61,7 +62,9 @@ class OauthLogin : ActivityResultContract() { return if (resultCode == Activity.RESULT_CANCELED) { LoginResult.Cancel } else { - intent!!.getParcelableExtra(RESULT_EXTRA)!! + intent?.let { + IntentCompat.getParcelableExtra(it, RESULT_EXTRA, LoginResult::class.java) + } ?: LoginResult.Err("failed parsing LoginWebViewActivity result") } } @@ -70,7 +73,7 @@ class OauthLogin : ActivityResultContract() { private const val DATA_EXTRA = "data" fun parseData(intent: Intent): LoginData { - return intent.getParcelableExtra(DATA_EXTRA)!! + return IntentCompat.getParcelableExtra(intent, DATA_EXTRA, LoginData::class.java)!! } fun makeResultIntent(result: LoginResult): Intent { 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 688ce7c58..24c8feb03 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt @@ -26,6 +26,7 @@ import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.widget.ImageView +import androidx.core.os.BundleCompat import com.bumptech.glide.Glide import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.engine.GlideException @@ -92,7 +93,7 @@ class ViewImageFragment : ViewMediaFragment() { super.onViewCreated(view, savedInstanceState) val arguments = this.requireArguments() - val attachment = arguments.getParcelable(ARG_ATTACHMENT) + val attachment = BundleCompat.getParcelable(arguments, ARG_ATTACHMENT, Attachment::class.java) this.shouldStartTransition = arguments.getBoolean(ARG_START_POSTPONED_TRANSITION) val url: String? var description: String? = null diff --git a/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt b/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt index b34c3221e..cf03115b3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt +++ b/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt @@ -16,6 +16,7 @@ import android.util.Log import androidx.annotation.StringRes import androidx.core.app.NotificationCompat import androidx.core.app.ServiceCompat +import androidx.core.content.IntentCompat import at.connyduck.calladapter.networkresult.fold import com.keylesspalace.tusky.MainActivity import com.keylesspalace.tusky.R @@ -83,7 +84,7 @@ class SendStatusService : Service(), Injectable { override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { if (intent.hasExtra(KEY_STATUS)) { - val statusToSend: StatusToSend = intent.getParcelableExtra(KEY_STATUS) + val statusToSend: StatusToSend = IntentCompat.getParcelableExtra(intent, KEY_STATUS, StatusToSend::class.java) ?: throw IllegalStateException("SendStatusService started without $KEY_STATUS extra") if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { From 4025ab35ff193241fd9ab35aff6c268bd449baf0 Mon Sep 17 00:00:00 2001 From: Nik Clayton Date: Sun, 11 Jun 2023 13:17:30 +0200 Subject: [PATCH 018/128] Move cache pruning to a WorkManager worker (#3649) - Extend what was `NotificationWorkerFactory` to `WorkerFactory`. This can construct arbitrary Workers as long as they provide their own Factory for construction. The per-Worker factory contains any injected components just for that worker type, keeping `WorkerFactory` clean. - Move `NotificationWorkerFactory` to the new model. - Implement `PruneCacheWorker`, and remove the code from `CachedTimelineViewModel`. - Create the periodic worker in `TuskyApplication`, ensuring that the database is only pruned when the device is idle. --- .../keylesspalace/tusky/TuskyApplication.kt | 21 ++++++- .../notifications/NotificationHelper.java | 1 + .../notifications/NotificationWorker.kt | 51 ---------------- .../viewmodel/CachedTimelineViewModel.kt | 13 ----- .../keylesspalace/tusky/di/AppComponent.kt | 3 +- .../keylesspalace/tusky/di/WorkerModule.kt | 45 ++++++++++++++ .../receiver/UnifiedPushBroadcastReceiver.kt | 2 +- .../tusky/worker/NotificationWorker.kt | 44 ++++++++++++++ .../tusky/worker/PruneCacheWorker.kt | 58 +++++++++++++++++++ .../tusky/worker/WorkerFactory.kt | 56 ++++++++++++++++++ gradle/libs.versions.toml | 4 +- 11 files changed, 227 insertions(+), 71 deletions(-) delete mode 100644 app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationWorker.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/di/WorkerModule.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/worker/NotificationWorker.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/worker/PruneCacheWorker.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/worker/WorkerFactory.kt diff --git a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt index ef5b8cab4..3107cea8c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt +++ b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt @@ -18,15 +18,19 @@ package com.keylesspalace.tusky import android.app.Application import android.content.SharedPreferences import android.util.Log +import androidx.work.Constraints +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.PeriodicWorkRequestBuilder 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.settings.SCHEMA_VERSION import com.keylesspalace.tusky.util.APP_THEME_DEFAULT import com.keylesspalace.tusky.util.LocaleManager import com.keylesspalace.tusky.util.setAppNightMode +import com.keylesspalace.tusky.worker.PruneCacheWorker +import com.keylesspalace.tusky.worker.WorkerFactory import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector import de.c1710.filemojicompat_defaults.DefaultEmojiPackList @@ -35,6 +39,7 @@ import de.c1710.filemojicompat_ui.helpers.EmojiPreference import io.reactivex.rxjava3.plugins.RxJavaPlugins import org.conscrypt.Conscrypt import java.security.Security +import java.util.concurrent.TimeUnit import javax.inject.Inject class TuskyApplication : Application(), HasAndroidInjector { @@ -42,7 +47,7 @@ class TuskyApplication : Application(), HasAndroidInjector { lateinit var androidInjector: DispatchingAndroidInjector @Inject - lateinit var notificationWorkerFactory: NotificationWorkerFactory + lateinit var workerFactory: WorkerFactory @Inject lateinit var localeManager: LocaleManager @@ -93,9 +98,19 @@ class TuskyApplication : Application(), HasAndroidInjector { WorkManager.initialize( this, androidx.work.Configuration.Builder() - .setWorkerFactory(notificationWorkerFactory) + .setWorkerFactory(workerFactory) .build() ) + + // Prune the database every ~ 12 hours when the device is idle. + val pruneCacheWorker = PeriodicWorkRequestBuilder(12, TimeUnit.HOURS) + .setConstraints(Constraints.Builder().setRequiresDeviceIdle(true).build()) + .build() + WorkManager.getInstance(this).enqueueUniquePeriodicWork( + PruneCacheWorker.PERIODIC_WORK_TAG, + ExistingPeriodicWorkPolicy.KEEP, + pruneCacheWorker + ) } override fun androidInjector() = androidInjector 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 1127c2610..5e7f2ecee 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 @@ -63,6 +63,7 @@ import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.receiver.SendStatusBroadcastReceiver; import com.keylesspalace.tusky.util.StringUtils; import com.keylesspalace.tusky.viewdata.PollViewDataKt; +import com.keylesspalace.tusky.worker.NotificationWorker; import java.util.ArrayList; import java.util.Collections; 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 deleted file mode 100644 index 42b9c869e..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationWorker.kt +++ /dev/null @@ -1,51 +0,0 @@ -/* Copyright 2020 Tusky Contributors - * - * This file is part of Tusky. - * - * Tusky is free software: you can redistribute it and/or modify it under the terms of the GNU - * Lesser 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 Lesser - * General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License along with Tusky. If - * not, see . */ - -package com.keylesspalace.tusky.components.notifications - -import android.content.Context -import androidx.work.ListenableWorker -import androidx.work.Worker -import androidx.work.WorkerFactory -import androidx.work.WorkerParameters -import javax.inject.Inject - -class NotificationWorker( - context: Context, - params: WorkerParameters, - private val notificationsFetcher: NotificationFetcher -) : Worker(context, params) { - - override fun doWork(): Result { - notificationsFetcher.fetchAndShow() - return Result.success() - } -} - -class NotificationWorkerFactory @Inject constructor( - private val notificationsFetcher: NotificationFetcher -) : WorkerFactory() { - - override fun createWorker( - appContext: Context, - workerClassName: String, - workerParameters: WorkerParameters - ): ListenableWorker? { - if (workerClassName == NotificationWorker::class.java.name) { - return NotificationWorker(appContext, workerParameters, notificationsFetcher) - } - return null - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt index 71cdcd2f3..d33bf7e41 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt @@ -50,14 +50,11 @@ import com.keylesspalace.tusky.util.EmptyPagingSource import com.keylesspalace.tusky.viewdata.StatusViewData import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.asExecutor -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import retrofit2.HttpException import javax.inject.Inject -import kotlin.time.DurationUnit -import kotlin.time.toDuration /** * TimelineViewModel that caches all statuses in a local database @@ -107,16 +104,6 @@ class CachedTimelineViewModel @Inject constructor( .flowOn(Dispatchers.Default) .cachedIn(viewModelScope) - init { - viewModelScope.launch { - delay(5.toDuration(DurationUnit.SECONDS)) // delay so the db is not locked during initial ui refresh - accountManager.activeAccount?.id?.let { accountId -> - db.timelineDao().cleanup(accountId, MAX_STATUSES_IN_CACHE) - db.timelineDao().cleanupAccounts(accountId) - } - } - } - override fun updatePoll(newPoll: Poll, status: StatusViewData.Concrete) { // handled by CacheUpdater } 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 2cf480469..73aceeab2 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/AppComponent.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/AppComponent.kt @@ -34,7 +34,8 @@ import javax.inject.Singleton ActivitiesModule::class, ServicesModule::class, BroadcastReceiverModule::class, - ViewModelModule::class + ViewModelModule::class, + WorkerModule::class ] ) interface AppComponent { diff --git a/app/src/main/java/com/keylesspalace/tusky/di/WorkerModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/WorkerModule.kt new file mode 100644 index 000000000..212d4d319 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/di/WorkerModule.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2023 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.di + +import androidx.work.ListenableWorker +import com.keylesspalace.tusky.worker.ChildWorkerFactory +import com.keylesspalace.tusky.worker.NotificationWorker +import com.keylesspalace.tusky.worker.PruneCacheWorker +import dagger.Binds +import dagger.MapKey +import dagger.Module +import dagger.multibindings.IntoMap +import kotlin.reflect.KClass + +@Retention(AnnotationRetention.RUNTIME) +@MapKey +annotation class WorkerKey(val value: KClass) + +@Module +abstract class WorkerModule { + @Binds + @IntoMap + @WorkerKey(NotificationWorker::class) + internal abstract fun bindNotificationWorkerFactory(worker: NotificationWorker.Factory): ChildWorkerFactory + + @Binds + @IntoMap + @WorkerKey(PruneCacheWorker::class) + internal abstract fun bindPruneCacheWorkerFactory(worker: PruneCacheWorker.Factory): ChildWorkerFactory +} diff --git a/app/src/main/java/com/keylesspalace/tusky/receiver/UnifiedPushBroadcastReceiver.kt b/app/src/main/java/com/keylesspalace/tusky/receiver/UnifiedPushBroadcastReceiver.kt index 45a5ae2b6..b95e53105 100644 --- a/app/src/main/java/com/keylesspalace/tusky/receiver/UnifiedPushBroadcastReceiver.kt +++ b/app/src/main/java/com/keylesspalace/tusky/receiver/UnifiedPushBroadcastReceiver.kt @@ -20,11 +20,11 @@ import android.content.Intent import android.util.Log import androidx.work.OneTimeWorkRequest import androidx.work.WorkManager -import com.keylesspalace.tusky.components.notifications.NotificationWorker import com.keylesspalace.tusky.components.notifications.registerUnifiedPushEndpoint import com.keylesspalace.tusky.components.notifications.unregisterUnifiedPushEndpoint import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.worker.NotificationWorker import dagger.android.AndroidInjection import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope diff --git a/app/src/main/java/com/keylesspalace/tusky/worker/NotificationWorker.kt b/app/src/main/java/com/keylesspalace/tusky/worker/NotificationWorker.kt new file mode 100644 index 000000000..84fabd4a0 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/worker/NotificationWorker.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2023 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.worker + +import android.content.Context +import androidx.work.Worker +import androidx.work.WorkerParameters +import com.keylesspalace.tusky.components.notifications.NotificationFetcher +import javax.inject.Inject + +/** Fetch and show new notifications. */ +class NotificationWorker( + appContext: Context, + params: WorkerParameters, + private val notificationsFetcher: NotificationFetcher +) : Worker(appContext, params) { + override fun doWork(): Result { + notificationsFetcher.fetchAndShow() + return Result.success() + } + + class Factory @Inject constructor( + private val notificationsFetcher: NotificationFetcher + ) : ChildWorkerFactory { + override fun createWorker(appContext: Context, params: WorkerParameters): Worker { + return NotificationWorker(appContext, params, notificationsFetcher) + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/worker/PruneCacheWorker.kt b/app/src/main/java/com/keylesspalace/tusky/worker/PruneCacheWorker.kt new file mode 100644 index 000000000..c0ebdb79e --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/worker/PruneCacheWorker.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2023 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.worker + +import android.content.Context +import android.util.Log +import androidx.work.CoroutineWorker +import androidx.work.ListenableWorker +import androidx.work.WorkerParameters +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.AppDatabase +import javax.inject.Inject + +/** Prune the database cache of old statuses. */ +class PruneCacheWorker( + appContext: Context, + workerParams: WorkerParameters, + private val appDatabase: AppDatabase, + private val accountManager: AccountManager +) : CoroutineWorker(appContext, workerParams) { + override suspend fun doWork(): Result { + for (account in accountManager.accounts) { + Log.d(TAG, "Pruning database using account ID: ${account.id}") + appDatabase.timelineDao().cleanup(account.id, MAX_STATUSES_IN_CACHE) + } + return Result.success() + } + + companion object { + private const val TAG = "PruneCacheWorker" + private const val MAX_STATUSES_IN_CACHE = 1000 + const val PERIODIC_WORK_TAG = "PruneCacheWorker_periodic" + } + + class Factory @Inject constructor( + private val appDatabase: AppDatabase, + private val accountManager: AccountManager + ) : ChildWorkerFactory { + override fun createWorker(appContext: Context, params: WorkerParameters): ListenableWorker { + return PruneCacheWorker(appContext, params, appDatabase, accountManager) + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/worker/WorkerFactory.kt b/app/src/main/java/com/keylesspalace/tusky/worker/WorkerFactory.kt new file mode 100644 index 000000000..73c87b2e5 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/worker/WorkerFactory.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2023 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.worker + +import android.content.Context +import androidx.work.ListenableWorker +import androidx.work.WorkerFactory +import androidx.work.WorkerParameters +import javax.inject.Inject +import javax.inject.Provider + +/** + * Workers implement this and are added to the map in [com.keylesspalace.tusky.di.WorkerModule] + * so they can be created by [WorkerFactory.createWorker]. + */ +interface ChildWorkerFactory { + /** Create a new instance of the given worker. */ + fun createWorker(appContext: Context, params: WorkerParameters): ListenableWorker +} + +/** + * Creates workers, delegating to each worker's [ChildWorkerFactory.createWorker] to do the + * creation. + * + * @see [com.keylesspalace.tusky.components.notifications.NotificationWorker] + */ +class WorkerFactory @Inject constructor( + val workerFactories: Map, @JvmSuppressWildcards Provider> +) : WorkerFactory() { + override fun createWorker( + appContext: Context, + workerClassName: String, + workerParameters: WorkerParameters + ): ListenableWorker? { + val key = Class.forName(workerClassName) + workerFactories[key]?.let { + return it.get().createWorker(appContext, workerParameters) + } + return null + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 19b2f6c13..dc4e43217 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -92,7 +92,7 @@ androidx-sharetarget = { module = "androidx.sharetarget:sharetarget", version.re androidx-swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "androidx-swiperefresh-layout" } androidx-test-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-junit" } androidx-viewpager2 = { module = "androidx.viewpager2:viewpager2", version.ref = "androidx-viewpager2" } -androidx-work-runtime = { module = "androidx.work:work-runtime", version.ref = "androidx-work" } +androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "androidx-work" } androidx-work-testing = { module = "androidx.work:work-testing", version.ref = "androidx-work" } autodispose-android-lifecycle = { module = "com.uber.autodispose2:autodispose-androidx-lifecycle", version.ref = "autodispose" } autodispose-core = { module = "com.uber.autodispose2:autodispose", version.ref = "autodispose" } @@ -146,7 +146,7 @@ androidx = ["androidx-core-ktx", "androidx-appcompat", "androidx-fragment-ktx", "androidx-recyclerview", "androidx-exifinterface", "androidx-cardview", "androidx-preference-ktx", "androidx-sharetarget", "androidx-emoji2-core", "androidx-emoji2-views-core", "androidx-emoji2-view-helper", "androidx-lifecycle-viewmodel-ktx", "androidx-lifecycle-livedata-ktx", "androidx-lifecycle-common-java8", "androidx-lifecycle-reactivestreams-ktx", - "androidx-constraintlayout", "androidx-paging-runtime-ktx", "androidx-viewpager2", "androidx-work-runtime", + "androidx-constraintlayout", "androidx-paging-runtime-ktx", "androidx-viewpager2", "androidx-work-runtime-ktx", "androidx-core-splashscreen", "androidx-activity"] autodispose = ["autodispose-core", "autodispose-android-lifecycle"] dagger = ["dagger-core", "dagger-android-core", "dagger-android-support"] From 5e8a63a0463f7f84d5ba9aa27daf02a3d42f8c9f Mon Sep 17 00:00:00 2001 From: Nik Clayton Date: Sun, 11 Jun 2023 13:34:22 +0200 Subject: [PATCH 019/128] Throttle UI actions instead of debouncing (#3651) Introduce Flow.throttleFirst(). In a flow this emits the first value, and each value afterwards that is > some timeout after the previous value. This prevents accidental double-taps on UI elements from generating multiple-actions. The previous code used debounce(). That has a similar effect, but with debounce() the code has to wait until after the timeout period has elapsed before it can process the action, leading to an unnecessary UI delay. With throttleFirst a value is emitted immediately, there's no need to wait. It's subsequent values that are potentially throttled. --- .../notifications/NotificationsViewModel.kt | 12 ++-- .../tusky/util/FlowExtensions.kt | 69 +++++++++++++++++++ .../tusky/util/FlowExtensionsTest.kt | 55 +++++++++++++++ 3 files changed, 131 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/FlowExtensions.kt create mode 100644 app/src/test/java/com/keylesspalace/tusky/util/FlowExtensionsTest.kt diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsViewModel.kt index 0cf5d46bf..ea0f2e8e4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsViewModel.kt @@ -40,6 +40,7 @@ import com.keylesspalace.tusky.usecase.TimelineCases import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.util.deserialize import com.keylesspalace.tusky.util.serialize +import com.keylesspalace.tusky.util.throttleFirst import com.keylesspalace.tusky.util.toViewData import com.keylesspalace.tusky.viewdata.NotificationViewData import com.keylesspalace.tusky.viewdata.StatusViewData @@ -52,7 +53,6 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterIsInstance @@ -65,6 +65,8 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.rx3.await import retrofit2.HttpException import javax.inject.Inject +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.ExperimentalTime data class UiState( /** Filtered notification types */ @@ -274,7 +276,7 @@ sealed class UiError( } } -@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) +@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class, ExperimentalTime::class) class NotificationsViewModel @Inject constructor( private val repository: NotificationsRepository, private val preferences: SharedPreferences, @@ -390,7 +392,7 @@ class NotificationsViewModel @Inject constructor( // Handle NotificationAction.* viewModelScope.launch { uiAction.filterIsInstance() - .debounce(DEBOUNCE_TIMEOUT_MS) + .throttleFirst(THROTTLE_TIMEOUT) .collect { action -> try { when (action) { @@ -409,7 +411,7 @@ class NotificationsViewModel @Inject constructor( // Handle StatusAction.* viewModelScope.launch { uiAction.filterIsInstance() - .debounce(DEBOUNCE_TIMEOUT_MS) // avoid double-taps + .throttleFirst(THROTTLE_TIMEOUT) // avoid double-taps .collect { action -> try { when (action) { @@ -517,6 +519,6 @@ class NotificationsViewModel @Inject constructor( companion object { private const val TAG = "NotificationsViewModel" - private const val DEBOUNCE_TIMEOUT_MS = 500L + private val THROTTLE_TIMEOUT = 500.milliseconds } } diff --git a/app/src/main/java/com/keylesspalace/tusky/util/FlowExtensions.kt b/app/src/main/java/com/keylesspalace/tusky/util/FlowExtensions.kt new file mode 100644 index 000000000..7fcf77359 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/FlowExtensions.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2023 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 kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlin.time.Duration +import kotlin.time.ExperimentalTime +import kotlin.time.TimeMark +import kotlin.time.TimeSource + +/** + * Returns a flow that mirrors the original flow, but filters out values that occur within + * [timeout] of the previously emitted value. The first value is always emitted. + * + * Example: + * + * ```kotlin + * flow { + * emit(1) + * delay(90.milliseconds) + * emit(2) + * delay(90.milliseconds) + * emit(3) + * delay(1010.milliseconds) + * emit(4) + * delay(1010.milliseconds) + * emit(5) + * }.throttleFirst(1000.milliseconds) + * ``` + * + * produces the following emissions. + * + * ```text + * 1, 4, 5 + * ``` + * + * @see kotlinx.coroutines.flow.debounce(Duration) + * @param timeout Emissions within this duration of the last emission are filtered + * @param timeSource Used to measure elapsed time. Normally only overridden in tests + */ +@OptIn(ExperimentalTime::class) +fun Flow.throttleFirst( + timeout: Duration, + timeSource: TimeSource = TimeSource.Monotonic +) = flow { + var marker: TimeMark? = null + collect { + if (marker == null || marker!!.elapsedNow() >= timeout) { + emit(it) + marker = timeSource.markNow() + } + } +} diff --git a/app/src/test/java/com/keylesspalace/tusky/util/FlowExtensionsTest.kt b/app/src/test/java/com/keylesspalace/tusky/util/FlowExtensionsTest.kt new file mode 100644 index 000000000..5cbb231b6 --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/util/FlowExtensionsTest.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2023 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 app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.Test +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.ExperimentalTime + +@OptIn(ExperimentalCoroutinesApi::class, ExperimentalTime::class) +class FlowExtensionsTest { + @Test + fun `throttleFirst throttles first`() = runTest { + flow { + emit(1) // t = 0, emitted + delay(90.milliseconds) + emit(2) // throttled, t = 90 + delay(90.milliseconds) + emit(3) // throttled, t == 180 + delay(1010.milliseconds) + emit(4) // t = 1190, emitted + delay(1010.milliseconds) + emit(5) // t = 2200, emitted + } + .throttleFirst(1000.milliseconds, timeSource = testScheduler.timeSource) + .test { + advanceUntilIdle() + assertThat(awaitItem()).isEqualTo(1) + assertThat(awaitItem()).isEqualTo(4) + assertThat(awaitItem()).isEqualTo(5) + awaitComplete() + } + } +} From 8fec41c2ae8aa2324e00206097f52aeba4d7bd41 Mon Sep 17 00:00:00 2001 From: Nik Clayton Date: Sun, 11 Jun 2023 14:00:05 +0200 Subject: [PATCH 020/128] Send UI errors to a channel instead of a shared flow (#3652) In the previous code any errors that occured *before* a subscriber was listening to `uiError` would be dropped, so the user would be unware of them. By implementing as a channel these errors will be shown to the user, with an opportunity to retry the operation or report the error. --- .../notifications/NotificationsFragment.kt | 2 +- .../notifications/NotificationsViewModel.kt | 80 +++++++++++-------- 2 files changed, 46 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsFragment.kt index 352e87cae..19d6edf92 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsFragment.kt @@ -267,7 +267,7 @@ class NotificationsFragment : Log.d(TAG, error.toString()) val message = getString( error.message, - error.exception.localizedMessage + error.throwable.localizedMessage ?: getString(R.string.ui_error_unknown) ) val snackbar = Snackbar.make( diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsViewModel.kt index ea0f2e8e4..5eb54e100 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsViewModel.kt @@ -25,6 +25,7 @@ import androidx.lifecycle.viewModelScope import androidx.paging.PagingData import androidx.paging.cachedIn import androidx.paging.map +import at.connyduck.calladapter.networkresult.getOrThrow import com.keylesspalace.tusky.R import com.keylesspalace.tusky.appstore.BlockEvent import com.keylesspalace.tusky.appstore.EventHub @@ -46,6 +47,7 @@ import com.keylesspalace.tusky.viewdata.NotificationViewData import com.keylesspalace.tusky.viewdata.StatusViewData import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -60,6 +62,7 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.rx3.await @@ -220,7 +223,7 @@ sealed class StatusActionSuccess(open val action: StatusAction) : UiSuccess() { /** Errors from fallible view model actions that the UI will need to show */ sealed class UiError( /** The exception associated with the error */ - open val exception: Exception, + open val throwable: Throwable, /** String resource with an error message to show the user */ @StringRes val message: Int, @@ -228,50 +231,50 @@ sealed class UiError( /** The action that failed. Can be resent to retry the action */ open val action: UiAction? = null ) { - data class ClearNotifications(override val exception: Exception) : UiError( - exception, + data class ClearNotifications(override val throwable: Throwable) : UiError( + throwable, R.string.ui_error_clear_notifications ) data class Bookmark( - override val exception: Exception, + override val throwable: Throwable, override val action: StatusAction.Bookmark - ) : UiError(exception, R.string.ui_error_bookmark, action) + ) : UiError(throwable, R.string.ui_error_bookmark, action) data class Favourite( - override val exception: Exception, + override val throwable: Throwable, override val action: StatusAction.Favourite - ) : UiError(exception, R.string.ui_error_favourite, action) + ) : UiError(throwable, R.string.ui_error_favourite, action) data class Reblog( - override val exception: Exception, + override val throwable: Throwable, override val action: StatusAction.Reblog - ) : UiError(exception, R.string.ui_error_reblog, action) + ) : UiError(throwable, R.string.ui_error_reblog, action) data class VoteInPoll( - override val exception: Exception, + override val throwable: Throwable, override val action: StatusAction.VoteInPoll - ) : UiError(exception, R.string.ui_error_vote, action) + ) : UiError(throwable, R.string.ui_error_vote, action) data class AcceptFollowRequest( - override val exception: Exception, + override val throwable: Throwable, override val action: NotificationAction.AcceptFollowRequest - ) : UiError(exception, R.string.ui_error_accept_follow_request, action) + ) : UiError(throwable, R.string.ui_error_accept_follow_request, action) data class RejectFollowRequest( - override val exception: Exception, + override val throwable: Throwable, override val action: NotificationAction.RejectFollowRequest - ) : UiError(exception, R.string.ui_error_reject_follow_request, action) + ) : UiError(throwable, R.string.ui_error_reject_follow_request, action) companion object { - fun make(exception: Exception, action: FallibleUiAction) = when (action) { - is StatusAction.Bookmark -> Bookmark(exception, action) - is StatusAction.Favourite -> Favourite(exception, action) - is StatusAction.Reblog -> Reblog(exception, action) - is StatusAction.VoteInPoll -> VoteInPoll(exception, action) - is NotificationAction.AcceptFollowRequest -> AcceptFollowRequest(exception, action) - is NotificationAction.RejectFollowRequest -> RejectFollowRequest(exception, action) - FallibleUiAction.ClearNotifications -> ClearNotifications(exception) + fun make(throwable: Throwable, action: FallibleUiAction) = when (action) { + is StatusAction.Bookmark -> Bookmark(throwable, action) + is StatusAction.Favourite -> Favourite(throwable, action) + is StatusAction.Reblog -> Reblog(throwable, action) + is StatusAction.VoteInPoll -> VoteInPoll(throwable, action) + is NotificationAction.AcceptFollowRequest -> AcceptFollowRequest(throwable, action) + is NotificationAction.RejectFollowRequest -> RejectFollowRequest(throwable, action) + FallibleUiAction.ClearNotifications -> ClearNotifications(throwable) } } } @@ -298,14 +301,21 @@ class NotificationsViewModel @Inject constructor( private val uiAction = MutableSharedFlow() /** Flow of successful action results */ - // Note: These are a SharedFlow instead of a StateFlow because success or error state does not - // need to be retained. A message is shown once to a user and then dismissed. Re-collecting the - // flow (e.g., after a device orientation change) should not re-show the most recent success or - // error message, as it will be confusing to the user. + // Note: This is a SharedFlow instead of a StateFlow because success state does not need to be + // retained. A message is shown once to a user and then dismissed. Re-collecting the flow + // (e.g., after a device orientation change) should not re-show the most recent success + // message, as it will be confusing to the user. val uiSuccess = MutableSharedFlow() - /** Flow of transient errors for the UI to present */ - val uiError = MutableSharedFlow() + /** Channel for error results */ + // Errors are sent to a channel to ensure that any errors that occur *before* there are any + // subscribers are retained. If this was a SharedFlow any errors would be dropped, and if it + // was a StateFlow any errors would be retained, and there would need to be an explicit + // mechanism to dismiss them. + private val _uiErrorChannel = Channel() + + /** Expose UI errors as a flow */ + val uiError = _uiErrorChannel.receiveAsFlow() /** Accept UI actions in to actionStateFlow */ val accept: (UiAction) -> Unit = { action -> @@ -380,11 +390,11 @@ class NotificationsViewModel @Inject constructor( if (this.isSuccessful) { repository.invalidate() } else { - uiError.emit(UiError.make(HttpException(this), it)) + _uiErrorChannel.send(UiError.make(HttpException(this), it)) } } } catch (e: Exception) { - ifExpected(e) { uiError.emit(UiError.make(e, it)) } + ifExpected(e) { _uiErrorChannel.send(UiError.make(e, it)) } } } } @@ -403,7 +413,7 @@ class NotificationsViewModel @Inject constructor( } uiSuccess.emit(NotificationActionSuccess.from(action)) } catch (e: Exception) { - ifExpected(e) { uiError.emit(UiError.make(e, action)) } + ifExpected(e) { _uiErrorChannel.send(UiError.make(e, action)) } } } } @@ -436,10 +446,10 @@ class NotificationsViewModel @Inject constructor( action.poll.id, action.choices ) - } + }.getOrThrow() uiSuccess.emit(StatusActionSuccess.from(action)) - } catch (e: Exception) { - ifExpected(e) { uiError.emit(UiError.make(e, action)) } + } catch (t: Throwable) { + _uiErrorChannel.send(UiError.make(t, action)) } } } From 97b44228b7608ce8acc55917eb6c61752cfdeeee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sveinn=20=C3=AD=20Felli?= Date: Sun, 11 Jun 2023 11:41:44 +0000 Subject: [PATCH 021/128] Translated using Weblate (Icelandic) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (601 of 601 strings) Translated using Weblate (Icelandic) Currently translated at 99.8% (600 of 601 strings) Co-authored-by: Sveinn í Felli Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/is/ Translation: Tusky/Tusky --- app/src/main/res/values-is/strings.xml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index e2123ac9a..43b819242 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -298,8 +298,7 @@ Fjarlægja notandaaðganginn af listanum Sendi sem %1$s - Lýstu þessu fyrir sjónskerta -\n(hámark %d stafur) + Lýstu þessu fyrir sjónskerta \n(hámark %d stafir) @@ -651,4 +650,12 @@ Bæta við stikkorði Breyta stikkorði %s: %s - + Lýsing + Mynd + Þú ert ekki með neina lista + Sýsla með lista + Villa við að hlaða inn listum + Þetta er tímalínan þín. Hún sýnir nýlegar færslur þeirra sem þú fylgist með. +\n +\nTil að skoða hvað aðrir eru að gera getur þú til dæmis uppgötvað viðkomandi í einni af hinum tímalínunum. Til dæmis á staðværu tímalínu netþjónsins þíns [iconics gmd_group]. Eða að þú leitar að þeim eftir nafni [iconics gmd_search]; til dæmis geturðu leitað að Tusky til að finna Mastodon-aðganginn okkar. + \ No newline at end of file From c3229760c1a9c93e7013153acdd12c13acb044cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C4=81rti=C5=86=C5=A1=20Bru=C5=86enieks?= Date: Sun, 11 Jun 2023 11:41:44 +0000 Subject: [PATCH 022/128] Translated using Weblate (Latvian) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 92.8% (558 of 601 strings) Co-authored-by: Mārtiņš Bruņenieks Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/lv/ Translation: Tusky/Tusky --- app/src/main/res/values-lv/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index d1e85e09c..2c02764ca 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -608,4 +608,6 @@ Brīdināt Paslēpt Paslēpt ar brīdinājumu + Labot atslēgvārdu + Pievienot atslēgvārdu \ No newline at end of file From d131df06a4af61367297c6bf459d5e2bb9bcbf34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Quent=C3=AD?= Date: Sun, 11 Jun 2023 11:41:44 +0000 Subject: [PATCH 023/128] Translated using Weblate (Occitan) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (601 of 601 strings) Co-authored-by: Quentí Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/oc/ Translation: Tusky/Tusky --- app/src/main/res/values-oc/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-oc/strings.xml b/app/src/main/res/values-oc/strings.xml index d675d2358..b9fba1d50 100644 --- a/app/src/main/res/values-oc/strings.xml +++ b/app/src/main/res/values-oc/strings.xml @@ -321,7 +321,7 @@ Suprimir aqueste compte de la lista Descriure lo contengut pels mal vesents (%d caractèr maximum) - "Descriure los contenguts pels mal vesents (%d caractèrs maximum)" + Descriure los contenguts pels mal vesents (%d caractèrs maximum) CC-BY 4.0 CC-BY-SA 4.0 From 48c435d4996ab9330c9a33d80e1bab3dd721d478 Mon Sep 17 00:00:00 2001 From: Danial Behzadi Date: Sun, 11 Jun 2023 11:41:44 +0000 Subject: [PATCH 024/128] Translated using Weblate (Persian) Currently translated at 100.0% (601 of 601 strings) Co-authored-by: Danial Behzadi Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/fa/ Translation: Tusky/Tusky --- app/src/main/res/values-fa/strings.xml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index eb99e7857..ade3ef1dc 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -190,7 +190,7 @@ %1$s، %2$s و %3$s %1$s و %2$s - %d برهم‌کنش جدید + برهم‌کنشی جدید %d برهم‌کنش جدید حساب قفل‌شده @@ -339,11 +339,11 @@ نگارش ۴٫۰ CC-BY نگارش ۴٫۰ CC-BY-SA - ۱ برگزیدن + ۱ برگزیدن %1$s برگزیدن - %s تقویت + ۱ تقویت %s تقویت تقویت‌شده به دست @@ -352,7 +352,7 @@ %1$s و %2$s %1$s، %2$s و %3$d بیش‌تر - رسیده به بیشینهٔ %1$d زبانه + رسیده به بیشینهٔ زبانه‌ها رسیده به بیشینهٔ %1$d زبانه رسانه: %s @@ -379,15 +379,15 @@ یک نظرسنجی که در آن رأی دادید، تمام شد یک نظرسنجی که ساختید، تمام شد - %d روز مانده + ۱ روز مانده %d روز مانده - %d ساعت مانده + ۱ ساعت مانده %d ساعت مانده - %d دقیقه مانده + ۱‍ دقیقه مانده %d دقیقه مانده ادامه @@ -439,7 +439,7 @@ پیش‌نمایش پیوندها در خط‌زمانی‌ها به کار انداختن اشارهٔ کشیدنی برای تعویض بین زبانه‌ها - %s نفر + ۱ نفر %s نفر برچسب‌ها @@ -453,7 +453,7 @@ خموشی گفت‌وگو %s می‌خواهد پی‌گیرتان شود - %d ثانیه مانده + ۱ ثانیه مانده %d ثانیه مانده پایین @@ -464,7 +464,7 @@ ناخموشی %s ناخموشی %s - %s رأی + رأی داده %s رأی نهفتن عنوان نوارابزار بالایی @@ -501,7 +501,7 @@ \n فرستادن آگاهی‌ها تأثیر نمی‌پذیرد، ولی می‌توانید ترجیحات آگاهیتان را به صورت دستی بازبینی کنید. واقعاً می‌خواهید فهرست %s را حذف کنید؟ - نمی‌توانید بیش از %1$d رسانه بارگذارید. + نمی‌توانید چند رسانه بارگذارید. نمی‌توانید بیش از %1$d رسانه بارگذارید. نامعیّن @@ -664,4 +664,4 @@ هنوز هیچ سیاهه‌ای ندارید مدیریت سیاهه‌ها خطا در بار کردن سیاهه‌ها - + \ No newline at end of file From 152ca710c98f5a25ae929f389aa79a17f04d9706 Mon Sep 17 00:00:00 2001 From: Nik Clayton Date: Sun, 11 Jun 2023 11:41:44 +0000 Subject: [PATCH 025/128] Translated using Weblate (Persian) Currently translated at 100.0% (601 of 601 strings) Translated using Weblate (Persian) Currently translated at 100.0% (601 of 601 strings) Translated using Weblate (Icelandic) Currently translated at 100.0% (601 of 601 strings) Co-authored-by: Nik Clayton Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/fa/ Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/is/ Translation: Tusky/Tusky --- app/src/main/res/values-fa/strings.xml | 20 ++++++++++---------- app/src/main/res/values-is/strings.xml | 5 ++--- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index ade3ef1dc..f0c6961d8 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -339,11 +339,11 @@ نگارش ۴٫۰ CC-BY نگارش ۴٫۰ CC-BY-SA - ۱ برگزیدن + %1$s برگزیدن %1$s برگزیدن - ۱ تقویت + %s تقویت %s تقویت تقویت‌شده به دست @@ -352,7 +352,7 @@ %1$s و %2$s %1$s، %2$s و %3$d بیش‌تر - رسیده به بیشینهٔ زبانه‌ها + رسیده به بیشینهٔ %1$d زبانه رسیده به بیشینهٔ %1$d زبانه رسانه: %s @@ -379,15 +379,15 @@ یک نظرسنجی که در آن رأی دادید، تمام شد یک نظرسنجی که ساختید، تمام شد - ۱ روز مانده + %d روز مانده %d روز مانده - ۱ ساعت مانده + %d ساعت مانده %d ساعت مانده - ۱‍ دقیقه مانده + %d دقیقه مانده %d دقیقه مانده ادامه @@ -439,7 +439,7 @@ پیش‌نمایش پیوندها در خط‌زمانی‌ها به کار انداختن اشارهٔ کشیدنی برای تعویض بین زبانه‌ها - ۱ نفر + %s نفر %s نفر برچسب‌ها @@ -453,7 +453,7 @@ خموشی گفت‌وگو %s می‌خواهد پی‌گیرتان شود - ۱ ثانیه مانده + %d ثانیه مانده %d ثانیه مانده پایین @@ -464,7 +464,7 @@ ناخموشی %s ناخموشی %s - رأی داده + %s رأی %s رأی نهفتن عنوان نوارابزار بالایی @@ -501,7 +501,7 @@ \n فرستادن آگاهی‌ها تأثیر نمی‌پذیرد، ولی می‌توانید ترجیحات آگاهیتان را به صورت دستی بازبینی کنید. واقعاً می‌خواهید فهرست %s را حذف کنید؟ - نمی‌توانید چند رسانه بارگذارید. + نمی‌توانید بیش از %1$d رسانه بارگذارید. نمی‌توانید بیش از %1$d رسانه بارگذارید. نامعیّن diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index 43b819242..f8c010a83 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -298,9 +298,8 @@ Fjarlægja notandaaðganginn af listanum Sendi sem %1$s - - Lýstu þessu fyrir sjónskerta -\n(hámark %d stafir) + Lýstu þessu fyrir sjónskerta (hámark %d stafur) + Lýstu þessu fyrir sjónskerta (hámark %d stafir) Setja skýringatexta Fjarlægja From ea064edddf6598b44dbcae2126f64f557ac694a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=E1=BB=93=20Nh=E1=BA=A5t=20Duy?= Date: Sun, 11 Jun 2023 11:41:44 +0000 Subject: [PATCH 026/128] Translated using Weblate (Vietnamese) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (601 of 601 strings) Co-authored-by: Hồ Nhất Duy Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/vi/ Translation: Tusky/Tusky --- 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 c3a449c58..6b1ff130a 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -46,7 +46,7 @@ Làm tươi Tìm kiếm Hồ sơ - Tài khoản + Cá nhân Cài đặt Đăng xuất Xong @@ -603,8 +603,8 @@ Bỏ qua Hashtag nổi bật %1$d người đang thảo luận về %2$s - Lượt dùng - Người dùng + lượt dùng + người dùng Theo dõi hashtag #hashtag Làm mới From fc3b3f76bf0e8c4b59da3222474ec0a74ce115f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Alves?= Date: Sun, 11 Jun 2023 11:41:44 +0000 Subject: [PATCH 027/128] Translated using Weblate (Portuguese (Portugal)) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 84.6% (509 of 601 strings) Co-authored-by: João Alves Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/pt_PT/ Translation: Tusky/Tusky --- app/src/main/res/values-pt-rPT/strings.xml | 35 +++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index 9c6ae10fe..927055e71 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -563,4 +563,37 @@ Erro ao seguir #%s Erro ao deixar de seguir #%s adicionar reação - + Seguir hashtag + #hashtag + Desativado + <indefinido> + Edições + existe um novo relatório + Imagem/Vídeo precisa de uma descrição. + Descrição + Porta deve ser entre %d e %d + Falha ao fazer upload + A tua publicação não conseguiu ser enviada e foi salva nos rascunhos. +\n +\nO servidor não pode ser contactado, ou rejeitou a publicação. + As tuas publicações não conseguiram ser enviadas e foram salvas nos rascunhos. +\n +\nO servidor não pode ser contactado, ou rejeitou as publicações. + Mostrar rascunhos + Descartar + ALT + Erro a silenciar #%s + Iniciar sessão com navegador + %s reportou %s + Descartar alterações + Continuar a editar + Partilhar ligação para a conta + Partilhar nome de utilizador da conta + Partilhar URL da conta para… + Partilhar nome de utilizador da conta para… + Nome de utilizador copiado + Parou de seguir #%s + Editado %s + Hashtags seguidas + Hashtags populares + \ No newline at end of file From 2a9ad92e55db10fa63fcf0ef2838850d58974cb3 Mon Sep 17 00:00:00 2001 From: Nik Clayton Date: Sun, 11 Jun 2023 15:34:58 +0200 Subject: [PATCH 028/128] Make AccountPreferenceDataStore injectable (#3653) This will make tests that need it easier. - Rename from AccountPreferenceHandler - Inject its dependencies - Create an injectable CoroutineScope it can use for launching coroutines - Use it in AccountPreferences --- .../preference/AccountPreferencesFragment.kt | 21 +++------ .../keylesspalace/tusky/di/AppComponent.kt | 1 + .../tusky/di/CoroutineScopeModule.kt | 44 +++++++++++++++++++ ...ndler.kt => AccountPreferenceDataStore.kt} | 16 +++++-- 4 files changed, 64 insertions(+), 18 deletions(-) create mode 100644 app/src/main/java/com/keylesspalace/tusky/di/CoroutineScopeModule.kt rename app/src/main/java/com/keylesspalace/tusky/settings/{AccountPreferenceHandler.kt => AccountPreferenceDataStore.kt} (67%) 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 49107339c..b0fedb2b1 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 @@ -21,7 +21,6 @@ import android.os.Build import android.os.Bundle import android.util.Log import androidx.annotation.DrawableRes -import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceFragmentCompat import com.google.android.material.color.MaterialColors import com.google.android.material.snackbar.Snackbar @@ -30,7 +29,6 @@ import com.keylesspalace.tusky.BuildConfig 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.accountlist.AccountListActivity import com.keylesspalace.tusky.components.filters.FiltersActivity import com.keylesspalace.tusky.components.followedtags.FollowedTagsActivity @@ -42,7 +40,7 @@ import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.settings.AccountPreferenceHandler +import com.keylesspalace.tusky.settings.AccountPreferenceDataStore import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.settings.listPreference import com.keylesspalace.tusky.settings.makePreferenceScreen @@ -58,7 +56,6 @@ import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.sizeRes -import kotlinx.coroutines.launch import retrofit2.Call import retrofit2.Callback import retrofit2.Response @@ -74,6 +71,9 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable { @Inject lateinit var eventHub: EventHub + @Inject + lateinit var accountPreferenceDataStore: AccountPreferenceDataStore + private val iconSize by unsafeLazy { resources.getDimensionPixelSize(R.dimen.preference_icon_size) } override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { @@ -245,27 +245,26 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable { preferenceCategory(R.string.pref_title_timelines) { // TODO having no activeAccount in this fragment does not really make sense, enforce it? // All other locations here make it optional, however. - val accountPreferenceHandler = AccountPreferenceHandler(accountManager.activeAccount!!, accountManager, ::dispatchEvent) switchPreference { key = PrefKeys.MEDIA_PREVIEW_ENABLED setTitle(R.string.pref_title_show_media_preview) isSingleLineTitle = false - preferenceDataStore = accountPreferenceHandler + preferenceDataStore = accountPreferenceDataStore } switchPreference { key = PrefKeys.ALWAYS_SHOW_SENSITIVE_MEDIA setTitle(R.string.pref_title_alway_show_sensitive_media) isSingleLineTitle = false - preferenceDataStore = accountPreferenceHandler + preferenceDataStore = accountPreferenceDataStore } switchPreference { key = PrefKeys.ALWAYS_OPEN_SPOILER setTitle(R.string.pref_title_alway_open_spoiler) isSingleLineTitle = false - preferenceDataStore = accountPreferenceHandler + preferenceDataStore = accountPreferenceDataStore } } } @@ -353,12 +352,6 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable { activity?.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left) } - private fun dispatchEvent(event: PreferenceChangedEvent) { - lifecycleScope.launch { - eventHub.dispatch(event) - } - } - companion object { fun newInstance() = AccountPreferencesFragment() } 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 73aceeab2..d922ab370 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/AppComponent.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/AppComponent.kt @@ -29,6 +29,7 @@ import javax.inject.Singleton @Component( modules = [ AppModule::class, + CoroutineScopeModule::class, NetworkModule::class, AndroidSupportInjectionModule::class, ActivitiesModule::class, diff --git a/app/src/main/java/com/keylesspalace/tusky/di/CoroutineScopeModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/CoroutineScopeModule.kt new file mode 100644 index 000000000..bee62f7ec --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/di/CoroutineScopeModule.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2023 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.di + +import dagger.Module +import dagger.Provides +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import javax.inject.Qualifier + +/** + * Scope for potentially long-running tasks that should outlive the viewmodel that + * started them. For example, if the API call to bookmark a status is taking a long + * time, that call should not be cancelled because the user has navigated away from + * the viewmodel that made the call. + * + * @see https://developer.android.com/topic/architecture/data-layer#make_an_operation_live_longer_than_the_screen + */ +@Retention(AnnotationRetention.BINARY) +@Qualifier +annotation class ApplicationScope + +@Module +class CoroutineScopeModule { + @ApplicationScope + @Provides + fun providesApplicationScope() = CoroutineScope(SupervisorJob() + Dispatchers.Default) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/settings/AccountPreferenceHandler.kt b/app/src/main/java/com/keylesspalace/tusky/settings/AccountPreferenceDataStore.kt similarity index 67% rename from app/src/main/java/com/keylesspalace/tusky/settings/AccountPreferenceHandler.kt rename to app/src/main/java/com/keylesspalace/tusky/settings/AccountPreferenceDataStore.kt index cfdc27b44..a95134414 100644 --- a/app/src/main/java/com/keylesspalace/tusky/settings/AccountPreferenceHandler.kt +++ b/app/src/main/java/com/keylesspalace/tusky/settings/AccountPreferenceDataStore.kt @@ -1,15 +1,21 @@ package com.keylesspalace.tusky.settings import androidx.preference.PreferenceDataStore +import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.PreferenceChangedEvent import com.keylesspalace.tusky.db.AccountEntity import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.di.ApplicationScope +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import javax.inject.Inject -class AccountPreferenceHandler( - private val account: AccountEntity, +class AccountPreferenceDataStore @Inject constructor( private val accountManager: AccountManager, - private val dispatchEvent: (PreferenceChangedEvent) -> Unit + private val eventHub: EventHub, + @ApplicationScope private val externalScope: CoroutineScope ) : PreferenceDataStore() { + private val account: AccountEntity = accountManager.activeAccount!! override fun getBoolean(key: String, defValue: Boolean): Boolean { return when (key) { @@ -29,6 +35,8 @@ class AccountPreferenceHandler( accountManager.saveAccount(account) - dispatchEvent(PreferenceChangedEvent(key)) + externalScope.launch { + eventHub.dispatch(PreferenceChangedEvent(key)) + } } } From 327254d759cbf0a6de1f669f892239849f6cdb65 Mon Sep 17 00:00:00 2001 From: Nik Clayton Date: Sun, 11 Jun 2023 15:50:34 +0200 Subject: [PATCH 029/128] Remove android:textAppearance="@android:style/TextAppearance.DeviceDefault.Small" (#3663) It caused text size differences between the text in this view and all the other textviews in this layout. It's not used in other layouts. Fixes https://github.com/tuskyapp/Tusky/issues/3494 --- app/src/main/res/layout/item_status_notification.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/res/layout/item_status_notification.xml b/app/src/main/res/layout/item_status_notification.xml index 2cf63e554..0ed4d0e84 100644 --- a/app/src/main/res/layout/item_status_notification.xml +++ b/app/src/main/res/layout/item_status_notification.xml @@ -57,7 +57,6 @@ android:paddingStart="0dp" android:paddingEnd="@dimen/status_display_name_padding_end" android:paddingBottom="4dp" - android:textAppearance="@android:style/TextAppearance.DeviceDefault.Small" android:textColor="?android:textColorTertiary" android:textSize="?attr/status_text_medium" android:textStyle="normal|bold" From 5fd532d69bb27be7c65178c17416ae7ef5dfc8fd Mon Sep 17 00:00:00 2001 From: Nik Clayton Date: Sun, 11 Jun 2023 16:23:52 +0200 Subject: [PATCH 030/128] Notification tab cleanups (#3692) - Use NO_POSITION instead of hardcoding 0. - Don't set a state restoration policy, PagingDataAdapter already does that - Return the closest item, not just the closest page, in getRefreshKey --- .../tusky/components/notifications/NotificationsFragment.kt | 3 ++- .../components/notifications/NotificationsPagingAdapter.kt | 4 ---- .../components/notifications/NotificationsPagingSource.kt | 5 +++-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsFragment.kt index 19d6edf92..6191f5f68 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsFragment.kt @@ -41,6 +41,7 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.NO_POSITION import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE import androidx.recyclerview.widget.SimpleItemAnimator import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener @@ -201,7 +202,7 @@ class NotificationsFragment : // Save the ID of the first notification visible in the list, so the user's // reading position is always restorable. - layoutManager.findFirstVisibleItemPosition().takeIf { it >= 0 }?.let { position -> + layoutManager.findFirstVisibleItemPosition().takeIf { it != NO_POSITION }?.let { position -> adapter.snapshot().getOrNull(position)?.id?.let { id -> viewModel.accept(InfallibleUiAction.SaveVisibleId(visibleId = id)) } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsPagingAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsPagingAdapter.kt index d97a69573..faa1aefce 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsPagingAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsPagingAdapter.kt @@ -117,10 +117,6 @@ class NotificationsPagingAdapter( ) } - init { - stateRestorationPolicy = StateRestorationPolicy.PREVENT_WHEN_EMPTY - } - override fun getItemViewType(position: Int): Int { return NotificationViewKind.from(getItem(position)?.type).ordinal } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsPagingSource.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsPagingSource.kt index 480d5d681..b754989d0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsPagingSource.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsPagingSource.kt @@ -204,8 +204,9 @@ class NotificationsPagingSource @Inject constructor( override fun getRefreshKey(state: PagingState): String? { return state.anchorPosition?.let { anchorPosition -> - val anchorPage = state.closestPageToPosition(anchorPosition) - anchorPage?.prevKey ?: anchorPage?.nextKey + val id = state.closestItemToPosition(anchorPosition)?.id + Log.d(TAG, " getRefreshKey returning $id") + return id } } From 84486c7f134159ce7031ccb93a95fcc5a53173be Mon Sep 17 00:00:00 2001 From: Nik Clayton Date: Sun, 11 Jun 2023 18:39:48 +0200 Subject: [PATCH 031/128] Ensure textview fields can be copy/pasted (#3707) The Android libraries have a bug where a TextView can forget that it contains selectable text, can be pasted in to, etc. See https://issuetracker.google.com/issues/37095917 Fix this with an extension method that toggles the selectable state to re-enable it, and use this on the profile fields when editing an account. Fixes https://github.com/tuskyapp/Tusky/issues/3706 --- .../tusky/adapter/AccountFieldEditAdapter.kt | 5 +++++ .../com/keylesspalace/tusky/util/ViewExtensions.kt | 12 ++++++++++++ 2 files changed, 17 insertions(+) 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 fc258f083..96774ac0b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldEditAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldEditAdapter.kt @@ -22,6 +22,7 @@ import androidx.recyclerview.widget.RecyclerView import com.keylesspalace.tusky.databinding.ItemEditFieldBinding import com.keylesspalace.tusky.entity.StringField import com.keylesspalace.tusky.util.BindingHolder +import com.keylesspalace.tusky.util.fixTextSelection class AccountFieldEditAdapter : RecyclerView.Adapter>() { @@ -87,6 +88,10 @@ class AccountFieldEditAdapter : RecyclerView.Adapter fieldData[holder.bindingAdapterPosition].second = newText.toString() } + + // Ensure the textview contents are selectable + holder.binding.accountFieldNameText.fixTextSelection() + holder.binding.accountFieldValueText.fixTextSelection() } class MutableStringPair(var first: String, var second: String) 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 dc55ea527..392f65b77 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ViewExtensions.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/ViewExtensions.kt @@ -18,6 +18,7 @@ package com.keylesspalace.tusky.util import android.util.Log import android.view.View +import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import androidx.viewpager2.widget.ViewPager2 @@ -66,3 +67,14 @@ fun ViewPager2.reduceSwipeSensitivity() { Log.w("reduceSwipeSensitivity", e) } } + +/** + * TextViews with an ancestor RecyclerView can forget that they are selectable. Toggling + * calls to [TextView.setTextIsSelectable] fixes this. + * + * @see https://issuetracker.google.com/issues/37095917 + */ +fun TextView.fixTextSelection() { + setTextIsSelectable(false) + post { setTextIsSelectable(true) } +} From 66a394245b53340bdbf814cd9263e1e7852a1855 Mon Sep 17 00:00:00 2001 From: Nik Clayton Date: Sun, 11 Jun 2023 19:12:05 +0200 Subject: [PATCH 032/128] Remove ReplacementSpan, display diffs using CharacterStyle (#3431) Remove the use of ReplacementSpan. It turns out this span type is incompatible with spans that occupy more than one line, and the result is that a longer diff can run off the end of the screen. The alternative means that the diff'd text doesn't have additional padding and rounded corners, but it's better than not being visible. Display the most recent version of the status with larger text. Again, consistent with the thread view. Display the avatar, name, and username of the poster in a pinned header at the top of the screen, instead of duplicating the information on every edit. This reduces the amount of redundant information on the screen. --- .../viewthread/edits/ViewEditsAdapter.kt | 143 ++++++------------ .../viewthread/edits/ViewEditsFragment.kt | 19 ++- .../viewthread/edits/ViewEditsViewModel.kt | 5 +- .../main/res/layout/fragment_view_edits.xml | 112 ++++++++++++++ app/src/main/res/layout/item_status_edit.xml | 69 +++------ app/src/main/res/values-ar/strings.xml | 4 +- app/src/main/res/values-be/strings.xml | 6 +- app/src/main/res/values-ca/strings.xml | 4 +- app/src/main/res/values-cy/strings.xml | 6 +- app/src/main/res/values-de/strings.xml | 6 +- app/src/main/res/values-es/strings.xml | 6 +- app/src/main/res/values-fa/strings.xml | 4 +- app/src/main/res/values-fr/strings.xml | 6 +- app/src/main/res/values-gd/strings.xml | 6 +- app/src/main/res/values-gl/strings.xml | 4 +- app/src/main/res/values-hu/strings.xml | 4 +- app/src/main/res/values-is/strings.xml | 4 +- app/src/main/res/values-it/strings.xml | 6 +- app/src/main/res/values-ja/strings.xml | 6 +- app/src/main/res/values-lv/strings.xml | 6 +- app/src/main/res/values-nl/strings.xml | 4 +- app/src/main/res/values-oc/strings.xml | 6 +- app/src/main/res/values-pl/strings.xml | 4 +- app/src/main/res/values-pt-rBR/strings.xml | 4 +- app/src/main/res/values-ru/strings.xml | 4 +- app/src/main/res/values-sa/strings.xml | 4 +- app/src/main/res/values-sv/strings.xml | 8 +- app/src/main/res/values-tr/strings.xml | 6 +- app/src/main/res/values-uk/strings.xml | 6 +- app/src/main/res/values-vi/strings.xml | 6 +- app/src/main/res/values-zh-rCN/strings.xml | 4 +- app/src/main/res/values/dimens.xml | 3 - app/src/main/res/values/strings.xml | 4 +- 33 files changed, 266 insertions(+), 223 deletions(-) create mode 100644 app/src/main/res/layout/fragment_view_edits.xml diff --git a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsAdapter.kt index 075a84e23..33085f82b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsAdapter.kt @@ -1,8 +1,6 @@ package com.keylesspalace.tusky.components.viewthread.edits import android.content.Context -import android.graphics.Canvas -import android.graphics.Paint import android.graphics.Typeface.DEFAULT_BOLD import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable @@ -11,7 +9,9 @@ import android.text.Html import android.text.Spannable import android.text.SpannableStringBuilder import android.text.Spanned -import android.text.style.ReplacementSpan +import android.text.TextPaint +import android.text.style.CharacterStyle +import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -33,11 +33,9 @@ import com.keylesspalace.tusky.util.aspectRatios import com.keylesspalace.tusky.util.decodeBlurHash import com.keylesspalace.tusky.util.emojify import com.keylesspalace.tusky.util.hide -import com.keylesspalace.tusky.util.loadAvatar import com.keylesspalace.tusky.util.parseAsMastodonHtml import com.keylesspalace.tusky.util.setClickableText import com.keylesspalace.tusky.util.show -import com.keylesspalace.tusky.util.unicodeWrap import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.viewdata.toViewData import org.xml.sax.XMLReader @@ -52,13 +50,28 @@ class ViewEditsAdapter( private val absoluteTimeFormatter = AbsoluteTimeFormatter() + /** Size of large text in this theme, in px */ + var largeTextSizePx: Float = 0f + + /** Size of medium text in this theme, in px */ + var mediumTextSizePx: Float = 0f + override fun onCreateViewHolder( parent: ViewGroup, viewType: Int ): BindingHolder { val binding = ItemStatusEditBinding.inflate(LayoutInflater.from(parent.context), parent, false) + binding.statusEditMediaPreview.clipToOutline = true + val typedValue = TypedValue() + val context = binding.root.context + val displayMetrics = context.resources.displayMetrics + context.theme.resolveAttribute(R.attr.status_text_large, typedValue, true) + largeTextSizePx = typedValue.getDimension(displayMetrics) + context.theme.resolveAttribute(R.attr.status_text_medium, typedValue, true) + mediumTextSizePx = typedValue.getDimension(displayMetrics) + return BindingHolder(binding) } @@ -69,24 +82,26 @@ class ViewEditsAdapter( val context = binding.root.context - val avatarRadius: Int = context.resources - .getDimensionPixelSize(R.dimen.avatar_radius_48dp) - - loadAvatar(edit.account.avatar, binding.statusEditAvatar, avatarRadius, animateAvatars) - - val infoStringRes = if (position == edits.size - 1) { + val infoStringRes = if (position == edits.lastIndex) { R.string.status_created_info } else { R.string.status_edit_info } + // Show the most recent version of the status using large text to make it clearer for + // the user, and for similarity with thread view. + val variableTextSize = if (position == edits.lastIndex) { + mediumTextSizePx + } else { + largeTextSizePx + } + binding.statusEditContentWarningDescription.setTextSize(TypedValue.COMPLEX_UNIT_PX, variableTextSize) + binding.statusEditContent.setTextSize(TypedValue.COMPLEX_UNIT_PX, variableTextSize) + binding.statusEditMediaSensitivity.setTextSize(TypedValue.COMPLEX_UNIT_PX, variableTextSize) + val timestamp = absoluteTimeFormatter.format(edit.createdAt, false) - binding.statusEditInfo.text = context.getString( - infoStringRes, - edit.account.name.unicodeWrap(), - timestamp - ).emojify(edit.account.emojis, binding.statusEditInfo, animateEmojis) + binding.statusEditInfo.text = context.getString(infoStringRes, timestamp) if (edit.spoilerText.isEmpty()) { binding.statusEditContentWarningDescription.hide() @@ -198,6 +213,11 @@ class ViewEditsAdapter( } override fun getItemCount() = edits.size + + companion object { + private const val VIEW_TYPE_EDITS_NEWEST = 0 + private const val VIEW_TYPE_EDITS = 1 + } } /** @@ -266,98 +286,31 @@ class TuskyTagHandler(val context: Context) : Html.TagHandler { } } - /** - * A span that draws text with additional padding at the start/end of the text. The padding - * is the width of [separator]. - * - * Note: The separator string is not included in the final text, so it will not be included - * if the user cuts or copies the text. - */ - open class LRPaddedSpan(val separator: String = " ") : ReplacementSpan() { - /** The width of the separator string, used as padding */ - var paddingWidth = 0f - - /** Measured width of the span */ - var spanWidth = 0f - - override fun getSize( - paint: Paint, - text: CharSequence?, - start: Int, - end: Int, - fm: Paint.FontMetricsInt? - ): Int { - paddingWidth = paint.measureText(separator, 0, separator.length) - spanWidth = (paddingWidth * 2) + paint.measureText(text, start, end) - return spanWidth.toInt() - } - - override fun draw( - canvas: Canvas, - text: CharSequence?, - start: Int, - end: Int, - x: Float, - top: Int, - y: Int, - bottom: Int, - paint: Paint - ) { - canvas.drawText(text?.subSequence(start, end).toString(), x + paddingWidth, y.toFloat(), paint) - } - } - /** Span that signifies deleted text */ - class DeletedTextSpan(context: Context) : LRPaddedSpan() { - private val bgPaint = Paint() - val radius: Float + class DeletedTextSpan(context: Context) : CharacterStyle() { + private var bgColor: Int init { - bgPaint.color = context.getColor(R.color.view_edits_background_delete) - radius = context.resources.getDimension(R.dimen.lrPaddedSpanRadius) + bgColor = context.getColor(R.color.view_edits_background_delete) } - override fun draw( - canvas: Canvas, - text: CharSequence?, - start: Int, - end: Int, - x: Float, - top: Int, - y: Int, - bottom: Int, - paint: Paint - ) { - canvas.drawRoundRect(x, top.toFloat(), x + spanWidth, bottom.toFloat(), radius, radius, bgPaint) - paint.isStrikeThruText = true - super.draw(canvas, text, start, end, x, top, y, bottom, paint) + override fun updateDrawState(tp: TextPaint) { + tp.bgColor = bgColor + tp.isStrikeThruText = true } } /** Span that signifies inserted text */ - class InsertedTextSpan(context: Context) : LRPaddedSpan() { - val bgPaint = Paint() - val radius: Float + class InsertedTextSpan(context: Context) : CharacterStyle() { + private var bgColor: Int init { - bgPaint.color = context.getColor(R.color.view_edits_background_insert) - radius = context.resources.getDimension(R.dimen.lrPaddedSpanRadius) + bgColor = context.getColor(R.color.view_edits_background_insert) } - override fun draw( - canvas: Canvas, - text: CharSequence?, - start: Int, - end: Int, - x: Float, - top: Int, - y: Int, - bottom: Int, - paint: Paint - ) { - canvas.drawRoundRect(x, top.toFloat(), x + spanWidth, bottom.toFloat(), radius, radius, bgPaint) - paint.typeface = DEFAULT_BOLD - super.draw(canvas, text, start, end, x, top, y, bottom, paint) + override fun updateDrawState(tp: TextPaint) { + tp.bgColor = bgColor + tp.typeface = DEFAULT_BOLD } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsFragment.kt index f829141ab..3378b7a27 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsFragment.kt @@ -37,13 +37,16 @@ import com.keylesspalace.tusky.BottomSheetActivity import com.keylesspalace.tusky.R import com.keylesspalace.tusky.StatusListActivity import com.keylesspalace.tusky.components.account.AccountActivity -import com.keylesspalace.tusky.databinding.FragmentViewThreadBinding +import com.keylesspalace.tusky.databinding.FragmentViewEditsBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.interfaces.LinkListener import com.keylesspalace.tusky.settings.PrefKeys +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.unicodeWrap import com.keylesspalace.tusky.util.viewBinding import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial @@ -54,7 +57,7 @@ import java.io.IOException import javax.inject.Inject class ViewEditsFragment : - Fragment(R.layout.fragment_view_thread), + Fragment(R.layout.fragment_view_edits), LinkListener, OnRefreshListener, MenuProvider, @@ -65,7 +68,7 @@ class ViewEditsFragment : private val viewModel: ViewEditsViewModel by viewModels { viewModelFactory } - private val binding by viewBinding(FragmentViewThreadBinding::bind) + private val binding by viewBinding(FragmentViewEditsBinding::bind) private lateinit var statusId: String @@ -88,6 +91,7 @@ class ViewEditsFragment : val animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false) val animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) val useBlurhash = preferences.getBoolean(PrefKeys.USE_BLURHASH, true) + val avatarRadius: Int = requireContext().resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp) viewLifecycleOwner.lifecycleScope.launch { viewModel.uiState.collect { uiState -> @@ -130,6 +134,15 @@ class ViewEditsFragment : useBlurhash = useBlurhash, listener = this@ViewEditsFragment ) + + // Focus on the most recent version + (binding.recyclerView.layoutManager as LinearLayoutManager).scrollToPosition(0) + + val account = uiState.edits.first().account + loadAvatar(account.avatar, binding.statusAvatar, avatarRadius, animateAvatars) + + binding.statusDisplayName.text = account.name.unicodeWrap().emojify(account.emojis, binding.statusDisplayName, animateEmojis) + binding.statusUsername.text = account.username } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsViewModel.kt index 6dc0e25fc..c1e76da3c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsViewModel.kt @@ -98,10 +98,7 @@ class ViewEditsViewModel @Inject constructor(private val api: MastodonApi) : Vie if (i < sortedEdits.size - 1) { currentContent = previousContent previousContent = loader.load( - sortedEdits[i + 1].content.replace( - "
", - "
" - ) + sortedEdits[i + 1].content.replace("
", "
") ) } } diff --git a/app/src/main/res/layout/fragment_view_edits.xml b/app/src/main/res/layout/fragment_view_edits.xml new file mode 100644 index 000000000..6aabe13ab --- /dev/null +++ b/app/src/main/res/layout/fragment_view_edits.xml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_status_edit.xml b/app/src/main/res/layout/item_status_edit.xml index e43b167f2..af0d2a1b7 100644 --- a/app/src/main/res/layout/item_status_edit.xml +++ b/app/src/main/res/layout/item_status_edit.xml @@ -6,33 +6,21 @@ android:layout_height="wrap_content" android:gravity="center_vertical" android:orientation="horizontal" - android:paddingBottom="6dp"> - - + android:paddingLeft="16dp" + android:paddingRight="16dp" + android:paddingBottom="8dp"> @@ -40,34 +28,27 @@ android:id="@+id/status_edit_content_warning_description" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="14dp" - android:layout_marginTop="4dp" - android:layout_marginEnd="14dp" - android:layout_marginBottom="8dp" + android:layout_marginTop="8dp" android:hyphenationFrequency="full" android:importantForAccessibility="no" android:lineSpacingMultiplier="1.1" android:textColor="?android:textColorPrimary" android:textSize="?attr/status_text_medium" android:visibility="gone" - app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintEnd_toEndOf="@+id/status_edit_info" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/status_edit_avatar" + app:layout_constraintTop_toBottomOf="@+id/status_edit_info" tools:text="content warning which is very long and it doesn't fit" tools:visibility="visible" /> @@ -75,62 +56,52 @@ android:id="@+id/status_edit_content" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="14dp" - android:layout_marginTop="8dp" - android:layout_marginEnd="14dp" - android:layout_marginBottom="8dp" + android:layout_marginTop="4dp" android:focusable="true" android:hyphenationFrequency="full" android:importantForAccessibility="no" android:lineSpacingMultiplier="1.1" android:textColor="?android:textColorPrimary" android:textSize="?attr/status_text_medium" - app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintEnd_toEndOf="@+id/status_edit_info" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/status_edit_content_warning_separator" tools:text="This is an edited status" /> + - ليس لديك أي قوائم. عدَّلَ %s شكوى جديدة عن %s - عدّله %1$s في %2$s - أنشأه %1$s في %2$s + عدّله %1$s + أنشأه %1$s سيتم إخفاء بعض المعلومات التي قد تؤثر على صحتك العقلية. هذا يتضمن: \n \n- المفضلة/المشاركات/متابعة الاشعارات diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index b6f82177a..0c4ec3198 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -572,8 +572,8 @@ Далучыўся(-лася) %1$s Захавана! Вы паўторна зайшлі ў бягучы ўліковы запіс, каб дазволіць Tusky падпісацца на push-апавяшчэнні. Але ў Вас яшчэ засталіся ўліковыя запісы, якія не мігрыравалі такім чынам. Пераключыцеся на іх і зайдзіце паўторна, каб уключыць падтрымку апавяшчэнняў праз UnifiedPush. - %1$s адрэдагаваў(-ла) %2$s - %1$s стварыў(-ла) %2$s + %1$s адрэдагаваў(-ла) + %1$s стварыў(-ла) Схаваць загаловак верхняй панэлі інструментаў Ваша асабістая нататка пра гэты ўліковы запіс Патрабаваць пацвярджэнне перад упадабаннем @@ -671,4 +671,4 @@ %s (цэлае слова) Дадаць ключавое слова Змяніць ключавое слова - \ 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 b3cfc4bab..ccdd3ee58 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -617,12 +617,12 @@ Per utilitzar les notificacions push mitjançant UnifiedPush, Tusky necessita permís per subscriure\'s a les notificacions al vostre servidor Mastodon. Això requereix un nou inici de sessió per canviar els àmbits d\'OAuth concedits a Tusky. Si feu servir l\'opció de tornar a iniciar sessió aquí o a les preferències del compte, es conservaran tots els esborranys locals i la memòria cau. Torneu a iniciar sessió a tots els comptes per activar el suport de notificacions push. Editat - %1$s ha editat %2$s + %1$s ha editat Subscriu-te Cancel·la la subscripció %s regles Silencia les notificacions - %1$s ha creat %2$s + %1$s ha creat Hashtags populars %1$d persones parlen del hashtag %2$s Ús total diff --git a/app/src/main/res/values-cy/strings.xml b/app/src/main/res/values-cy/strings.xml index 75100b1d5..ec0e253fb 100644 --- a/app/src/main/res/values-cy/strings.xml +++ b/app/src/main/res/values-cy/strings.xml @@ -636,8 +636,8 @@ Disgrifiwch y cynnwys ar gyfer pobl â nam ar eu golwg (terfyn nodau o %d) Parhau i olygu - Golygodd %1$s %2$s - Creodd %1$s %2$s + Golygodd %1$s + Creodd %1$s Golygiadau AMGEN Hepgor newidiadau @@ -712,4 +712,4 @@ \n \nI archwilio cyfrifon gallwch un ai eu darganfod o fewn un o\'r llinellau amser eraill. Er enghraifft, mae llinell amser eich enghraifft chi [iconics gmd_group]. Neu gallwch eu chwilio yn ôl eu henw [iconics gmd_search]; er enghraifft, chwilio am Tusky i ganfod ein cyfrif Mastodon. Dangos ystadegau postiadau mewn llinell amser - \ No newline at end of file + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index e1f690b27..247c4159c 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -613,8 +613,8 @@ Profil-Link teilen an … Profilname teilen an … Profilname kopiert - %1$s bearbeitete %2$s - %1$s erstellte %2$s + %1$s bearbeitete + %1$s erstellte Neue Meldung über %s Benachrichtigungen über Moderationsmeldungen es eine neue Meldung gibt @@ -664,4 +664,4 @@ \nDamit du andere Konten entdeckst, kannst du entweder andere Timelines lesen – z. B. die Lokale Timeline deiner Instanz [iconics gmd_group] – oder du suchst nach Namen [iconics gmd_search] – z. B. Tusky, um unser Mastodon-Konto zu finden. Bild Beitragsstatistiken in der Timeline anzeigen - \ No newline at end of file + diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 8b123a690..4b84f235e 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -621,8 +621,8 @@ Orden de lectura Más antiguas primero Más recientes primero - %1$s ha editado %2$s - %1$s ha editado %2$s + %1$s ha editado + %1$s ha editado Desactivado <sin establecer> <inválido> @@ -685,4 +685,4 @@ Añadir %s (palabra completa) Añadir palabra - \ No newline at end of file + diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index f0c6961d8..2dc207427 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -595,8 +595,8 @@ <نامعتبر> ترتیب خواندن نخست جدیدترین - %1$s %2$s را ویراست - %1$s %2$s را ایجاد کرد + %1$s را ویراست + %1$s را ایجاد کرد بارگذاری شکست خورد نمایش پیش‌نویس‌ها رد کردن diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index b5cac1a09..3eb21848d 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -626,8 +626,8 @@ Les plus récents en premier Arrêter de suivre #%s \? Échec de l\'épinglage - Modification de %1$s le %2$s - Création par %1$s le %2$s + Modification de %1$s + Création par %1$s Enregistrer comme brouillon \? (Les pièces jointes seront à nouveau téléchargées lorsque le brouillon sera réouvert.) Vous n\'avez aucune liste. %s (%s) @@ -684,4 +684,4 @@ Ajouter un mot-clé Modifier mot-clé %s : %s - \ No newline at end of file + diff --git a/app/src/main/res/values-gd/strings.xml b/app/src/main/res/values-gd/strings.xml index 1c325cf4f..ad77f0a88 100644 --- a/app/src/main/res/values-gd/strings.xml +++ b/app/src/main/res/values-gd/strings.xml @@ -594,8 +594,8 @@ Deasachaidhean an-dràsta Cànan bunaiteach nam post - Chruthaich %1$s %2$s - Dheasaich %1$s %2$s + Chruthaich %1$s + Dheasaich %1$s Chan eil liosta sam bith agad. %s (%s) Bu chòir dhan phort a bhith eadar %d is %d @@ -681,4 +681,4 @@ Seo loidhne-ama do dhachaigh. Seallaidh i na postaichean o chionn goirid aig na cunntasan a leanas tu. \n \nAirson cunntasan a rùrachadh, lorg iad air tè dhe na loidhnichean-ama eile; mar eisimpleir, loidhne-ama ionadail an ionstans agad [iconics gmd_group]. Air neo lorg cunntasan a-rèir ainm [iconics gmd_search]; mar eisimpleir, lorg “Tusky” ach am faigh thu grèim air a’ chunntas againne air Mastodon. - \ No newline at end of file + diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 114af9f57..32e11f621 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -584,8 +584,8 @@ Cargando fío Acalar notificacións Edicións - Editado por %1$s o %2$s - Creado por %1$s o %2$s + Editado por %1$s + Creado por %1$s Desbotar cambios Continuar a edición Hai cambios non gardados. diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 27d9291c9..3454271d0 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -599,8 +599,8 @@ <nincs beállítva> <érvénytelen> Szerkesztések - %1$s létrehozta %2$s - %1$s szerkesztette %2$s + %1$s létrehozta + %1$s szerkesztette ALT Változtatások elvetése Szerkesztés folytatása diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index f8c010a83..0e9aecb23 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -589,8 +589,8 @@ <ekki stillt> <ógilt> Breytingar - %1$s bjó til %2$s - %1$s breytti %2$s + %1$s bjó til + %1$s breytti AUKA Henda breytingum Halda breytingum áfram diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index aa6438a0b..9d5a56218 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -610,8 +610,8 @@ Silenzia notifiche Modifiche Spam - %1$s ha modificato %2$s - %1$s ha creato %2$s + %1$s ha modificato + %1$s ha creato Altro Smettere di seguire #%s\? Caricamento fallito @@ -645,4 +645,4 @@ Account totali Aggiorna Hashtag di tendenza - \ No newline at end of file + diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 592500e5e..7ee976085 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -587,8 +587,8 @@ サムネイル画像で常に表示される中心点を設定するには、円をタップまたはドラッグして中してくだだい。 通知のミュート %1$s に参加 - %1$s 編集 %2$s - %1$s の投稿 %2$s + %1$s 編集 + %1$s の投稿 投稿 %s の検索エラー アカウントがロックされていなかったとしても、%1$s のスタッフは以下のアカウントのフォローリクエストを確認した方がいいと判断しました。 中心点の設定 @@ -623,4 +623,4 @@ プロフィール トレンドノハッシュタグ タイムラインに投稿の統計情報を表示する - \ No newline at end of file + diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index 2c02764ca..677745138 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -514,7 +514,7 @@ Vecākos vispirms Jaunākos vispirms - %1$s izveidoja %2$s + %1$s izveidoja %s personas %s persona @@ -544,7 +544,7 @@ Rādīt melnrakstus Aizvākt Tēmturis bez # - %1$s laboja %2$s + %1$s laboja Pieslēgties ar pārlūku Strādā vairumā gadījumu. Dati netiek nopludināti uz citām lietotnēm. beidzas %s @@ -610,4 +610,4 @@ Paslēpt ar brīdinājumu Labot atslēgvārdu Pievienot atslēgvārdu - \ No newline at end of file + diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 68329c4d5..8956cf714 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -569,8 +569,8 @@ Standaardtaal van berichten Rapporten Bewerkt - %1$s bewerkte %2$s - %1$s maakte %2$s + %1$s bewerkte + %1$s maakte Door in te loggen ben je het eens met de regels van %s. Spam Overig diff --git a/app/src/main/res/values-oc/strings.xml b/app/src/main/res/values-oc/strings.xml index b9fba1d50..f9e3bae30 100644 --- a/app/src/main/res/values-oc/strings.xml +++ b/app/src/main/res/values-oc/strings.xml @@ -590,9 +590,9 @@ Modificada Fracàs del cargament de l’estatut a partir del servidor. Amudir las notificacions - %1$s creèt %2$s + %1$s creèt Modificacions - %1$s modifiquèt %2$s + %1$s modifiquèt ALT Ignorar las modificacions Téner de modificar @@ -666,4 +666,4 @@ Aquò es vòstra cronologia. Mòstra las publicacions recentas dels comptes que seguissètz. \n \nPer explorar mai de compte podètz siá los descobrir dins d’autres fils, per exemple lo fil local de vòstra instància [iconics gmd_group], siá los trapar per lor nom [iconics gmd_search], per exemple « Tusky » per trobar nòstre compte Mastodon. - \ No newline at end of file + diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index b8359f0fe..38ebf6cde 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -602,8 +602,8 @@ Najpierw najnowsze Nie masz żadnych list. Wycisz powiadomienia - %1$s edytował %2$s - %1$s stworzył %2$s + %1$s edytował + %1$s stworzył Edycje Masz niezapisane zmiany. %s regulamin diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index aaa1c59aa..4f901bbef 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -562,7 +562,7 @@ Falha ao desafixar Salvar rascunho\? (Os anexos serão reenviados assim que você restaurar o rascunho.) Faça login novamente em todas as contas para habilitar o suporte de notificação push. - %1$s publicou em %2$s + %1$s publicou Edições Idioma padrão dos toots há uma nova denúncia @@ -587,7 +587,7 @@ Funciona na maioria dos casos. Nenhum dado é vazado para outros aplicativos. Pode oferecer suporte a métodos de autenticação adicionais, mas requer um navegador compatível. Falha ao remover a conta da lista - %1$s editou em %2$s + %1$s editou Continuar editando Você tem alterações não salvas. Editado diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 4654ffb66..a0266819a 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -540,8 +540,8 @@ Загрузка ветки Сначала новые Правки - %1$s отредактировали %2$s - %1$s создали %2$s + %1$s отредактировали + %1$s создали Войти Вход через Браузер diff --git a/app/src/main/res/values-sa/strings.xml b/app/src/main/res/values-sa/strings.xml index ea1a8f067..b16160da6 100644 --- a/app/src/main/res/values-sa/strings.xml +++ b/app/src/main/res/values-sa/strings.xml @@ -508,8 +508,8 @@ उत्सृज्यताम् लेखविकर्षान् दर्शयतु सम्पादनं कृतम् - %1$s निर्मितम् %2$s - %1$s सम्पादितम् %2$s + %1$s निर्मितम् + %1$s सम्पादितम् ग्राहकता प्रारूपं निष्कासितम् नियम-उल्लङ्घनम् diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index b43d726b0..3c663f95f 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -139,7 +139,7 @@ Vad är en instans? Ansluter… Adressen eller domänen för varje instans kan anges - här, till exempel mastodon.social, icosahedron.website, social.tchncs.de och + här, till exempel mastodon.social, icosahedron.website, social.tchncs.de och mer! \n\nOm du inte har något konto kan du ange namnet på instansen du vill ansluta till och skapa ett konto där. \n\nEn instans är en plats där ditt konto finns, men du kan enkelt kommunicera med och följa andra personer på andra instanser, @@ -282,7 +282,7 @@ Beskriv innehåll för synskadade \n (max %d tecken) - Beskriv innehåll för synskadade + Beskriv innehåll för synskadade \n (max %d tecken) Ange bildtext @@ -602,13 +602,13 @@ Laddar tråd Tysta notiser Redigeringar - %1$s redigerade %2$s + %1$s redigerade Dela länk med konto Dela användarnamn för konto Dela konto-URL med… Dela kontots användarnamn med… Kopierade användarnamn - %1$s skapade %2$s + %1$s skapade Förkasta ändringar Fortsätt redigera Du har ändringar som inte sparats. diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index fae65fa96..61d10e5a3 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -598,11 +598,11 @@ <ayarlanmadı> <geçersiz> Etkisizleştirildi - %1$s oluşturdu %2$s + %1$s oluşturdu Etiketi takip et #etiket Kaydedilmemiş değişikliklerin var. - %1$s düzenledi %2$s + %1$s düzenledi Yenile Düzenlemeler Açıklama @@ -664,4 +664,4 @@ Toplam kullanım Toplam hesap %1$d kişi %2$s etiketi hakkında konuşuyor - \ No newline at end of file + diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 53e5d6772..744dc67b8 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -610,8 +610,8 @@ Продовжити редагування Беззвучні сповіщення Редагування - %1$s редагує %2$s - %1$s створює %2$s + %1$s редагує + %1$s створює Завантаження стрічки Поділитися посиланням на обліковий запис Поділитися іменем користувача облікового запису @@ -681,4 +681,4 @@ Помилка завантаження списків У вас ще немає списків Керувати списками - \ No newline at end of file + diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 6b1ff130a..be6911222 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -574,8 +574,8 @@ Tiếp tục sửa Thay đổi chưa được lưu. Ẩn thông báo - %1$s sửa %2$s - %1$s đăng %2$s + %1$s sửa + %1$s đăng Những lượt sửa tút Đang tải thảo luận Chia sẻ URL người dùng @@ -646,4 +646,4 @@ Quản lý danh sách Xảy ra lỗi khi tải danh sách Bạn chưa có danh sách - \ No newline at end of file + diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 1ba737042..f95c2a0b5 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -591,10 +591,10 @@ 放弃更改 继续编辑 你有未保存的更改。 - %1$s 创建了 %2$s + %1$s 创建了 将通知静音 编辑 - %1$s 编辑了 %2$s + %1$s 编辑了 加载帖子 分享账户链接 分享账户用户名 diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index df2bd8a5f..0966630da 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -63,7 +63,4 @@ 1dp 48dp - - 4dp - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 656d082cd..02c106c49 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -764,9 +764,9 @@ Newest first - %1$s edited %2$s + Edited: %1$s - %1$s created %2$s + Created: %1$s Loading thread From 4cddd2c5e6e445ba05e685a7a2496d97f443657a Mon Sep 17 00:00:00 2001 From: Levi Bard Date: Sun, 11 Jun 2023 19:59:26 +0200 Subject: [PATCH 033/128] Add delete button to edit filter activity. (#3553) Adds workaround for #3545 --- .../components/filters/EditFilterActivity.kt | 32 +++++++++++++++++++ .../main/res/layout/activity_edit_filter.xml | 30 +++++++++++++---- 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/filters/EditFilterActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/filters/EditFilterActivity.kt index 664073949..2ef7902cc 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/filters/EditFilterActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/filters/EditFilterActivity.kt @@ -11,6 +11,7 @@ import androidx.core.content.IntentCompat import androidx.core.view.size import androidx.core.widget.doAfterTextChanged import androidx.lifecycle.lifecycleScope +import at.connyduck.calladapter.networkresult.fold import com.google.android.material.chip.Chip import com.google.android.material.snackbar.Snackbar import com.google.android.material.switchmaterial.SwitchMaterial @@ -24,7 +25,9 @@ import com.keylesspalace.tusky.entity.Filter import com.keylesspalace.tusky.entity.FilterKeyword import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.viewBinding +import com.keylesspalace.tusky.util.visible import kotlinx.coroutines.launch +import retrofit2.HttpException import java.util.Date import javax.inject.Inject @@ -78,6 +81,9 @@ class EditFilterActivity : BaseActivity() { binding.actionChip.setOnClickListener { showAddKeywordDialog() } binding.filterSaveButton.setOnClickListener { saveChanges() } + binding.filterDeleteButton.setOnClickListener { deleteFilter() } + binding.filterDeleteButton.visible(originalFilter != null) + for (switch in contextSwitches.keys) { switch.setOnCheckedChangeListener { _, isChecked -> val context = contextSwitches[switch]!! @@ -259,6 +265,32 @@ class EditFilterActivity : BaseActivity() { } } + private fun deleteFilter() { + originalFilter?.let { filter -> + lifecycleScope.launch { + api.deleteFilter(filter.id).fold( + { + finish() + }, + { throwable -> + if (throwable is HttpException && throwable.code() == 404) { + api.deleteFilterV1(filter.id).fold( + { + finish() + }, + { + Snackbar.make(binding.root, "Error deleting filter '${filter.title}'", Snackbar.LENGTH_SHORT).show() + } + ) + } else { + Snackbar.make(binding.root, "Error deleting filter '${filter.title}'", Snackbar.LENGTH_SHORT).show() + } + } + ) + } + } + } + companion object { const val FILTER_TO_EDIT = "FilterToEdit" diff --git a/app/src/main/res/layout/activity_edit_filter.xml b/app/src/main/res/layout/activity_edit_filter.xml index 3aa8e7e48..755aab5ac 100644 --- a/app/src/main/res/layout/activity_edit_filter.xml +++ b/app/src/main/res/layout/activity_edit_filter.xml @@ -144,14 +144,30 @@ android:minHeight="48dp" android:text="@string/pref_title_account_filter_keywords" /> -