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:
+
+ Deck Mode bygevoeg vir ’n meerkolom-tydlynervaring.
+ Ryker tydlynoortjie-aanpassing bygevoeg, insluitend ikone, filters, groepe en voorkoms per oortjie.
+ Regex-ondersteuning vir plaaslike sleutelwoordfilters bygevoeg.
+ Werkverrigtingverbeterings en foutoplossings.
+
+ ]]>
+
+
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:
+
+ Добавен е Deck Mode за времева линия с няколко колони.
+ Добавено е по-богато персонализиране на разделите във времевата линия, включително икони, филтри, групи и външен вид за всеки раздел.
+ Добавена е поддръжка на regex за локални филтри по ключови думи.
+ Подобрения в производителността и корекции на грешки.
+
+ ]]>
+
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:
+
+ S’ha afegit el Deck Mode per a una experiència de línia de temps amb diverses columnes.
+ S’ha afegit una personalització més completa de les pestanyes de la línia de temps, incloent-hi icones, filtres, grups i aparença per pestanya.
+ S’ha afegit compatibilitat amb regex per als filtres locals de paraules clau.
+ Millores de rendiment i correccions d’errors.
+
+ ]]>
+
+
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:
+
+ Přidán Deck Mode pro vícesloupcové zobrazení časové osy.
+ Přidáno bohatší přizpůsobení karet časové osy včetně ikon, filtrů, skupin a vzhledu jednotlivých karet.
+ Přidána podpora regex pro místní filtry klíčových slov.
+ Vylepšení výkonu a opravy chyb.
+
+ ]]>
+
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ó:
+
+ Új Deck Mode a többoszlopos idővonalnézethez.
+ Részletesebb idővonallap-testreszabás ikonokkal, szűrőkkel, csoportokkal és laponkénti megjelenéssel.
+ Regex-támogatás a helyi kulcsszószűrőkhöz.
+ Teljesítményjavítások és hibajavítások.
+
+ ]]>
+
+
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:
+
+ נוסף Deck Mode לחוויית ציר זמן מרובת עמודות.
+ נוספה התאמה אישית עשירה יותר ללשוניות ציר הזמן, כולל סמלים, מסננים, קבוצות ומראה לכל לשונית.
+ נוספה תמיכה ב-regex למסנני מילות מפתח מקומיים.
+ שיפורי ביצועים ותיקוני באגים.
+
+ ]]>
+
+
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:
+
+ 複数列のタイムラインを利用できる Deck Mode を追加しました。
+ アイコン、フィルター、グループ、タブごとの外観など、タイムラインタブのカスタマイズを強化しました。
+ ローカルキーワードフィルターで正規表現をサポートしました。
+ パフォーマンス改善と不具合修正を行いました。
+
+ ]]>
+
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:
+
+ 여러 열 타임라인을 위한 Deck Mode를 추가했습니다.
+ 아이콘, 필터, 그룹, 탭별 모양을 포함한 타임라인 탭 사용자 지정을 강화했습니다.
+ 로컬 키워드 필터에 regex 지원을 추가했습니다.
+ 성능 개선 및 버그 수정.
+
+ ]]>
+
+
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:
+
+ Adicionado Deck Mode para uma experiência de linha do tempo em várias colunas.
+ Adicionada personalização mais completa das abas da linha do tempo, incluindo ícones, filtros, grupos e aparência por aba.
+ Adicionado suporte a regex para filtros locais de palavras-chave.
+ Melhorias de desempenho e correções de bugs.
+
+ ]]>
+
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:
+
+ Adicionado Deck Mode para uma experiência de linha temporal em várias colunas.
+ Adicionada personalização mais completa dos separadores da linha temporal, incluindo ícones, filtros, grupos e aparência por separador.
+ Adicionado suporte a regex para filtros locais de palavras-chave.
+ Melhorias de desempenho e correções de erros.
+
+ ]]>
+
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:
+
+ Додат је Deck Mode за временску линију са више колона.
+ Додато је богатије прилагођавање картица временске линије, укључујући иконе, филтере, групе и изглед по картици.
+ Додата је regex подршка за локалне филтере кључних речи.
+ Побољшања перформанси и исправке грешака.
+
+ ]]>
+
+
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:
+
+ Çok sütunlu zaman akışı deneyimi için Deck Mode eklendi.
+ Simgeler, filtreler, gruplar ve sekmeye özel görünüm dahil olmak üzere zaman akışı sekmeleri için daha zengin özelleştirme eklendi.
+ Yerel anahtar kelime filtreleri için regex desteği eklendi.
+ Performans iyileştirmeleri ve hata düzeltmeleri.
+
+ ]]>
+
+
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:
+
+ Додано Deck Mode для багатоколонкової стрічки.
+ Додано розширене налаштування вкладок стрічки, зокрема іконки, фільтри, групи та вигляд для кожної вкладки.
+ Додано підтримку regex для локальних фільтрів ключових слів.
+ Покращення продуктивності та виправлення помилок.
+
+ ]]>
+
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:
+
+ Đã thêm Deck Mode cho trải nghiệm dòng thời gian nhiều cột.
+ Đã thêm tùy chỉnh tab dòng thời gian phong phú hơn, bao gồm biểu tượng, bộ lọc, nhóm và giao diện theo từng tab.
+ Đã thêm hỗ trợ regex cho bộ lọc từ khóa cục bộ.
+ Cải thiện hiệu năng và sửa lỗi.
+
+ ]]>
+
+
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:
+
+ 新增 Deck Mode,帶來多欄時間軸體驗。
+ 新增更豐富的時間軸分頁自訂,包括圖示、篩選器、群組和單一分頁外觀。
+ 本機關鍵字篩選新增正規表示式支援。
+ 效能改善與錯誤修正。
+
+ ]]>
+
+
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:
- Added new timeline display modes, including a Gallery layout.
- Improved RSS reading, sharing, translation, and summaries.
- Reorganized Appearance settings for easier customization.
- Bug fixes and performance improvements.
+ Added Deck Mode for a multi-column timeline experience.
+ Added richer timeline tab customization, including icons, filters, groups, and per-tab appearance.
+ Added regex support for local keyword filters.
+ Performance improvements and bug fixes.
]]>
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,
+ )
+}