diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 3a53d4b7f3..2943ed544f 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -100,4 +100,4 @@ jobs: sed -i -e '/START Non-FOSS component/,/END Non-FOSS component/d' app/build.gradle.kts - name: Build F-Droid variant - run: ./gradlew :app:assembleRelease :app:check :app:lint --stacktrace + run: ./gradlew :app:assembleRelease :app:check :app:lint --stacktrace \ No newline at end of file diff --git a/app/src/main/java/dev/dimension/flare/MainActivity.kt b/app/src/main/java/dev/dimension/flare/MainActivity.kt index 45c0781f8f..e0d1bc2975 100644 --- a/app/src/main/java/dev/dimension/flare/MainActivity.kt +++ b/app/src/main/java/dev/dimension/flare/MainActivity.kt @@ -1,6 +1,7 @@ package dev.dimension.flare import android.content.Context +import android.content.Intent import android.net.ConnectivityManager import android.net.Network import android.net.NetworkCapabilities @@ -52,6 +53,11 @@ class MainActivity : ComponentActivity() { } } + override fun onNewIntent(intent: Intent) { + setIntent(intent) + super.onNewIntent(intent) + } + override fun onResume() { super.onResume() videoDownloadHelper.onResume() diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/compose/ShortcutComposeActivity.kt b/app/src/main/java/dev/dimension/flare/ui/screen/compose/ShortcutComposeActivity.kt index edd42460e4..38db297c90 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/compose/ShortcutComposeActivity.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/compose/ShortcutComposeActivity.kt @@ -5,13 +5,17 @@ import android.net.Uri import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.compose.ui.ComposeUiFlags +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.core.content.IntentCompat import dev.dimension.flare.ui.FlareApp import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList class ShortcutComposeActivity : ComponentActivity() { + @OptIn(ExperimentalComposeUiApi::class) override fun onCreate(savedInstanceState: Bundle?) { + ComposeUiFlags.isMediaQueryIntegrationEnabled = true super.onCreate(savedInstanceState) val initialText = when { diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/profile/ProfileScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/profile/ProfileScreen.kt index 188f66b543..8a5400bd29 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/profile/ProfileScreen.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/profile/ProfileScreen.kt @@ -27,7 +27,6 @@ import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Card import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ScaffoldDefaults @@ -57,13 +56,13 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.toSize import dev.dimension.flare.R import dev.dimension.flare.common.PagingState -import dev.dimension.flare.common.onLoading import dev.dimension.flare.common.onSuccess import dev.dimension.flare.common.refreshSuspend import dev.dimension.flare.data.datasource.microblog.ProfileTab import dev.dimension.flare.data.model.VideoAutoplay import dev.dimension.flare.model.AccountType import dev.dimension.flare.model.MicroBlogKey +import dev.dimension.flare.ui.common.items import dev.dimension.flare.ui.component.BackButton import dev.dimension.flare.ui.component.FlareScaffold import dev.dimension.flare.ui.component.FlareTopAppBar @@ -546,56 +545,45 @@ private fun ProfileMediaTab( horizontalArrangement = Arrangement.spacedBy(8.dp), contentPadding = PaddingValues(vertical = 8.dp, horizontal = screenHorizontalPadding), ) { - mediaState - .onSuccess { - items(itemCount) { index -> - val item = get(index) - if (item != null) { - val media = item.media - MediaItem( - media = media, - showCountdown = false, - modifier = - Modifier - .clip(MaterialTheme.shapes.medium) - .clipToBounds() - .clickable { - val content = item.status - if (content is UiTimelineV2.Post) { - onItemClicked( - item.statusKey, - item.index, - when (media) { - is UiMedia.Image -> media.previewUrl - is UiMedia.Video -> media.thumbnailUrl - is UiMedia.Gif -> media.previewUrl - else -> null - }, - ) - } + items( + mediaState, + key = { + it.key + }, + loadingContent = { + Box( + modifier = + Modifier + .size(120.dp) + .placeholder(true), + ) + }, + ) { item -> + val media = item.media + MediaItem( + media = media, + showCountdown = false, + modifier = + Modifier + .clip(MaterialTheme.shapes.medium) + .clipToBounds() + .clickable { + val content = item.status + if (content is UiTimelineV2.Post) { + onItemClicked( + item.statusKey, + item.index, + when (media) { + is UiMedia.Image -> media.previewUrl + is UiMedia.Video -> media.thumbnailUrl + is UiMedia.Gif -> media.previewUrl + else -> null }, - ) - } else { - Card { - Box( - modifier = - Modifier - .size(120.dp) - .placeholder(true), - ) - } - } - } - }.onLoading { - items(10) { - Box( - modifier = - Modifier - .size(120.dp) - .placeholder(true), - ) - } - } + ) + } + }, + ) + } } } } diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/rss/RssDetailScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/rss/RssDetailScreen.kt index aaf42eea5c..21119139d4 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/rss/RssDetailScreen.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/rss/RssDetailScreen.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.widthIn @@ -92,6 +93,7 @@ internal fun RssDetailScreen( val context = LocalContext.current FlareScaffold( modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + containerColor = MaterialTheme.colorScheme.surface, topBar = { FlareTopAppBar( title = {}, @@ -99,6 +101,12 @@ internal fun RssDetailScreen( BackButton(onBack) }, scrollBehavior = scrollBehavior, + colors = + TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + scrolledContainerColor = MaterialTheme.colorScheme.surface, + actionIconContentColor = MaterialTheme.colorScheme.primary, + ), actions = { IconButton( onClick = { @@ -165,7 +173,7 @@ internal fun RssDetailScreen( Column( modifier = Modifier - .fillMaxWidth() + .fillMaxSize() .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally, ) { diff --git a/app/src/main/res/values-af-rZA/changelog.xml b/app/src/main/res/values-af-rZA/changelog.xml index 3ea04e700d..88fec61a9b 100644 --- a/app/src/main/res/values-af-rZA/changelog.xml +++ b/app/src/main/res/values-af-rZA/changelog.xml @@ -1,2 +1,14 @@ - + + + Weergawe %1$s: + + ]]> + + diff --git a/app/src/main/res/values-ar-rSA/changelog.xml b/app/src/main/res/values-ar-rSA/changelog.xml index dcfc43a6a4..6c55c68c7c 100644 --- a/app/src/main/res/values-ar-rSA/changelog.xml +++ b/app/src/main/res/values-ar-rSA/changelog.xml @@ -2,12 +2,15 @@ مرحبا بعودتك! لقد قمنا ببعض التغييرات منذ آخر تسجيل دخولك. وفيما يلي التفاصيل: - الإصدار %1$s: + + الإصدار %1$s:
    -
  • إضافة أوضاع جديدة لعرض الخط الزمني، بما في ذلك تصميم المعرض.
  • -
  • تحسين قراءة RSS ، المشاركة والترجمة والموجزات.
  • -
  • إعادة تنظيم إعدادات الظهور لتخصيص أسهل.
  • -
  • إصلاحات الشوائب وتحسينات الأداء.
  • +
  • تمت إضافة وضع Deck لتجربة خط زمني متعددة الأعمدة.
  • +
  • تمت إضافة تخصيص أوسع لتبويبات الخط الزمني، بما في ذلك الأيقونات والمرشحات والمجموعات والمظهر لكل تبويب.
  • +
  • تمت إضافة دعم regex لمرشحات الكلمات المفتاحية المحلية.
  • +
  • تحسينات في الأداء وإصلاحات للأخطاء.
- ]]>
+ ]]> +
diff --git a/app/src/main/res/values-bg-rBG/changelog.xml b/app/src/main/res/values-bg-rBG/changelog.xml index e23d00af21..9d0d8a0a53 100644 --- a/app/src/main/res/values-bg-rBG/changelog.xml +++ b/app/src/main/res/values-bg-rBG/changelog.xml @@ -1,4 +1,15 @@ Добре дошли отново! + + Версия %1$s: + + ]]> + diff --git a/app/src/main/res/values-ca-rES/changelog.xml b/app/src/main/res/values-ca-rES/changelog.xml index 3ea04e700d..4a7045641b 100644 --- a/app/src/main/res/values-ca-rES/changelog.xml +++ b/app/src/main/res/values-ca-rES/changelog.xml @@ -1,2 +1,14 @@ - + + + Versió %1$s: + + ]]> + + diff --git a/app/src/main/res/values-cs-rCZ/changelog.xml b/app/src/main/res/values-cs-rCZ/changelog.xml index 77278104a3..c2e925e369 100644 --- a/app/src/main/res/values-cs-rCZ/changelog.xml +++ b/app/src/main/res/values-cs-rCZ/changelog.xml @@ -2,4 +2,15 @@ Vítejte zpět! Od posledního přihlášení jsme provedli nějaké změny. Zde jsou detaily: + + Verze %1$s: + + ]]> + diff --git a/app/src/main/res/values-da-rDK/changelog.xml b/app/src/main/res/values-da-rDK/changelog.xml index e697a7192c..e989fd9dc2 100644 --- a/app/src/main/res/values-da-rDK/changelog.xml +++ b/app/src/main/res/values-da-rDK/changelog.xml @@ -2,12 +2,15 @@ Velkommen tilbage! Vi har foretaget nogle ændringer siden du sidst er logget ind. Her er detaljerne: - Version %1$s: + + Version %1$s:
    -
  • Tilføjet nye tidslinje visningstilstande, herunder et Galleri layout.
  • -
  • Forbedret RSS-læsning, deling, oversættelse og resuméer.
  • -
  • Omorganiseret udseende indstillinger for lettere tilpasning.
  • -
  • Fejlrettelser og forbedringer af ydeevnen.
  • +
  • Tilføjet Deck Mode til en tidslinjeoplevelse med flere kolonner.
  • +
  • Tilføjet mere avanceret tilpasning af tidslinjefaner, herunder ikoner, filtre, grupper og udseende pr. fane.
  • +
  • Tilføjet regex-understøttelse for lokale nøgleordsfiltre.
  • +
  • Ydelsesforbedringer og fejlrettelser.
- ]]>
+ ]]> +
diff --git a/app/src/main/res/values-de-rDE/changelog.xml b/app/src/main/res/values-de-rDE/changelog.xml index b2c27c89ee..588238d2ec 100644 --- a/app/src/main/res/values-de-rDE/changelog.xml +++ b/app/src/main/res/values-de-rDE/changelog.xml @@ -2,13 +2,15 @@ Willkommen zurück! Seit der letzten Anmeldung haben wir einige Änderungen vorgenommen. Hier sind die Details: - Version %1$s: + + Version %1$s:
    -
  • Neue Timeline-Anzeigemodi hinzugefügt inklusive Galerie-Layout.
  • - -
  • Verbessertes RSS-Lesen teilen, übersetzen und zusammenfassen.
  • -
  • Umorganisierte Darstellungseinstellungen für einfachere Anpassung.
  • -
  • Fehlerbehebungen und Leistungsverbesserungen.
  • +
  • Deck Mode für eine mehrspaltige Timeline-Ansicht hinzugefügt.
  • +
  • Erweiterte Anpassung von Timeline-Tabs hinzugefügt, einschließlich Symbolen, Filtern, Gruppen und Erscheinungsbild pro Tab.
  • +
  • Regex-Unterstützung für lokale Schlüsselwortfilter hinzugefügt.
  • +
  • Leistungsverbesserungen und Fehlerbehebungen.
- ]]>
+ ]]> +
diff --git a/app/src/main/res/values-el-rGR/changelog.xml b/app/src/main/res/values-el-rGR/changelog.xml index a056ab27b4..b8a7b54734 100644 --- a/app/src/main/res/values-el-rGR/changelog.xml +++ b/app/src/main/res/values-el-rGR/changelog.xml @@ -2,12 +2,15 @@ Καλώς ήρθατε! Έχουμε κάνει κάποιες αλλαγές από τότε που συνδεθήκατε την τελευταία φορά. Εδώ είναι οι λεπτομέρειες: - Έκδοση %1$s: + + Έκδοση %1$s:
    -
  • Προστέθηκε νέα λειτουργία εμφάνισης χρονοδιαγράμματος, συμπεριλαμβανομένης μιας διάταξης συλλογής.
  • -
  • Βελτιωμένη ανάγνωση RSS, από κοινού, μετάφραση και περιλήψεις.
  • -
  • Επανοργανωμένες ρυθμίσεις εμφάνισης για ευκολότερη προσαρμογή.
  • -
  • Διορθώσεις σφαλμάτων και βελτιώσεις απόδοσης.
  • +
  • Προστέθηκε το Deck Mode για εμπειρία χρονολογίου πολλών στηλών.
  • +
  • Προστέθηκε πιο πλούσια προσαρμογή καρτελών χρονολογίου, με εικονίδια, φίλτρα, ομάδες και εμφάνιση ανά καρτέλα.
  • +
  • Προστέθηκε υποστήριξη regex για τοπικά φίλτρα λέξεων-κλειδιών.
  • +
  • Βελτιώσεις απόδοσης και διορθώσεις σφαλμάτων.
- ]]>
+ ]]> +
diff --git a/app/src/main/res/values-es-rES/changelog.xml b/app/src/main/res/values-es-rES/changelog.xml index 5b084cf4bf..759917e2a5 100644 --- a/app/src/main/res/values-es-rES/changelog.xml +++ b/app/src/main/res/values-es-rES/changelog.xml @@ -2,12 +2,15 @@ ¡Bienvenido de nuevo! Hemos realizado algunos cambios desde la última vez que iniciaste sesión. Estos son los detalles: - Versión %1$s: + + Versión %1$s:
    -
  • Se añadieron nuevos modos de visualización de timeline, incluyendo un diseño de galería.
  • -
  • Lectura RSS mejorada, compartir, traducir y resumir.
  • -
  • Ajustes de apariencia reorganizados para una personalización más fácil.
  • -
  • correcciones de errores y mejoras de rendimiento.
  • +
  • Se añadió Deck Mode para una experiencia de línea de tiempo con varias columnas.
  • +
  • Se añadió una personalización más completa de las pestañas de la línea de tiempo, incluyendo iconos, filtros, grupos y apariencia por pestaña.
  • +
  • Se añadió compatibilidad con regex para los filtros locales de palabras clave.
  • +
  • Mejoras de rendimiento y correcciones de errores.
- ]]>
+ ]]> +
diff --git a/app/src/main/res/values-fi-rFI/changelog.xml b/app/src/main/res/values-fi-rFI/changelog.xml index 13a7360670..c7962a1880 100644 --- a/app/src/main/res/values-fi-rFI/changelog.xml +++ b/app/src/main/res/values-fi-rFI/changelog.xml @@ -2,12 +2,15 @@ Tervetuloa takaisin! Olemme tehneet joitakin muutoksia, koska olet viimeksi kirjautunut sisään. Tässä on yksityiskohdat: - Versio %1$s: + + Versio %1$s:
    -
  • Lisätty uusia aikajanan näyttötiloja, mukaan lukien gallerian asettelu.
  • -
  • Parannettu RSS-lukema, jakaminen, kääntäminen ja yhteenvedot.
  • -
  • Järjestä uudelleen ulkoasun asetukset helpommin muokattavaksi.
  • -
  • Virheenkorjauksia ja suorituskyvyn parannuksia.
  • +
  • Lisätty Deck Mode usean sarakkeen aikajanakokemusta varten.
  • +
  • Lisätty monipuolisempi aikajanavälilehtien mukautus, mukaan lukien kuvakkeet, suodattimet, ryhmät ja välilehtikohtainen ulkoasu.
  • +
  • Lisätty regex-tuki paikallisille avainsanasuodattimille.
  • +
  • Suorituskykyparannuksia ja virheenkorjauksia.
- ]]>
+ ]]> +
diff --git a/app/src/main/res/values-hu-rHU/changelog.xml b/app/src/main/res/values-hu-rHU/changelog.xml index 3ea04e700d..1a5e6fa23e 100644 --- a/app/src/main/res/values-hu-rHU/changelog.xml +++ b/app/src/main/res/values-hu-rHU/changelog.xml @@ -1,2 +1,14 @@ - + + + %1$s verzió: + + ]]> + + diff --git a/app/src/main/res/values-iw-rIL/changelog.xml b/app/src/main/res/values-iw-rIL/changelog.xml index 3ea04e700d..718e5891d8 100644 --- a/app/src/main/res/values-iw-rIL/changelog.xml +++ b/app/src/main/res/values-iw-rIL/changelog.xml @@ -1,2 +1,14 @@ - + + + גרסה %1$s: + + ]]> + + diff --git a/app/src/main/res/values-ja-rJP/changelog.xml b/app/src/main/res/values-ja-rJP/changelog.xml index 4f3110cb19..420762016b 100644 --- a/app/src/main/res/values-ja-rJP/changelog.xml +++ b/app/src/main/res/values-ja-rJP/changelog.xml @@ -2,4 +2,15 @@ おかえりなさい! 最後にログインしてからいくつかの変更がありました。詳細は次のとおりです: + + バージョン %1$s: + + ]]> + diff --git a/app/src/main/res/values-ko-rKR/changelog.xml b/app/src/main/res/values-ko-rKR/changelog.xml index 3ea04e700d..f2cd40c5a1 100644 --- a/app/src/main/res/values-ko-rKR/changelog.xml +++ b/app/src/main/res/values-ko-rKR/changelog.xml @@ -1,2 +1,14 @@ - + + + 버전 %1$s: + + ]]> + + diff --git a/app/src/main/res/values-nl-rNL/changelog.xml b/app/src/main/res/values-nl-rNL/changelog.xml index cf3f69074d..e5336b15ec 100644 --- a/app/src/main/res/values-nl-rNL/changelog.xml +++ b/app/src/main/res/values-nl-rNL/changelog.xml @@ -2,12 +2,15 @@ Welkom terug! We hebben enkele wijzigingen aangebracht sinds de laatste keer dat u bent ingelogd. Hier zijn de details: - Versie %1$s: + + Versie %1$s:
    -
  • heeft nieuwe tijdlijnweergave modi toegevoegd, inclusief een galerij lay-out.
  • -
  • verbeterd RSS-lezen. delen, vertalen en samenvattingen.
  • -
  • Gereorganiseerd uiterlijk-instellingen voor makkelijker aanpassingen.
  • -
  • bugfixes en prestatieverbeteringen.
  • +
  • Deck Mode toegevoegd voor een tijdlijnervaring met meerdere kolommen.
  • +
  • Uitgebreidere aanpassing van tijdlijntabbladen toegevoegd, inclusief pictogrammen, filters, groepen en uiterlijk per tabblad.
  • +
  • Regex-ondersteuning toegevoegd voor lokale trefwoordfilters.
  • +
  • Prestatieverbeteringen en bugfixes.
- ]]>
+ ]]> +
diff --git a/app/src/main/res/values-no-rNO/changelog.xml b/app/src/main/res/values-no-rNO/changelog.xml index ac64d1c716..794653cedd 100644 --- a/app/src/main/res/values-no-rNO/changelog.xml +++ b/app/src/main/res/values-no-rNO/changelog.xml @@ -2,12 +2,15 @@ Velkommen tilbake! Vi har gjort noen endringer siden sist du logget inn. Her er detaljer: - Versjon %1$s: + + Versjon %1$s:
    -
  • La til nye tidslinjevisningsmodus, inkludert et galleri layout.
  • -
  • Forbedret RSS-avlesning, deling, oversettelse, og oppsummeringer.
  • -
  • Reorganisert Innstilling for enklere tilpasning.
  • -
  • Feilrettinger og ytelsesforbedringer.
  • +
  • Lagt til Deck Mode for en tidslinjeopplevelse med flere kolonner.
  • +
  • Lagt til rikere tilpasning av tidslinjefaner, inkludert ikoner, filtre, grupper og utseende per fane.
  • +
  • Lagt til regex-støtte for lokale nøkkelordfiltre.
  • +
  • Ytelsesforbedringer og feilrettinger.
- ]]>
+ ]]> +
diff --git a/app/src/main/res/values-pl-rPL/changelog.xml b/app/src/main/res/values-pl-rPL/changelog.xml index e93bad2b81..638c1e5cfe 100644 --- a/app/src/main/res/values-pl-rPL/changelog.xml +++ b/app/src/main/res/values-pl-rPL/changelog.xml @@ -2,12 +2,15 @@ Witaj ponownie! Wprowadziliśmy pewne zmiany od ostatniego logowania. Oto szczegóły: - Wersja %1$s: + + Wersja %1$s:
    -
  • Dodano nowe tryby wyświetlania osi czasu, łącznie z układem galerii.
  • -
  • Ulepszone czytanie RSS, udostępnianie, tłumaczenie i streszczenia.
  • -
  • Przeorganizowane ustawienia wyglądu dla łatwiejszego dostosowywania.
  • -
  • Poprawki błędów i ulepszenia wydajności.
  • +
  • Dodano Deck Mode dla wielokolumnowego widoku osi czasu.
  • +
  • Dodano bogatsze dostosowywanie kart osi czasu, w tym ikony, filtry, grupy i wygląd każdej karty.
  • +
  • Dodano obsługę regex dla lokalnych filtrów słów kluczowych.
  • +
  • Ulepszenia wydajności i poprawki błędów.
- ]]>
+ ]]> +
diff --git a/app/src/main/res/values-pt-rBR/changelog.xml b/app/src/main/res/values-pt-rBR/changelog.xml index 777fac9ca7..d135806a24 100644 --- a/app/src/main/res/values-pt-rBR/changelog.xml +++ b/app/src/main/res/values-pt-rBR/changelog.xml @@ -2,4 +2,15 @@ Bem-vindo de volta! Fizemos algumas alterações desde a última vez que você entrou. Aqui estão os detalhes: + + Versão %1$s: + + ]]> + diff --git a/app/src/main/res/values-pt-rPT/changelog.xml b/app/src/main/res/values-pt-rPT/changelog.xml index 777fac9ca7..c91c49cdd9 100644 --- a/app/src/main/res/values-pt-rPT/changelog.xml +++ b/app/src/main/res/values-pt-rPT/changelog.xml @@ -2,4 +2,15 @@ Bem-vindo de volta! Fizemos algumas alterações desde a última vez que você entrou. Aqui estão os detalhes: + + Versão %1$s: + + ]]> + diff --git a/app/src/main/res/values-ro-rRO/changelog.xml b/app/src/main/res/values-ro-rRO/changelog.xml index 69b6f3b27c..27d05996c5 100644 --- a/app/src/main/res/values-ro-rRO/changelog.xml +++ b/app/src/main/res/values-ro-rRO/changelog.xml @@ -2,12 +2,15 @@ Bine ai revenit! Am făcut unele modificări de când v-ați autentificat ultima dată. Iată detaliile: - Versiunea %1$s: + + Versiunea %1$s:
    -
  • a adăugat noi moduri de afişare a cronologiei, incluzând un aspect de galerie.
  • -
  • S-a îmbunătățit citirea RSS, partajarea, traducerea și rezumatele.
  • -
  • Setări reorganizate de Appearance pentru o personalizare mai ușoară.
  • -
  • Remedierea erorilor şi îmbunătăţirea performanţei.
  • +
  • A fost adăugat Deck Mode pentru o cronologie pe mai multe coloane.
  • +
  • A fost adăugată personalizare mai bogată pentru filele cronologiei, inclusiv pictograme, filtre, grupuri și aspect per filă.
  • +
  • A fost adăugat suport regex pentru filtrele locale de cuvinte-cheie.
  • +
  • Îmbunătățiri de performanță și remedieri de erori.
- ]]>
+ ]]> +
diff --git a/app/src/main/res/values-ru-rRU/changelog.xml b/app/src/main/res/values-ru-rRU/changelog.xml index ffecb164ab..56cbe3ec12 100644 --- a/app/src/main/res/values-ru-rRU/changelog.xml +++ b/app/src/main/res/values-ru-rRU/changelog.xml @@ -2,12 +2,15 @@ С возвращением! Мы внесли некоторые изменения с момента последнего входа. Ниже приведены подробности: - Версия %1$s: + + Версия %1$s:
    -
  • Добавлены новые режимы отображения ленты времени, включая макет Галереи.
  • -
  • Улучшенное чтение RSS разделение, перевод и резюме.
  • -
  • Настройки внешнего вида для облегчения настройки.
  • -
  • Исправления ошибок и улучшения производительности.
  • +
  • Добавлен Deck Mode для ленты в несколько колонок.
  • +
  • Добавлена расширенная настройка вкладок ленты: значки, фильтры, группы и внешний вид для каждой вкладки.
  • +
  • Добавлена поддержка regex для локальных фильтров ключевых слов.
  • +
  • Улучшения производительности и исправления ошибок.
- ]]>
+ ]]> +
diff --git a/app/src/main/res/values-sr-rSP/changelog.xml b/app/src/main/res/values-sr-rSP/changelog.xml index 3ea04e700d..b6b3e17a58 100644 --- a/app/src/main/res/values-sr-rSP/changelog.xml +++ b/app/src/main/res/values-sr-rSP/changelog.xml @@ -1,2 +1,14 @@ - + + + Верзија %1$s: + + ]]> + + diff --git a/app/src/main/res/values-sv-rSE/changelog.xml b/app/src/main/res/values-sv-rSE/changelog.xml index b614fcf04c..7852f4919e 100644 --- a/app/src/main/res/values-sv-rSE/changelog.xml +++ b/app/src/main/res/values-sv-rSE/changelog.xml @@ -2,12 +2,15 @@ Välkommen tillbaka! Vi har gjort några ändringar sedan du senast loggade in. Här är detaljerna: - Version %1$s: + + Version %1$s:
    -
  • Lade till nya tidslinjevisningslägen, inklusive en Galleri layout.
  • -
  • Förbättrad RSS-läsning, dela, översättning och sammanfattningar.
  • -
  • Omorganiserade Utseendeinställningar för enklare anpassning.
  • -
  • Felrättelser och prestandaförbättringar.
  • +
  • Lade till Deck Mode för en tidslinjeupplevelse med flera kolumner.
  • +
  • Lade till rikare anpassning av tidslinjeflikar, inklusive ikoner, filter, grupper och utseende per flik.
  • +
  • Lade till regex-stöd för lokala nyckelordsfilter.
  • +
  • Prestandaförbättringar och buggfixar.
- ]]>
+ ]]> +
diff --git a/app/src/main/res/values-tr-rTR/changelog.xml b/app/src/main/res/values-tr-rTR/changelog.xml index 3ea04e700d..ab24ed1cac 100644 --- a/app/src/main/res/values-tr-rTR/changelog.xml +++ b/app/src/main/res/values-tr-rTR/changelog.xml @@ -1,2 +1,14 @@ - + + + Sürüm %1$s: + + ]]> + + diff --git a/app/src/main/res/values-uk-rUA/changelog.xml b/app/src/main/res/values-uk-rUA/changelog.xml index 82f9391a6a..91abb65ae3 100644 --- a/app/src/main/res/values-uk-rUA/changelog.xml +++ b/app/src/main/res/values-uk-rUA/changelog.xml @@ -2,4 +2,15 @@ З поверненням! Ми внесли деякі зміни після моменту, коли ви увійшли в систему. Детальніше: + + Версія %1$s: + + ]]> + diff --git a/app/src/main/res/values-vi-rVN/changelog.xml b/app/src/main/res/values-vi-rVN/changelog.xml index 3ea04e700d..d5216c81d2 100644 --- a/app/src/main/res/values-vi-rVN/changelog.xml +++ b/app/src/main/res/values-vi-rVN/changelog.xml @@ -1,2 +1,14 @@ - + + + Phiên bản %1$s: + + ]]> + + diff --git a/app/src/main/res/values-zh-rCN/changelog.xml b/app/src/main/res/values-zh-rCN/changelog.xml index d5562a6a22..fbe06f7cda 100644 --- a/app/src/main/res/values-zh-rCN/changelog.xml +++ b/app/src/main/res/values-zh-rCN/changelog.xml @@ -2,12 +2,15 @@ 欢迎回来! 自您上次登录以来,我们已经做了一些更改。以下是详细信息: - %1$s版本: + + 版本 %1$s:
    -
  • 添加了新的时间线显示模式, 包括图库布局。
  • -
  • 改进RSS阅读, 分享、翻译和摘要。
  • -
  • 更容易自定义的重整外观设置。
  • -
  • Bug 修复和性能改进。
  • +
  • 新增 Deck Mode,带来多列时间线体验。
  • +
  • 新增更丰富的时间线标签自定义,包括图标、过滤器、分组和单标签外观。
  • +
  • 本地关键词过滤新增正则表达式支持。
  • +
  • 性能改进和问题修复。
