mirror of
https://github.com/ouchadam/small-talk.git
synced 2025-02-16 12:10:45 +01:00
Merge pull request #196 from ouchadam/feature/more-error-handling
More error handling
This commit is contained in:
commit
65957f6854
@ -3,6 +3,13 @@ package app.dapk.st.core.extensions
|
|||||||
inline fun <T> T?.ifNull(block: () -> T): T = this ?: block()
|
inline fun <T> T?.ifNull(block: () -> T): T = this ?: block()
|
||||||
inline fun <T> ifOrNull(condition: Boolean, block: () -> T): T? = if (condition) block() else null
|
inline fun <T> ifOrNull(condition: Boolean, block: () -> T): T? = if (condition) block() else null
|
||||||
|
|
||||||
|
inline fun <reified T> Any.takeAs(): T? {
|
||||||
|
return when (this) {
|
||||||
|
is T -> this
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
inline fun <T, T1 : T, T2 : T> Iterable<T>.firstOrNull(predicate: (T) -> Boolean, predicate2: (T) -> Boolean): Pair<T1, T2>? {
|
inline fun <T, T1 : T, T2 : T> Iterable<T>.firstOrNull(predicate: (T) -> Boolean, predicate2: (T) -> Boolean): Pair<T1, T2>? {
|
||||||
var firstValue: T1? = null
|
var firstValue: T1? = null
|
||||||
|
@ -1,21 +1,46 @@
|
|||||||
package app.dapk.st.design.components
|
package app.dapk.st.design.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun GenericError(retryAction: () -> Unit) {
|
fun GenericError(message: String = "Something went wrong...", label: String = "Retry", cause: Throwable? = null, action: () -> Unit) {
|
||||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
val moreDetails = cause?.let { "${it::class.java.simpleName}: ${it.message}" }
|
||||||
|
|
||||||
|
val openDetailsDialog = remember { mutableStateOf(false) }
|
||||||
|
if (openDetailsDialog.value) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { openDetailsDialog.value = false },
|
||||||
|
confirmButton = {
|
||||||
|
Button(onClick = { openDetailsDialog.value = false }) {
|
||||||
|
Text("OK")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = { Text("Details") },
|
||||||
|
text = {
|
||||||
|
Text(moreDetails!!)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
Text("Something went wrong...")
|
Text(message)
|
||||||
Button(onClick = { retryAction() }) {
|
if (moreDetails != null) {
|
||||||
Text("Retry")
|
Text("Tap for more details".uppercase(), fontSize = 12.sp, modifier = Modifier.clickable { openDetailsDialog.value = true }.padding(12.dp))
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
Button(onClick = { action() }) {
|
||||||
|
Text(label.uppercase())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,22 +10,27 @@ fun <T : Any> Spider(currentPage: SpiderPage<T>, onNavigate: (SpiderPage<out T>?
|
|||||||
val pageCache = remember { mutableMapOf<Route<*>, SpiderPage<out T>>() }
|
val pageCache = remember { mutableMapOf<Route<*>, SpiderPage<out T>>() }
|
||||||
pageCache[currentPage.route] = currentPage
|
pageCache[currentPage.route] = currentPage
|
||||||
|
|
||||||
|
val navigateAndPopStack = {
|
||||||
|
pageCache.remove(currentPage.route)
|
||||||
|
onNavigate(pageCache[currentPage.parent])
|
||||||
|
}
|
||||||
|
val itemScope = object : SpiderItemScope {
|
||||||
|
override fun goBack() {
|
||||||
|
navigateAndPopStack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val computedWeb = remember(true) {
|
val computedWeb = remember(true) {
|
||||||
mutableMapOf<Route<*>, @Composable (T) -> Unit>().also { computedWeb ->
|
mutableMapOf<Route<*>, @Composable (T) -> Unit>().also { computedWeb ->
|
||||||
val scope = object : SpiderScope {
|
val scope = object : SpiderScope {
|
||||||
override fun <T> item(route: Route<T>, content: @Composable (T) -> Unit) {
|
override fun <T> item(route: Route<T>, content: @Composable SpiderItemScope.(T) -> Unit) {
|
||||||
computedWeb[route] = { content(it as T) }
|
computedWeb[route] = { content(itemScope, it as T) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
graph.invoke(scope)
|
graph.invoke(scope)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val navigateAndPopStack = {
|
|
||||||
pageCache.remove(currentPage.route)
|
|
||||||
onNavigate(pageCache[currentPage.parent])
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
if (currentPage.hasToolbar) {
|
if (currentPage.hasToolbar) {
|
||||||
Toolbar(
|
Toolbar(
|
||||||
@ -40,7 +45,11 @@ fun <T : Any> Spider(currentPage: SpiderPage<T>, onNavigate: (SpiderPage<out T>?
|
|||||||
|
|
||||||
|
|
||||||
interface SpiderScope {
|
interface SpiderScope {
|
||||||
fun <T> item(route: Route<T>, content: @Composable (T) -> Unit)
|
fun <T> item(route: Route<T>, content: @Composable SpiderItemScope.(T) -> Unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SpiderItemScope {
|
||||||
|
fun goBack()
|
||||||
}
|
}
|
||||||
|
|
||||||
data class SpiderPage<T>(
|
data class SpiderPage<T>(
|
||||||
|
@ -7,5 +7,6 @@ dependencies {
|
|||||||
implementation project(":matrix:services:auth")
|
implementation project(":matrix:services:auth")
|
||||||
implementation project(":matrix:services:profile")
|
implementation project(":matrix:services:profile")
|
||||||
implementation project(":matrix:services:crypto")
|
implementation project(":matrix:services:crypto")
|
||||||
|
implementation project(":design-library")
|
||||||
implementation project(":core")
|
implementation project(":core")
|
||||||
}
|
}
|
@ -1,7 +1,6 @@
|
|||||||
package app.dapk.st.login
|
package app.dapk.st.login
|
||||||
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
@ -32,6 +31,8 @@ import androidx.compose.ui.text.input.VisualTransformation
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import app.dapk.st.core.StartObserving
|
import app.dapk.st.core.StartObserving
|
||||||
|
import app.dapk.st.core.components.CenteredLoading
|
||||||
|
import app.dapk.st.design.components.GenericError
|
||||||
import app.dapk.st.login.LoginEvent.LoginComplete
|
import app.dapk.st.login.LoginEvent.LoginComplete
|
||||||
import app.dapk.st.login.LoginScreenState.*
|
import app.dapk.st.login.LoginScreenState.*
|
||||||
|
|
||||||
@ -49,42 +50,8 @@ fun LoginScreen(loginViewModel: LoginViewModel, onLoggedIn: () -> Unit) {
|
|||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
|
||||||
when (val state = loginViewModel.state) {
|
when (val state = loginViewModel.state) {
|
||||||
is Error -> {
|
is Error -> GenericError(cause = state.cause, action = { loginViewModel.start() })
|
||||||
val openDetailsDialog = remember { mutableStateOf(false) }
|
Loading -> CenteredLoading()
|
||||||
|
|
||||||
if (openDetailsDialog.value) {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = { openDetailsDialog.value = false },
|
|
||||||
confirmButton = {
|
|
||||||
Button(onClick = { openDetailsDialog.value = false }) {
|
|
||||||
Text("OK")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
title = { Text("Details") },
|
|
||||||
text = {
|
|
||||||
Text(state.cause.message ?: "Unknown")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
|
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
|
||||||
Text("Something went wrong")
|
|
||||||
Text("Tap for more details".uppercase(), fontSize = 12.sp, modifier = Modifier.clickable { openDetailsDialog.value = true }.padding(12.dp))
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
Button(onClick = {
|
|
||||||
loginViewModel.start()
|
|
||||||
}) {
|
|
||||||
Text("Retry".uppercase())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loading -> {
|
|
||||||
Box(contentAlignment = Alignment.Center) {
|
|
||||||
CircularProgressIndicator()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is Content ->
|
is Content ->
|
||||||
Row {
|
Row {
|
||||||
|
@ -43,10 +43,7 @@ import app.dapk.st.core.LifecycleEffect
|
|||||||
import app.dapk.st.core.StartObserving
|
import app.dapk.st.core.StartObserving
|
||||||
import app.dapk.st.core.components.CenteredLoading
|
import app.dapk.st.core.components.CenteredLoading
|
||||||
import app.dapk.st.core.extensions.takeIfContent
|
import app.dapk.st.core.extensions.takeIfContent
|
||||||
import app.dapk.st.design.components.MessengerUrlIcon
|
import app.dapk.st.design.components.*
|
||||||
import app.dapk.st.design.components.MissingAvatarIcon
|
|
||||||
import app.dapk.st.design.components.SmallTalkTheme
|
|
||||||
import app.dapk.st.design.components.Toolbar
|
|
||||||
import app.dapk.st.matrix.common.RoomId
|
import app.dapk.st.matrix.common.RoomId
|
||||||
import app.dapk.st.matrix.common.UserId
|
import app.dapk.st.matrix.common.UserId
|
||||||
import app.dapk.st.matrix.sync.MessageMeta
|
import app.dapk.st.matrix.sync.MessageMeta
|
||||||
@ -95,7 +92,7 @@ internal fun MessengerScreen(
|
|||||||
})
|
})
|
||||||
when (state.composerState) {
|
when (state.composerState) {
|
||||||
is ComposerState.Text -> {
|
is ComposerState.Text -> {
|
||||||
Room(state.roomState, replyActions)
|
Room(state.roomState, replyActions, onRetry = { viewModel.post(MessengerAction.OnMessengerVisible(roomId, attachments)) })
|
||||||
TextComposer(
|
TextComposer(
|
||||||
state.composerState,
|
state.composerState,
|
||||||
onTextChange = { viewModel.post(MessengerAction.ComposerTextUpdate(it)) },
|
onTextChange = { viewModel.post(MessengerAction.ComposerTextUpdate(it)) },
|
||||||
@ -132,7 +129,7 @@ private fun MessengerViewModel.ObserveEvents(galleryLauncher: ActivityResultLaun
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ColumnScope.Room(roomStateLce: Lce<MessengerState>, replyActions: ReplyActions) {
|
private fun ColumnScope.Room(roomStateLce: Lce<MessengerState>, replyActions: ReplyActions, onRetry: () -> Unit) {
|
||||||
when (val state = roomStateLce) {
|
when (val state = roomStateLce) {
|
||||||
is Lce.Loading -> CenteredLoading()
|
is Lce.Loading -> CenteredLoading()
|
||||||
is Lce.Content -> {
|
is Lce.Content -> {
|
||||||
@ -165,16 +162,7 @@ private fun ColumnScope.Room(roomStateLce: Lce<MessengerState>, replyActions: Re
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is Lce.Error -> {
|
is Lce.Error -> GenericError(cause = state.cause, action = onRetry)
|
||||||
Box(contentAlignment = Alignment.Center) {
|
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
|
||||||
Text("Something went wrong...")
|
|
||||||
Button(onClick = {}) {
|
|
||||||
Text("Retry")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import app.dapk.st.core.*
|
import app.dapk.st.core.*
|
||||||
import app.dapk.st.core.extensions.unsafeLazy
|
import app.dapk.st.core.extensions.unsafeLazy
|
||||||
|
import app.dapk.st.design.components.GenericError
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@ -59,7 +60,10 @@ fun Activity.PermissionGuard(state: State<Lce<PermissionResult>>, onGranted: @Co
|
|||||||
PermissionResult.ShowRational -> finish()
|
PermissionResult.ShowRational -> finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
is Lce.Error -> finish()
|
is Lce.Error -> GenericError(message = "Store permission required", label = "Close") {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
is Lce.Loading -> {
|
is Lce.Loading -> {
|
||||||
// loading should be quick, let's avoid displaying anything
|
// loading should be quick, let's avoid displaying anything
|
||||||
}
|
}
|
||||||
|
@ -42,20 +42,17 @@ fun ImageGalleryScreen(viewModel: ImageGalleryViewModel, onTopLevelBack: () -> U
|
|||||||
|
|
||||||
Spider(currentPage = viewModel.state.page, onNavigate = onNavigate) {
|
Spider(currentPage = viewModel.state.page, onNavigate = onNavigate) {
|
||||||
item(ImageGalleryPage.Routes.folders) {
|
item(ImageGalleryPage.Routes.folders) {
|
||||||
ImageGalleryFolders(it) { folder ->
|
ImageGalleryFolders(it, onClick = { viewModel.selectFolder(it) }, onRetry = { viewModel.start() })
|
||||||
viewModel.selectFolder(folder)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
item(ImageGalleryPage.Routes.files) {
|
item(ImageGalleryPage.Routes.files) {
|
||||||
ImageGalleryMedia(it, onImageSelected)
|
ImageGalleryMedia(it, onImageSelected, onRetry = { viewModel.selectFolder(it.folder) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ImageGalleryFolders(state: ImageGalleryPage.Folders, onClick: (Folder) -> Unit) {
|
fun ImageGalleryFolders(state: ImageGalleryPage.Folders, onClick: (Folder) -> Unit, onRetry: () -> Unit) {
|
||||||
val screenWidth = LocalConfiguration.current.screenWidthDp
|
val screenWidth = LocalConfiguration.current.screenWidthDp
|
||||||
|
|
||||||
val gradient = Brush.verticalGradient(
|
val gradient = Brush.verticalGradient(
|
||||||
@ -106,12 +103,12 @@ fun ImageGalleryFolders(state: ImageGalleryPage.Folders, onClick: (Folder) -> Un
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is Lce.Error -> GenericError { }
|
is Lce.Error -> GenericError(cause = content.cause, action = onRetry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ImageGalleryMedia(state: ImageGalleryPage.Files, onFileSelected: (Media) -> Unit) {
|
fun ImageGalleryMedia(state: ImageGalleryPage.Files, onFileSelected: (Media) -> Unit, onRetry: () -> Unit) {
|
||||||
val screenWidth = LocalConfiguration.current.screenWidthDp
|
val screenWidth = LocalConfiguration.current.screenWidthDp
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
@ -149,7 +146,7 @@ fun ImageGalleryMedia(state: ImageGalleryPage.Files, onFileSelected: (Media) ->
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is Lce.Error -> GenericError { }
|
is Lce.Error -> GenericError(cause = content.cause, action = onRetry)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -48,7 +48,7 @@ class ImageGalleryViewModel(
|
|||||||
route = ImageGalleryPage.Routes.files,
|
route = ImageGalleryPage.Routes.files,
|
||||||
label = page.label,
|
label = page.label,
|
||||||
parent = ImageGalleryPage.Routes.folders,
|
parent = ImageGalleryPage.Routes.folders,
|
||||||
state = ImageGalleryPage.Files(Lce.Loading())
|
state = ImageGalleryPage.Files(Lce.Loading(), folder)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -78,7 +78,7 @@ data class ImageGalleryState(
|
|||||||
|
|
||||||
sealed interface ImageGalleryPage {
|
sealed interface ImageGalleryPage {
|
||||||
data class Folders(val content: Lce<List<Folder>>) : ImageGalleryPage
|
data class Folders(val content: Lce<List<Folder>>) : ImageGalleryPage
|
||||||
data class Files(val content: Lce<List<Media>>) : ImageGalleryPage
|
data class Files(val content: Lce<List<Media>>, val folder: Folder) : ImageGalleryPage
|
||||||
|
|
||||||
object Routes {
|
object Routes {
|
||||||
val folders = Route<Folders>("Folders")
|
val folders = Route<Folders>("Folders")
|
||||||
|
@ -119,7 +119,7 @@ private fun ProfilePage(context: Context, viewModel: ProfileViewModel, profile:
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun Invitations(viewModel: ProfileViewModel, invitations: Page.Invitations) {
|
private fun SpiderItemScope.Invitations(viewModel: ProfileViewModel, invitations: Page.Invitations) {
|
||||||
when (val state = invitations.content) {
|
when (val state = invitations.content) {
|
||||||
is Lce.Loading -> CenteredLoading()
|
is Lce.Loading -> CenteredLoading()
|
||||||
is Lce.Content -> {
|
is Lce.Content -> {
|
||||||
@ -147,7 +147,7 @@ private fun Invitations(viewModel: ProfileViewModel, invitations: Page.Invitatio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is Lce.Error -> TODO()
|
is Lce.Error -> GenericError(label = "Go back", cause = state.cause) { goBack() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,7 +31,6 @@ import androidx.compose.ui.text.input.ImeAction
|
|||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
import androidx.compose.ui.text.input.VisualTransformation
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
@ -40,6 +39,7 @@ import app.dapk.st.core.Lce
|
|||||||
import app.dapk.st.core.StartObserving
|
import app.dapk.st.core.StartObserving
|
||||||
import app.dapk.st.core.components.CenteredLoading
|
import app.dapk.st.core.components.CenteredLoading
|
||||||
import app.dapk.st.core.components.Header
|
import app.dapk.st.core.components.Header
|
||||||
|
import app.dapk.st.core.extensions.takeAs
|
||||||
import app.dapk.st.core.getActivity
|
import app.dapk.st.core.getActivity
|
||||||
import app.dapk.st.design.components.*
|
import app.dapk.st.design.components.*
|
||||||
import app.dapk.st.matrix.crypto.ImportResult
|
import app.dapk.st.matrix.crypto.ImportResult
|
||||||
@ -63,7 +63,7 @@ internal fun SettingsScreen(viewModel: SettingsViewModel, onSignOut: () -> Unit,
|
|||||||
}
|
}
|
||||||
Spider(currentPage = viewModel.state.page, onNavigate = onNavigate) {
|
Spider(currentPage = viewModel.state.page, onNavigate = onNavigate) {
|
||||||
item(Page.Routes.root) {
|
item(Page.Routes.root) {
|
||||||
RootSettings(it) { viewModel.onClick(it) }
|
RootSettings(it, onClick = { viewModel.onClick(it) }, onRetry = { viewModel.start() })
|
||||||
}
|
}
|
||||||
item(Page.Routes.encryption) {
|
item(Page.Routes.encryption) {
|
||||||
Encryption(viewModel, it)
|
Encryption(viewModel, it)
|
||||||
@ -149,21 +149,19 @@ internal fun SettingsScreen(viewModel: SettingsViewModel, onSignOut: () -> Unit,
|
|||||||
}
|
}
|
||||||
|
|
||||||
is ImportResult.Error -> {
|
is ImportResult.Error -> {
|
||||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
val message = when (result.cause) {
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
ImportResult.Error.Type.NoKeysFound -> "No keys found in the file"
|
||||||
val message = when (val type = result.cause) {
|
ImportResult.Error.Type.UnexpectedDecryptionOutput -> "Unable to decrypt file, double check your passphrase"
|
||||||
ImportResult.Error.Type.NoKeysFound -> "No keys found in the file"
|
is ImportResult.Error.Type.Unknown -> "Unknown error"
|
||||||
ImportResult.Error.Type.UnexpectedDecryptionOutput -> "Unable to decrypt file, double check your passphrase"
|
ImportResult.Error.Type.UnableToOpenFile -> "Unable to open file"
|
||||||
is ImportResult.Error.Type.Unknown -> "${type.cause::class.java.simpleName}: ${type.cause.message}"
|
ImportResult.Error.Type.InvalidFile -> "Unable to process file"
|
||||||
ImportResult.Error.Type.UnableToOpenFile -> "Unable to open file"
|
}
|
||||||
}
|
GenericError(
|
||||||
|
message = message,
|
||||||
Text(text = "Import failed\n$message", textAlign = TextAlign.Center)
|
label = "Close",
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
cause = result.cause.takeAs<ImportResult.Error.Type.Unknown>()?.cause
|
||||||
Button(onClick = { navigator.navigate.upToHome() }) {
|
) {
|
||||||
Text(text = "Close".uppercase())
|
navigator.navigate.upToHome()
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -182,7 +180,7 @@ internal fun SettingsScreen(viewModel: SettingsViewModel, onSignOut: () -> Unit,
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RootSettings(page: Page.Root, onClick: (SettingItem) -> Unit) {
|
private fun RootSettings(page: Page.Root, onClick: (SettingItem) -> Unit, onRetry: () -> Unit) {
|
||||||
when (val content = page.content) {
|
when (val content = page.content) {
|
||||||
is Lce.Content -> {
|
is Lce.Content -> {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
@ -228,12 +226,10 @@ private fun RootSettings(page: Page.Root, onClick: (SettingItem) -> Unit) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is Lce.Error -> {
|
is Lce.Error -> GenericError(cause = content.cause, action = onRetry)
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
is Lce.Loading -> {
|
is Lce.Loading -> {
|
||||||
// TODO
|
// Should be quick enough to avoid needing a loading state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -266,7 +262,7 @@ private fun PushProviders(viewModel: SettingsViewModel, state: Page.PushProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is Lce.Error -> TODO()
|
is Lce.Error -> GenericError(cause = lce.cause) { viewModel.fetchPushProviders() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,6 +14,8 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import app.dapk.st.core.AppLogTag
|
import app.dapk.st.core.AppLogTag
|
||||||
import app.dapk.st.core.Lce
|
import app.dapk.st.core.Lce
|
||||||
|
import app.dapk.st.core.components.CenteredLoading
|
||||||
|
import app.dapk.st.design.components.GenericError
|
||||||
import app.dapk.st.matrix.common.MatrixLogTag
|
import app.dapk.st.matrix.common.MatrixLogTag
|
||||||
|
|
||||||
private val filterItems = listOf<String?>(null) + (MatrixLogTag.values().map { it.key } + AppLogTag.values().map { it.key }).distinct()
|
private val filterItems = listOf<String?>(null) + (MatrixLogTag.values().map { it.key } + AppLogTag.values().map { it.key }).distinct()
|
||||||
@ -33,11 +35,13 @@ fun EventLogScreen(viewModel: EventLoggerViewModel) {
|
|||||||
viewModel.selectLog(it, filter = null)
|
viewModel.selectLog(it, filter = null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
Events(
|
Events(
|
||||||
selectedPageContent = state.selectedState,
|
selectedPageContent = state.selectedState,
|
||||||
onExit = { viewModel.exitLog() },
|
onExit = { viewModel.exitLog() },
|
||||||
onSelectTag = { viewModel.selectLog(state.selectedState.selectedPage, it) }
|
onSelectTag = { viewModel.selectLog(state.selectedState.selectedPage, it) },
|
||||||
|
onRetry = { viewModel.start() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -46,6 +50,7 @@ fun EventLogScreen(viewModel: EventLoggerViewModel) {
|
|||||||
is Lce.Error -> {
|
is Lce.Error -> {
|
||||||
// TODO
|
// TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
is Lce.Loading -> {
|
is Lce.Loading -> {
|
||||||
// TODO
|
// TODO
|
||||||
}
|
}
|
||||||
@ -69,7 +74,7 @@ private fun LogKeysList(keys: List<String>, onSelected: (String) -> Unit) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun Events(selectedPageContent: SelectedState, onExit: () -> Unit, onSelectTag: (String?) -> Unit) {
|
private fun Events(selectedPageContent: SelectedState, onExit: () -> Unit, onSelectTag: (String?) -> Unit, onRetry: () -> Unit) {
|
||||||
BackHandler(onBack = onExit)
|
BackHandler(onBack = onExit)
|
||||||
when (val content = selectedPageContent.content) {
|
when (val content = selectedPageContent.content) {
|
||||||
is Lce.Content -> {
|
is Lce.Content -> {
|
||||||
@ -112,9 +117,8 @@ private fun Events(selectedPageContent: SelectedState, onExit: () -> Unit, onSel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is Lce.Error -> TODO()
|
|
||||||
is Lce.Loading -> {
|
is Lce.Error -> GenericError(cause = content.cause, action = onRetry)
|
||||||
// TODO
|
is Lce.Loading -> CenteredLoading()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -184,6 +184,7 @@ sealed interface ImportResult {
|
|||||||
object NoKeysFound : Type
|
object NoKeysFound : Type
|
||||||
object UnexpectedDecryptionOutput : Type
|
object UnexpectedDecryptionOutput : Type
|
||||||
object UnableToOpenFile : Type
|
object UnableToOpenFile : Type
|
||||||
|
object InvalidFile : Type
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -86,21 +86,27 @@ class RoomKeyImporter(
|
|||||||
val line = it.joinToString(separator = "").replace("\n", "")
|
val line = it.joinToString(separator = "").replace("\n", "")
|
||||||
val toByteArray = base64.decode(line)
|
val toByteArray = base64.decode(line)
|
||||||
if (index == 0) {
|
if (index == 0) {
|
||||||
decryptCipher.initialize(toByteArray, password)
|
toByteArray.ensureHasCipherPayloadOrThrow()
|
||||||
toByteArray
|
val initializer = toByteArray.copyOfRange(0, 37)
|
||||||
.copyOfRange(37, toByteArray.size)
|
decryptCipher.initialize(initializer, password)
|
||||||
.decrypt(decryptCipher)
|
val content = toByteArray.copyOfRange(37, toByteArray.size)
|
||||||
.also {
|
content.decrypt(decryptCipher).also {
|
||||||
if (!it.startsWith("[{")) {
|
if (!it.startsWith("[{")) {
|
||||||
throw ImportException(ImportResult.Error.Type.UnexpectedDecryptionOutput)
|
throw ImportException(ImportResult.Error.Type.UnexpectedDecryptionOutput)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
toByteArray.decrypt(decryptCipher)
|
toByteArray.decrypt(decryptCipher)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun ByteArray.ensureHasCipherPayloadOrThrow() {
|
||||||
|
if (this.size < 37) {
|
||||||
|
throw ImportException(ImportResult.Error.Type.InvalidFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun Cipher.initialize(payload: ByteArray, passphrase: String) {
|
private fun Cipher.initialize(payload: ByteArray, passphrase: String) {
|
||||||
val salt = payload.copyOfRange(1, 1 + 16)
|
val salt = payload.copyOfRange(1, 1 + 16)
|
||||||
val iv = payload.copyOfRange(17, 17 + 16)
|
val iv = payload.copyOfRange(17, 17 + 16)
|
||||||
@ -176,6 +182,7 @@ private class JsonAccumulator {
|
|||||||
jsonSegment = withLatest
|
jsonSegment = withLatest
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
val string = withLatest.substring(objectRange)
|
val string = withLatest.substring(objectRange)
|
||||||
importJson.decodeFromString(ElementMegolmExportObject.serializer(), string).also {
|
importJson.decodeFromString(ElementMegolmExportObject.serializer(), string).also {
|
||||||
@ -200,6 +207,7 @@ private class JsonAccumulator {
|
|||||||
}
|
}
|
||||||
opens++
|
opens++
|
||||||
}
|
}
|
||||||
|
|
||||||
c == '}' -> {
|
c == '}' -> {
|
||||||
opens--
|
opens--
|
||||||
if (opens == 0) {
|
if (opens == 0) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user