From 1edf60362e111fb79820929ba465f43bae72a835 Mon Sep 17 00:00:00 2001 From: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com> Date: Tue, 22 Feb 2022 00:09:51 +0400 Subject: [PATCH] android: UserProfileView (#341) * android: update user profile view logic * indentation * format * UserProfileView * remove prints * empty line * undo format * change by value * separate layout * layout * unconditionally editProfile = false * add header and close button to profile page, add links to "settings" * use generic navigate in settings, remove terminal button from the list of chats Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../.idea/codeStyles/codeStyleConfig.xml | 1 + .../java/chat/simplex/app/MainActivity.kt | 20 +- .../java/chat/simplex/app/model/ChatModel.kt | 13 +- .../java/chat/simplex/app/model/SimpleXAPI.kt | 23 ++- .../chat/simplex/app/views/chat/ChatView.kt | 4 +- .../app/views/chatlist/ChatListView.kt | 19 +- .../app/views/usersettings/SettingsView.kt | 115 ++++++----- .../app/views/usersettings/UserProfileView.kt | 191 ++++++++++++++++++ 8 files changed, 306 insertions(+), 80 deletions(-) create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserProfileView.kt diff --git a/apps/android/.idea/codeStyles/codeStyleConfig.xml b/apps/android/.idea/codeStyles/codeStyleConfig.xml index 79ee123c2b..6e6eec1148 100644 --- a/apps/android/.idea/codeStyles/codeStyleConfig.xml +++ b/apps/android/.idea/codeStyles/codeStyleConfig.xml @@ -1,5 +1,6 @@ \ No newline at end of file diff --git a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt index ac74e93a0d..ed237c9df2 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt @@ -21,6 +21,8 @@ import chat.simplex.app.views.chat.ChatView import chat.simplex.app.views.chatlist.ChatListView import chat.simplex.app.views.helpers.withApi import chat.simplex.app.views.newchat.* +import chat.simplex.app.views.usersettings.SettingsView +import chat.simplex.app.views.usersettings.UserProfileView import com.google.accompanist.permissions.ExperimentalPermissionsApi import kotlinx.coroutines.DelicateCoroutinesApi @@ -42,7 +44,7 @@ class MainActivity: ComponentActivity() { } @DelicateCoroutinesApi -class SimplexViewModel(application: Application) : AndroidViewModel(application) { +class SimplexViewModel(application: Application): AndroidViewModel(application) { val chatModel = getApplication().chatModel } @@ -103,6 +105,12 @@ fun Navigation(chatModel: ChatModel) { } ) ) { entry -> DetailView(entry.arguments!!.getLong("identifier"), chatModel.terminalItems, nav) } + composable(route = Pages.Settings.route) { + SettingsView(chatModel, nav) + } + composable(route = Pages.UserProfile.route) { + UserProfileView(chatModel, nav) + } } val am = chatModel.alertManager if (am.presentAlert.value) am.alertView.value?.invoke() @@ -110,15 +118,17 @@ fun Navigation(chatModel: ChatModel) { } sealed class Pages(val route: String) { - object Home : Pages("home") - object Terminal : Pages("terminal") - object Welcome : Pages("welcome") - object TerminalItemDetails : Pages("details") + object Home: Pages("home") + object Terminal: Pages("terminal") + object Welcome: Pages("welcome") + object TerminalItemDetails: Pages("details") object ChatList: Pages("chats") object Chat: Pages("chat") object AddContact: Pages("add_contact") object Connect: Pages("connect") object ChatInfo: Pages("chat_info") + object Settings: Pages("settings") + object UserProfile: Pages("user_profile") } @DelicateCoroutinesApi 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 cddc588055..f634e1101b 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 @@ -7,10 +7,6 @@ import chat.simplex.app.SimplexApp import kotlinx.datetime.* import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import kotlin.Boolean -import kotlin.Int -import kotlin.Long -import kotlin.String class ChatModel(val controller: ChatController, val alertManager: SimplexApp.AlertManager) { var currentUser = mutableStateOf(null) @@ -23,6 +19,13 @@ class ChatModel(val controller: ChatController, val alertManager: SimplexApp.Ale // set when app is opened via contact or invitation URI var appOpenUrl = mutableStateOf(null) + fun updateUserProfile(profile: Profile) { + val user = currentUser.value + if (user != null) { + currentUser.value = user.copy(profile = profile) + } + } + fun hasChat(id: String): Boolean = chats.firstOrNull() { it.id == id } != null fun getChat(id: String): Chat? = chats.firstOrNull { it.id == id } private fun getChatIndex(id: String): Int = chats.indexOfFirst { it.id == id } @@ -178,7 +181,7 @@ enum class ChatType(val type: String) { } @Serializable -class User ( +data class User( val userId: Long, val userContactId: Long, val localDisplayName: String, 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 c005dfd02e..b513098587 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 @@ -211,25 +211,28 @@ open class ChatController(val ctrl: ChatCtrl, val alertManager: SimplexApp.Alert fun processReceivedMsg(r: CR) { chatModel.terminalItems.add(TerminalItem.resp(r)) - when { - r is CR.ContactConnected -> chatModel.updateContact(r.contact) -// r is CR.ReceivedContactRequest -> return - r is CR.ContactUpdated -> { + when (r) { + is CR.ContactConnected -> { + chatModel.updateContact(r.contact) + chatModel.updateNetworkStatus(r.contact, Chat.NetworkStatus.Connected()) +// NtfManager.shared.notifyContactConnected(contact) + } +// is CR.ReceivedContactRequest -> return + is CR.ContactUpdated -> { val cInfo = ChatInfo.Direct(r.toContact) if (chatModel.hasChat(r.toContact.id)) { chatModel.updateChatInfo(cInfo) } } - - r is CR.ContactSubscribed -> { + is CR.ContactSubscribed -> { chatModel.updateContact(r.contact) chatModel.updateNetworkStatus(r.contact, Chat.NetworkStatus.Connected()) } - r is CR.ContactDisconnected -> { + is CR.ContactDisconnected -> { chatModel.updateContact(r.contact) chatModel.updateNetworkStatus(r.contact, Chat.NetworkStatus.Disconnected()) } - r is CR.ContactSubError -> { + is CR.ContactSubError -> { chatModel.updateContact(r.contact) val e = r.chatError val err: String = @@ -244,7 +247,7 @@ open class ChatController(val ctrl: ChatCtrl, val alertManager: SimplexApp.Alert else e.string chatModel.updateNetworkStatus(r.contact, Chat.NetworkStatus.Error(err)) } - r is CR.NewChatItem -> { + is CR.NewChatItem -> { val cInfo = r.chatItem.chatInfo val cItem = r.chatItem.chatItem chatModel.addChatItem(cInfo, cItem) @@ -252,8 +255,6 @@ open class ChatController(val ctrl: ChatCtrl, val alertManager: SimplexApp.Alert } // switch res { -// chatModel.updateNetworkStatus(contact, .connected) -// NtfManager.shared.notifyContactConnected(contact) // case let .receivedContactRequest(contactRequest): // chatModel.addChat(Chat( // chatInfo: ChatInfo.contactRequest(contactRequest: contactRequest), 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 f5e93b8258..b2bb96331c 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 @@ -118,9 +118,9 @@ fun ChatItemsList(chatItems: List) { } } -@Preview +@Preview(showBackground = true) @Composable -fun PreviewChatViewLayout() { +fun PreviewChatLayout() { SimpleXTheme { val chatItems = listOf( ChatItem.getSampleData( 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 38a8736056..9b52c930ae 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 @@ -63,13 +63,9 @@ fun ChatListView(chatModel: ChatModel, nav: NavController) { .fillMaxSize() .background(MaterialTheme.colors.background) ) { - ChatListToolbar(newChatCtrl) - Button( - onClick = { nav.navigate(Pages.Terminal.route) }, - modifier = Modifier.padding(14.dp) - ) { - Text("Terminal") - } + ChatListToolbar( + newChatCtrl, + settings = { nav.navigate(Pages.Settings.route) }) ChatList(chatModel, nav) } if (newChatCtrl.state.bottomSheetState.isExpanded) { @@ -85,7 +81,7 @@ fun ChatListView(chatModel: ChatModel, nav: NavController) { @ExperimentalMaterialApi @Composable -fun ChatListToolbar(newChatSheetCtrl: ScaffoldController) { +fun ChatListToolbar(newChatSheetCtrl: ScaffoldController, settings: () -> Unit) { Row( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, @@ -96,7 +92,9 @@ fun ChatListToolbar(newChatSheetCtrl: ScaffoldController) { Icons.Outlined.Settings, "Settings Cog", tint = MaterialTheme.colors.primary, - modifier = Modifier.padding(horizontal = 10.dp) + modifier = Modifier + .padding(horizontal = 10.dp) + .clickable(onClick = settings) ) Text( "Your chats", @@ -115,14 +113,13 @@ fun ChatListToolbar(newChatSheetCtrl: ScaffoldController) { } } - @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.chatId.value = cInfo.id chatModel.chatItems = chat.chatItems.toMutableStateList() navController.navigate(Pages.Chat.route) } else { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt index 527494be95..b0e3fde03e 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt @@ -4,55 +4,78 @@ import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler +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.model.Profile +import chat.simplex.app.ui.theme.SimpleXTheme -//@Preview(showBackground = true) @Composable -fun SettingsView(profile: Profile) { - Column() { - Text("Your Settings") - Spacer(Modifier.height(4.dp)) - Text("YOU", style= MaterialTheme.typography.h4) - Button( - onClick = { println(profile.displayName) } - ) { - Text(profile.displayName) - } - Button( - onClick = { println(profile.hashCode()) } - ) { - Text("Your SimpleX contact address", style= MaterialTheme.typography.body1) - } - Spacer(Modifier.height(10.dp)) - Text("HELP", style= MaterialTheme.typography.h4) - Button( - onClick = { println("navigate to help") } - ) { - Text("How to use SimpleX Chat") - } - Button( - onClick = { println("start help chat") } - ) { - Text("Get help & advice via chat") - } - Button( - onClick = { println("navigate to email") } - ) { - Text("Ask questions via email") - } - Spacer(Modifier.height(10.dp)) - Text("DEVELOP", style= MaterialTheme.typography.h4) - Button( - onClick = { println("navigate to console") } - ) { - Text("Chat console") - } - Button( - onClick = { println("navigate to github") } - ) { - Text("Install SimpleX for terminal") - } - } +fun SettingsView(chatModel: ChatModel, nav: NavController) { + val user = chatModel.currentUser.value + if (user != null) { + SettingsLayout( + profile = user.profile, + back = nav::popBackStack, + navigate = nav::navigate + ) + } } +val simplexTeamUri = "simplex:/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D" + +@Composable +fun SettingsLayout( + profile: Profile, + back: () -> Unit, + navigate: (String) -> Unit +) { + val uriHandler = LocalUriHandler.current + Column() { + Button(onClick = back) { + Text("Back") + } + Text("Your Settings") + Spacer(Modifier.height(4.dp)) + + Text("YOU", style = MaterialTheme.typography.h4) + Button(onClick = { navigate(Pages.UserProfile.route) }) { + Text(profile.displayName) + } + + Text("HELP", style = MaterialTheme.typography.h4) + Button(onClick = { println("navigate to help") }) { + Text("How to use SimpleX Chat") + } + Button(onClick = { uriHandler.openUri(simplexTeamUri) }) { + Text("Get help & advice via chat") + } + Button(onClick = { uriHandler.openUri("mailto:chat@simplex.chat") }) { + Text("Ask questions via email") + } + Spacer(Modifier.height(10.dp)) + + Text("DEVELOP", style = MaterialTheme.typography.h4) + Button(onClick = { navigate(Pages.Terminal.route) }) { + Text("Chat console") + } + Button(onClick = { uriHandler.openUri("https://github.com/simplex-chat/simplex-chat") }) { + Text("Install SimpleX for terminal") + } + } +} + +@Preview(showBackground = true) +@Composable +fun PreviewSettingsLayout() { + SimpleXTheme { + SettingsLayout( + profile = Profile.sampleData, + back = {}, + navigate = {} + ) + } +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserProfileView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserProfileView.kt new file mode 100644 index 0000000000..dc646d3246 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserProfileView.kt @@ -0,0 +1,191 @@ +package chat.simplex.app.views.usersettings + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import chat.simplex.app.model.ChatModel +import chat.simplex.app.model.Profile +import chat.simplex.app.ui.theme.SimpleXTheme +import chat.simplex.app.views.helpers.CloseSheetBar +import chat.simplex.app.views.helpers.withApi + +@Composable +fun UserProfileView(chatModel: ChatModel, nav: NavController) { + val user = chatModel.currentUser.value + if (user != null) { + var editProfile by remember { mutableStateOf(false) } + var profile by remember { mutableStateOf(user.profile) } + UserProfileLayout( + editProfile = editProfile, + profile = profile, + back = { nav.popBackStack() }, + editProfileOff = { editProfile = false }, + editProfileOn = { editProfile = true }, + saveProfile = { displayName: String, fullName: String -> + withApi { + val newProfile = chatModel.controller.apiUpdateProfile( + profile = Profile(displayName, fullName) + ) + if (newProfile != null) { + chatModel.updateUserProfile(newProfile) + profile = newProfile + } + editProfile = false + } + } + ) + } +} + +@Composable +fun UserProfileLayout( + editProfile: Boolean, + profile: Profile, + back: () -> Unit, + editProfileOff: () -> Unit, + editProfileOn: () -> Unit, + saveProfile: (String, String) -> Unit, +) { + Column( + modifier = Modifier + .padding(horizontal = 8.dp) + .fillMaxWidth(), + horizontalAlignment = Alignment.Start + ) { + CloseSheetBar(back) + Text("Your chat profile", + Modifier.padding(bottom = 24.dp), + style = MaterialTheme.typography.h1 + ) + Text( + "Your profile is stored on your device and shared only with your contacts.\n" + + "SimpleX servers cannot see your profile.", + Modifier.padding(bottom = 24.dp) + ) + if (editProfile) { + var displayName by remember { mutableStateOf(profile.displayName) } + var fullName by remember { mutableStateOf(profile.fullName) } + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.Start + ) { + // TODO hints + BasicTextField( + value = displayName, + onValueChange = { displayName = it }, + modifier = Modifier + .padding(bottom = 24.dp) + .fillMaxWidth(), + textStyle = TextStyle(fontSize = 16.sp), + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.None, + autoCorrect = false + ), + singleLine = true + ) + BasicTextField( + value = fullName, + onValueChange = { fullName = it }, + modifier = Modifier + .padding(bottom = 24.dp) + .fillMaxWidth(), + textStyle = TextStyle(fontSize = 16.sp), + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.None, + autoCorrect = false + ), + singleLine = true + ) + Row { + Text( + "Cancel", + color = MaterialTheme.colors.primary, + modifier = Modifier + .clickable(onClick = editProfileOff) + ) + Spacer(Modifier.padding(horizontal = 8.dp)) + Text( + "Save (and notify contacts)", + color = MaterialTheme.colors.primary, + modifier = Modifier + .clickable(onClick = { saveProfile(displayName, fullName) }) + ) + } + } + } else { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.Start + ) { + Row( + Modifier.padding(bottom = 24.dp) + ) { + Text("Display name:") + Spacer(Modifier.padding(horizontal = 4.dp)) + Text( + profile.displayName, + fontWeight = FontWeight.Bold + ) + } + Row( + Modifier.padding(bottom = 24.dp) + ) { + Text("Full name:") + Spacer(Modifier.padding(horizontal = 4.dp)) + Text( + profile.fullName, + fontWeight = FontWeight.Bold + ) + } + Text( + "Edit", + color = MaterialTheme.colors.primary, + modifier = Modifier + .clickable(onClick = editProfileOn) + ) + } + } + } +} + +@Preview(showBackground = true) +@Composable +fun PreviewUserProfileLayoutEditOff() { + SimpleXTheme { + UserProfileLayout( + profile = Profile.sampleData, + editProfile = false, + back = {}, + editProfileOff = {}, + editProfileOn = {}, + saveProfile = { _, _ -> } + ) + } +} + +@Preview(showBackground = true) +@Composable +fun PreviewUserProfileLayoutEditOn() { + SimpleXTheme { + UserProfileLayout( + profile = Profile.sampleData, + editProfile = true, + back = {}, + editProfileOff = {}, + editProfileOn = {}, + saveProfile = { _, _ -> } + ) + } +}