mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
android app: connect via QR code (#338)
* connecting via QR code works * add contact/scan qr code pages * new chat sheet layout * remove unused imports and some warnings
This commit is contained in:
committed by
GitHub
parent
f57a7009a3
commit
d48d4ed8f9
@@ -65,16 +65,27 @@ dependencies {
|
||||
implementation "androidx.compose.ui:ui:$compose_version"
|
||||
implementation "androidx.compose.material:material:$compose_version"
|
||||
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
|
||||
implementation 'androidx.activity:activity-compose:1.3.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
|
||||
implementation 'androidx.activity:activity-compose:1.4.0'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-datetime:0.3.2'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2'
|
||||
implementation "androidx.compose.material:material-icons-extended:$compose_version"
|
||||
implementation "androidx.navigation:navigation-compose:2.4.1"
|
||||
|
||||
def camerax_version = "1.1.0-beta01"
|
||||
implementation "androidx.camera:camera-core:${camerax_version}"
|
||||
implementation "androidx.camera:camera-camera2:${camerax_version}"
|
||||
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
|
||||
implementation "androidx.camera:camera-view:${camerax_version}"
|
||||
//Barcode
|
||||
implementation 'com.google.zxing:core:3.4.0'
|
||||
implementation 'com.google.mlkit:barcode-scanning:17.0.2'
|
||||
//Camera Permission
|
||||
implementation "com.google.accompanist:accompanist-permissions:0.19.0"
|
||||
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
|
||||
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
|
||||
implementation "androidx.navigation:navigation-compose:2.4.1"
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="chat.simplex.app">
|
||||
|
||||
<uses-feature android:name="android.hardware.camera" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
@@ -19,8 +19,10 @@ import chat.simplex.app.views.*
|
||||
import chat.simplex.app.views.chat.ChatView
|
||||
import chat.simplex.app.views.chatlist.*
|
||||
import chat.simplex.app.views.newchat.AddContactView
|
||||
import chat.simplex.app.views.newchat.ConnectContactView
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
|
||||
|
||||
@ExperimentalPermissionsApi
|
||||
@ExperimentalMaterialApi
|
||||
class MainActivity: ComponentActivity() {
|
||||
private val viewModel by viewModels<SimplexViewModel>()
|
||||
@@ -39,6 +41,7 @@ class SimplexViewModel(application: Application) : AndroidViewModel(application)
|
||||
val chatModel = getApplication<SimplexApp>().chatModel
|
||||
}
|
||||
|
||||
@ExperimentalPermissionsApi
|
||||
@ExperimentalMaterialApi
|
||||
@Composable
|
||||
fun Navigation(chatModel: ChatModel) {
|
||||
@@ -64,6 +67,9 @@ fun Navigation(chatModel: ChatModel) {
|
||||
composable(route = Pages.AddContact.route) {
|
||||
AddContactView(chatModel, nav)
|
||||
}
|
||||
composable(route = Pages.Connect.route) {
|
||||
ConnectContactView(chatModel, nav)
|
||||
}
|
||||
composable(route = Pages.Terminal.route) {
|
||||
TerminalView(chatModel, navController = nav)
|
||||
}
|
||||
@@ -86,4 +92,5 @@ sealed class Pages(val route: String) {
|
||||
object ChatList: Pages("chats")
|
||||
object Chat: Pages("chat")
|
||||
object AddContact: Pages("add_contact")
|
||||
object Connect: Pages("connect")
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@ import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.views.TerminalView
|
||||
import chat.simplex.app.views.WelcomeView
|
||||
import chat.simplex.app.views.chatlist.ChatListView
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
|
||||
@ExperimentalPermissionsApi
|
||||
@ExperimentalMaterialApi
|
||||
@Composable
|
||||
fun MainPage(chatModel: ChatModel, nav: NavController) {
|
||||
|
||||
@@ -16,13 +16,18 @@ val Typography = Typography(
|
||||
h2 = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 20.sp
|
||||
fontSize = 24.sp
|
||||
),
|
||||
body1 = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp
|
||||
),
|
||||
body2 = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 14.sp
|
||||
),
|
||||
button = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
@@ -31,6 +36,6 @@ val Typography = Typography(
|
||||
caption = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 14.sp
|
||||
fontSize = 20.sp
|
||||
)
|
||||
)
|
||||
@@ -13,22 +13,14 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.navigation.*
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import chat.simplex.app.model.*
|
||||
import androidx.navigation.navArgument
|
||||
import chat.simplex.app.Pages
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.model.TerminalItem
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
import chat.simplex.app.views.chat.SendMsgView
|
||||
import chat.simplex.app.views.helpers.withApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
|
||||
@DelicateCoroutinesApi
|
||||
@Composable
|
||||
fun TerminalView(chatModel: ChatModel, navController: NavController) {
|
||||
Column {
|
||||
|
||||
@@ -13,14 +13,12 @@ import androidx.compose.material.TextField
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.Modifier
|
||||
import chat.simplex.app.SimplexViewModel
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.model.Profile
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import chat.simplex.app.views.helpers.withApi
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
|
||||
@DelicateCoroutinesApi
|
||||
@Composable
|
||||
fun WelcomeView(chatModel: ChatModel, routeHome: () -> Unit) {
|
||||
Column(
|
||||
@@ -38,6 +36,7 @@ fun WelcomeView(chatModel: ChatModel, routeHome: () -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
@DelicateCoroutinesApi
|
||||
@Composable
|
||||
fun CreateProfilePanel(chatModel: ChatModel, routeHome: () -> Unit) {
|
||||
var displayName by remember { mutableStateOf("") }
|
||||
@@ -51,14 +50,12 @@ fun CreateProfilePanel(chatModel: ChatModel, routeHome: () -> Unit) {
|
||||
Text("Full Name (Optional)")
|
||||
TextField(value = fullName, onValueChange = { fullName = it })
|
||||
Button(onClick={
|
||||
GlobalScope.launch {
|
||||
withContext(Dispatchers.Main) {
|
||||
val user = chatModel.controller.apiCreateActiveUser(
|
||||
Profile(displayName, fullName)
|
||||
)
|
||||
chatModel.controller.startChat(user)
|
||||
routeHome()
|
||||
}
|
||||
withApi {
|
||||
val user = chatModel.controller.apiCreateActiveUser(
|
||||
Profile(displayName, fullName)
|
||||
)
|
||||
chatModel.controller.startChat(user)
|
||||
routeHome()
|
||||
}
|
||||
},
|
||||
enabled = displayName.isNotEmpty()
|
||||
|
||||
@@ -19,8 +19,10 @@ import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
import chat.simplex.app.views.chat.item.ChatItemView
|
||||
import chat.simplex.app.views.helpers.withApi
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
@DelicateCoroutinesApi
|
||||
@Composable
|
||||
fun ChatView(chatModel: ChatModel, nav: NavController) {
|
||||
if (chatModel.chatId.value != null && chatModel.chats.count() > 0) {
|
||||
|
||||
@@ -20,11 +20,9 @@ val ReceivedColorLight = Color(0x1EF1F0F5)
|
||||
@Composable
|
||||
fun TextItemView(chatItem: ChatItem) {
|
||||
val sent = chatItem.chatDir.sent
|
||||
val color = if (sent) SentColorLight else ReceivedColorLight
|
||||
|
||||
Surface(
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
color = color
|
||||
color = if (sent) SentColorLight else ReceivedColorLight
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.padding(vertical = 6.dp, horizontal = 12.dp)
|
||||
|
||||
@@ -5,25 +5,25 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import chat.simplex.app.Pages
|
||||
import chat.simplex.app.model.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import chat.simplex.app.views.helpers.withApi
|
||||
import chat.simplex.app.views.newchat.NewChatSheet
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
|
||||
@@ -46,6 +46,8 @@ fun scaffoldController(): ScaffoldController {
|
||||
)
|
||||
}
|
||||
|
||||
@DelicateCoroutinesApi
|
||||
@ExperimentalPermissionsApi
|
||||
@ExperimentalMaterialApi
|
||||
@Composable
|
||||
fun ChatListView(chatModel: ChatModel, nav: NavController) {
|
||||
@@ -53,20 +55,31 @@ fun ChatListView(chatModel: ChatModel, nav: NavController) {
|
||||
BottomSheetScaffold(
|
||||
scaffoldState = newChatCtrl.state,
|
||||
sheetPeekHeight = 0.dp,
|
||||
topBar = { ChatListToolbar(newChatCtrl) },
|
||||
sheetContent = { NewChatSheet(chatModel, newChatCtrl, nav) }
|
||||
sheetContent = { NewChatSheet(chatModel, newChatCtrl, nav) },
|
||||
sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp),
|
||||
) {
|
||||
Column(modifier = Modifier
|
||||
.padding(vertical = 8.dp)
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colors.background)
|
||||
) {
|
||||
ChatList(chatModel, nav)
|
||||
Button(
|
||||
onClick = { nav.navigate(Pages.Terminal.route) },
|
||||
modifier = Modifier.padding(14.dp)
|
||||
Box {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 8.dp)
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colors.background)
|
||||
) {
|
||||
Text("Terminal")
|
||||
ChatListToolbar(newChatCtrl)
|
||||
ChatList(chatModel, nav)
|
||||
Button(
|
||||
onClick = { nav.navigate(Pages.Terminal.route) },
|
||||
modifier = Modifier.padding(14.dp)
|
||||
) {
|
||||
Text("Terminal")
|
||||
}
|
||||
}
|
||||
if (newChatCtrl.state.bottomSheetState.isExpanded) {
|
||||
Surface(Modifier
|
||||
.fillMaxSize()
|
||||
.clickable { newChatCtrl.collapse() },
|
||||
color = Color.Black.copy(alpha = 0.12F)
|
||||
) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,29 +110,30 @@ fun ChatListToolbar(newChatSheetCtrl: ScaffoldController) {
|
||||
Icons.Outlined.PersonAdd,
|
||||
"Add Contact",
|
||||
tint = MaterialTheme.colors.primary,
|
||||
modifier = Modifier.padding(horizontal = 10.dp)
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 10.dp)
|
||||
.clickable { newChatSheetCtrl.toggle() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun goToChat(chat: Chat, chatModel: ChatModel, navController: NavController) {
|
||||
GlobalScope.launch {
|
||||
withContext(Dispatchers.Main) {
|
||||
val cInfo = chat.chatInfo
|
||||
val chat = chatModel.controller.apiGetChat(cInfo.chatType, cInfo.apiId)
|
||||
if (chat != null ) {
|
||||
chatModel.chatId = mutableStateOf(cInfo.id)
|
||||
chatModel.chatItems = chat.chatItems.toMutableStateList()
|
||||
navController.navigate(Pages.Chat.route)
|
||||
} else {
|
||||
// TODO show error? or will apiGetChat show it
|
||||
}
|
||||
@DelicateCoroutinesApi
|
||||
fun goToChat(chatPreview: Chat, chatModel: ChatModel, navController: NavController) {
|
||||
withApi {
|
||||
val cInfo = chatPreview.chatInfo
|
||||
val chat = chatModel.controller.apiGetChat(cInfo.chatType, cInfo.apiId)
|
||||
if (chat != null ) {
|
||||
chatModel.chatId = mutableStateOf(cInfo.id)
|
||||
chatModel.chatItems = chat.chatItems.toMutableStateList()
|
||||
navController.navigate(Pages.Chat.route)
|
||||
} else {
|
||||
// TODO show error? or will apiGetChat show it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@DelicateCoroutinesApi
|
||||
@Composable
|
||||
fun ChatList(chatModel: ChatModel, navController: NavController) {
|
||||
LazyColumn(
|
||||
|
||||
@@ -18,16 +18,19 @@ import chat.simplex.app.views.newchat.AddContactLayout
|
||||
|
||||
@Composable
|
||||
fun CloseSheetBar(close: () -> Unit) {
|
||||
Row (Modifier
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End
|
||||
Row (
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(40.dp),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Outlined.Close,
|
||||
"Close button",
|
||||
tint = MaterialTheme.colors.primary,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 10.dp)
|
||||
.padding(vertical = 10.dp)
|
||||
.clickable { close() }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ fun SimpleButton(text: String, icon: ImageVector, click: () -> Unit) {
|
||||
modifier = Modifier.padding(horizontal = 10.dp)
|
||||
)
|
||||
Text(text,
|
||||
style = MaterialTheme.typography.button,
|
||||
style = MaterialTheme.typography.caption,
|
||||
color = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,4 @@ import kotlinx.coroutines.*
|
||||
|
||||
@DelicateCoroutinesApi
|
||||
fun withApi(action: suspend CoroutineScope.() -> Unit): Job =
|
||||
GlobalScope.launch {
|
||||
withContext(Dispatchers.Main) { action() }
|
||||
}
|
||||
|
||||
GlobalScope.launch { withContext(Dispatchers.Main, action) }
|
||||
|
||||
@@ -3,11 +3,8 @@ package chat.simplex.app.views.newchat
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Share
|
||||
@@ -15,11 +12,12 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import chat.simplex.app.Pages
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.ui.theme.SimpleButton
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
@@ -56,14 +54,24 @@ fun AddContactLayout(connReq: String, close: () -> Unit, share: () -> Unit) {
|
||||
Text(
|
||||
"Show QR code to your contact\nto scan from the app",
|
||||
style = MaterialTheme.typography.h2,
|
||||
textAlign = TextAlign.Center
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
QRCode(connReq)
|
||||
Text(
|
||||
"If you can't show QR code, you can share the invitation link via any channel.\nIt does not have to be secure or private, you just have to know who you are sharing it with.",
|
||||
buildAnnotatedString {
|
||||
append("If you cannot meet in person, you can ")
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
append("scan QR code in the video call")
|
||||
}
|
||||
append(", or you can share the invitation link via any other channel.")
|
||||
},
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.caption,
|
||||
modifier = Modifier.padding(bottom = 16.dp).padding(horizontal = 16.dp)
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(top = 8.dp)
|
||||
.padding(bottom = 16.dp)
|
||||
)
|
||||
SimpleButton("Share invitation link", icon = Icons.Outlined.Share, click = share)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package chat.simplex.app.views.newchat
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
import chat.simplex.app.views.helpers.CloseSheetBar
|
||||
import chat.simplex.app.views.helpers.withApi
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
|
||||
@DelicateCoroutinesApi
|
||||
@Composable
|
||||
fun ConnectContactView(chatModel: ChatModel, nav: NavController) {
|
||||
ConnectContactLayout(
|
||||
qrCodeScanner = {
|
||||
QRCodeScanner { connReqUri ->
|
||||
withApi {
|
||||
val res = chatModel.controller.apiConnect(connReqUri)
|
||||
// check if it is valid
|
||||
nav.popBackStack()
|
||||
}
|
||||
}
|
||||
},
|
||||
close = { nav.popBackStack() }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ConnectContactLayout(qrCodeScanner: @Composable () -> Unit, close: () -> Unit) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 8.dp)
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colors.background),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
CloseSheetBar(close)
|
||||
Text(
|
||||
"Scan QR code",
|
||||
style = MaterialTheme.typography.h1,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
Text(
|
||||
"Your chat profile will be sent\nto your contact",
|
||||
style = MaterialTheme.typography.h2,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
)
|
||||
Box (
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(ratio = 1F)
|
||||
) { qrCodeScanner() }
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
append("If you cannot meet in person, you can ")
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
append("scan QR code in the video call")
|
||||
}
|
||||
append(", or you can create the invitation link.")
|
||||
},
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.caption,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(top = 16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewConnectContactLayout() {
|
||||
SimpleXTheme {
|
||||
ConnectContactLayout(
|
||||
qrCodeScanner = { Surface {} },
|
||||
close = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,57 @@
|
||||
package chat.simplex.app.views.newchat
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import android.Manifest
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import chat.simplex.app.Pages
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.chatlist.ScaffoldController
|
||||
import chat.simplex.app.views.helpers.withApi
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
|
||||
@DelicateCoroutinesApi
|
||||
@ExperimentalPermissionsApi
|
||||
@ExperimentalMaterialApi
|
||||
@Composable
|
||||
fun NewChatSheet(chatModel: ChatModel, newChatCtrl: ScaffoldController, nav: NavController) {
|
||||
val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
|
||||
NewChatSheetLayout(
|
||||
addContact = {
|
||||
withApi {
|
||||
// show spinner
|
||||
// show spinner
|
||||
chatModel.connReqInvitation = chatModel.controller.apiAddContact()
|
||||
// hide spinner
|
||||
// hide spinner
|
||||
if (chatModel.connReqInvitation != null) {
|
||||
newChatCtrl.collapse()
|
||||
nav.navigate(Pages.AddContact.route)
|
||||
}
|
||||
}
|
||||
},
|
||||
scanCode = {},
|
||||
scanCode = {
|
||||
newChatCtrl.collapse()
|
||||
nav.navigate(Pages.Connect.route)
|
||||
cameraPermissionState.launchPermissionRequest()
|
||||
},
|
||||
close = {
|
||||
newChatCtrl.collapse()
|
||||
}
|
||||
@@ -40,19 +60,60 @@ fun NewChatSheet(chatModel: ChatModel, newChatCtrl: ScaffoldController, nav: Nav
|
||||
|
||||
@Composable
|
||||
fun NewChatSheetLayout(addContact: () -> Unit, scanCode: () -> Unit, close: () -> Unit) {
|
||||
Column ( Modifier.padding(all = 8.dp) ) {
|
||||
Button(onClick = addContact) {
|
||||
Text("Add contact")
|
||||
Row(Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 48.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
Box(Modifier.weight(1F).fillMaxWidth()) {
|
||||
ActionButton(
|
||||
"Add contact", "(create QR code\nor link)",
|
||||
Icons.Outlined.PersonAdd, click = addContact
|
||||
)
|
||||
}
|
||||
Button(onClick = scanCode) {
|
||||
Text("Scan QR code")
|
||||
Box(Modifier.weight(1F).fillMaxWidth()) {
|
||||
ActionButton(
|
||||
"Scan QR code", "(in person or in video call)",
|
||||
Icons.Outlined.QrCode, click = scanCode
|
||||
)
|
||||
}
|
||||
Button(onClick = close) {
|
||||
Text("Cancel")
|
||||
Box(Modifier.weight(1F).fillMaxWidth()) {
|
||||
ActionButton(
|
||||
"Create Group", "(coming soon!)",
|
||||
Icons.Outlined.GroupAdd, disabled = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ActionButton(text: String, comment: String, icon: ImageVector, disabled: Boolean = false,
|
||||
click: () -> Unit = {}) {
|
||||
Column(
|
||||
Modifier
|
||||
.clickable(onClick = click)
|
||||
.padding(horizontal = 8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
val tint = if (disabled) DarkGray else MaterialTheme.colors.primary
|
||||
Icon(icon, text,
|
||||
tint = tint,
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.padding(bottom = 8.dp))
|
||||
Text(text,
|
||||
textAlign = TextAlign.Center,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = tint,
|
||||
modifier = Modifier.padding(bottom = 4.dp)
|
||||
)
|
||||
Text(comment,
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.body2
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewNewChatSheet() {
|
||||
|
||||
@@ -3,15 +3,9 @@ package chat.simplex.app.views.newchat
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.toSize
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.EncodeHintType
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
package chat.simplex.app.views.newchat
|
||||
|
||||
import android.util.Log
|
||||
import android.view.ViewGroup
|
||||
import androidx.camera.core.*
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||
import androidx.camera.view.PreviewView
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
|
||||
import com.google.mlkit.vision.barcode.BarcodeScanning
|
||||
import com.google.mlkit.vision.barcode.common.Barcode
|
||||
import com.google.mlkit.vision.common.InputImage
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
// Bar code scanner adapted from https://github.com/MakeItEasyDev/Jetpack-Compose-BarCode-Scanner
|
||||
|
||||
@Composable
|
||||
fun QRCodeScanner(onBarcode: (String) -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
var preview by remember { mutableStateOf<Preview?>(null) }
|
||||
|
||||
AndroidView(
|
||||
factory = { AndroidViewContext ->
|
||||
PreviewView(AndroidViewContext).apply {
|
||||
this.scaleType = PreviewView.ScaleType.FILL_CENTER
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
)
|
||||
implementationMode = PreviewView.ImplementationMode.COMPATIBLE
|
||||
}
|
||||
},
|
||||
// modifier = Modifier.fillMaxSize(),
|
||||
update = { previewView ->
|
||||
val cameraSelector: CameraSelector = CameraSelector.Builder()
|
||||
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
|
||||
.build()
|
||||
val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor()
|
||||
val cameraProviderFuture: ListenableFuture<ProcessCameraProvider> =
|
||||
ProcessCameraProvider.getInstance(context)
|
||||
|
||||
cameraProviderFuture.addListener({
|
||||
preview = Preview.Builder().build().also {
|
||||
it.setSurfaceProvider(previewView.surfaceProvider)
|
||||
}
|
||||
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
|
||||
val barcodeAnalyser = BarCodeAnalyser { barcodes ->
|
||||
barcodes.firstOrNull()?.rawValue?.let(onBarcode)
|
||||
}
|
||||
val imageAnalysis: ImageAnalysis = ImageAnalysis.Builder()
|
||||
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||
.build()
|
||||
.also { it.setAnalyzer(cameraExecutor, barcodeAnalyser) }
|
||||
|
||||
try {
|
||||
cameraProvider.unbindAll()
|
||||
cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview, imageAnalysis)
|
||||
} catch (e: Exception) {
|
||||
Log.d("SIMPLEX", "CameraPreview: ${e.localizedMessage}")
|
||||
}
|
||||
}, ContextCompat.getMainExecutor(context))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
class BarCodeAnalyser(
|
||||
private val onBarcodeDetected: (barcodes: List<Barcode>) -> Unit,
|
||||
): ImageAnalysis.Analyzer {
|
||||
private var lastAnalyzedTimeStamp = 0L
|
||||
|
||||
@ExperimentalGetImage
|
||||
override fun analyze(image: ImageProxy) {
|
||||
val currentTimestamp = System.currentTimeMillis()
|
||||
if (currentTimestamp - lastAnalyzedTimeStamp >= TimeUnit.SECONDS.toMillis(1)) {
|
||||
image.image?.let { imageToAnalyze ->
|
||||
val options = BarcodeScannerOptions.Builder()
|
||||
.setBarcodeFormats(Barcode.FORMAT_ALL_FORMATS)
|
||||
.build()
|
||||
val barcodeScanner = BarcodeScanning.getClient(options)
|
||||
val imageToProcess = InputImage.fromMediaImage(imageToAnalyze, image.imageInfo.rotationDegrees)
|
||||
|
||||
barcodeScanner.process(imageToProcess)
|
||||
.addOnSuccessListener { barcodes ->
|
||||
if (barcodes.isNotEmpty()) {
|
||||
onBarcodeDetected(barcodes)
|
||||
} else {
|
||||
Log.d("SIMPLEX", "BarcodeAnalyser: No barcode Scanned")
|
||||
}
|
||||
}
|
||||
.addOnFailureListener { exception ->
|
||||
Log.d("SIMPLEX", "BarcodeAnalyser: Something went wrong $exception")
|
||||
}
|
||||
.addOnCompleteListener {
|
||||
image.close()
|
||||
}
|
||||
}
|
||||
lastAnalyzedTimeStamp = currentTimestamp
|
||||
} else {
|
||||
image.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user