feat: add new changelogs system
This commit is contained in:
parent
c6f90f6bb2
commit
990b495044
|
|
@ -275,6 +275,13 @@ dependencies {
|
||||||
implementation(libs.koin.compose)
|
implementation(libs.koin.compose)
|
||||||
implementation(libs.koin.compose.viewmodel.navigation)
|
implementation(libs.koin.compose.viewmodel.navigation)
|
||||||
|
|
||||||
|
implementation(libs.multiplatform.markdown.android)
|
||||||
|
implementation(libs.multiplatform.markdown.m3)
|
||||||
|
implementation(libs.multiplatform.markdown.coil3)
|
||||||
|
|
||||||
|
implementation(libs.coil.compose)
|
||||||
|
implementation(libs.coil.network.okhttp)
|
||||||
|
|
||||||
androidTestImplementation(libs.android.test.core)
|
androidTestImplementation(libs.android.test.core)
|
||||||
androidTestImplementation(libs.android.test.rules)
|
androidTestImplementation(libs.android.test.rules)
|
||||||
androidTestImplementation(libs.compose.ui.test.junit4)
|
androidTestImplementation(libs.compose.ui.test.junit4)
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
package chat.stoat
|
package chat.stoat
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.StrictMode
|
import android.os.StrictMode
|
||||||
import chat.stoat.di.appModule
|
import chat.stoat.di.appModule
|
||||||
import chat.stoat.di.viewModelModule
|
import chat.stoat.di.viewModelModule
|
||||||
|
import coil3.ImageLoader
|
||||||
|
import coil3.SingletonImageLoader
|
||||||
|
import coil3.request.crossfade
|
||||||
import com.google.android.material.color.DynamicColors
|
import com.google.android.material.color.DynamicColors
|
||||||
import logcat.AndroidLogcatLogger
|
import logcat.AndroidLogcatLogger
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
|
|
@ -12,7 +16,7 @@ import org.koin.android.ext.koin.androidContext
|
||||||
import org.koin.android.ext.koin.androidLogger
|
import org.koin.android.ext.koin.androidLogger
|
||||||
import org.koin.core.context.startKoin
|
import org.koin.core.context.startKoin
|
||||||
|
|
||||||
class StoatApplication : Application() {
|
class StoatApplication : Application(), SingletonImageLoader.Factory {
|
||||||
companion object {
|
companion object {
|
||||||
lateinit var instance: StoatApplication
|
lateinit var instance: StoatApplication
|
||||||
}
|
}
|
||||||
|
|
@ -45,6 +49,12 @@ class StoatApplication : Application() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun newImageLoader(context: Context): ImageLoader {
|
||||||
|
return ImageLoader.Builder(context)
|
||||||
|
.crossfade(true)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
instance = this
|
instance = this
|
||||||
DynamicColors.applyToActivitiesIfAvailable(this)
|
DynamicColors.applyToActivitiesIfAvailable(this)
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,7 @@ import chat.stoat.persistence.KVStorage
|
||||||
import chat.stoat.screens.DefaultDestinationScreen
|
import chat.stoat.screens.DefaultDestinationScreen
|
||||||
import chat.stoat.screens.about.AboutScreen
|
import chat.stoat.screens.about.AboutScreen
|
||||||
import chat.stoat.screens.about.AttributionScreen
|
import chat.stoat.screens.about.AttributionScreen
|
||||||
|
import chat.stoat.screens.changelogs.ReadChangelogScreen
|
||||||
import chat.stoat.screens.chat.ChannelPinsScreen
|
import chat.stoat.screens.chat.ChannelPinsScreen
|
||||||
import chat.stoat.screens.chat.ChatRouterScreen
|
import chat.stoat.screens.chat.ChatRouterScreen
|
||||||
import chat.stoat.screens.chat.standalone.CatchUpScreen
|
import chat.stoat.screens.chat.standalone.CatchUpScreen
|
||||||
|
|
@ -734,6 +735,8 @@ fun AppEntrypoint(
|
||||||
composable("about/oss") { AttributionScreen(navController) }
|
composable("about/oss") { AttributionScreen(navController) }
|
||||||
|
|
||||||
composable("labs") { LabsRootScreen(navController) }
|
composable("labs") { LabsRootScreen(navController) }
|
||||||
|
|
||||||
|
composable("changelog/{id}") { ReadChangelogScreen(navController) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
package chat.stoat.api.routes.microservices.gazette
|
||||||
|
|
||||||
|
import chat.stoat.api.HitRateLimitException
|
||||||
|
import chat.stoat.api.StoatHttp
|
||||||
|
import chat.stoat.api.StoatJson
|
||||||
|
import chat.stoat.api.buildUserAgent
|
||||||
|
import chat.stoat.core.model.data.STOAT_CHANGELOG
|
||||||
|
import io.ktor.client.request.get
|
||||||
|
import io.ktor.client.request.header
|
||||||
|
import io.ktor.client.statement.bodyAsText
|
||||||
|
import io.ktor.http.HttpStatusCode
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class GazetteChangelog(
|
||||||
|
val id: String,
|
||||||
|
val title: String,
|
||||||
|
@SerialName("markdown_content") val markdownContent: String,
|
||||||
|
@SerialName("ios_version") val iosVersion: String,
|
||||||
|
@SerialName("android_version") val androidVersion: String,
|
||||||
|
@SerialName("web_version") val webVersion: String,
|
||||||
|
@SerialName("published_at") val publishedAt: String,
|
||||||
|
@SerialName("created_at") val createdAt: String,
|
||||||
|
@SerialName("updated_at") val updatedAt: String
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun getLatestChangelog(): GazetteChangelog {
|
||||||
|
try {
|
||||||
|
val response = StoatHttp.get("$STOAT_CHANGELOG/v1/changelogs/latest") {
|
||||||
|
header("User-Agent", buildUserAgent())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status == HttpStatusCode.OK) {
|
||||||
|
return StoatJson.decodeFromString(response.bodyAsText())
|
||||||
|
} else throw Exception("Failed to query changelog: ${response.status.value} ${response.status.description}")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw Exception("Failed to query changelog: ${e.message}", e).also {
|
||||||
|
if (e is HitRateLimitException) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getChangelogById(id: String): GazetteChangelog {
|
||||||
|
try {
|
||||||
|
val response = StoatHttp.get("$STOAT_CHANGELOG/v1/changelogs/$id") {
|
||||||
|
header("User-Agent", buildUserAgent())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status == HttpStatusCode.OK) {
|
||||||
|
return StoatJson.decodeFromString(response.bodyAsText())
|
||||||
|
} else throw Exception("Failed to query changelog: ${response.status.value} ${response.status.description}")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw Exception("Failed to query changelog: ${e.message}", e).also {
|
||||||
|
if (e is HitRateLimitException) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ import chat.stoat.api.routes.sync.setKey
|
||||||
import chat.stoat.core.model.schemas.AndroidSpecificSettings
|
import chat.stoat.core.model.schemas.AndroidSpecificSettings
|
||||||
import chat.stoat.core.model.schemas.NotificationSettings
|
import chat.stoat.core.model.schemas.NotificationSettings
|
||||||
import chat.stoat.core.model.schemas.OrderingSettings
|
import chat.stoat.core.model.schemas.OrderingSettings
|
||||||
|
import chat.stoat.core.model.schemas.ReleaseNotesSettings
|
||||||
import chat.stoat.core.model.schemas._NotificationSettingsToParse
|
import chat.stoat.core.model.schemas._NotificationSettingsToParse
|
||||||
import kotlinx.serialization.json.JsonPrimitive
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
|
@ -33,6 +34,7 @@ object SyncedSettings {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
private val _notifications = mutableStateOf(NotificationSettings())
|
private val _notifications = mutableStateOf(NotificationSettings())
|
||||||
|
private val _releaseNotes = mutableStateOf(ReleaseNotesSettings())
|
||||||
|
|
||||||
val ordering: OrderingSettings
|
val ordering: OrderingSettings
|
||||||
get() = _ordering.value
|
get() = _ordering.value
|
||||||
|
|
@ -40,11 +42,13 @@ object SyncedSettings {
|
||||||
get() = _android.value
|
get() = _android.value
|
||||||
val notifications: NotificationSettings
|
val notifications: NotificationSettings
|
||||||
get() = _notifications.value
|
get() = _notifications.value
|
||||||
|
val releaseNotes: ReleaseNotesSettings
|
||||||
|
get() = _releaseNotes.value
|
||||||
|
|
||||||
suspend fun fetch(apiToken: String = StoatAPI.sessionToken) {
|
suspend fun fetch(apiToken: String = StoatAPI.sessionToken) {
|
||||||
try {
|
try {
|
||||||
val settings =
|
val settings =
|
||||||
getKeys("ordering", "android", "notifications", token = apiToken)
|
getKeys("ordering", "android", "notifications", "release-notes", token = apiToken)
|
||||||
|
|
||||||
settings["ordering"]?.let {
|
settings["ordering"]?.let {
|
||||||
try {
|
try {
|
||||||
|
|
@ -75,6 +79,18 @@ object SyncedSettings {
|
||||||
// Because it is written in typescript and does what it wants
|
// Because it is written in typescript and does what it wants
|
||||||
_notifications.value = parseNotificationSettings(it.value)
|
_notifications.value = parseNotificationSettings(it.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
settings["release-notes"]?.let {
|
||||||
|
try {
|
||||||
|
_releaseNotes.value = StoatJson.decodeFromString(
|
||||||
|
ReleaseNotesSettings.serializer(),
|
||||||
|
it.value
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LoadedSettings.poorlyFormedSettingsKeys += "release-notes"
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
|
|
@ -122,6 +138,11 @@ object SyncedSettings {
|
||||||
setKey("notifications", StoatJson.encodeToString(NotificationSettings.serializer(), value))
|
setKey("notifications", StoatJson.encodeToString(NotificationSettings.serializer(), value))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun updateReleaseNotes(value: ReleaseNotesSettings) {
|
||||||
|
_releaseNotes.value = value
|
||||||
|
setKey("release-notes", StoatJson.encodeToString(ReleaseNotesSettings.serializer(), value))
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun resetOrdering() {
|
suspend fun resetOrdering() {
|
||||||
val default = OrderingSettings()
|
val default = OrderingSettings()
|
||||||
_ordering.value = default
|
_ordering.value = default
|
||||||
|
|
@ -147,4 +168,13 @@ object SyncedSettings {
|
||||||
StoatJson.encodeToString(NotificationSettings.serializer(), default)
|
StoatJson.encodeToString(NotificationSettings.serializer(), default)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun resetReleaseNotes() {
|
||||||
|
val default = ReleaseNotesSettings()
|
||||||
|
_releaseNotes.value = default
|
||||||
|
setKey(
|
||||||
|
"release-notes",
|
||||||
|
StoatJson.encodeToString(ReleaseNotesSettings.serializer(), default)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
package chat.stoat.composables.markdown.prose
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.mikepenz.markdown.coil3.Coil3ImageTransformerImpl
|
||||||
|
import com.mikepenz.markdown.compose.components.markdownComponents
|
||||||
|
import com.mikepenz.markdown.compose.elements.MarkdownHeader
|
||||||
|
import com.mikepenz.markdown.compose.elements.MarkdownParagraph
|
||||||
|
import com.mikepenz.markdown.m3.Markdown
|
||||||
|
import com.mikepenz.markdown.m3.elements.MarkdownCheckBox
|
||||||
|
import com.mikepenz.markdown.m3.markdownTypography
|
||||||
|
import com.mikepenz.markdown.model.rememberMarkdownState
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ProseMarkdown(markdownText: String, modifier: Modifier = Modifier) {
|
||||||
|
Markdown(
|
||||||
|
markdownState = rememberMarkdownState(content = markdownText),
|
||||||
|
imageTransformer = Coil3ImageTransformerImpl,
|
||||||
|
typography = markdownTypography(
|
||||||
|
h1 = MaterialTheme.typography.displaySmall.copy(lineHeight = 45.sp),
|
||||||
|
h2 = MaterialTheme.typography.titleLarge,
|
||||||
|
h3 = MaterialTheme.typography.titleMedium,
|
||||||
|
h4 = MaterialTheme.typography.bodyMedium,
|
||||||
|
h5 = MaterialTheme.typography.bodySmall,
|
||||||
|
h6 = MaterialTheme.typography.labelSmall,
|
||||||
|
paragraph = MaterialTheme.typography.bodyLarge.copy(lineHeight = 25.sp),
|
||||||
|
),
|
||||||
|
components = markdownComponents(
|
||||||
|
checkbox = {
|
||||||
|
MarkdownCheckBox(
|
||||||
|
it.content,
|
||||||
|
it.node,
|
||||||
|
it.typography.text
|
||||||
|
)
|
||||||
|
},
|
||||||
|
paragraph = {
|
||||||
|
MarkdownParagraph(
|
||||||
|
it.content,
|
||||||
|
it.node,
|
||||||
|
style = it.typography.paragraph,
|
||||||
|
modifier = Modifier.padding(bottom = 16.dp)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
heading1 = {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.padding(top = 8.dp, bottom = 16.dp)
|
||||||
|
) {
|
||||||
|
MarkdownHeader(
|
||||||
|
it.content,
|
||||||
|
it.node,
|
||||||
|
style = it.typography.h1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,193 @@
|
||||||
|
package chat.stoat.screens.changelogs
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.expandIn
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.LoadingIndicator
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import chat.stoat.R
|
||||||
|
import chat.stoat.api.routes.microservices.gazette.GazetteChangelog
|
||||||
|
import chat.stoat.api.routes.microservices.gazette.getChangelogById
|
||||||
|
import chat.stoat.composables.markdown.prose.ProseMarkdown
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.onStart
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.ZoneId
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.time.format.FormatStyle
|
||||||
|
|
||||||
|
sealed interface ReadChangelogScreenUiState {
|
||||||
|
data object Loading : ReadChangelogScreenUiState
|
||||||
|
data class Success(val changelog: GazetteChangelog) : ReadChangelogScreenUiState
|
||||||
|
data class Error(val throwable: Throwable) : ReadChangelogScreenUiState
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReadChangelogScreenViewModel(
|
||||||
|
handle: SavedStateHandle
|
||||||
|
) : ViewModel() {
|
||||||
|
private val id: String = checkNotNull(handle["id"])
|
||||||
|
private val retry = MutableSharedFlow<Unit>(replay = 1).apply { tryEmit(Unit) }
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
val state: StateFlow<ReadChangelogScreenUiState> = retry
|
||||||
|
.flatMapLatest {
|
||||||
|
flow { emit(ReadChangelogScreenUiState.Success(getChangelogById(id)) as ReadChangelogScreenUiState) }
|
||||||
|
.catch { emit(ReadChangelogScreenUiState.Error(it)) }
|
||||||
|
.onStart { emit(ReadChangelogScreenUiState.Loading) }
|
||||||
|
}
|
||||||
|
.stateIn(
|
||||||
|
viewModelScope,
|
||||||
|
SharingStarted.WhileSubscribed(5_000),
|
||||||
|
ReadChangelogScreenUiState.Loading
|
||||||
|
)
|
||||||
|
|
||||||
|
fun retry() {
|
||||||
|
retry.tryEmit(Unit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun rememberDateFormatter(): DateTimeFormatter {
|
||||||
|
val locale = LocalConfiguration.current.locales[0]
|
||||||
|
return remember(locale) {
|
||||||
|
DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(locale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun rememberFormattedDate(isoDateTime: String?): String? {
|
||||||
|
val formatter = rememberDateFormatter()
|
||||||
|
val zone = remember { ZoneId.systemDefault() }
|
||||||
|
return remember(isoDateTime, formatter, zone) {
|
||||||
|
isoDateTime?.let {
|
||||||
|
Instant.parse(it).atZone(zone).toLocalDate().format(formatter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
fun ReadChangelogScreen(
|
||||||
|
navController: NavController,
|
||||||
|
viewModel: ReadChangelogScreenViewModel = viewModel()
|
||||||
|
) {
|
||||||
|
val state = viewModel.state.collectAsState()
|
||||||
|
val formattedDate =
|
||||||
|
rememberFormattedDate((state.value as? ReadChangelogScreenUiState.Success)?.changelog?.publishedAt)
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.changelog_header),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
subtitle = {
|
||||||
|
Box(
|
||||||
|
Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = state.value is ReadChangelogScreenUiState.Success,
|
||||||
|
enter = fadeIn() + expandIn(expandFrom = Alignment.BottomCenter),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
formattedDate ?: stringResource(R.string.unknown),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { navController.popBackStack() }) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_arrow_back_24dp),
|
||||||
|
contentDescription = stringResource(R.string.back)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { pv ->
|
||||||
|
Box(Modifier.padding(pv)) {
|
||||||
|
AnimatedContent(targetState = state.value) { uiState ->
|
||||||
|
when (uiState) {
|
||||||
|
is ReadChangelogScreenUiState.Loading -> Box(
|
||||||
|
Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
LoadingIndicator(
|
||||||
|
modifier = Modifier.size(72.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is ReadChangelogScreenUiState.Error -> Column(
|
||||||
|
Modifier.fillMaxSize(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.changelog_load_error),
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
|
)
|
||||||
|
Button(
|
||||||
|
onClick = { viewModel.retry() },
|
||||||
|
modifier = Modifier.padding(top = 8.dp)
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.changelog_retry))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is ReadChangelogScreenUiState.Success -> {
|
||||||
|
ProseMarkdown(
|
||||||
|
markdownText = uiState.changelog.markdownContent,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package chat.stoat.screens.chat
|
package chat.stoat.screens.chat
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
|
@ -70,11 +71,14 @@ import chat.stoat.api.StoatAPI
|
||||||
import chat.stoat.api.internals.DirectMessages
|
import chat.stoat.api.internals.DirectMessages
|
||||||
import chat.stoat.api.realtime.DisconnectionState
|
import chat.stoat.api.realtime.DisconnectionState
|
||||||
import chat.stoat.api.realtime.RealtimeSocket
|
import chat.stoat.api.realtime.RealtimeSocket
|
||||||
|
import chat.stoat.api.routes.microservices.gazette.getLatestChangelog
|
||||||
import chat.stoat.api.routes.push.subscribePush
|
import chat.stoat.api.routes.push.subscribePush
|
||||||
|
import chat.stoat.api.settings.SyncedSettings
|
||||||
import chat.stoat.callbacks.Action
|
import chat.stoat.callbacks.Action
|
||||||
import chat.stoat.callbacks.ActionChannel
|
import chat.stoat.callbacks.ActionChannel
|
||||||
import chat.stoat.composables.chat.DisconnectedNotice
|
import chat.stoat.composables.chat.DisconnectedNotice
|
||||||
import chat.stoat.composables.screens.chat.drawer.ChannelSideDrawer
|
import chat.stoat.composables.screens.chat.drawer.ChannelSideDrawer
|
||||||
|
import chat.stoat.core.model.schemas.ReleaseNotesSettings
|
||||||
import chat.stoat.dialogs.NotificationRationaleDialog
|
import chat.stoat.dialogs.NotificationRationaleDialog
|
||||||
import chat.stoat.internals.extensions.zero
|
import chat.stoat.internals.extensions.zero
|
||||||
import chat.stoat.persistence.KVStorage
|
import chat.stoat.persistence.KVStorage
|
||||||
|
|
@ -100,6 +104,10 @@ import com.google.firebase.messaging.FirebaseMessaging
|
||||||
import io.sentry.Sentry
|
import io.sentry.Sentry
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
import logcat.LogPriority
|
||||||
|
import logcat.logcat
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
|
||||||
sealed class ChatRouterDestination {
|
sealed class ChatRouterDestination {
|
||||||
|
|
@ -139,17 +147,17 @@ sealed class ChatRouterDestination {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("StaticFieldLeak")
|
||||||
class ChatRouterViewModel(
|
class ChatRouterViewModel(
|
||||||
private val kvStorage: KVStorage,
|
private val kvStorage: KVStorage,
|
||||||
val context: Context
|
val context: Context,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
var currentDestination by mutableStateOf<ChatRouterDestination>(ChatRouterDestination.default)
|
var currentDestination by mutableStateOf<ChatRouterDestination>(ChatRouterDestination.default)
|
||||||
var latestChangelogRead by mutableStateOf(true)
|
|
||||||
var latestChangelog by mutableStateOf("")
|
|
||||||
var latestChangelogBody by mutableStateOf("")
|
|
||||||
var showNotificationRationale by mutableStateOf(false)
|
var showNotificationRationale by mutableStateOf(false)
|
||||||
var showEarlyAccessSpark by mutableStateOf(false)
|
var showEarlyAccessSpark by mutableStateOf(false)
|
||||||
var showSwipeToReplySpark by mutableStateOf(false)
|
var showSwipeToReplySpark by mutableStateOf(false)
|
||||||
|
var showChangelogScreenForId by mutableStateOf<String?>(null)
|
||||||
|
private var changelogCheckDone = false
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
|
@ -247,6 +255,40 @@ class ChatRouterViewModel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun maybeShowChangelog() {
|
||||||
|
if (changelogCheckDone) return
|
||||||
|
changelogCheckDone = true
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
val latestChangelog = runCatching { getLatestChangelog() }
|
||||||
|
.onFailure {
|
||||||
|
logcat(LogPriority.ERROR) { "Failed to fetch latest changelog: ${it.message}" }
|
||||||
|
}
|
||||||
|
.getOrNull()
|
||||||
|
|
||||||
|
if (latestChangelog != null) {
|
||||||
|
val isInFuture =
|
||||||
|
runCatching { Instant.parse(latestChangelog.publishedAt) > Clock.System.now() }.getOrNull()
|
||||||
|
?: false
|
||||||
|
if (isInFuture) {
|
||||||
|
logcat(LogPriority.WARN) { "Latest changelog is from the future (${latestChangelog.publishedAt} > ${Clock.System.now()}), not showing it!" }
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
val lastSeenChangelog = SyncedSettings.releaseNotes.lastSeenId
|
||||||
|
if (lastSeenChangelog == null || lastSeenChangelog != latestChangelog.id) {
|
||||||
|
showChangelogScreenForId = latestChangelog.id
|
||||||
|
SyncedSettings.updateReleaseNotes(
|
||||||
|
ReleaseNotesSettings(
|
||||||
|
lastSeenId = latestChangelog.id,
|
||||||
|
lastSeenAt = Clock.System.now().toString()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val LocalIsConnected = compositionLocalOf(structuralEqualityPolicy()) { false }
|
val LocalIsConnected = compositionLocalOf(structuralEqualityPolicy()) { false }
|
||||||
|
|
@ -360,6 +402,21 @@ fun ChatRouterScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
viewModel.maybeShowChangelog()
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
snapshotFlow { viewModel.showChangelogScreenForId }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.collect { changelogId ->
|
||||||
|
if (changelogId != null) {
|
||||||
|
viewModel.showChangelogScreenForId = null
|
||||||
|
topNav.navigate("changelog/${changelogId}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(DirectMessages.unreadDMs()) {
|
LaunchedEffect(DirectMessages.unreadDMs()) {
|
||||||
snapshotFlow { DirectMessages.unreadDMs() }
|
snapshotFlow { DirectMessages.unreadDMs() }
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package chat.stoat.screens.chat.views
|
package chat.stoat.screens.chat.views
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
|
@ -58,8 +58,10 @@ import androidx.compose.ui.platform.LocalResources
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.net.toUri
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import chat.stoat.R
|
import chat.stoat.R
|
||||||
|
import chat.stoat.activities.InviteActivity
|
||||||
import chat.stoat.api.StoatAPI
|
import chat.stoat.api.StoatAPI
|
||||||
import chat.stoat.api.routes.user.fetchSelf
|
import chat.stoat.api.routes.user.fetchSelf
|
||||||
import chat.stoat.composables.generic.NonIdealState
|
import chat.stoat.composables.generic.NonIdealState
|
||||||
|
|
@ -331,8 +333,7 @@ fun OverviewScreen(
|
||||||
item(key = "changelog") {
|
item(key = "changelog") {
|
||||||
OverviewScreenLink(
|
OverviewScreenLink(
|
||||||
onClick = {
|
onClick = {
|
||||||
// navController.navigate("settings/changelogs")
|
navController.navigate("changelog/latest")
|
||||||
// TODO replace with new changelog
|
|
||||||
},
|
},
|
||||||
backgroundColour = MaterialTheme.colorScheme.errorContainer,
|
backgroundColour = MaterialTheme.colorScheme.errorContainer,
|
||||||
foregroundColour = MaterialTheme.colorScheme.onErrorContainer,
|
foregroundColour = MaterialTheme.colorScheme.onErrorContainer,
|
||||||
|
|
@ -342,7 +343,7 @@ fun OverviewScreen(
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(R.drawable.ic_wand_shine_24dp),
|
painter = painterResource(R.drawable.ic_campaign_24dp),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(22.dp)
|
modifier = Modifier.size(22.dp)
|
||||||
)
|
)
|
||||||
|
|
@ -353,15 +354,16 @@ fun OverviewScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
item(key = "feedback") {
|
item(key = "join-lounge") {
|
||||||
OverviewScreenLink(
|
OverviewScreenLink(
|
||||||
onClick = {
|
onClick = {
|
||||||
Toast.makeText(
|
val intent = Intent(
|
||||||
context,
|
context,
|
||||||
resources.getString(R.string.comingsoon_toast),
|
InviteActivity::class.java
|
||||||
Toast.LENGTH_SHORT
|
).setAction(Intent.ACTION_VIEW)
|
||||||
).show()
|
|
||||||
// navController.navigate("feedback")
|
intent.data = "https://stt.gg/Testers".toUri()
|
||||||
|
context.startActivity(intent)
|
||||||
},
|
},
|
||||||
backgroundColour = MaterialTheme.colorScheme.primary,
|
backgroundColour = MaterialTheme.colorScheme.primary,
|
||||||
foregroundColour = MaterialTheme.colorScheme.onPrimary,
|
foregroundColour = MaterialTheme.colorScheme.onPrimary,
|
||||||
|
|
@ -371,14 +373,14 @@ fun OverviewScreen(
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(R.drawable.ic_star_shine_24dp),
|
painter = painterResource(R.drawable.ic_waving_hand_24dp),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(22.dp)
|
modifier = Modifier.size(22.dp)
|
||||||
)
|
)
|
||||||
Text(stringResource(R.string.overview_screen_feedback))
|
Text(stringResource(R.string.overview_screen_join_lounge))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
body = { Text(stringResource(R.string.overview_screen_feedback_description)) }
|
body = { Text(stringResource(R.string.overview_screen_join_lounge_description)) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,7 @@ fun ChatSettingsScreen(
|
||||||
"ordering" -> stringResource(R.string.settings_chat_hint_poorly_formed_settings_keys_key_ordering)
|
"ordering" -> stringResource(R.string.settings_chat_hint_poorly_formed_settings_keys_key_ordering)
|
||||||
"android" -> stringResource(R.string.settings_chat_hint_poorly_formed_settings_keys_key_android)
|
"android" -> stringResource(R.string.settings_chat_hint_poorly_formed_settings_keys_key_android)
|
||||||
"notifications" -> stringResource(R.string.settings_chat_hint_poorly_formed_settings_keys_key_notifications)
|
"notifications" -> stringResource(R.string.settings_chat_hint_poorly_formed_settings_keys_key_notifications)
|
||||||
|
"release-notes" -> stringResource(R.string.settings_chat_hint_poorly_formed_settings_keys_key_release_notes)
|
||||||
else -> stringResource(
|
else -> stringResource(
|
||||||
R.string.settings_chat_hint_poorly_formed_settings_keys_key_unknown,
|
R.string.settings_chat_hint_poorly_formed_settings_keys_key_unknown,
|
||||||
key
|
key
|
||||||
|
|
@ -137,7 +138,7 @@ fun ChatSettingsScreen(
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
for (key in LoadedSettings.poorlyFormedSettingsKeys.filter {
|
for (key in LoadedSettings.poorlyFormedSettingsKeys.filter {
|
||||||
it in setOf("ordering", "android", "notifications")
|
it in setOf("ordering", "android", "notifications", "release-notes")
|
||||||
}) {
|
}) {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
|
|
@ -146,6 +147,7 @@ fun ChatSettingsScreen(
|
||||||
"ordering" -> SyncedSettings.resetOrdering()
|
"ordering" -> SyncedSettings.resetOrdering()
|
||||||
"android" -> SyncedSettings.resetAndroid()
|
"android" -> SyncedSettings.resetAndroid()
|
||||||
"notifications" -> SyncedSettings.resetNotifications()
|
"notifications" -> SyncedSettings.resetNotifications()
|
||||||
|
"release-notes" -> SyncedSettings.resetReleaseNotes()
|
||||||
}
|
}
|
||||||
LoadedSettings.poorlyFormedSettingsKeys -= key
|
LoadedSettings.poorlyFormedSettingsKeys -= key
|
||||||
}
|
}
|
||||||
|
|
@ -158,6 +160,7 @@ fun ChatSettingsScreen(
|
||||||
"ordering" -> stringResource(R.string.settings_chat_hint_poorly_formed_settings_keys_key_ordering)
|
"ordering" -> stringResource(R.string.settings_chat_hint_poorly_formed_settings_keys_key_ordering)
|
||||||
"android" -> stringResource(R.string.settings_chat_hint_poorly_formed_settings_keys_key_android)
|
"android" -> stringResource(R.string.settings_chat_hint_poorly_formed_settings_keys_key_android)
|
||||||
"notifications" -> stringResource(R.string.settings_chat_hint_poorly_formed_settings_keys_key_notifications)
|
"notifications" -> stringResource(R.string.settings_chat_hint_poorly_formed_settings_keys_key_notifications)
|
||||||
|
"release-notes" -> stringResource(R.string.settings_chat_hint_poorly_formed_settings_keys_key_release_notes)
|
||||||
else -> key
|
else -> key
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ import androidx.lifecycle.viewModelScope
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import chat.stoat.R
|
import chat.stoat.R
|
||||||
import chat.stoat.api.routes.push.subscribePush
|
import chat.stoat.api.routes.push.subscribePush
|
||||||
|
import chat.stoat.api.settings.SyncedSettings
|
||||||
import chat.stoat.dialogs.NotificationRationaleDialog
|
import chat.stoat.dialogs.NotificationRationaleDialog
|
||||||
import chat.stoat.persistence.Database
|
import chat.stoat.persistence.Database
|
||||||
import chat.stoat.persistence.KVStorage
|
import chat.stoat.persistence.KVStorage
|
||||||
|
|
@ -88,7 +89,7 @@ class DebugSettingsScreenViewModel(
|
||||||
|
|
||||||
fun forgetLatestChangelog() {
|
fun forgetLatestChangelog() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
kvStorage.remove("latestChangelogRead")
|
SyncedSettings.resetReleaseNotes()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -276,7 +277,7 @@ fun DebugSettingsScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "Changelogs",
|
text = "Release Notes (Gazette)",
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
modifier = Modifier.padding(bottom = 10.dp)
|
modifier = Modifier.padding(bottom = 10.dp)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -315,6 +315,27 @@ fun SettingsScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.settings_changelog)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leadingContent = {
|
||||||
|
SettingsIcon {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_campaign_24dp),
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.testTag("settings_view_changelog")
|
||||||
|
.clickable {
|
||||||
|
navController.navigate("changelog/latest")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
Text(
|
Text(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package chat.stoat.sheets
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChangelogSheet(modifier: Modifier = Modifier) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -149,10 +149,10 @@
|
||||||
<string name="overview_screen_settings_description">Change your look, manage your account, and more</string>
|
<string name="overview_screen_settings_description">Change your look, manage your account, and more</string>
|
||||||
<string name="overview_screen_share_profile">Share Profile</string>
|
<string name="overview_screen_share_profile">Share Profile</string>
|
||||||
<string name="overview_screen_share_profile_description">Take a look at your user card and get a special link to share</string>
|
<string name="overview_screen_share_profile_description">Take a look at your user card and get a special link to share</string>
|
||||||
<string name="overview_screen_changelog">Changelog</string>
|
<string name="overview_screen_changelog">What\'s New?</string>
|
||||||
<string name="overview_screen_changelog_description">Stoat never sleeps, take a look to see what\'s been in the works</string>
|
<string name="overview_screen_changelog_description">See what we\'ve been working on recently and what\'s coming up next</string>
|
||||||
<string name="overview_screen_feedback">Provide Feedback</string>
|
<string name="overview_screen_join_lounge">Join the Stoat server</string>
|
||||||
<string name="overview_screen_feedback_description">Got something to say? We\'re all ears</string>
|
<string name="overview_screen_join_lounge_description">Hang out with other Stoat users, give feedback, and see what\'s happening with Stoat</string>
|
||||||
<string name="overview_screen_catch_up">Catch Up</string>
|
<string name="overview_screen_catch_up">Catch Up</string>
|
||||||
<string name="overview_screen_catch_up_description">See what you\'ve missed while you were away</string>
|
<string name="overview_screen_catch_up_description">See what you\'ve missed while you were away</string>
|
||||||
<string name="overview_screen_catch_up_none">All caught up</string>
|
<string name="overview_screen_catch_up_none">All caught up</string>
|
||||||
|
|
@ -770,16 +770,14 @@
|
||||||
<string name="settings_chat_hint_poorly_formed_settings_keys_key_notifications">Notifications</string>
|
<string name="settings_chat_hint_poorly_formed_settings_keys_key_notifications">Notifications</string>
|
||||||
<string name="settings_chat_hint_poorly_formed_settings_keys_key_ordering">Server Ordering</string>
|
<string name="settings_chat_hint_poorly_formed_settings_keys_key_ordering">Server Ordering</string>
|
||||||
<string name="settings_chat_hint_poorly_formed_settings_keys_key_android">Stoat for Android</string>
|
<string name="settings_chat_hint_poorly_formed_settings_keys_key_android">Stoat for Android</string>
|
||||||
|
<string name="settings_chat_hint_poorly_formed_settings_keys_key_release_notes">Release Notes</string>
|
||||||
<string name="settings_chat_hint_poorly_formed_settings_keys_key_unknown">%1$s (unknown)</string>
|
<string name="settings_chat_hint_poorly_formed_settings_keys_key_unknown">%1$s (unknown)</string>
|
||||||
<string name="settings_chat_hint_poorly_formed_settings_keys_reset">Reset %1$s settings</string>
|
<string name="settings_chat_hint_poorly_formed_settings_keys_reset">Reset %1$s settings</string>
|
||||||
|
|
||||||
<string name="settings_feedback">Feedback</string>
|
<string name="settings_feedback">Feedback</string>
|
||||||
<string name="settings_feedback_description">Join the Stoat server to give feedback or suggestions and report bugs.</string>
|
<string name="settings_feedback_description">Join the Stoat server to give feedback or suggestions and report bugs.</string>
|
||||||
|
|
||||||
<string name="settings_changelogs">Changelogs</string>
|
<string name="settings_changelog">What\'s New</string>
|
||||||
<string name="settings_changelogs_new_header">What\'s been cooking ✨</string>
|
|
||||||
<string name="settings_changelogs_historical_version_header">Changelog for %1$s</string>
|
|
||||||
<string name="settings_changelogs_historical_version_header_placeholder">that version</string>
|
|
||||||
|
|
||||||
<string name="settings_language">Language</string>
|
<string name="settings_language">Language</string>
|
||||||
<string name="settings_language_auto">System default</string>
|
<string name="settings_language_auto">System default</string>
|
||||||
|
|
@ -833,4 +831,8 @@
|
||||||
<string name="geogate_description">Stoat may block content in certain jurisdictions in response to legislation or legal notices</string>
|
<string name="geogate_description">Stoat may block content in certain jurisdictions in response to legislation or legal notices</string>
|
||||||
<string name="geogate_description_variant_osa_uk_25" translatable="false">This channel is not available in your region while we review options on legal compliance.</string>
|
<string name="geogate_description_variant_osa_uk_25" translatable="false">This channel is not available in your region while we review options on legal compliance.</string>
|
||||||
<string name="geogate_acknowledge">Acknowledge</string>
|
<string name="geogate_acknowledge">Acknowledge</string>
|
||||||
|
|
||||||
|
<string name="changelog_header">What\'s new</string>
|
||||||
|
<string name="changelog_load_error">There was an error loading the changelog.</string>
|
||||||
|
<string name="changelog_retry">Retry</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,12 @@ data class _NotificationSettingsToParse( // quirk
|
||||||
val server: Map<String, JsonElement?> = emptyMap()
|
val server: Map<String, JsonElement?> = emptyMap()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ReleaseNotesSettings(
|
||||||
|
val lastSeenId: String? = null,
|
||||||
|
val lastSeenAt: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class AndroidSpecificSettingsSpecialEmbedSettings(
|
data class AndroidSpecificSettingsSpecialEmbedSettings(
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@ androidx-test = "1.6.1"
|
||||||
livekit = "2.24.1"
|
livekit = "2.24.1"
|
||||||
livekit-compose = "2.3.0"
|
livekit-compose = "2.3.0"
|
||||||
koin = "4.2.1"
|
koin = "4.2.1"
|
||||||
|
multiplatform-markdown = "0.41.0-b01"
|
||||||
|
coil = "3.4.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
android-core-ktx = { module = "androidx.core:core-ktx", version = "1.16.0" }
|
android-core-ktx = { module = "androidx.core:core-ktx", version = "1.16.0" }
|
||||||
|
|
@ -101,6 +103,11 @@ livekit-android-compose = { module = "io.livekit:livekit-android-compose-compone
|
||||||
koin = { module = "io.insert-koin:koin-android", version.ref = "koin" }
|
koin = { module = "io.insert-koin:koin-android", version.ref = "koin" }
|
||||||
koin-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koin" }
|
koin-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koin" }
|
||||||
koin-compose-viewmodel-navigation = { module = "io.insert-koin:koin-compose-viewmodel-navigation", version.ref = "koin" }
|
koin-compose-viewmodel-navigation = { module = "io.insert-koin:koin-compose-viewmodel-navigation", version.ref = "koin" }
|
||||||
|
multiplatform-markdown-android = { module = "com.mikepenz:multiplatform-markdown-renderer-android", version.ref = "multiplatform-markdown" }
|
||||||
|
multiplatform-markdown-m3 = { module = "com.mikepenz:multiplatform-markdown-renderer-m3", version.ref = "multiplatform-markdown" }
|
||||||
|
multiplatform-markdown-coil3 = { module = "com.mikepenz:multiplatform-markdown-renderer-coil3", version.ref = "multiplatform-markdown" }
|
||||||
|
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
|
||||||
|
coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue