diff --git a/apps/android/app/src/main/AndroidManifest.xml b/apps/android/app/src/main/AndroidManifest.xml index bbf247909..3c1d1da83 100644 --- a/apps/android/app/src/main/AndroidManifest.xml +++ b/apps/android/app/src/main/AndroidManifest.xml @@ -63,6 +63,12 @@ + + + + + + { + // Close active chat and show a list of chats + chatModel.chatId.value = null + chatModel.clearOverlays.value = true + when { + "text/plain" == intent.type -> intent.getStringExtra(Intent.EXTRA_TEXT)?.let { + chatModel.sharedContent.value = SharedContent.Text(it) + } + intent.type?.startsWith("image/") == true -> (intent.getParcelableExtra(Intent.EXTRA_STREAM) as? Uri)?.let { + chatModel.sharedContent.value = SharedContent.Image(it) + } // All other mime types + else -> (intent.getParcelableExtra(Intent.EXTRA_STREAM) as? Uri)?.let { + chatModel.sharedContent.value = SharedContent.File(it) + } + } + } + } +} + fun connectIfOpenedViaUri(uri: Uri, chatModel: ChatModel) { Log.d(TAG, "connectIfOpenedViaUri: opened via link") if (chatModel.currentUser.value == null) { 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 37affd8bd..5372ac6b6 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 @@ -10,8 +10,7 @@ import androidx.compose.ui.text.style.TextDecoration import chat.simplex.app.R import chat.simplex.app.ui.theme.* import chat.simplex.app.views.call.* -import chat.simplex.app.views.helpers.DBMigrationResult -import chat.simplex.app.views.helpers.generalGetString +import chat.simplex.app.views.helpers.* import chat.simplex.app.views.onboarding.OnboardingStage import chat.simplex.app.views.usersettings.NotificationPreviewMode import chat.simplex.app.views.usersettings.NotificationsMode @@ -64,6 +63,9 @@ class ChatModel(val controller: ChatController) { val showCallView = mutableStateOf(false) val switchingCall = mutableStateOf(false) + // working with external intents + val sharedContent = mutableStateOf(null as SharedContent?) + fun updateUserProfile(profile: LocalProfile) { val user = currentUser.value if (user != null) { @@ -621,7 +623,7 @@ data class GroupInfo ( override val chatType get() = ChatType.Group override val id get() = "#$groupId" override val apiId get() = groupId - override val ready get() = true + override val ready get() = membership.memberActive override val sendMsgEnabled get() = membership.memberActive override val ntfsEnabled get() = chatSettings.enableNtfs override val displayName get() = groupProfile.displayName 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 90da13cef..5a873dccd 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 @@ -222,9 +222,7 @@ fun ComposeView( } } } - val galleryLauncher = rememberLauncherForActivityResult(contract = PickFromGallery(), processPickedImage) - val galleryLauncherFallback = rememberGetContentLauncher(processPickedImage) - val filesLauncher = rememberGetContentLauncher { uri: Uri? -> + val processPickedFile = { uri: Uri? -> if (uri != null) { val fileSize = getFileSize(context, uri) if (fileSize != null && fileSize <= MAX_FILE_SIZE) { @@ -241,6 +239,9 @@ fun ComposeView( } } } + val galleryLauncher = rememberLauncherForActivityResult(contract = PickFromGallery(), processPickedImage) + val galleryLauncherFallback = rememberGetContentLauncher(processPickedImage) + val filesLauncher = rememberGetContentLauncher(processPickedFile) LaunchedEffect(attachmentOption.value) { when (attachmentOption.value) { @@ -495,6 +496,16 @@ fun ComposeView( } } + LaunchedEffect(chatModel.sharedContent.value) { + when (val shared = chatModel.sharedContent.value) { + is SharedContent.Text -> onMessageChange(shared.text) + is SharedContent.Image -> processPickedImage(shared.uri) + is SharedContent.File -> processPickedFile(shared.uri) + null -> {} + } + chatModel.sharedContent.value = null + } + Column { contextItemView() when { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt index 0b8d2f7d4..52762f234 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt @@ -177,7 +177,7 @@ private fun ConnectButton(text: String, onClick: () -> Unit) { } @Composable -fun ChatListToolbar(chatModel: ChatModel, scaffoldCtrl: ScaffoldController, stopped: Boolean, onSearchValueChanged: (String) -> Unit) { +private fun ChatListToolbar(chatModel: ChatModel, scaffoldCtrl: ScaffoldController, stopped: Boolean, onSearchValueChanged: (String) -> Unit) { var showSearch by rememberSaveable { mutableStateOf(false) } val hideSearchOnBack = { onSearchValueChanged(""); showSearch = false } if (showSearch) { @@ -236,7 +236,7 @@ fun ChatListToolbar(chatModel: ChatModel, scaffoldCtrl: ScaffoldController, stop } @Composable -fun ChatList(chatModel: ChatModel, search: String) { +private fun ChatList(chatModel: ChatModel, search: String) { val filter: (Chat) -> Boolean = { chat: Chat -> chat.chatInfo.chatViewName.lowercase().contains(search.lowercase()) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ShareListNavLinkView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ShareListNavLinkView.kt new file mode 100644 index 000000000..e9a007b1c --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ShareListNavLinkView.kt @@ -0,0 +1,66 @@ +package chat.simplex.app.views.chatlist + +import SectionItemView +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import chat.simplex.app.model.* +import chat.simplex.app.ui.theme.Indigo +import chat.simplex.app.views.helpers.ProfileImage + +@Composable +fun ShareListNavLinkView(chat: Chat, chatModel: ChatModel) { + val stopped = chatModel.chatRunning.value == false + when (chat.chatInfo) { + is ChatInfo.Direct -> + ShareListNavLinkLayout( + chatLinkPreview = { SharePreviewView(chat) }, + click = { directChatAction(chat.chatInfo, chatModel) }, + stopped + ) + is ChatInfo.Group -> + ShareListNavLinkLayout( + chatLinkPreview = { SharePreviewView(chat) }, + click = { groupChatAction(chat.chatInfo.groupInfo, chatModel) }, + stopped + ) + is ChatInfo.ContactRequest, is ChatInfo.ContactConnection -> {} + } +} + +@Composable +private fun ShareListNavLinkLayout( + chatLinkPreview: @Composable () -> Unit, + click: () -> Unit, + stopped: Boolean +) { + SectionItemView(minHeight = 50.dp, click = click, disabled = stopped) { + chatLinkPreview() + } + Divider(Modifier.padding(horizontal = 8.dp)) +} + +@Composable +private fun SharePreviewView(chat: Chat) { + Row( + Modifier.fillMaxSize(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + ProfileImage(size = 46.dp, chat.chatInfo.image) + Text( + chat.chatInfo.chatViewName, maxLines = 1, overflow = TextOverflow.Ellipsis, + color = if (chat.chatInfo.incognito) Indigo else Color.Unspecified + ) + } + } +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ShareListView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ShareListView.kt new file mode 100644 index 000000000..e4df296c7 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ShareListView.kt @@ -0,0 +1,138 @@ +package chat.simplex.app.views.chatlist + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.* +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 +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.capitalize +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.intl.Locale +import androidx.compose.ui.unit.dp +import chat.simplex.app.R +import chat.simplex.app.model.* +import chat.simplex.app.ui.theme.HighOrLowlight +import chat.simplex.app.ui.theme.Indigo +import chat.simplex.app.views.helpers.* + +@Composable +fun ShareListView(chatModel: ChatModel, stopped: Boolean) { + var searchInList by rememberSaveable { mutableStateOf("") } + Scaffold( + topBar = { Column { ShareListToolbar(chatModel, stopped) { searchInList = it.trim() } } }, + ) { + Box(Modifier.padding(it)) { + Column( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colors.background) + ) { + if (chatModel.chats.isNotEmpty()) { + ShareList(chatModel, search = searchInList) + } else { + EmptyList() + } + } + } + } +} + +@Composable +private fun EmptyList() { + Box { + Text(stringResource(R.string.you_have_no_chats), Modifier.align(Alignment.Center), color = HighOrLowlight) + } +} + +@Composable +private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchValueChanged: (String) -> Unit) { + var showSearch by rememberSaveable { mutableStateOf(false) } + val hideSearchOnBack = { onSearchValueChanged(""); showSearch = false } + if (showSearch) { + BackHandler(onBack = hideSearchOnBack) + } + val barButtons = arrayListOf<@Composable RowScope.() -> Unit>() + if (chatModel.chats.size >= 8) { + barButtons.add { + IconButton({ showSearch = true }) { + Icon(Icons.Outlined.Search, stringResource(android.R.string.search_go).capitalize(Locale.current), tint = MaterialTheme.colors.primary) + } + } + } + if (stopped) { + barButtons.add { + IconButton(onClick = { + AlertManager.shared.showAlertMsg( + generalGetString(R.string.chat_is_stopped_indication), + generalGetString(R.string.you_can_start_chat_via_setting_or_by_restarting_the_app) + ) + }) { + Icon( + Icons.Filled.Report, + generalGetString(R.string.chat_is_stopped_indication), + tint = Color.Red, + ) + } + } + } + + DefaultTopAppBar( + navigationButton = { if (showSearch) NavigationButtonBack(hideSearchOnBack) else NavigationButtonBack { chatModel.sharedContent.value = null } }, + title = { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + when (chatModel.sharedContent.value) { + is SharedContent.Text -> stringResource(R.string.share_message) + is SharedContent.Image -> stringResource(R.string.share_image) + is SharedContent.File -> stringResource(R.string.share_file) + else -> stringResource(R.string.share_message) + }, + color = MaterialTheme.colors.onBackground, + fontWeight = FontWeight.SemiBold, + ) + if (chatModel.incognito.value) { + Icon( + Icons.Filled.TheaterComedy, + stringResource(R.string.incognito), + tint = Indigo, + modifier = Modifier.padding(10.dp).size(26.dp) + ) + } + } + }, + onTitleClick = null, + showSearch = showSearch, + onSearchValueChanged = onSearchValueChanged, + buttons = barButtons + ) + Divider() +} + +@Composable +private fun ShareList(chatModel: ChatModel, search: String) { + val filter: (Chat) -> Boolean = { chat: Chat -> + chat.chatInfo.chatViewName.lowercase().contains(search.lowercase()) + } + val chats by remember(search) { + derivedStateOf { + if (search.isEmpty()) chatModel.chats.filter { it.chatInfo.ready } else chatModel.chats.filter { it.chatInfo.ready }.filter(filter) + } + } + LazyColumn( + modifier = Modifier.fillMaxWidth() + ) { + items(chats) { chat -> + ShareListNavLinkView(chat, chatModel) + } + } +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Enums.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Enums.kt new file mode 100644 index 000000000..d12ecd84b --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Enums.kt @@ -0,0 +1,9 @@ +package chat.simplex.app.views.helpers + +import android.net.Uri + +sealed class SharedContent { + data class Text(val text: String): SharedContent() + data class Image(val uri: Uri): SharedContent() + data class File(val uri: Uri): SharedContent() +} diff --git a/apps/android/app/src/main/res/values-de/strings.xml b/apps/android/app/src/main/res/values-de/strings.xml index 16f753a34..472525d67 100644 --- a/apps/android/app/src/main/res/values-de/strings.xml +++ b/apps/android/app/src/main/res/values-de/strings.xml @@ -170,6 +170,11 @@ Chatten Sie mit den Entwicklern Sie haben keine Chats + + Share message… + Share image… + Share file… + Anhängen Kontextsymbol diff --git a/apps/android/app/src/main/res/values-ru/strings.xml b/apps/android/app/src/main/res/values-ru/strings.xml index 4a7ba2ade..f12840a1c 100644 --- a/apps/android/app/src/main/res/values-ru/strings.xml +++ b/apps/android/app/src/main/res/values-ru/strings.xml @@ -170,6 +170,11 @@ Соединиться с разработчиками У вас нет чатов + + Отправить сообщение… + Отправить изображение… + Отправить файл… + Прикрепить Значок контекста diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index 3ec7a3e4c..f3425051d 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -170,6 +170,11 @@ Chat with the developers You have no chats + + Share message… + Share image… + Share file… + Attach Context icon