From 0e18b13beaaf46c65374ec6d5f82a2f1bcf6238c Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Fri, 1 Dec 2023 03:38:21 +0800 Subject: [PATCH] desktop: adapting onboarding process to linking devices (#3490) * desktop: adapting onboarding process to linking devices * show progress on long operations * changes * clearing chat cache logic * lines --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../views/onboarding/SimpleXInfo.android.kt | 15 ++ .../kotlin/chat/simplex/common/App.kt | 56 ++++-- .../chat/simplex/common/model/ChatModel.kt | 9 +- .../chat/simplex/common/model/SimpleXAPI.kt | 55 +++++- .../chat/simplex/common/platform/Core.kt | 14 +- .../simplex/common/platform/NtfManager.kt | 8 +- .../chat/simplex/common/views/WelcomeView.kt | 23 ++- .../common/views/chatlist/ChatListView.kt | 29 +-- .../common/views/chatlist/ShareListView.kt | 4 +- .../common/views/chatlist/UserPicker.kt | 53 ++++-- .../simplex/common/views/helpers/Section.kt | 6 +- .../views/onboarding/CreateSimpleXAddress.kt | 4 + .../views/onboarding/LinkAMobileView.kt | 91 +++++++++ .../common/views/onboarding/OnboardingView.kt | 1 + .../onboarding/SetupDatabasePassphrase.kt | 11 +- .../common/views/onboarding/SimpleXInfo.kt | 16 +- .../common/views/remote/ConnectMobileView.kt | 144 +++++++++------ .../views/usersettings/NetworkAndServers.kt | 23 ++- .../views/usersettings/PrivacySettings.kt | 64 +++---- .../common/views/usersettings/SettingsView.kt | 173 ++++++++++-------- .../views/usersettings/UserProfilesView.kt | 5 +- .../commonMain/resources/MR/base/strings.xml | 3 + .../resources/MR/images/ic_refresh.svg | 1 + .../views/onboarding/SimpleXInfo.desktop.kt | 23 +++ 24 files changed, 565 insertions(+), 266 deletions(-) create mode 100644 apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.android.kt create mode 100644 apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/LinkAMobileView.kt create mode 100644 apps/multiplatform/common/src/commonMain/resources/MR/images/ic_refresh.svg create mode 100644 apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.desktop.kt diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.android.kt new file mode 100644 index 000000000..bfe87b17d --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.android.kt @@ -0,0 +1,15 @@ +package chat.simplex.common.views.onboarding + +import androidx.compose.runtime.Composable +import chat.simplex.common.model.SharedPreference +import chat.simplex.common.model.User +import chat.simplex.res.MR + +@Composable +actual fun OnboardingActionButton(user: User?, onboardingStage: SharedPreference, onclick: (() -> Unit)?) { + if (user == null) { + OnboardingActionButton(MR.strings.create_your_profile, onboarding = OnboardingStage.Step2_CreateProfile, true, onclick = onclick) + } else { + OnboardingActionButton(MR.strings.make_private_connection, onboarding = OnboardingStage.OnboardingComplete, true, onclick = onclick) + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt index cfbcb7aa4..d4062a1aa 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt @@ -37,8 +37,7 @@ import kotlinx.coroutines.flow.* data class SettingsViewState( val userPickerState: MutableStateFlow, - val scaffoldState: ScaffoldState, - val switchingUsersAndHosts: MutableState + val scaffoldState: ScaffoldState ) @Composable @@ -102,11 +101,8 @@ fun MainScreen() { } Box { - var onboarding by remember { mutableStateOf(chatModel.controller.appPrefs.onboardingStage.get()) } - LaunchedEffect(Unit) { - snapshotFlow { chatModel.controller.appPrefs.onboardingStage.state.value }.distinctUntilChanged().collect { onboarding = it } - } - val userCreated = chatModel.userCreated.value + val onboarding by remember { chatModel.controller.appPrefs.onboardingStage.state } + val localUserCreated = chatModel.localUserCreated.value var showInitializationView by remember { mutableStateOf(false) } when { chatModel.chatDbStatus.value == null && showInitializationView -> InitializationView() @@ -115,14 +111,18 @@ fun MainScreen() { DatabaseErrorView(chatModel.chatDbStatus, chatModel.controller.appPrefs) } } - remember { chatModel.chatDbEncrypted }.value == null || userCreated == null -> SplashView() - onboarding == OnboardingStage.OnboardingComplete && userCreated -> { + remember { chatModel.chatDbEncrypted }.value == null || localUserCreated == null -> SplashView() + onboarding == OnboardingStage.OnboardingComplete -> { Box { showAdvertiseLAAlert = true - val userPickerState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) } + val userPickerState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(if (chatModel.desktopNoUserNoRemote()) AnimatedViewState.VISIBLE else AnimatedViewState.GONE)) } + KeyChangeEffect(chatModel.desktopNoUserNoRemote) { + if (chatModel.desktopNoUserNoRemote() && !ModalManager.start.hasModalsOpen()) { + userPickerState.value = AnimatedViewState.VISIBLE + } + } val scaffoldState = rememberScaffoldState() - val switchingUsersAndHosts = rememberSaveable { mutableStateOf(false) } - val settingsState = remember { SettingsViewState(userPickerState, scaffoldState, switchingUsersAndHosts) } + val settingsState = remember { SettingsViewState(userPickerState, scaffoldState) } if (appPlatform.isAndroid) { AndroidScreen(settingsState) } else { @@ -137,12 +137,14 @@ fun MainScreen() { } } onboarding == OnboardingStage.Step2_CreateProfile -> CreateFirstProfile(chatModel) {} + onboarding == OnboardingStage.LinkAMobile -> LinkAMobile() onboarding == OnboardingStage.Step2_5_SetupDatabasePassphrase -> SetupDatabasePassphrase(chatModel) onboarding == OnboardingStage.Step3_CreateSimpleXAddress -> CreateSimpleXAddress(chatModel, null) onboarding == OnboardingStage.Step4_SetNotificationsMode -> SetNotificationsMode(chatModel) } if (appPlatform.isAndroid) { ModalManager.fullscreen.showInView() + SwitchingUsersView() } val unauthorized = remember { derivedStateOf { AppLock.userAuthorized.value != true } } @@ -262,7 +264,7 @@ fun CenterPartOfScreen() { .background(MaterialTheme.colors.background), contentAlignment = Alignment.Center ) { - Text(stringResource(MR.strings.no_selected_chat)) + Text(stringResource(if (chatModel.desktopNoUserNoRemote) MR.strings.no_connected_mobile else MR.strings.no_selected_chat)) } } else { ModalManager.center.showInView() @@ -286,6 +288,7 @@ fun DesktopScreen(settingsState: SettingsViewState) { } Box(Modifier.widthIn(max = DEFAULT_START_MODAL_WIDTH)) { ModalManager.start.showInView() + SwitchingUsersView() } Row(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH).clipToBounds()) { Box(Modifier.widthIn(min = DEFAULT_MIN_CENTER_MODAL_WIDTH).weight(1f)) { @@ -298,7 +301,7 @@ fun DesktopScreen(settingsState: SettingsViewState) { EndPartOfScreen() } } - val (userPickerState, scaffoldState, switchingUsersAndHosts ) = settingsState + val (userPickerState, scaffoldState ) = settingsState val scope = rememberCoroutineScope() if (scaffoldState.drawerState.isOpen) { Box( @@ -312,7 +315,7 @@ fun DesktopScreen(settingsState: SettingsViewState) { ) } VerticalDivider(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH)) - UserPicker(chatModel, userPickerState, switchingUsersAndHosts) { + UserPicker(chatModel, userPickerState) { scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() } userPickerState.value = AnimatedViewState.GONE } @@ -335,3 +338,26 @@ fun InitializationView() { } } } + +@Composable +private fun SwitchingUsersView() { + if (remember { chatModel.switchingUsersAndHosts }.value) { + Box( + Modifier.fillMaxSize().clickable(enabled = false, onClick = {}), + contentAlignment = Alignment.Center + ) { + ProgressIndicator() + } + } +} + +@Composable +private fun ProgressIndicator() { + CircularProgressIndicator( + Modifier + .padding(horizontal = 2.dp) + .size(30.dp), + color = MaterialTheme.colors.secondary, + strokeWidth = 2.5.dp + ) +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 60b70c0ae..808b1938e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -2,7 +2,7 @@ package chat.simplex.common.model import androidx.compose.material.* import androidx.compose.runtime.* -import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.SpanStyle @@ -43,7 +43,7 @@ object ChatModel { val setDeliveryReceipts = mutableStateOf(false) val currentUser = mutableStateOf(null) val users = mutableStateListOf() - val userCreated = mutableStateOf(null) + val localUserCreated = mutableStateOf(null) val chatRunning = mutableStateOf(null) val chatDbChanged = mutableStateOf(false) val chatDbEncrypted = mutableStateOf(false) @@ -51,6 +51,7 @@ object ChatModel { val chats = mutableStateListOf() // map of connections network statuses, key is agent connection id val networkStatuses = mutableStateMapOf() + val switchingUsersAndHosts = mutableStateOf(false) // current chat val chatId = mutableStateOf(null) @@ -108,6 +109,9 @@ object ChatModel { var updatingChatsMutex: Mutex = Mutex() + val desktopNoUserNoRemote: Boolean @Composable get() = appPlatform.isDesktop && currentUser.value == null && currentRemoteHost.value == null + fun desktopNoUserNoRemote(): Boolean = appPlatform.isDesktop && currentUser.value == null && currentRemoteHost.value == null + // remote controller val remoteHosts = mutableStateListOf() val currentRemoteHost = mutableStateOf(null) @@ -620,6 +624,7 @@ object ChatModel { terminalItems.add(item) } + val connectedToRemote: Boolean @Composable get() = currentRemoteHost.value != null || remoteCtrlSession.value?.active == true fun connectedToRemote(): Boolean = currentRemoteHost.value != null || remoteCtrlSession.value?.active == true } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 34b6fd99f..c61772578 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -362,7 +362,7 @@ object ChatController { chatModel.users.addAll(users) if (justStarted) { chatModel.currentUser.value = user - chatModel.userCreated.value = true + chatModel.localUserCreated.value = true getUserChatData(null) appPrefs.chatLastStart.set(Clock.System.now()) chatModel.chatRunning.value = true @@ -382,6 +382,31 @@ object ChatController { } } + suspend fun startChatWithoutUser() { + Log.d(TAG, "user: null") + try { + if (chatModel.chatRunning.value == true) return + apiSetTempFolder(coreTmpDir.absolutePath) + apiSetFilesFolder(appFilesDir.absolutePath) + if (appPlatform.isDesktop) { + apiSetRemoteHostsFolder(remoteHostsDir.absolutePath) + } + apiSetXFTPConfig(getXFTPCfg()) + apiSetEncryptLocalFiles(appPrefs.privacyEncryptLocalFiles.get()) + chatModel.users.clear() + chatModel.currentUser.value = null + chatModel.localUserCreated.value = false + appPrefs.chatLastStart.set(Clock.System.now()) + chatModel.chatRunning.value = true + startReceiver() + setLocalDeviceName(appPrefs.deviceNameForRemoteAccess.get()!!) + Log.d(TAG, "startChat: started without user") + } catch (e: Error) { + Log.e(TAG, "failed starting chat without user $e") + throw e + } + } + suspend fun changeActiveUser(rhId: Long?, toUserId: Long, viewPwd: String?) { try { changeActiveUser_(rhId, toUserId, viewPwd) @@ -475,7 +500,9 @@ object ChatController { val r = sendCmd(rh, CC.ShowActiveUser()) if (r is CR.ActiveUser) return r.user.updateRemoteHostId(rh) Log.d(TAG, "apiGetActiveUser: ${r.responseType} ${r.details}") - chatModel.userCreated.value = false + if (rh == null) { + chatModel.localUserCreated.value = false + } return null } @@ -1990,7 +2017,7 @@ object ChatController { chatModel.setContactNetworkStatus(contact, NetworkStatus.Error(err)) } - suspend fun switchUIRemoteHost(rhId: Long?) { + suspend fun switchUIRemoteHost(rhId: Long?) = showProgressIfNeeded { // TODO lock the switch so that two switches can't run concurrently? chatModel.chatId.value = null ModalManager.center.closeModals() @@ -2003,7 +2030,10 @@ object ChatController { chatModel.users.clear() chatModel.users.addAll(users) chatModel.currentUser.value = user - chatModel.userCreated.value = true + if (user == null) { + chatModel.chatItems.clear() + chatModel.chats.clear() + } val statuses = apiGetNetworkStatuses(rhId) if (statuses != null) { chatModel.networkStatuses.clear() @@ -2013,6 +2043,23 @@ object ChatController { getUserChatData(rhId) } + suspend fun showProgressIfNeeded(block: suspend () -> Unit) { + val job = withBGApi { + try { + delay(500) + chatModel.switchingUsersAndHosts.value = true + } catch (e: Throwable) { + chatModel.switchingUsersAndHosts.value = false + } + } + try { + block() + } finally { + job.cancel() + chatModel.switchingUsersAndHosts.value = false + } + } + fun getXFTPCfg(): XFTPFileConfig { return XFTPFileConfig(minFileSize = 0) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt index 3d3a91cb3..a4c1c333e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt @@ -55,10 +55,22 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat if (appPreferences.encryptionStartedAt.get() != null) appPreferences.encryptionStartedAt.set(null) val user = chatController.apiGetActiveUser(null) if (user == null) { - chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true) chatModel.currentUser.value = null chatModel.users.clear() + if (appPlatform.isDesktop) { + /** + * Setting it here to null because otherwise the screen will flash in [MainScreen] after the first start + * because of default value of [OnboardingStage.OnboardingComplete] + * */ + chatModel.localUserCreated.value = null + if (chatController.listRemoteHosts()?.isEmpty() == true) { + chatController.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) + } + chatController.startChatWithoutUser() + } else { + chatController.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) + } } else { val savedOnboardingStage = appPreferences.onboardingStage.get() appPreferences.onboardingStage.set(if (listOf(OnboardingStage.Step1_SimpleXInfo, OnboardingStage.Step2_CreateProfile).contains(savedOnboardingStage) && chatModel.users.size == 1) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt index 06925e28a..6ca065086 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt @@ -59,7 +59,9 @@ abstract class NtfManager { awaitChatStartedIfNeeded(chatModel) if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) { // TODO include remote host ID in desktop notifications? - chatModel.controller.changeActiveUser(null, userId, null) + chatModel.controller.showProgressIfNeeded { + chatModel.controller.changeActiveUser(null, userId, null) + } } val cInfo = chatModel.getChat(chatId)?.chatInfo chatModel.clearOverlays.value = true @@ -72,7 +74,9 @@ abstract class NtfManager { awaitChatStartedIfNeeded(chatModel) if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) { // TODO include remote host ID in desktop notifications? - chatModel.controller.changeActiveUser(null, userId, null) + chatModel.controller.showProgressIfNeeded { + chatModel.controller.changeActiveUser(null, userId, null) + } } chatModel.chatId.value = null chatModel.clearOverlays.value = true diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt index fa86a6291..a63c4c528 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt @@ -21,8 +21,8 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.* import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.common.model.ChatModel -import chat.simplex.common.model.Profile +import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* @@ -76,7 +76,13 @@ fun CreateProfile(chatModel: ChatModel, close: () -> Unit) { disabled = !canCreateProfile(displayName.value), textColor = MaterialTheme.colors.primary, iconColor = MaterialTheme.colors.primary, - click = { createProfileInProfiles(chatModel, displayName.value, close) }, + click = { + if (chatModel.localUserCreated.value == true) { + createProfileInProfiles(chatModel, displayName.value, close) + } else { + createProfileInNoProfileSetup(displayName.value, close) + } + }, ) SectionTextFooter(generalGetString(MR.strings.your_profile_is_stored_on_your_device)) SectionTextFooter(generalGetString(MR.strings.profile_is_only_shared_with_your_contacts)) @@ -168,6 +174,17 @@ fun CreateFirstProfile(chatModel: ChatModel, close: () -> Unit) { } } +fun createProfileInNoProfileSetup(displayName: String, close: () -> Unit) { + withApi { + val user = controller.apiCreateActiveUser(null, Profile(displayName.trim(), "", null)) ?: return@withApi + controller.appPrefs.onboardingStage.set(OnboardingStage.Step3_CreateSimpleXAddress) + chatModel.chatRunning.value = false + controller.startChat(user) + controller.switchUIRemoteHost(null) + close() + } +} + fun createProfileInProfiles(chatModel: ChatModel, displayName: String, close: () -> Unit) { withApi { val rhId = chatModel.remoteHostId() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt index 301f09033..a91e5e7b3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt @@ -68,14 +68,14 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp var searchInList by rememberSaveable { mutableStateOf("") } val scope = rememberCoroutineScope() - val (userPickerState, scaffoldState, switchingUsersAndHosts ) = settingsState + val (userPickerState, scaffoldState ) = settingsState Scaffold(topBar = { Box(Modifier.padding(end = endPadding)) { ChatListToolbar(chatModel, scaffoldState.drawerState, userPickerState, stopped) { searchInList = it.trim() } } }, scaffoldState = scaffoldState, drawerContent = { SettingsView(chatModel, setPerformLA, scaffoldState.drawerState) }, drawerScrimColor = MaterialTheme.colors.onSurface.copy(alpha = if (isInDarkTheme()) 0.16f else 0.32f), drawerGesturesEnabled = appPlatform.isAndroid, floatingActionButton = { - if (searchInList.isEmpty()) { + if (searchInList.isEmpty() && !chatModel.desktopNoUserNoRemote) { FloatingActionButton( onClick = { if (!stopped) { @@ -104,7 +104,7 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf ) { if (chatModel.chats.isNotEmpty()) { ChatList(chatModel, search = searchInList) - } else if (!switchingUsersAndHosts.value) { + } else if (!chatModel.switchingUsersAndHosts.value && !chatModel.desktopNoUserNoRemote) { Box(Modifier.fillMaxSize()) { if (!stopped && !newChatSheetState.collectAsState().value.isVisible()) { OnboardingButtons(showNewChatSheet) @@ -121,19 +121,11 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf NewChatSheet(chatModel, newChatSheetState, stopped, hideNewChatSheet) } if (appPlatform.isAndroid) { - UserPicker(chatModel, userPickerState, switchingUsersAndHosts) { + UserPicker(chatModel, userPickerState) { scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() } userPickerState.value = AnimatedViewState.GONE } } - if (switchingUsersAndHosts.value) { - Box( - Modifier.fillMaxSize().clickable(enabled = false, onClick = {}), - contentAlignment = Alignment.Center - ) { - ProgressIndicator() - } - } } @Composable @@ -209,7 +201,7 @@ private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, user navigationButton = { if (showSearch) { NavigationButtonBack(hideSearchOnBack) - } else if (chatModel.users.isEmpty()) { + } else if (chatModel.users.isEmpty() && !chatModel.desktopNoUserNoRemote) { NavigationButtonMenu { scope.launch { if (drawerState.isOpen) drawerState.close() else drawerState.open() } } } else { val users by remember { derivedStateOf { chatModel.users.filter { u -> u.user.activeUser || !u.user.hidden } } } @@ -304,17 +296,6 @@ private fun ToggleFilterButton() { } } -@Composable -private fun ProgressIndicator() { - CircularProgressIndicator( - Modifier - .padding(horizontal = 2.dp) - .size(30.dp), - color = MaterialTheme.colors.secondary, - strokeWidth = 2.5.dp - ) -} - @Composable expect fun DesktopActiveCallOverlayLayout(newChatSheetState: MutableStateFlow) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt index ecd47c937..8338d2960 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt @@ -26,7 +26,7 @@ import kotlinx.coroutines.flow.MutableStateFlow @Composable fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stopped: Boolean) { var searchInList by rememberSaveable { mutableStateOf("") } - val (userPickerState, scaffoldState, switchingUsersAndHosts) = settingsState + val (userPickerState, scaffoldState) = settingsState val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp Scaffold( Modifier.padding(end = endPadding), @@ -47,7 +47,7 @@ fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stoppe } } if (appPlatform.isAndroid) { - UserPicker(chatModel, userPickerState, switchingUsersAndHosts, showSettings = false, showCancel = true, cancelClicked = { + UserPicker(chatModel, userPickerState, showSettings = false, showCancel = true, cancelClicked = { chatModel.sharedContent.value = null userPickerState.value = AnimatedViewState.GONE }) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt index 46a333f61..d87c05a91 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt @@ -26,7 +26,9 @@ import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.platform.* +import chat.simplex.common.views.CreateProfile import chat.simplex.common.views.remote.* +import chat.simplex.common.views.usersettings.doWithAuth import chat.simplex.res.MR import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.delay @@ -38,7 +40,6 @@ import kotlin.math.roundToInt fun UserPicker( chatModel: ChatModel, userPickerState: MutableStateFlow, - switchingUsersAndHosts: MutableState, showSettings: Boolean = true, showCancel: Boolean = false, cancelClicked: () -> Unit = {}, @@ -123,14 +124,10 @@ fun UserPicker( userPickerState.value = AnimatedViewState.HIDING if (!u.user.activeUser) { scope.launch { - val job = launch { - delay(500) - switchingUsersAndHosts.value = true + controller.showProgressIfNeeded { + ModalManager.closeAllModalsEverywhere() + chatModel.controller.changeActiveUser(u.user.remoteHostId, u.user.userId, null) } - ModalManager.closeAllModalsEverywhere() - chatModel.controller.changeActiveUser(u.user.remoteHostId, u.user.userId, null) - job.cancel() - switchingUsersAndHosts.value = false } } } @@ -162,13 +159,13 @@ fun UserPicker( val currentRemoteHost = remember { chatModel.currentRemoteHost }.value Column(Modifier.weight(1f).verticalScroll(rememberScrollState())) { if (remoteHosts.isNotEmpty()) { - if (currentRemoteHost == null) { + if (currentRemoteHost == null && chatModel.localUserCreated.value == true) { LocalDevicePickerItem(true) { userPickerState.value = AnimatedViewState.HIDING switchToLocalDevice() } Divider(Modifier.requiredHeight(1.dp)) - } else { + } else if (currentRemoteHost != null) { val connecting = rememberSaveable { mutableStateOf(false) } RemoteHostPickerItem(currentRemoteHost, actionButtonClick = { @@ -176,7 +173,7 @@ fun UserPicker( stopRemoteHostAndReloadHosts(currentRemoteHost, true) }) { userPickerState.value = AnimatedViewState.HIDING - switchToRemoteHost(currentRemoteHost, switchingUsersAndHosts, connecting) + switchToRemoteHost(currentRemoteHost, connecting) } Divider(Modifier.requiredHeight(1.dp)) } @@ -184,7 +181,7 @@ fun UserPicker( UsersView() - if (remoteHosts.isNotEmpty() && currentRemoteHost != null) { + if (remoteHosts.isNotEmpty() && currentRemoteHost != null && chatModel.localUserCreated.value == true) { LocalDevicePickerItem(false) { userPickerState.value = AnimatedViewState.HIDING switchToLocalDevice() @@ -199,7 +196,7 @@ fun UserPicker( stopRemoteHostAndReloadHosts(h, false) }) { userPickerState.value = AnimatedViewState.HIDING - switchToRemoteHost(h, switchingUsersAndHosts, connecting) + switchToRemoteHost(h, connecting) } Divider(Modifier.requiredHeight(1.dp)) } @@ -220,6 +217,18 @@ fun UserPicker( userPickerState.value = AnimatedViewState.GONE } Divider(Modifier.requiredHeight(1.dp)) + } else if (chatModel.desktopNoUserNoRemote) { + CreateInitialProfile { + doWithAuth(generalGetString(MR.strings.auth_open_chat_profiles), generalGetString(MR.strings.auth_log_in_using_credential)) { + ModalManager.center.showModalCloseable { close -> + LaunchedEffect(Unit) { + userPickerState.value = AnimatedViewState.HIDING + } + CreateProfile(chat.simplex.common.platform.chatModel, close) + } + } + } + Divider(Modifier.requiredHeight(1.dp)) } if (showSettings) { SettingsPickerItem(settingsClicked) @@ -401,6 +410,16 @@ private fun LinkAMobilePickerItem(onClick: () -> Unit) { } } +@Composable +private fun CreateInitialProfile(onClick: () -> Unit) { + SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) { + val text = generalGetString(MR.strings.create_chat_profile) + Icon(painterResource(MR.images.ic_manage_accounts), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground) + Spacer(Modifier.width(DEFAULT_PADDING + 6.dp)) + Text(text, color = if (isInDarkTheme()) MenuTextColorDark else Color.Black) + } +} + @Composable private fun SettingsPickerItem(onClick: () -> Unit) { SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) { @@ -441,21 +460,15 @@ private fun switchToLocalDevice() { } } -private fun switchToRemoteHost(h: RemoteHostInfo, switchingUsersAndHosts: MutableState, connecting: MutableState) { +private fun switchToRemoteHost(h: RemoteHostInfo, connecting: MutableState) { if (!h.activeHost()) { withBGApi { - val job = launch { - delay(500) - switchingUsersAndHosts.value = true - } ModalManager.closeAllModalsEverywhere() if (h.sessionState != null) { chatModel.controller.switchUIRemoteHost(h.remoteHostId) } else { connectMobileDevice(h, connecting) } - job.cancel() - switchingUsersAndHosts.value = false } } else { connectMobileDevice(h, connecting) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt index 16d7e88d6..ba0edb98d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.LocalDensity import dev.icerock.moko.resources.compose.painterResource import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.* import chat.simplex.common.platform.onRightClick @@ -202,13 +203,14 @@ fun SectionTextFooter(text: String) { } @Composable -fun SectionTextFooter(text: AnnotatedString) { +fun SectionTextFooter(text: AnnotatedString, textAlign: TextAlign = TextAlign.Start) { Text( text, Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF).fillMaxWidth(0.9F), color = MaterialTheme.colors.secondary, lineHeight = 18.sp, - fontSize = 14.sp + fontSize = 14.sp, + textAlign = textAlign ) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/CreateSimpleXAddress.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/CreateSimpleXAddress.kt index 49e62fb06..092fb8bf6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/CreateSimpleXAddress.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/CreateSimpleXAddress.kt @@ -182,6 +182,10 @@ private fun prepareChatBeforeAddressCreation(rhId: Long?) { val user = chatModel.controller.apiGetActiveUser(rhId) ?: return@withApi chatModel.currentUser.value = user if (chatModel.users.isEmpty()) { + if (appPlatform.isDesktop) { + // Make possible to use chat after going to remote device linking and returning back to local profile creation + chatModel.chatRunning.value = false + } chatModel.controller.startChat(user) } else { val users = chatModel.controller.listUsers(rhId) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/LinkAMobileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/LinkAMobileView.kt new file mode 100644 index 000000000..03b3d6dbd --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/LinkAMobileView.kt @@ -0,0 +1,91 @@ +package chat.simplex.common.views.onboarding + +import SectionTextFooter +import SectionView +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.dp +import chat.simplex.common.model.ChatModel +import chat.simplex.common.platform.chatModel +import chat.simplex.common.ui.theme.DEFAULT_PADDING +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.remote.AddingMobileDevice +import chat.simplex.common.views.remote.DeviceNameField +import chat.simplex.common.views.usersettings.PreferenceToggle +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource + +@Composable +fun LinkAMobile() { + val connecting = rememberSaveable { mutableStateOf(false) } + val deviceName = chatModel.controller.appPrefs.deviceNameForRemoteAccess + var deviceNameInQrCode by remember { mutableStateOf(chatModel.controller.appPrefs.deviceNameForRemoteAccess.get()) } + val staleQrCode = remember { mutableStateOf(false) } + + LinkAMobileLayout( + deviceName = remember { deviceName.state }, + connecting, + staleQrCode, + updateDeviceName = { + withBGApi { + if (it != "" && it != deviceName.get()) { + chatModel.controller.setLocalDeviceName(it) + deviceName.set(it) + staleQrCode.value = deviceName.get() != deviceNameInQrCode + } + } + } + ) + KeyChangeEffect(staleQrCode.value) { + if (!staleQrCode.value) { + deviceNameInQrCode = deviceName.get() + } + } +} + +@Composable +private fun LinkAMobileLayout( + deviceName: State, + connecting: MutableState, + staleQrCode: MutableState, + updateDeviceName: (String) -> Unit, +) { + Column(Modifier.padding(top = 20.dp)) { + AppBarTitle(stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles)) + Row(Modifier.weight(1f).padding(horizontal = DEFAULT_PADDING * 2), verticalAlignment = Alignment.CenterVertically) { + Column( + Modifier.weight(0.3f), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + SectionView(generalGetString(MR.strings.this_device_name).uppercase()) { + DeviceNameField(deviceName.value ?: "") { updateDeviceName(it) } + SectionTextFooter(generalGetString(MR.strings.this_device_name_shared_with_mobile)) + PreferenceToggle(stringResource(MR.strings.multicast_discoverable_via_local_network), remember { ChatModel.controller.appPrefs.offerRemoteMulticast.state }.value) { + ChatModel.controller.appPrefs.offerRemoteMulticast.set(it) + } + } + } + Box(Modifier.weight(0.7f)) { + AddingMobileDevice(false, staleQrCode, connecting) { + if (chatModel.remoteHosts.isEmpty()) { + chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) + } else { + chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.OnboardingComplete) + } + } + } + } + SimpleButtonDecorated( + text = stringResource(MR.strings.about_simplex), + icon = painterResource(MR.images.ic_arrow_back_ios_new), + textDecoration = TextDecoration.None, + fontWeight = FontWeight.Medium + ) { chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/OnboardingView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/OnboardingView.kt index 0a48bbd1e..d4c63248e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/OnboardingView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/OnboardingView.kt @@ -3,6 +3,7 @@ package chat.simplex.common.views.onboarding enum class OnboardingStage { Step1_SimpleXInfo, Step2_CreateProfile, + LinkAMobile, Step2_5_SetupDatabasePassphrase, Step3_CreateSimpleXAddress, Step4_SetNotificationsMode, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt index a4ebd23de..d9d012a50 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt @@ -43,7 +43,11 @@ fun SetupDatabasePassphrase(m: ChatModel) { val newKey = rememberSaveable { mutableStateOf("") } val confirmNewKey = rememberSaveable { mutableStateOf("") } fun nextStep() { - m.controller.appPrefs.onboardingStage.set(OnboardingStage.Step3_CreateSimpleXAddress) + if (appPlatform.isAndroid || chatModel.currentUser.value != null) { + m.controller.appPrefs.onboardingStage.set(OnboardingStage.Step3_CreateSimpleXAddress) + } else { + m.controller.appPrefs.onboardingStage.set(OnboardingStage.LinkAMobile) + } } SetupDatabasePassphraseLayout( currentKey, @@ -159,10 +163,7 @@ private fun SetupDatabasePassphraseLayout( } }, isValid = { confirmNewKey.value == "" || newKey.value == confirmNewKey.value }, - keyboardActions = KeyboardActions(onDone = { - if (!disabled) onClickUpdate() - defaultKeyboardAction(ImeAction.Done) - }), + keyboardActions = KeyboardActions(onDone = { defaultKeyboardAction(ImeAction.Done) }), ) Box(Modifier.align(Alignment.CenterHorizontally).padding(vertical = DEFAULT_PADDING)) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt index f20c4508b..2aad2556a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt @@ -8,6 +8,7 @@ import androidx.compose.material.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource @@ -99,26 +100,22 @@ private fun InfoRow(icon: Painter, titleId: StringResource, textId: StringResour } @Composable -fun OnboardingActionButton(user: User?, onboardingStage: SharedPreference, onclick: (() -> Unit)? = null) { - if (user == null) { - OnboardingActionButton(MR.strings.create_your_profile, onboarding = OnboardingStage.Step2_CreateProfile, true, onclick) - } else { - OnboardingActionButton(MR.strings.make_private_connection, onboarding = OnboardingStage.OnboardingComplete, true, onclick) - } -} +expect fun OnboardingActionButton(user: User?, onboardingStage: SharedPreference, onclick: (() -> Unit)? = null) @Composable fun OnboardingActionButton( labelId: StringResource, onboarding: OnboardingStage?, border: Boolean, + icon: Painter? = null, + iconColor: Color = MaterialTheme.colors.primary, onclick: (() -> Unit)? ) { val modifier = if (border) { Modifier .border(border = BorderStroke(1.dp, MaterialTheme.colors.primary), shape = RoundedCornerShape(50)) .padding( - horizontal = DEFAULT_PADDING * 2, + horizontal = if (icon == null) DEFAULT_PADDING * 2 else DEFAULT_PADDING_HALF, vertical = 4.dp ) } else { @@ -131,6 +128,9 @@ fun OnboardingActionButton( ChatController.appPrefs.onboardingStage.set(onboarding) } }, modifier) { + if (icon != null) { + Icon(icon, stringResource(labelId), Modifier.padding(end = DEFAULT_PADDING_HALF), tint = iconColor) + } Text(stringResource(labelId), style = MaterialTheme.typography.h2, color = MaterialTheme.colors.primary, fontSize = 20.sp) Icon( painterResource(MR.images.ic_arrow_forward_ios), "next stage", tint = MaterialTheme.colors.primary, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt index 53f0339be..b6ed14150 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt @@ -13,12 +13,14 @@ import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.* 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.platform.LocalClipboardManager import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.* import androidx.compose.ui.text.input.* +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.* @@ -97,9 +99,11 @@ fun ConnectMobileLayout( SectionDividerSpaced(maxBottomPadding = false) } SectionView(stringResource(MR.strings.devices).uppercase()) { - SettingsActionItemWithContent(text = stringResource(MR.strings.this_device), icon = painterResource(MR.images.ic_desktop), click = connectDesktop) { - if (connectedHost.value == null) { - Icon(painterResource(MR.images.ic_done_filled), null, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground) + if (chatModel.localUserCreated.value == true) { + SettingsActionItemWithContent(text = stringResource(MR.strings.this_device), icon = painterResource(MR.images.ic_desktop), click = connectDesktop) { + if (connectedHost.value == null) { + Icon(painterResource(MR.images.ic_done_filled), null, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground) + } } } @@ -162,26 +166,37 @@ fun DeviceNameField( @Composable private fun ConnectMobileViewLayout( - title: String, + title: String?, invitation: String?, deviceName: String?, sessionCode: String?, - port: String? + port: String?, + staleQrCode: Boolean = false, + refreshQrCode: () -> Unit = {} ) { Column( Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(8.dp) ) { - AppBarTitle(title) + if (title != null) { + AppBarTitle(title) + } SectionView { if (invitation != null && sessionCode == null && port != null) { - QRCode( - invitation, Modifier - .padding(start = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING_HALF) - .aspectRatio(1f) - ) - SectionTextFooter(annotatedStringResource(MR.strings.open_on_mobile_and_scan_qr_code)) - SectionTextFooter(annotatedStringResource(MR.strings.waiting_for_mobile_to_connect_on_port, port)) + Box { + QRCode( + invitation, Modifier + .padding(start = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING_HALF) + .aspectRatio(1f) + ) + if (staleQrCode) { + Box(Modifier.matchParentSize().background(MaterialTheme.colors.background.copy(alpha = 0.9f)), contentAlignment = Alignment.Center) { + SimpleButtonDecorated(stringResource(MR.strings.refresh_qr_code), painterResource(MR.images.ic_refresh), click = refreshQrCode) + } + } + } + SectionTextFooter(annotatedStringResource(MR.strings.open_on_mobile_and_scan_qr_code), textAlign = TextAlign.Center) + SectionTextFooter(annotatedStringResource(MR.strings.waiting_for_mobile_to_connect_on_port, port), textAlign = TextAlign.Center) if (remember { controller.appPrefs.developerTools.state }.value) { val clipboard = LocalClipboardManager.current @@ -237,55 +252,72 @@ fun connectMobileDevice(rh: RemoteHostInfo, connecting: MutableState) { private fun showAddingMobileDevice(connecting: MutableState) { ModalManager.start.showModalCloseable { close -> - val invitation = rememberSaveable { mutableStateOf(null) } - val port = rememberSaveable { mutableStateOf(null) } - val pairing = remember { chatModel.remoteHostPairing } - val sessionCode = when (val state = pairing.value?.second) { - is RemoteHostSessionState.PendingConfirmation -> state.sessionCode - else -> null + AddingMobileDevice(true, remember { mutableStateOf(false) }, connecting, close) + } +} + +@Composable +fun AddingMobileDevice(showTitle: Boolean, staleQrCode: MutableState, connecting: MutableState, close: () -> Unit) { + val invitation = rememberSaveable { mutableStateOf(null) } + val port = rememberSaveable { mutableStateOf(null) } + val startRemoteHost = suspend { + val r = chatModel.controller.startRemoteHost(null, controller.appPrefs.offerRemoteMulticast.get()) + if (r != null) { + connecting.value = true + invitation.value = r.second + port.value = r.third + chatModel.remoteHostPairing.value = null to RemoteHostSessionState.Starting } - /** It's needed to prevent screen flashes when [chatModel.newRemoteHostPairing] sets to null in background */ - var cachedSessionCode by remember { mutableStateOf(null) } - if (cachedSessionCode == null && sessionCode != null) { - cachedSessionCode = sessionCode - } - val remoteDeviceName = pairing.value?.first?.hostDeviceName - ConnectMobileViewLayout( - title = if (cachedSessionCode == null) stringResource(MR.strings.link_a_mobile) else stringResource(MR.strings.verify_connection), - invitation = invitation.value, - deviceName = remoteDeviceName, - sessionCode = cachedSessionCode, - port = port.value - ) - val oldRemoteHostId by remember { mutableStateOf(chatModel.currentRemoteHost.value?.remoteHostId) } - LaunchedEffect(remember { chatModel.currentRemoteHost }.value) { - if (chatModel.currentRemoteHost.value?.remoteHostId != null && chatModel.currentRemoteHost.value?.remoteHostId != oldRemoteHostId) { - close() - } - } - KeyChangeEffect(pairing.value) { - if (pairing.value == null) { - close() - } - } - DisposableEffect(Unit) { + } + val pairing = remember { chatModel.remoteHostPairing } + val sessionCode = when (val state = pairing.value?.second) { + is RemoteHostSessionState.PendingConfirmation -> state.sessionCode + else -> null + } + /** It's needed to prevent screen flashes when [chatModel.newRemoteHostPairing] sets to null in background */ + var cachedSessionCode by remember { mutableStateOf(null) } + if (cachedSessionCode == null && sessionCode != null) { + cachedSessionCode = sessionCode + } + val remoteDeviceName = pairing.value?.first?.hostDeviceName + ConnectMobileViewLayout( + title = if (!showTitle) null else if (cachedSessionCode == null) stringResource(MR.strings.link_a_mobile) else stringResource(MR.strings.verify_connection), + invitation = invitation.value, + deviceName = remoteDeviceName, + sessionCode = cachedSessionCode, + port = port.value, + staleQrCode = staleQrCode.value, + refreshQrCode = { withBGApi { - val r = chatModel.controller.startRemoteHost(null, controller.appPrefs.offerRemoteMulticast.get()) - if (r != null) { - connecting.value = true - invitation.value = r.second - port.value = r.third - chatModel.remoteHostPairing.value = null to RemoteHostSessionState.Starting + if (chatController.stopRemoteHost(null)) { + startRemoteHost() + staleQrCode.value = false } } - onDispose { - if (chatModel.currentRemoteHost.value?.remoteHostId == oldRemoteHostId) { - withBGApi { - chatController.stopRemoteHost(null) - } + }, + ) + val oldRemoteHostId by remember { mutableStateOf(chatModel.currentRemoteHost.value?.remoteHostId) } + LaunchedEffect(remember { chatModel.currentRemoteHost }.value) { + if (chatModel.currentRemoteHost.value?.remoteHostId != null && chatModel.currentRemoteHost.value?.remoteHostId != oldRemoteHostId) { + close() + } + } + KeyChangeEffect(pairing.value) { + if (pairing.value == null) { + close() + } + } + DisposableEffect(Unit) { + withBGApi { + startRemoteHost() + } + onDispose { + if (chatModel.currentRemoteHost.value?.remoteHostId == oldRemoteHostId) { + withBGApi { + chatController.stopRemoteHost(null) } - chatModel.remoteHostPairing.value = null } + chatModel.remoteHostPairing.value = null } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt index ad30b57c6..a96de71b9 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.* +import chat.simplex.common.platform.chatModel import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.item.ClickableText import chat.simplex.common.views.helpers.* @@ -169,18 +170,20 @@ fun NetworkAndServersView( verticalArrangement = Arrangement.spacedBy(8.dp) ) { AppBarTitle(stringResource(MR.strings.network_and_servers)) - SectionView(generalGetString(MR.strings.settings_section_title_messages)) { - SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.smp_servers), showCustomModal { m, close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.SMP, close) }) + if (!chatModel.desktopNoUserNoRemote) { + SectionView(generalGetString(MR.strings.settings_section_title_messages)) { + SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.smp_servers), showCustomModal { m, close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.SMP, close) }) - SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.xftp_servers), showCustomModal { m, close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.XFTP, close) }) + SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.xftp_servers), showCustomModal { m, close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.XFTP, close) }) - if (currentRemoteHost == null) { - UseSocksProxySwitch(networkUseSocksProxy, proxyPort, toggleSocksProxy, showSettingsModal) - UseOnionHosts(onionHosts, networkUseSocksProxy, showSettingsModal, useOnion) - if (developerTools) { - SessionModePicker(sessionMode, showSettingsModal, updateSessionMode) + if (currentRemoteHost == null) { + UseSocksProxySwitch(networkUseSocksProxy, proxyPort, toggleSocksProxy, showSettingsModal) + UseOnionHosts(onionHosts, networkUseSocksProxy, showSettingsModal, useOnion) + if (developerTools) { + SessionModePicker(sessionMode, showSettingsModal, updateSessionMode) + } + SettingsActionItem(painterResource(MR.images.ic_cable), stringResource(MR.strings.network_settings), showSettingsModal { AdvancedNetworkSettingsView(it) }) } - SettingsActionItem(painterResource(MR.images.ic_cable), stringResource(MR.strings.network_settings), showSettingsModal { AdvancedNetworkSettingsView(it) }) } } if (currentRemoteHost == null && networkUseSocksProxy.value) { @@ -192,7 +195,7 @@ fun NetworkAndServersView( } } Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 32.dp, end = DEFAULT_PADDING_HALF, bottom = 30.dp)) - } else { + } else if (!chatModel.desktopNoUserNoRemote) { Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 24.dp, end = DEFAULT_PADDING_HALF, bottom = 30.dp)) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt index 1a5aa49eb..3d2b7b7fa 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt @@ -92,7 +92,6 @@ fun PrivacySettingsView( chatModel.simplexLinkMode.value = it }) } - SectionDividerSpaced() val currentUser = chatModel.currentUser.value if (currentUser != null) { @@ -142,39 +141,42 @@ fun PrivacySettingsView( } } - DeliveryReceiptsSection( - currentUser = currentUser, - setOrAskSendReceiptsContacts = { enable -> - val contactReceiptsOverrides = chatModel.chats.fold(0) { count, chat -> - if (chat.chatInfo is ChatInfo.Direct) { - val sendRcpts = chat.chatInfo.contact.chatSettings.sendRcpts - count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1) + if (!chatModel.desktopNoUserNoRemote) { + SectionDividerSpaced() + DeliveryReceiptsSection( + currentUser = currentUser, + setOrAskSendReceiptsContacts = { enable -> + val contactReceiptsOverrides = chatModel.chats.fold(0) { count, chat -> + if (chat.chatInfo is ChatInfo.Direct) { + val sendRcpts = chat.chatInfo.contact.chatSettings.sendRcpts + count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1) + } else { + count + } + } + if (contactReceiptsOverrides == 0) { + setSendReceiptsContacts(enable, clearOverrides = false) } else { - count + showUserContactsReceiptsAlert(enable, contactReceiptsOverrides, ::setSendReceiptsContacts) + } + }, + setOrAskSendReceiptsGroups = { enable -> + val groupReceiptsOverrides = chatModel.chats.fold(0) { count, chat -> + if (chat.chatInfo is ChatInfo.Group) { + val sendRcpts = chat.chatInfo.groupInfo.chatSettings.sendRcpts + count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1) + } else { + count + } + } + if (groupReceiptsOverrides == 0) { + setSendReceiptsGroups(enable, clearOverrides = false) + } else { + showUserGroupsReceiptsAlert(enable, groupReceiptsOverrides, ::setSendReceiptsGroups) } } - if (contactReceiptsOverrides == 0) { - setSendReceiptsContacts(enable, clearOverrides = false) - } else { - showUserContactsReceiptsAlert(enable, contactReceiptsOverrides, ::setSendReceiptsContacts) - } - }, - setOrAskSendReceiptsGroups = { enable -> - val groupReceiptsOverrides = chatModel.chats.fold(0) { count, chat -> - if (chat.chatInfo is ChatInfo.Group) { - val sendRcpts = chat.chatInfo.groupInfo.chatSettings.sendRcpts - count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1) - } else { - count - } - } - if (groupReceiptsOverrides == 0) { - setSendReceiptsGroups(enable, clearOverrides = false) - } else { - showUserGroupsReceiptsAlert(enable, groupReceiptsOverrides, ::setSendReceiptsGroups) - } - } - ) + ) + } } SectionBottomSpacer() } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt index dfe358152..c73cb0142 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.unit.* import chat.simplex.common.model.* import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.CreateProfile import chat.simplex.common.views.database.DatabaseView import chat.simplex.common.views.helpers.* import chat.simplex.common.views.onboarding.SimpleXInfo @@ -38,76 +39,39 @@ import kotlinx.coroutines.launch fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, drawerState: DrawerState) { val user = chatModel.currentUser.value val stopped = chatModel.chatRunning.value == false - - if (user != null) { - val requireAuth = remember { chatModel.controller.appPrefs.performLA.state } - SettingsLayout( - profile = user.profile, - stopped, - chatModel.chatDbEncrypted.value == true, - remember { chatModel.controller.appPrefs.storeDBPassphrase.state }.value, - remember { chatModel.controller.appPrefs.notificationsMode.state }, - user.displayName, - setPerformLA = setPerformLA, - showModal = { modalView -> { ModalManager.start.showModal { modalView(chatModel) } } }, - showSettingsModal = { modalView -> { ModalManager.start.showModal(true) { modalView(chatModel) } } }, - showSettingsModalWithSearch = { modalView -> - ModalManager.start.showCustomModal { close -> - val search = rememberSaveable { mutableStateOf("") } - ModalView( - { close() }, - endButtons = { - SearchTextField(Modifier.fillMaxWidth(), placeholder = stringResource(MR.strings.search_verb), alwaysVisible = true) { search.value = it } - }, - content = { modalView(chatModel, search) }) + SettingsLayout( + profile = user?.profile, + stopped, + chatModel.chatDbEncrypted.value == true, + remember { chatModel.controller.appPrefs.storeDBPassphrase.state }.value, + remember { chatModel.controller.appPrefs.notificationsMode.state }, + user?.displayName, + setPerformLA = setPerformLA, + showModal = { modalView -> { ModalManager.start.showModal { modalView(chatModel) } } }, + showSettingsModal = { modalView -> { ModalManager.start.showModal(true) { modalView(chatModel) } } }, + showSettingsModalWithSearch = { modalView -> + ModalManager.start.showCustomModal { close -> + val search = rememberSaveable { mutableStateOf("") } + ModalView( + { close() }, + endButtons = { + SearchTextField(Modifier.fillMaxWidth(), placeholder = stringResource(MR.strings.search_verb), alwaysVisible = true) { search.value = it } + }, + content = { modalView(chatModel, search) }) + } + }, + showCustomModal = { modalView -> { ModalManager.start.showCustomModal { close -> modalView(chatModel, close) } } }, + showVersion = { + withApi { + val info = chatModel.controller.apiGetVersion() + if (info != null) { + ModalManager.start.showModal { VersionInfoView(info) } } - }, - showCustomModal = { modalView -> { ModalManager.start.showCustomModal { close -> modalView(chatModel, close) } } }, - showVersion = { - withApi { - val info = chatModel.controller.apiGetVersion() - if (info != null) { - ModalManager.start.showModal { VersionInfoView(info) } - } - } - }, - withAuth = { title, desc, block -> - if (!requireAuth.value) { - block() - } else { - var autoShow = true - ModalManager.fullscreen.showModalCloseable { close -> - val onFinishAuth = { success: Boolean -> - if (success) { - close() - block() - } - } - - LaunchedEffect(Unit) { - if (autoShow) { - autoShow = false - runAuth(title, desc, onFinishAuth) - } - } - Box( - Modifier.fillMaxSize().background(MaterialTheme.colors.background), - contentAlignment = Alignment.Center - ) { - SimpleButton( - stringResource(MR.strings.auth_unlock), - icon = painterResource(MR.images.ic_lock), - click = { - runAuth(title, desc, onFinishAuth) - } - ) - } - } - } - }, - drawerState = drawerState, - ) - } + } + }, + withAuth = ::doWithAuth, + drawerState = drawerState, + ) } val simplexTeamUri = @@ -115,12 +79,12 @@ val simplexTeamUri = @Composable fun SettingsLayout( - profile: LocalProfile, + profile: LocalProfile?, stopped: Boolean, encrypted: Boolean, passphraseSaved: Boolean, notificationsMode: State, - userDisplayName: String, + userDisplayName: String?, setPerformLA: (Boolean) -> Unit, showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), @@ -150,13 +114,22 @@ fun SettingsLayout( AppBarTitle(stringResource(MR.strings.your_settings)) SectionView(stringResource(MR.strings.settings_section_title_you)) { - SectionItemView(showCustomModal { chatModel, close -> UserProfileView(chatModel, close) }, 80.dp, padding = PaddingValues(start = 16.dp, end = DEFAULT_PADDING), disabled = stopped) { - ProfilePreview(profile, stopped = stopped) - } val profileHidden = rememberSaveable { mutableStateOf(false) } - SettingsActionItem(painterResource(MR.images.ic_manage_accounts), stringResource(MR.strings.your_chat_profiles), { withAuth(generalGetString(MR.strings.auth_open_chat_profiles), generalGetString(MR.strings.auth_log_in_using_credential)) { showSettingsModalWithSearch { it, search -> UserProfilesView(it, search, profileHidden) } } }, disabled = stopped, extraPadding = true) - SettingsActionItem(painterResource(MR.images.ic_qr_code), stringResource(MR.strings.your_simplex_contact_address), showCustomModal { it, close -> UserAddressView(it, shareViaProfile = it.currentUser.value!!.addressShared, close = close) }, disabled = stopped, extraPadding = true) - ChatPreferencesItem(showCustomModal, stopped = stopped) + if (profile != null) { + SectionItemView(showCustomModal { chatModel, close -> UserProfileView(chatModel, close) }, 80.dp, padding = PaddingValues(start = 16.dp, end = DEFAULT_PADDING), disabled = stopped) { + ProfilePreview(profile, stopped = stopped) + } + SettingsActionItem(painterResource(MR.images.ic_manage_accounts), stringResource(MR.strings.your_chat_profiles), { withAuth(generalGetString(MR.strings.auth_open_chat_profiles), generalGetString(MR.strings.auth_log_in_using_credential)) { showSettingsModalWithSearch { it, search -> UserProfilesView(it, search, profileHidden) } } }, disabled = stopped, extraPadding = true) + SettingsActionItem(painterResource(MR.images.ic_qr_code), stringResource(MR.strings.your_simplex_contact_address), showCustomModal { it, close -> UserAddressView(it, shareViaProfile = it.currentUser.value!!.addressShared, close = close) }, disabled = stopped, extraPadding = true) + ChatPreferencesItem(showCustomModal, stopped = stopped) + } else if (chatModel.localUserCreated.value == false) { + SettingsActionItem(painterResource(MR.images.ic_manage_accounts), stringResource(MR.strings.create_chat_profile), { withAuth(generalGetString(MR.strings.auth_open_chat_profiles), generalGetString(MR.strings.auth_log_in_using_credential)) { ModalManager.center.showModalCloseable { close -> + LaunchedEffect(Unit) { + closeSettings() + } + CreateProfile(chatModel, close) + } } }, disabled = stopped, extraPadding = true) + } if (appPlatform.isDesktop) { SettingsActionItem(painterResource(MR.images.ic_smartphone), stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles), showModal { ConnectMobileView() }, disabled = stopped, extraPadding = true) } else { @@ -171,15 +144,19 @@ fun SettingsLayout( SettingsActionItem(painterResource(MR.images.ic_videocam), stringResource(MR.strings.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped, extraPadding = true) SettingsActionItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.privacy_and_security), showSettingsModal { PrivacySettingsView(it, showSettingsModal, setPerformLA) }, disabled = stopped, extraPadding = true) SettingsActionItem(painterResource(MR.images.ic_light_mode), stringResource(MR.strings.appearance_settings), showSettingsModal { AppearanceView(it, showSettingsModal) }, extraPadding = true) - DatabaseItem(encrypted, passphraseSaved, showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped) + if (!chatModel.desktopNoUserNoRemote) { + DatabaseItem(encrypted, passphraseSaved, showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped) + } } SectionDividerSpaced() SectionView(stringResource(MR.strings.settings_section_title_help)) { - SettingsActionItem(painterResource(MR.images.ic_help), stringResource(MR.strings.how_to_use_simplex_chat), showModal { HelpView(userDisplayName) }, disabled = stopped, extraPadding = true) + SettingsActionItem(painterResource(MR.images.ic_help), stringResource(MR.strings.how_to_use_simplex_chat), showModal { HelpView(userDisplayName ?: "") }, disabled = stopped, extraPadding = true) SettingsActionItem(painterResource(MR.images.ic_add), stringResource(MR.strings.whats_new), showCustomModal { _, close -> WhatsNewView(viaSettings = true, close) }, disabled = stopped, extraPadding = true) SettingsActionItem(painterResource(MR.images.ic_info), stringResource(MR.strings.about_simplex_chat), showModal { SimpleXInfo(it, onboarding = false) }, extraPadding = true) - SettingsActionItem(painterResource(MR.images.ic_tag), stringResource(MR.strings.chat_with_the_founder), { uriHandler.openVerifiedSimplexUri(simplexTeamUri) }, textColor = MaterialTheme.colors.primary, disabled = stopped, extraPadding = true) + if (!chatModel.desktopNoUserNoRemote) { + SettingsActionItem(painterResource(MR.images.ic_tag), stringResource(MR.strings.chat_with_the_founder), { uriHandler.openVerifiedSimplexUri(simplexTeamUri) }, textColor = MaterialTheme.colors.primary, disabled = stopped, extraPadding = true) + } SettingsActionItem(painterResource(MR.images.ic_mail), stringResource(MR.strings.send_us_an_email), { uriHandler.openUriCatching("mailto:chat@simplex.chat") }, textColor = MaterialTheme.colors.primary, extraPadding = true) } SectionDividerSpaced() @@ -469,6 +446,42 @@ fun PreferenceToggleWithIcon( } } +fun doWithAuth(title: String, desc: String, block: () -> Unit) { + val requireAuth = chatModel.controller.appPrefs.performLA.get() + if (!requireAuth) { + block() + } else { + var autoShow = true + ModalManager.fullscreen.showModalCloseable { close -> + val onFinishAuth = { success: Boolean -> + if (success) { + close() + block() + } + } + + LaunchedEffect(Unit) { + if (autoShow) { + autoShow = false + runAuth(title, desc, onFinishAuth) + } + } + Box( + Modifier.fillMaxSize().background(MaterialTheme.colors.background), + contentAlignment = Alignment.Center + ) { + SimpleButton( + stringResource(MR.strings.auth_unlock), + icon = painterResource(MR.images.ic_lock), + click = { + runAuth(title, desc, onFinishAuth) + } + ) + } + } + } +} + private fun runAuth(title: String, desc: String, onFinish: (success: Boolean) -> Unit) { authenticate( title, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt index 401c3afed..390f1cac9 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.item.ItemAction @@ -56,7 +57,9 @@ fun UserProfilesView(m: ChatModel, search: MutableState, profileHidden: ModalManager.end.closeModals() } withBGApi { - m.controller.changeActiveUser(user.remoteHostId, user.userId, userViewPassword(user, searchTextOrPassword.value.trim())) + controller.showProgressIfNeeded { + m.controller.changeActiveUser(user.remoteHostId, user.userId, userViewPassword(user, searchTextOrPassword.value.trim())) + } } }, removeUser = { user -> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 22f5d2fbe..82d571d4a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -565,6 +565,7 @@ Your settings Your SimpleX address Your chat profiles + Create chat profile Database passphrase & export About SimpleX Chat How to use it @@ -1689,6 +1690,8 @@ Paste desktop address Desktop Not compatible! + Refresh + No connected mobile Coming soon! diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_refresh.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_refresh.svg new file mode 100644 index 000000000..a1f27d579 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_refresh.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.desktop.kt new file mode 100644 index 000000000..83c438e4a --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.desktop.kt @@ -0,0 +1,23 @@ +package chat.simplex.common.views.onboarding + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.runtime.Composable +import chat.simplex.common.model.ChatModel.controller +import chat.simplex.common.model.SharedPreference +import chat.simplex.common.model.User +import chat.simplex.common.ui.theme.DEFAULT_PADDING +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource + +@Composable +actual fun OnboardingActionButton(user: User?, onboardingStage: SharedPreference, onclick: (() -> Unit)?) { + if (user == null) { + Row(horizontalArrangement = Arrangement.spacedBy(DEFAULT_PADDING * 2.5f)) { + OnboardingActionButton(MR.strings.link_a_mobile, onboarding = if (controller.appPrefs.initialRandomDBPassphrase.get()) OnboardingStage.Step2_5_SetupDatabasePassphrase else OnboardingStage.LinkAMobile, true, icon = painterResource(MR.images.ic_smartphone_300), onclick = onclick) + OnboardingActionButton(MR.strings.create_your_profile, onboarding = OnboardingStage.Step2_CreateProfile, true, icon = painterResource(MR.images.ic_desktop), onclick = onclick) + } + } else { + OnboardingActionButton(MR.strings.make_private_connection, onboarding = OnboardingStage.OnboardingComplete, true, onclick = onclick) + } +}