diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index 3e9741f0af..4d7fcfa4b1 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -307,16 +307,17 @@ class ChatModel(val controller: ChatController) { } fun upsertGroupMember(groupInfo: GroupInfo, member: GroupMember): Boolean { + if (groupInfo.membership.groupMemberId == member.groupMemberId) { + // Current user was updated (like his role, for example) + updateChatInfo(ChatInfo.Group(groupInfo)) + return false + } // update current chat return if (chatId.value == groupInfo.id) { val memberIndex = groupMembers.indexOfFirst { it.id == member.id } if (memberIndex >= 0) { groupMembers[memberIndex] = member false - } else if (groupInfo.membership.groupMemberId == member.groupMemberId) { - // Current user was updated (like his role, for example) - updateChatInfo(ChatInfo.Group(groupInfo)) - true } else { groupMembers.add(member) true @@ -430,7 +431,7 @@ sealed class ChatInfo: SomeChat, NamedChat { abstract val incognito: Boolean @Serializable @SerialName("direct") - class Direct(val contact: Contact): ChatInfo() { + data class Direct(val contact: Contact): ChatInfo() { override val chatType get() = ChatType.Direct override val localDisplayName get() = contact.localDisplayName override val id get() = contact.id diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index a3f5238d3f..a77cc0dfee 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -244,6 +244,10 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a chatModel.onboardingStage.value = OnboardingStage.OnboardingComplete chatModel.controller.appPrefs.chatLastStart.set(Clock.System.now()) chatModel.chatRunning.value = true + chatModel.appOpenUrl.value?.let { + chatModel.appOpenUrl.value = null + connectIfOpenedViaUri(it, chatModel) + } startReceiver() Log.d(TAG, "startChat: started") } else { @@ -1222,23 +1226,12 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a // service or periodic mode was chosen and battery optimization is disabled SimplexApp.context.schedulePeriodicServiceRestartWorker() SimplexApp.context.schedulePeriodicWakeUp() - chatModel.appOpenUrl.value?.let { - chatModel.appOpenUrl.value = null - connectIfOpenedViaUri(it, chatModel) - } } } private fun showBGServiceNotice(mode: NotificationsMode) = AlertManager.shared.showAlert { - val hideAlert: () -> Unit = { - AlertManager.shared.hideAlert() - chatModel.appOpenUrl.value?.let { - chatModel.appOpenUrl.value = null - connectIfOpenedViaUri(it, chatModel) - } - } AlertDialog( - onDismissRequest = hideAlert, + onDismissRequest = AlertManager.shared::hideAlert, title = { Row { Icon( @@ -1264,7 +1257,7 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a } }, confirmButton = { - TextButton(onClick = hideAlert) { Text(stringResource(R.string.ok)) } + TextButton(onClick = AlertManager.shared::hideAlert) { Text(stringResource(R.string.ok)) } } ) } @@ -1305,15 +1298,8 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a } private fun showDisablingServiceNotice(mode: NotificationsMode) = AlertManager.shared.showAlert { - val hideAlert: () -> Unit = { - AlertManager.shared.hideAlert() - chatModel.appOpenUrl.value?.let { - chatModel.appOpenUrl.value = null - connectIfOpenedViaUri(it, chatModel) - } - } AlertDialog( - onDismissRequest = hideAlert, + onDismissRequest = AlertManager.shared::hideAlert, title = { Row { Icon( @@ -1336,7 +1322,7 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a } }, confirmButton = { - TextButton(onClick = hideAlert) { Text(stringResource(R.string.ok)) } + TextButton(onClick = AlertManager.shared::hideAlert) { Text(stringResource(R.string.ok)) } } ) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt index 90528e8e15..33e882f26b 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt @@ -25,8 +25,7 @@ import androidx.compose.ui.unit.sp import androidx.fragment.app.FragmentActivity import chat.simplex.app.R import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.SimpleButton -import chat.simplex.app.ui.theme.SimpleXTheme +import chat.simplex.app.ui.theme.* import chat.simplex.app.views.chat.* import chat.simplex.app.views.helpers.* import com.google.accompanist.insets.ProvideWindowInsets @@ -164,7 +163,8 @@ fun TerminalLog(terminalItems: List) { val reversedTerminalItems by remember { derivedStateOf { terminalItems.reversed() } } LazyColumn(state = listState, reverseLayout = true) { items(reversedTerminalItems) { item -> - Text("${item.date.toString().subSequence(11, 19)} ${item.label}", + Text( + "${item.date.toString().subSequence(11, 19)} ${item.label}", style = TextStyle(fontFamily = FontFamily.Monospace, fontSize = 18.sp, color = MaterialTheme.colors.primary), maxLines = 1, overflow = TextOverflow.Ellipsis, @@ -173,7 +173,7 @@ fun TerminalLog(terminalItems: List) { .clickable { ModalManager.shared.showModal { SelectionContainer(modifier = Modifier.verticalScroll(rememberScrollState())) { - Text(item.details) + Text(item.details, modifier = Modifier.padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING)) } } }.padding(horizontal = 8.dp, vertical = 4.dp) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt index 0d805d9066..7e6c0cf4d1 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt @@ -47,7 +47,6 @@ fun ChatInfoView( customUserProfile: Profile?, localAlias: String, close: () -> Unit, - onChatUpdated: (Chat) -> Unit, ) { BackHandler(onBack = close) val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value } @@ -61,7 +60,7 @@ fun ChatInfoView( localAlias, developerTools, onLocalAliasChanged = { - setContactAlias(chat.chatInfo.apiId, it, chatModel, onChatUpdated) + setContactAlias(chat.chatInfo.apiId, it, chatModel) }, deleteContact = { deleteContactDialog(chat.chatInfo, chatModel, close) }, clearChat = { clearChatDialog(chat.chatInfo, chatModel, close) }, @@ -350,10 +349,9 @@ fun DeleteContactButton(onClick: () -> Unit) { ) } -private fun setContactAlias(contactApiId: Long, localAlias: String, chatModel: ChatModel, onChatUpdated: (Chat) -> Unit) = withApi { +private fun setContactAlias(contactApiId: Long, localAlias: String, chatModel: ChatModel) = withApi { chatModel.controller.apiSetContactAlias(contactApiId, localAlias)?.let { chatModel.updateContact(it) - onChatUpdated(chatModel.getChat(chatModel.chatId.value ?: return@withApi) ?: return@withApi) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt index 51bfd329d7..88aad59f6f 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt @@ -65,20 +65,30 @@ fun ChatView(chatModel: ChatModel) { LaunchedEffect(Unit) { // snapshotFlow here is because it reacts much faster on changes in chatModel.chatId.value. // With LaunchedEffect(chatModel.chatId.value) there is a noticeable delay before reconstruction of the view - snapshotFlow { chatModel.chatId.value } - .distinctUntilChanged() - .collect { - if (activeChat.value?.id != chatModel.chatId.value) { - activeChat.value = if (chatModel.chatId.value == null) { - null - } else { + launch { + snapshotFlow { chatModel.chatId.value } + .distinctUntilChanged() + .collect { + if (activeChat.value?.id != chatModel.chatId.value && chatModel.chatId.value != null) { // Redisplay the whole hierarchy if the chat is different to make going from groups to direct chat working correctly // Also for situation when chatId changes after clicking in notification, etc - chatModel.getChat(chatModel.chatId.value!!) + activeChat.value = chatModel.getChat(chatModel.chatId.value!!) } + markUnreadChatAsRead(activeChat, chatModel) } - markUnreadChatAsRead(activeChat, chatModel) - } + } + launch { + // .toList() is important for making observation working + snapshotFlow { chatModel.chats.toList() } + .distinctUntilChanged() + .collect { chats -> + chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value }.let { + // Only changed chatInfo is important thing. Other properties can be skipped for reducing recompositions + if (it?.chatInfo != activeChat.value?.chatInfo) { + activeChat.value = it + }} + } + } } if (activeChat.value == null || user == null) { @@ -121,9 +131,7 @@ fun ChatView(chatModel: ChatModel) { if (cInfo is ChatInfo.Direct) { val contactInfo = chatModel.controller.apiContactInfo(cInfo.apiId) ModalManager.shared.showModalCloseable(true) { close -> - ChatInfoView(chatModel, cInfo.contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, close) { - activeChat.value = it - } + ChatInfoView(chatModel, cInfo.contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, close) } } else if (cInfo is ChatInfo.Group) { setGroupMembers(cInfo.groupInfo, chatModel) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt index ca69ad2247..6e0045fedd 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt @@ -243,7 +243,7 @@ fun ComposeView( } } } - val galleryLauncher = rememberLauncherForActivityResult(contract = PickFromGallery()) { processPickedImage(it, null) } + val galleryLauncher = rememberLauncherForActivityResult(contract = PickMultipleFromGallery()) { processPickedImage(it, null) } val galleryLauncherFallback = rememberGetMultipleContentsLauncher { processPickedImage(it, null) } val filesLauncher = rememberGetContentLauncher { processPickedFile(it, null) } @@ -550,7 +550,16 @@ fun ComposeView( } } -class PickFromGallery: ActivityResultContract>() { +class PickFromGallery: ActivityResultContract() { + override fun createIntent(context: Context, input: Int) = + Intent(Intent.ACTION_PICK, MediaStore.Images.Media.INTERNAL_CONTENT_URI).apply { + type = "image/*" + } + + override fun parseResult(resultCode: Int, intent: Intent?): Uri? = intent?.data +} + +class PickMultipleFromGallery: ActivityResultContract>() { override fun createIntent(context: Context, input: Int) = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.INTERNAL_CONTENT_URI).apply { putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/AlertManager.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/AlertManager.kt index 171cbe5552..79d27f0f47 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/AlertManager.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/AlertManager.kt @@ -3,25 +3,20 @@ package chat.simplex.app.views.helpers import android.util.Log import androidx.compose.material.* import androidx.compose.runtime.* -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import chat.simplex.app.R import chat.simplex.app.TAG class AlertManager { - var alertView = mutableStateOf<(@Composable () -> Unit)?>(null) - var presentAlert = mutableStateOf(false) + var alertViews = mutableStateListOf<(@Composable () -> Unit)>() fun showAlert(alert: @Composable () -> Unit) { Log.d(TAG, "AlertManager.showAlert") - alertView.value = alert - presentAlert.value = true + alertViews.add(alert) } fun hideAlert() { - presentAlert.value = false - alertView.value = null + alertViews.removeLastOrNull() } fun showAlertDialogButtons( @@ -101,7 +96,7 @@ class AlertManager { @Composable fun showInView() { - if (presentAlert.value) alertView.value?.invoke() + remember { alertViews }.lastOrNull()?.invoke() } companion object { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/GetImageView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/GetImageView.kt index 66dfbf763b..57fe3ec454 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/GetImageView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/GetImageView.kt @@ -2,8 +2,7 @@ package chat.simplex.app.views.helpers import android.Manifest import android.app.Activity -import android.content.Context -import android.content.Intent +import android.content.* import android.content.pm.PackageManager import android.graphics.* import android.net.Uri @@ -31,6 +30,7 @@ import androidx.core.content.ContextCompat import androidx.core.content.FileProvider import chat.simplex.app.* import chat.simplex.app.R +import chat.simplex.app.views.chat.PickFromGallery import chat.simplex.app.views.newchat.ActionButton import java.io.ByteArrayOutputStream import java.io.File @@ -170,7 +170,7 @@ fun GetImageBottomSheet( hideBottomSheet: () -> Unit ) { val context = LocalContext.current - val galleryLauncher = rememberGetContentLauncher { uri: Uri? -> + val processPickedImage = { uri: Uri? -> if (uri != null) { val source = ImageDecoder.createSource(context.contentResolver, uri) val bitmap = ImageDecoder.decodeBitmap(source) @@ -178,6 +178,8 @@ fun GetImageBottomSheet( onImageChange(bitmap) } } + val galleryLauncher = rememberLauncherForActivityResult(contract = PickFromGallery()) { processPickedImage(it) } + val galleryLauncherFallback = rememberGetContentLauncher { processPickedImage(it) } val cameraLauncher = rememberCameraLauncher { bitmap: Bitmap? -> if (bitmap != null) { imageBitmap.value = bitmap @@ -219,7 +221,11 @@ fun GetImageBottomSheet( } } ActionButton(null, stringResource(R.string.from_gallery_button), icon = Icons.Outlined.Collections) { - galleryLauncher.launch("image/*") + try { + galleryLauncher.launch(0) + } catch (e: ActivityNotFoundException) { + galleryLauncherFallback.launch("image/*") + } hideBottomSheet() } }