State preserving for some UI elements which otherwise would be lost on orientation change (#994)

- restore message text as well as reply state
- restore search view
- don't display blank view on orientation change for a moment
- better saving of local user name while typing. Prevent loosing state on orientation change and hard killing the app
- don't display same messages in MainActivity from old intents on orientation change (no double processing of intent)
This commit is contained in:
Stanislav Dmitrenko
2022-08-31 23:49:19 +03:00
committed by GitHub
parent 74b11d1c5d
commit 1e587df3d4
5 changed files with 59 additions and 26 deletions

View File

@@ -15,6 +15,7 @@ import androidx.compose.material.Surface
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Replay
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
@@ -51,7 +52,11 @@ class MainActivity: FragmentActivity(), LifecycleEventObserver {
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
// testJson()
val m = vm.chatModel
processNotificationIntent(intent, m)
// When call ended and orientation changes, it re-process old intent, it's unneeded.
// Only needed to be processed on first creation of activity
if (savedInstanceState == null) {
processNotificationIntent(intent, m)
}
setContent {
SimpleXTheme {
Surface(
@@ -223,7 +228,7 @@ fun MainPage(
showLANotice: () -> Unit
) {
// this with LaunchedEffect(userAuthorized.value) fixes bottom sheet visibly collapsing after authentication
var chatsAccessAuthorized by remember { mutableStateOf(false) }
var chatsAccessAuthorized by rememberSaveable { mutableStateOf(false) }
LaunchedEffect(userAuthorized.value) {
if (chatModel.controller.appPrefs.performLA.get()) {
delay(500L)

View File

@@ -16,6 +16,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -34,6 +35,8 @@ import chat.simplex.app.SimplexApp
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
@Composable
fun ChatInfoView(
@@ -250,10 +253,10 @@ fun ChatInfoHeader(cInfo: ChatInfo, contact: Contact) {
@Composable
private fun LocalAliasEditor(initialValue: String, updateValue: (String) -> Unit) {
var value by remember { mutableStateOf(initialValue) }
var value by rememberSaveable { mutableStateOf(initialValue) }
DefaultBasicTextField(
Modifier.fillMaxWidth().padding(horizontal = 10.dp),
initialValue,
value,
{
Text(
generalGetString(R.string.text_field_set_contact_placeholder),
@@ -268,8 +271,17 @@ private fun LocalAliasEditor(initialValue: String, updateValue: (String) -> Unit
) {
value = it
}
LaunchedEffect(Unit) {
snapshotFlow { value }
.onEach { delay(500) } // wait a little after every new character, don't emit until user stops typing
.conflate() // get the latest value
.filter { it == value } // don't process old ones
.collect {
updateValue(value)
}
}
DisposableEffect(Unit) {
onDispose { updateValue(value) }
onDispose { updateValue(value) } // just in case snapshotFlow will be canceled when user presses Back too fast
}
}

View File

@@ -47,11 +47,13 @@ import kotlinx.datetime.Clock
@Composable
fun ChatView(chatModel: ChatModel) {
var activeChat by remember { mutableStateOf(chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value }) }
val searchText = remember { mutableStateOf("") }
val searchText = rememberSaveable { mutableStateOf("") }
val user = chatModel.currentUser.value
val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get()
val composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = useLinkPreviews)) }
val attachmentOption = remember { mutableStateOf<AttachmentOption?>(null) }
val composeState = rememberSaveable(saver = ComposeState.saver()) {
mutableStateOf(ComposeState(useLinkPreviews = useLinkPreviews))
}
val attachmentOption = rememberSaveable { mutableStateOf<AttachmentOption?>(null) }
val attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
val scope = rememberCoroutineScope()
@@ -304,8 +306,8 @@ fun ChatInfoToolbar(
addMembers: (GroupInfo) -> Unit,
onSearchValueChanged: (String) -> Unit,
) {
var showMenu by remember { mutableStateOf(false) }
var showSearch by remember { mutableStateOf(false) }
var showMenu by rememberSaveable { mutableStateOf(false) }
var showSearch by rememberSaveable { mutableStateOf(false) }
val onBackClicked = {
if (!showSearch) {
back()

View File

@@ -19,13 +19,13 @@ import androidx.annotation.CallSuper
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AttachFile
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.outlined.Reply
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.Saver
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -42,21 +42,26 @@ import chat.simplex.app.views.chat.item.*
import chat.simplex.app.views.helpers.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import java.io.File
@Serializable
sealed class ComposePreview {
object NoPreview: ComposePreview()
class CLinkPreview(val linkPreview: LinkPreview?): ComposePreview()
class ImagePreview(val image: String): ComposePreview()
class FilePreview(val fileName: String): ComposePreview()
@Serializable object NoPreview: ComposePreview()
@Serializable class CLinkPreview(val linkPreview: LinkPreview?): ComposePreview()
@Serializable class ImagePreview(val image: String): ComposePreview()
@Serializable class FilePreview(val fileName: String): ComposePreview()
}
@Serializable
sealed class ComposeContextItem {
object NoContextItem: ComposeContextItem()
class QuotedItem(val chatItem: ChatItem): ComposeContextItem()
class EditingItem(val chatItem: ChatItem): ComposeContextItem()
@Serializable object NoContextItem: ComposeContextItem()
@Serializable class QuotedItem(val chatItem: ChatItem): ComposeContextItem()
@Serializable class EditingItem(val chatItem: ChatItem): ComposeContextItem()
}
@Serializable
data class ComposeState(
val message: String = "",
val preview: ComposePreview = ComposePreview.NoPreview,
@@ -99,6 +104,15 @@ data class ComposeState(
is ComposePreview.CLinkPreview -> preview.linkPreview
else -> null
}
companion object {
fun saver(): Saver<MutableState<ComposeState>, *> = Saver(
save = { json.encodeToString(serializer(), it.value) },
restore = {
mutableStateOf(json.decodeFromString(it))
}
)
}
}
fun chatItemPreview(chatItem: ChatItem): ComposePreview {

View File

@@ -11,6 +11,7 @@ import androidx.compose.material.TextFieldDefaults.textFieldWithLabelPadding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
@@ -20,8 +21,7 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.input.*
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
@@ -30,7 +30,7 @@ import kotlinx.coroutines.delay
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun SearchTextField(modifier: Modifier, placeholder: String, onValueChange: (String) -> Unit) {
var searchText by remember { mutableStateOf("") }
var searchText by rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
val focusRequester = remember { FocusRequester() }
val keyboard = LocalSoftwareKeyboardController.current
@@ -61,7 +61,7 @@ fun SearchTextField(modifier: Modifier, placeholder: String, onValueChange: (Str
),
onValueChange = {
searchText = it
onValueChange(it)
onValueChange(it.text)
},
cursorBrush = SolidColor(colors.cursorColor(false).value),
visualTransformation = VisualTransformation.None,
@@ -75,13 +75,13 @@ fun SearchTextField(modifier: Modifier, placeholder: String, onValueChange: (Str
interactionSource = interactionSource,
decorationBox = @Composable { innerTextField ->
TextFieldDefaults.TextFieldDecorationBox(
value = searchText,
value = searchText.text,
innerTextField = innerTextField,
placeholder = {
Text(placeholder)
},
trailingIcon = if (searchText.isNotEmpty()) {{
IconButton({ searchText = ""; onValueChange("") }) {
trailingIcon = if (searchText.text.isNotEmpty()) {{
IconButton({ searchText = TextFieldValue(""); onValueChange("") }) {
Icon(Icons.Default.Close, stringResource(R.string.icon_descr_close_button), tint = MaterialTheme.colors.primary,)
}
}} else null,