- ]]>
+ ]]> +
diff --git a/app/src/main/res/values-zh-rTW/changelog.xml b/app/src/main/res/values-zh-rTW/changelog.xml index 3ea04e700d..7dadd57f4a 100644 --- a/app/src/main/res/values-zh-rTW/changelog.xml +++ b/app/src/main/res/values-zh-rTW/changelog.xml @@ -1,2 +1,14 @@ - + + + 版本 %1$s: + + ]]> + + diff --git a/app/src/main/res/values/changelog.xml b/app/src/main/res/values/changelog.xml index e508dd3797..6f2fe581bc 100644 --- a/app/src/main/res/values/changelog.xml +++ b/app/src/main/res/values/changelog.xml @@ -6,10 +6,10 @@ Version %1$s: ]]> diff --git a/compose-ui/src/commonMain/composeResources/values-af-rZA/strings.xml b/compose-ui/src/commonMain/composeResources/values-af-rZA/strings.xml index 47b9bbff72..1636567eb6 100644 --- a/compose-ui/src/commonMain/composeResources/values-af-rZA/strings.xml +++ b/compose-ui/src/commonMain/composeResources/values-af-rZA/strings.xml @@ -9,13 +9,14 @@ %1$s Plasings %1$s Plasings Volg + Request follow Volg tans Aangevra Geblokkeer Volg jou het jou begin volg het jou plasing as gunsteling gemerk - het herplasing gedoen + het 'n plasing herplaas \'n Meningspeiling waaraan jy deelgeneem het, het geëindig het jou genoem het versoek om jou te volg @@ -28,7 +29,7 @@ Wys volledige teks het jou begin volg hou van jou plasing - het jou plasing herplaas + het 'n plasing herplaas \'n Meningspeiling waaraan jy deelgeneem het, het geëindig het jou genoem herplaas @@ -39,7 +40,7 @@ het \'n plasing vasgespeld het jou genoem het op jou geantwoord - het jou plasing herplaas + het 'n plasing herplaas het jou plasing aangehaal het op jou plasing gereageer \'n Meningspeiling waaraan jy deelgeneem het, het geëindig diff --git a/compose-ui/src/commonMain/composeResources/values-ar-rSA/strings.xml b/compose-ui/src/commonMain/composeResources/values-ar-rSA/strings.xml index 74c1bddab7..76ccde0cc4 100644 --- a/compose-ui/src/commonMain/composeResources/values-ar-rSA/strings.xml +++ b/compose-ui/src/commonMain/composeResources/values-ar-rSA/strings.xml @@ -9,13 +9,14 @@ %1$s منشورات %1$s منشورات متابعة + Request follow يتابع تم الطلب محظور يتابعك تابعك أعجب بمنشورك - أعاد النشر + أعاد نشر منشور انتهى استطلاع كنت تشارك فيه ذكرك طلب متابعتك @@ -28,7 +29,7 @@ عرض النص الكامل تابعك أعجب بمنشورك - أعاد نشر منشورك + أعاد نشر منشور انتهى استطلاع كنت تشارك فيه ذكرك أعاد النشر @@ -39,7 +40,7 @@ ثبت منشوراً ذكرك رد عليك - أعاد نشر منشورك + أعاد نشر منشور اقتبس منشورك تفاعل مع منشورك انتهى استطلاع كنت تشارك فيه diff --git a/compose-ui/src/commonMain/composeResources/values-bg-rBG/strings.xml b/compose-ui/src/commonMain/composeResources/values-bg-rBG/strings.xml index 36d0943814..e6d095286b 100644 --- a/compose-ui/src/commonMain/composeResources/values-bg-rBG/strings.xml +++ b/compose-ui/src/commonMain/composeResources/values-bg-rBG/strings.xml @@ -9,13 +9,14 @@ %1$s Публикации %1$s Публикации Последване + Request follow Следване Заявено Блокиран Следва ви ви последва хареса публикацията ви - сподели + препубликува публикация Анкета, в която сте участвали, приключи ви спомена поиска да ви последва @@ -28,7 +29,7 @@ Покажи целия текст ви последва хареса публикацията ви - препубликува публикацията ви + препубликува публикация Анкета, в която сте участвали, приключи ви спомена препубликува @@ -39,7 +40,7 @@ закачи публикация ви спомена ви отговори - препубликува публикацията ви + препубликува публикация цитира публикацията ви реагира на публикацията ви Анкета, в която сте участвали, приключи diff --git a/compose-ui/src/commonMain/composeResources/values-ca-rES/strings.xml b/compose-ui/src/commonMain/composeResources/values-ca-rES/strings.xml index ef8a44bd1d..6d44033730 100644 --- a/compose-ui/src/commonMain/composeResources/values-ca-rES/strings.xml +++ b/compose-ui/src/commonMain/composeResources/values-ca-rES/strings.xml @@ -9,13 +9,14 @@ %1$s Publicacions %1$s Publicacions Segueix + Request follow Seguint Sol·licitat Bloquejat Et segueix t\'ha seguit ha marcat com a preferida la teva publicació - ha republicat + ha republicat una publicació Ha finalitzat una enquesta en què participaves t\'ha esmentat ha sol·licitat seguir-te @@ -28,7 +29,7 @@ Mostra el text complet t\'ha seguit li ha agradat la teva publicació - ha republicat la teva publicació + ha republicat una publicació Ha finalitzat una enquesta en què participaves t\'ha esmentat ha republicat @@ -39,7 +40,7 @@ ha fixat una publicació t\'ha esmentat t\'ha respost - ha republicat la teva publicació + ha republicat una publicació ha citat la teva publicació ha reaccionat a la teva publicació Ha finalitzat una enquesta en què participaves diff --git a/compose-ui/src/commonMain/composeResources/values-cs-rCZ/strings.xml b/compose-ui/src/commonMain/composeResources/values-cs-rCZ/strings.xml index 0ae774d6bc..6c91acc313 100644 --- a/compose-ui/src/commonMain/composeResources/values-cs-rCZ/strings.xml +++ b/compose-ui/src/commonMain/composeResources/values-cs-rCZ/strings.xml @@ -9,13 +9,14 @@ %1$s Příspěvky %1$s Příspěvky Sledovat + Request follow Sleduji Požádáno Blokován Sleduje vás vás začal(a) sledovat přidal(a) váš příspěvek do oblíbených - sdílel(a) + přesdílel(a) příspěvek Anketa, které jste se účastnili, skončila vás zmínil(a) požádal(a) o sledování @@ -28,7 +29,7 @@ Zobrazit celý text vás začal(a) sledovat olajkoval(a) váš příspěvek - přesdílel(a) váš příspěvek + přesdílel(a) příspěvek Anketa, které jste se účastnili, skončila vás zmínil(a) přesdílel(a) @@ -39,7 +40,7 @@ přišpendlil(a) příspěvek vás zmínil(a) vám odpověděl(a) - přesdílel(a) váš příspěvek + přesdílel(a) příspěvek citoval(a) váš příspěvek reagoval(a) na váš příspěvek Anketa, které jste se účastnili, skončila diff --git a/compose-ui/src/commonMain/composeResources/values-da-rDK/strings.xml b/compose-ui/src/commonMain/composeResources/values-da-rDK/strings.xml index 06aef32ac0..e5a594ca98 100644 --- a/compose-ui/src/commonMain/composeResources/values-da-rDK/strings.xml +++ b/compose-ui/src/commonMain/composeResources/values-da-rDK/strings.xml @@ -9,13 +9,14 @@ %1$s Indlæg %1$s Indlæg Følg + Request follow Følger Anmodet Blokeret Følger dig fulgte dig favoriserede dit indlæg - delte igen + postede et indlæg igen En afstemning, du deltog i, er afsluttet nævnte dig anmodede om at følge dig @@ -28,7 +29,7 @@ Vis fuld tekst fulgte dig kunne lide dit indlæg - postede dit indlæg igen + postede et indlæg igen En afstemning, du deltog i, er afsluttet nævnte dig postede igen @@ -39,7 +40,7 @@ fastgjorde et indlæg nævnte dig svarede dig - postede dit indlæg igen + postede et indlæg igen citerede dit indlæg reagerede på dit indlæg En afstemning, du deltog i, er afsluttet diff --git a/compose-ui/src/commonMain/composeResources/values-de-rDE/strings.xml b/compose-ui/src/commonMain/composeResources/values-de-rDE/strings.xml index 5636b20e17..7c74190f81 100644 --- a/compose-ui/src/commonMain/composeResources/values-de-rDE/strings.xml +++ b/compose-ui/src/commonMain/composeResources/values-de-rDE/strings.xml @@ -9,13 +9,14 @@ %1$s Beiträge %1$s Beiträge Folgen + Request follow Ich folge Angefragt Blockiert Folgt dir folgt dir jetzt hat deinen Beitrag favorisiert - hat geteilt + hat einen Beitrag geteilt Eine Umfrage, an der du teilgenommen hast, ist beendet hat dich erwähnt möchte dir folgen @@ -28,7 +29,7 @@ Vollständigen Text anzeigen folgt dir jetzt gefällt dein Beitrag - hat deinen Beitrag geteilt + hat einen Beitrag geteilt Eine Umfrage, an der du teilgenommen hast, ist beendet hat dich erwähnt geteilt @@ -39,7 +40,7 @@ hat einen Beitrag angepinnt hat dich erwähnt hat dir geantwortet - hat deinen Beitrag geteilt + hat einen Beitrag geteilt hat deinen Beitrag zitiert hat auf deinen Beitrag reagiert Eine Umfrage, an der du teilgenommen hast, ist beendet diff --git a/compose-ui/src/commonMain/composeResources/values-el-rGR/strings.xml b/compose-ui/src/commonMain/composeResources/values-el-rGR/strings.xml index ac52174c9f..2285a168fc 100644 --- a/compose-ui/src/commonMain/composeResources/values-el-rGR/strings.xml +++ b/compose-ui/src/commonMain/composeResources/values-el-rGR/strings.xml @@ -9,13 +9,14 @@ %1$s Δημοσιεύσεις %1$s Δημοσιεύσεις Ακολουθήστε + Request follow Ακολουθείτε Εκκρεμεί αίτημα Αποκλεισμένος Σας ακολουθεί σας ακολούθησε πρόσθεσε στα αγαπημένα τη δημοσίευσή σας - αναδημοσίευσε + αναδημοσίευσε μια δημοσίευση Μια δημοσκόπηση στην οποία συμμετείχατε έληξε σας ανέφερε ζήτησε να σας ακολουθήσει @@ -28,7 +29,7 @@ Εμφάνιση πλήρους κειμένου σας ακολούθησε του/της άρεσε η δημοσίευσή σας - αναδημοσίευσε τη δημοσίευσή σας + αναδημοσίευσε μια δημοσίευση Μια δημοσκόπηση στην οποία συμμετείχατε έληξε σας ανέφερε αναδημοσίευσε @@ -39,7 +40,7 @@ κάρφωσε μια δημοσίευση σας ανέφερε σας απάντησε - αναδημοσίευσε τη δημοσίευσή σας + αναδημοσίευσε μια δημοσίευση παρέθεσε τη δημοσίευσή σας αντέδρασε στη δημοσίευσή σας Μια δημοσκόπηση στην οποία συμμετείχατε έληξε diff --git a/compose-ui/src/commonMain/composeResources/values-es-rES/strings.xml b/compose-ui/src/commonMain/composeResources/values-es-rES/strings.xml index 8566782c53..9340e51062 100644 --- a/compose-ui/src/commonMain/composeResources/values-es-rES/strings.xml +++ b/compose-ui/src/commonMain/composeResources/values-es-rES/strings.xml @@ -9,13 +9,14 @@ %1$s Publicaciones %1$s Publicaciones Seguir + Request follow Siguiendo Solicitado Bloqueado Te sigue te ha seguido ha marcado como favorito tu publicación - ha compartido + ha compartido una publicación Una encuesta en la que participabas ha terminado te ha mencionado ha solicitado seguirte @@ -28,7 +29,7 @@ Mostrar texto completo te ha seguido le ha gustado tu publicación - ha compartido tu publicación + ha compartido una publicación Una encuesta en la que participabas ha terminado te ha mencionado ha compartido @@ -39,7 +40,7 @@ ha fijado una publicación te ha mencionado te ha respondido - ha compartido tu publicación + ha compartido una publicación ha citado tu publicación ha reaccionado a tu publicación Una encuesta en la que participabas ha terminado diff --git a/compose-ui/src/commonMain/composeResources/values-fi-rFI/strings.xml b/compose-ui/src/commonMain/composeResources/values-fi-rFI/strings.xml index 96c0017407..43614efac7 100644 --- a/compose-ui/src/commonMain/composeResources/values-fi-rFI/strings.xml +++ b/compose-ui/src/commonMain/composeResources/values-fi-rFI/strings.xml @@ -9,13 +9,14 @@ %1$s Julkaisua %1$s Julkaisua Seuraa + Request follow Seurataan Pyydetty Estetty Seuraa sinua seurasi sinua tykkäsi julkaisustasi - jakoi julkaisusi + jakoi julkaisun Kysely, johon osallistuit, on päättynyt mainitsi sinut pyysi saada seurata sinua @@ -28,7 +29,7 @@ Näytä koko teksti seurasi sinua tykkäsi julkaisustasi - jakoi julkaisusi + jakoi julkaisun Kysely, johon osallistuit, on päättynyt mainitsi sinut jakoi @@ -39,7 +40,7 @@ kiinnitti julkaisun mainitsi sinut vastasi sinulle - jakoi julkaisusi + jakoi julkaisun lainasi julkaisuasi reagoi julkaisuusi Kysely, johon osallistuit, on päättynyt diff --git a/compose-ui/src/commonMain/composeResources/values-fr-rFR/strings.xml b/compose-ui/src/commonMain/composeResources/values-fr-rFR/strings.xml index 00f3f35ec0..0869656059 100644 --- a/compose-ui/src/commonMain/composeResources/values-fr-rFR/strings.xml +++ b/compose-ui/src/commonMain/composeResources/values-fr-rFR/strings.xml @@ -9,13 +9,14 @@ %1$s publications %1$s publications Suivre + Request follow Abonné En attente Bloqué Vous suit vous suit a aimé votre publication - a partagé + a repartagé une publication Un sondage auquel vous avez participé est terminé vous a mentionné a demandé à vous suivre @@ -28,7 +29,7 @@ Afficher le texte complet vous suit a aimé votre publication - a repartagé votre publication + a repartagé une publication Un sondage auquel vous avez participé est terminé vous a mentionné a repartagé @@ -39,7 +40,7 @@ a épinglé une publication vous a mentionné vous a répondu - a repartagé votre publication + a repartagé une publication a cité votre publication a réagi à votre publication Un sondage auquel vous avez participé est terminé diff --git a/compose-ui/src/commonMain/composeResources/values-hu-rHU/strings.xml b/compose-ui/src/commonMain/composeResources/values-hu-rHU/strings.xml index 7361c4594c..4ab0b8db95 100644 --- a/compose-ui/src/commonMain/composeResources/values-hu-rHU/strings.xml +++ b/compose-ui/src/commonMain/composeResources/values-hu-rHU/strings.xml @@ -9,13 +9,14 @@ %1$s Bejegyzés %1$s Bejegyzés Követés + Request follow Követve Kérve Tiltva Követ téged követni kezdett téged kedvencnek jelölte a bejegyzésedet - megosztotta + újraközölt egy bejegyzést Egy szavazás, amiben részt vettél, véget ért megemlített téged követési kérést küldött @@ -28,7 +29,7 @@ Teljes szöveg megjelenítése követni kezdett téged kedvelte a bejegyzésedet - újraközölte a bejegyzésedet + újraközölt egy bejegyzést Egy szavazás, amiben részt vettél, véget ért megemlített téged újraközölte @@ -39,7 +40,7 @@ rögzített egy bejegyzést megemlített téged válaszolt neked - újraközölte a bejegyzésedet + újraközölt egy bejegyzést idézte a bejegyzésedet reagált a bejegyzésedre Egy szavazás, amiben részt vettél, véget ért diff --git a/compose-ui/src/commonMain/composeResources/values-it-rIT/strings.xml b/compose-ui/src/commonMain/composeResources/values-it-rIT/strings.xml index 99184c427a..a5eee530c4 100644 --- a/compose-ui/src/commonMain/composeResources/values-it-rIT/strings.xml +++ b/compose-ui/src/commonMain/composeResources/values-it-rIT/strings.xml @@ -9,13 +9,14 @@ %1$s Post %1$s Post Segui + Request follow Segui già Richiesto Bloccato Ti segue ti segue ha messo tra i preferiti il tuo post - ha condiviso + ha ripubblicato un post Un sondaggio a cui hai partecipato è terminato ti ha menzionato ha chiesto di seguirti @@ -28,7 +29,7 @@ Mostra testo completo ti segue ha messo mi piace al tuo post - ha ripubblicato il tuo post + ha ripubblicato un post Un sondaggio a cui hai partecipato è terminato ti ha menzionato ha ripubblicato @@ -39,7 +40,7 @@ ha fissato un post ti ha menzionato ti ha risposto - ha ripubblicato il tuo post + ha ripubblicato un post ha citato il tuo post ha reagito al tuo post Un sondaggio a cui hai partecipato è terminato diff --git a/compose-ui/src/commonMain/composeResources/values-iw-rIL/strings.xml b/compose-ui/src/commonMain/composeResources/values-iw-rIL/strings.xml index aca55b1854..bcd9db4c40 100644 --- a/compose-ui/src/commonMain/composeResources/values-iw-rIL/strings.xml +++ b/compose-ui/src/commonMain/composeResources/values-iw-rIL/strings.xml @@ -9,13 +9,14 @@ %1$s פוסטים %1$s פוסטים מעקב + Request follow במעקב נשלחה בקשה חסום עוקב אחריך עקב אחריך סימן את הפוסט שלך כמועדף - שיתף מחדש + פרסם מחדש פוסט סקר שהשתתפת בו הסתיים הזכיר אותך ביקש לעקוב אחריך @@ -28,7 +29,7 @@ הצג טקסט מלא עקב אחריך אהב את הפוסט שלך - פרסם מחדש את הפוסט שלך + פרסם מחדש פוסט סקר שהשתתפת בו הסתיים הזכיר אותך פרסם מחדש @@ -39,7 +40,7 @@ נעץ פוסט הזכיר אותך ענה לך - פרסם מחדש את הפוסט שלך + פרסם מחדש פוסט ציטט את הפוסט שלך הגיב לפוסט שלך סקר שהשתתפת בו הסתיים diff --git a/compose-ui/src/commonMain/composeResources/values-ja-rJP/strings.xml b/compose-ui/src/commonMain/composeResources/values-ja-rJP/strings.xml index 7f31eb9874..d28ddd8ff3 100644 --- a/compose-ui/src/commonMain/composeResources/values-ja-rJP/strings.xml +++ b/compose-ui/src/commonMain/composeResources/values-ja-rJP/strings.xml @@ -9,13 +9,14 @@ %1$s 投稿 %1$s 投稿 フォロー + フォローをリクエスト フォロー中 承認待ち ブロック中 フォローされています さんにフォローされました さんがあなたの投稿をお気に入りに登録しました - さんがリブロードしました + さんが投稿をブーストしました 参加したアンケートが終了しました さんがあなたに返信しました さんからフォローリクエストが届きました @@ -28,7 +29,7 @@ 全文を表示 さんにフォローされました さんがあなたの投稿に「いいね」しました - さんがあなたの投稿をリポストしました + さんが投稿をリポストしました 参加したアンケートが終了しました さんがあなたにメンションしました さんがリポストしました @@ -39,7 +40,7 @@ さんが投稿をピン留めしました さんがあなたにメンションしました さんがあなたに返信しました - さんがあなたの投稿をリノートしました + さんが投稿をリノートしました さんがあなたの投稿を引用しました さんがあなたの投稿にリアクションしました 参加したアンケートが終了しました diff --git a/compose-ui/src/commonMain/composeResources/values-ko-rKR/strings.xml b/compose-ui/src/commonMain/composeResources/values-ko-rKR/strings.xml index 7fca64b92f..69e657f6a2 100644 --- a/compose-ui/src/commonMain/composeResources/values-ko-rKR/strings.xml +++ b/compose-ui/src/commonMain/composeResources/values-ko-rKR/strings.xml @@ -9,6 +9,7 @@ %1$s 게시물 %1$s 게시물 팔로우 + Request follow 팔로잉 요청됨 차단됨 @@ -28,7 +29,7 @@ 전문 보기 님이 나를 팔로우했습니다 님이 내 게시물을 좋아합니다 - 님이 내 게시물을 리포스트했습니다 + 님이 게시물을 리포스트했습니다 참여하신 설문조사가 종료되었습니다 님이 나를 언급했습니다 리포스트함 @@ -39,7 +40,7 @@ 님이 게시물을 고정했습니다 님이 나를 언급했습니다 님이 답글을 남겼습니다 - 님이 내 게시물을 리노트했습니다 + 님이 게시물을 리노트했습니다 님이 내 게시물을 인용했습니다 님이 내 게시물에 반응을 남겼습니다 참여하신 설문조사가 종료되었습니다 diff --git a/compose-ui/src/commonMain/composeResources/values-nl-rNL/strings.xml b/compose-ui/src/commonMain/composeResources/values-nl-rNL/strings.xml index 53c8cafabb..6237ce70ba 100644 --- a/compose-ui/src/commonMain/composeResources/values-nl-rNL/strings.xml +++ b/compose-ui/src/commonMain/composeResources/values-nl-rNL/strings.xml @@ -8,13 +8,14 @@ %1$s Berichten %1$s Berichten Volgen + Request follow Volgend Verzocht Geblokkeerd Volgt jou heeft je gevolgd heeft je bericht als favoriet gemarkeerd - heeft geboost + heeft een bericht gedeeld Een enquête waaraan je deelnam is beëindigd noemde je verzocht om je te volgen @@ -27,7 +28,7 @@ Toon volledige tekst heeft je gevolgd vond je bericht leuk - heeft je bericht gedeeld + heeft een bericht gedeeld Een enquête waaraan je deelnam is beëindigd noemde je gedeeld @@ -38,7 +39,7 @@ heeft een bericht vastgezet noemde je reageerde op jou - heeft je bericht gedeeld + heeft een bericht gedeeld citeerde je bericht reageerde op je bericht Een enquête waaraan je deelnam is beëindigd diff --git a/compose-ui/src/commonMain/composeResources/values-no-rNO/strings.xml b/compose-ui/src/commonMain/composeResources/values-no-rNO/strings.xml index ac8b5d0998..d5d1601186 100644 --- a/compose-ui/src/commonMain/composeResources/values-no-rNO/strings.xml +++ b/compose-ui/src/commonMain/composeResources/values-no-rNO/strings.xml @@ -8,13 +8,14 @@ %1$s innlegg %1$s innlegg Følg + Request follow Følger Forespurt Blokkert Følger deg fulgte deg likte innlegget ditt - fremmet + reposter et innlegg En avstemning du deltok i er avsluttet nevnte deg ba om å få følge deg @@ -27,7 +28,7 @@ Vis full tekst fulgte deg likte innlegget ditt - reposter innlegget ditt + reposter et innlegg En avstemning du deltok i er avsluttet nevnte deg reposter @@ -38,7 +39,7 @@ festet et innlegg nevnte deg svarte deg - reposter innlegget ditt + reposter et innlegg siterte innlegget ditt reagerte på innlegget ditt En avstemning du deltok i er avsluttet diff --git a/compose-ui/src/commonMain/composeResources/values-pl-rPL/strings.xml b/compose-ui/src/commonMain/composeResources/values-pl-rPL/strings.xml index c76ed72ca1..0920dc372c 100644 --- a/compose-ui/src/commonMain/composeResources/values-pl-rPL/strings.xml +++ b/compose-ui/src/commonMain/composeResources/values-pl-rPL/strings.xml @@ -8,13 +8,14 @@ %1$s Wpisów %1$s Wpisów Obserwuj + Request follow Obserwujesz Oczekuje Zablokowany Obserwuje Cię zaczął Cię obserwować dodał Twój wpis do ulubionych - podał dalej + podał dalej wpis Ankieta, w której brałeś udział, zakończyła się wspomniał o Tobie poprosił o możliwość obserwowania Cię @@ -27,7 +28,7 @@ Pokaż pełny tekst zaczął Cię obserwować polubił Twój wpis - podał dalej Twój wpis + podał dalej wpis Ankieta, w której brałeś udział, zakończyła się wspomniał o Tobie podał dalej @@ -38,7 +39,7 @@ przypiął wpis wspomniał o Tobie odpowiedział Ci - podał dalej Twój wpis + podał dalej wpis zacytował Twój wpis zareagował na Twój wpis Ankieta, w której brałeś udział, zakończyła się diff --git a/compose-ui/src/commonMain/composeResources/values-pt-rBR/strings.xml b/compose-ui/src/commonMain/composeResources/values-pt-rBR/strings.xml index a6de9e092e..7edba24544 100644 --- a/compose-ui/src/commonMain/composeResources/values-pt-rBR/strings.xml +++ b/compose-ui/src/commonMain/composeResources/values-pt-rBR/strings.xml @@ -8,13 +8,14 @@ %1$s Postagens %1$s Postagens Seguir + Request follow Seguindo Solicitado Bloqueado Segue você seguiu você curtiu sua postagem - recompartilhou + repostou uma postagem Uma enquete da qual você participou terminou mencionou você solicitou para seguir você @@ -27,7 +28,7 @@ Mostrar texto completo seguiu você curtiu sua postagem - repostou sua postagem + repostou uma postagem Uma enquete da qual você participou terminou mencionou você repostou @@ -38,7 +39,7 @@ fixou uma postagem mencionou você respondeu a você - repostou sua postagem + repostou uma postagem citou sua postagem reagiu à sua postagem Uma enquete da qual você participou terminou diff --git a/compose-ui/src/commonMain/composeResources/values-pt-rPT/strings.xml b/compose-ui/src/commonMain/composeResources/values-pt-rPT/strings.xml index e726fd5348..0128df146f 100644 --- a/compose-ui/src/commonMain/composeResources/values-pt-rPT/strings.xml +++ b/compose-ui/src/commonMain/composeResources/values-pt-rPT/strings.xml @@ -8,13 +8,14 @@ %1$s Publicações %1$s Publicações Seguir + Request follow Seguindo Solicitado Bloqueado Segue-o seguiu-o marcou como favorita a sua publicação - repartilhou + republicou uma publicação Uma sondagem em que participou terminou mencionou-o solicitou segui-lo @@ -27,7 +28,7 @@ Mostrar texto completo seguiu-o gostou da sua publicação - republicou a sua publicação + republicou uma publicação Uma sondagem em que participou terminou mencionou-o republicou @@ -38,7 +39,7 @@ fixou uma publicação mencionou-o respondeu-lhe - republicou a sua publicação + republicou uma publicação citou a sua publicação reagiu à sua publicação Uma sondagem em que participou terminou diff --git a/compose-ui/src/commonMain/composeResources/values-ro-rRO/strings.xml b/compose-ui/src/commonMain/composeResources/values-ro-rRO/strings.xml index d05bc3667b..000fc5753e 100644 --- a/compose-ui/src/commonMain/composeResources/values-ro-rRO/strings.xml +++ b/compose-ui/src/commonMain/composeResources/values-ro-rRO/strings.xml @@ -8,13 +8,14 @@ %1$s Postări %1$s Postări Urmărește + Request follow Urmărești Solicitat Blocat Te urmărește te-a urmărit ți-a apreciat postarea - a redistribuit + a repostat o postare Un sondaj la care ai participat s-a încheiat te-a menționat a solicitat să te urmărească @@ -27,7 +28,7 @@ Arată textul complet te-a urmărit ți-a apreciat postarea - a repostat postarea ta + a repostat o postare Un sondaj la care ai participat s-a încheiat te-a menționat repostat @@ -38,7 +39,7 @@ a fixat o postare te-a menționat ți-a răspuns - a repostat postarea ta + a repostat o postare ți-a citat postarea a reacționat la postarea ta Un sondaj la care ai participat s-a încheiat diff --git a/compose-ui/src/commonMain/composeResources/values-ru-rRU/strings.xml b/compose-ui/src/commonMain/composeResources/values-ru-rRU/strings.xml index b8f9668e5b..a455fafa4c 100644 --- a/compose-ui/src/commonMain/composeResources/values-ru-rRU/strings.xml +++ b/compose-ui/src/commonMain/composeResources/values-ru-rRU/strings.xml @@ -8,13 +8,14 @@ %1$s Посты %1$s Посты Подписаться + Request follow Вы подписаны Запрос отправлен Заблокирован Подписан на вас подписался на вас добавил ваш пост в избранное - сделал репост + сделал репост поста Опрос, в котором вы участвовали, завершен упомянул вас отправил запрос на подписку @@ -27,7 +28,7 @@ Показать весь текст подписался на вас лайкнул ваш пост - репостнул ваш пост + сделал репост поста Опрос, в котором вы участвовали, завершен упомянул вас сделал репост @@ -38,7 +39,7 @@ закрепил пост упомянул вас ответил вам - репостнул ваш пост + сделал репост поста процитировал ваш пост отреагировал на ваш пост Опрос, в котором вы участвовали, завершен diff --git a/compose-ui/src/commonMain/composeResources/values-sr-rSP/strings.xml b/compose-ui/src/commonMain/composeResources/values-sr-rSP/strings.xml index 1509ffe1ed..edb9dace00 100644 --- a/compose-ui/src/commonMain/composeResources/values-sr-rSP/strings.xml +++ b/compose-ui/src/commonMain/composeResources/values-sr-rSP/strings.xml @@ -11,6 +11,7 @@ %1$s Objave Prati + Request follow Pratite Poslat zahtev Blokirano @@ -18,7 +19,7 @@ vas prati je označio vašu objavu kao omiljenu - je podelio vašu objavu + je ponovo objavio objavu Anketa u kojoj ste učestvovali je završena vas je pomenuo je poslao zahtev da vas prati @@ -33,7 +34,7 @@ vas prati mu se sviđa vaša objava - je ponovo objavio vašu objavu + je ponovo objavio objavu Anketa u kojoj ste učestvovali je završena vas je pomenuo je ponovo objavio @@ -45,7 +46,7 @@ vas je pomenuo vam je odgovorio - je ponovo objavio vašu objavu + je ponovo objavio objavu je citirao vašu objavu je reagovao na vašu objavu Anketa u kojoj ste učestvovali je završena diff --git a/compose-ui/src/commonMain/composeResources/values-sv-rSE/strings.xml b/compose-ui/src/commonMain/composeResources/values-sv-rSE/strings.xml index 6ccf7606a3..2d279dbfd0 100644 --- a/compose-ui/src/commonMain/composeResources/values-sv-rSE/strings.xml +++ b/compose-ui/src/commonMain/composeResources/values-sv-rSE/strings.xml @@ -11,6 +11,7 @@ %1$s Inlägg Följ + Request follow Följer Begärt Blockerad @@ -18,7 +19,7 @@ följde dig favoritmarkerade ditt inlägg - delade vidare + repostade ett inlägg En omröstning du deltog i har avslutats nämnde dig begärde att få följa dig @@ -33,7 +34,7 @@ följde dig gillade ditt inlägg - repostade ditt inlägg + repostade ett inlägg En omröstning du deltog i har avslutats nämnde dig repostade @@ -45,7 +46,7 @@ nämnde dig svarade dig - repostade ditt inlägg + repostade ett inlägg citerade ditt inlägg reagerade på ditt inlägg En omröstning du deltog i har avslutats diff --git a/compose-ui/src/commonMain/composeResources/values-tr-rTR/strings.xml b/compose-ui/src/commonMain/composeResources/values-tr-rTR/strings.xml index b654b1a11a..d366279559 100644 --- a/compose-ui/src/commonMain/composeResources/values-tr-rTR/strings.xml +++ b/compose-ui/src/commonMain/composeResources/values-tr-rTR/strings.xml @@ -11,6 +11,7 @@ %1$s Gönderi Takip Et + Request follow Takip Ediliyor İstek Gönderildi Engellendi @@ -18,7 +19,7 @@ seni takip etti gönderini favorilerine ekledi - paylaştı + bir gönderiyi yeniden paylaştı Katıldığınız bir anket sona erdi senden bahsetti seni takip etmek için istek gönderdi @@ -33,7 +34,7 @@ seni takip etti gönderini beğendi - gönderini yeniden paylaştı + bir gönderiyi yeniden paylaştı Katıldığınız bir anket sona erdi senden bahsetti yeniden paylaştı @@ -45,7 +46,7 @@ senden bahsetti sana yanıt verdi - gönderini yeniden paylaştı + bir gönderiyi yeniden paylaştı gönderini alıntıladı gönderine tepki verdi Katıldığınız bir anket sona erdi diff --git a/compose-ui/src/commonMain/composeResources/values-uk-rUA/strings.xml b/compose-ui/src/commonMain/composeResources/values-uk-rUA/strings.xml index 87faccb516..23aee0100b 100644 --- a/compose-ui/src/commonMain/composeResources/values-uk-rUA/strings.xml +++ b/compose-ui/src/commonMain/composeResources/values-uk-rUA/strings.xml @@ -11,6 +11,7 @@ %1$s Дописи Підписатися + Request follow Ви підписані Запит надіслано Заблоковано @@ -18,7 +19,7 @@ підписався(-лася) на вас додав(-ла) ваш допис до обраного - поширив(-ла) + поширив(-ла) допис Опитування, в якому ви брали участь, завершилося згадав(-ла) вас надіслав(-ла) запит на підписку @@ -33,7 +34,7 @@ підписався(-лася) на вас вподобав(-ла) ваш допис - поширив(-ла) ваш допис + поширив(-ла) допис Опитування, в якому ви брали участь, завершилося згадав(-ла) вас поширив(-ла) @@ -45,7 +46,7 @@ згадав(-ла) вас відповів(-ла) вам - поширив(-ла) ваш допис + поширив(-ла) допис процитував(-ла) ваш допис відреагував(-ла) на ваш допис Опитування, в якому ви брали участь, завершилося diff --git a/compose-ui/src/commonMain/composeResources/values-vi-rVN/strings.xml b/compose-ui/src/commonMain/composeResources/values-vi-rVN/strings.xml index ba2d301434..6db661dee6 100644 --- a/compose-ui/src/commonMain/composeResources/values-vi-rVN/strings.xml +++ b/compose-ui/src/commonMain/composeResources/values-vi-rVN/strings.xml @@ -11,6 +11,7 @@ %1$s Bài đăng Theo dõi + Request follow Đang theo dõi Đã gửi yêu cầu Đã chặn @@ -18,7 +19,7 @@ đã theo dõi bạn đã thích bài đăng của bạn - đã chia sẻ lại + đã đăng lại một bài đăng Một cuộc thăm dò bạn tham gia đã kết thúc đã nhắc đến bạn đã yêu cầu theo dõi bạn @@ -33,7 +34,7 @@ đã theo dõi bạn đã thích bài đăng của bạn - đã đăng lại bài đăng của bạn + đã đăng lại một bài đăng Một cuộc thăm dò bạn tham gia đã kết thúc đã nhắc đến bạn đã đăng lại @@ -45,7 +46,7 @@ đã nhắc đến bạn đã trả lời bạn - đã đăng lại bài đăng của bạn + đã đăng lại một bài đăng đã trích dẫn bài đăng của bạn đã bày tỏ cảm xúc với bài đăng của bạn Một cuộc thăm dò bạn tham gia đã kết thúc diff --git a/compose-ui/src/commonMain/composeResources/values-zh-rCN/strings.xml b/compose-ui/src/commonMain/composeResources/values-zh-rCN/strings.xml index 4e1213c9af..0998b5edca 100644 --- a/compose-ui/src/commonMain/composeResources/values-zh-rCN/strings.xml +++ b/compose-ui/src/commonMain/composeResources/values-zh-rCN/strings.xml @@ -11,6 +11,7 @@ %1$s 帖子 关注 + 请求关注 正在关注 已请求 已屏蔽 @@ -18,7 +19,7 @@ 关注了你 收藏了你的帖子 - 转推了你的帖子 + 转发了帖子 你参与的投票已结束 提到了你 请求关注你 @@ -33,7 +34,7 @@ 关注了你 赞了你的帖子 - 转发了你的帖子 + 转发了帖子 你参与的投票已结束 提到了你 转发了帖子 @@ -45,7 +46,7 @@ 提到了你 回复了你 - 转发了你的帖子 + 转发了帖子 引用了你的帖子 回应了你的帖子 你参与的投票已结束 diff --git a/compose-ui/src/commonMain/composeResources/values-zh-rTW/strings.xml b/compose-ui/src/commonMain/composeResources/values-zh-rTW/strings.xml index db50224a9f..aeadb5fb3c 100644 --- a/compose-ui/src/commonMain/composeResources/values-zh-rTW/strings.xml +++ b/compose-ui/src/commonMain/composeResources/values-zh-rTW/strings.xml @@ -11,6 +11,7 @@ %1$s 貼文 關注 + 請求關注 正在關注 已請求 已封鎖 @@ -18,7 +19,7 @@ 關注了您 收藏了您的貼文 - 轉推了您的貼文 + 轉發了貼文 您參與的投票已結束 標記了您 請求關注您 @@ -33,7 +34,7 @@ 關注了您 讚了您的貼文 - 轉發了您的貼文 + 轉發了貼文 您參與的投票已結束 標記了您 轉發了貼文 @@ -45,7 +46,7 @@ 標記了您 回覆了您 - 轉發了您的貼文 + 轉發了貼文 引用了您的貼文 回應了您的貼文 您參與的投票已結束 diff --git a/compose-ui/src/commonMain/composeResources/values/strings.xml b/compose-ui/src/commonMain/composeResources/values/strings.xml index 7c18664b0f..9e203940ca 100644 --- a/compose-ui/src/commonMain/composeResources/values/strings.xml +++ b/compose-ui/src/commonMain/composeResources/values/strings.xml @@ -11,6 +11,7 @@ %1$s Posts Follow + Request follow Following Requested Blocked @@ -18,7 +19,7 @@ followed you favorited your post - reblogged + reblogged a post A poll you were participating in has ended mentioned you requested to follow you @@ -33,7 +34,7 @@ followed you liked your post - reposted your post + reposted a post A poll you were participating in has ended mentioned you reposted @@ -45,7 +46,7 @@ mentioned you replied to you - reposted your post + reposted a post quoted your post reacted to your post A poll you were participating in has ended diff --git a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/common/PagingStateExt.kt b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/common/PagingStateExt.kt index e3cdcb4730..087eb666f5 100644 --- a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/common/PagingStateExt.kt +++ b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/common/PagingStateExt.kt @@ -10,6 +10,7 @@ import dev.dimension.flare.common.onEmpty import dev.dimension.flare.common.onError import dev.dimension.flare.common.onLoading import dev.dimension.flare.common.onSuccess +import dev.dimension.flare.ui.component.status.appendStateUI import kotlin.native.HiddenFromObjC @HiddenFromObjC @@ -117,8 +118,8 @@ public fun LazyStaggeredGridScope.items( errorContent: @Composable LazyStaggeredGridItemScope.(Throwable) -> Unit = {}, loadingContent: @Composable LazyStaggeredGridItemScope.() -> Unit = {}, loadingCount: Int = 10, - key: (PagingState.Success.(index: Int) -> Any)? = null, - contentType: PagingState.Success.(index: Int) -> Any? = { null }, + key: ((item: T) -> Any)? = null, + contentType: ((item: T) -> Any?)? = { null }, itemContent: @Composable LazyStaggeredGridItemScope.(T) -> Unit, ) { state @@ -127,12 +128,16 @@ public fun LazyStaggeredGridScope.items( count = itemCount, key = key?.let { - { - key(this, it) + this.itemKey { + key(it) } }, contentType = { - contentType(this, it) + contentType?.let { + this.itemContentType { + contentType(it) + } + } }, ) { index -> val item = get(index) @@ -142,6 +147,7 @@ public fun LazyStaggeredGridScope.items( loadingContent() } } + appendStateUI(this) }.onLoading { items(loadingCount) { loadingContent() @@ -191,6 +197,7 @@ public fun LazyStaggeredGridScope.itemsIndexed( loadingContent() } } + appendStateUI(this) }.onLoading { items(loadingCount) { loadingContent() diff --git a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/ProfileHeader.kt b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/ProfileHeader.kt index 174fd32664..65975e44b9 100644 --- a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/ProfileHeader.kt +++ b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/ProfileHeader.kt @@ -46,6 +46,7 @@ import dev.dimension.flare.compose.ui.profile_header_button_blocked import dev.dimension.flare.compose.ui.profile_header_button_follow import dev.dimension.flare.compose.ui.profile_header_button_following import dev.dimension.flare.compose.ui.profile_header_button_is_fans +import dev.dimension.flare.compose.ui.profile_header_button_request_follow import dev.dimension.flare.compose.ui.profile_header_button_requested import dev.dimension.flare.model.MicroBlogKey import dev.dimension.flare.ui.component.platform.PlatformErrorButton @@ -60,6 +61,7 @@ import dev.dimension.flare.ui.model.UiRelation import dev.dimension.flare.ui.model.UiState import dev.dimension.flare.ui.model.onSuccess import dev.dimension.flare.ui.model.takeSuccess +import dev.dimension.flare.ui.presenter.profile.FollowButtonState import dev.dimension.flare.ui.presenter.profile.ProfileState import dev.dimension.flare.ui.route.DeeplinkRoute import dev.dimension.flare.ui.route.toUri @@ -117,6 +119,7 @@ public fun ProfileHeader( modifier = modifier, user = userState.data, relationState = state.relationState, + followButtonState = state.followButtonState, myAccountKey = state.myAccountKey, onFollowClick = state::follow, onUnfollowClick = state::unfollow, @@ -136,6 +139,7 @@ public fun ProfileHeader( private fun ProfileHeaderSuccess( user: UiProfile, relationState: UiState, + followButtonState: UiState, myAccountKey: UiState, onFollowClick: (userKey: MicroBlogKey) -> Unit, onUnfollowClick: (userKey: MicroBlogKey) -> Unit, @@ -159,7 +163,7 @@ private fun ProfileHeaderSuccess( headerTrailing = { isMe.onSuccess { if (!it) { - when (relationState) { + when (followButtonState) { is UiState.Error -> { Unit } @@ -180,12 +184,11 @@ private fun ProfileHeaderSuccess( } is UiState.Success -> { - val relation = relationState.data Column( horizontalAlignment = Alignment.CenterHorizontally, ) { AnimatedContent( - targetState = FollowButtonState.from(relation), + targetState = followButtonState.data, transitionSpec = { (fadeIn() + scaleIn(initialScale = 0.92f)) togetherWith (fadeOut() + scaleOut(targetScale = 0.92f)) @@ -229,6 +232,16 @@ private fun ProfileHeaderSuccess( } } + FollowButtonState.RequestFollow -> { + PlatformFilledTonalButton( + onClick = { + onFollowClick.invoke(user.key) + }, + ) { + PlatformText(text = stringResource(Res.string.profile_header_button_request_follow)) + } + } + FollowButtonState.Follow -> { PlatformFilledTonalButton( onClick = { @@ -240,7 +253,7 @@ private fun ProfileHeaderSuccess( } } } - if (relation.isFans) { + if (relationState.takeSuccess()?.isFans == true) { PlatformText( text = stringResource(Res.string.profile_header_button_is_fans), textAlign = TextAlign.Center, @@ -371,24 +384,6 @@ private fun ProfileHeaderSuccess( ) } -private enum class FollowButtonState { - Follow, - Requested, - Following, - Blocked, - ; - - companion object { - fun from(relation: UiRelation): FollowButtonState = - when { - relation.blocking -> Blocked - relation.following -> Following - relation.hasPendingFollowRequestFromYou -> Requested - else -> Follow - } - } -} - @Composable private fun ProfileHeaderError() { } diff --git a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/status/LazyStatusItems.kt b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/status/LazyStatusItems.kt index 5685ffb102..8d0050a3b9 100644 --- a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/status/LazyStatusItems.kt +++ b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/component/status/LazyStatusItems.kt @@ -104,47 +104,7 @@ public fun LazyStaggeredGridScope.status( ) } } - appendState - .onError { - item( - span = StaggeredGridItemSpan.FullLine, - ) { - OnError( -// modifier = Modifier.animateItem(), - error = it, - onRetry = { retry() }, - ) - } - }.onLoading { - item( - span = StaggeredGridItemSpan.FullLine, - ) { - PlatformLinearProgressIndicator( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = screenHorizontalPadding), - ) - } - }.onEndOfList { - item( - span = StaggeredGridItemSpan.FullLine, - ) { - Column( - modifier = - Modifier -// .animateItem() - .fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Spacer(modifier = Modifier.height(8.dp)) - PlatformText( - text = stringResource(Res.string.status_loadmore_end), - ) - Spacer(modifier = Modifier.height(8.dp)) - } - } - } + appendStateUI(this@onSuccess) } onError { item( @@ -190,6 +150,51 @@ public fun LazyStaggeredGridScope.status( } } +@HiddenFromObjC +public fun LazyStaggeredGridScope.appendStateUI(success: PagingState.Success) { + success.appendState + .onError { + item( + span = StaggeredGridItemSpan.FullLine, + ) { + OnError( +// modifier = Modifier.animateItem(), + error = it, + onRetry = { success.retry() }, + ) + } + }.onLoading { + item( + span = StaggeredGridItemSpan.FullLine, + ) { + PlatformLinearProgressIndicator( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = screenHorizontalPadding, vertical = 8.dp), + ) + } + }.onEndOfList { + item( + span = StaggeredGridItemSpan.FullLine, + ) { + Column( + modifier = + Modifier +// .animateItem() + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.height(8.dp)) + PlatformText( + text = stringResource(Res.string.status_loadmore_end), + ) + Spacer(modifier = Modifier.height(8.dp)) + } + } + } +} + @Composable public fun ListEmptyView(modifier: Modifier = Modifier) { Column( diff --git a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/settings/EditTabPresenter.kt b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/settings/EditTabPresenter.kt index 69a8766d74..c0050f0c6f 100644 --- a/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/settings/EditTabPresenter.kt +++ b/compose-ui/src/commonMain/kotlin/dev/dimension/flare/ui/screen/settings/EditTabPresenter.kt @@ -11,7 +11,7 @@ import dev.dimension.flare.data.model.tab.TimelineResolver import dev.dimension.flare.data.model.tab.TimelineTabItemV2 import dev.dimension.flare.model.MicroBlogKey import dev.dimension.flare.ui.component.res -import dev.dimension.flare.ui.model.UiIcon +import dev.dimension.flare.ui.model.TabPickerUiIcons import dev.dimension.flare.ui.model.UiState import dev.dimension.flare.ui.model.UiText import dev.dimension.flare.ui.presenter.PresenterBase @@ -78,7 +78,7 @@ public class EditTabPresenter( IconType.FavIcon(it.host), ) }.orEmpty() + - UiIcon.entries.map { + TabPickerUiIcons.map { IconType.Material(it) } + listOfNotNull(tabItem.icon as? IconType.Url) diff --git a/desktopApp/build.gradle.kts b/desktopApp/build.gradle.kts index b34ef4eb53..e7b06b28a6 100644 --- a/desktopApp/build.gradle.kts +++ b/desktopApp/build.gradle.kts @@ -63,7 +63,7 @@ val fdroidProp = Properties().apply { val desktopVersionName = System.getenv("BUILD_VERSION")?.takeIf { // match semantic versioning - Regex("""\d+\.\d+\.\d+(-\S+)?""").matches(it) + Regex("""\d+\.\d+\.\d+""").matches(it) } ?: fdroidProp.getProperty("versionName") ?: "1.0.0" val desktopVersionCode = System.getenv("BUILD_NUMBER")?.toIntOrNull() ?: fdroidProp.getProperty("versionCode")?.toIntOrNull() ?: 1 @@ -158,7 +158,7 @@ nucleus.application { } ?.sorted() ?: emptyList() - + val localizationsXml = (listOf("en") + locales).distinct() .joinToString("\n") { " $it" } diff --git a/desktopApp/src/main/composeResources/values-af-rZA/strings.xml b/desktopApp/src/main/composeResources/values-af-rZA/strings.xml index aa396d8584..87d09c1057 100644 --- a/desktopApp/src/main/composeResources/values-af-rZA/strings.xml +++ b/desktopApp/src/main/composeResources/values-af-rZA/strings.xml @@ -187,7 +187,7 @@ KI-tipe Kies watter KI-agterkant Flare gebruik KI op toestel - OpenAI-versoenbare API + KI-versoenbare API API-sleutel Voer API-sleutel in Model diff --git a/desktopApp/src/main/composeResources/values-ar-rSA/strings.xml b/desktopApp/src/main/composeResources/values-ar-rSA/strings.xml index 2175474e2f..d0668f11a4 100644 --- a/desktopApp/src/main/composeResources/values-ar-rSA/strings.xml +++ b/desktopApp/src/main/composeResources/values-ar-rSA/strings.xml @@ -191,7 +191,7 @@ نوع الذكاء الاصطناعي اختر محرك الذكاء الاصطناعي الذي يستخدمه Flare ذكاء اصطناعي على الجهاز - واجهة برمجة تطبيقات متوافقة مع OpenAI + واجهة API متوافقة مع الذكاء الاصطناعي مفتاح واجهة برمجة التطبيقات (API Key) أدخل مفتاح واجهة برمجة التطبيقات النموذج diff --git a/desktopApp/src/main/composeResources/values-bg-rBG/strings.xml b/desktopApp/src/main/composeResources/values-bg-rBG/strings.xml index c4d57aca8e..429909928a 100644 --- a/desktopApp/src/main/composeResources/values-bg-rBG/strings.xml +++ b/desktopApp/src/main/composeResources/values-bg-rBG/strings.xml @@ -187,7 +187,7 @@ Тип ИИ Изберете кой ИИ бекенд да използва Flare ИИ на устройството - OpenAI-съвместим API + API, съвместим с ИИ API ключ Въведете API ключ Модел diff --git a/desktopApp/src/main/composeResources/values-ca-rES/strings.xml b/desktopApp/src/main/composeResources/values-ca-rES/strings.xml index 08daf5e203..3f05801084 100644 --- a/desktopApp/src/main/composeResources/values-ca-rES/strings.xml +++ b/desktopApp/src/main/composeResources/values-ca-rES/strings.xml @@ -187,7 +187,7 @@ Tipus d\'IA Tria quin motor d\'IA utilitza Flare IA al dispositiu - API compatible amb OpenAI + API compatible amb IA Clau API Introdueix la clau API Model diff --git a/desktopApp/src/main/composeResources/values-cs-rCZ/strings.xml b/desktopApp/src/main/composeResources/values-cs-rCZ/strings.xml index 8de5978fa9..e14dfd54a2 100644 --- a/desktopApp/src/main/composeResources/values-cs-rCZ/strings.xml +++ b/desktopApp/src/main/composeResources/values-cs-rCZ/strings.xml @@ -189,7 +189,7 @@ Typ AI Vyberte, které AI rozhraní Flare používá AI v zařízení - API kompatibilní s OpenAI + API kompatibilní s AI API klíč Zadejte API klíč Model diff --git a/desktopApp/src/main/composeResources/values-da-rDK/strings.xml b/desktopApp/src/main/composeResources/values-da-rDK/strings.xml index aefd571567..91b2f1868e 100644 --- a/desktopApp/src/main/composeResources/values-da-rDK/strings.xml +++ b/desktopApp/src/main/composeResources/values-da-rDK/strings.xml @@ -187,7 +187,7 @@ AI-type Vælg hvilken AI-backend Flare bruger AI på enheden - OpenAI-kompatibel API + AI-kompatibel API API-nøgle Indtast API-nøgle Model diff --git a/desktopApp/src/main/composeResources/values-de-rDE/strings.xml b/desktopApp/src/main/composeResources/values-de-rDE/strings.xml index 8fb9274a1c..d3b7f693a8 100644 --- a/desktopApp/src/main/composeResources/values-de-rDE/strings.xml +++ b/desktopApp/src/main/composeResources/values-de-rDE/strings.xml @@ -187,7 +187,7 @@ KI-Typ Wählen Sie das KI-Backend für Flare On-Device KI - OpenAI-kompatible API + KI-kompatible API API-Schlüssel API-Schlüssel eingeben Modell diff --git a/desktopApp/src/main/composeResources/values-el-rGR/strings.xml b/desktopApp/src/main/composeResources/values-el-rGR/strings.xml index 4780234cc5..c4e4c0f33b 100644 --- a/desktopApp/src/main/composeResources/values-el-rGR/strings.xml +++ b/desktopApp/src/main/composeResources/values-el-rGR/strings.xml @@ -187,7 +187,7 @@ Τύπος AI Επιλέξτε ποιο σύστημα AI χρησιμοποιεί το Flare AI στη συσκευή - API συμβατό με OpenAI + API συμβατό με AI Κλειδί API Εισαγάγετε το κλειδί API Μοντέλο diff --git a/desktopApp/src/main/composeResources/values-es-rES/strings.xml b/desktopApp/src/main/composeResources/values-es-rES/strings.xml index 0c81996500..e5d646d48c 100644 --- a/desktopApp/src/main/composeResources/values-es-rES/strings.xml +++ b/desktopApp/src/main/composeResources/values-es-rES/strings.xml @@ -187,7 +187,7 @@ Tipo de IA Elige qué motor de IA utiliza Flare IA en el dispositivo - API compatible con OpenAI + API compatible con IA Clave API Introduce la clave API Modelo diff --git a/desktopApp/src/main/composeResources/values-fi-rFI/strings.xml b/desktopApp/src/main/composeResources/values-fi-rFI/strings.xml index 466702eb07..ad95650fc6 100644 --- a/desktopApp/src/main/composeResources/values-fi-rFI/strings.xml +++ b/desktopApp/src/main/composeResources/values-fi-rFI/strings.xml @@ -187,7 +187,7 @@ Tekoälyn tyyppi Valitse, mitä tekoälyalustaa Flare käyttää Laitteen oma tekoäly - OpenAI-yhteensopiva API + AI-yhteensopiva API API-avain Anna API-avain Malli diff --git a/desktopApp/src/main/composeResources/values-fr-rFR/strings.xml b/desktopApp/src/main/composeResources/values-fr-rFR/strings.xml index 0dcb5955a9..7cd67c9186 100644 --- a/desktopApp/src/main/composeResources/values-fr-rFR/strings.xml +++ b/desktopApp/src/main/composeResources/values-fr-rFR/strings.xml @@ -187,7 +187,7 @@ Type d\'IA Choisissez quel moteur d\'IA Flare utilise IA sur l\'appareil - API compatible OpenAI + API compatible IA Clé API Entrez la clé API Modèle diff --git a/desktopApp/src/main/composeResources/values-hu-rHU/strings.xml b/desktopApp/src/main/composeResources/values-hu-rHU/strings.xml index 7faf6b516e..e4ae189e01 100644 --- a/desktopApp/src/main/composeResources/values-hu-rHU/strings.xml +++ b/desktopApp/src/main/composeResources/values-hu-rHU/strings.xml @@ -187,7 +187,7 @@ MI típusa Válaszd ki, melyik MI hátteret használja a Flare Eszközön lévő MI - OpenAI-kompatibilis API + MI-kompatibilis API API kulcs Add meg az API kulcsot Modell diff --git a/desktopApp/src/main/composeResources/values-it-rIT/strings.xml b/desktopApp/src/main/composeResources/values-it-rIT/strings.xml index cc9f375b34..36f101caf6 100644 --- a/desktopApp/src/main/composeResources/values-it-rIT/strings.xml +++ b/desktopApp/src/main/composeResources/values-it-rIT/strings.xml @@ -187,7 +187,7 @@ Tipo di IA Scegli quale backend di IA deve utilizzare Flare IA sul dispositivo - API compatibile con OpenAI + API compatibile con IA Chiave API Inserisci la chiave API Modello diff --git a/desktopApp/src/main/composeResources/values-iw-rIL/strings.xml b/desktopApp/src/main/composeResources/values-iw-rIL/strings.xml index fec1ef0470..360d1e3946 100644 --- a/desktopApp/src/main/composeResources/values-iw-rIL/strings.xml +++ b/desktopApp/src/main/composeResources/values-iw-rIL/strings.xml @@ -187,7 +187,7 @@ סוג בינה מלאכותית בחרו באיזה מנוע בינה מלאכותית Flare ישתמש בינה מלאכותית על המכשיר - API תואם OpenAI + API תואם בינה מלאכותית מפתח API הזינו מפתח API מודל diff --git a/desktopApp/src/main/composeResources/values-ja-rJP/strings.xml b/desktopApp/src/main/composeResources/values-ja-rJP/strings.xml index 9f20d5236d..ce6b1ad5d8 100644 --- a/desktopApp/src/main/composeResources/values-ja-rJP/strings.xml +++ b/desktopApp/src/main/composeResources/values-ja-rJP/strings.xml @@ -187,7 +187,7 @@ AIタイプ Flareで使用するAIバックエンドを選択 デバイス上のAI - OpenAI互換API + AI互換API APIキー APIキーを入力 モデル diff --git a/desktopApp/src/main/composeResources/values-ko-rKR/strings.xml b/desktopApp/src/main/composeResources/values-ko-rKR/strings.xml index 5ff2ade272..9f77fb4fc8 100644 --- a/desktopApp/src/main/composeResources/values-ko-rKR/strings.xml +++ b/desktopApp/src/main/composeResources/values-ko-rKR/strings.xml @@ -168,7 +168,7 @@ AI 유형 Flare가 사용할 AI 백엔드 선택 온디바이스 AI - OpenAI 호환 API + AI 호환 API API 키 API 키 입력 모델 diff --git a/desktopApp/src/main/composeResources/values-nl-rNL/strings.xml b/desktopApp/src/main/composeResources/values-nl-rNL/strings.xml index c204ed1e19..e9928ec3b4 100644 --- a/desktopApp/src/main/composeResources/values-nl-rNL/strings.xml +++ b/desktopApp/src/main/composeResources/values-nl-rNL/strings.xml @@ -168,7 +168,7 @@ AI-type Kies welke AI-backend Flare gebruikt On-device AI - OpenAI-compatibele API + AI-compatibele API API-sleutel Voer API-sleutel in Model diff --git a/desktopApp/src/main/composeResources/values-no-rNO/strings.xml b/desktopApp/src/main/composeResources/values-no-rNO/strings.xml index ef92b95af3..f89127d10f 100644 --- a/desktopApp/src/main/composeResources/values-no-rNO/strings.xml +++ b/desktopApp/src/main/composeResources/values-no-rNO/strings.xml @@ -168,7 +168,7 @@ AI-type Velg hvilken AI-backend Flare skal bruke AI på enheten - OpenAI-kompatibel API + AI-kompatibel API API-nøkkel Skriv inn API-nøkkel Modell diff --git a/desktopApp/src/main/composeResources/values-pl-rPL/strings.xml b/desktopApp/src/main/composeResources/values-pl-rPL/strings.xml index b46fb0cc5b..e852a4bc43 100644 --- a/desktopApp/src/main/composeResources/values-pl-rPL/strings.xml +++ b/desktopApp/src/main/composeResources/values-pl-rPL/strings.xml @@ -170,7 +170,7 @@ Typ AI Wybierz, którego zaplecza AI używa Flare AI na urządzeniu - API kompatybilne z OpenAI + API zgodne z AI Klucz API Wprowadź klucz API Model diff --git a/desktopApp/src/main/composeResources/values-pt-rBR/strings.xml b/desktopApp/src/main/composeResources/values-pt-rBR/strings.xml index 6b810cb462..b022495aaf 100644 --- a/desktopApp/src/main/composeResources/values-pt-rBR/strings.xml +++ b/desktopApp/src/main/composeResources/values-pt-rBR/strings.xml @@ -168,7 +168,7 @@ Tipo de IA Escolha qual backend de IA o Flare usa IA no dispositivo - API compatível com OpenAI + API compatível com IA Chave de API Insira a chave da API Modelo diff --git a/desktopApp/src/main/composeResources/values-pt-rPT/strings.xml b/desktopApp/src/main/composeResources/values-pt-rPT/strings.xml index 53eb0fc07d..c5f97f1597 100644 --- a/desktopApp/src/main/composeResources/values-pt-rPT/strings.xml +++ b/desktopApp/src/main/composeResources/values-pt-rPT/strings.xml @@ -168,7 +168,7 @@ Tipo de IA Escolha qual backend de IA o Flare usa IA no dispositivo - API compatível com OpenAI + API compatível com IA Chave de API Insira a chave da API Modelo diff --git a/desktopApp/src/main/composeResources/values-ro-rRO/strings.xml b/desktopApp/src/main/composeResources/values-ro-rRO/strings.xml index cdf8b30477..9c96883854 100644 --- a/desktopApp/src/main/composeResources/values-ro-rRO/strings.xml +++ b/desktopApp/src/main/composeResources/values-ro-rRO/strings.xml @@ -169,7 +169,7 @@ Tip IA Alege ce backend de IA folosește Flare IA pe dispozitiv - API compatibil cu OpenAI + API compatibil cu AI Cheie API Introdu cheia API Model diff --git a/desktopApp/src/main/composeResources/values-ru-rRU/strings.xml b/desktopApp/src/main/composeResources/values-ru-rRU/strings.xml index 649fa0d019..37f3de9cbd 100644 --- a/desktopApp/src/main/composeResources/values-ru-rRU/strings.xml +++ b/desktopApp/src/main/composeResources/values-ru-rRU/strings.xml @@ -171,7 +171,7 @@ AI Type Нажмите, чтобы выбрать тип AI бэкэнда На устройстве ИИ - AI Compatible API + AI-совместимый API Ключ API Введите ключ API Модель diff --git a/desktopApp/src/main/composeResources/values-sr-rSP/strings.xml b/desktopApp/src/main/composeResources/values-sr-rSP/strings.xml index 80a3db4dc0..74c66d40f4 100644 --- a/desktopApp/src/main/composeResources/values-sr-rSP/strings.xml +++ b/desktopApp/src/main/composeResources/values-sr-rSP/strings.xml @@ -169,7 +169,7 @@ Tip AI Izaberite koji AI pozadinski sistem Flare koristi AI na uređaju - OpenAI-kompatibilan API + API компатибилан са ВИ API ključ Unesite API ključ Model diff --git a/desktopApp/src/main/composeResources/values-sv-rSE/strings.xml b/desktopApp/src/main/composeResources/values-sv-rSE/strings.xml index 67e2576fcd..57196e0bb7 100644 --- a/desktopApp/src/main/composeResources/values-sv-rSE/strings.xml +++ b/desktopApp/src/main/composeResources/values-sv-rSE/strings.xml @@ -168,7 +168,7 @@ AI-typ Välj vilken AI-backend Flare ska använda AI på enheten - OpenAI-kompatibelt API + AI-kompatibelt API API-nyckel Ange API-nyckel Modell diff --git a/desktopApp/src/main/composeResources/values-tr-rTR/strings.xml b/desktopApp/src/main/composeResources/values-tr-rTR/strings.xml index d605b6209b..7c7229877a 100644 --- a/desktopApp/src/main/composeResources/values-tr-rTR/strings.xml +++ b/desktopApp/src/main/composeResources/values-tr-rTR/strings.xml @@ -168,7 +168,7 @@ AI Türü Flare\'in hangi AI altyapısını kullanacağını seçin Cihaz üzerindeki AI - OpenAI uyumlu API + YZ uyumlu API API Anahtarı API anahtarını girin Model diff --git a/desktopApp/src/main/composeResources/values-uk-rUA/strings.xml b/desktopApp/src/main/composeResources/values-uk-rUA/strings.xml index 2faf1def99..bdbb66d88b 100644 --- a/desktopApp/src/main/composeResources/values-uk-rUA/strings.xml +++ b/desktopApp/src/main/composeResources/values-uk-rUA/strings.xml @@ -170,7 +170,7 @@ Тип ШІ Виберіть, який ШІ-бекенд використовує Flare ШІ на пристрої - OpenAI-сумісний API + AI-сумісний API Ключ API Введіть ключ API Модель diff --git a/desktopApp/src/main/composeResources/values-vi-rVN/strings.xml b/desktopApp/src/main/composeResources/values-vi-rVN/strings.xml index 8a23a8451a..7d620c97b3 100644 --- a/desktopApp/src/main/composeResources/values-vi-rVN/strings.xml +++ b/desktopApp/src/main/composeResources/values-vi-rVN/strings.xml @@ -168,7 +168,7 @@ Loại AI Chọn phần phụ trợ AI mà Flare sử dụng AI trên thiết bị - API tương thích với OpenAI + API tương thích AI Khóa API Nhập khóa API Mô hình diff --git a/desktopApp/src/main/composeResources/values-zh-rCN/strings.xml b/desktopApp/src/main/composeResources/values-zh-rCN/strings.xml index fe230a0624..39d8dca750 100644 --- a/desktopApp/src/main/composeResources/values-zh-rCN/strings.xml +++ b/desktopApp/src/main/composeResources/values-zh-rCN/strings.xml @@ -167,7 +167,7 @@ AI 类型 选择 Flare 使用的 AI 后端 设备端 AI - 兼容 OpenAI 的 API + AI 兼容 API API 密钥 输入 API 密钥 模型 diff --git a/desktopApp/src/main/composeResources/values-zh-rTW/strings.xml b/desktopApp/src/main/composeResources/values-zh-rTW/strings.xml index 45dd23f0bc..cc7a58b8b8 100644 --- a/desktopApp/src/main/composeResources/values-zh-rTW/strings.xml +++ b/desktopApp/src/main/composeResources/values-zh-rTW/strings.xml @@ -167,7 +167,7 @@ AI 類型 選擇 Flare 使用的 AI 後端 裝置端 AI - OpenAI 相容 API + AI 相容 API API 密鑰 輸入 API 密鑰 模型 diff --git a/desktopApp/src/main/composeResources/values/strings.xml b/desktopApp/src/main/composeResources/values/strings.xml index 5a537c79aa..47654cdbec 100644 --- a/desktopApp/src/main/composeResources/values/strings.xml +++ b/desktopApp/src/main/composeResources/values/strings.xml @@ -187,7 +187,7 @@ AI Type Choose which AI backend Flare uses On-device AI - OpenAI-compatible API + AI-compatible API API Key Enter API key Model diff --git a/desktopApp/src/main/kotlin/dev/dimension/flare/ui/screen/rss/RssDetailScreen.kt b/desktopApp/src/main/kotlin/dev/dimension/flare/ui/screen/rss/RssDetailScreen.kt index ebfca15e8f..cc426b6e4f 100644 --- a/desktopApp/src/main/kotlin/dev/dimension/flare/ui/screen/rss/RssDetailScreen.kt +++ b/desktopApp/src/main/kotlin/dev/dimension/flare/ui/screen/rss/RssDetailScreen.kt @@ -91,7 +91,10 @@ internal fun RssDetailScreen( val inAppNotification: ComposeInAppNotification = koinInject() val scrollState = rememberScrollState() androidx.compose.foundation.layout.Box( - modifier = Modifier.fillMaxSize(), + modifier = + Modifier + .fillMaxSize() + .background(FluentTheme.colors.background.card.default), contentAlignment = Alignment.TopCenter, ) { FlareScrollBar( diff --git a/fdroid.properties b/fdroid.properties index e3707884ea..abea0d7f26 100644 --- a/fdroid.properties +++ b/fdroid.properties @@ -1,2 +1,2 @@ -versionName=1.4.5 -versionCode=1450 +versionName=1.5.0 +versionCode=1500 diff --git a/iosApp/Flare.xcodeproj/project.pbxproj b/iosApp/Flare.xcodeproj/project.pbxproj index de18cd6204..103d966f3b 100644 --- a/iosApp/Flare.xcodeproj/project.pbxproj +++ b/iosApp/Flare.xcodeproj/project.pbxproj @@ -330,11 +330,12 @@ 06E4340A2E6A9A2700CD0826 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "AppIcon_black AppIcon_blue AppIcon_cyan AppIcon_light_blue AppIcon_orange AppIcon_red AppIcon_teal AppIcon_white AppIcon_yellow"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1450; + CURRENT_PROJECT_VERSION = 1501; DEVELOPMENT_TEAM = 7LFDZ96332; ENABLE_PREVIEWS = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -346,7 +347,7 @@ INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Flare requesting the ability to add to the photo library"; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UILaunchStoryboardName = SplashScreen; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; IPHONEOS_DEPLOYMENT_TARGET = 17.0; @@ -354,7 +355,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.4.5; + MARKETING_VERSION = 1.5.0; PRODUCT_BUNDLE_IDENTIFIER = dev.dimension.flare; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = YES; @@ -370,11 +371,12 @@ 06E4340B2E6A9A2700CD0826 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "AppIcon_black AppIcon_blue AppIcon_cyan AppIcon_light_blue AppIcon_orange AppIcon_red AppIcon_teal AppIcon_white AppIcon_yellow"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1450; + CURRENT_PROJECT_VERSION = 1501; DEVELOPMENT_TEAM = 7LFDZ96332; ENABLE_PREVIEWS = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -386,7 +388,7 @@ INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Flare requesting the ability to add to the photo library"; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UILaunchStoryboardName = SplashScreen; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; IPHONEOS_DEPLOYMENT_TARGET = 17.0; @@ -394,7 +396,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.4.5; + MARKETING_VERSION = 1.5.0; PRODUCT_BUNDLE_IDENTIFIER = dev.dimension.flare; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = YES; diff --git a/iosApp/flare/AppIcon.icon/icon.json b/iosApp/flare/AppIcon.icon/icon.json index 3844b2efbf..f94fe0e12a 100644 --- a/iosApp/flare/AppIcon.icon/icon.json +++ b/iosApp/flare/AppIcon.icon/icon.json @@ -7,7 +7,7 @@ "blur-material" : 0.5, "layers" : [ { - "glass" : false, + "glass" : true, "hidden-specializations" : [ { "value" : true @@ -30,7 +30,7 @@ { "blend-mode" : "normal", "fill" : "automatic", - "glass" : false, + "glass" : true, "hidden-specializations" : [ { "value" : false diff --git a/iosApp/flare/AppIcon_black.icon/Assets/Group-2.svg b/iosApp/flare/AppIcon_black.icon/Assets/Group-2.svg new file mode 100644 index 0000000000..983932a98f --- /dev/null +++ b/iosApp/flare/AppIcon_black.icon/Assets/Group-2.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iosApp/flare/AppIcon_black.icon/Assets/Group.svg b/iosApp/flare/AppIcon_black.icon/Assets/Group.svg new file mode 100644 index 0000000000..ad2f3e9a94 --- /dev/null +++ b/iosApp/flare/AppIcon_black.icon/Assets/Group.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iosApp/flare/AppIcon_black.icon/icon.json b/iosApp/flare/AppIcon_black.icon/icon.json new file mode 100644 index 0000000000..2c5f403931 --- /dev/null +++ b/iosApp/flare/AppIcon_black.icon/icon.json @@ -0,0 +1,72 @@ +{ + "fill" : { + "automatic-gradient" : "extended-gray:0.00000,1.00000" + }, + "groups" : [ + { + "blur-material" : 0.5, + "layers" : [ + { + "blend-mode" : "normal", + "fill" : { + "solid" : "extended-gray:1.00000,1.00000" + }, + "glass" : true, + "hidden-specializations" : [ + { + "value" : false + }, + { + "appearance" : "dark", + "value" : false + } + ], + "image-name" : "Group.svg", + "name" : "Group", + "position" : { + "scale" : 3.18, + "translation-in-points" : [ + 0, + 0 + ] + } + }, + { + "blend-mode" : "normal", + "fill" : "automatic", + "glass" : false, + "hidden-specializations" : [ + { + "value" : true + }, + { + "appearance" : "dark", + "value" : true + } + ], + "image-name" : "Group-2.svg", + "name" : "Group-2", + "position" : { + "scale" : 3.18, + "translation-in-points" : [ + 0, + 0 + ] + } + } + ], + "lighting" : "individual", + "shadow" : { + "kind" : "neutral", + "opacity" : 0.5 + }, + "translucency" : { + "enabled" : true, + "value" : 0.5 + } + } + ], + "supported-platforms" : { + "squares" : "shared" + } +} \ No newline at end of file diff --git a/iosApp/flare/AppIcon_blue.icon/Assets/Group-2.svg b/iosApp/flare/AppIcon_blue.icon/Assets/Group-2.svg new file mode 100644 index 0000000000..983932a98f --- /dev/null +++ b/iosApp/flare/AppIcon_blue.icon/Assets/Group-2.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iosApp/flare/AppIcon_blue.icon/Assets/Group.svg b/iosApp/flare/AppIcon_blue.icon/Assets/Group.svg new file mode 100644 index 0000000000..ad2f3e9a94 --- /dev/null +++ b/iosApp/flare/AppIcon_blue.icon/Assets/Group.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iosApp/flare/AppIcon_blue.icon/icon.json b/iosApp/flare/AppIcon_blue.icon/icon.json new file mode 100644 index 0000000000..57488e18ba --- /dev/null +++ b/iosApp/flare/AppIcon_blue.icon/icon.json @@ -0,0 +1,72 @@ +{ + "fill" : { + "automatic-gradient" : "extended-srgb:0.00000,0.53333,1.00000,1.00000" + }, + "groups" : [ + { + "blur-material" : 0.5, + "layers" : [ + { + "blend-mode" : "normal", + "fill" : { + "solid" : "extended-gray:1.00000,1.00000" + }, + "glass" : true, + "hidden-specializations" : [ + { + "value" : false + }, + { + "appearance" : "dark", + "value" : false + } + ], + "image-name" : "Group.svg", + "name" : "Group", + "position" : { + "scale" : 3.18, + "translation-in-points" : [ + 0, + 0 + ] + } + }, + { + "blend-mode" : "normal", + "fill" : "automatic", + "glass" : false, + "hidden-specializations" : [ + { + "value" : true + }, + { + "appearance" : "dark", + "value" : true + } + ], + "image-name" : "Group-2.svg", + "name" : "Group-2", + "position" : { + "scale" : 3.18, + "translation-in-points" : [ + 0, + 0 + ] + } + } + ], + "lighting" : "individual", + "shadow" : { + "kind" : "neutral", + "opacity" : 0.5 + }, + "translucency" : { + "enabled" : true, + "value" : 0.5 + } + } + ], + "supported-platforms" : { + "squares" : "shared" + } +} \ No newline at end of file diff --git a/iosApp/flare/AppIcon_cyan.icon/Assets/Group-2.svg b/iosApp/flare/AppIcon_cyan.icon/Assets/Group-2.svg new file mode 100644 index 0000000000..983932a98f --- /dev/null +++ b/iosApp/flare/AppIcon_cyan.icon/Assets/Group-2.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iosApp/flare/AppIcon_cyan.icon/Assets/Group.svg b/iosApp/flare/AppIcon_cyan.icon/Assets/Group.svg new file mode 100644 index 0000000000..ad2f3e9a94 --- /dev/null +++ b/iosApp/flare/AppIcon_cyan.icon/Assets/Group.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iosApp/flare/AppIcon_cyan.icon/icon.json b/iosApp/flare/AppIcon_cyan.icon/icon.json new file mode 100644 index 0000000000..68141ea3a6 --- /dev/null +++ b/iosApp/flare/AppIcon_cyan.icon/icon.json @@ -0,0 +1,72 @@ +{ + "fill" : { + "automatic-gradient" : "extended-srgb:0.00000,0.76471,0.81569,1.00000" + }, + "groups" : [ + { + "blur-material" : 0.5, + "layers" : [ + { + "blend-mode" : "normal", + "fill" : { + "solid" : "extended-gray:1.00000,1.00000" + }, + "glass" : true, + "hidden-specializations" : [ + { + "value" : false + }, + { + "appearance" : "dark", + "value" : false + } + ], + "image-name" : "Group.svg", + "name" : "Group", + "position" : { + "scale" : 3.18, + "translation-in-points" : [ + 0, + 0 + ] + } + }, + { + "blend-mode" : "normal", + "fill" : "automatic", + "glass" : false, + "hidden-specializations" : [ + { + "value" : true + }, + { + "appearance" : "dark", + "value" : true + } + ], + "image-name" : "Group-2.svg", + "name" : "Group-2", + "position" : { + "scale" : 3.18, + "translation-in-points" : [ + 0, + 0 + ] + } + } + ], + "lighting" : "individual", + "shadow" : { + "kind" : "neutral", + "opacity" : 0.5 + }, + "translucency" : { + "enabled" : true, + "value" : 0.5 + } + } + ], + "supported-platforms" : { + "squares" : "shared" + } +} \ No newline at end of file diff --git a/iosApp/flare/AppIcon_light_blue.icon/Assets/Group-2.svg b/iosApp/flare/AppIcon_light_blue.icon/Assets/Group-2.svg new file mode 100644 index 0000000000..983932a98f --- /dev/null +++ b/iosApp/flare/AppIcon_light_blue.icon/Assets/Group-2.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iosApp/flare/AppIcon_light_blue.icon/Assets/Group.svg b/iosApp/flare/AppIcon_light_blue.icon/Assets/Group.svg new file mode 100644 index 0000000000..ad2f3e9a94 --- /dev/null +++ b/iosApp/flare/AppIcon_light_blue.icon/Assets/Group.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iosApp/flare/AppIcon_light_blue.icon/icon.json b/iosApp/flare/AppIcon_light_blue.icon/icon.json new file mode 100644 index 0000000000..ac797a4130 --- /dev/null +++ b/iosApp/flare/AppIcon_light_blue.icon/icon.json @@ -0,0 +1,72 @@ +{ + "fill" : { + "automatic-gradient" : "extended-srgb:0.00000,0.75294,0.90980,1.00000" + }, + "groups" : [ + { + "blur-material" : 0.5, + "layers" : [ + { + "blend-mode" : "normal", + "fill" : { + "solid" : "extended-gray:1.00000,1.00000" + }, + "glass" : true, + "hidden-specializations" : [ + { + "value" : false + }, + { + "appearance" : "dark", + "value" : false + } + ], + "image-name" : "Group.svg", + "name" : "Group", + "position" : { + "scale" : 3.18, + "translation-in-points" : [ + 0, + 0 + ] + } + }, + { + "blend-mode" : "normal", + "fill" : "automatic", + "glass" : false, + "hidden-specializations" : [ + { + "value" : true + }, + { + "appearance" : "dark", + "value" : true + } + ], + "image-name" : "Group-2.svg", + "name" : "Group-2", + "position" : { + "scale" : 3.18, + "translation-in-points" : [ + 0, + 0 + ] + } + } + ], + "lighting" : "individual", + "shadow" : { + "kind" : "neutral", + "opacity" : 0.5 + }, + "translucency" : { + "enabled" : true, + "value" : 0.5 + } + } + ], + "supported-platforms" : { + "squares" : "shared" + } +} \ No newline at end of file diff --git a/iosApp/flare/AppIcon_orange.icon/Assets/Group-2.svg b/iosApp/flare/AppIcon_orange.icon/Assets/Group-2.svg new file mode 100644 index 0000000000..983932a98f --- /dev/null +++ b/iosApp/flare/AppIcon_orange.icon/Assets/Group-2.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iosApp/flare/AppIcon_orange.icon/Assets/Group.svg b/iosApp/flare/AppIcon_orange.icon/Assets/Group.svg new file mode 100644 index 0000000000..ad2f3e9a94 --- /dev/null +++ b/iosApp/flare/AppIcon_orange.icon/Assets/Group.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iosApp/flare/AppIcon_orange.icon/icon.json b/iosApp/flare/AppIcon_orange.icon/icon.json new file mode 100644 index 0000000000..7813eb3805 --- /dev/null +++ b/iosApp/flare/AppIcon_orange.icon/icon.json @@ -0,0 +1,72 @@ +{ + "fill" : { + "automatic-gradient" : "extended-srgb:1.00000,0.55294,0.15686,1.00000" + }, + "groups" : [ + { + "blur-material" : 0.5, + "layers" : [ + { + "blend-mode" : "normal", + "fill" : { + "solid" : "extended-gray:1.00000,1.00000" + }, + "glass" : true, + "hidden-specializations" : [ + { + "value" : false + }, + { + "appearance" : "dark", + "value" : false + } + ], + "image-name" : "Group.svg", + "name" : "Group", + "position" : { + "scale" : 3.18, + "translation-in-points" : [ + 0, + 0 + ] + } + }, + { + "blend-mode" : "normal", + "fill" : "automatic", + "glass" : false, + "hidden-specializations" : [ + { + "value" : true + }, + { + "appearance" : "dark", + "value" : true + } + ], + "image-name" : "Group-2.svg", + "name" : "Group-2", + "position" : { + "scale" : 3.18, + "translation-in-points" : [ + 0, + 0 + ] + } + } + ], + "lighting" : "individual", + "shadow" : { + "kind" : "neutral", + "opacity" : 0.5 + }, + "translucency" : { + "enabled" : true, + "value" : 0.5 + } + } + ], + "supported-platforms" : { + "squares" : "shared" + } +} \ No newline at end of file diff --git a/iosApp/flare/AppIcon_red.icon/Assets/Group-2.svg b/iosApp/flare/AppIcon_red.icon/Assets/Group-2.svg new file mode 100644 index 0000000000..983932a98f --- /dev/null +++ b/iosApp/flare/AppIcon_red.icon/Assets/Group-2.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iosApp/flare/AppIcon_red.icon/Assets/Group.svg b/iosApp/flare/AppIcon_red.icon/Assets/Group.svg new file mode 100644 index 0000000000..ad2f3e9a94 --- /dev/null +++ b/iosApp/flare/AppIcon_red.icon/Assets/Group.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iosApp/flare/AppIcon_red.icon/icon.json b/iosApp/flare/AppIcon_red.icon/icon.json new file mode 100644 index 0000000000..f2a87f88e1 --- /dev/null +++ b/iosApp/flare/AppIcon_red.icon/icon.json @@ -0,0 +1,72 @@ +{ + "fill" : { + "automatic-gradient" : "extended-srgb:1.00000,0.21961,0.23529,1.00000" + }, + "groups" : [ + { + "blur-material" : 0.5, + "layers" : [ + { + "blend-mode" : "normal", + "fill" : { + "solid" : "extended-gray:1.00000,1.00000" + }, + "glass" : true, + "hidden-specializations" : [ + { + "value" : false + }, + { + "appearance" : "dark", + "value" : false + } + ], + "image-name" : "Group.svg", + "name" : "Group", + "position" : { + "scale" : 3.18, + "translation-in-points" : [ + 0, + 0 + ] + } + }, + { + "blend-mode" : "normal", + "fill" : "automatic", + "glass" : false, + "hidden-specializations" : [ + { + "value" : true + }, + { + "appearance" : "dark", + "value" : true + } + ], + "image-name" : "Group-2.svg", + "name" : "Group-2", + "position" : { + "scale" : 3.18, + "translation-in-points" : [ + 0, + 0 + ] + } + } + ], + "lighting" : "individual", + "shadow" : { + "kind" : "neutral", + "opacity" : 0.5 + }, + "translucency" : { + "enabled" : true, + "value" : 0.5 + } + } + ], + "supported-platforms" : { + "squares" : "shared" + } +} \ No newline at end of file diff --git a/iosApp/flare/AppIcon_teal.icon/Assets/Group-2.svg b/iosApp/flare/AppIcon_teal.icon/Assets/Group-2.svg new file mode 100644 index 0000000000..983932a98f --- /dev/null +++ b/iosApp/flare/AppIcon_teal.icon/Assets/Group-2.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iosApp/flare/AppIcon_teal.icon/Assets/Group.svg b/iosApp/flare/AppIcon_teal.icon/Assets/Group.svg new file mode 100644 index 0000000000..ad2f3e9a94 --- /dev/null +++ b/iosApp/flare/AppIcon_teal.icon/Assets/Group.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iosApp/flare/AppIcon_teal.icon/icon.json b/iosApp/flare/AppIcon_teal.icon/icon.json new file mode 100644 index 0000000000..afd2639a75 --- /dev/null +++ b/iosApp/flare/AppIcon_teal.icon/icon.json @@ -0,0 +1,72 @@ +{ + "fill" : { + "automatic-gradient" : "extended-srgb:0.38039,0.33333,0.96078,1.00000" + }, + "groups" : [ + { + "blur-material" : 0.5, + "layers" : [ + { + "blend-mode" : "normal", + "fill" : { + "solid" : "extended-gray:1.00000,1.00000" + }, + "glass" : true, + "hidden-specializations" : [ + { + "value" : false + }, + { + "appearance" : "dark", + "value" : false + } + ], + "image-name" : "Group.svg", + "name" : "Group", + "position" : { + "scale" : 3.18, + "translation-in-points" : [ + 0, + 0 + ] + } + }, + { + "blend-mode" : "normal", + "fill" : "automatic", + "glass" : false, + "hidden-specializations" : [ + { + "value" : true + }, + { + "appearance" : "dark", + "value" : true + } + ], + "image-name" : "Group-2.svg", + "name" : "Group-2", + "position" : { + "scale" : 3.18, + "translation-in-points" : [ + 0, + 0 + ] + } + } + ], + "lighting" : "individual", + "shadow" : { + "kind" : "neutral", + "opacity" : 0.5 + }, + "translucency" : { + "enabled" : true, + "value" : 0.5 + } + } + ], + "supported-platforms" : { + "squares" : "shared" + } +} \ No newline at end of file diff --git a/iosApp/flare/AppIcon_white.icon/Assets/Group-2.svg b/iosApp/flare/AppIcon_white.icon/Assets/Group-2.svg new file mode 100644 index 0000000000..983932a98f --- /dev/null +++ b/iosApp/flare/AppIcon_white.icon/Assets/Group-2.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iosApp/flare/AppIcon_white.icon/Assets/Group.svg b/iosApp/flare/AppIcon_white.icon/Assets/Group.svg new file mode 100644 index 0000000000..ad2f3e9a94 --- /dev/null +++ b/iosApp/flare/AppIcon_white.icon/Assets/Group.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iosApp/flare/AppIcon_white.icon/icon.json b/iosApp/flare/AppIcon_white.icon/icon.json new file mode 100644 index 0000000000..d481480da2 --- /dev/null +++ b/iosApp/flare/AppIcon_white.icon/icon.json @@ -0,0 +1,82 @@ +{ + "fill" : { + "automatic-gradient" : "extended-gray:1.00000,1.00000" + }, + "groups" : [ + { + "blur-material" : 0.5, + "layers" : [ + { + "blend-mode" : "normal", + "fill-specializations" : [ + { + "value" : { + "solid" : "extended-gray:0.00000,1.00000" + } + }, + { + "appearance" : "dark", + "value" : { + "solid" : "extended-gray:1.00000,1.00000" + } + } + ], + "glass" : true, + "hidden-specializations" : [ + { + "value" : false + }, + { + "appearance" : "dark", + "value" : false + } + ], + "image-name" : "Group.svg", + "name" : "Group", + "position" : { + "scale" : 3.18, + "translation-in-points" : [ + 0, + 0 + ] + } + }, + { + "blend-mode" : "normal", + "fill" : "automatic", + "glass" : false, + "hidden-specializations" : [ + { + "value" : true + }, + { + "appearance" : "dark", + "value" : true + } + ], + "image-name" : "Group-2.svg", + "name" : "Group-2", + "position" : { + "scale" : 3.18, + "translation-in-points" : [ + 0, + 0 + ] + } + } + ], + "lighting" : "individual", + "shadow" : { + "kind" : "neutral", + "opacity" : 0.5 + }, + "translucency" : { + "enabled" : true, + "value" : 0.5 + } + } + ], + "supported-platforms" : { + "squares" : "shared" + } +} \ No newline at end of file diff --git a/iosApp/flare/AppIcon_yellow.icon/Assets/Group-2.svg b/iosApp/flare/AppIcon_yellow.icon/Assets/Group-2.svg new file mode 100644 index 0000000000..983932a98f --- /dev/null +++ b/iosApp/flare/AppIcon_yellow.icon/Assets/Group-2.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iosApp/flare/AppIcon_yellow.icon/Assets/Group.svg b/iosApp/flare/AppIcon_yellow.icon/Assets/Group.svg new file mode 100644 index 0000000000..ad2f3e9a94 --- /dev/null +++ b/iosApp/flare/AppIcon_yellow.icon/Assets/Group.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iosApp/flare/AppIcon_yellow.icon/icon.json b/iosApp/flare/AppIcon_yellow.icon/icon.json new file mode 100644 index 0000000000..947b060d88 --- /dev/null +++ b/iosApp/flare/AppIcon_yellow.icon/icon.json @@ -0,0 +1,92 @@ +{ + "fill" : { + "automatic-gradient" : "display-p3:0.98933,0.90944,0.43580,1.00000", + "orientation" : { + "start" : { + "x" : 0.5, + "y" : 1 + }, + "stop" : { + "x" : 0.5, + "y" : 0.3 + } + } + }, + "groups" : [ + { + "blur-material" : 0.5, + "layers" : [ + { + "blend-mode" : "normal", + "fill-specializations" : [ + { + "value" : { + "solid" : "extended-gray:0.00000,1.00000" + } + }, + { + "appearance" : "dark", + "value" : { + "solid" : "display-p3:0.98933,0.90944,0.43580,1.00000" + } + } + ], + "glass" : true, + "hidden-specializations" : [ + { + "value" : false + }, + { + "appearance" : "dark", + "value" : false + } + ], + "image-name" : "Group.svg", + "name" : "Group", + "position" : { + "scale" : 3.18, + "translation-in-points" : [ + 0, + 0 + ] + } + }, + { + "blend-mode" : "normal", + "fill" : "automatic", + "glass" : false, + "hidden-specializations" : [ + { + "value" : true + }, + { + "appearance" : "dark", + "value" : true + } + ], + "image-name" : "Group-2.svg", + "name" : "Group-2", + "position" : { + "scale" : 3.18, + "translation-in-points" : [ + 0, + 0 + ] + } + } + ], + "lighting" : "individual", + "shadow" : { + "kind" : "neutral", + "opacity" : 0.5 + }, + "translucency" : { + "enabled" : true, + "value" : 0.5 + } + } + ], + "supported-platforms" : { + "squares" : "shared" + } +} \ No newline at end of file diff --git a/iosApp/flare/Assets.xcassets/AppIcon.appiconset/Contents.json b/iosApp/flare/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 2305880107..0000000000 --- a/iosApp/flare/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "platform" : "ios", - "size" : "1024x1024" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "idiom" : "universal", - "platform" : "ios", - "size" : "1024x1024" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "tinted" - } - ], - "idiom" : "universal", - "platform" : "ios", - "size" : "1024x1024" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/iosApp/flare/Assets.xcassets/app_icon_preview_black.imageset/Contents.json b/iosApp/flare/Assets.xcassets/app_icon_preview_black.imageset/Contents.json new file mode 100644 index 0000000000..139e7663c3 --- /dev/null +++ b/iosApp/flare/Assets.xcassets/app_icon_preview_black.imageset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "filename" : "app_icon_preview_black.png", + "idiom" : "universal", + "scale" : "1x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iosApp/flare/Assets.xcassets/app_icon_preview_black.imageset/app_icon_preview_black.png b/iosApp/flare/Assets.xcassets/app_icon_preview_black.imageset/app_icon_preview_black.png new file mode 100644 index 0000000000..f8a2a1e2bc Binary files /dev/null and b/iosApp/flare/Assets.xcassets/app_icon_preview_black.imageset/app_icon_preview_black.png differ diff --git a/iosApp/flare/Assets.xcassets/app_icon_preview_blue.imageset/Contents.json b/iosApp/flare/Assets.xcassets/app_icon_preview_blue.imageset/Contents.json new file mode 100644 index 0000000000..7f9599fa5b --- /dev/null +++ b/iosApp/flare/Assets.xcassets/app_icon_preview_blue.imageset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "filename" : "app_icon_preview_blue.png", + "idiom" : "universal", + "scale" : "1x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iosApp/flare/Assets.xcassets/app_icon_preview_blue.imageset/app_icon_preview_blue.png b/iosApp/flare/Assets.xcassets/app_icon_preview_blue.imageset/app_icon_preview_blue.png new file mode 100644 index 0000000000..11ff6c7911 Binary files /dev/null and b/iosApp/flare/Assets.xcassets/app_icon_preview_blue.imageset/app_icon_preview_blue.png differ diff --git a/iosApp/flare/Assets.xcassets/app_icon_preview_cyan.imageset/Contents.json b/iosApp/flare/Assets.xcassets/app_icon_preview_cyan.imageset/Contents.json new file mode 100644 index 0000000000..e48446fda2 --- /dev/null +++ b/iosApp/flare/Assets.xcassets/app_icon_preview_cyan.imageset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "filename" : "app_icon_preview_cyan.png", + "idiom" : "universal", + "scale" : "1x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iosApp/flare/Assets.xcassets/app_icon_preview_cyan.imageset/app_icon_preview_cyan.png b/iosApp/flare/Assets.xcassets/app_icon_preview_cyan.imageset/app_icon_preview_cyan.png new file mode 100644 index 0000000000..f1475368f2 Binary files /dev/null and b/iosApp/flare/Assets.xcassets/app_icon_preview_cyan.imageset/app_icon_preview_cyan.png differ diff --git a/iosApp/flare/Assets.xcassets/app_icon_preview_default.imageset/Contents.json b/iosApp/flare/Assets.xcassets/app_icon_preview_default.imageset/Contents.json new file mode 100644 index 0000000000..78da82fb0b --- /dev/null +++ b/iosApp/flare/Assets.xcassets/app_icon_preview_default.imageset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "filename" : "app_icon_preview_default.png", + "idiom" : "universal", + "scale" : "1x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iosApp/flare/Assets.xcassets/app_icon_preview_default.imageset/app_icon_preview_default.png b/iosApp/flare/Assets.xcassets/app_icon_preview_default.imageset/app_icon_preview_default.png new file mode 100644 index 0000000000..571dba1841 Binary files /dev/null and b/iosApp/flare/Assets.xcassets/app_icon_preview_default.imageset/app_icon_preview_default.png differ diff --git a/iosApp/flare/Assets.xcassets/app_icon_preview_light_blue.imageset/Contents.json b/iosApp/flare/Assets.xcassets/app_icon_preview_light_blue.imageset/Contents.json new file mode 100644 index 0000000000..eae19404cf --- /dev/null +++ b/iosApp/flare/Assets.xcassets/app_icon_preview_light_blue.imageset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "filename" : "app_icon_preview_light_blue.png", + "idiom" : "universal", + "scale" : "1x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iosApp/flare/Assets.xcassets/app_icon_preview_light_blue.imageset/app_icon_preview_light_blue.png b/iosApp/flare/Assets.xcassets/app_icon_preview_light_blue.imageset/app_icon_preview_light_blue.png new file mode 100644 index 0000000000..4dac0ed2d8 Binary files /dev/null and b/iosApp/flare/Assets.xcassets/app_icon_preview_light_blue.imageset/app_icon_preview_light_blue.png differ diff --git a/iosApp/flare/Assets.xcassets/app_icon_preview_orange.imageset/Contents.json b/iosApp/flare/Assets.xcassets/app_icon_preview_orange.imageset/Contents.json new file mode 100644 index 0000000000..f445dd0234 --- /dev/null +++ b/iosApp/flare/Assets.xcassets/app_icon_preview_orange.imageset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "filename" : "app_icon_preview_orange.png", + "idiom" : "universal", + "scale" : "1x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iosApp/flare/Assets.xcassets/app_icon_preview_orange.imageset/app_icon_preview_orange.png b/iosApp/flare/Assets.xcassets/app_icon_preview_orange.imageset/app_icon_preview_orange.png new file mode 100644 index 0000000000..9f3931eb52 Binary files /dev/null and b/iosApp/flare/Assets.xcassets/app_icon_preview_orange.imageset/app_icon_preview_orange.png differ diff --git a/iosApp/flare/Assets.xcassets/app_icon_preview_red.imageset/Contents.json b/iosApp/flare/Assets.xcassets/app_icon_preview_red.imageset/Contents.json new file mode 100644 index 0000000000..0433d567b4 --- /dev/null +++ b/iosApp/flare/Assets.xcassets/app_icon_preview_red.imageset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "filename" : "app_icon_preview_red.png", + "idiom" : "universal", + "scale" : "1x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iosApp/flare/Assets.xcassets/app_icon_preview_red.imageset/app_icon_preview_red.png b/iosApp/flare/Assets.xcassets/app_icon_preview_red.imageset/app_icon_preview_red.png new file mode 100644 index 0000000000..9088880a4a Binary files /dev/null and b/iosApp/flare/Assets.xcassets/app_icon_preview_red.imageset/app_icon_preview_red.png differ diff --git a/iosApp/flare/Assets.xcassets/app_icon_preview_teal.imageset/Contents.json b/iosApp/flare/Assets.xcassets/app_icon_preview_teal.imageset/Contents.json new file mode 100644 index 0000000000..58e3b362b2 --- /dev/null +++ b/iosApp/flare/Assets.xcassets/app_icon_preview_teal.imageset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "filename" : "app_icon_preview_teal.png", + "idiom" : "universal", + "scale" : "1x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iosApp/flare/Assets.xcassets/app_icon_preview_teal.imageset/app_icon_preview_teal.png b/iosApp/flare/Assets.xcassets/app_icon_preview_teal.imageset/app_icon_preview_teal.png new file mode 100644 index 0000000000..a1e921ad45 Binary files /dev/null and b/iosApp/flare/Assets.xcassets/app_icon_preview_teal.imageset/app_icon_preview_teal.png differ diff --git a/iosApp/flare/Assets.xcassets/app_icon_preview_white.imageset/Contents.json b/iosApp/flare/Assets.xcassets/app_icon_preview_white.imageset/Contents.json new file mode 100644 index 0000000000..c24431238c --- /dev/null +++ b/iosApp/flare/Assets.xcassets/app_icon_preview_white.imageset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "filename" : "app_icon_preview_white.png", + "idiom" : "universal", + "scale" : "1x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iosApp/flare/Assets.xcassets/app_icon_preview_white.imageset/app_icon_preview_white.png b/iosApp/flare/Assets.xcassets/app_icon_preview_white.imageset/app_icon_preview_white.png new file mode 100644 index 0000000000..b0c2834e43 Binary files /dev/null and b/iosApp/flare/Assets.xcassets/app_icon_preview_white.imageset/app_icon_preview_white.png differ diff --git a/iosApp/flare/Assets.xcassets/app_icon_preview_yellow.imageset/Contents.json b/iosApp/flare/Assets.xcassets/app_icon_preview_yellow.imageset/Contents.json new file mode 100644 index 0000000000..19c5f52192 --- /dev/null +++ b/iosApp/flare/Assets.xcassets/app_icon_preview_yellow.imageset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "filename" : "app_icon_preview_yellow.png", + "idiom" : "universal", + "scale" : "1x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iosApp/flare/Assets.xcassets/app_icon_preview_yellow.imageset/app_icon_preview_yellow.png b/iosApp/flare/Assets.xcassets/app_icon_preview_yellow.imageset/app_icon_preview_yellow.png new file mode 100644 index 0000000000..83f7c61459 Binary files /dev/null and b/iosApp/flare/Assets.xcassets/app_icon_preview_yellow.imageset/app_icon_preview_yellow.png differ diff --git a/iosApp/flare/Assets.xcassets/flare_logo.imageset/Contents.json b/iosApp/flare/Assets.xcassets/flare_logo.imageset/Contents.json deleted file mode 100644 index 190edd77ef..0000000000 --- a/iosApp/flare/Assets.xcassets/flare_logo.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "filename" : "flare_logo.svg", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/iosApp/flare/Assets.xcassets/flare_logo.imageset/flare_logo.svg b/iosApp/flare/Assets.xcassets/flare_logo.imageset/flare_logo.svg deleted file mode 100644 index 03f7832157..0000000000 --- a/iosApp/flare/Assets.xcassets/flare_logo.imageset/flare_logo.svg +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/iosApp/flare/Common/AppIconOption.swift b/iosApp/flare/Common/AppIconOption.swift new file mode 100644 index 0000000000..b1aa9ae8d0 --- /dev/null +++ b/iosApp/flare/Common/AppIconOption.swift @@ -0,0 +1,30 @@ +import Foundation + +struct AppIconOption: Identifiable { + let title: String + let alternateIconName: String? + let previewImageName: String + + var id: String { + alternateIconName ?? "AppIcon" + } +} + +extension AppIconOption { + static let all: [AppIconOption] = [ + .init(title: "Black", alternateIconName: "AppIcon_black", previewImageName: "app_icon_preview_black"), + .init(title: "Blue", alternateIconName: "AppIcon_blue", previewImageName: "app_icon_preview_blue"), + .init(title: "Cyan", alternateIconName: "AppIcon_cyan", previewImageName: "app_icon_preview_cyan"), + .init(title: "Light Blue", alternateIconName: "AppIcon_light_blue", previewImageName: "app_icon_preview_light_blue"), + .init(title: "Orange", alternateIconName: "AppIcon_orange", previewImageName: "app_icon_preview_orange"), + .init(title: "Red", alternateIconName: "AppIcon_red", previewImageName: "app_icon_preview_red"), + .init(title: "Teal", alternateIconName: "AppIcon_teal", previewImageName: "app_icon_preview_teal"), + .init(title: "White", alternateIconName: "AppIcon_white", previewImageName: "app_icon_preview_white"), + .init(title: "Yellow", alternateIconName: "AppIcon_yellow", previewImageName: "app_icon_preview_yellow"), + .init(title: "Default", alternateIconName: nil, previewImageName: "app_icon_preview_default"), + ] + + static func previewImageName(for alternateIconName: String?) -> String { + all.first { $0.alternateIconName == alternateIconName }?.previewImageName ?? "app_icon_preview_default" + } +} diff --git a/iosApp/flare/Localizable.xcstrings b/iosApp/flare/Localizable.xcstrings index 778ada160a..ad4e5fc675 100644 --- a/iosApp/flare/Localizable.xcstrings +++ b/iosApp/flare/Localizable.xcstrings @@ -3197,186 +3197,186 @@ } } }, - "AI Compatible" : { + "AI-compatible API" : { "localizations" : { "af" : { "stringUnit" : { "state" : "translated", - "value" : "AI-versoenbaar" + "value" : "KI-versoenbare API" } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "متوافق مع الذكاء الاصطناعي" + "value" : "واجهة API متوافقة مع الذكاء الاصطناعي" } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "Съвместим с ИИ" + "value" : "API, съвместим с ИИ" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Compatible amb IA" + "value" : "API compatible amb IA" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Kompatibilní s AI" + "value" : "API kompatibilní s AI" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "AI-kompatibel" + "value" : "AI-kompatibel API" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "KI-kompatibel" + "value" : "KI-kompatible API" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Συμβατό με ΤΝ" + "value" : "API συμβατό με AI" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Compatible con IA" + "value" : "API compatible con IA" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "tekoäly-yhteensopiva" + "value" : "AI-yhteensopiva API" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Compatible IA" + "value" : "API compatible IA" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "תואם בינה מלאכותית" + "value" : "API תואם בינה מלאכותית" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "MI-kompatibilis" + "value" : "MI-kompatibilis API" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Compatibile con IA" + "value" : "API compatibile con IA" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "AI互換" + "value" : "AI互換API" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "AI 호환" + "value" : "AI 호환 API" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "AI-kompatibel" + "value" : "AI-kompatibel API" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "AI-compatibel" + "value" : "AI-compatibele API" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Zgodny z AI" + "value" : "API zgodne z AI" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "Compatível com IA" + "value" : "API compatível com IA" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Compatível com IA" + "value" : "API compatível com IA" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Compatibil cu IA" + "value" : "API compatibil cu AI" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Совместимо с ИИ" + "value" : "AI-совместимый API" } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "Компатибилно са ВИ" + "value" : "API компатибилан са ВИ" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "AI-kompatibel" + "value" : "AI-kompatibel API" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "YZ Uyumlu" + "value" : "YZ uyumlu API" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Сумісний з ШІ" + "value" : "AI-сумісний API" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Tương thích AI" + "value" : "API tương thích AI" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "AI 兼容" + "value" : "AI 兼容 API" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "AI 相容" + "value" : "AI 相容 API" } } } @@ -7363,6 +7363,10 @@ } } }, + "App Icon" : { + "comment" : "A screen that allows the user to change the app icon.", + "isCommentAutoGenerated" : true + }, "app_log" : { "localizations" : { "af" : { @@ -22178,187 +22182,187 @@ "af" : { "stringUnit" : { "state" : "translated", - "value" : "het jou plasing hergeplaas" + "value" : "het 'n plasing herplaas" } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "أعاد نشر منشورك" + "value" : "أعاد نشر منشور" } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "препубликува вашата публикация" + "value" : "препубликува публикация" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "ha tornat a publicar el teu paràgraf" + "value" : "ha republicat una publicació" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "přeposlal(a) váš příspěvek" + "value" : "přesdílel(a) příspěvek" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "repostede dit opslag" + "value" : "repostede et opslag" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "hat deinen Beitrag geteilt" + "value" : "hat einen Beitrag geteilt" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "αναδημοσίευσε την ανάρτησή σας" + "value" : "αναδημοσίευσε μια ανάρτηση" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "reposted your post" + "value" : "reposted a post" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "republicó tu publicación" + "value" : "republicó una publicación" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "uudelleenjulkaisi julkaisusi" + "value" : "uudelleenjulkaisi julkaisun" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "a repartagé votre message" + "value" : "a repartagé une publication" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "פרסם מחדש את הפוסט שלך" + "value" : "פרסם מחדש פוסט" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "újraközölte a bejegyzésedet" + "value" : "újraközölt egy bejegyzést" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "ha ripubblicato il tuo post" + "value" : "ha ripubblicato un post" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "あなたの投稿をリポストしました" + "value" : "投稿をリポストしました" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "회원님의 게시물을 리포스트했습니다" + "value" : "게시물을 리포스트했습니다" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "repostet innlegget ditt" + "value" : "repostet et innlegg" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "heeft je bericht gedeeld" + "value" : "heeft een bericht gedeeld" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "podał Twój post dalej" + "value" : "podał dalej wpis" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "republicou a sua publicação" + "value" : "republicou uma publicação" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "repostou sua postagem" + "value" : "repostou uma postagem" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "ți-a redistribuit postarea" + "value" : "a redistribuit o postare" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "сделал репост вашего поста" + "value" : "сделал репост поста" } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "је поново објавио вашу објаву" + "value" : "је поново објавио објаву" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "repostade ditt inlägg" + "value" : "repostade ett inlägg" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "gönderini paylaştı" + "value" : "bir gönderiyi yeniden paylaştı" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "перепостив ваш допис" + "value" : "перепостив допис" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "đã đăng lại bài đăng của bạn" + "value" : "đã đăng lại một bài đăng" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "转发了您的帖子" + "value" : "转发了帖子" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "轉發了您的貼文" + "value" : "轉發了貼文" } } } @@ -26538,6 +26542,10 @@ } } }, + "Choose the icon shown on your Home Screen" : { + "comment" : "A description of the app icon settings.", + "isCommentAutoGenerated" : true + }, "Choose which service handles translation" : { "localizations" : { "af" : { @@ -52066,187 +52074,187 @@ "af" : { "stringUnit" : { "state" : "translated", - "value" : "herblogged" + "value" : "het 'n plasing herplaas" } }, "ar" : { "stringUnit" : { "state" : "translated", - "value" : "إعادة التدوين" + "value" : "أعاد نشر منشور" } }, "bg" : { "stringUnit" : { "state" : "translated", - "value" : "препубликува" + "value" : "препубликува публикация" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "reblogat" + "value" : "ha republicat una publicació" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "sdíleno" + "value" : "přesdílel(a) příspěvek" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "rebloggede" + "value" : "repostede et opslag" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "hat geboostet" + "value" : "hat einen Beitrag geteilt" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "αναδημοσίευσε" + "value" : "αναδημοσίευσε μια ανάρτηση" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "reblogged" + "value" : "reblogged a post" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "ha compartido" + "value" : "republicó una publicación" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "uudelleenbloggasi" + "value" : "uudelleenjulkaisi julkaisun" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "a boosté" + "value" : "a repartagé une publication" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "שיתף מחדש" + "value" : "פרסם מחדש פוסט" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "továbbosztotta" + "value" : "újraközölt egy bejegyzést" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "ha amplificato" + "value" : "ha ripubblicato un post" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "がブーストしました" + "value" : "投稿をブーストしました" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "부스트함" + "value" : "게시물을 부스트했습니다" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "fremhevet" + "value" : "repostet et innlegg" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "heeft geboost" + "value" : "heeft een bericht gedeeld" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "podbił(a)" + "value" : "podał dalej wpis" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "impulsionou" + "value" : "republicou uma publicação" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "repostou" + "value" : "repostou uma postagem" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "a promovat" + "value" : "a redistribuit o postare" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "продвинул(а)" + "value" : "сделал репост поста" } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "је проследио/ла" + "value" : "је поново објавио објаву" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "boostade" + "value" : "repostade ett inlägg" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "boostladı" + "value" : "bir gönderiyi yeniden paylaştı" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "поширив(-ла)" + "value" : "перепостив допис" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "đã thúc đẩy" + "value" : "đã đăng lại một bài đăng" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "转推了" + "value" : "转发了帖子" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "轉推了" + "value" : "轉發了貼文" } } } @@ -101968,6 +101976,34 @@ } } }, + "relation_request_follow" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Request follow" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "フォローをリクエスト" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "请求关注" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "請求關注" + } + } + } + }, "relation_following" : { "localizations" : { "af" : { @@ -126198,6 +126234,10 @@ } } }, + "This device does not support alternate app icons." : { + "comment" : "A message displayed when the user tries to change the app icon on a device that doesn't support it.", + "isCommentAutoGenerated" : true + }, "Translate" : { "localizations" : { "af" : { diff --git a/iosApp/flare/SplashScreen.storyboard b/iosApp/flare/SplashScreen.storyboard index d5e098aac7..b28806f967 100644 --- a/iosApp/flare/SplashScreen.storyboard +++ b/iosApp/flare/SplashScreen.storyboard @@ -1,9 +1,10 @@ - + - + + @@ -15,8 +16,8 @@ - - + + @@ -24,10 +25,10 @@ - + - - + + @@ -37,6 +38,9 @@ - + + + + diff --git a/iosApp/flare/UI/Component/CollectionViewTimeline.swift b/iosApp/flare/UI/Component/CollectionViewTimeline.swift index d0212683fb..42ea5d49f9 100644 --- a/iosApp/flare/UI/Component/CollectionViewTimeline.swift +++ b/iosApp/flare/UI/Component/CollectionViewTimeline.swift @@ -207,6 +207,7 @@ final class CollectionViewTimelineController: UIViewController, UICollectionView private var scrollingState = IsScrollingState() private var lastAppliedSignature: SnapshotSignature? private var lastRenderHashMap: [String: Int32] = [:] + private var lastLoadedTimelineItemIDs: Set = [] private let autoplayPlayerView = VideoPlayerView() private var autoplaySelectionTask: Task? private var autoplayCountdownTask: Task? @@ -224,6 +225,8 @@ final class CollectionViewTimelineController: UIViewController, UICollectionView // Maps item identifier → index for timeline cells private var itemIndexMap: [String: Int] = [:] private var stableTimelineItemIDs: Set = [] + private var lastKnownTimelineItemIDByIndex: [Int: String] = [:] + private var lastKnownTimelineRenderHashByItemID: [String: Int32] = [:] private struct SnapshotSignature: Equatable, Sendable { let accessoryIDs: [String] @@ -239,6 +242,7 @@ final class CollectionViewTimelineController: UIViewController, UICollectionView let indexMap: [String: Int] let renderHashMap: [String: Int32] let stableTimelineItemIDs: Set + let loadedTimelineItemIDs: Set let isRefreshing: Bool } @@ -400,6 +404,13 @@ final class CollectionViewTimelineController: UIViewController, UICollectionView "\(itemID):\(renderHash):\(heightCacheWidthKey(for: width))" } + private func cachedStabilizedHeight(for itemID: String, width: CGFloat) -> CGFloat? { + guard let renderHash = lastKnownTimelineRenderHashByItemID[itemID] else { + return nil + } + return heightCache[timelineHeightCacheKey(itemID: itemID, renderHash: renderHash, width: width)] + } + private func measuredCompressedCardHeight(_ card: UIView, width: CGFloat) -> CGFloat { card.bounds = CGRect(x: 0, y: 0, width: width, height: UIView.layoutFittingCompressedSize.height) card.setNeedsLayout() @@ -444,6 +455,15 @@ final class CollectionViewTimelineController: UIViewController, UICollectionView } } + private func pruneStabilizedPagingCache(keepingItemIDs: Set, itemCount: Int) { + lastKnownTimelineItemIDByIndex = lastKnownTimelineItemIDByIndex.filter { index, itemID in + index >= 0 && index < itemCount && keepingItemIDs.contains(itemID) + } + lastKnownTimelineRenderHashByItemID = lastKnownTimelineRenderHashByItemID.filter { itemID, _ in + keepingItemIDs.contains(itemID) + } + } + private func applyMeasuredHeightCorrection( itemID: String, renderHash: Int32, @@ -631,7 +651,12 @@ final class CollectionViewTimelineController: UIViewController, UICollectionView } else { cell.cachedPreferredHeight = nil cell.onPreferredHeightChanged = nil - cell.setHostedView(nil) + cell.configurePlaceholder( + index: index, + totalCount: totalCount, + appearance: appearance, + isMultipleColumn: columnCount > 1 + ) } } @@ -915,6 +940,7 @@ final class CollectionViewTimelineController: UIViewController, UICollectionView var newIndexMap: [String: Int] = [:] var newRenderHashMap: [String: Int32] = [:] var newStableTimelineItemIDs = Set() + var newLoadedTimelineItemIDs = Set() let accessoryIDs = accessoryItems.map { "\(Self.accessoryPrefix)\($0.id)" } var itemIDs: [String] = [] var footerIDs: [String] = [] @@ -933,25 +959,55 @@ final class CollectionViewTimelineController: UIViewController, UICollectionView case .success(let success): let itemCount = Int(success.itemCount) + var loadedIDsByIndex: [Int: String] = [:] + var loadedRenderHashByItemID: [String: Int32] = [:] + var loadedTimelineItemIDs = Set() + + for i in 0..() items.reserveCapacity(itemCount) for i in 0.. + ) -> [String] { + itemIDs.filter { + itemNeedsReconfigure( + $0, + newRenderHashMap: newRenderHashMap, + newLoadedItemIDs: newLoadedItemIDs + ) + } + } + + private func itemNeedsReconfigure( + _ itemID: String, + newRenderHashMap: [String: Int32], + newLoadedItemIDs: Set + ) -> Bool { + lastRenderHashMap[itemID] != newRenderHashMap[itemID] || + lastLoadedTimelineItemIDs.contains(itemID) != newLoadedItemIDs.contains(itemID) } private func restorePendingScrollAnchorIfNeeded() { @@ -1478,6 +1573,11 @@ final class CollectionViewTimelineController: UIViewController, UICollectionView return CGSize(width: width, height: height) } + if itemID.hasPrefix(Self.timelinePrefix), + let cachedHeight = cachedStabilizedHeight(for: itemID, width: width) { + return CGSize(width: width, height: cachedHeight) + } + return CGSize(width: width, height: 200) } @@ -1568,6 +1668,8 @@ private final class TimelineUIKitCollectionViewCell: UICollectionViewCell { private var hostedBottomConstraint: NSLayoutConstraint? private var timelineViewStorage: TimelineUIView? private var timelineCardStorage: AdaptiveTimelineCardUIView? + private var placeholderViewStorage: TimelinePlaceholderUIView? + private var placeholderCardStorage: AdaptiveTimelineCardUIView? // Rebuild-skip signature. When the incoming data + appearance + detail-key are // identical to the previous configure we short-circuit the expensive @@ -1604,7 +1706,10 @@ private final class TimelineUIKitCollectionViewCell: UICollectionViewCell { } func autoplayCandidates(prefix: String) -> [TimelineVideoAutoplayCandidate] { - timelineViewStorage?.autoplayCandidates(prefix: prefix) ?? [] + guard hostedView === timelineCardStorage else { + return [] + } + return timelineViewStorage?.autoplayCandidates(prefix: prefix) ?? [] } func performDeferredPoolCleanup() { @@ -1683,6 +1788,19 @@ private final class TimelineUIKitCollectionViewCell: UICollectionViewCell { setHostedView(timelineCard, usesWaterfallLayout: isMultipleColumn) } + func configurePlaceholder( + index: Int, + totalCount: Int, + appearance: TimelineUIKitAppearance, + isMultipleColumn: Bool + ) { + let placeholderCard = resolvedPlaceholderCard() + placeholderCard.isPlainTimelineDisplayMode = appearance.isPlainTimelineDisplayMode + placeholderCard.isMultipleColumn = isMultipleColumn + placeholderCard.configure(index: index, totalCount: totalCount) + setHostedView(placeholderCard, usesWaterfallLayout: isMultipleColumn) + } + override func layoutSubviews() { super.layoutSubviews() hostedView?.frame = contentView.bounds @@ -1818,6 +1936,25 @@ private final class TimelineUIKitCollectionViewCell: UICollectionViewCell { timelineCardStorage = card return card } + + private func resolvedPlaceholderView() -> TimelinePlaceholderUIView { + if let placeholderViewStorage { + return placeholderViewStorage + } + let view = TimelinePlaceholderUIView() + placeholderViewStorage = view + return view + } + + private func resolvedPlaceholderCard() -> AdaptiveTimelineCardUIView { + if let placeholderCardStorage { + return placeholderCardStorage + } + let card = AdaptiveTimelineCardUIView() + card.setContent(UIView.padding(resolvedPlaceholderView(), insets: UIEdgeInsets(top: 12, left: 16, bottom: 12, right: 16))) + placeholderCardStorage = card + return card + } } private final class TimelinePlaceholderCollectionViewCell: UICollectionViewCell { diff --git a/iosApp/flare/UI/Component/CommonProfileHeader.swift b/iosApp/flare/UI/Component/CommonProfileHeader.swift index 976394cf36..d4e1bfd30a 100644 --- a/iosApp/flare/UI/Component/CommonProfileHeader.swift +++ b/iosApp/flare/UI/Component/CommonProfileHeader.swift @@ -7,32 +7,17 @@ enum CommonProfileHeaderConstants { static let avatarSize: CGFloat = 96 } -private enum FollowButtonState: Equatable { - case blocked - case following - case requested - case follow - - init(_ relation: UiRelation) { - if relation.blocking { - self = .blocked - } else if relation.following { - self = .following - } else if relation.hasPendingFollowRequestFromYou { - self = .requested - } else { - self = .follow - } - } - +private extension FollowButtonState { var titleKey: LocalizedStringKey { - switch self { + switch onEnum(of: self) { case .blocked: "relation_blocked" case .following: "relation_following" case .requested: "relation_requested" + case .requestFollow: + "relation_request_follow" case .follow: "relation_follow" } @@ -45,8 +30,9 @@ struct CommonProfileHeader: View { @Environment(\.openURL) private var openURL let user: UiProfile let relation: UiState + let followButtonState: UiState let isMe: UiState - let onFollowClick: (UiRelation) -> Void + let onFollowClick: (FollowButtonState) -> Void let onFollowingClick: () -> Void let onFansClick: () -> Void @@ -91,24 +77,23 @@ struct CommonProfileHeader: View { Spacer() .frame(height: CommonProfileHeaderConstants.headerHeight) if case .success(let data) = onEnum(of: isMe), !data.data.boolValue { - switch onEnum(of: relation) { - case .success(let relationState): - let buttonState = FollowButtonState(relationState.data) + switch onEnum(of: followButtonState) { + case .success(let buttonState): VStack(spacing: 4) { - followButton(state: buttonState) { - onFollowClick(relationState.data) + followButton(state: buttonState.data) { + onFollowClick(buttonState.data) } - .id(buttonState) + .id(buttonState.data.id) .transition(.opacity.combined(with: .scale(scale: 0.92))) - if relationState.data.isFans { + if case .success(let relationState) = onEnum(of: relation), relationState.data.isFans { Text("relation_is_fans") .font(.caption) .foregroundStyle(.secondary) .multilineTextAlignment(.center) } } - .animation(.spring(response: 0.25, dampingFraction: 0.86), value: buttonState) + .animation(.spring(response: 0.25, dampingFraction: 0.86), value: buttonState.data.id) case .loading: Button(action: {}, label: { Text("#loading") }) @@ -136,7 +121,7 @@ struct CommonProfileHeader: View { @ViewBuilder private func followButton(state: FollowButtonState, action: @escaping () -> Void) -> some View { - switch state { + switch onEnum(of: state) { case .blocked: Button(action: action) { Text(state.titleKey) @@ -149,7 +134,7 @@ struct CommonProfileHeader: View { } .backport .glassButtonStyle(fallbackStyle: .bordered) - case .follow: + case .follow, .requestFollow: Button(action: action) { Text(state.titleKey) } diff --git a/iosApp/flare/UI/Component/GalleryTimelinePagingView.swift b/iosApp/flare/UI/Component/GalleryTimelinePagingView.swift index e121e9caa6..b9e726ff93 100644 --- a/iosApp/flare/UI/Component/GalleryTimelinePagingView.swift +++ b/iosApp/flare/UI/Component/GalleryTimelinePagingView.swift @@ -57,8 +57,12 @@ final class GalleryTimelineController: UIViewController, UICollectionViewDelegat private var currentData: PagingState? private var currentSuccess: PagingStateSuccess? private var itemIndexMap: [String: Int] = [:] + private var stableGalleryItemIDs: Set = [] + private var lastKnownItemIDByIndex: [Int: String] = [:] + private var lastKnownRenderHashByItemID: [String: Int32] = [:] private var lastAppliedSignature: SnapshotSignature? private var lastRenderHashMap: [String: Int32] = [:] + private var lastLoadedItemIDs: Set = [] private var lastProcessedDataRef: AnyObject? private var lastProcessedUpdateSignature: UpdateSignature? @@ -84,17 +88,39 @@ final class GalleryTimelineController: UIViewController, UICollectionViewDelegat private var heightCache: [String: CGFloat] = [:] private var heightCacheKeysByItemID: [String: Set] = [:] private var isUserRefreshing = false + private var pendingScrollAnchor: ScrollAnchor? + private var isRestoringScrollAnchor = false + private var scrollingState = IsScrollingState() private struct SnapshotSignature: Equatable { let itemIDs: [String] let footerIDs: [String] } + private struct ItemSnapshotState { + let itemIDs: [String] + let indexMap: [String: Int] + let renderHashMap: [String: Int32] + let stableItemIDs: Set + let loadedItemIDs: Set + } + + private struct ScrollAnchor { + let itemID: String + let distanceFromViewportTop: CGFloat + } + private enum UpdateSignature: Equatable { case loading case error case empty - case success(itemCount: Int, isRefreshing: Bool, footerIDs: [String]) + case success( + itemIDs: [String], + renderHashMap: [String: Int32], + loadedItemIDs: Set, + isRefreshing: Bool, + footerIDs: [String] + ) } override func viewDidLoad() { @@ -173,13 +199,16 @@ final class GalleryTimelineController: UIViewController, UICollectionViewDelegat // MARK: - Cell configuration private func configureCell(_ cell: GalleryTimelineCollectionViewCell, itemID: String) { - if itemID.hasPrefix(Self.itemPrefix), - let index = itemIndexMap[itemID], - let success = currentSuccess, - index >= 0, - index < Int(success.itemCount), - let item = success.peek(index: Int32(index)) { - cell.configureTile(item: item, appearance: appearance, openURL: openURL) + if itemID.hasPrefix(Self.itemPrefix) { + if let index = itemIndexMap[itemID], + let success = currentSuccess, + index >= 0, + index < Int(success.itemCount), + let item = success.peek(index: Int32(index)) { + cell.configureTile(item: item, appearance: appearance, openURL: openURL) + } else { + cell.configurePlaceholder() + } } else if itemID.hasPrefix(Self.placeholderPrefix) { cell.configurePlaceholder() } else if itemID == Self.emptyID { @@ -279,18 +308,202 @@ final class GalleryTimelineController: UIViewController, UICollectionViewDelegat case .empty: return .empty case .success(let success): + let itemState = makeItemSnapshotState(for: success, updatesLastKnownItems: false) return .success( - itemCount: Int(success.itemCount), + itemIDs: itemState.itemIDs, + renderHashMap: itemState.renderHashMap, + loadedItemIDs: itemState.loadedItemIDs, isRefreshing: success.isRefreshing, footerIDs: footerItemIDs(for: success) ) } } + private func makeItemSnapshotState( + for success: PagingStateSuccess, + updatesLastKnownItems: Bool + ) -> ItemSnapshotState { + let itemCount = Int(success.itemCount) + var loadedIDsByIndex: [Int: String] = [:] + var loadedRenderHashByItemID: [String: Int32] = [:] + var loadedItemIDs = Set() + + for i in 0..() + var usedItemIDs = Set() + itemIDs.reserveCapacity(itemCount) + + for i in 0.. CGFloat { + let minY = -collectionView.adjustedContentInset.top + let maxY = max( + minY, + collectionView.contentSize.height - collectionView.bounds.height + collectionView.adjustedContentInset.bottom + ) + return min(max(offsetY, minY), maxY) + } + + private func isStableGalleryItemID(_ itemID: String) -> Bool { + itemID.hasPrefix(Self.itemPrefix) && stableGalleryItemIDs.contains(itemID) + } + + private func pruneStabilizedPagingCache(keepingItemIDs: Set, itemCount: Int) { + lastKnownItemIDByIndex = lastKnownItemIDByIndex.filter { index, itemID in + index >= 0 && index < itemCount && keepingItemIDs.contains(itemID) + } + lastKnownRenderHashByItemID = lastKnownRenderHashByItemID.filter { itemID, _ in + keepingItemIDs.contains(itemID) + } + } + + private func captureScrollAnchor() -> ScrollAnchor? { + guard isViewLoaded, + currentSuccess != nil, + allowsScrollAnchorRestoration, + collectionView.bounds.height > 1 else { + return nil + } + + let viewportTop = effectiveViewportTop + let viewportBottom = collectionView.contentOffset.y + collectionView.bounds.height - collectionView.adjustedContentInset.bottom + return collectionView.indexPathsForVisibleItems + .compactMap { indexPath -> (itemID: String, frame: CGRect)? in + guard let itemID = dataSource.itemIdentifier(for: indexPath), + isStableGalleryItemID(itemID) else { + return nil + } + let frame = collectionView.layoutAttributesForItem(at: indexPath)?.frame + ?? collectionView.cellForItem(at: indexPath)?.frame + ?? .null + guard !frame.isNull, + frame.maxY > viewportTop, + frame.minY < viewportBottom else { + return nil + } + return (itemID, frame) + } + .min { lhs, rhs in + if abs(lhs.frame.minY - rhs.frame.minY) > 0.5 { + return lhs.frame.minY < rhs.frame.minY + } + return lhs.frame.minX < rhs.frame.minX + } + .map { + ScrollAnchor( + itemID: $0.itemID, + distanceFromViewportTop: $0.frame.minY - viewportTop + ) + } + } + + @discardableResult + private func restoreScrollAnchorIfNeeded(_ anchor: ScrollAnchor?) -> Bool { + guard let anchor, + isViewLoaded, + allowsScrollAnchorRestoration, + let indexPath = dataSource.indexPath(for: anchor.itemID) else { + return false + } + + view.layoutIfNeeded() + collectionView.layoutIfNeeded() + + guard let attributes = collectionView.layoutAttributesForItem(at: indexPath) else { + return false + } + + let targetOffsetY = attributes.frame.minY - anchor.distanceFromViewportTop - collectionView.adjustedContentInset.top + let targetOffset = CGPoint(x: collectionView.contentOffset.x, y: clampedContentOffsetY(targetOffsetY)) + if abs(collectionView.contentOffset.y - targetOffset.y) > 0.5 { + isRestoringScrollAnchor = true + collectionView.setContentOffset(targetOffset, animated: false) + isRestoringScrollAnchor = false + } + return true + } + + private func restorePendingScrollAnchorIfNeeded() { + guard !isRestoringScrollAnchor, + allowsScrollAnchorRestoration, + let pendingScrollAnchor else { + return + } + if restoreScrollAnchorIfNeeded(pendingScrollAnchor) { + collectionView.layer.removeAllAnimations() + } + } + private func applySnapshot(data: PagingState) { var snapshot = NSDiffableDataSourceSnapshot() var newIndexMap: [String: Int] = [:] var newRenderHashMap: [String: Int32] = [:] + var newStableItemIDs = Set() + var newLoadedItemIDs = Set() var itemIDs: [String] = [] var footerIDs: [String] = [] @@ -310,19 +523,12 @@ final class GalleryTimelineController: UIViewController, UICollectionViewDelegat snapshot.appendItems(itemIDs, toSection: Self.sectionMain) case .success(let success): snapshot.appendSections([Self.sectionMain]) - let itemCount = Int(success.itemCount) - var items: [String] = [] - items.reserveCapacity(itemCount) - for i in 0.. + ) -> [String] { + itemIDs.filter { + itemNeedsReconfigure( + $0, + newRenderHashMap: newRenderHashMap, + newLoadedItemIDs: newLoadedItemIDs + ) + } + } + + private func itemNeedsReconfigure( + _ itemID: String, + newRenderHashMap: [String: Int32], + newLoadedItemIDs: Set + ) -> Bool { + lastRenderHashMap[itemID] != newRenderHashMap[itemID] || + lastLoadedItemIDs.contains(itemID) != newLoadedItemIDs.contains(itemID) } private func reconfigureItems(_ itemIDs: [String]) { @@ -460,6 +728,11 @@ final class GalleryTimelineController: UIViewController, UICollectionViewDelegat return CGSize(width: width, height: estimatedHeight(for: item, itemID: itemID, width: width)) } + if itemID.hasPrefix(Self.itemPrefix), + let cachedHeight = cachedStabilizedHeight(for: itemID, width: width) { + return CGSize(width: width, height: cachedHeight) + } + return CGSize(width: width, height: 200) } @@ -495,10 +768,14 @@ final class GalleryTimelineController: UIViewController, UICollectionViewDelegat } private func heightCacheKey(for item: UiTimelineV2, itemID: String, width: CGFloat) -> String { + heightCacheKey(itemID: itemID, renderHash: item.renderHash, width: width) + } + + private func heightCacheKey(itemID: String, renderHash: Int32, width: CGFloat) -> String { let scaledWidth = Int((width * UIScreen.main.scale).rounded(.toNearestOrAwayFromZero)) return [ itemID, - String(item.renderHash), + String(renderHash), String(scaledWidth), appearance.showMedia ? "media" : "text", appearance.avatarShapeID, @@ -506,6 +783,13 @@ final class GalleryTimelineController: UIViewController, UICollectionViewDelegat ].joined(separator: "|") } + private func cachedStabilizedHeight(for itemID: String, width: CGFloat) -> CGFloat? { + guard let renderHash = lastKnownRenderHashByItemID[itemID] else { + return nil + } + return heightCache[heightCacheKey(itemID: itemID, renderHash: renderHash, width: width)] + } + private func setCachedHeight(_ height: CGFloat, for key: String, itemID: String) { heightCache[key] = height heightCacheKeysByItemID[itemID, default: []].insert(key) @@ -549,6 +833,31 @@ final class GalleryTimelineController: UIViewController, UICollectionViewDelegat index < Int(success.itemCount) else { return } _ = success.get(index: Int32(index)) } + + // MARK: - UIScrollViewDelegate + + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + scrollingState.isScrolling = true + pendingScrollAnchor = nil + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + restorePendingScrollAnchorIfNeeded() + } + + func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + if !decelerate { + scrollingState.isScrolling = false + } + } + + func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + scrollingState.isScrolling = false + } + + func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { + scrollingState.isScrolling = false + } } // MARK: - UIKit tile views diff --git a/iosApp/flare/UI/Component/Status/RichTextUIView.swift b/iosApp/flare/UI/Component/Status/RichTextUIView.swift index e2acfae401..ad1fd9e4e2 100644 --- a/iosApp/flare/UI/Component/Status/RichTextUIView.swift +++ b/iosApp/flare/UI/Component/Status/RichTextUIView.swift @@ -127,6 +127,8 @@ final class RichTextUIView: UIView, TimelineHeightProviding { traitRegistration = registerForTraitChanges([ UITraitUserInterfaceStyle.self, UITraitPreferredContentSizeCategory.self, + UITraitLegibilityWeight.self, + UITraitAccessibilityContrast.self, ]) { (view: RichTextUIView, _) in view.update(force: true) } @@ -650,10 +652,41 @@ final class RichTextUIView: UIView, TimelineHeightProviding { private func preferredFont(forTextStyle textStyle: UIFont.TextStyle) -> UIFont { UIFont.preferredFont( forTextStyle: textStyle, - compatibleWith: UITraitCollection(preferredContentSizeCategory: preferredContentSizeCategory) + compatibleWith: effectiveFontTraitCollection() ) } + private func effectiveFontTraitCollection() -> UITraitCollection { + UITraitCollection(traitsFrom: [ + traitCollection, + UITraitCollection(preferredContentSizeCategory: preferredContentSizeCategory), + ]) + } + + private func systemFont(size: CGFloat, weight: UIFont.Weight) -> UIFont { + let resolvedWeight: UIFont.Weight = traitCollection.legibilityWeight == .bold && weight == .regular + ? .semibold + : weight + return UIFont.systemFont(ofSize: size, weight: resolvedWeight) + } + + private func monospacedFont(size: CGFloat, weight: UIFont.Weight) -> UIFont { + let resolvedWeight: UIFont.Weight = traitCollection.legibilityWeight == .bold && weight == .regular + ? .semibold + : weight + return UIFont.monospacedSystemFont(ofSize: size, weight: resolvedWeight) + } + + private func addingItalicIfNeeded(_ font: UIFont, enabled: Bool) -> UIFont { + guard enabled, + let descriptor = font.fontDescriptor.withSymbolicTraits( + font.fontDescriptor.symbolicTraits.union(.traitItalic) + ) else { + return font + } + return UIFont(descriptor: descriptor, size: font.pointSize) + } + private func color(for style: RenderTextStyle, block: RenderBlockStyle) -> UIColor { if block.isBlockQuote || style.small { return baseTextColor.withAlphaComponent(0.7) @@ -690,28 +723,25 @@ final class RichTextUIView: UIView, TimelineHeightProviding { let baseSize = preferredFont(forTextStyle: textStyle).pointSize let baseFont: UIFont if style.code || style.monospace { - baseFont = UIFont.monospacedSystemFont( - ofSize: style.small ? baseSize * 0.8 : baseSize, - weight: style.bold ? .bold : .regular + return addingItalicIfNeeded( + monospacedFont( + size: style.small ? baseSize * 0.8 : baseSize, + weight: style.bold ? .bold : .regular + ), + enabled: style.italic || block.isBlockQuote || block.isFigCaption ) } else if style.small { - baseFont = UIFont.systemFont(ofSize: baseSize * 0.8) + baseFont = systemFont(size: baseSize * 0.8, weight: style.bold ? .bold : .regular) + } else if style.bold { + baseFont = systemFont(size: baseSize, weight: .bold) } else { baseFont = preferredFont(forTextStyle: textStyle) } - var traits: UIFontDescriptor.SymbolicTraits = [] - if style.bold { - traits.insert(.traitBold) - } - if style.italic || block.isBlockQuote || block.isFigCaption { - traits.insert(.traitItalic) - } - guard !traits.isEmpty, - let descriptor = baseFont.fontDescriptor.withSymbolicTraits(traits) else { - return baseFont - } - return UIFont(descriptor: descriptor, size: baseFont.pointSize) + return addingItalicIfNeeded( + baseFont, + enabled: style.italic || block.isBlockQuote || block.isFigCaption + ) } private func font(for descriptor: PlatformTextStyleDescriptor) -> UIFont { @@ -735,28 +765,25 @@ final class RichTextUIView: UIView, TimelineHeightProviding { let baseSize = preferredFont(forTextStyle: baseTextStyle).pointSize let baseFont: UIFont if descriptor.code || descriptor.monospace { - baseFont = UIFont.monospacedSystemFont( - ofSize: descriptor.small ? baseSize * 0.8 : baseSize, - weight: descriptor.bold ? .bold : .regular + return addingItalicIfNeeded( + monospacedFont( + size: descriptor.small ? baseSize * 0.8 : baseSize, + weight: descriptor.bold ? .bold : .regular + ), + enabled: descriptor.italic || descriptor.isBlockQuote || descriptor.isFigCaption ) } else if descriptor.small { - baseFont = UIFont.systemFont(ofSize: baseSize * 0.8) + baseFont = systemFont(size: baseSize * 0.8, weight: descriptor.bold ? .bold : .regular) + } else if descriptor.bold { + baseFont = systemFont(size: baseSize, weight: .bold) } else { baseFont = preferredFont(forTextStyle: baseTextStyle) } - var traits: UIFontDescriptor.SymbolicTraits = [] - if descriptor.bold { - traits.insert(.traitBold) - } - if descriptor.italic || descriptor.isBlockQuote || descriptor.isFigCaption { - traits.insert(.traitItalic) - } - guard !traits.isEmpty, - let fontDescriptor = baseFont.fontDescriptor.withSymbolicTraits(traits) else { - return baseFont - } - return UIFont(descriptor: fontDescriptor, size: baseFont.pointSize) + return addingItalicIfNeeded( + baseFont, + enabled: descriptor.italic || descriptor.isBlockQuote || descriptor.isFigCaption + ) } private func linkColor() -> UIColor { diff --git a/iosApp/flare/UI/Route/Route.swift b/iosApp/flare/UI/Route/Route.swift index f95a74542a..9dd1e794d8 100644 --- a/iosApp/flare/UI/Route/Route.swift +++ b/iosApp/flare/UI/Route/Route.swift @@ -10,8 +10,6 @@ enum Route: Hashable, Identifiable { switch (lhs, rhs) { case (.timeline(let lhs), .timeline(let rhs)): return lhs.id == rhs.id - case (.tabGroupConfig(let lhs), .tabGroupConfig(let rhs)): - return lhs?.id == rhs?.id default: return lhs.hashValue == rhs.hashValue } @@ -22,9 +20,6 @@ enum Route: Hashable, Identifiable { case .timeline(let item): hasher.combine("timeline") hasher.combine(item.id) - case .tabGroupConfig(let item): - hasher.combine("tabGroupConfig") - hasher.combine(item?.id) default: hasher.combine(String(describing: self)) } @@ -85,6 +80,8 @@ enum Route: Hashable, Identifiable { AppearanceDisplayScreen() case .appearanceMedia: AppearanceMediaScreen() + case .appIconSettings: + AppIconSettingsScreen() case .about: AboutScreen() case .localHostory: @@ -99,8 +96,6 @@ enum Route: Hashable, Identifiable { TranslationConfigScreen() case .tabSettings: TabSettingsScreen() - case .tabGroupConfig(let item): - GroupConfigScreen(item: item) case .rssDetail(let url, let descriptionHtml, let title): RssDetailScreen(url: url, descriptionHtml: descriptionHtml, descriptionTitle: title) case .twitterArticle(let accountType, let tweetId, let articleId): @@ -197,6 +192,7 @@ enum Route: Hashable, Identifiable { case appearanceLayout case appearanceDisplay case appearanceMedia + case appIconSettings case settings case about case notification @@ -219,7 +215,6 @@ enum Route: Hashable, Identifiable { case allAntennas(AccountType) case allChannels(AccountType) case allDirectMessages(AccountType) - case tabGroupConfig(GroupTimelineTabItemV2?) case rssManagement case draftBox case secondaryMenu diff --git a/iosApp/flare/UI/Screen/AiConfigScreen.swift b/iosApp/flare/UI/Screen/AiConfigScreen.swift index c1fb7829f8..14898df780 100644 --- a/iosApp/flare/UI/Screen/AiConfigScreen.swift +++ b/iosApp/flare/UI/Screen/AiConfigScreen.swift @@ -262,7 +262,7 @@ struct AiConfigScreen: View { case .onDevice: return "On Device" case .openAi: - return "AI Compatible" + return "AI-compatible API" } } diff --git a/iosApp/flare/UI/Screen/AppIconSettingsScreen.swift b/iosApp/flare/UI/Screen/AppIconSettingsScreen.swift new file mode 100644 index 0000000000..03f42686bc --- /dev/null +++ b/iosApp/flare/UI/Screen/AppIconSettingsScreen.swift @@ -0,0 +1,114 @@ +import SwiftUI +import UIKit + +struct AppIconSettingsScreen: View { + @State private var currentIconName = UIApplication.shared.alternateIconName + @State private var pendingIconID: String? + @State private var errorMessage: String? + + private let columns = [ + GridItem(.adaptive(minimum: 88, maximum: 120), spacing: 20) + ] + + var body: some View { + ScrollView { + LazyVGrid(columns: columns, spacing: 20) { + ForEach(AppIconOption.all) { option in + Button { + setIcon(option) + } label: { + AppIconGridItem( + option: option, + isSelected: currentIconName == option.alternateIconName, + isPending: pendingIconID == option.id + ) + } + .buttonStyle(.plain) + .accessibilityLabel(option.title) + .accessibilityAddTraits(currentIconName == option.alternateIconName ? [.isSelected] : []) + .disabled( + !UIApplication.shared.supportsAlternateIcons || + pendingIconID != nil + ) + } + } + .padding() + + if !UIApplication.shared.supportsAlternateIcons { + Text("This device does not support alternate app icons.") + .foregroundStyle(.secondary) + .padding(.horizontal) + } + + if let errorMessage { + Text(errorMessage) + .font(.footnote) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + .padding(.horizontal) + } + } + .navigationTitle("App Icon") + } + + private func setIcon(_ option: AppIconOption) { + guard UIApplication.shared.supportsAlternateIcons else { + return + } + guard currentIconName != option.alternateIconName else { + return + } + + errorMessage = nil + pendingIconID = option.id + UIApplication.shared.setAlternateIconName(option.alternateIconName) { error in + DispatchQueue.main.async { + pendingIconID = nil + if let error { + errorMessage = error.localizedDescription + } else { + currentIconName = UIApplication.shared.alternateIconName + } + } + } + } +} + +private struct AppIconGridItem: View { + let option: AppIconOption + let isSelected: Bool + let isPending: Bool + + var body: some View { + GeometryReader { proxy in + let cornerRadius = proxy.size.width * 0.218 + let shape = RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) + + ZStack(alignment: .topTrailing) { + Image(option.previewImageName) + .resizable() + .aspectRatio(1, contentMode: .fit) + .clipShape(shape) + .overlay { + shape.stroke(isSelected ? Color.accentColor : Color.clear, lineWidth: 3) + } + .shadow(color: .black.opacity(0.08), radius: 8, y: 4) + + if isPending { + ProgressView() + .padding(8) + .background(.regularMaterial, in: Circle()) + } else if isSelected { + Image(.faCheck) + .font(.caption.weight(.bold)) + .foregroundStyle(.white) + .frame(width: 24, height: 24) + .background(Color.accentColor, in: Circle()) + .padding(6) + } + } + .contentShape(shape) + } + .aspectRatio(1, contentMode: .fit) + } +} diff --git a/iosApp/flare/UI/Screen/GroupConfigScreen.swift b/iosApp/flare/UI/Screen/GroupConfigScreen.swift index f76c5348cc..d7177eddab 100644 --- a/iosApp/flare/UI/Screen/GroupConfigScreen.swift +++ b/iosApp/flare/UI/Screen/GroupConfigScreen.swift @@ -6,6 +6,7 @@ struct GroupConfigScreen: View { @Environment(\.dismiss) private var dismiss @Environment(\.timelineAppearance) private var baseTimelineAppearance let item: GroupTimelineTabItemV2? + let onConfirm: (GroupTimelineTabItemV2?) -> Void @State private var name: String @State private var icon: IconType @State private var enabled: Bool @@ -18,8 +19,12 @@ struct GroupConfigScreen: View { @State private var editItem: TimelineTabItemV2? = nil @StateObject private var presenter: KotlinPresenter - init(item: GroupTimelineTabItemV2? = nil) { + init( + item: GroupTimelineTabItemV2? = nil, + onConfirm: @escaping (GroupTimelineTabItemV2?) -> Void + ) { self.item = item + self.onConfirm = onConfirm _name = State(initialValue: item?.title.text ?? "") _icon = State(initialValue: item?.icon ?? IconType.Material(icon: .rss)) _enabled = State(initialValue: item?.enabled ?? true) @@ -167,16 +172,19 @@ struct GroupConfigScreen: View { } ToolbarItem(placement: .confirmationAction) { Button { - presenter.state.commit( - initialItem: item, - name: name, - icon: icon, - appearancePatch: appearancePatch, - enabled: enabled, - tabs: tabs, - mergePolicy: mergePolicy, - filterConfig: filterConfig, - defaultGroupName: NSLocalizedString("tab_settings_group_default_name", comment: "") + let defaultGroupName = NSLocalizedString("tab_settings_group_default_name", comment: "") + onConfirm( + presenter.state.buildGroupItem( + initialItem: item, + name: name, + icon: icon, + appearancePatch: appearancePatch, + enabled: enabled, + tabs: tabs, + mergePolicy: mergePolicy, + filterConfig: filterConfig, + defaultGroupName: defaultGroupName + ) ) dismiss() } label: { diff --git a/iosApp/flare/UI/Screen/ProfileScreen.swift b/iosApp/flare/UI/Screen/ProfileScreen.swift index dde4a468bd..dddcf15d70 100644 --- a/iosApp/flare/UI/Screen/ProfileScreen.swift +++ b/iosApp/flare/UI/Screen/ProfileScreen.swift @@ -88,9 +88,10 @@ struct ProfileScreen: View { ProfileHeader( user: presenter.state.userState, relation: presenter.state.relationState, + followButtonState: presenter.state.followButtonState, isMe: presenter.state.isMe, - onFollowClick: { user, relation in - handleFollowAction(user: user, relation: relation) + onFollowClick: { user, followButtonState in + handleFollowAction(user: user, followButtonState: followButtonState) }, onFollowingClick: onFollowingClick, onFansClick: onFansClick @@ -122,8 +123,8 @@ struct ProfileScreen: View { profileState: presenter.state, tabs: tabs, selectedTab: $selectedTab, - onFollowClick: { user, relation in - handleFollowAction(user: user, relation: relation) + onFollowClick: { user, followButtonState in + handleFollowAction(user: user, followButtonState: followButtonState) }, onFollowingClick: onFollowingClick, onFansClick: onFansClick, @@ -161,17 +162,18 @@ struct ProfileScreen: View { } } - private func handleFollowAction(user: UiProfile, relation: UiRelation) { - if relation.blocking { + private func handleFollowAction(user: UiProfile, followButtonState: FollowButtonState) { + switch onEnum(of: followButtonState) { + case .blocked: if case .success(let state) = onEnum(of: presenter.state.myAccountKey) { let route = DeeplinkRoute.UnblockUser(accountKey: state.data, userKey: user.key) if let url = URL(string: route.toUri()) { openURL(url) } } - } else if relation.following || relation.hasPendingFollowRequestFromYou { + case .following, .requested: presenter.state.unfollow(userKey: user.key) - } else { + case .follow, .requestFollow: presenter.state.follow(userKey: user.key) } } @@ -231,7 +233,7 @@ private struct ProfileCompatTimelineView: UIViewControllerRepresentable { let profileState: ProfileState let tabs: [ProfileState.Tab] @Binding var selectedTab: Int - let onFollowClick: (UiProfile, UiRelation) -> Void + let onFollowClick: (UiProfile, FollowButtonState) -> Void let onFollowingClick: (MicroBlogKey) -> Void let onFansClick: (MicroBlogKey) -> Void let onHeaderVisibilityChanged: (Bool) -> Void @@ -330,7 +332,7 @@ private struct ProfileCompatTimelineView: UIViewControllerRepresentable { timelineAppearance: TimelineAppearance, openURL: OpenURLAction, horizontalSizeClass: UserInterfaceSizeClass?, - onFollowClick: @escaping (UiProfile, UiRelation) -> Void, + onFollowClick: @escaping (UiProfile, FollowButtonState) -> Void, onFollowingClick: @escaping (MicroBlogKey) -> Void, onFansClick: @escaping (MicroBlogKey) -> Void, onHeaderVisibilityChanged: @escaping (Bool) -> Void, @@ -350,6 +352,7 @@ private struct ProfileCompatTimelineView: UIViewControllerRepresentable { ProfileHeader( user: profileState.userState, relation: profileState.relationState, + followButtonState: profileState.followButtonState, isMe: profileState.isMe, onFollowClick: onFollowClick, onFollowingClick: onFollowingClick, @@ -482,6 +485,7 @@ private struct ProfileCompatTimelineView: UIViewControllerRepresentable { private struct ProfileHeaderAccessorySignature: Equatable { let userState: String let relationState: String + let followButtonState: String let isMeState: String let appearance: TimelineUIKitAppearance let horizontalSizeClass: UserInterfaceSizeClass? @@ -493,6 +497,7 @@ private struct ProfileHeaderAccessorySignature: Equatable { ) { userState = Self.userStateSignature(profileState.userState) relationState = Self.relationStateSignature(profileState.relationState) + followButtonState = Self.followButtonStateSignature(profileState.followButtonState) isMeState = Self.isMeStateSignature(profileState.isMe) appearance = TimelineUIKitAppearance(timeline: timelineAppearance) self.horizontalSizeClass = horizontalSizeClass @@ -540,6 +545,17 @@ private struct ProfileHeaderAccessorySignature: Equatable { } } + private static func followButtonStateSignature(_ state: UiState) -> String { + switch onEnum(of: state) { + case .error: + "error" + case .loading: + "loading" + case .success(let success): + "success|\(success.data.id)" + } + } + private static func isMeStateSignature(_ state: UiState) -> String { switch onEnum(of: state) { case .error: @@ -650,8 +666,9 @@ extension ProfileScreen { struct ProfileHeader: View { let user: UiState let relation: UiState + let followButtonState: UiState let isMe: UiState - let onFollowClick: (UiProfile, UiRelation) -> Void + let onFollowClick: (UiProfile, FollowButtonState) -> Void let onFollowingClick: (MicroBlogKey) -> Void let onFansClick: (MicroBlogKey) -> Void var body: some View { @@ -662,6 +679,7 @@ struct ProfileHeader: View { CommonProfileHeader( user: createSampleUser(), relation: relation, + followButtonState: followButtonState, isMe: isMe, onFollowClick: { _ in }, onFollowingClick: {}, @@ -672,8 +690,9 @@ struct ProfileHeader: View { ProfileHeaderSuccess( user: data.data, relation: relation, + followButtonState: followButtonState, isMe: isMe, - onFollowClick: { relation in onFollowClick(data.data, relation) }, + onFollowClick: { followButtonState in onFollowClick(data.data, followButtonState) }, onFollowingClick: onFollowingClick, onFansClick: onFansClick ) @@ -684,14 +703,16 @@ struct ProfileHeader: View { struct ProfileHeaderSuccess: View { let user: UiProfile let relation: UiState + let followButtonState: UiState let isMe: UiState - let onFollowClick: (UiRelation) -> Void + let onFollowClick: (FollowButtonState) -> Void let onFollowingClick: (MicroBlogKey) -> Void let onFansClick: (MicroBlogKey) -> Void var body: some View { CommonProfileHeader( user: user, relation: relation, + followButtonState: followButtonState, isMe: isMe, onFollowClick: onFollowClick, onFollowingClick: { diff --git a/iosApp/flare/UI/Screen/SettingsScreen.swift b/iosApp/flare/UI/Screen/SettingsScreen.swift index 51e51f6567..4ce7d56e13 100644 --- a/iosApp/flare/UI/Screen/SettingsScreen.swift +++ b/iosApp/flare/UI/Screen/SettingsScreen.swift @@ -46,6 +46,14 @@ struct SettingsScreen: View { Image(.faPhotoFilm) } } + NavigationLink(value: Route.appIconSettings) { + Label { + Text("App Icon") + Text("Choose the icon shown on your Home Screen") + } icon: { + Image("fa-palette") + } + } if let url = URL(string: UIApplication.openSettingsURLString) { Button { UIApplication.shared.open(url) diff --git a/iosApp/flare/UI/Screen/SplashScreen.swift b/iosApp/flare/UI/Screen/SplashScreen.swift index 64677a5178..e5fcbb0032 100644 --- a/iosApp/flare/UI/Screen/SplashScreen.swift +++ b/iosApp/flare/UI/Screen/SplashScreen.swift @@ -1,9 +1,14 @@ import SwiftUI +import UIKit struct SplashScreen: View { + private var iconPreviewImageName: String { + AppIconOption.previewImageName(for: UIApplication.shared.alternateIconName) + } + var body: some View { VStack { - Image(.flareLogo) + Image(iconPreviewImageName) .resizable() .scaledToFit() .frame(width: 96, height: 96) diff --git a/iosApp/flare/UI/Screen/TabSettingsScreen.swift b/iosApp/flare/UI/Screen/TabSettingsScreen.swift index a5ac5a51c4..cf0af3f4bc 100644 --- a/iosApp/flare/UI/Screen/TabSettingsScreen.swift +++ b/iosApp/flare/UI/Screen/TabSettingsScreen.swift @@ -165,13 +165,17 @@ struct TabSettingsScreen: View { })) { NavigationStack { if let item = editGroup { - GroupConfigScreen(item: item) + GroupConfigScreen(item: item) { updated in + upsertGroup(initialItem: item, updatedItem: updated) + } } } } .sheet(isPresented: $showCreateGroup) { NavigationStack { - GroupConfigScreen(item: nil) + GroupConfigScreen(item: nil) { updated in + upsertGroup(initialItem: nil, updatedItem: updated) + } } } .sheet(isPresented: Binding(get: { @@ -225,6 +229,23 @@ struct TabSettingsScreen: View { .first(where: { isSystemHomeMixedTimeline($0) })? .mergePolicy ?? .timePerPage } + + private func upsertGroup( + initialItem: GroupTimelineTabItemV2?, + updatedItem: GroupTimelineTabItemV2? + ) { + let targetIndex = initialItem + .flatMap { item in tabItems.firstIndex(where: { $0.id == item.id }) } + ?? tabItems.count + + tabItems.removeAll { item in + item.id == initialItem?.id || item.id == updatedItem?.id + } + + if let updatedItem { + tabItems.insert(updatedItem, at: min(targetIndex, tabItems.count)) + } + } } struct EditTabSheet: View { diff --git a/iosApp/scripts/generate_icon_previews.sh b/iosApp/scripts/generate_icon_previews.sh new file mode 100755 index 0000000000..03f8504df9 --- /dev/null +++ b/iosApp/scripts/generate_icon_previews.sh @@ -0,0 +1,296 @@ +#!/usr/bin/env bash +set -euo pipefail + +icon_dir="iosApp/flare" +asset_catalog="iosApp/flare/Assets.xcassets" +swift_output="iosApp/flare/Common/AppIconOption.swift" +project_file="iosApp/Flare.xcodeproj/project.pbxproj" +prefix="app_icon_preview" +size="1024" +platform="iOS" +rendition="Default" +overwrite="false" +generate_previews="true" +generate_swift="true" +update_project="true" +ictool="/Applications/Xcode.app/Contents/Applications/Icon Composer.app/Contents/Executables/ictool" + +usage() { + cat <<'EOF' +Generate regular image assets from Icon Composer .icon bundles. + +Usage: + iosApp/scripts/generate_icon_previews.sh [options] + +Options: + --icon-dir Directory containing AppIcon*.icon bundles. Default: iosApp/flare + --asset-catalog Output .xcassets directory. Default: iosApp/flare/Assets.xcassets + --swift-output Output AppIconOption.swift path. Default: iosApp/flare/Common/AppIconOption.swift + --project-file Xcode project.pbxproj to update. Default: iosApp/Flare.xcodeproj/project.pbxproj + --prefix Output image asset prefix. Default: app_icon_preview + --size PNG width/height. Default: 1024 + --platform ictool platform. Default: iOS + --rendition ictool rendition. Default: Default + --ictool Path to Icon Composer ictool. + --overwrite Replace existing preview .imageset directories. + --skip-previews Only generate the Swift icon list. + --skip-swift Only generate preview image assets. + --skip-project Do not update alternate app icon names in the Xcode project. + -h, --help Show this help. +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --icon-dir) + icon_dir="$2" + shift 2 + ;; + --asset-catalog) + asset_catalog="$2" + shift 2 + ;; + --swift-output) + swift_output="$2" + shift 2 + ;; + --project-file) + project_file="$2" + shift 2 + ;; + --prefix) + prefix="$2" + shift 2 + ;; + --size) + size="$2" + shift 2 + ;; + --platform) + platform="$2" + shift 2 + ;; + --rendition) + rendition="$2" + shift 2 + ;; + --ictool) + ictool="$2" + shift 2 + ;; + --overwrite) + overwrite="true" + shift + ;; + --skip-previews) + generate_previews="false" + shift + ;; + --skip-swift) + generate_swift="false" + shift + ;; + --skip-project) + update_project="false" + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "error: unknown argument: $1" >&2 + usage >&2 + exit 1 + ;; + esac +done + +if [[ "$generate_previews" == "true" && ! -x "$ictool" ]]; then + echo "error: ictool not found at: $ictool" >&2 + echo "Install/open Xcode with Icon Composer, or pass --ictool ." >&2 + exit 1 +fi + +if [[ ! -d "$icon_dir" ]]; then + echo "error: icon directory does not exist: $icon_dir" >&2 + exit 1 +fi + +if [[ "$generate_previews" == "true" && ! -d "$asset_catalog" ]]; then + echo "error: asset catalog does not exist: $asset_catalog" >&2 + exit 1 +fi + +if [[ "$generate_previews" == "true" ]] && { ! [[ "$size" =~ ^[0-9]+$ ]] || [[ "$size" -le 0 ]]; }; then + echo "error: --size must be a positive integer" >&2 + exit 1 +fi + +shopt -s nullglob +icon_bundles=("$icon_dir"/AppIcon*.icon) +shopt -u nullglob + +if [[ "${#icon_bundles[@]}" -eq 0 ]]; then + echo "error: no AppIcon*.icon bundles found in: $icon_dir" >&2 + exit 1 +fi + +IFS=$'\n' icon_bundles=($(printf '%s\n' "${icon_bundles[@]}" | sort)) +unset IFS + +preview_suffix_for_icon_name() { + local icon_name="$1" + + if [[ "$icon_name" == "AppIcon" ]]; then + echo "default" + elif [[ "$icon_name" == AppIcon_* ]]; then + echo "${icon_name#AppIcon_}" + else + echo "$icon_name" + fi +} + +title_for_icon_name() { + local icon_name="$1" + local suffix + + suffix="$(preview_suffix_for_icon_name "$icon_name")" + if [[ "$suffix" == "default" ]]; then + echo "Default" + else + printf '%s\n' "$suffix" | tr '_' ' ' | awk '{ + for (i = 1; i <= NF; i++) { + $i = toupper(substr($i, 1, 1)) substr($i, 2) + } + print + }' + fi +} + +if [[ "$generate_swift" == "true" ]]; then + mkdir -p "$(dirname "$swift_output")" + + { + cat <<'EOF' +import Foundation + +struct AppIconOption: Identifiable { + let title: String + let alternateIconName: String? + let previewImageName: String + + var id: String { + alternateIconName ?? "AppIcon" + } +} + +extension AppIconOption { + static let all: [AppIconOption] = [ +EOF + + for icon_bundle in "${icon_bundles[@]}"; do + icon_name="$(basename "$icon_bundle" .icon)" + suffix="$(preview_suffix_for_icon_name "$icon_name")" + title="$(title_for_icon_name "$icon_name")" + asset_name="${prefix}_${suffix}" + + if [[ "$icon_name" == "AppIcon" ]]; then + echo " .init(title: \"$title\", alternateIconName: nil, previewImageName: \"$asset_name\")," + else + echo " .init(title: \"$title\", alternateIconName: \"$icon_name\", previewImageName: \"$asset_name\")," + fi + done + + cat <<'EOF' + ] + + static func previewImageName(for alternateIconName: String?) -> String { + all.first { $0.alternateIconName == alternateIconName }?.previewImageName ?? "app_icon_preview_default" + } +} +EOF + } > "$swift_output" + + echo "Generated $swift_output" +fi + +if [[ "$update_project" == "true" ]]; then + if [[ ! -f "$project_file" ]]; then + echo "error: Xcode project file does not exist: $project_file" >&2 + exit 1 + fi + + alternate_names=() + for icon_bundle in "${icon_bundles[@]}"; do + icon_name="$(basename "$icon_bundle" .icon)" + if [[ "$icon_name" != "AppIcon" ]]; then + alternate_names+=("$icon_name") + fi + done + + alternate_names_string="${alternate_names[*]}" + if ! grep -q "ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES" "$project_file"; then + echo "warning: ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES was not found in $project_file" >&2 + else + perl -0pi -e "s/ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = \"[^\"]*\";/ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = \"$alternate_names_string\";/g" "$project_file" + echo "Updated alternate app icon names in $project_file" + fi +fi + +if [[ "$generate_previews" != "true" ]]; then + exit 0 +fi + +for icon_bundle in "${icon_bundles[@]}"; do + icon_name="$(basename "$icon_bundle" .icon)" + suffix="$(preview_suffix_for_icon_name "$icon_name")" + + asset_name="${prefix}_${suffix}" + imageset="$asset_catalog/${asset_name}.imageset" + png_name="${asset_name}.png" + png_path="$imageset/$png_name" + + if [[ -e "$imageset" ]]; then + if [[ "$overwrite" == "true" ]]; then + rm -rf "$imageset" + else + echo "Skipping existing $(basename "$imageset"). Use --overwrite to replace it." + continue + fi + fi + + mkdir -p "$imageset" + + if ! "$ictool" "$icon_bundle" \ + --export-image \ + --output-file "$png_path" \ + --platform "$platform" \ + --rendition "$rendition" \ + --width "$size" \ + --height "$size" \ + --scale 1; then + rm -rf "$imageset" + echo "error: ictool failed for $icon_bundle" >&2 + echo "If this happens only inside Codex, run this script from Terminal or allow the ictool command outside the sandbox." >&2 + exit 1 + fi + + cat > "$imageset/Contents.json" <.renderThread(accountKey: MicroBlogKey): List { +internal fun List.renderThread(accountKey: MicroBlogKey): List { val renderedPosts = mutableListOf>() val stack = mutableListOf>() @@ -103,7 +103,7 @@ private fun List.renderThread(accountKey: MicroBlogKe } }.forEach { (depth, post) -> while (stack.lastOrNull()?.first?.let { it >= depth } == true) { - stack.removeLast() + stack.removeAt(stack.lastIndex) } val parents = when { @@ -128,10 +128,21 @@ private fun List.renderThread(accountKey: MicroBlogKe .flatMap { it.collectParentKeys() } .toSet() - return visiblePosts - .filterNot { (depth, post) -> - depth > 0L && post.statusKey in descendantParentKeys - }.map { it.second } + val topLevelPosts = + visiblePosts + .filterNot { (depth, post) -> + depth > 0L && post.statusKey in descendantParentKeys + }.map { it.second } + val topLevelPostKeys = topLevelPosts.map { it.statusKey }.toSet() + + return topLevelPosts.map { post -> + post.copy( + parents = + post.parents + .filterNot { it.statusKey in topLevelPostKeys } + .toPersistentList(), + ) + } } private fun UiTimelineV2.Post.collectParentKeys(): Set = diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/handler/RelationHandler.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/handler/RelationHandler.kt index cca502a8b7..f13b03ce8c 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/handler/RelationHandler.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/handler/RelationHandler.kt @@ -45,51 +45,55 @@ internal class RelationHandler( }, ) - fun follow(userKey: MicroBlogKey) = - coroutineScope.launch { - tryRun { + fun follow( + userKey: MicroBlogKey, + requestFollow: Boolean = false, + ) = coroutineScope.launch { + var previousRelation: UiRelation? = null + tryRun { + previousRelation = updateRelation( userKey = userKey, update = { relation -> relation.copy( - following = true, + following = !requestFollow, + hasPendingFollowRequestFromYou = requestFollow, ) }, ) - dataSource.follow(userKey) - }.onFailure { - updateRelation( + dataSource.follow(userKey) + }.onFailure { + previousRelation?.let { relation -> + setRelation( userKey = userKey, - update = { relation -> - relation.copy( - following = false, - ) - }, + relation = relation, ) } } + } fun unfollow(userKey: MicroBlogKey) = coroutineScope.launch { + var previousRelation: UiRelation? = null tryRun { - updateRelation( - userKey = userKey, - update = { relation -> - relation.copy( - following = false, - ) - }, - ) + previousRelation = + updateRelation( + userKey = userKey, + update = { relation -> + relation.copy( + following = false, + hasPendingFollowRequestFromYou = false, + ) + }, + ) dataSource.unfollow(userKey) }.onFailure { - updateRelation( - userKey = userKey, - update = { relation -> - relation.copy( - following = true, - ) - }, - ) + previousRelation?.let { relation -> + setRelation( + userKey = userKey, + relation = relation, + ) + } } } @@ -216,7 +220,7 @@ internal class RelationHandler( private suspend fun updateRelation( userKey: MicroBlogKey, update: (UiRelation) -> UiRelation, - ) { + ): UiRelation? { val currentRelation = database .userDao() @@ -224,13 +228,24 @@ internal class RelationHandler( accountType = accountType as DbAccountType, userKey = userKey, ).mapNotNull { it?.relation } - .firstOrNull() ?: return + .firstOrNull() ?: return null val newRelation = update(currentRelation) + setRelation( + userKey = userKey, + relation = newRelation, + ) + return currentRelation + } + + private suspend fun setRelation( + userKey: MicroBlogKey, + relation: UiRelation, + ) { database.userDao().insertUserRelation( DbUserRelation( accountType = accountType as DbAccountType, userKey = userKey, - relation = newRelation, + relation = relation, ), ) } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/paging/TimelinePagingMapper.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/paging/TimelinePagingMapper.kt index c3687eb7d8..66e0823fbe 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/paging/TimelinePagingMapper.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/paging/TimelinePagingMapper.kt @@ -175,13 +175,13 @@ internal object TimelinePagingMapper { val currentKey = current.statusKey resolvedPosts[currentKey]?.let { resolvingKeys -= currentKey - stack.removeLast() + stack.removeAt(stack.lastIndex) continue } if (!frame.expanded) { if (!resolvingKeys.add(currentKey)) { - stack.removeLast() + stack.removeAt(stack.lastIndex) continue } frame.expanded = true @@ -205,7 +205,7 @@ internal object TimelinePagingMapper { ) resolvedPosts[currentKey] = resolved resolvingKeys -= currentKey - stack.removeLast() + stack.removeAt(stack.lastIndex) } return resolvedPosts[post.statusKey] ?: post diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/CommentPagingSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/CommentPagingSource.kt index 9b6a35a66d..7ea692f862 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/CommentPagingSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/CommentPagingSource.kt @@ -1,8 +1,8 @@ package dev.dimension.flare.data.datasource.vvo +import dev.dimension.flare.data.datasource.microblog.paging.CacheableRemoteLoader import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest import dev.dimension.flare.data.datasource.microblog.paging.PagingResult -import dev.dimension.flare.data.datasource.microblog.paging.RemoteLoader import dev.dimension.flare.data.network.vvo.VVOService import dev.dimension.flare.data.repository.LoginExpiredException import dev.dimension.flare.model.MicroBlogKey @@ -14,7 +14,9 @@ internal class CommentPagingSource( private val service: VVOService, private val accountKey: MicroBlogKey, private val onClearMarker: suspend () -> Unit, -) : RemoteLoader { +) : CacheableRemoteLoader { + override val pagingKey: String = "notification_comment_$accountKey" + override suspend fun load( pageSize: Int, request: PagingRequest, diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/LikePagingSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/LikePagingSource.kt index f7984e8e7d..7ad261d397 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/LikePagingSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/LikePagingSource.kt @@ -1,8 +1,8 @@ package dev.dimension.flare.data.datasource.vvo +import dev.dimension.flare.data.datasource.microblog.paging.CacheableRemoteLoader import dev.dimension.flare.data.datasource.microblog.paging.PagingRequest import dev.dimension.flare.data.datasource.microblog.paging.PagingResult -import dev.dimension.flare.data.datasource.microblog.paging.RemoteLoader import dev.dimension.flare.data.network.vvo.VVOService import dev.dimension.flare.data.repository.LoginExpiredException import dev.dimension.flare.model.MicroBlogKey @@ -14,7 +14,9 @@ internal class LikePagingSource( private val service: VVOService, private val accountKey: MicroBlogKey, private val onClearMarker: suspend () -> Unit, -) : RemoteLoader { +) : CacheableRemoteLoader { + override val pagingKey: String = "notification_like_$accountKey" + override suspend fun load( pageSize: Int, request: PagingRequest, diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/mastodon/api/FriendshipResources.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/mastodon/api/FriendshipResources.kt index b33a46b58a..08bb5e1558 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/mastodon/api/FriendshipResources.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/mastodon/api/FriendshipResources.kt @@ -5,7 +5,6 @@ import de.jensklingenberg.ktorfit.http.GET import de.jensklingenberg.ktorfit.http.POST import de.jensklingenberg.ktorfit.http.Path import de.jensklingenberg.ktorfit.http.Query -import dev.dimension.flare.data.network.mastodon.api.model.Account import dev.dimension.flare.data.network.mastodon.api.model.PostReport import dev.dimension.flare.data.network.mastodon.api.model.RelationshipResponse @@ -13,12 +12,12 @@ internal interface FriendshipResources { @POST("api/v1/accounts/{id}/follow") suspend fun follow( @Path(value = "id") id: String, - ): Account + ): RelationshipResponse @POST("api/v1/accounts/{id}/unfollow") suspend fun unfollow( @Path(value = "id") id: String, - ): Account + ): RelationshipResponse @GET("api/v1/accounts/relationships") suspend fun showFriendships( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/mastodon/api/model/RelationshipResponse.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/mastodon/api/model/RelationshipResponse.kt index e4f046a7c0..299e06a535 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/mastodon/api/model/RelationshipResponse.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/mastodon/api/model/RelationshipResponse.kt @@ -18,9 +18,14 @@ internal data class RelationshipResponse( val muting: Boolean? = null, @SerialName("muting_notifications") val mutingNotifications: Boolean? = null, + @SerialName("muting_expires_at") + val mutingExpiresAt: String? = null, val requested: Boolean? = null, + @SerialName("requested_by") + val requestedBy: Boolean? = null, @SerialName("domain_blocking") val domainBlocking: Boolean? = null, val endorsed: Boolean? = null, + val languages: List? = null, val note: String? = null, ) diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/platform/BlueskyPlatformSpec.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/platform/BlueskyPlatformSpec.kt index 81f98e5b4c..57ae984318 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/platform/BlueskyPlatformSpec.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/platform/BlueskyPlatformSpec.kt @@ -40,12 +40,10 @@ internal data object BlueskyPlatformSpec : PlatformSpec { override fun deepLinkPatterns(host: String): ImmutableList> = buildList { - add(DeepLinkPattern(DeepLinkMapping.Type.Profile.serializer(), Url("https://$host/profile/{handle}"))) + add(DeepLinkPattern(DeepLinkMapping.Type.BlueskyProfile.serializer(), Url("https://$host/profile/{handle}"))) add(DeepLinkPattern(DeepLinkMapping.Type.BlueskyPost.serializer(), Url("https://$host/profile/{handle}/post/{id}"))) - if (host == "bsky.social") { - add(DeepLinkPattern(DeepLinkMapping.Type.Profile.serializer(), Url("https://bsky.app/profile/{handle}"))) - add(DeepLinkPattern(DeepLinkMapping.Type.BlueskyPost.serializer(), Url("https://bsky.app/profile/{handle}/post/{id}"))) - } + add(DeepLinkPattern(DeepLinkMapping.Type.BlueskyProfile.serializer(), Url("https://bsky.app/profile/{handle}"))) + add(DeepLinkPattern(DeepLinkMapping.Type.BlueskyPost.serializer(), Url("https://bsky.app/profile/{handle}/post/{id}"))) }.toImmutableList() internal val bookmarkTimelineSpec = diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiIcon.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiIcon.kt index 2863c52a57..6519668fad 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiIcon.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiIcon.kt @@ -62,4 +62,58 @@ public enum class UiIcon { UnFavourite, } +/** + * Icons exposed by the tab/group icon picker. + * + * Keep this list in sync with UiIcon.toImageVector(): entries that render to the + * same Font Awesome icon should appear only once here. + */ +public val TabPickerUiIcons: List = + listOf( + UiIcon.Twitter, + UiIcon.Mastodon, + UiIcon.Misskey, + UiIcon.Bluesky, + UiIcon.Weibo, + UiIcon.Nostr, + UiIcon.X, + UiIcon.Home, + UiIcon.Notification, + UiIcon.Search, + UiIcon.Profile, + UiIcon.Settings, + UiIcon.Local, + UiIcon.World, + UiIcon.Featured, + UiIcon.Bookmark, + UiIcon.Heart, + UiIcon.List, + UiIcon.Messages, + UiIcon.Rss, + UiIcon.Channel, + UiIcon.Translate, + UiIcon.Like, + UiIcon.Retweet, + UiIcon.Reply, + UiIcon.Comment, + UiIcon.Unbookmark, + UiIcon.More, + UiIcon.MoreVerticel, + UiIcon.Delete, + UiIcon.React, + UiIcon.UnReact, + UiIcon.Share, + UiIcon.Mute, + UiIcon.Block, + UiIcon.Follow, + UiIcon.Favourite, + UiIcon.Mention, + UiIcon.Poll, + UiIcon.Edit, + UiIcon.Info, + UiIcon.Pin, + UiIcon.Check, + UiIcon.UnFavourite, + ) + public fun UiIcon.asType(): IconType = IconType.Material(this) diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiTimelineV2.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiTimelineV2.kt index 9d1e25ef2c..230bf0a0cb 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiTimelineV2.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiTimelineV2.kt @@ -259,7 +259,12 @@ public sealed class UiTimelineV2 { append(" ") } } + internalRepost?.content?.raw?.let { repostContent -> + append(repostContent) + append(" ") + } } + val onClicked: ClickContext.() -> Unit by lazy { clickEvent.onClicked } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/Mastodon.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/Mastodon.kt index 73499648ab..dc9448d52f 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/Mastodon.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/Mastodon.kt @@ -1079,8 +1079,10 @@ internal fun RelationshipResponse.toUi(): UiRelation = following = following ?: false, isFans = followedBy ?: false, blocking = blocking ?: false, + blockedBy = blockedBy ?: false, muted = muting ?: false, hasPendingFollowRequestFromYou = requested ?: false, + hasPendingFollowRequestToYou = requestedBy ?: false, ) private fun parseName(status: Account): UiRichText { diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/VVO.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/VVO.kt index 8a772eaa72..4368affae1 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/VVO.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/VVO.kt @@ -89,7 +89,7 @@ private fun Status.renderStatusV2(accountKey: MicroBlogKey): UiTimelineV2.Post { url = url, width = it.large?.geoValue?.widthValue ?: it.geoValue?.widthValue ?: 0f, height = it.large?.geoValue?.heightValue ?: it.geoValue?.heightValue ?: 0f, - previewUrl = it.url ?: url, + previewUrl = (it.url ?: url).replace("orj360", "mw600"), description = null, sensitive = false, ) @@ -323,7 +323,7 @@ private fun Comment.renderStatusV2(accountKey: MicroBlogKey): UiTimelineV2.Post url = imageUrl, width = it.large?.geoValue?.widthValue ?: it.geoValue?.widthValue ?: 0f, height = it.large?.geoValue?.heightValue ?: it.geoValue?.heightValue ?: 0f, - previewUrl = previewUrl, + previewUrl = previewUrl.replace("orj360", "mw600"), description = null, sensitive = false, ) diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/GroupConfigPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/GroupConfigPresenter.kt index 1a288bc47f..63e2e5f847 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/GroupConfigPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/GroupConfigPresenter.kt @@ -17,7 +17,7 @@ import dev.dimension.flare.data.model.tab.TimelineSlot import dev.dimension.flare.data.model.tab.TimelineSlotContent import dev.dimension.flare.data.model.tab.TimelineTabItemV2 import dev.dimension.flare.data.repository.SettingsRepository -import dev.dimension.flare.ui.model.UiIcon +import dev.dimension.flare.ui.model.TabPickerUiIcons import dev.dimension.flare.ui.presenter.PresenterBase import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -37,12 +37,44 @@ public class GroupConfigPresenter : override fun body(): State { val availableIcons = remember { - UiIcon.entries.map { IconType.Material(it) }.toImmutableList() + TabPickerUiIcons.map { IconType.Material(it) }.toImmutableList() } return object : State { override val availableIcons: ImmutableList = availableIcons + override fun buildGroupItem( + initialItem: GroupTimelineTabItemV2?, + name: String, + icon: IconType, + appearancePatch: AppearancePatch?, + enabled: Boolean, + tabs: List, + mergePolicy: TimelineMergePolicy, + filterConfig: TimelineFilterConfig, + defaultGroupName: String, + ): GroupTimelineTabItemV2? { + val childSlots = + tabs + .distinctBy { it.id } + .map { timelineResolver.toSlot(it) } + if (childSlots.isEmpty()) { + return null + } + return timelineResolver.toTabItem( + buildGroupSlot( + name = name, + icon = icon, + appearancePatch = appearancePatch, + enabled = enabled, + mergePolicy = mergePolicy, + filterConfig = filterConfig, + defaultGroupName = defaultGroupName, + childSlots = childSlots, + ), + ) as GroupTimelineTabItemV2 + } + override fun commit( initialItem: GroupTimelineTabItemV2?, name: String, @@ -78,6 +110,18 @@ public class GroupConfigPresenter : public interface State { public val availableIcons: ImmutableList + public fun buildGroupItem( + initialItem: GroupTimelineTabItemV2?, + name: String, + icon: IconType, + appearancePatch: AppearancePatch?, + enabled: Boolean, + tabs: List, + mergePolicy: TimelineMergePolicy = initialItem?.mergePolicy ?: TimelineMergePolicy.TimePerPage, + filterConfig: TimelineFilterConfig = initialItem?.filterConfig ?: TimelineFilterConfig(), + defaultGroupName: String, + ): GroupTimelineTabItemV2? + public fun commit( initialItem: GroupTimelineTabItemV2?, name: String, diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/TimelinePresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/TimelinePresenter.kt index 00d51731b5..1a3ff4699e 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/TimelinePresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/TimelinePresenter.kt @@ -242,7 +242,13 @@ internal data class TimelinePostTraits( internal fun UiTimelineV2.Post.traits(): TimelinePostTraits { val kinds = buildSet { - if (replyToHandle != null) { + val currentUserKey = user?.key + val hasParentFromOtherUser = + currentUserKey != null && + parents.any { parent -> + parent.user?.key?.let { it != currentUserKey } == true + } + if (hasParentFromOtherUser) { add(TimelinePostKind.Reply) } if (internalRepost != null) { diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/profile/ProfileMediaPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/profile/ProfileMediaPresenter.kt index bc1230a05e..31fe84c030 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/profile/ProfileMediaPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/profile/ProfileMediaPresenter.kt @@ -75,12 +75,12 @@ private class MediaTimelinePresenter( createPager(scope).map { data -> data.flatMap { status -> if (status is UiTimelineV2.Post) { - status.images.map { + status.images.mapIndexed { index, it -> ProfileMedia( it, status, status.statusKey, - status.images.indexOf(it), + index, ) } } else { @@ -101,4 +101,6 @@ public data class ProfileMedia internal constructor( val status: UiTimelineV2, val statusKey: MicroBlogKey, val index: Int, -) +) { + val key: String = "$statusKey-$index" +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/profile/ProfilePresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/profile/ProfilePresenter.kt index dba629d2df..a6a97c39c6 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/profile/ProfilePresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/profile/ProfilePresenter.kt @@ -35,6 +35,7 @@ import dev.dimension.flare.ui.model.onSuccess import dev.dimension.flare.ui.model.takeSuccess import dev.dimension.flare.ui.model.takeSuccessOr import dev.dimension.flare.ui.model.toUi +import dev.dimension.flare.ui.model.zipState import dev.dimension.flare.ui.presenter.PresenterBase import dev.dimension.flare.ui.presenter.home.TimelinePresenter import dev.dimension.flare.ui.presenter.home.UserState @@ -238,10 +239,18 @@ public class ProfilePresenter( val myAccountKey by myAccountKeyFlow.collectAsUiState() val profileMenus by profileMenusFlow.collectAsState(emptyList().toImmutableList()) val tabs by tabsFlow.collectAsUiState() + val followButtonState = + zipState(userState, relationState) { user, relation -> + FollowButtonState.from( + profile = user, + relation = relation, + ) + } return object : ProfileState( userState = userState, relationState = relationState, + followButtonState = followButtonState, isMe = isMe, actions = profileMenus, myAccountKey = myAccountKey, @@ -249,7 +258,10 @@ public class ProfilePresenter( ) { override fun follow(userKey: MicroBlogKey) { service.onSuccess { service -> - (service as RelationDataSource).relationHandler.follow(userKey) + (service as RelationDataSource).relationHandler.follow( + userKey = userKey, + requestFollow = followButtonState.takeSuccess() == FollowButtonState.RequestFollow, + ) } } @@ -424,6 +436,7 @@ private fun buildProfileMenus( public abstract class ProfileState( public val userState: UiState, public val relationState: UiState, + public val followButtonState: UiState, public val isMe: UiState, public val actions: ImmutableList, public val myAccountKey: UiState, @@ -452,6 +465,50 @@ public abstract class ProfileState( } } +@Immutable +public sealed class FollowButtonState { + public abstract val id: String + + @Immutable + public data object Follow : FollowButtonState() { + override val id: String = "follow" + } + + @Immutable + public data object RequestFollow : FollowButtonState() { + override val id: String = "request_follow" + } + + @Immutable + public data object Requested : FollowButtonState() { + override val id: String = "requested" + } + + @Immutable + public data object Following : FollowButtonState() { + override val id: String = "following" + } + + @Immutable + public data object Blocked : FollowButtonState() { + override val id: String = "blocked" + } + + public companion object { + public fun from( + profile: UiProfile, + relation: UiRelation, + ): FollowButtonState = + when { + relation.blocking -> Blocked + relation.following -> Following + relation.hasPendingFollowRequestFromYou -> Requested + UiProfile.Mark.Locked in profile.mark -> RequestFollow + else -> Follow + } + } +} + public class ProfileWithUserNameAndHostPresenter( private val userName: String, private val host: String, diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/status/StatusContextPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/status/StatusContextPresenter.kt index 5ab87cdada..c80806b880 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/status/StatusContextPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/status/StatusContextPresenter.kt @@ -25,6 +25,7 @@ import dev.dimension.flare.ui.model.toUi import dev.dimension.flare.ui.presenter.PresenterBase import dev.dimension.flare.ui.presenter.home.TimelinePresenter import dev.dimension.flare.ui.presenter.home.TimelineState +import dev.dimension.flare.ui.render.UiDateTime import dev.dimension.flare.ui.render.compareTo import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList @@ -111,31 +112,10 @@ public class StatusContextPresenter( override suspend fun transform(data: UiTimelineV2): UiTimelineV2 { val currentCreatedAt = currentStatusFlow.firstOrNull()?.takeSuccess()?.createdAt - if (data !is UiTimelineV2.Post) { - return data - } - return if (( - currentCreatedAt != null && - data.createdAt <= currentCreatedAt - ) || - data.parents.all { it.statusKey == statusKey } - ) { - data.copy( - parents = persistentListOf(), - ) - } else { - data.copy( - parents = - data.parents - .filter { - ( - currentCreatedAt == null || - it.createdAt > currentCreatedAt - ) && - it.statusKey != statusKey - }.toPersistentList(), - ) - } + return data.filterDetailParents( + statusKey = statusKey, + currentCreatedAt = currentCreatedAt, + ) } } } @@ -157,3 +137,34 @@ public class StatusContextPresenter( } } } + +internal fun UiTimelineV2.filterDetailParents( + statusKey: MicroBlogKey, + currentCreatedAt: UiDateTime?, +): UiTimelineV2 { + if (this !is UiTimelineV2.Post) { + return this + } + if ( + this.statusKey == statusKey || + ( + currentCreatedAt != null && + createdAt <= currentCreatedAt + ) + ) { + return copy( + parents = persistentListOf(), + ) + } + return copy( + parents = + parents + .filter { + ( + currentCreatedAt == null || + it.createdAt > currentCreatedAt + ) && + it.statusKey != statusKey + }.toPersistentList(), + ) +} diff --git a/shared/src/commonTest/kotlin/dev/dimension/flare/common/deeplink/DeepLinkMappingTest.kt b/shared/src/commonTest/kotlin/dev/dimension/flare/common/deeplink/DeepLinkMappingTest.kt index 038ef92762..773da8797c 100644 --- a/shared/src/commonTest/kotlin/dev/dimension/flare/common/deeplink/DeepLinkMappingTest.kt +++ b/shared/src/commonTest/kotlin/dev/dimension/flare/common/deeplink/DeepLinkMappingTest.kt @@ -80,10 +80,10 @@ class DeepLinkMappingTest { val patterns = PlatformType.Bluesky.spec.deepLinkPatterns(host) - assertEquals(2, patterns.size) + assertEquals(4, patterns.size) val profile = patterns[0] - assertEquals(DeepLinkMapping.Type.Profile.serializer(), profile.serializer) + assertEquals(DeepLinkMapping.Type.BlueskyProfile.serializer(), profile.serializer) assertEquals(Url("https://$host/profile/{handle}"), profile.uriPattern) assertEquals( listOf("profile" to false, "handle" to true), @@ -101,6 +101,14 @@ class DeepLinkMappingTest { .filter { it.stringValue.isNotEmpty() } .map { it.stringValue to it.isParamArg }, ) + + val bskyAppProfile = patterns[2] + assertEquals(DeepLinkMapping.Type.BlueskyProfile.serializer(), bskyAppProfile.serializer) + assertEquals(Url("https://bsky.app/profile/{handle}"), bskyAppProfile.uriPattern) + + val bskyAppPost = patterns[3] + assertEquals(DeepLinkMapping.Type.BlueskyPost.serializer(), bskyAppPost.serializer) + assertEquals(Url("https://bsky.app/profile/{handle}/post/{id}"), bskyAppPost.uriPattern) } @Test @@ -209,6 +217,34 @@ class DeepLinkMappingTest { assertEquals(DeepLinkMapping.Type.Profile("alice"), matches[account2]) } + @Test + fun bskyAppLinksMatchBlueskyAccountOnExampleComPds() { + val account = + UiAccount.Bluesky( + accountKey = MicroBlogKey(id = "did:plc:alice", host = "example.com"), + ) + val mapping: + ImmutableMap>> = + persistentMapOf( + account to + PlatformType.Bluesky.spec.deepLinkPatterns(account.accountKey.host), + ) + + val profileMatch = DeepLinkMapping.matches("https://bsky.app/profile/example.com", mapping) + assertEquals(1, profileMatch.size) + assertEquals( + DeepLinkMapping.Type.BlueskyProfile("example.com"), + profileMatch[account], + ) + + val postMatch = DeepLinkMapping.matches("https://bsky.app/profile/example.com/post/12345", mapping) + assertEquals(1, postMatch.size) + assertEquals( + DeepLinkMapping.Type.BlueskyPost("example.com", "12345"), + postMatch[account], + ) + } + @Test fun matchesReturnsEmptyForNonMatchingUrl() { val account = @@ -290,7 +326,7 @@ class DeepLinkMappingTest { val bskyProfileMatch = DeepLinkMapping.matches("https://bsky.example/profile/alice.bsky.social", mapping) assertEquals( - DeepLinkMapping.Type.Profile("alice.bsky.social"), + DeepLinkMapping.Type.BlueskyProfile("alice.bsky.social"), bskyProfileMatch[bskyAccount], ) @@ -305,6 +341,22 @@ class DeepLinkMappingTest { bskyPostMatch[bskyAccount], ) + // https://bsky.app/profile/skircle.me + val bskyAppProfileMatch = + DeepLinkMapping.matches("https://bsky.app/profile/skircle.me", mapping) + assertEquals( + DeepLinkMapping.Type.BlueskyProfile("skircle.me"), + bskyAppProfileMatch[bskyAccount], + ) + + // https://bsky.app/profile/skircle.me/post/12345 + val bskyAppPostMatch = + DeepLinkMapping.matches("https://bsky.app/profile/skircle.me/post/12345", mapping) + assertEquals( + DeepLinkMapping.Type.BlueskyPost("skircle.me", "12345"), + bskyAppPostMatch[bskyAccount], + ) + // https://x.example/alice val xProfileMatch = DeepLinkMapping.matches("https://$xqtHost/alice", mapping) assertEquals(DeepLinkMapping.Type.Profile("alice"), xProfileMatch[xAccount]) @@ -354,5 +406,17 @@ class DeepLinkMappingTest { ), post.deepLink(accountKey), ) + + // Bluesky profile keeps the full handle as the profile lookup actor + val blueskyAccountKey = MicroBlogKey(id = "did:plc:alice", host = "example.pds") + val blueskyProfile = DeepLinkMapping.Type.BlueskyProfile("@skircle.me") + assertEquals( + DeeplinkRoute.Profile.UserNameWithHost( + accountType = AccountType.Specific(blueskyAccountKey), + userName = "skircle.me", + host = "example.pds", + ), + blueskyProfile.deepLink(blueskyAccountKey), + ) } } diff --git a/shared/src/commonTest/kotlin/dev/dimension/flare/data/datasource/bluesky/StatusDetailRemoteMediatorTest.kt b/shared/src/commonTest/kotlin/dev/dimension/flare/data/datasource/bluesky/StatusDetailRemoteMediatorTest.kt new file mode 100644 index 0000000000..33743585d2 --- /dev/null +++ b/shared/src/commonTest/kotlin/dev/dimension/flare/data/datasource/bluesky/StatusDetailRemoteMediatorTest.kt @@ -0,0 +1,113 @@ +package dev.dimension.flare.data.datasource.bluesky + +import app.bsky.actor.ProfileViewBasic +import app.bsky.feed.PostView +import app.bsky.unspecced.GetPostThreadV2ThreadItem +import app.bsky.unspecced.GetPostThreadV2ThreadItemValueUnion +import app.bsky.unspecced.ThreadItemPost +import dev.dimension.flare.common.TestFormatter +import dev.dimension.flare.model.MicroBlogKey +import dev.dimension.flare.ui.humanizer.PlatformFormatter +import dev.dimension.flare.ui.model.UiTimelineV2 +import dev.dimension.flare.ui.model.mapper.bskyJson +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject +import org.koin.core.context.startKoin +import org.koin.core.context.stopKoin +import org.koin.dsl.module +import sh.christian.ozone.api.AtUri +import sh.christian.ozone.api.Cid +import sh.christian.ozone.api.Did +import sh.christian.ozone.api.Handle +import sh.christian.ozone.api.model.JsonContent.Companion.encodeAsJsonContent +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.time.Instant + +class StatusDetailRemoteMediatorTest { + private val accountKey = MicroBlogKey(id = "me", host = "bsky.social") + + @BeforeTest + fun setup() { + startKoin { + modules( + module { + single { TestFormatter() } + }, + ) + } + } + + @AfterTest + fun tearDown() { + stopKoin() + } + + @Test + fun renderThread_doesNotRepeatVisiblePostsAsParents() { + val anchor = createPostView("anchor", "anchor post") + val firstReply = createPostView("reply-1", "first reply") + val secondReply = createPostView("reply-2", "second reply") + + val rendered = + listOf( + createThreadItem(anchor, depth = 0), + createThreadItem(firstReply, depth = 1), + createThreadItem(secondReply, depth = 2), + ).renderThread(accountKey) + .filterIsInstance() + + val visibleKeys = rendered.map { it.statusKey }.toSet() + + rendered.forEach { post -> + assertFalse( + post.parents.any { it.statusKey in visibleKeys }, + "Visible thread posts should not be repeated in ${post.statusKey.id} parents", + ) + } + } + + private fun createThreadItem( + post: PostView, + depth: Long, + ): GetPostThreadV2ThreadItem = + GetPostThreadV2ThreadItem( + uri = post.uri, + depth = depth, + value = + GetPostThreadV2ThreadItemValueUnion.Post( + ThreadItemPost( + post = post, + moreParents = false, + moreReplies = 0, + opThread = false, + hiddenByThreadgate = false, + mutedByViewer = false, + ), + ), + ) + + private fun createPostView( + id: String, + text: String, + ): PostView = + PostView( + uri = AtUri("at://did:plc:$id/app.bsky.feed.post/$id"), + cid = Cid("cid-$id"), + author = + ProfileViewBasic( + did = Did("did:plc:$id"), + handle = Handle("$id.bsky.social"), + displayName = id, + ), + record = + bskyJson.encodeAsJsonContent( + buildJsonObject { + put("text", JsonPrimitive(text)) + }, + ), + indexedAt = Instant.parse("2024-01-01T00:00:00Z"), + ) +} diff --git a/shared/src/commonTest/kotlin/dev/dimension/flare/data/datasource/microblog/handler/RelationHandlerTest.kt b/shared/src/commonTest/kotlin/dev/dimension/flare/data/datasource/microblog/handler/RelationHandlerTest.kt index e3b2e30d51..86f6cdd1e7 100644 --- a/shared/src/commonTest/kotlin/dev/dimension/flare/data/datasource/microblog/handler/RelationHandlerTest.kt +++ b/shared/src/commonTest/kotlin/dev/dimension/flare/data/datasource/microblog/handler/RelationHandlerTest.kt @@ -122,6 +122,36 @@ class RelationHandlerTest : RobolectricTest() { assertEquals(1, loader.followCallCount) } + @Test + fun followRequestSuccessSetsPendingRequestTrue() = + runTest { + startKoin { + modules( + module { + single { db } + single { this@runTest } + }, + ) + } + + db.userDao().insertUserRelation( + DbUserRelation( + accountType = AccountType.Specific(accountKey), + userKey = userKey, + relation = UiRelation(following = false, hasPendingFollowRequestFromYou = false), + ), + ) + + handler = RelationHandler(accountType = AccountType.Specific(accountKey), dataSource = loader) + handler.follow(userKey, requestFollow = true) + advanceUntilIdle() + + val saved = assertNotNull(db.userDao().getUserRelation(AccountType.Specific(accountKey), userKey).first()) + assertTrue(saved.relation.following == false) + assertTrue(saved.relation.hasPendingFollowRequestFromYou == true) + assertEquals(1, loader.followCallCount) + } + @Test fun followFailureRevertsFollowingFlag() = runTest { @@ -152,6 +182,35 @@ class RelationHandlerTest : RobolectricTest() { assertEquals(1, loader.followCallCount) } + @Test + fun unfollowSuccessClearsPendingRequest() = + runTest { + startKoin { + modules( + module { + single { db } + single { this@runTest } + }, + ) + } + + db.userDao().insertUserRelation( + DbUserRelation( + accountType = AccountType.Specific(accountKey), + userKey = userKey, + relation = UiRelation(following = false, hasPendingFollowRequestFromYou = true), + ), + ) + + handler = RelationHandler(accountType = AccountType.Specific(accountKey), dataSource = loader) + handler.unfollow(userKey) + advanceUntilIdle() + + val saved = assertNotNull(db.userDao().getUserRelation(AccountType.Specific(accountKey), userKey).first()) + assertTrue(saved.relation.following == false) + assertTrue(saved.relation.hasPendingFollowRequestFromYou == false) + } + @Test fun approveAndRejectFollowRequestUpdateRelationLocally() = runTest { diff --git a/shared/src/commonTest/kotlin/dev/dimension/flare/ui/presenter/home/TimelinePresenterFilterTest.kt b/shared/src/commonTest/kotlin/dev/dimension/flare/ui/presenter/home/TimelinePresenterFilterTest.kt index 8ff4099f69..5547193779 100644 --- a/shared/src/commonTest/kotlin/dev/dimension/flare/ui/presenter/home/TimelinePresenterFilterTest.kt +++ b/shared/src/commonTest/kotlin/dev/dimension/flare/ui/presenter/home/TimelinePresenterFilterTest.kt @@ -5,6 +5,7 @@ import dev.dimension.flare.data.model.tab.TimelineFilterConfig import dev.dimension.flare.data.model.tab.TimelinePostContent import dev.dimension.flare.data.model.tab.TimelinePostKind import dev.dimension.flare.data.repository.KeywordFilterPattern +import dev.dimension.flare.model.MicroBlogKey import dev.dimension.flare.ui.humanizer.PlatformFormatter import dev.dimension.flare.ui.model.UiMedia import dev.dimension.flare.ui.model.createSampleStatus @@ -40,9 +41,12 @@ class TimelinePresenterFilterTest { @Test fun postTraitsCaptureKindsAndContents() { - val base = createSampleStatus(createSampleUser()) + val currentUser = createSampleUser() + val parentUser = currentUser.copy(key = MicroBlogKey("parentKey", "sampleHost")) + val base = createSampleStatus(currentUser) val repost = base.copy(statusKey = base.statusKey.copy(id = "repost")) val quote = base.copy(statusKey = base.statusKey.copy(id = "quote")) + val parent = createSampleStatus(parentUser) val filtered = base.copy( content = "".toUiPlainText(), @@ -64,7 +68,7 @@ class TimelinePresenterFilterTest { width = 100f, ), ), - replyToHandle = "@flare", + parents = persistentListOf(parent), quote = persistentListOf(quote), internalRepost = repost, ) @@ -83,7 +87,9 @@ class TimelinePresenterFilterTest { @Test fun matchesTimelineFilterExcludesConfiguredKindsAndContents() { - val originalTextOnly = createSampleStatus(createSampleUser()) + val currentUser = createSampleUser() + val parentUser = currentUser.copy(key = MicroBlogKey("parentKey", "sampleHost")) + val originalTextOnly = createSampleStatus(currentUser) val replyWithImage = originalTextOnly.copy( statusKey = originalTextOnly.statusKey.copy(id = "reply"), @@ -98,7 +104,7 @@ class TimelinePresenterFilterTest { sensitive = false, ), ), - replyToHandle = "@flare", + parents = persistentListOf(createSampleStatus(parentUser)), ) val filter = TimelineFilterConfig( @@ -110,6 +116,30 @@ class TimelinePresenterFilterTest { assertFalse(replyWithImage.matchesTimelineFilter(filter)) } + @Test + fun postTraitsOnlyMarksReplyWhenParentUserDiffersFromCurrentUser() { + val currentUser = createSampleUser() + val original = createSampleStatus(currentUser) + val selfThread = + original.copy( + statusKey = original.statusKey.copy(id = "self-thread"), + parents = persistentListOf(createSampleStatus(currentUser)), + ) + val replyToOtherUser = + original.copy( + statusKey = original.statusKey.copy(id = "reply-to-other-user"), + parents = + persistentListOf( + createSampleStatus( + currentUser.copy(key = MicroBlogKey("parentKey", "sampleHost")), + ), + ), + ) + + assertFalse(TimelinePostKind.Reply in selfThread.traits().kinds) + assertTrue(TimelinePostKind.Reply in replyToOtherUser.traits().kinds) + } + @Test fun matchesKeywordFiltersUsesRegexForRegexRules() { val status = @@ -141,6 +171,66 @@ class TimelinePresenterFilterTest { ) } + @Test + fun matchesKeywordFiltersChecksInternalRepostContent() { + val base = createSampleStatus(createSampleUser()) + val original = + base.copy( + statusKey = base.statusKey.copy(id = "original"), + content = "Visible original #blocked".toUiPlainText(), + ) + val repostWrapper = + base.copy( + statusKey = original.statusKey.copy(id = "repost"), + content = "".toUiPlainText(), + internalRepost = original, + ) + + assertFalse( + repostWrapper.matchesKeywordFilters( + listOf( + KeywordFilterPattern( + keyword = "#blocked", + isRegex = false, + ), + ), + ), + ) + } + + @Test + fun matchesKeywordFiltersDoesNotSearchNestedInternalRepostContent() { + val base = createSampleStatus(createSampleUser()) + val nestedOriginal = + base.copy( + statusKey = base.statusKey.copy(id = "nested-original"), + content = "Nested original #deepblocked".toUiPlainText(), + ) + val directRepost = + base.copy( + statusKey = base.statusKey.copy(id = "direct-repost"), + content = "".toUiPlainText(), + internalRepost = nestedOriginal, + ) + val repostWrapper = + base.copy( + statusKey = base.statusKey.copy(id = "repost-wrapper"), + content = "".toUiPlainText(), + internalRepost = directRepost, + ) + + assertTrue( + repostWrapper.matchesKeywordFilters( + listOf( + KeywordFilterPattern( + keyword = "#deepblocked", + isRegex = false, + ), + ), + ), + ) + } + @Test fun matchesKeywordFiltersIgnoresInvalidRegexRules() { val status = diff --git a/shared/src/commonTest/kotlin/dev/dimension/flare/ui/presenter/status/StatusContextPresenterTest.kt b/shared/src/commonTest/kotlin/dev/dimension/flare/ui/presenter/status/StatusContextPresenterTest.kt new file mode 100644 index 0000000000..6723442754 --- /dev/null +++ b/shared/src/commonTest/kotlin/dev/dimension/flare/ui/presenter/status/StatusContextPresenterTest.kt @@ -0,0 +1,109 @@ +package dev.dimension.flare.ui.presenter.status + +import dev.dimension.flare.model.AccountType +import dev.dimension.flare.model.MicroBlogKey +import dev.dimension.flare.model.PlatformType +import dev.dimension.flare.ui.model.ClickEvent +import dev.dimension.flare.ui.model.UiTimelineV2 +import dev.dimension.flare.ui.render.toUi +import dev.dimension.flare.ui.render.toUiPlainText +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.time.Instant + +class StatusContextPresenterTest { + private val accountKey = MicroBlogKey("me", "mastodon.example") + private val accountType = AccountType.Specific(accountKey) + private val detailKey = MicroBlogKey("detail", "mastodon.example") + + @Test + fun filterDetailParents_removesParentsFromDetailPostEvenBeforeCurrentLoads() { + val parent = + createPost( + id = "parent", + createdAt = "2024-01-01T00:00:00Z", + ) + val detail = + createPost( + id = detailKey.id, + createdAt = "2024-01-01T00:01:00Z", + parents = listOf(parent), + ) + + val filtered = + detail.filterDetailParents( + statusKey = detailKey, + currentCreatedAt = null, + ) + + val post = filtered as UiTimelineV2.Post + assertTrue(post.parents.isEmpty()) + } + + @Test + fun filterDetailParents_removesCurrentAndOlderParentsFromDescendants() { + val olderParent = + createPost( + id = "older-parent", + createdAt = "2024-01-01T00:00:00Z", + ) + val detail = + createPost( + id = detailKey.id, + createdAt = "2024-01-01T00:01:00Z", + parents = listOf(olderParent), + ) + val newerParent = + createPost( + id = "newer-parent", + createdAt = "2024-01-01T00:02:00Z", + ) + val descendant = + createPost( + id = "descendant", + createdAt = "2024-01-01T00:03:00Z", + parents = listOf(olderParent, detail, newerParent), + ) + + val filtered = + descendant.filterDetailParents( + statusKey = detailKey, + currentCreatedAt = detail.createdAt, + ) + + val post = filtered as UiTimelineV2.Post + assertEquals(listOf(newerParent.statusKey), post.parents.map { it.statusKey }) + } + + private fun createPost( + id: String, + createdAt: String, + parents: List = emptyList(), + ): UiTimelineV2.Post = + UiTimelineV2.Post( + message = null, + platformType = PlatformType.Mastodon, + images = persistentListOf(), + sensitive = false, + contentWarning = null, + user = null, + quote = persistentListOf(), + content = id.toUiPlainText(), + actions = persistentListOf(), + poll = null, + statusKey = MicroBlogKey(id, "mastodon.example"), + card = null, + createdAt = Instant.parse(createdAt).toUi(), + emojiReactions = persistentListOf(), + sourceChannel = null, + visibility = null, + replyToHandle = null, + references = persistentListOf(), + parents = parents.toPersistentList(), + clickEvent = ClickEvent.Noop, + accountType = accountType, + ) +}