From be5a41152ed071711381194df2315390ac1622a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=E1=BB=93=20Nh=E1=BA=A5t=20Duy?= Date: Wed, 6 Apr 2022 17:40:24 +0000 Subject: [PATCH 01/21] Translated using Weblate (Vietnamese) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (469 of 469 strings) Translated using Weblate (Vietnamese) Currently translated at 100.0% (469 of 469 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 cd3f07248..caf759fee 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -2,7 +2,7 @@ Bạn có muốn xóa toàn bộ thông báo\? Đã lưu tút vào nháp - Đã hủy đăng + Hủy đăng Đăng Tút Đang đăng… @@ -192,7 +192,7 @@ Ghim Trả lời Tút - Chuỗi tút + Nội dung tút Xếp tab Tin nhắn Thế giới @@ -394,7 +394,7 @@ Những người thích tút này Những người đăng lại tút này - %s lượt đăng lại + %s Đăng lại %1$s Thích From cd8f335e88d109c3a7e73f0dbe1a9001e790c6f7 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Wed, 6 Apr 2022 17:40:24 +0000 Subject: [PATCH 02/21] Translated using Weblate (Ukrainian) Currently translated at 100.0% (469 of 469 strings) Co-authored-by: Ihor Hordiichuk Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/uk/ Translation: Tusky/Tusky --- app/src/main/res/values-uk/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 0c78cfa83..0031334b8 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -16,7 +16,7 @@ Сповіщення Головна Помилка надсилання допису. - Зображення та відео не можуть бути прикріплені до статусу одночасно. + Зображення та відео не можуть бути прикріплені до допису одночасно. Потрібен дозвіл на зберігання медіа. Потрібен дозвіл на читання медіа. Не вдається відкрити цей файл. @@ -24,7 +24,7 @@ Аудіофайли повинні бути менше 40 МБ. Відео повинне бути менше 40 МБ. Файл повинен бути менше 8 МБ. - Статус надто довгий! + Допис задовгий! Не вдалося знайти браузер, який можна використати. Не може бути порожнім. Сталася помилка мережі! Перевірте інтернет-з\'єднання та спробуйте знову! From 9b344fcb7cc71967e318ee17c70beea87da3e33a Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Wed, 13 Apr 2022 16:40:26 +0000 Subject: [PATCH 03/21] Translated using Weblate (Ukrainian) Currently translated at 100.0% (16 of 16 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/uk/ --- fastlane/metadata/android/uk/changelogs/89.txt | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 fastlane/metadata/android/uk/changelogs/89.txt diff --git a/fastlane/metadata/android/uk/changelogs/89.txt b/fastlane/metadata/android/uk/changelogs/89.txt new file mode 100644 index 000000000..fbed5e83d --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/89.txt @@ -0,0 +1,7 @@ +Tusky v17.0 + +- «Відкрити як...» тепер також доступно в меню профілів облікових записів за користування кількома обліковими записами +- Тепер вхід обробляється у WebView у застосунку +- Підтримка Android 12 +- підтримка нового API конфігурації сервера Mastodon +- і багато інших дрібних виправлень і вдосконалень From 86ec71f0a9944df3c8861e76ab72f695c37035fe Mon Sep 17 00:00:00 2001 From: knuxify Date: Wed, 13 Apr 2022 16:40:26 +0000 Subject: [PATCH 04/21] Translated using Weblate (Polish) Currently translated at 93.7% (15 of 16 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/pl/ --- fastlane/metadata/android/pl/changelogs/87.txt | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 fastlane/metadata/android/pl/changelogs/87.txt diff --git a/fastlane/metadata/android/pl/changelogs/87.txt b/fastlane/metadata/android/pl/changelogs/87.txt new file mode 100644 index 000000000..2b8d41f69 --- /dev/null +++ b/fastlane/metadata/android/pl/changelogs/87.txt @@ -0,0 +1,8 @@ +Tusky v16.0 + +- Logika ładowania osi czasu została przepisana w celu przyspieszenia jej i naprawienia błędów. +- Tusky wspiera teraz animowane emotikony w formatach APNG i Animated WebP. +- Mnóstwo poprawek +- Wsparcie dla Androida 11 +- Nowe tłumaczenia: Gaelicki szkocki, galicyjski, ukraiński +- Ulepszone tłumaczenia From b4a913b2d5d160f917489dc45e098e96b7f0922e Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Wed, 13 Apr 2022 19:22:01 +0200 Subject: [PATCH 05/21] fix black theme on Android 12 (#2424) * fix black theme on Android 12 * Revert "fix black theme on Android 12" This reverts commit 2286706fdb239e15be72ac8943405ffeb2258219. * bring back SplashActivity --- app/src/main/AndroidManifest.xml | 25 +++++---- .../com/keylesspalace/tusky/MainActivity.kt | 5 -- .../com/keylesspalace/tusky/SplashActivity.kt | 51 +++++++++++++++++++ .../components/preference/EmojiPreference.kt | 4 +- .../tusky/di/ActivitiesModule.kt | 4 ++ 5 files changed, 73 insertions(+), 16 deletions(-) create mode 100644 app/src/main/java/com/keylesspalace/tusky/SplashActivity.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 858f8a371..3b0a977e3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -21,6 +21,22 @@ android:theme="@style/TuskyTheme" android:usesCleartextTraffic="false"> + + + + + + + + + + + @@ -29,13 +45,7 @@ - - - - - @@ -83,9 +93,6 @@ - . */ + +package com.keylesspalace.tusky + +import android.annotation.SuppressLint +import android.content.Intent +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import com.keylesspalace.tusky.components.login.LoginActivity +import com.keylesspalace.tusky.components.notifications.NotificationHelper +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.di.Injectable +import javax.inject.Inject + +@SuppressLint("CustomSplashScreen") +class SplashActivity : AppCompatActivity(), Injectable { + + @Inject + lateinit var accountManager: AccountManager + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + /** delete old notification channels */ + NotificationHelper.deleteLegacyNotificationChannels(this, accountManager) + + /** Determine whether the user is currently logged in, and if so go ahead and load the + * timeline. Otherwise, start the activity_login screen. */ + + val intent = if (accountManager.activeAccount != null) { + Intent(this, MainActivity::class.java) + } else { + LoginActivity.getIntent(this, false) + } + startActivity(intent) + finish() + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt index 2c565a710..47cb37ae7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt @@ -13,8 +13,8 @@ import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.preference.Preference import androidx.preference.PreferenceManager -import com.keylesspalace.tusky.MainActivity import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.SplashActivity import com.keylesspalace.tusky.components.notifications.NotificationHelper import com.keylesspalace.tusky.databinding.DialogEmojicompatBinding import com.keylesspalace.tusky.databinding.ItemEmojiPrefBinding @@ -216,7 +216,7 @@ class EmojiPreference( .setPositiveButton(R.string.restart) { _, _ -> // Restart the app // From https://stackoverflow.com/a/17166729/5070653 - val launchIntent = Intent(context, MainActivity::class.java) + val launchIntent = Intent(context, SplashActivity::class.java) val mPendingIntent = PendingIntent.getActivity( context, 0x1f973, // This is the codepoint of the party face emoji :D diff --git a/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt index b74bac79d..d85f6c452 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt @@ -23,6 +23,7 @@ import com.keylesspalace.tusky.FiltersActivity import com.keylesspalace.tusky.LicenseActivity import com.keylesspalace.tusky.ListsActivity import com.keylesspalace.tusky.MainActivity +import com.keylesspalace.tusky.SplashActivity import com.keylesspalace.tusky.StatusListActivity import com.keylesspalace.tusky.TabPreferenceActivity import com.keylesspalace.tusky.ViewMediaActivity @@ -116,4 +117,7 @@ abstract class ActivitiesModule { @ContributesAndroidInjector abstract fun contributesDraftActivity(): DraftsActivity + + @ContributesAndroidInjector + abstract fun contributesSplashActivity(): SplashActivity } From c705e9cbbb00ca147286869cc5a4df2746d3ba80 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Wed, 13 Apr 2022 19:22:09 +0200 Subject: [PATCH 06/21] remove extra slash in OAuth authorize url (#2425) --- .../main/java/com/keylesspalace/tusky/network/MastodonApi.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 3ee8d4cec..28d83eca3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -68,7 +68,7 @@ import retrofit2.http.Query interface MastodonApi { companion object { - const val ENDPOINT_AUTHORIZE = "/oauth/authorize" + const val ENDPOINT_AUTHORIZE = "oauth/authorize" const val DOMAIN_HEADER = "domain" const val PLACEHOLDER_DOMAIN = "dummy.placeholder" } From 1d20a02d1757b7a023e9c5f6e25d4a349e260e7a Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Wed, 13 Apr 2022 19:22:19 +0200 Subject: [PATCH 07/21] fix crash in ConversationsFragment (#2426) --- .../tusky/components/conversation/ConversationsFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt index 684144556..a09026c24 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt @@ -102,7 +102,7 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res initSwipeToRefresh() - lifecycleScope.launch { + viewLifecycleOwner.lifecycleScope.launch { viewModel.conversationFlow.collectLatest { pagingData -> adapter.submitData(pagingData) } From 63bb4d9395272ae377a654eaea3dbbe34016895c Mon Sep 17 00:00:00 2001 From: knuxify Date: Thu, 14 Apr 2022 03:40:26 +0000 Subject: [PATCH 08/21] Translated using Weblate (Polish) Currently translated at 100.0% (469 of 469 strings) Co-authored-by: knuxify Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/pl/ Translation: Tusky/Tusky --- app/src/main/res/values-pl/strings.xml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 9854db065..d468ee009 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -506,10 +506,10 @@ Ogranicz liczbę powiadomień o zmianach na osi czasu Czas trwania Nowe wpisy - Niektóre informacje, które mogą wpływać na Twoj dobrostan psychiczny zostaną ukryte. W ich skład wchodzą: + Niektóre informacje, które mogą wpływać na Twój dobrostan psychiczny zostaną ukryte. W ich skład wchodzą: \n \n - powiadomienia o ulubionych/podbiciach/obserwowaniu -\n - liczba polubień/podbić toota +\n - liczba polubień/podbić wpisu \n - statystyki obserwujących/postów na profilach \n \nNie będzie to miało wpływu na powiadomienia typu push, ale możesz zmienić ustawienia powiadomień ręcznie. @@ -542,4 +542,11 @@ %s poprosił(a) o możliwość śledzenia Cię Usuń z zakładek Pytaj o potwierdzenie przed dodaniem do ulubionych + 14 dni + 30 dni + 60 dni + 90 dni + 180 dni + 365 dni + Utwórz wpis \ No newline at end of file From b84d41522d3bc7db469ebde9b8ca4f951c15c9db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sveinn=20=C3=AD=20Felli?= Date: Thu, 14 Apr 2022 03:40:27 +0000 Subject: [PATCH 09/21] Translated using Weblate (Icelandic) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (469 of 469 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 | 31 +++++++++++++------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index be5ee7524..0dc453c5c 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -21,23 +21,23 @@ Óskilgreind auðkenningarvilla kom upp. Heimild var hafnað. Mistókst að fá innskráningarteikn. - Stöðufærslan er of löng! + Færslan er of löng! Skráin verður að vera minni en 8MB. Myndskeiðaskrár verða að vera minni en 40MB. Þessa tegund skrár er ekki hægt að senda inn. Ekki var hægt að opna skrána. Krafist er heimilda til að lesa gögn. Krafist er heimilda til að geyma gögn. - Ekki er hægt að hengja bæði myndir og myndskeið við sömu stöðufærslu. + Ekki er hægt að hengja bæði myndir og myndskeið við sömu færslu. Sendingin mistókst. - Villa við að senda tíst. + Villa við að senda færslu. Heim Tilkynningar Staðvært Sameiginlegt Bein skilaboð Flipar - Tíst + Þráður Færslur Með svörum Fest @@ -62,8 +62,8 @@ Fella saman Ekkert hér. Ekkert hér. Togaðu niður til að endurhlaða! - %s endurbirti tístið þitt - %s setti tíst frá þér í eftirlæti + %s endurbirti færsluna þína + %s setti færslu frá þér í eftirlæti %s fylgist núna með þér Kæra @%s Aðrar athugasemdir\? @@ -117,7 +117,7 @@ Hafna Drög Áætluð tíst - Sýnileiki tísts + Sýnileiki færslu Aðvörun vegna efnis Lyklaborð með tjáningartáknum Tímasetja tíst @@ -234,9 +234,9 @@ Nýir fylgjendur Tilkynningar um nýja fylgjendur Endurbirtingar - Tilkynningar þegar tístin þín eru endurbirt + Tilkynningar þegar færslurnar þínar eru endurbirtar Eftirlæti - Tilkynningar þegar tístin þín eru sett í eftirlæti + Tilkynningar þegar færslurnar þínar eru settar í eftirlæti Kannanir Tilkynningar um kannanir sem er lokið %s minntist á þig @@ -273,7 +273,7 @@ %ds Fylgir þér Alltaf birta myndefni sem merkt er viðkvæmt - Alltaf fletta út tístum sem eru með aðvörun vegna efnis + Alltaf fletta út færslum sem eru með aðvörun vegna efnis Gagnaskrár Svar til @%s hlaða inn fleiru @@ -376,7 +376,7 @@ Hreinsa Sía Virkja - Semja tíst + Semja færslu Semja skilaboð Ertu viss um að þú viljir endanlega eyða öllum tilkynningunum þínum\? Aðgerðir fyrir mynd %s @@ -478,7 +478,7 @@ Sumar upplýsingar sem gætu haft áhrif á andlega vellíðan þína verða faldar. Þetta hefur áhrif á: \n \n - Eftirlæti/Endurbirtingar/Tilkynningar um fylgjendabeiðnir -\n - Eftirlæti/Talningu á endurbirtingum tísta +\n - Eftirlæti/Talningu á endurbirtingum færslna \n - Fylgjendur/Tölfræði færslna í notendasniðum \n \n Þetta hefur ekki áhrif á ýti-tilkynningar, en þú getur yfirfarið handvirkt kjörstillingar þínar varðandi tilkynningar. @@ -503,9 +503,9 @@ Takmarka tilkynningar á tímalínu Yfirfara tilkynningar Vellíðan - Tilkynningar þegar einhver sem þú ert áskrifandi að hefur birt nýtt tíst - Ný tíst - einhver sem ég er áskrifandi að birti nýtt tíst + Tilkynningar þegar einhver sem þú ert áskrifandi að hefur birt nýja færslu + Nýjar færslur + einhver sem ég er áskrifandi að birti nýja færslu %s sendi inn rétt í þessu Jafnvel þótt aðgangurinn þinn sé ekki læstur, fannst starfsfólki %1$s að þú gætir viljað yfirfara handvirkt fylgjendabeiðnir frá þessum aðgöngum. Fjarlægja bókamerki @@ -518,4 +518,5 @@ 180 dagar 365 dagar 14 dagar + Semja færslu \ No newline at end of file From 0c38f0f09c5c3b0925b881a48426a1a5875751f1 Mon Sep 17 00:00:00 2001 From: XoseM Date: Thu, 14 Apr 2022 03:40:27 +0000 Subject: [PATCH 10/21] Translated using Weblate (Galician) Currently translated at 100.0% (469 of 469 strings) Co-authored-by: XoseM Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/gl/ Translation: Tusky/Tusky --- app/src/main/res/values-gl/strings.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index e99b718a8..bfabe4c8e 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -54,7 +54,7 @@ Seguir Tes a certeza de que queres desconectar a conta %1$s\? Desconectar - Conecta con Mastodon + Accede con Mastodon Redactar Máis Eliminar favorito @@ -116,7 +116,7 @@ Os ficheiros de vídeo teñen que ser menores de 40MB. O ficheiro debe ser menor de 8MB. A publicación é demasiado longa! - Fallou a obtención do token de conexión. + Fallou a obtención do token de acceso. A autorización foi rexeitada. Aconteceu un erro non identificado de autorización. Non se atopou un navegador para utilizar. @@ -411,8 +411,8 @@ Bloquear @%s\? Agochar todo o dominio Tes a certeza de querer bloquear a todo %s\? Non verás o contido dese dominio en ningunha cronoloxía pública ou nas notificacións. As túas seguidoras nese dominio serán eliminadas. - Eliminar e reescribir este toot\? - Eliminar este toot\? + Eliminar e reescribir esta publicación\? + Eliminar esta publicación\? Deixar de seguir esta conta\? Revogar a solicitude de seguimento\? Descargar From 660c5c08d7914a5192fac24bdf40509cfc4ab825 Mon Sep 17 00:00:00 2001 From: Conny Duck Date: Thu, 14 Apr 2022 18:33:23 +0200 Subject: [PATCH 11/21] remove buggy string from gd translation --- app/src/main/res/values-gd/strings.xml | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/src/main/res/values-gd/strings.xml b/app/src/main/res/values-gd/strings.xml index b55faa6c2..bf2a2d521 100644 --- a/app/src/main/res/values-gd/strings.xml +++ b/app/src/main/res/values-gd/strings.xml @@ -253,9 +253,6 @@ Mìnich e dhan fheadhainn air a bheil cion-lèirsinn \n(%d caractar(an) air a char as fhaide) - - - Cha deach leinn am fo-thiotal a shuidheachadh A’ postadh leis a’ chunntas %1$s From 0c840a706d92aa0b20cf8adb9e9ce63632c54660 Mon Sep 17 00:00:00 2001 From: Conny Duck Date: Thu, 14 Apr 2022 19:07:17 +0200 Subject: [PATCH 12/21] Release 89 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index ad1ef72f6..645bcb052 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -20,8 +20,8 @@ android { applicationId APP_ID minSdkVersion 21 targetSdkVersion 31 - versionCode 88 - versionName "17.0 beta 1" + versionCode 89 + versionName "17.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true From d21d045eda16a8daa4c61f6e9b6d4213a8f62532 Mon Sep 17 00:00:00 2001 From: kyori19 Date: Fri, 15 Apr 2022 02:39:30 +0900 Subject: [PATCH 13/21] Support new signup notifications (#2357) --- .../32.json | 815 ++++++++++++++++++ .../tusky/adapter/NotificationsAdapter.java | 9 +- .../notifications/NotificationHelper.java | 15 +- .../NotificationPreferencesFragment.kt | 11 + .../keylesspalace/tusky/db/AccountEntity.kt | 1 + .../keylesspalace/tusky/db/AppDatabase.java | 9 +- .../com/keylesspalace/tusky/di/AppModule.kt | 2 +- .../tusky/entity/Notification.kt | 6 +- .../tusky/fragment/NotificationsFragment.java | 10 +- .../tusky/settings/SettingsConstants.kt | 1 + app/src/main/res/values/strings.xml | 4 + 11 files changed, 869 insertions(+), 14 deletions(-) create mode 100644 app/schemas/com.keylesspalace.tusky.db.AppDatabase/32.json diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/32.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/32.json new file mode 100644 index 000000000..97ad414e0 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/32.json @@ -0,0 +1,815 @@ +{ + "formatVersion": 1, + "database": { + "version": 32, + "identityHash": "c92343960c9d46d9cfd49f1873cce47d", + "entities": [ + { + "tableName": "DraftEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationsSignUps` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSignUps", + "columnName": "notificationsSignUps", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "minPollDuration", + "columnName": "minPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollDuration", + "columnName": "maxPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "charactersReservedPerUrl", + "columnName": "charactersReservedPerUrl", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentCollapsed", + "columnName": "contentCollapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentShowing", + "columnName": "contentShowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.tags", + "columnName": "s_tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c92343960c9d46d9cfd49f1873cce47d')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java index 9cef6245e..936f20a3d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java @@ -226,7 +226,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter { case VIEW_TYPE_FOLLOW: { if (payloadForHolder == null) { FollowViewHolder holder = (FollowViewHolder) viewHolder; - holder.setMessage(concreteNotificaton.getAccount()); + holder.setMessage(concreteNotificaton.getAccount(), concreteNotificaton.getType() == Notification.Type.SIGN_UP); holder.setupButtons(notificationActionListener, concreteNotificaton.getAccount().getId()); } break; @@ -283,7 +283,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter { case REBLOG: { return VIEW_TYPE_STATUS_NOTIFICATION; } - case FOLLOW: { + case FOLLOW: + case SIGN_UP: { return VIEW_TYPE_FOLLOW; } case FOLLOW_REQUEST: { @@ -335,10 +336,10 @@ public class NotificationsAdapter extends RecyclerView.Adapter { this.statusDisplayOptions = statusDisplayOptions; } - void setMessage(TimelineAccount account) { + void setMessage(TimelineAccount account, Boolean isSignUp) { Context context = message.getContext(); - String format = context.getString(R.string.notification_follow_format); + String format = context.getString(isSignUp ? R.string.notification_sign_up_format : R.string.notification_follow_format); String wrappedDisplayName = StringUtils.unicodeWrap(account.getName()); String wholeMessage = String.format(format, wrappedDisplayName); CharSequence emojifiedMessage = CustomEmojiHelper.emojify( 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 6b9afce1d..63c170822 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 @@ -16,6 +16,8 @@ package com.keylesspalace.tusky.components.notifications; +import static com.keylesspalace.tusky.viewdata.PollViewDataKt.buildDescription; + import android.app.NotificationChannel; import android.app.NotificationChannelGroup; import android.app.NotificationManager; @@ -73,8 +75,6 @@ import java.util.concurrent.TimeUnit; import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.schedulers.Schedulers; -import static com.keylesspalace.tusky.viewdata.PollViewDataKt.buildDescription; - public class NotificationHelper { private static int notificationId = 0; @@ -116,6 +116,7 @@ public class NotificationHelper { public static final String CHANNEL_FAVOURITE = "CHANNEL_FAVOURITE"; public static final String CHANNEL_POLL = "CHANNEL_POLL"; public static final String CHANNEL_SUBSCRIPTIONS = "CHANNEL_SUBSCRIPTIONS"; + public static final String CHANNEL_SIGN_UP = "CHANNEL_SIGN_UP"; /** * WorkManager Tag @@ -392,6 +393,7 @@ public class NotificationHelper { CHANNEL_FAVOURITE + account.getIdentifier(), CHANNEL_POLL + account.getIdentifier(), CHANNEL_SUBSCRIPTIONS + account.getIdentifier(), + CHANNEL_SIGN_UP + account.getIdentifier(), }; int[] channelNames = { R.string.notification_mention_name, @@ -401,6 +403,7 @@ public class NotificationHelper { R.string.notification_favourite_name, R.string.notification_poll_name, R.string.notification_subscription_name, + R.string.notification_sign_up_name, }; int[] channelDescriptions = { R.string.notification_mention_descriptions, @@ -410,6 +413,7 @@ public class NotificationHelper { R.string.notification_favourite_description, R.string.notification_poll_description, R.string.notification_subscription_description, + R.string.notification_sign_up_description, }; List channels = new ArrayList<>(6); @@ -560,6 +564,8 @@ public class NotificationHelper { return account.getNotificationsFavorited(); case POLL: return account.getNotificationsPolls(); + case SIGN_UP: + return account.getNotificationsSignUps(); default: return false; } @@ -582,6 +588,8 @@ public class NotificationHelper { return CHANNEL_FAVOURITE + account.getIdentifier(); case POLL: return CHANNEL_POLL + account.getIdentifier(); + case SIGN_UP: + return CHANNEL_SIGN_UP + account.getIdentifier(); default: return null; } @@ -663,6 +671,8 @@ public class NotificationHelper { } else { return context.getString(R.string.poll_ended_voted); } + case SIGN_UP: + return String.format(context.getString(R.string.notification_sign_up_format), accountName); } return null; } @@ -671,6 +681,7 @@ public class NotificationHelper { switch (notification.getType()) { case FOLLOW: case FOLLOW_REQUEST: + case SIGN_UP: return "@" + notification.getAccount().getUsername(); case MENTION: case FAVOURITE: diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt index 4d8ba84f3..82ee0a384 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt @@ -122,6 +122,17 @@ class NotificationPreferencesFragment : PreferenceFragmentCompat(), Injectable { true } } + + switchPreference { + setTitle(R.string.pref_title_notification_filter_sign_ups) + key = PrefKeys.NOTIFICATION_FILTER_SIGN_UPS + isIconSpaceReserved = false + isChecked = activeAccount.notificationsSignUps + setOnPreferenceChangeListener { _, newValue -> + updateAccount { it.notificationsSignUps = newValue as Boolean } + true + } + } } preferenceCategory(R.string.pref_title_notification_alerts) { category -> diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt index 0c25cbbc9..5da91e201 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt @@ -50,6 +50,7 @@ data class AccountEntity( var notificationsFavorited: Boolean = true, var notificationsPolls: Boolean = true, var notificationsSubscriptions: Boolean = true, + var notificationsSignUps: Boolean = true, var notificationSound: Boolean = true, var notificationVibration: Boolean = true, var notificationLight: Boolean = true, diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java index 159a6f529..2131300c7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java +++ b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java @@ -31,7 +31,7 @@ import java.io.File; */ @Database(entities = { DraftEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class, TimelineAccountEntity.class, ConversationEntity.class - }, version = 31) + }, version = 32) public abstract class AppDatabase extends RoomDatabase { public abstract AccountDao accountDao(); @@ -483,4 +483,11 @@ public abstract class AppDatabase extends RoomDatabase { database.execSQL("DELETE FROM `TimelineStatusEntity`"); } }; + + public static final Migration MIGRATION_31_32 = new Migration(31, 32) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsSignUps` INTEGER NOT NULL DEFAULT 1"); + } + }; } diff --git a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt index b0f28261e..677f81677 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt @@ -62,7 +62,7 @@ class AppModule { AppDatabase.MIGRATION_22_23, AppDatabase.MIGRATION_23_24, AppDatabase.MIGRATION_24_25, AppDatabase.Migration25_26(appContext.getExternalFilesDir("Tusky")), AppDatabase.MIGRATION_26_27, AppDatabase.MIGRATION_27_28, AppDatabase.MIGRATION_28_29, - AppDatabase.MIGRATION_29_30, AppDatabase.MIGRATION_30_31 + AppDatabase.MIGRATION_29_30, AppDatabase.MIGRATION_30_31, AppDatabase.MIGRATION_31_32, ) .build() } diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt index ae2d74a90..ddcf5e618 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt @@ -37,7 +37,9 @@ data class Notification( FOLLOW("follow"), FOLLOW_REQUEST("follow_request"), POLL("poll"), - STATUS("status"); + STATUS("status"), + SIGN_UP("admin.sign_up"), + ; companion object { @@ -49,7 +51,7 @@ data class Notification( } return UNKNOWN } - val asList = listOf(MENTION, REBLOG, FAVOURITE, FOLLOW, FOLLOW_REQUEST, POLL, STATUS) + val asList = listOf(MENTION, REBLOG, FAVOURITE, FOLLOW, FOLLOW_REQUEST, POLL, STATUS, SIGN_UP) } override fun toString(): String { diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java index 467ebc8c6..f267f29e9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java @@ -15,6 +15,10 @@ package com.keylesspalace.tusky.fragment; +import static com.keylesspalace.tusky.util.StringUtils.isLessThan; +import static autodispose2.AutoDispose.autoDisposable; +import static autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from; + import android.app.Activity; import android.content.Context; import android.content.DialogInterface; @@ -111,10 +115,6 @@ import kotlin.Unit; import kotlin.collections.CollectionsKt; import kotlin.jvm.functions.Function1; -import static autodispose2.AutoDispose.autoDisposable; -import static autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from; -import static com.keylesspalace.tusky.util.StringUtils.isLessThan; - public class NotificationsFragment extends SFragment implements SwipeRefreshLayout.OnRefreshListener, StatusActionListener, @@ -707,6 +707,8 @@ public class NotificationsFragment extends SFragment implements return getString(R.string.notification_poll_name); case STATUS: return getString(R.string.notification_subscription_name); + case SIGN_UP: + return getString(R.string.notification_sign_up_name); default: return "Unknown"; } diff --git a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt index c59ba58b0..c728e1f6d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt +++ b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt @@ -59,6 +59,7 @@ object PrefKeys { const val NOTIFICATION_FILTER_FOLLOW_REQUESTS = "notificationFilterFollowRequests" const val NOTIFICATIONS_FILTER_FOLLOWS = "notificationFilterFollows" const val NOTIFICATION_FILTER_SUBSCRIPTIONS = "notificationFilterSubscriptions" + const val NOTIFICATION_FILTER_SIGN_UPS = "notificationFilterSignUps" const val TAB_FILTER_HOME_REPLIES = "tabFilterHomeReplies" const val TAB_FILTER_HOME_BOOSTS = "tabFilterHomeBoosts" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e9daafe0b..e6ce23387 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -62,6 +62,7 @@ %s favorited your post %s followed you %s requested to follow you + %s signed up %s just posted Report @%s @@ -228,6 +229,7 @@ my posts are favorited polls have ended somebody I\'m subscribed to published a new post + somebody signed up Appearance App Theme Timelines @@ -295,6 +297,8 @@ Notifications about polls that have ended New posts Notifications when somebody you\'re subscribed to published a new post + Sign ups + Notifications about new users %s mentioned you %1$s, %2$s, %3$s and %4$d others From 3e8c6a318a99744e4f7c480fc43c43130d1d1c75 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Thu, 14 Apr 2022 19:49:49 +0200 Subject: [PATCH 14/21] introduce KotlinResultCallAdapter for nice suspending network calls (#2415) * introduce KotlinResultCallAdapter for nice suspending network calls * fix tests --- app/build.gradle | 5 +- .../com/keylesspalace/tusky/MainActivity.kt | 21 ++-- .../announcements/AnnouncementsViewModel.kt | 6 +- .../components/compose/ComposeViewModel.kt | 8 +- .../tusky/components/login/LoginActivity.kt | 95 +++++++++---------- .../scheduled/ScheduledStatusViewModel.kt | 15 +-- .../keylesspalace/tusky/di/NetworkModule.kt | 2 + .../tusky/network/MastodonApi.kt | 16 ++-- .../tusky/viewmodel/EditProfileViewModel.kt | 87 ++++++++--------- .../tusky/BottomSheetActivityTest.kt | 10 +- .../tusky/ComposeActivityTest.kt | 57 ++++++----- .../com/keylesspalace/tusky/FilterTest.kt | 4 +- .../CachedTimelineRemoteMediatorTest.kt | 6 +- .../NetworkTimelinePagingSourceTest.kt | 4 +- .../NetworkTimelineRemoteMediatorTest.kt | 11 +-- 15 files changed, 168 insertions(+), 179 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 645bcb052..f16f96754 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -137,6 +137,7 @@ dependencies { implementation "com.squareup.retrofit2:retrofit:$retrofitVersion" implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion" implementation "com.squareup.retrofit2:adapter-rxjava3:$retrofitVersion" + implementation "at.connyduck:kotlin-result-calladapter:1.0.0" implementation "com.squareup.okhttp3:okhttp:$okhttpVersion" implementation "com.squareup.okhttp3:logging-interceptor:$okhttpVersion" @@ -176,8 +177,8 @@ dependencies { testImplementation "androidx.test.ext:junit:1.1.3" testImplementation "org.robolectric:robolectric:4.4" - testImplementation "org.mockito:mockito-inline:3.6.28" - testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0" + testImplementation "org.mockito:mockito-inline:4.4.0" + testImplementation "org.mockito.kotlin:mockito-kotlin:4.0.0" androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0" androidTestImplementation "androidx.room:room-testing:$roomVersion" diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt index 5c7901c56..a934ff9a0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt @@ -682,18 +682,15 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje } } - private fun fetchUserInfo() { - mastodonApi.accountVerifyCredentials() - .observeOn(AndroidSchedulers.mainThread()) - .autoDispose(this, Lifecycle.Event.ON_DESTROY) - .subscribe( - { userInfo -> - onFetchUserInfoSuccess(userInfo) - }, - { throwable -> - Log.e(TAG, "Failed to fetch user info. " + throwable.message) - } - ) + private fun fetchUserInfo() = lifecycleScope.launch { + mastodonApi.accountVerifyCredentials().fold( + { userInfo -> + onFetchUserInfoSuccess(userInfo) + }, + { throwable -> + Log.e(TAG, "Failed to fetch user info. " + throwable.message) + } + ) } private fun onFetchUserInfoSuccess(me: Account) { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt index d1ae0b9e3..10dc303f6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt @@ -35,6 +35,7 @@ import com.keylesspalace.tusky.util.Resource import com.keylesspalace.tusky.util.RxAwareViewModel import com.keylesspalace.tusky.util.Success import io.reactivex.rxjava3.core.Single +import kotlinx.coroutines.rx3.rxSingle import javax.inject.Inject class AnnouncementsViewModel @Inject constructor( @@ -56,8 +57,9 @@ class AnnouncementsViewModel @Inject constructor( appDatabase.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!) .map> { Either.Left(it) } .onErrorResumeNext { - mastodonApi.getInstance() - .map { Either.Right(it) } + rxSingle { + mastodonApi.getInstance().getOrThrow() + }.map { Either.Right(it) } } ) { emojis, either -> either.asLeftOrNull()?.copy(emojiList = emojis) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt index 66dacfb45..08df6dc91 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt @@ -48,6 +48,7 @@ import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.disposables.Disposable import kotlinx.coroutines.launch +import kotlinx.coroutines.rx3.rxSingle import java.util.Locale import javax.inject.Inject @@ -105,7 +106,10 @@ class ComposeViewModel @Inject constructor( init { Single.zip( - api.getCustomEmojis(), api.getInstance() + api.getCustomEmojis(), + rxSingle { + api.getInstance().getOrThrow() + } ) { emojis, instance -> InstanceEntity( instance = accountManager.activeAccount?.domain!!, @@ -291,7 +295,7 @@ class ComposeViewModel @Inject constructor( ): LiveData { val deletionObservable = if (isEditingScheduledToot) { - api.deleteScheduledStatus(scheduledTootId.toString()).toObservable().map { } + rxSingle { api.deleteScheduledStatus(scheduledTootId.toString()) }.toObservable().map { } } else { Observable.just(Unit) }.toLiveData() diff --git a/app/src/main/java/com/keylesspalace/tusky/components/login/LoginActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/login/LoginActivity.kt index cc2bd776b..4df7abc1d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/login/LoginActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/login/LoginActivity.kt @@ -33,7 +33,6 @@ import com.keylesspalace.tusky.MainActivity import com.keylesspalace.tusky.R import com.keylesspalace.tusky.databinding.ActivityLoginBinding import com.keylesspalace.tusky.di.Injectable -import com.keylesspalace.tusky.entity.AppCredentials import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.getNonNullString import com.keylesspalace.tusky.util.rickRoll @@ -166,32 +165,33 @@ class LoginActivity : BaseActivity(), Injectable { setLoading(true) lifecycleScope.launch { - val credentials: AppCredentials = try { - mastodonApi.authenticateApp( - domain, getString(R.string.app_name), oauthRedirectUri, - OAUTH_SCOPES, getString(R.string.tusky_website) - ) - } catch (e: Exception) { - binding.loginButton.isEnabled = true - binding.domainTextInputLayout.error = - getString(R.string.error_failed_app_registration) - setLoading(false) - Log.e(TAG, Log.getStackTraceString(e)) - return@launch - } + mastodonApi.authenticateApp( + domain, getString(R.string.app_name), oauthRedirectUri, + OAUTH_SCOPES, getString(R.string.tusky_website) + ).fold( + { credentials -> + // Before we open browser page we save the data. + // Even if we don't open other apps user may go to password manager or somewhere else + // and we will need to pick up the process where we left off. + // Alternatively we could pass it all as part of the intent and receive it back + // but it is a bit of a workaround. + preferences.edit() + .putString(DOMAIN, domain) + .putString(CLIENT_ID, credentials.clientId) + .putString(CLIENT_SECRET, credentials.clientSecret) + .apply() - // Before we open browser page we save the data. - // Even if we don't open other apps user may go to password manager or somewhere else - // and we will need to pick up the process where we left off. - // Alternatively we could pass it all as part of the intent and receive it back - // but it is a bit of a workaround. - preferences.edit() - .putString(DOMAIN, domain) - .putString(CLIENT_ID, credentials.clientId) - .putString(CLIENT_SECRET, credentials.clientSecret) - .apply() - - redirectUserToAuthorizeAndLogin(domain, credentials.clientId) + redirectUserToAuthorizeAndLogin(domain, credentials.clientId) + }, + { e -> + binding.loginButton.isEnabled = true + binding.domainTextInputLayout.error = + getString(R.string.error_failed_app_registration) + setLoading(false) + Log.e(TAG, Log.getStackTraceString(e)) + return@launch + } + ) } } @@ -224,29 +224,28 @@ class LoginActivity : BaseActivity(), Injectable { setLoading(true) - val accessToken = try { - mastodonApi.fetchOAuthToken( - domain, clientId, clientSecret, oauthRedirectUri, code, - "authorization_code" - ) - } catch (e: Exception) { - setLoading(false) - binding.domainTextInputLayout.error = - getString(R.string.error_retrieving_oauth_token) - Log.e( - TAG, - "%s %s".format(getString(R.string.error_retrieving_oauth_token), e.message), - ) - return - } + mastodonApi.fetchOAuthToken( + domain, clientId, clientSecret, oauthRedirectUri, code, "authorization_code" + ).fold( + { accessToken -> + accountManager.addAccount(accessToken.accessToken, domain) - accountManager.addAccount(accessToken.accessToken, domain) - - val intent = Intent(this, MainActivity::class.java) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - startActivity(intent) - finish() - overridePendingTransition(R.anim.explode, R.anim.explode) + val intent = Intent(this, MainActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + startActivity(intent) + finish() + overridePendingTransition(R.anim.explode, R.anim.explode) + }, + { e -> + setLoading(false) + binding.domainTextInputLayout.error = + getString(R.string.error_retrieving_oauth_token) + Log.e( + TAG, + "%s %s".format(getString(R.string.error_retrieving_oauth_token), e.message), + ) + } + ) } private fun setLoading(loadingState: Boolean) { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusViewModel.kt index cd3e5ac0c..766ed44ab 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusViewModel.kt @@ -25,7 +25,6 @@ import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.entity.ScheduledStatus import com.keylesspalace.tusky.network.MastodonApi import kotlinx.coroutines.launch -import kotlinx.coroutines.rx3.await import javax.inject.Inject class ScheduledStatusViewModel @Inject constructor( @@ -43,12 +42,14 @@ class ScheduledStatusViewModel @Inject constructor( fun deleteScheduledStatus(status: ScheduledStatus) { viewModelScope.launch { - try { - mastodonApi.deleteScheduledStatus(status.id).await() - pagingSourceFactory.remove(status) - } catch (throwable: Throwable) { - Log.w("ScheduledTootViewModel", "Error deleting scheduled status", throwable) - } + mastodonApi.deleteScheduledStatus(status.id).fold( + { + pagingSourceFactory.remove(status) + }, + { throwable -> + Log.w("ScheduledTootViewModel", "Error deleting scheduled status", throwable) + } + ) } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt index 7bda6ef74..d927c2999 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt @@ -19,6 +19,7 @@ import android.content.Context import android.content.SharedPreferences import android.os.Build import android.text.Spanned +import at.connyduck.calladapter.kotlinresult.KotlinResultCallAdapterFactory import com.google.gson.Gson import com.google.gson.GsonBuilder import com.keylesspalace.tusky.BuildConfig @@ -111,6 +112,7 @@ class NetworkModule { .client(httpClient) .addConverterFactory(GsonConverterFactory.create(gson)) .addCallAdapterFactory(RxJava3CallAdapterFactory.create()) + .addCallAdapterFactory(KotlinResultCallAdapterFactory.create()) .build() } diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt index 28d83eca3..111cad56c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -80,7 +80,7 @@ interface MastodonApi { fun getCustomEmojis(): Single> @GET("api/v1/instance") - fun getInstance(): Single + suspend fun getInstance(): Result @GET("api/v1/filters") fun getFilters(): Single> @@ -249,12 +249,12 @@ interface MastodonApi { ): Single> @DELETE("api/v1/scheduled_statuses/{id}") - fun deleteScheduledStatus( + suspend fun deleteScheduledStatus( @Path("id") scheduledStatusId: String - ): Single + ): Result @GET("api/v1/accounts/verify_credentials") - fun accountVerifyCredentials(): Single + suspend fun accountVerifyCredentials(): Result @FormUrlEncoded @PATCH("api/v1/accounts/update_credentials") @@ -265,7 +265,7 @@ interface MastodonApi { @Multipart @PATCH("api/v1/accounts/update_credentials") - fun accountUpdateCredentials( + suspend fun accountUpdateCredentials( @Part(value = "display_name") displayName: RequestBody?, @Part(value = "note") note: RequestBody?, @Part(value = "locked") locked: RequestBody?, @@ -279,7 +279,7 @@ interface MastodonApi { @Part(value = "fields_attributes[2][value]") fieldValue2: RequestBody?, @Part(value = "fields_attributes[3][name]") fieldName3: RequestBody?, @Part(value = "fields_attributes[3][value]") fieldValue3: RequestBody? - ): Call + ): Result @GET("api/v1/accounts/search") fun searchAccounts( @@ -447,7 +447,7 @@ interface MastodonApi { @Field("redirect_uris") redirectUris: String, @Field("scopes") scopes: String, @Field("website") website: String - ): AppCredentials + ): Result @FormUrlEncoded @POST("oauth/token") @@ -458,7 +458,7 @@ interface MastodonApi { @Field("redirect_uri") redirectUri: String, @Field("code") code: String, @Field("grant_type") grantType: String - ): AccessToken + ): Result @FormUrlEncoded @POST("api/v1/lists") diff --git a/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt index f3539f8dd..17aa38c75 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt @@ -20,6 +20,7 @@ import android.net.Uri import androidx.core.net.toUri import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.ProfileEditedEvent import com.keylesspalace.tusky.entity.Account @@ -31,8 +32,7 @@ import com.keylesspalace.tusky.util.Loading import com.keylesspalace.tusky.util.Resource import com.keylesspalace.tusky.util.Success import com.keylesspalace.tusky.util.randomAlphanumericString -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.addTo +import kotlinx.coroutines.launch import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody import okhttp3.RequestBody @@ -40,9 +40,7 @@ import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.toRequestBody import org.json.JSONException import org.json.JSONObject -import retrofit2.Call -import retrofit2.Callback -import retrofit2.Response +import retrofit2.HttpException import java.io.File import javax.inject.Inject @@ -63,24 +61,20 @@ class EditProfileViewModel @Inject constructor( private var oldProfileData: Account? = null - private val disposables = CompositeDisposable() - - fun obtainProfile() { + fun obtainProfile() = viewModelScope.launch { if (profileData.value == null || profileData.value is Error) { profileData.postValue(Loading()) - mastodonApi.accountVerifyCredentials() - .subscribe( - { profile -> - oldProfileData = profile - profileData.postValue(Success(profile)) - }, - { - profileData.postValue(Error()) - } - ) - .addTo(disposables) + mastodonApi.accountVerifyCredentials().fold( + { profile -> + oldProfileData = profile + profileData.postValue(Success(profile)) + }, + { + profileData.postValue(Error()) + } + ) } } @@ -151,34 +145,34 @@ class EditProfileViewModel @Inject constructor( return } - mastodonApi.accountUpdateCredentials( - displayName, note, locked, avatar, header, - field1?.first, field1?.second, field2?.first, field2?.second, field3?.first, field3?.second, field4?.first, field4?.second - ).enqueue(object : Callback { - override fun onResponse(call: Call, response: Response) { - val newProfileData = response.body() - if (!response.isSuccessful || newProfileData == null) { - val errorResponse = response.errorBody()?.string() - val errorMsg = if (!errorResponse.isNullOrBlank()) { - try { - JSONObject(errorResponse).optString("error", null) - } catch (e: JSONException) { + viewModelScope.launch { + mastodonApi.accountUpdateCredentials( + displayName, note, locked, avatar, header, + field1?.first, field1?.second, field2?.first, field2?.second, field3?.first, field3?.second, field4?.first, field4?.second + ).fold( + { newProfileData -> + saveData.postValue(Success()) + eventHub.dispatch(ProfileEditedEvent(newProfileData)) + }, + { throwable -> + if (throwable is HttpException) { + val errorResponse = throwable.response()?.errorBody()?.string() + val errorMsg = if (!errorResponse.isNullOrBlank()) { + try { + JSONObject(errorResponse).optString("error", "") + } catch (e: JSONException) { + null + } + } else { null } + saveData.postValue(Error(errorMessage = errorMsg)) } else { - null + saveData.postValue(Error()) } - saveData.postValue(Error(errorMessage = errorMsg)) - return } - saveData.postValue(Success()) - eventHub.dispatch(ProfileEditedEvent(newProfileData)) - } - - override fun onFailure(call: Call, t: Throwable) { - saveData.postValue(Error()) - } - }) + ) + } } // cache activity state for rotation change @@ -208,15 +202,11 @@ class EditProfileViewModel @Inject constructor( return File(application.cacheDir, filename) } - override fun onCleared() { - disposables.dispose() - } - - fun obtainInstance() { + fun obtainInstance() = viewModelScope.launch { if (instanceData.value == null || instanceData.value is Error) { instanceData.postValue(Loading()) - mastodonApi.getInstance().subscribe( + mastodonApi.getInstance().fold( { instance -> instanceData.postValue(Success(instance)) }, @@ -224,7 +214,6 @@ class EditProfileViewModel @Inject constructor( instanceData.postValue(Error()) } ) - .addTo(disposables) } } } diff --git a/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt index ef6d26327..ff2088231 100644 --- a/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt @@ -16,15 +16,11 @@ package com.keylesspalace.tusky import android.text.SpannedString -import android.widget.LinearLayout import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import com.google.android.material.bottomsheet.BottomSheetBehavior import com.keylesspalace.tusky.entity.SearchResult import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.TimelineAccount import com.keylesspalace.tusky.network.MastodonApi -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.mock import io.reactivex.rxjava3.android.plugins.RxAndroidPlugins import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.plugins.RxJavaPlugins @@ -39,8 +35,8 @@ import org.junit.runner.RunWith import org.junit.runners.Parameterized import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.Mockito.eq -import org.mockito.Mockito.mock -import java.util.ArrayList +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock import java.util.Date import java.util.concurrent.TimeUnit @@ -306,7 +302,7 @@ class BottomSheetActivityTest { init { mastodonApi = api @Suppress("UNCHECKED_CAST") - bottomSheet = mock(BottomSheetBehavior::class.java) as BottomSheetBehavior + bottomSheet = mock() } override fun openLink(url: String) { diff --git a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt index e7b3a1a91..dc4a412ff 100644 --- a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt @@ -24,8 +24,6 @@ import com.keylesspalace.tusky.components.compose.ComposeActivity import com.keylesspalace.tusky.components.compose.ComposeViewModel import com.keylesspalace.tusky.components.compose.DEFAULT_CHARACTER_LIMIT import com.keylesspalace.tusky.components.compose.DEFAULT_MAXIMUM_URL_LENGTH -import com.keylesspalace.tusky.components.compose.MediaUploader -import com.keylesspalace.tusky.components.drafts.DraftHelper import com.keylesspalace.tusky.db.AccountEntity import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AppDatabase @@ -37,18 +35,16 @@ import com.keylesspalace.tusky.entity.Instance import com.keylesspalace.tusky.entity.InstanceConfiguration import com.keylesspalace.tusky.entity.StatusConfiguration import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.service.ServiceClient -import com.nhaarman.mockitokotlin2.any import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.core.SingleObserver import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mockito.`when` -import org.mockito.Mockito.mock +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock import org.robolectric.Robolectric import org.robolectric.Shadows.shadowOf import org.robolectric.annotation.Config @@ -94,44 +90,47 @@ class ComposeActivityTest { val controller = Robolectric.buildActivity(ComposeActivity::class.java) activity = controller.get() - accountManagerMock = mock(AccountManager::class.java) - `when`(accountManagerMock.activeAccount).thenReturn(account) + accountManagerMock = mock { + on { activeAccount } doReturn account + } - apiMock = mock(MastodonApi::class.java) - `when`(apiMock.getCustomEmojis()).thenReturn(Single.just(emptyList())) - `when`(apiMock.getInstance()).thenReturn(object : Single() { - override fun subscribeActual(observer: SingleObserver) { - val instance = instanceResponseCallback?.invoke() + apiMock = mock { + on { getCustomEmojis() } doReturn Single.just(emptyList()) + onBlocking { getInstance() } doReturn instanceResponseCallback?.invoke().let { instance -> if (instance == null) { - observer.onError(Throwable()) + Result.failure(Throwable()) } else { - observer.onSuccess(instance) + Result.success(instance) } } - }) + } - val instanceDaoMock = mock(InstanceDao::class.java) - `when`(instanceDaoMock.loadMetadataForInstance(any())).thenReturn( - Single.just(InstanceEntity(instanceDomain, emptyList(), null, null, null, null, null, null, null)) - ) + val instanceDaoMock: InstanceDao = mock { + on { loadMetadataForInstance(any()) } doReturn + Single.just(InstanceEntity(instanceDomain, emptyList(), null, null, null, null, null, null, null)) + on { loadMetadataForInstance(any()) } doReturn + Single.just(InstanceEntity(instanceDomain, emptyList(), null, null, null, null, null, null, null)) + } - val dbMock = mock(AppDatabase::class.java) - `when`(dbMock.instanceDao()).thenReturn(instanceDaoMock) + val dbMock: AppDatabase = mock { + on { instanceDao() } doReturn instanceDaoMock + } val viewModel = ComposeViewModel( apiMock, accountManagerMock, - mock(MediaUploader::class.java), - mock(ServiceClient::class.java), - mock(DraftHelper::class.java), + mock(), + mock(), + mock(), dbMock ) activity.intent = Intent(activity, ComposeActivity::class.java).apply { putExtra(ComposeActivity.COMPOSE_OPTIONS_EXTRA, composeOptions) } - val viewModelFactoryMock = mock(ViewModelFactory::class.java) - `when`(viewModelFactoryMock.create(ComposeViewModel::class.java)).thenReturn(viewModel) + val viewModelFactoryMock: ViewModelFactory = mock { + on { create(ComposeViewModel::class.java) } doReturn viewModel + } activity.accountManager = accountManagerMock activity.viewModelFactory = viewModelFactoryMock @@ -490,7 +489,7 @@ class ComposeActivityTest { ) } - fun getCustomInstanceConfiguration(maximumStatusCharacters: Int? = null, charactersReservedPerUrl: Int? = null): InstanceConfiguration { + private fun getCustomInstanceConfiguration(maximumStatusCharacters: Int? = null, charactersReservedPerUrl: Int? = null): InstanceConfiguration { return InstanceConfiguration( statuses = StatusConfiguration( maxCharacters = maximumStatusCharacters, diff --git a/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt b/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt index 03fff5ee1..d50639431 100644 --- a/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt @@ -8,12 +8,12 @@ import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.entity.PollOption import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.network.FilterModel -import com.nhaarman.mockitokotlin2.mock import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.mock import org.robolectric.annotation.Config import java.util.ArrayList import java.util.Date @@ -22,7 +22,7 @@ import java.util.Date @RunWith(AndroidJUnit4::class) class FilterTest { - lateinit var filterModel: FilterModel + private lateinit var filterModel: FilterModel @Before fun setup() { diff --git a/app/src/test/java/com/keylesspalace/tusky/components/timeline/CachedTimelineRemoteMediatorTest.kt b/app/src/test/java/com/keylesspalace/tusky/components/timeline/CachedTimelineRemoteMediatorTest.kt index 462b0a4a0..2778f8c26 100644 --- a/app/src/test/java/com/keylesspalace/tusky/components/timeline/CachedTimelineRemoteMediatorTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/components/timeline/CachedTimelineRemoteMediatorTest.kt @@ -17,9 +17,6 @@ import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.db.Converters import com.keylesspalace.tusky.db.TimelineStatusWithAccount -import com.nhaarman.mockitokotlin2.anyOrNull -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.mock import io.reactivex.rxjava3.core.Single import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.runBlocking @@ -31,6 +28,9 @@ import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock import org.robolectric.Shadows.shadowOf import org.robolectric.annotation.Config import retrofit2.HttpException diff --git a/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelinePagingSourceTest.kt b/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelinePagingSourceTest.kt index 2e67c6fe3..60dda4193 100644 --- a/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelinePagingSourceTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelinePagingSourceTest.kt @@ -3,11 +3,11 @@ package com.keylesspalace.tusky.components.timeline import androidx.paging.PagingSource import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelinePagingSource import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelineViewModel -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.mock import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Test +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock class NetworkTimelinePagingSourceTest { diff --git a/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelineRemoteMediatorTest.kt b/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelineRemoteMediatorTest.kt index 74d0fe257..eabf744c2 100644 --- a/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelineRemoteMediatorTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelineRemoteMediatorTest.kt @@ -12,11 +12,6 @@ import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelineView import com.keylesspalace.tusky.db.AccountEntity import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.viewdata.StatusViewData -import com.nhaarman.mockitokotlin2.anyOrNull -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.doThrow -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.verify import kotlinx.coroutines.runBlocking import okhttp3.Headers import okhttp3.ResponseBody.Companion.toResponseBody @@ -24,6 +19,11 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.doThrow +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify import org.robolectric.annotation.Config import retrofit2.HttpException import retrofit2.Response @@ -331,7 +331,6 @@ class NetworkTimelineRemoteMediatorTest { mockStatusViewData("2"), mockStatusViewData("1"), ) - verify(timelineViewModel).nextKey = "0" assertTrue(result is RemoteMediator.MediatorResult.Success) assertEquals(false, (result as RemoteMediator.MediatorResult.Success).endOfPaginationReached) From ad077cf09293ded4db4e9a00e73b6d04a9a20b01 Mon Sep 17 00:00:00 2001 From: Levi Bard Date: Thu, 14 Apr 2022 19:58:08 +0200 Subject: [PATCH 15/21] Don't show preview cards on statuses with polls. (#2430) Fixes #2427 --- .../keylesspalace/tusky/adapter/StatusBaseViewHolder.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 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 1239ea715..dbca518ac 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java @@ -1043,9 +1043,11 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { StatusDisplayOptions statusDisplayOptions, final StatusActionListener listener ) { - final Card card = status.getActionable().getCard(); + final Status actionable = status.getActionable(); + final Card card = actionable.getCard(); if (cardViewMode != CardViewMode.NONE && - status.getActionable().getAttachments().size() == 0 && + actionable.getAttachments().size() == 0 && + actionable.getPoll() == null && card != null && !TextUtils.isEmpty(card.getUrl()) && (!status.isCollapsible() || !status.isCollapsed())) { @@ -1067,7 +1069,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { // Statuses from other activitypub sources can be marked sensitive even if there's no media, // so let's blur the preview in that case // If media previews are disabled, show placeholder for cards as well - if (statusDisplayOptions.mediaPreviewEnabled() && !status.getActionable().getSensitive() && !TextUtils.isEmpty(card.getImage())) { + if (statusDisplayOptions.mediaPreviewEnabled() && !actionable.getSensitive() && !TextUtils.isEmpty(card.getImage())) { int topLeftRadius = 0; int topRightRadius = 0; From 7aa328b3dced477946ef00dee9e4eef38da18905 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Fri, 15 Apr 2022 10:50:28 +0200 Subject: [PATCH 16/21] fix login on Android API level <24 (#2432) --- .../components/login/LoginWebViewActivity.kt | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) 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 01f6c3b0e..827b56208 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 @@ -16,6 +16,7 @@ import android.webkit.WebStorage import android.webkit.WebView import android.webkit.WebViewClient import androidx.activity.result.contract.ActivityResultContract +import androidx.core.net.toUri import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.BuildConfig import com.keylesspalace.tusky.databinding.LoginWebviewBinding @@ -103,8 +104,8 @@ class LoginWebViewActivity : BaseActivity(), Injectable { webView.webViewClient = object : WebViewClient() { override fun onReceivedError( - view: WebView?, - request: WebResourceRequest?, + view: WebView, + request: WebResourceRequest, error: WebResourceError ) { Log.d("LoginWeb", "Failed to load ${data.url}: $error") @@ -115,7 +116,17 @@ class LoginWebViewActivity : BaseActivity(), Injectable { view: WebView, request: WebResourceRequest ): Boolean { - val url = request.url + return shouldOverrideUrlLoading(request.url) + } + + /* overriding this deprecated method is necessary for it to work on api levels < 24 */ + @Suppress("OVERRIDE_DEPRECATION") + override fun shouldOverrideUrlLoading(view: WebView?, urlString: String?): Boolean { + val url = urlString?.toUri() ?: return false + return shouldOverrideUrlLoading(url) + } + + fun shouldOverrideUrlLoading(url: Uri): Boolean { return if (url.scheme == oauthUrl.scheme && url.host == oauthUrl.host) { val error = url.getQueryParameter("error") if (error != null) { @@ -130,6 +141,7 @@ class LoginWebViewActivity : BaseActivity(), Injectable { } } } + webView.setBackgroundColor(Color.TRANSPARENT) if (savedInstanceState == null) { From ffbc4b64037901a6d257cfbdc6bd09cbfc64ba88 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Fri, 15 Apr 2022 11:00:36 +0200 Subject: [PATCH 17/21] upgrade Kotlin Result CallAdapter to v1.0.1 to fix crash (#2433) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index f16f96754..3c5cb1624 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -137,7 +137,7 @@ dependencies { implementation "com.squareup.retrofit2:retrofit:$retrofitVersion" implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion" implementation "com.squareup.retrofit2:adapter-rxjava3:$retrofitVersion" - implementation "at.connyduck:kotlin-result-calladapter:1.0.0" + implementation "at.connyduck:kotlin-result-calladapter:1.0.1" implementation "com.squareup.okhttp3:okhttp:$okhttpVersion" implementation "com.squareup.okhttp3:logging-interceptor:$okhttpVersion" From 3e849244f9d1c77eee4614193832f9b263961ab0 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Fri, 15 Apr 2022 13:20:27 +0200 Subject: [PATCH 18/21] move Html parsing to ViewData (#2414) * move Html parsing to ViewData * refactor reports to use viewdata * cleanup code * refactor conversations * fix getEditableText * rename StatusParsingHelper * fix tests * commit db schema file * add file header * rename helper function to parseAsMastodonHtml * order imports correctly * move mapping off main thread to default dispatcher * fix ktlint --- .../33.json | 809 ++++++++++++++++++ .../components/account/AccountActivity.kt | 24 +- .../components/account/AccountFieldAdapter.kt | 3 +- .../conversation/ConversationAdapter.kt | 12 +- .../conversation/ConversationEntity.kt | 133 +-- .../conversation/ConversationViewData.kt | 87 ++ .../conversation/ConversationViewHolder.java | 20 +- .../conversation/ConversationsFragment.kt | 30 +- .../conversation/ConversationsViewModel.kt | 79 +- .../components/report/ReportViewModel.kt | 12 +- .../report/adapter/StatusViewHolder.kt | 58 +- .../report/adapter/StatusesAdapter.kt | 12 +- .../timeline/TimelineTypeMappers.kt | 16 +- .../viewmodel/CachedTimelineViewModel.kt | 11 +- .../viewmodel/NetworkTimelineViewModel.kt | 6 +- .../keylesspalace/tusky/db/AppDatabase.java | 39 +- .../tusky/db/ConversationsDao.kt | 5 +- .../com/keylesspalace/tusky/db/Converters.kt | 21 - .../com/keylesspalace/tusky/di/AppModule.kt | 1 + .../keylesspalace/tusky/di/NetworkModule.kt | 9 +- .../com/keylesspalace/tusky/entity/Account.kt | 55 +- .../tusky/entity/Announcement.kt | 3 +- .../com/keylesspalace/tusky/entity/Card.kt | 9 +- .../com/keylesspalace/tusky/entity/Status.kt | 74 +- .../tusky/json/SpannedTypeAdapter.kt | 54 -- .../tusky/util/StatusParsingHelper.kt | 62 ++ .../keylesspalace/tusky/util/ViewDataUtils.kt | 3 - .../tusky/viewdata/StatusViewData.kt | 53 +- .../tusky/BottomSheetActivityTest.kt | 3 +- .../tusky/ComposeActivityTest.kt | 3 +- .../com/keylesspalace/tusky/FilterTest.kt | 3 +- .../tusky/StatusComparisonTest.kt | 14 +- .../NetworkTimelinePagingSourceTest.kt | 5 + .../tusky/components/timeline/StatusMocker.kt | 4 +- 34 files changed, 1232 insertions(+), 500 deletions(-) create mode 100644 app/schemas/com.keylesspalace.tusky.db.AppDatabase/33.json create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewData.kt delete mode 100644 app/src/main/java/com/keylesspalace/tusky/json/SpannedTypeAdapter.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/StatusParsingHelper.kt diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/33.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/33.json new file mode 100644 index 000000000..e6d8ec7dc --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/33.json @@ -0,0 +1,809 @@ +{ + "formatVersion": 1, + "database": { + "version": 33, + "identityHash": "920a0e0c9a600bd236f6bf959b469c18", + "entities": [ + { + "tableName": "DraftEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationsSignUps` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSignUps", + "columnName": "notificationsSignUps", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "minPollDuration", + "columnName": "minPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollDuration", + "columnName": "maxPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "charactersReservedPerUrl", + "columnName": "charactersReservedPerUrl", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentCollapsed", + "columnName": "contentCollapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentShowing", + "columnName": "contentShowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.tags", + "columnName": "s_tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '920a0e0c9a600bd236f6bf959b469c18')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt index c218e1114..114a6cd0b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt @@ -78,7 +78,7 @@ import com.keylesspalace.tusky.util.emojify import com.keylesspalace.tusky.util.getDomain import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.loadAvatar -import com.keylesspalace.tusky.util.openLink +import com.keylesspalace.tusky.util.parseAsMastodonHtml import com.keylesspalace.tusky.util.setClickableText import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.viewBinding @@ -375,12 +375,11 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI } } viewModel.accountFieldData.observe( - this, - { - accountFieldAdapter.fields = it - accountFieldAdapter.notifyDataSetChanged() - } - ) + this + ) { + accountFieldAdapter.fields = it + accountFieldAdapter.notifyDataSetChanged() + } viewModel.noteSaved.observe(this) { binding.saveNoteInfo.visible(it, View.INVISIBLE) } @@ -395,11 +394,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI adapter.refreshContent() } viewModel.isRefreshing.observe( - this, - { isRefreshing -> - binding.swipeToRefreshLayout.isRefreshing = isRefreshing == true - } - ) + this + ) { isRefreshing -> + binding.swipeToRefreshLayout.isRefreshing = isRefreshing == true + } binding.swipeToRefreshLayout.setColorSchemeResources(R.color.tusky_blue) } @@ -410,7 +408,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI binding.accountUsernameTextView.text = usernameFormatted binding.accountDisplayNameTextView.text = account.name.emojify(account.emojis, binding.accountDisplayNameTextView, animateEmojis) - val emojifiedNote = account.note.emojify(account.emojis, binding.accountNoteTextView, animateEmojis) + val emojifiedNote = account.note.parseAsMastodonHtml().emojify(account.emojis, binding.accountNoteTextView, animateEmojis) setClickableText(binding.accountNoteTextView, emojifiedNote, emptyList(), null, this) // accountFieldAdapter.fields = account.fields ?: emptyList() diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountFieldAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountFieldAdapter.kt index 093dbcfb5..d51bb1452 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountFieldAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountFieldAdapter.kt @@ -29,6 +29,7 @@ import com.keylesspalace.tusky.util.BindingHolder import com.keylesspalace.tusky.util.Either import com.keylesspalace.tusky.util.createClickableText import com.keylesspalace.tusky.util.emojify +import com.keylesspalace.tusky.util.parseAsMastodonHtml import com.keylesspalace.tusky.util.setClickableText class AccountFieldAdapter( @@ -65,7 +66,7 @@ class AccountFieldAdapter( val emojifiedName = field.name.emojify(emojis, nameTextView, animateEmojis) nameTextView.text = emojifiedName - val emojifiedValue = field.value.emojify(emojis, valueTextView, animateEmojis) + val emojifiedValue = field.value.parseAsMastodonHtml().emojify(emojis, valueTextView, animateEmojis) setClickableText(valueTextView, emojifiedValue, emptyList(), null, linkListener) if (field.verifiedAt != null) { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt index 89c1ad0f1..0c9465142 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt @@ -26,7 +26,7 @@ import com.keylesspalace.tusky.util.StatusDisplayOptions class ConversationAdapter( private val statusDisplayOptions: StatusDisplayOptions, private val listener: StatusActionListener -) : PagingDataAdapter(CONVERSATION_COMPARATOR) { +) : PagingDataAdapter(CONVERSATION_COMPARATOR) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ConversationViewHolder { val view = LayoutInflater.from(parent.context).inflate(R.layout.item_conversation, parent, false) @@ -37,17 +37,13 @@ class ConversationAdapter( holder.setupWithConversation(getItem(position)) } - fun item(position: Int): ConversationEntity? { - return getItem(position) - } - companion object { - val CONVERSATION_COMPARATOR = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: ConversationEntity, newItem: ConversationEntity): Boolean { + val CONVERSATION_COMPARATOR = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: ConversationViewData, newItem: ConversationViewData): Boolean { return oldItem.id == newItem.id } - override fun areContentsTheSame(oldItem: ConversationEntity, newItem: ConversationEntity): Boolean { + override fun areContentsTheSame(oldItem: ConversationViewData, newItem: ConversationViewData): Boolean { return oldItem == newItem } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt index 88c9dbad1..f585b4ea5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt @@ -15,7 +15,6 @@ package com.keylesspalace.tusky.components.conversation -import android.text.Spanned import androidx.room.Embedded import androidx.room.Entity import androidx.room.TypeConverters @@ -27,7 +26,7 @@ import com.keylesspalace.tusky.entity.HashTag import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.TimelineAccount -import com.keylesspalace.tusky.util.shouldTrimStatus +import com.keylesspalace.tusky.viewdata.StatusViewData import java.util.Date @Entity(primaryKeys = ["id", "accountId"]) @@ -38,7 +37,16 @@ data class ConversationEntity( val accounts: List, val unread: Boolean, @Embedded(prefix = "s_") val lastStatus: ConversationStatusEntity -) +) { + fun toViewData(): ConversationViewData { + return ConversationViewData( + id = id, + accounts = accounts, + unread = unread, + lastStatus = lastStatus.toViewData() + ) + } +} data class ConversationAccountEntity( val id: String, @@ -67,7 +75,7 @@ data class ConversationStatusEntity( val inReplyToId: String?, val inReplyToAccountId: String?, val account: ConversationAccountEntity, - val content: Spanned, + val content: String, val createdAt: Date, val emojis: List, val favouritesCount: Int, @@ -80,95 +88,43 @@ data class ConversationStatusEntity( val tags: List?, val showingHiddenContent: Boolean, val expanded: Boolean, - val collapsible: Boolean, val collapsed: Boolean, val muted: Boolean, val poll: Poll? ) { - /** its necessary to override this because Spanned.equals does not work as expected */ - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - other as ConversationStatusEntity - - if (id != other.id) return false - if (url != other.url) return false - if (inReplyToId != other.inReplyToId) return false - if (inReplyToAccountId != other.inReplyToAccountId) return false - if (account != other.account) return false - if (content.toString() != other.content.toString()) return false - if (createdAt != other.createdAt) return false - if (emojis != other.emojis) return false - if (favouritesCount != other.favouritesCount) return false - if (favourited != other.favourited) return false - if (sensitive != other.sensitive) return false - if (spoilerText != other.spoilerText) return false - if (attachments != other.attachments) return false - if (mentions != other.mentions) return false - if (tags != other.tags) return false - if (showingHiddenContent != other.showingHiddenContent) return false - if (expanded != other.expanded) return false - if (collapsible != other.collapsible) return false - if (collapsed != other.collapsed) return false - if (muted != other.muted) return false - if (poll != other.poll) return false - - return true - } - - override fun hashCode(): Int { - var result = id.hashCode() - result = 31 * result + (url?.hashCode() ?: 0) - result = 31 * result + (inReplyToId?.hashCode() ?: 0) - result = 31 * result + (inReplyToAccountId?.hashCode() ?: 0) - result = 31 * result + account.hashCode() - result = 31 * result + content.toString().hashCode() - result = 31 * result + createdAt.hashCode() - result = 31 * result + emojis.hashCode() - result = 31 * result + favouritesCount - result = 31 * result + favourited.hashCode() - result = 31 * result + sensitive.hashCode() - result = 31 * result + spoilerText.hashCode() - result = 31 * result + attachments.hashCode() - result = 31 * result + mentions.hashCode() - result = 31 * result + tags.hashCode() - result = 31 * result + showingHiddenContent.hashCode() - result = 31 * result + expanded.hashCode() - result = 31 * result + collapsible.hashCode() - result = 31 * result + collapsed.hashCode() - result = 31 * result + muted.hashCode() - result = 31 * result + poll.hashCode() - return result - } - - fun toStatus(): Status { - return Status( - id = id, - url = url, - account = account.toAccount(), - inReplyToId = inReplyToId, - inReplyToAccountId = inReplyToAccountId, - content = content, - reblog = null, - createdAt = createdAt, - emojis = emojis, - reblogsCount = 0, - favouritesCount = favouritesCount, - reblogged = false, - favourited = favourited, - bookmarked = bookmarked, - sensitive = sensitive, - spoilerText = spoilerText, - visibility = Status.Visibility.DIRECT, - attachments = attachments, - mentions = mentions, - tags = tags, - application = null, - pinned = false, - muted = muted, - poll = poll, - card = null + fun toViewData(): StatusViewData.Concrete { + return StatusViewData.Concrete( + status = Status( + id = id, + url = url, + account = account.toAccount(), + inReplyToId = inReplyToId, + inReplyToAccountId = inReplyToAccountId, + content = content, + reblog = null, + createdAt = createdAt, + emojis = emojis, + reblogsCount = 0, + favouritesCount = favouritesCount, + reblogged = false, + favourited = favourited, + bookmarked = bookmarked, + sensitive = sensitive, + spoilerText = spoilerText, + visibility = Status.Visibility.DIRECT, + attachments = attachments, + mentions = mentions, + tags = tags, + application = null, + pinned = false, + muted = muted, + poll = poll, + card = null + ), + isExpanded = expanded, + isShowingContent = showingHiddenContent, + isCollapsed = collapsed ) } } @@ -202,7 +158,6 @@ fun Status.toEntity() = tags = tags, showingHiddenContent = false, expanded = false, - collapsible = shouldTrimStatus(content), collapsed = true, muted = muted ?: false, poll = poll diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewData.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewData.kt new file mode 100644 index 000000000..470675d17 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewData.kt @@ -0,0 +1,87 @@ +/* Copyright 2022 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.conversation + +import com.keylesspalace.tusky.entity.Poll +import com.keylesspalace.tusky.viewdata.StatusViewData + +data class ConversationViewData( + val id: String, + val accounts: List, + val unread: Boolean, + val lastStatus: StatusViewData.Concrete +) { + fun toEntity( + accountId: Long, + favourited: Boolean = lastStatus.status.favourited, + bookmarked: Boolean = lastStatus.status.bookmarked, + muted: Boolean = lastStatus.status.muted ?: false, + poll: Poll? = lastStatus.status.poll, + expanded: Boolean = lastStatus.isExpanded, + collapsed: Boolean = lastStatus.isCollapsed, + showingHiddenContent: Boolean = lastStatus.isShowingContent + ): ConversationEntity { + return ConversationEntity( + accountId = accountId, + id = id, + accounts = accounts, + unread = unread, + lastStatus = lastStatus.toConversationStatusEntity( + favourited = favourited, + bookmarked = bookmarked, + muted = muted, + poll = poll, + expanded = expanded, + collapsed = collapsed, + showingHiddenContent = showingHiddenContent + ) + ) + } +} + +fun StatusViewData.Concrete.toConversationStatusEntity( + favourited: Boolean = status.favourited, + bookmarked: Boolean = status.bookmarked, + muted: Boolean = status.muted ?: false, + poll: Poll? = status.poll, + expanded: Boolean = isExpanded, + collapsed: Boolean = isCollapsed, + showingHiddenContent: Boolean = isShowingContent +): ConversationStatusEntity { + return ConversationStatusEntity( + id = id, + url = status.url, + inReplyToId = status.inReplyToId, + inReplyToAccountId = status.inReplyToAccountId, + account = status.account.toEntity(), + content = status.content, + createdAt = status.createdAt, + emojis = status.emojis, + favouritesCount = status.favouritesCount, + favourited = favourited, + bookmarked = bookmarked, + sensitive = status.sensitive, + spoilerText = status.spoilerText, + attachments = status.attachments, + mentions = status.mentions, + tags = status.tags, + showingHiddenContent = showingHiddenContent, + expanded = expanded, + collapsed = collapsed, + muted = muted, + poll = poll + ) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java index 436ba84ea..ffb88a942 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java @@ -28,11 +28,14 @@ import androidx.recyclerview.widget.RecyclerView; import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.adapter.StatusBaseViewHolder; import com.keylesspalace.tusky.entity.Attachment; +import com.keylesspalace.tusky.entity.Status; +import com.keylesspalace.tusky.entity.TimelineAccount; import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.util.ImageLoadingHelper; import com.keylesspalace.tusky.util.SmartLengthInputFilter; import com.keylesspalace.tusky.util.StatusDisplayOptions; import com.keylesspalace.tusky.viewdata.PollViewDataKt; +import com.keylesspalace.tusky.viewdata.StatusViewData; import java.util.List; @@ -69,11 +72,12 @@ public class ConversationViewHolder extends StatusBaseViewHolder { return context.getResources().getDimensionPixelSize(R.dimen.status_media_preview_height); } - void setupWithConversation(ConversationEntity conversation) { - ConversationStatusEntity status = conversation.getLastStatus(); - ConversationAccountEntity account = status.getAccount(); + void setupWithConversation(ConversationViewData conversation) { + StatusViewData.Concrete statusViewData = conversation.getLastStatus(); + Status status = statusViewData.getStatus(); + TimelineAccount account = status.getAccount(); - setupCollapsedState(status.getCollapsible(), status.getCollapsed(), status.getExpanded(), status.getSpoilerText(), listener); + setupCollapsedState(statusViewData.isCollapsible(), statusViewData.isCollapsed(), statusViewData.isExpanded(), statusViewData.getSpoilerText(), listener); setDisplayName(account.getDisplayName(), account.getEmojis(), statusDisplayOptions); setUsername(account.getUsername()); @@ -84,7 +88,7 @@ public class ConversationViewHolder extends StatusBaseViewHolder { List attachments = status.getAttachments(); boolean sensitive = status.getSensitive(); if (statusDisplayOptions.mediaPreviewEnabled() && hasPreviewableAttachment(attachments)) { - setMediaPreviews(attachments, sensitive, listener, status.getShowingHiddenContent(), + setMediaPreviews(attachments, sensitive, listener, statusViewData.isShowingContent(), statusDisplayOptions.useBlurhash()); if (attachments.size() == 0) { @@ -95,7 +99,7 @@ public class ConversationViewHolder extends StatusBaseViewHolder { mediaLabel.setVisibility(View.GONE); } } else { - setMediaLabel(attachments, sensitive, listener, status.getShowingHiddenContent()); + setMediaLabel(attachments, sensitive, listener, statusViewData.isShowingContent()); // Hide all unused views. mediaPreviews[0].setVisibility(View.GONE); mediaPreviews[1].setVisibility(View.GONE); @@ -104,10 +108,10 @@ public class ConversationViewHolder extends StatusBaseViewHolder { hideSensitiveMediaWarning(); } - setupButtons(listener, account.getId(), status.getContent().toString(), + setupButtons(listener, account.getId(), statusViewData.getContent().toString(), statusDisplayOptions); - setSpoilerAndContent(status.getExpanded(), status.getContent(), status.getSpoilerText(), + setSpoilerAndContent(statusViewData.isExpanded(), statusViewData.getContent(), status.getSpoilerText(), status.getMentions(), status.getTags(), status.getEmojis(), PollViewDataKt.toViewData(status.getPoll()), statusDisplayOptions, listener); diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt index a09026c24..243c37448 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt @@ -153,24 +153,24 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res } override fun onFavourite(favourite: Boolean, position: Int) { - adapter.item(position)?.let { conversation -> + adapter.peek(position)?.let { conversation -> viewModel.favourite(favourite, conversation) } } override fun onBookmark(favourite: Boolean, position: Int) { - adapter.item(position)?.let { conversation -> + adapter.peek(position)?.let { conversation -> viewModel.bookmark(favourite, conversation) } } override fun onMore(view: View, position: Int) { - adapter.item(position)?.let { conversation -> + adapter.peek(position)?.let { conversation -> val popup = PopupMenu(requireContext(), view) popup.inflate(R.menu.conversation_more) - if (conversation.lastStatus.muted) { + if (conversation.lastStatus.status.muted == true) { popup.menu.removeItem(R.id.status_mute_conversation) } else { popup.menu.removeItem(R.id.status_unmute_conversation) @@ -189,14 +189,14 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res } override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) { - adapter.item(position)?.let { conversation -> - viewMedia(attachmentIndex, AttachmentViewData.list(conversation.lastStatus.toStatus()), view) + adapter.peek(position)?.let { conversation -> + viewMedia(attachmentIndex, AttachmentViewData.list(conversation.lastStatus.status), view) } } override fun onViewThread(position: Int) { - adapter.item(position)?.let { conversation -> - viewThread(conversation.lastStatus.id, conversation.lastStatus.url) + adapter.peek(position)?.let { conversation -> + viewThread(conversation.lastStatus.id, conversation.lastStatus.status.url) } } @@ -205,13 +205,13 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res } override fun onExpandedChange(expanded: Boolean, position: Int) { - adapter.item(position)?.let { conversation -> + adapter.peek(position)?.let { conversation -> viewModel.expandHiddenStatus(expanded, conversation) } } override fun onContentHiddenChange(isShowing: Boolean, position: Int) { - adapter.item(position)?.let { conversation -> + adapter.peek(position)?.let { conversation -> viewModel.showContent(isShowing, conversation) } } @@ -221,7 +221,7 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res } override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) { - adapter.item(position)?.let { conversation -> + adapter.peek(position)?.let { conversation -> viewModel.collapseLongStatus(isCollapsed, conversation) } } @@ -241,12 +241,12 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res } override fun onReply(position: Int) { - adapter.item(position)?.let { conversation -> - reply(conversation.lastStatus.toStatus()) + adapter.peek(position)?.let { conversation -> + reply(conversation.lastStatus.status) } } - private fun deleteConversation(conversation: ConversationEntity) { + private fun deleteConversation(conversation: ConversationViewData) { AlertDialog.Builder(requireContext()) .setMessage(R.string.dialog_delete_conversation_warning) .setNegativeButton(android.R.string.cancel, null) @@ -268,7 +268,7 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res } override fun onVoteInPoll(position: Int, choices: MutableList) { - adapter.item(position)?.let { conversation -> + adapter.peek(position)?.let { conversation -> viewModel.voteInPoll(choices, conversation) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt index 396f8e486..9326a05c0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt @@ -16,16 +16,18 @@ package com.keylesspalace.tusky.components.conversation import android.util.Log +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.paging.ExperimentalPagingApi import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.cachedIn +import androidx.paging.map import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.TimelineCases -import com.keylesspalace.tusky.util.RxAwareViewModel +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.rx3.await import javax.inject.Inject @@ -35,7 +37,7 @@ class ConversationsViewModel @Inject constructor( private val database: AppDatabase, private val accountManager: AccountManager, private val api: MastodonApi -) : RxAwareViewModel() { +) : ViewModel() { @OptIn(ExperimentalPagingApi::class) val conversationFlow = Pager( @@ -44,104 +46,117 @@ class ConversationsViewModel @Inject constructor( pagingSourceFactory = { database.conversationDao().conversationsForAccount(accountManager.activeAccount!!.id) } ) .flow + .map { pagingData -> + pagingData.map { conversation -> conversation.toViewData() } + } .cachedIn(viewModelScope) - fun favourite(favourite: Boolean, conversation: ConversationEntity) { + fun favourite(favourite: Boolean, conversation: ConversationViewData) { viewModelScope.launch { try { timelineCases.favourite(conversation.lastStatus.id, favourite).await() - val newConversation = conversation.copy( - lastStatus = conversation.lastStatus.copy(favourited = favourite) + val newConversation = conversation.toEntity( + accountId = accountManager.activeAccount!!.id, + favourited = favourite ) - database.conversationDao().insert(newConversation) + saveConversationToDb(newConversation) } catch (e: Exception) { Log.w(TAG, "failed to favourite status", e) } } } - fun bookmark(bookmark: Boolean, conversation: ConversationEntity) { + fun bookmark(bookmark: Boolean, conversation: ConversationViewData) { viewModelScope.launch { try { timelineCases.bookmark(conversation.lastStatus.id, bookmark).await() - val newConversation = conversation.copy( - lastStatus = conversation.lastStatus.copy(bookmarked = bookmark) + val newConversation = conversation.toEntity( + accountId = accountManager.activeAccount!!.id, + bookmarked = bookmark ) - database.conversationDao().insert(newConversation) + saveConversationToDb(newConversation) } catch (e: Exception) { Log.w(TAG, "failed to bookmark status", e) } } } - fun voteInPoll(choices: List, conversation: ConversationEntity) { + fun voteInPoll(choices: List, conversation: ConversationViewData) { viewModelScope.launch { try { - val poll = timelineCases.voteInPoll(conversation.lastStatus.id, conversation.lastStatus.poll?.id!!, choices).await() - val newConversation = conversation.copy( - lastStatus = conversation.lastStatus.copy(poll = poll) + val poll = timelineCases.voteInPoll(conversation.lastStatus.id, conversation.lastStatus.status.poll?.id!!, choices).await() + val newConversation = conversation.toEntity( + accountId = accountManager.activeAccount!!.id, + poll = poll ) - database.conversationDao().insert(newConversation) + saveConversationToDb(newConversation) } catch (e: Exception) { Log.w(TAG, "failed to vote in poll", e) } } } - fun expandHiddenStatus(expanded: Boolean, conversation: ConversationEntity) { + fun expandHiddenStatus(expanded: Boolean, conversation: ConversationViewData) { viewModelScope.launch { - val newConversation = conversation.copy( - lastStatus = conversation.lastStatus.copy(expanded = expanded) + val newConversation = conversation.toEntity( + accountId = accountManager.activeAccount!!.id, + expanded = expanded ) saveConversationToDb(newConversation) } } - fun collapseLongStatus(collapsed: Boolean, conversation: ConversationEntity) { + fun collapseLongStatus(collapsed: Boolean, conversation: ConversationViewData) { viewModelScope.launch { - val newConversation = conversation.copy( - lastStatus = conversation.lastStatus.copy(collapsed = collapsed) + val newConversation = conversation.toEntity( + accountId = accountManager.activeAccount!!.id, + collapsed = collapsed ) saveConversationToDb(newConversation) } } - fun showContent(showing: Boolean, conversation: ConversationEntity) { + fun showContent(showing: Boolean, conversation: ConversationViewData) { viewModelScope.launch { - val newConversation = conversation.copy( - lastStatus = conversation.lastStatus.copy(showingHiddenContent = showing) + val newConversation = conversation.toEntity( + accountId = accountManager.activeAccount!!.id, + showingHiddenContent = showing ) saveConversationToDb(newConversation) } } - fun remove(conversation: ConversationEntity) { + fun remove(conversation: ConversationViewData) { viewModelScope.launch { try { api.deleteConversation(conversationId = conversation.id) - database.conversationDao().delete(conversation) + database.conversationDao().delete( + id = conversation.id, + accountId = accountManager.activeAccount!!.id + ) } catch (e: Exception) { Log.w(TAG, "failed to delete conversation", e) } } } - fun muteConversation(conversation: ConversationEntity) { + fun muteConversation(conversation: ConversationViewData) { viewModelScope.launch { try { - val newStatus = timelineCases.muteConversation( + timelineCases.muteConversation( conversation.lastStatus.id, - !conversation.lastStatus.muted + !(conversation.lastStatus.status.muted ?: false) ).await() - val newConversation = conversation.copy( - lastStatus = newStatus.toEntity() + val newConversation = conversation.toEntity( + accountId = accountManager.activeAccount!!.id, + muted = !(conversation.lastStatus.status.muted ?: false) ) database.conversationDao().insert(newConversation) @@ -151,7 +166,7 @@ class ConversationsViewModel @Inject constructor( } } - suspend fun saveConversationToDb(conversation: ConversationEntity) { + private suspend fun saveConversationToDb(conversation: ConversationEntity) { database.conversationDao().insert(conversation) } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt index f8991282d..9f99da530 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt @@ -21,6 +21,7 @@ import androidx.lifecycle.viewModelScope import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.cachedIn +import androidx.paging.map import com.keylesspalace.tusky.appstore.BlockEvent import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.MuteEvent @@ -34,11 +35,13 @@ import com.keylesspalace.tusky.util.Loading import com.keylesspalace.tusky.util.Resource import com.keylesspalace.tusky.util.RxAwareViewModel import com.keylesspalace.tusky.util.Success +import com.keylesspalace.tusky.util.toViewData import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.schedulers.Schedulers import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import javax.inject.Inject @@ -74,6 +77,11 @@ class ReportViewModel @Inject constructor( pagingSourceFactory = { StatusesPagingSource(accountId, mastodonApi) } ).flow } + .map { pagingData -> + /* TODO: refactor reports to use the isShowingContent / isExpanded / isCollapsed attributes from StatusViewData.Concrete + instead of StatusViewState */ + pagingData.map { status -> status.toViewData(false, false, false) } + } .cachedIn(viewModelScope) private val selectedIds = HashSet() @@ -155,7 +163,7 @@ class ReportViewModel @Inject constructor( .observeOn(AndroidSchedulers.mainThread()) .subscribe( { relationship -> - val muting = relationship?.muting == true + val muting = relationship.muting muteStateMutable.value = Success(muting) if (muting) { eventHub.dispatch(MuteEvent(accountId)) @@ -180,7 +188,7 @@ class ReportViewModel @Inject constructor( .observeOn(AndroidSchedulers.mainThread()) .subscribe( { relationship -> - val blocking = relationship?.blocking == true + val blocking = relationship.blocking blockStateMutable.value = Success(blocking) if (blocking) { eventHub.dispatch(BlockEvent(accountId)) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt index 1b3b0de62..9dceddecb 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt @@ -37,6 +37,7 @@ import com.keylesspalace.tusky.util.setClickableMentions import com.keylesspalace.tusky.util.setClickableText import com.keylesspalace.tusky.util.shouldTrimStatus import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.viewdata.StatusViewData import com.keylesspalace.tusky.viewdata.toViewData import java.util.Date @@ -45,20 +46,21 @@ class StatusViewHolder( private val statusDisplayOptions: StatusDisplayOptions, private val viewState: StatusViewState, private val adapterHandler: AdapterHandler, - private val getStatusForPosition: (Int) -> Status? + private val getStatusForPosition: (Int) -> StatusViewData.Concrete? ) : RecyclerView.ViewHolder(binding.root) { + private val mediaViewHeight = itemView.context.resources.getDimensionPixelSize(R.dimen.status_media_preview_height) private val statusViewHelper = StatusViewHelper(itemView) private val previewListener = object : StatusViewHelper.MediaPreviewListener { override fun onViewMedia(v: View?, idx: Int) { - status()?.let { status -> - adapterHandler.showMedia(v, status, idx) + viewdata()?.let { viewdata -> + adapterHandler.showMedia(v, viewdata.status, idx) } } override fun onContentHiddenChange(isShowing: Boolean) { - status()?.id?.let { id -> + viewdata()?.id?.let { id -> viewState.setMediaShow(id, isShowing) } } @@ -66,57 +68,57 @@ class StatusViewHolder( init { binding.statusSelection.setOnCheckedChangeListener { _, isChecked -> - status()?.let { status -> - adapterHandler.setStatusChecked(status, isChecked) + viewdata()?.let { viewdata -> + adapterHandler.setStatusChecked(viewdata.status, isChecked) } } binding.statusMediaPreviewContainer.clipToOutline = true } - fun bind(status: Status) { - binding.statusSelection.isChecked = adapterHandler.isStatusChecked(status.id) + fun bind(viewData: StatusViewData.Concrete) { + binding.statusSelection.isChecked = adapterHandler.isStatusChecked(viewData.id) updateTextView() - val sensitive = status.sensitive + val sensitive = viewData.status.sensitive statusViewHelper.setMediasPreview( - statusDisplayOptions, status.attachments, - sensitive, previewListener, viewState.isMediaShow(status.id, status.sensitive), + statusDisplayOptions, viewData.status.attachments, + sensitive, previewListener, viewState.isMediaShow(viewData.id, viewData.status.sensitive), mediaViewHeight ) - statusViewHelper.setupPollReadonly(status.poll.toViewData(), status.emojis, statusDisplayOptions) - setCreatedAt(status.createdAt) + statusViewHelper.setupPollReadonly(viewData.status.poll.toViewData(), viewData.status.emojis, statusDisplayOptions) + setCreatedAt(viewData.status.createdAt) } private fun updateTextView() { - status()?.let { status -> + viewdata()?.let { viewdata -> setupCollapsedState( - shouldTrimStatus(status.content), viewState.isCollapsed(status.id, true), - viewState.isContentShow(status.id, status.sensitive), status.spoilerText + shouldTrimStatus(viewdata.content), viewState.isCollapsed(viewdata.id, true), + viewState.isContentShow(viewdata.id, viewdata.status.sensitive), viewdata.spoilerText ) - if (status.spoilerText.isBlank()) { - setTextVisible(true, status.content, status.mentions, status.tags, status.emojis, adapterHandler) + if (viewdata.spoilerText.isBlank()) { + setTextVisible(true, viewdata.content, viewdata.status.mentions, viewdata.status.tags, viewdata.status.emojis, adapterHandler) binding.statusContentWarningButton.hide() binding.statusContentWarningDescription.hide() } else { - val emojiSpoiler = status.spoilerText.emojify(status.emojis, binding.statusContentWarningDescription, statusDisplayOptions.animateEmojis) + val emojiSpoiler = viewdata.spoilerText.emojify(viewdata.status.emojis, binding.statusContentWarningDescription, statusDisplayOptions.animateEmojis) binding.statusContentWarningDescription.text = emojiSpoiler binding.statusContentWarningDescription.show() binding.statusContentWarningButton.show() - setContentWarningButtonText(viewState.isContentShow(status.id, true)) + setContentWarningButtonText(viewState.isContentShow(viewdata.id, true)) binding.statusContentWarningButton.setOnClickListener { - status()?.let { status -> - val contentShown = viewState.isContentShow(status.id, true) + viewdata()?.let { viewdata -> + val contentShown = viewState.isContentShow(viewdata.id, true) binding.statusContentWarningDescription.invalidate() - viewState.setContentShow(status.id, !contentShown) - setTextVisible(!contentShown, status.content, status.mentions, status.tags, status.emojis, adapterHandler) + viewState.setContentShow(viewdata.id, !contentShown) + setTextVisible(!contentShown, viewdata.content, viewdata.status.mentions, viewdata.status.tags, viewdata.status.emojis, adapterHandler) setContentWarningButtonText(!contentShown) } } - setTextVisible(viewState.isContentShow(status.id, true), status.content, status.mentions, status.tags, status.emojis, adapterHandler) + setTextVisible(viewState.isContentShow(viewdata.id, true), viewdata.content, viewdata.status.mentions, viewdata.status.tags, viewdata.status.emojis, adapterHandler) } } } @@ -169,8 +171,8 @@ class StatusViewHolder( /* input filter for TextViews have to be set before text */ if (collapsible && (expanded || TextUtils.isEmpty(spoilerText))) { binding.buttonToggleContent.setOnClickListener { - status()?.let { status -> - viewState.setCollapsed(status.id, !collapsed) + viewdata()?.let { viewdata -> + viewState.setCollapsed(viewdata.id, !collapsed) updateTextView() } } @@ -189,5 +191,5 @@ class StatusViewHolder( } } - private fun status() = getStatusForPosition(bindingAdapterPosition) + private fun viewdata() = getStatusForPosition(bindingAdapterPosition) } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesAdapter.kt index 76ed2ebea..314513eb9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesAdapter.kt @@ -22,16 +22,16 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.keylesspalace.tusky.components.report.model.StatusViewState import com.keylesspalace.tusky.databinding.ItemReportStatusBinding -import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.viewdata.StatusViewData class StatusesAdapter( private val statusDisplayOptions: StatusDisplayOptions, private val statusViewState: StatusViewState, private val adapterHandler: AdapterHandler -) : PagingDataAdapter(STATUS_COMPARATOR) { +) : PagingDataAdapter(STATUS_COMPARATOR) { - private val statusForPosition: (Int) -> Status? = { position: Int -> + private val statusForPosition: (Int) -> StatusViewData.Concrete? = { position: Int -> if (position != RecyclerView.NO_POSITION) getItem(position) else null } @@ -50,11 +50,11 @@ class StatusesAdapter( } companion object { - val STATUS_COMPARATOR = object : DiffUtil.ItemCallback() { - override fun areContentsTheSame(oldItem: Status, newItem: Status): Boolean = + val STATUS_COMPARATOR = object : DiffUtil.ItemCallback() { + override fun areContentsTheSame(oldItem: StatusViewData.Concrete, newItem: StatusViewData.Concrete): Boolean = oldItem == newItem - override fun areItemsTheSame(oldItem: Status, newItem: Status): Boolean = + override fun areItemsTheSame(oldItem: StatusViewData.Concrete, newItem: StatusViewData.Concrete): Boolean = oldItem.id == newItem.id } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineTypeMappers.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineTypeMappers.kt index 252b98800..6ec954239 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineTypeMappers.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineTypeMappers.kt @@ -15,9 +15,6 @@ package com.keylesspalace.tusky.components.timeline -import android.text.SpannedString -import androidx.core.text.parseAsHtml -import androidx.core.text.toHtml import com.google.gson.Gson import com.google.gson.reflect.TypeToken import com.keylesspalace.tusky.db.TimelineAccountEntity @@ -29,8 +26,6 @@ import com.keylesspalace.tusky.entity.HashTag import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.TimelineAccount -import com.keylesspalace.tusky.util.shouldTrimStatus -import com.keylesspalace.tusky.util.trimTrailingWhitespace import com.keylesspalace.tusky.viewdata.StatusViewData import java.util.Date @@ -119,7 +114,7 @@ fun Status.toEntity( authorServerId = actionableStatus.account.id, inReplyToId = actionableStatus.inReplyToId, inReplyToAccountId = actionableStatus.inReplyToAccountId, - content = actionableStatus.content.toHtml(), + content = actionableStatus.content, createdAt = actionableStatus.createdAt.time, emojis = actionableStatus.emojis.let(gson::toJson), reblogsCount = actionableStatus.reblogsCount, @@ -165,8 +160,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData { inReplyToId = status.inReplyToId, inReplyToAccountId = status.inReplyToAccountId, reblog = null, - content = status.content?.parseAsHtml()?.trimTrailingWhitespace() - ?: SpannedString(""), + content = status.content.orEmpty(), createdAt = Date(status.createdAt), emojis = emojis, reblogsCount = status.reblogsCount, @@ -195,7 +189,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData { inReplyToId = null, inReplyToAccountId = null, reblog = reblog, - content = SpannedString(""), + content = "", createdAt = Date(status.createdAt), // lie but whatever? emojis = listOf(), reblogsCount = 0, @@ -223,8 +217,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData { inReplyToId = status.inReplyToId, inReplyToAccountId = status.inReplyToAccountId, reblog = null, - content = status.content?.parseAsHtml()?.trimTrailingWhitespace() - ?: SpannedString(""), + content = status.content.orEmpty(), createdAt = Date(status.createdAt), emojis = emojis, reblogsCount = status.reblogsCount, @@ -249,7 +242,6 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData { status = status, isExpanded = this.status.expanded, isShowingContent = this.status.contentShowing, - isCollapsible = shouldTrimStatus(status.content), isCollapsed = this.status.contentCollapsed ) } 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 304b4e5a0..7158a7b3a 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 @@ -42,7 +42,10 @@ import com.keylesspalace.tusky.network.FilterModel import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.TimelineCases 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 kotlinx.coroutines.rx3.await @@ -79,15 +82,13 @@ class CachedTimelineViewModel @Inject constructor( } ).flow .map { pagingData -> - pagingData.map { timelineStatus -> + pagingData.map(Dispatchers.Default.asExecutor()) { timelineStatus -> timelineStatus.toViewData(gson) - } - } - .map { pagingData -> - pagingData.filter { statusViewData -> + }.filter(Dispatchers.Default.asExecutor()) { statusViewData -> !shouldFilterStatus(statusViewData) } } + .flowOn(Dispatchers.Default) .cachedIn(viewModelScope) init { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt index f70fdcc81..ca7988bb9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt @@ -40,6 +40,9 @@ import com.keylesspalace.tusky.util.isLessThan import com.keylesspalace.tusky.util.isLessThanOrEqual import com.keylesspalace.tusky.util.toViewData import com.keylesspalace.tusky.viewdata.StatusViewData +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.asExecutor +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.rx3.await @@ -79,10 +82,11 @@ class NetworkTimelineViewModel @Inject constructor( remoteMediator = NetworkTimelineRemoteMediator(accountManager, this) ).flow .map { pagingData -> - pagingData.filter { statusViewData -> + pagingData.filter(Dispatchers.Default.asExecutor()) { statusViewData -> !shouldFilterStatus(statusViewData) } } + .flowOn(Dispatchers.Default) .cachedIn(viewModelScope) override fun updatePoll(newPoll: Poll, status: StatusViewData.Concrete) { diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java index 2131300c7..c541958ae 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java +++ b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java @@ -31,7 +31,7 @@ import java.io.File; */ @Database(entities = { DraftEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class, TimelineAccountEntity.class, ConversationEntity.class - }, version = 32) + }, version = 33) public abstract class AppDatabase extends RoomDatabase { public abstract AccountDao accountDao(); @@ -490,4 +490,41 @@ public abstract class AppDatabase extends RoomDatabase { database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsSignUps` INTEGER NOT NULL DEFAULT 1"); } }; + + public static final Migration MIGRATION_32_33 = new Migration(32, 33) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + + // ConversationEntity lost the s_collapsible column + // since SQLite does not support removing columns and it is just a cache table, we recreate the whole table. + database.execSQL("DROP TABLE `ConversationEntity`"); + database.execSQL("CREATE TABLE IF NOT EXISTS `ConversationEntity` (" + + "`accountId` INTEGER NOT NULL," + + "`id` TEXT NOT NULL," + + "`accounts` TEXT NOT NULL," + + "`unread` INTEGER NOT NULL," + + "`s_id` TEXT NOT NULL," + + "`s_url` TEXT," + + "`s_inReplyToId` TEXT," + + "`s_inReplyToAccountId` TEXT," + + "`s_account` TEXT NOT NULL," + + "`s_content` TEXT NOT NULL," + + "`s_createdAt` INTEGER NOT NULL," + + "`s_emojis` TEXT NOT NULL," + + "`s_favouritesCount` INTEGER NOT NULL," + + "`s_favourited` INTEGER NOT NULL," + + "`s_bookmarked` INTEGER NOT NULL," + + "`s_sensitive` INTEGER NOT NULL," + + "`s_spoilerText` TEXT NOT NULL," + + "`s_attachments` TEXT NOT NULL," + + "`s_mentions` TEXT NOT NULL," + + "`s_tags` TEXT," + + "`s_showingHiddenContent` INTEGER NOT NULL," + + "`s_expanded` INTEGER NOT NULL," + + "`s_collapsed` INTEGER NOT NULL," + + "`s_muted` INTEGER NOT NULL," + + "`s_poll` TEXT," + + "PRIMARY KEY(`id`, `accountId`))"); + } + }; } diff --git a/app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt index 393a23925..fe093bd0c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt @@ -17,7 +17,6 @@ package com.keylesspalace.tusky.db import androidx.paging.PagingSource import androidx.room.Dao -import androidx.room.Delete import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query @@ -31,8 +30,8 @@ interface ConversationsDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(conversation: ConversationEntity): Long - @Delete - suspend fun delete(conversation: ConversationEntity): Int + @Query("DELETE FROM ConversationEntity WHERE id = :id AND accountId = :accountId") + suspend fun delete(id: String, accountId: Long): Int @Query("SELECT * FROM ConversationEntity WHERE accountId = :accountId ORDER BY s_createdAt DESC") fun conversationsForAccount(accountId: Long): PagingSource diff --git a/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt b/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt index c9daec0a1..34ff6474b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt @@ -15,9 +15,6 @@ package com.keylesspalace.tusky.db -import android.text.Spanned -import androidx.core.text.parseAsHtml -import androidx.core.text.toHtml import androidx.room.ProvidedTypeConverter import androidx.room.TypeConverter import com.google.gson.Gson @@ -31,10 +28,8 @@ import com.keylesspalace.tusky.entity.HashTag import com.keylesspalace.tusky.entity.NewPoll import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.entity.Status -import com.keylesspalace.tusky.util.trimTrailingWhitespace import java.net.URLDecoder import java.net.URLEncoder -import java.util.ArrayList import java.util.Date import javax.inject.Inject import javax.inject.Singleton @@ -140,22 +135,6 @@ class Converters @Inject constructor ( return Date(date) } - @TypeConverter - fun spannedToString(spanned: Spanned?): String? { - if (spanned == null) { - return null - } - return spanned.toHtml() - } - - @TypeConverter - fun stringToSpanned(spannedString: String?): Spanned? { - if (spannedString == null) { - return null - } - return spannedString.parseAsHtml().trimTrailingWhitespace() - } - @TypeConverter fun pollToJson(poll: Poll?): String? { return gson.toJson(poll) diff --git a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt index 677f81677..7f0fbd015 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt @@ -63,6 +63,7 @@ class AppModule { AppDatabase.Migration25_26(appContext.getExternalFilesDir("Tusky")), AppDatabase.MIGRATION_26_27, AppDatabase.MIGRATION_27_28, AppDatabase.MIGRATION_28_29, AppDatabase.MIGRATION_29_30, AppDatabase.MIGRATION_30_31, AppDatabase.MIGRATION_31_32, + AppDatabase.MIGRATION_32_33 ) .build() } diff --git a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt index d927c2999..d8b52ca38 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt @@ -18,13 +18,10 @@ package com.keylesspalace.tusky.di import android.content.Context import android.content.SharedPreferences import android.os.Build -import android.text.Spanned import at.connyduck.calladapter.kotlinresult.KotlinResultCallAdapterFactory import com.google.gson.Gson -import com.google.gson.GsonBuilder import com.keylesspalace.tusky.BuildConfig import com.keylesspalace.tusky.db.AccountManager -import com.keylesspalace.tusky.json.SpannedTypeAdapter import com.keylesspalace.tusky.network.InstanceSwitchAuthInterceptor import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.getNonNullString @@ -52,11 +49,7 @@ class NetworkModule { @Provides @Singleton - fun providesGson(): Gson { - return GsonBuilder() - .registerTypeAdapter(Spanned::class.java, SpannedTypeAdapter()) - .create() - } + fun providesGson() = Gson() @Provides @Singleton diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Account.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Account.kt index 672bd5aae..bf5431ee6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Account.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Account.kt @@ -15,7 +15,6 @@ package com.keylesspalace.tusky.entity -import android.text.Spanned import com.google.gson.annotations.SerializedName import java.util.Date @@ -24,7 +23,7 @@ data class Account( @SerializedName("username") val localUsername: String, @SerializedName("acct") val username: String, @SerializedName("display_name") val displayName: String?, // should never be null per Api definition, but some servers break the contract - val note: Spanned, + val note: String, val url: String, val avatar: String, val header: String, @@ -46,56 +45,6 @@ data class Account( } else displayName fun isRemote(): Boolean = this.username != this.localUsername - - /** - * overriding equals & hashcode because Spanned does not always compare correctly otherwise - */ - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - other as Account - - if (id != other.id) return false - if (localUsername != other.localUsername) return false - if (username != other.username) return false - if (displayName != other.displayName) return false - if (note.toString() != other.note.toString()) return false - if (url != other.url) return false - if (avatar != other.avatar) return false - if (header != other.header) return false - if (locked != other.locked) return false - if (followersCount != other.followersCount) return false - if (followingCount != other.followingCount) return false - if (statusesCount != other.statusesCount) return false - if (source != other.source) return false - if (bot != other.bot) return false - if (emojis != other.emojis) return false - if (fields != other.fields) return false - if (moved != other.moved) return false - - return true - } - - override fun hashCode(): Int { - var result = id.hashCode() - result = 31 * result + localUsername.hashCode() - result = 31 * result + username.hashCode() - result = 31 * result + (displayName?.hashCode() ?: 0) - result = 31 * result + note.toString().hashCode() - result = 31 * result + url.hashCode() - result = 31 * result + avatar.hashCode() - result = 31 * result + header.hashCode() - result = 31 * result + locked.hashCode() - result = 31 * result + followersCount - result = 31 * result + followingCount - result = 31 * result + statusesCount - result = 31 * result + (source?.hashCode() ?: 0) - result = 31 * result + bot.hashCode() - result = 31 * result + (emojis?.hashCode() ?: 0) - result = 31 * result + (fields?.hashCode() ?: 0) - result = 31 * result + (moved?.hashCode() ?: 0) - return result - } } data class AccountSource( @@ -107,7 +56,7 @@ data class AccountSource( data class Field( val name: String, - val value: Spanned, + val value: String, @SerializedName("verified_at") val verifiedAt: Date? ) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Announcement.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Announcement.kt index 400e9764d..00d5659d5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Announcement.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Announcement.kt @@ -15,13 +15,12 @@ package com.keylesspalace.tusky.entity -import android.text.Spanned import com.google.gson.annotations.SerializedName import java.util.Date data class Announcement( val id: String, - val content: Spanned, + val content: String, @SerializedName("starts_at") val startsAt: Date?, @SerializedName("ends_at") val endsAt: Date?, @SerializedName("all_day") val allDay: Boolean, diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Card.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Card.kt index 52011f3d1..29fe7f8ee 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Card.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Card.kt @@ -15,13 +15,12 @@ package com.keylesspalace.tusky.entity -import android.text.Spanned import com.google.gson.annotations.SerializedName data class Card( val url: String, - val title: Spanned, - val description: Spanned, + val title: String, + val description: String, @SerializedName("author_name") val authorName: String, val image: String, val type: String, @@ -31,9 +30,7 @@ data class Card( val embed_url: String? ) { - override fun hashCode(): Int { - return url.hashCode() - } + override fun hashCode() = url.hashCode() override fun equals(other: Any?): Boolean { if (other !is Card) { diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt index f75ce4e76..19cb7aa64 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt @@ -16,9 +16,9 @@ package com.keylesspalace.tusky.entity import android.text.SpannableStringBuilder -import android.text.Spanned import android.text.style.URLSpan import com.google.gson.annotations.SerializedName +import com.keylesspalace.tusky.util.parseAsMastodonHtml import java.util.ArrayList import java.util.Date @@ -29,7 +29,7 @@ data class Status( @SerializedName("in_reply_to_id") var inReplyToId: String?, @SerializedName("in_reply_to_account_id") val inReplyToAccountId: String?, val reblog: Status?, - val content: Spanned, + val content: String, @SerializedName("created_at") val createdAt: Date, val emojis: List, @SerializedName("reblogs_count") val reblogsCount: Int, @@ -134,8 +134,9 @@ data class Status( } private fun getEditableText(): String { - val builder = SpannableStringBuilder(content) - for (span in content.getSpans(0, content.length, URLSpan::class.java)) { + val contentSpanned = content.parseAsMastodonHtml() + val builder = SpannableStringBuilder(content.parseAsMastodonHtml()) + for (span in contentSpanned.getSpans(0, content.length, URLSpan::class.java)) { val url = span.url for ((_, url1, username) in mentions) { if (url == url1) { @@ -149,71 +150,6 @@ data class Status( return builder.toString() } - /** - * overriding equals & hashcode because Spanned does not always compare correctly otherwise - */ - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - other as Status - - if (id != other.id) return false - if (url != other.url) return false - if (account != other.account) return false - if (inReplyToId != other.inReplyToId) return false - if (inReplyToAccountId != other.inReplyToAccountId) return false - if (reblog != other.reblog) return false - if (content.toString() != other.content.toString()) return false - if (createdAt != other.createdAt) return false - if (emojis != other.emojis) return false - if (reblogsCount != other.reblogsCount) return false - if (favouritesCount != other.favouritesCount) return false - if (reblogged != other.reblogged) return false - if (favourited != other.favourited) return false - if (bookmarked != other.bookmarked) return false - if (sensitive != other.sensitive) return false - if (spoilerText != other.spoilerText) return false - if (visibility != other.visibility) return false - if (attachments != other.attachments) return false - if (mentions != other.mentions) return false - if (tags != other.tags) return false - if (application != other.application) return false - if (pinned != other.pinned) return false - if (muted != other.muted) return false - if (poll != other.poll) return false - if (card != other.card) return false - return true - } - - override fun hashCode(): Int { - var result = id.hashCode() - result = 31 * result + (url?.hashCode() ?: 0) - result = 31 * result + account.hashCode() - result = 31 * result + (inReplyToId?.hashCode() ?: 0) - result = 31 * result + (inReplyToAccountId?.hashCode() ?: 0) - result = 31 * result + (reblog?.hashCode() ?: 0) - result = 31 * result + content.toString().hashCode() - result = 31 * result + createdAt.hashCode() - result = 31 * result + emojis.hashCode() - result = 31 * result + reblogsCount - result = 31 * result + favouritesCount - result = 31 * result + reblogged.hashCode() - result = 31 * result + favourited.hashCode() - result = 31 * result + bookmarked.hashCode() - result = 31 * result + sensitive.hashCode() - result = 31 * result + spoilerText.hashCode() - result = 31 * result + visibility.hashCode() - result = 31 * result + attachments.hashCode() - result = 31 * result + mentions.hashCode() - result = 31 * result + (tags?.hashCode() ?: 0) - result = 31 * result + (application?.hashCode() ?: 0) - result = 31 * result + (pinned?.hashCode() ?: 0) - result = 31 * result + (muted?.hashCode() ?: 0) - result = 31 * result + (poll?.hashCode() ?: 0) - result = 31 * result + (card?.hashCode() ?: 0) - return result - } - data class Mention( val id: String, val url: String, diff --git a/app/src/main/java/com/keylesspalace/tusky/json/SpannedTypeAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/json/SpannedTypeAdapter.kt deleted file mode 100644 index 60af61342..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/json/SpannedTypeAdapter.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* Copyright 2020 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.json - -import android.text.Spanned -import android.text.SpannedString -import androidx.core.text.HtmlCompat -import androidx.core.text.parseAsHtml -import androidx.core.text.toHtml -import com.google.gson.JsonDeserializationContext -import com.google.gson.JsonDeserializer -import com.google.gson.JsonElement -import com.google.gson.JsonParseException -import com.google.gson.JsonPrimitive -import com.google.gson.JsonSerializationContext -import com.google.gson.JsonSerializer -import com.keylesspalace.tusky.util.trimTrailingWhitespace -import java.lang.reflect.Type - -class SpannedTypeAdapter : JsonDeserializer, JsonSerializer { - @Throws(JsonParseException::class) - override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Spanned { - return json.asString - /* Mastodon uses 'white-space: pre-wrap;' so spaces are displayed as returned by the Api. - * We can't use CSS so we replace spaces with non-breaking-spaces to emulate the behavior. - */ - ?.replace("
", "
 ") - ?.replace("
", "
 ") - ?.replace("
", "
 ") - ?.replace(" ", "  ") - ?.parseAsHtml() - /* Html.fromHtml returns trailing whitespace if the html ends in a

tag, which - * most status contents do, so it should be trimmed. */ - ?.trimTrailingWhitespace() - ?: SpannedString("") - } - - override fun serialize(src: Spanned?, typeOfSrc: Type, context: JsonSerializationContext): JsonElement { - return JsonPrimitive(src!!.toHtml(HtmlCompat.TO_HTML_PARAGRAPH_LINES_INDIVIDUAL)) - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/StatusParsingHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/StatusParsingHelper.kt new file mode 100644 index 000000000..fc62c78d6 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/StatusParsingHelper.kt @@ -0,0 +1,62 @@ +/* Copyright 2022 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.util + +import android.text.SpannableStringBuilder +import android.text.Spanned +import androidx.core.text.parseAsHtml + +/** + * parse a String containing html from the Mastodon api to Spanned + */ +fun String.parseAsMastodonHtml(): Spanned { + return this.replace("
", "
 ") + .replace("
", "
 ") + .replace("
", "
 ") + .replace(" ", "  ") + .parseAsHtml() + /* Html.fromHtml returns trailing whitespace if the html ends in a

tag, which + * most status contents do, so it should be trimmed. */ + .trimTrailingWhitespace() +} + +fun replaceCrashingCharacters(content: Spanned): Spanned { + return replaceCrashingCharacters(content as CharSequence) as Spanned +} + +fun replaceCrashingCharacters(content: CharSequence): CharSequence? { + var replacing = false + var builder: SpannableStringBuilder? = null + val length = content.length + for (index in 0 until length) { + val character = content[index] + + // If there are more than one or two, switch to a map + if (character == SOFT_HYPHEN) { + if (!replacing) { + replacing = true + builder = SpannableStringBuilder(content, 0, index) + } + builder!!.append(ASCII_HYPHEN) + } else if (replacing) { + builder!!.append(character) + } + } + return if (replacing) builder else content +} + +private const val SOFT_HYPHEN = '\u00ad' +private const val ASCII_HYPHEN = '-' 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 52d9713f4..fef9c0bb8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.kt @@ -27,12 +27,9 @@ fun Status.toViewData( isExpanded: Boolean, isCollapsed: Boolean ): StatusViewData.Concrete { - val visibleStatus = this.reblog ?: this - return StatusViewData.Concrete( status = this, isShowingContent = isShowingContent, - isCollapsible = shouldTrimStatus(visibleStatus.content), isCollapsed = isCollapsed, isExpanded = isExpanded, ) diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt index d8f271578..8ac212d90 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt @@ -15,9 +15,11 @@ package com.keylesspalace.tusky.viewdata import android.os.Build -import android.text.SpannableStringBuilder import android.text.Spanned import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.util.parseAsMastodonHtml +import com.keylesspalace.tusky.util.replaceCrashingCharacters +import com.keylesspalace.tusky.util.shouldTrimStatus /** * Created by charlag on 11/07/2017. @@ -32,13 +34,6 @@ sealed class StatusViewData { val status: Status, val isExpanded: Boolean, val isShowingContent: Boolean, - /** - * Specifies whether the content of this post is allowed to be collapsed or if it should show - * all content regardless. - * - * @return Whether the post is collapsible or never collapsed. - */ - val isCollapsible: Boolean, /** * Specifies whether the content of this post is currently limited in visibility to the first * 500 characters or not. @@ -51,6 +46,14 @@ sealed class StatusViewData { override val id: String get() = status.id + /** + * Specifies whether the content of this post is allowed to be collapsed or if it should show + * all content regardless. + * + * @return Whether the post is collapsible or never collapsed. + */ + val isCollapsible: Boolean + val content: Spanned val spoilerText: String val username: String @@ -74,45 +77,17 @@ sealed class StatusViewData { init { if (Build.VERSION.SDK_INT == 23) { // https://github.com/tuskyapp/Tusky/issues/563 - this.content = replaceCrashingCharacters(status.actionableStatus.content) + this.content = replaceCrashingCharacters(status.actionableStatus.content.parseAsMastodonHtml()) this.spoilerText = replaceCrashingCharacters(status.actionableStatus.spoilerText).toString() this.username = replaceCrashingCharacters(status.actionableStatus.account.username).toString() } else { - this.content = status.actionableStatus.content + this.content = status.actionableStatus.content.parseAsMastodonHtml() this.spoilerText = status.actionableStatus.spoilerText this.username = status.actionableStatus.account.username } - } - - companion object { - private const val SOFT_HYPHEN = '\u00ad' - private const val ASCII_HYPHEN = '-' - fun replaceCrashingCharacters(content: Spanned): Spanned { - return replaceCrashingCharacters(content as CharSequence) as Spanned - } - - fun replaceCrashingCharacters(content: CharSequence): CharSequence? { - var replacing = false - var builder: SpannableStringBuilder? = null - val length = content.length - for (index in 0 until length) { - val character = content[index] - - // If there are more than one or two, switch to a map - if (character == SOFT_HYPHEN) { - if (!replacing) { - replacing = true - builder = SpannableStringBuilder(content, 0, index) - } - builder!!.append(ASCII_HYPHEN) - } else if (replacing) { - builder!!.append(character) - } - } - return if (replacing) builder else content - } + this.isCollapsible = shouldTrimStatus(this.content) } /** Helper for Java */ diff --git a/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt index ff2088231..beb6af9b4 100644 --- a/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt @@ -15,7 +15,6 @@ package com.keylesspalace.tusky -import android.text.SpannedString import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.keylesspalace.tusky.entity.SearchResult import com.keylesspalace.tusky.entity.Status @@ -70,7 +69,7 @@ class BottomSheetActivityTest { inReplyToId = null, inReplyToAccountId = null, reblog = null, - content = SpannedString("omgwat"), + content = "omgwat", createdAt = Date(), emojis = emptyList(), reblogsCount = 0, diff --git a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt index dc4a412ff..5396a21ec 100644 --- a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt @@ -17,7 +17,6 @@ package com.keylesspalace.tusky import android.content.Intent import android.os.Looper.getMainLooper -import android.text.SpannedString import android.widget.EditText import androidx.test.ext.junit.runners.AndroidJUnit4 import com.keylesspalace.tusky.components.compose.ComposeActivity @@ -469,7 +468,7 @@ class ComposeActivityTest { "admin", "admin", "admin", - SpannedString(""), + "", "https://example.token", "", "", diff --git a/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt b/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt index d50639431..91ea38d3b 100644 --- a/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt @@ -1,6 +1,5 @@ package com.keylesspalace.tusky -import android.text.SpannedString import androidx.test.ext.junit.runners.AndroidJUnit4 import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Filter @@ -162,7 +161,7 @@ class FilterTest { inReplyToId = null, inReplyToAccountId = null, reblog = null, - content = SpannedString(content), + content = content, createdAt = Date(), emojis = emptyList(), reblogsCount = 0, diff --git a/app/src/test/java/com/keylesspalace/tusky/StatusComparisonTest.kt b/app/src/test/java/com/keylesspalace/tusky/StatusComparisonTest.kt index ed06e27c6..3086036a0 100644 --- a/app/src/test/java/com/keylesspalace/tusky/StatusComparisonTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/StatusComparisonTest.kt @@ -1,10 +1,8 @@ package com.keylesspalace.tusky -import android.text.Spanned import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.google.gson.GsonBuilder +import com.google.gson.Gson import com.keylesspalace.tusky.entity.Status -import com.keylesspalace.tusky.json.SpannedTypeAdapter import com.keylesspalace.tusky.viewdata.StatusViewData import org.junit.Assert.assertEquals import org.junit.Assert.assertNotEquals @@ -39,9 +37,7 @@ class StatusComparisonTest { assertEquals(createStatus(note = "Test"), createStatus(note = "Test 123456")) } - private val gson = GsonBuilder().registerTypeAdapter( - Spanned::class.java, SpannedTypeAdapter() - ).create() + private val gson = Gson() @Test fun `two equal status view data - should be equal`() { @@ -49,14 +45,12 @@ class StatusComparisonTest { status = createStatus(), isExpanded = false, isShowingContent = false, - isCollapsible = false, isCollapsed = false ) val viewdata2 = StatusViewData.Concrete( status = createStatus(), isExpanded = false, isShowingContent = false, - isCollapsible = false, isCollapsed = false ) assertEquals(viewdata1, viewdata2) @@ -68,14 +62,12 @@ class StatusComparisonTest { status = createStatus(), isExpanded = true, isShowingContent = false, - isCollapsible = false, isCollapsed = false ) val viewdata2 = StatusViewData.Concrete( status = createStatus(), isExpanded = false, isShowingContent = false, - isCollapsible = false, isCollapsed = false ) assertNotEquals(viewdata1, viewdata2) @@ -87,14 +79,12 @@ class StatusComparisonTest { status = createStatus(content = "whatever"), isExpanded = true, isShowingContent = false, - isCollapsible = false, isCollapsed = false ) val viewdata2 = StatusViewData.Concrete( status = createStatus(), isExpanded = false, isShowingContent = false, - isCollapsible = false, isCollapsed = false ) assertNotEquals(viewdata1, viewdata2) diff --git a/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelinePagingSourceTest.kt b/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelinePagingSourceTest.kt index 60dda4193..33215e675 100644 --- a/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelinePagingSourceTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelinePagingSourceTest.kt @@ -1,14 +1,19 @@ package com.keylesspalace.tusky.components.timeline import androidx.paging.PagingSource +import androidx.test.ext.junit.runners.AndroidJUnit4 import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelinePagingSource import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelineViewModel import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Test +import org.junit.runner.RunWith import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock +import org.robolectric.annotation.Config +@Config(sdk = [28]) +@RunWith(AndroidJUnit4::class) class NetworkTimelinePagingSourceTest { private val status = mockStatusViewData() diff --git a/app/src/test/java/com/keylesspalace/tusky/components/timeline/StatusMocker.kt b/app/src/test/java/com/keylesspalace/tusky/components/timeline/StatusMocker.kt index f7c998b51..cc6a90bd9 100644 --- a/app/src/test/java/com/keylesspalace/tusky/components/timeline/StatusMocker.kt +++ b/app/src/test/java/com/keylesspalace/tusky/components/timeline/StatusMocker.kt @@ -1,6 +1,5 @@ package com.keylesspalace.tusky.components.timeline -import android.text.SpannedString import com.google.gson.Gson import com.keylesspalace.tusky.db.TimelineStatusWithAccount import com.keylesspalace.tusky.entity.Status @@ -25,7 +24,7 @@ fun mockStatus(id: String = "100") = Status( inReplyToId = null, inReplyToAccountId = null, reblog = null, - content = SpannedString("Test"), + content = "Test", createdAt = fixedDate, emojis = emptyList(), reblogsCount = 1, @@ -50,7 +49,6 @@ fun mockStatusViewData(id: String = "100") = StatusViewData.Concrete( status = mockStatus(id), isExpanded = false, isShowingContent = false, - isCollapsible = false, isCollapsed = true, ) From 027b659d1c0ecfcfa53607fffdd8fba0e98cbae6 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sat, 16 Apr 2022 09:44:05 +0200 Subject: [PATCH 19/21] fix notifications showing unparsed html (#2436) --- .../tusky/components/notifications/NotificationHelper.java | 7 ++++--- .../com/keylesspalace/tusky/util/StatusParsingHelper.kt | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) 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 63c170822..83682ab28 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 @@ -16,6 +16,7 @@ package com.keylesspalace.tusky.components.notifications; +import static com.keylesspalace.tusky.util.StatusParsingHelper.parseAsMastodonHtml; import static com.keylesspalace.tusky.viewdata.PollViewDataKt.buildDescription; import android.app.NotificationChannel; @@ -341,7 +342,7 @@ public class NotificationHelper { Status status = body.getStatus(); String citedLocalAuthor = status.getAccount().getLocalUsername(); - String citedText = status.getContent().toString(); + String citedText = parseAsMastodonHtml(status.getContent()).toString(); String inReplyToId = status.getId(); Status actionableStatus = status.getActionableStatus(); Status.Visibility replyVisibility = actionableStatus.getVisibility(); @@ -690,13 +691,13 @@ public class NotificationHelper { if (!TextUtils.isEmpty(notification.getStatus().getSpoilerText())) { return notification.getStatus().getSpoilerText(); } else { - return notification.getStatus().getContent().toString(); + return parseAsMastodonHtml(notification.getStatus().getContent()).toString(); } case POLL: if (!TextUtils.isEmpty(notification.getStatus().getSpoilerText())) { return notification.getStatus().getSpoilerText(); } else { - StringBuilder builder = new StringBuilder(notification.getStatus().getContent()); + StringBuilder builder = new StringBuilder(parseAsMastodonHtml(notification.getStatus().getContent())); builder.append('\n'); Poll poll = notification.getStatus().getPoll(); List options = poll.getOptions(); diff --git a/app/src/main/java/com/keylesspalace/tusky/util/StatusParsingHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/StatusParsingHelper.kt index fc62c78d6..2ac4782c3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/StatusParsingHelper.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/StatusParsingHelper.kt @@ -13,6 +13,7 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ +@file:JvmName("StatusParsingHelper") package com.keylesspalace.tusky.util import android.text.SpannableStringBuilder From f2fc87a79ee7bb22693b24f54ef05a5244ca0f58 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sat, 16 Apr 2022 09:44:37 +0200 Subject: [PATCH 20/21] upgrade Kotlin and Coroutines (#2434) --- app/build.gradle | 4 +--- build.gradle | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 3c5cb1624..6ed56f42c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -88,7 +88,7 @@ android { } } -ext.coroutinesVersion = "1.6.0" +ext.coroutinesVersion = "1.6.1" ext.lifecycleVersion = "2.4.1" ext.roomVersion = '2.4.2' ext.retrofitVersion = '2.9.0' @@ -99,8 +99,6 @@ ext.materialdrawerVersion = '8.4.5' // if libraries are changed here, they should also be changed in LicenseActivity dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx3:$coroutinesVersion" diff --git a/build.gradle b/build.gradle index c93117011..ace9a1179 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,4 @@ buildscript { - ext.kotlin_version = '1.6.10' repositories { google() mavenCentral() @@ -7,7 +6,7 @@ buildscript { } dependencies { classpath "com.android.tools.build:gradle:7.1.2" - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.20" classpath "org.jlleitschuh.gradle:ktlint-gradle:10.1.0" } } From 216f094e983018c27689835e277bd2611ab2bf9d Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sat, 16 Apr 2022 09:45:45 +0200 Subject: [PATCH 21/21] upgrade ktlint gradle plugin to 10.2.1 (#2435) --- .../main/java/com/keylesspalace/tusky/ViewMediaActivity.kt | 1 - build.gradle | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt index 64d29577b..fda2c82b6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt @@ -283,7 +283,6 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener } return@fromCallable false } - .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doOnDispose { diff --git a/build.gradle b/build.gradle index ace9a1179..3a5251fa0 100644 --- a/build.gradle +++ b/build.gradle @@ -7,11 +7,11 @@ buildscript { dependencies { classpath "com.android.tools.build:gradle:7.1.2" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.20" - classpath "org.jlleitschuh.gradle:ktlint-gradle:10.1.0" + classpath "org.jlleitschuh.gradle:ktlint-gradle:10.2.1" } } plugins { - id "org.jlleitschuh.gradle.ktlint" version "10.1.0" + id "org.jlleitschuh.gradle.ktlint" version "10.2.1" } allprojects {