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>
This commit is contained in:
Stanislav Dmitrenko 2023-12-01 03:38:21 +08:00 committed by GitHub
parent a4b44254bc
commit 0e18b13bea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 565 additions and 266 deletions

View File

@ -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<OnboardingStage>, 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)
}
}

View File

@ -37,8 +37,7 @@ import kotlinx.coroutines.flow.*
data class SettingsViewState( data class SettingsViewState(
val userPickerState: MutableStateFlow<AnimatedViewState>, val userPickerState: MutableStateFlow<AnimatedViewState>,
val scaffoldState: ScaffoldState, val scaffoldState: ScaffoldState
val switchingUsersAndHosts: MutableState<Boolean>
) )
@Composable @Composable
@ -102,11 +101,8 @@ fun MainScreen() {
} }
Box { Box {
var onboarding by remember { mutableStateOf(chatModel.controller.appPrefs.onboardingStage.get()) } val onboarding by remember { chatModel.controller.appPrefs.onboardingStage.state }
LaunchedEffect(Unit) { val localUserCreated = chatModel.localUserCreated.value
snapshotFlow { chatModel.controller.appPrefs.onboardingStage.state.value }.distinctUntilChanged().collect { onboarding = it }
}
val userCreated = chatModel.userCreated.value
var showInitializationView by remember { mutableStateOf(false) } var showInitializationView by remember { mutableStateOf(false) }
when { when {
chatModel.chatDbStatus.value == null && showInitializationView -> InitializationView() chatModel.chatDbStatus.value == null && showInitializationView -> InitializationView()
@ -115,14 +111,18 @@ fun MainScreen() {
DatabaseErrorView(chatModel.chatDbStatus, chatModel.controller.appPrefs) DatabaseErrorView(chatModel.chatDbStatus, chatModel.controller.appPrefs)
} }
} }
remember { chatModel.chatDbEncrypted }.value == null || userCreated == null -> SplashView() remember { chatModel.chatDbEncrypted }.value == null || localUserCreated == null -> SplashView()
onboarding == OnboardingStage.OnboardingComplete && userCreated -> { onboarding == OnboardingStage.OnboardingComplete -> {
Box { Box {
showAdvertiseLAAlert = true 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 scaffoldState = rememberScaffoldState()
val switchingUsersAndHosts = rememberSaveable { mutableStateOf(false) } val settingsState = remember { SettingsViewState(userPickerState, scaffoldState) }
val settingsState = remember { SettingsViewState(userPickerState, scaffoldState, switchingUsersAndHosts) }
if (appPlatform.isAndroid) { if (appPlatform.isAndroid) {
AndroidScreen(settingsState) AndroidScreen(settingsState)
} else { } else {
@ -137,12 +137,14 @@ fun MainScreen() {
} }
} }
onboarding == OnboardingStage.Step2_CreateProfile -> CreateFirstProfile(chatModel) {} onboarding == OnboardingStage.Step2_CreateProfile -> CreateFirstProfile(chatModel) {}
onboarding == OnboardingStage.LinkAMobile -> LinkAMobile()
onboarding == OnboardingStage.Step2_5_SetupDatabasePassphrase -> SetupDatabasePassphrase(chatModel) onboarding == OnboardingStage.Step2_5_SetupDatabasePassphrase -> SetupDatabasePassphrase(chatModel)
onboarding == OnboardingStage.Step3_CreateSimpleXAddress -> CreateSimpleXAddress(chatModel, null) onboarding == OnboardingStage.Step3_CreateSimpleXAddress -> CreateSimpleXAddress(chatModel, null)
onboarding == OnboardingStage.Step4_SetNotificationsMode -> SetNotificationsMode(chatModel) onboarding == OnboardingStage.Step4_SetNotificationsMode -> SetNotificationsMode(chatModel)
} }
if (appPlatform.isAndroid) { if (appPlatform.isAndroid) {
ModalManager.fullscreen.showInView() ModalManager.fullscreen.showInView()
SwitchingUsersView()
} }
val unauthorized = remember { derivedStateOf { AppLock.userAuthorized.value != true } } val unauthorized = remember { derivedStateOf { AppLock.userAuthorized.value != true } }
@ -262,7 +264,7 @@ fun CenterPartOfScreen() {
.background(MaterialTheme.colors.background), .background(MaterialTheme.colors.background),
contentAlignment = Alignment.Center 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 { } else {
ModalManager.center.showInView() ModalManager.center.showInView()
@ -286,6 +288,7 @@ fun DesktopScreen(settingsState: SettingsViewState) {
} }
Box(Modifier.widthIn(max = DEFAULT_START_MODAL_WIDTH)) { Box(Modifier.widthIn(max = DEFAULT_START_MODAL_WIDTH)) {
ModalManager.start.showInView() ModalManager.start.showInView()
SwitchingUsersView()
} }
Row(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH).clipToBounds()) { Row(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH).clipToBounds()) {
Box(Modifier.widthIn(min = DEFAULT_MIN_CENTER_MODAL_WIDTH).weight(1f)) { Box(Modifier.widthIn(min = DEFAULT_MIN_CENTER_MODAL_WIDTH).weight(1f)) {
@ -298,7 +301,7 @@ fun DesktopScreen(settingsState: SettingsViewState) {
EndPartOfScreen() EndPartOfScreen()
} }
} }
val (userPickerState, scaffoldState, switchingUsersAndHosts ) = settingsState val (userPickerState, scaffoldState ) = settingsState
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
if (scaffoldState.drawerState.isOpen) { if (scaffoldState.drawerState.isOpen) {
Box( Box(
@ -312,7 +315,7 @@ fun DesktopScreen(settingsState: SettingsViewState) {
) )
} }
VerticalDivider(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH)) 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() } scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() }
userPickerState.value = AnimatedViewState.GONE 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
)
}

View File

@ -2,7 +2,7 @@ package chat.simplex.common.model
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.runtime.* 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.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
@ -43,7 +43,7 @@ object ChatModel {
val setDeliveryReceipts = mutableStateOf(false) val setDeliveryReceipts = mutableStateOf(false)
val currentUser = mutableStateOf<User?>(null) val currentUser = mutableStateOf<User?>(null)
val users = mutableStateListOf<UserInfo>() val users = mutableStateListOf<UserInfo>()
val userCreated = mutableStateOf<Boolean?>(null) val localUserCreated = mutableStateOf<Boolean?>(null)
val chatRunning = mutableStateOf<Boolean?>(null) val chatRunning = mutableStateOf<Boolean?>(null)
val chatDbChanged = mutableStateOf<Boolean>(false) val chatDbChanged = mutableStateOf<Boolean>(false)
val chatDbEncrypted = mutableStateOf<Boolean?>(false) val chatDbEncrypted = mutableStateOf<Boolean?>(false)
@ -51,6 +51,7 @@ object ChatModel {
val chats = mutableStateListOf<Chat>() val chats = mutableStateListOf<Chat>()
// map of connections network statuses, key is agent connection id // map of connections network statuses, key is agent connection id
val networkStatuses = mutableStateMapOf<String, NetworkStatus>() val networkStatuses = mutableStateMapOf<String, NetworkStatus>()
val switchingUsersAndHosts = mutableStateOf(false)
// current chat // current chat
val chatId = mutableStateOf<String?>(null) val chatId = mutableStateOf<String?>(null)
@ -108,6 +109,9 @@ object ChatModel {
var updatingChatsMutex: Mutex = Mutex() 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 // remote controller
val remoteHosts = mutableStateListOf<RemoteHostInfo>() val remoteHosts = mutableStateListOf<RemoteHostInfo>()
val currentRemoteHost = mutableStateOf<RemoteHostInfo?>(null) val currentRemoteHost = mutableStateOf<RemoteHostInfo?>(null)
@ -620,6 +624,7 @@ object ChatModel {
terminalItems.add(item) 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 fun connectedToRemote(): Boolean = currentRemoteHost.value != null || remoteCtrlSession.value?.active == true
} }

View File

@ -362,7 +362,7 @@ object ChatController {
chatModel.users.addAll(users) chatModel.users.addAll(users)
if (justStarted) { if (justStarted) {
chatModel.currentUser.value = user chatModel.currentUser.value = user
chatModel.userCreated.value = true chatModel.localUserCreated.value = true
getUserChatData(null) getUserChatData(null)
appPrefs.chatLastStart.set(Clock.System.now()) appPrefs.chatLastStart.set(Clock.System.now())
chatModel.chatRunning.value = true 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?) { suspend fun changeActiveUser(rhId: Long?, toUserId: Long, viewPwd: String?) {
try { try {
changeActiveUser_(rhId, toUserId, viewPwd) changeActiveUser_(rhId, toUserId, viewPwd)
@ -475,7 +500,9 @@ object ChatController {
val r = sendCmd(rh, CC.ShowActiveUser()) val r = sendCmd(rh, CC.ShowActiveUser())
if (r is CR.ActiveUser) return r.user.updateRemoteHostId(rh) if (r is CR.ActiveUser) return r.user.updateRemoteHostId(rh)
Log.d(TAG, "apiGetActiveUser: ${r.responseType} ${r.details}") Log.d(TAG, "apiGetActiveUser: ${r.responseType} ${r.details}")
chatModel.userCreated.value = false if (rh == null) {
chatModel.localUserCreated.value = false
}
return null return null
} }
@ -1990,7 +2017,7 @@ object ChatController {
chatModel.setContactNetworkStatus(contact, NetworkStatus.Error(err)) 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? // TODO lock the switch so that two switches can't run concurrently?
chatModel.chatId.value = null chatModel.chatId.value = null
ModalManager.center.closeModals() ModalManager.center.closeModals()
@ -2003,7 +2030,10 @@ object ChatController {
chatModel.users.clear() chatModel.users.clear()
chatModel.users.addAll(users) chatModel.users.addAll(users)
chatModel.currentUser.value = user chatModel.currentUser.value = user
chatModel.userCreated.value = true if (user == null) {
chatModel.chatItems.clear()
chatModel.chats.clear()
}
val statuses = apiGetNetworkStatuses(rhId) val statuses = apiGetNetworkStatuses(rhId)
if (statuses != null) { if (statuses != null) {
chatModel.networkStatuses.clear() chatModel.networkStatuses.clear()
@ -2013,6 +2043,23 @@ object ChatController {
getUserChatData(rhId) 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 { fun getXFTPCfg(): XFTPFileConfig {
return XFTPFileConfig(minFileSize = 0) return XFTPFileConfig(minFileSize = 0)
} }

View File

@ -55,10 +55,22 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
if (appPreferences.encryptionStartedAt.get() != null) appPreferences.encryptionStartedAt.set(null) if (appPreferences.encryptionStartedAt.get() != null) appPreferences.encryptionStartedAt.set(null)
val user = chatController.apiGetActiveUser(null) val user = chatController.apiGetActiveUser(null)
if (user == null) { if (user == null) {
chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true) chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true)
chatModel.currentUser.value = null chatModel.currentUser.value = null
chatModel.users.clear() 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 { } else {
val savedOnboardingStage = appPreferences.onboardingStage.get() val savedOnboardingStage = appPreferences.onboardingStage.get()
appPreferences.onboardingStage.set(if (listOf(OnboardingStage.Step1_SimpleXInfo, OnboardingStage.Step2_CreateProfile).contains(savedOnboardingStage) && chatModel.users.size == 1) { appPreferences.onboardingStage.set(if (listOf(OnboardingStage.Step1_SimpleXInfo, OnboardingStage.Step2_CreateProfile).contains(savedOnboardingStage) && chatModel.users.size == 1) {

View File

@ -59,7 +59,9 @@ abstract class NtfManager {
awaitChatStartedIfNeeded(chatModel) awaitChatStartedIfNeeded(chatModel)
if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) { if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) {
// TODO include remote host ID in desktop notifications? // 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 val cInfo = chatModel.getChat(chatId)?.chatInfo
chatModel.clearOverlays.value = true chatModel.clearOverlays.value = true
@ -72,7 +74,9 @@ abstract class NtfManager {
awaitChatStartedIfNeeded(chatModel) awaitChatStartedIfNeeded(chatModel)
if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) { if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) {
// TODO include remote host ID in desktop notifications? // 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.chatId.value = null
chatModel.clearOverlays.value = true chatModel.clearOverlays.value = true

View File

@ -21,8 +21,8 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.* import androidx.compose.ui.text.style.*
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import chat.simplex.common.model.ChatModel import chat.simplex.common.model.*
import chat.simplex.common.model.Profile import chat.simplex.common.model.ChatModel.controller
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
@ -76,7 +76,13 @@ fun CreateProfile(chatModel: ChatModel, close: () -> Unit) {
disabled = !canCreateProfile(displayName.value), disabled = !canCreateProfile(displayName.value),
textColor = MaterialTheme.colors.primary, textColor = MaterialTheme.colors.primary,
iconColor = 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.your_profile_is_stored_on_your_device))
SectionTextFooter(generalGetString(MR.strings.profile_is_only_shared_with_your_contacts)) 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) { fun createProfileInProfiles(chatModel: ChatModel, displayName: String, close: () -> Unit) {
withApi { withApi {
val rhId = chatModel.remoteHostId() val rhId = chatModel.remoteHostId()

View File

@ -68,14 +68,14 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp
var searchInList by rememberSaveable { mutableStateOf("") } var searchInList by rememberSaveable { mutableStateOf("") }
val scope = rememberCoroutineScope() 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() } } }, Scaffold(topBar = { Box(Modifier.padding(end = endPadding)) { ChatListToolbar(chatModel, scaffoldState.drawerState, userPickerState, stopped) { searchInList = it.trim() } } },
scaffoldState = scaffoldState, scaffoldState = scaffoldState,
drawerContent = { SettingsView(chatModel, setPerformLA, scaffoldState.drawerState) }, drawerContent = { SettingsView(chatModel, setPerformLA, scaffoldState.drawerState) },
drawerScrimColor = MaterialTheme.colors.onSurface.copy(alpha = if (isInDarkTheme()) 0.16f else 0.32f), drawerScrimColor = MaterialTheme.colors.onSurface.copy(alpha = if (isInDarkTheme()) 0.16f else 0.32f),
drawerGesturesEnabled = appPlatform.isAndroid, drawerGesturesEnabled = appPlatform.isAndroid,
floatingActionButton = { floatingActionButton = {
if (searchInList.isEmpty()) { if (searchInList.isEmpty() && !chatModel.desktopNoUserNoRemote) {
FloatingActionButton( FloatingActionButton(
onClick = { onClick = {
if (!stopped) { if (!stopped) {
@ -104,7 +104,7 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
) { ) {
if (chatModel.chats.isNotEmpty()) { if (chatModel.chats.isNotEmpty()) {
ChatList(chatModel, search = searchInList) ChatList(chatModel, search = searchInList)
} else if (!switchingUsersAndHosts.value) { } else if (!chatModel.switchingUsersAndHosts.value && !chatModel.desktopNoUserNoRemote) {
Box(Modifier.fillMaxSize()) { Box(Modifier.fillMaxSize()) {
if (!stopped && !newChatSheetState.collectAsState().value.isVisible()) { if (!stopped && !newChatSheetState.collectAsState().value.isVisible()) {
OnboardingButtons(showNewChatSheet) OnboardingButtons(showNewChatSheet)
@ -121,19 +121,11 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
NewChatSheet(chatModel, newChatSheetState, stopped, hideNewChatSheet) NewChatSheet(chatModel, newChatSheetState, stopped, hideNewChatSheet)
} }
if (appPlatform.isAndroid) { if (appPlatform.isAndroid) {
UserPicker(chatModel, userPickerState, switchingUsersAndHosts) { UserPicker(chatModel, userPickerState) {
scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() } scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() }
userPickerState.value = AnimatedViewState.GONE userPickerState.value = AnimatedViewState.GONE
} }
} }
if (switchingUsersAndHosts.value) {
Box(
Modifier.fillMaxSize().clickable(enabled = false, onClick = {}),
contentAlignment = Alignment.Center
) {
ProgressIndicator()
}
}
} }
@Composable @Composable
@ -209,7 +201,7 @@ private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, user
navigationButton = { navigationButton = {
if (showSearch) { if (showSearch) {
NavigationButtonBack(hideSearchOnBack) 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() } } NavigationButtonMenu { scope.launch { if (drawerState.isOpen) drawerState.close() else drawerState.open() } }
} else { } else {
val users by remember { derivedStateOf { chatModel.users.filter { u -> u.user.activeUser || !u.user.hidden } } } 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 @Composable
expect fun DesktopActiveCallOverlayLayout(newChatSheetState: MutableStateFlow<AnimatedViewState>) expect fun DesktopActiveCallOverlayLayout(newChatSheetState: MutableStateFlow<AnimatedViewState>)

View File

@ -26,7 +26,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
@Composable @Composable
fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stopped: Boolean) { fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stopped: Boolean) {
var searchInList by rememberSaveable { mutableStateOf("") } var searchInList by rememberSaveable { mutableStateOf("") }
val (userPickerState, scaffoldState, switchingUsersAndHosts) = settingsState val (userPickerState, scaffoldState) = settingsState
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp
Scaffold( Scaffold(
Modifier.padding(end = endPadding), Modifier.padding(end = endPadding),
@ -47,7 +47,7 @@ fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stoppe
} }
} }
if (appPlatform.isAndroid) { if (appPlatform.isAndroid) {
UserPicker(chatModel, userPickerState, switchingUsersAndHosts, showSettings = false, showCancel = true, cancelClicked = { UserPicker(chatModel, userPickerState, showSettings = false, showCancel = true, cancelClicked = {
chatModel.sharedContent.value = null chatModel.sharedContent.value = null
userPickerState.value = AnimatedViewState.GONE userPickerState.value = AnimatedViewState.GONE
}) })

View File

@ -26,7 +26,9 @@ import chat.simplex.common.model.ChatModel.controller
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.views.CreateProfile
import chat.simplex.common.views.remote.* import chat.simplex.common.views.remote.*
import chat.simplex.common.views.usersettings.doWithAuth
import chat.simplex.res.MR import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -38,7 +40,6 @@ import kotlin.math.roundToInt
fun UserPicker( fun UserPicker(
chatModel: ChatModel, chatModel: ChatModel,
userPickerState: MutableStateFlow<AnimatedViewState>, userPickerState: MutableStateFlow<AnimatedViewState>,
switchingUsersAndHosts: MutableState<Boolean>,
showSettings: Boolean = true, showSettings: Boolean = true,
showCancel: Boolean = false, showCancel: Boolean = false,
cancelClicked: () -> Unit = {}, cancelClicked: () -> Unit = {},
@ -123,14 +124,10 @@ fun UserPicker(
userPickerState.value = AnimatedViewState.HIDING userPickerState.value = AnimatedViewState.HIDING
if (!u.user.activeUser) { if (!u.user.activeUser) {
scope.launch { scope.launch {
val job = launch { controller.showProgressIfNeeded {
delay(500) ModalManager.closeAllModalsEverywhere()
switchingUsersAndHosts.value = true 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 val currentRemoteHost = remember { chatModel.currentRemoteHost }.value
Column(Modifier.weight(1f).verticalScroll(rememberScrollState())) { Column(Modifier.weight(1f).verticalScroll(rememberScrollState())) {
if (remoteHosts.isNotEmpty()) { if (remoteHosts.isNotEmpty()) {
if (currentRemoteHost == null) { if (currentRemoteHost == null && chatModel.localUserCreated.value == true) {
LocalDevicePickerItem(true) { LocalDevicePickerItem(true) {
userPickerState.value = AnimatedViewState.HIDING userPickerState.value = AnimatedViewState.HIDING
switchToLocalDevice() switchToLocalDevice()
} }
Divider(Modifier.requiredHeight(1.dp)) Divider(Modifier.requiredHeight(1.dp))
} else { } else if (currentRemoteHost != null) {
val connecting = rememberSaveable { mutableStateOf(false) } val connecting = rememberSaveable { mutableStateOf(false) }
RemoteHostPickerItem(currentRemoteHost, RemoteHostPickerItem(currentRemoteHost,
actionButtonClick = { actionButtonClick = {
@ -176,7 +173,7 @@ fun UserPicker(
stopRemoteHostAndReloadHosts(currentRemoteHost, true) stopRemoteHostAndReloadHosts(currentRemoteHost, true)
}) { }) {
userPickerState.value = AnimatedViewState.HIDING userPickerState.value = AnimatedViewState.HIDING
switchToRemoteHost(currentRemoteHost, switchingUsersAndHosts, connecting) switchToRemoteHost(currentRemoteHost, connecting)
} }
Divider(Modifier.requiredHeight(1.dp)) Divider(Modifier.requiredHeight(1.dp))
} }
@ -184,7 +181,7 @@ fun UserPicker(
UsersView() UsersView()
if (remoteHosts.isNotEmpty() && currentRemoteHost != null) { if (remoteHosts.isNotEmpty() && currentRemoteHost != null && chatModel.localUserCreated.value == true) {
LocalDevicePickerItem(false) { LocalDevicePickerItem(false) {
userPickerState.value = AnimatedViewState.HIDING userPickerState.value = AnimatedViewState.HIDING
switchToLocalDevice() switchToLocalDevice()
@ -199,7 +196,7 @@ fun UserPicker(
stopRemoteHostAndReloadHosts(h, false) stopRemoteHostAndReloadHosts(h, false)
}) { }) {
userPickerState.value = AnimatedViewState.HIDING userPickerState.value = AnimatedViewState.HIDING
switchToRemoteHost(h, switchingUsersAndHosts, connecting) switchToRemoteHost(h, connecting)
} }
Divider(Modifier.requiredHeight(1.dp)) Divider(Modifier.requiredHeight(1.dp))
} }
@ -220,6 +217,18 @@ fun UserPicker(
userPickerState.value = AnimatedViewState.GONE userPickerState.value = AnimatedViewState.GONE
} }
Divider(Modifier.requiredHeight(1.dp)) 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) { if (showSettings) {
SettingsPickerItem(settingsClicked) 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 @Composable
private fun SettingsPickerItem(onClick: () -> Unit) { private fun SettingsPickerItem(onClick: () -> Unit) {
SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) { 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<Boolean>, connecting: MutableState<Boolean>) { private fun switchToRemoteHost(h: RemoteHostInfo, connecting: MutableState<Boolean>) {
if (!h.activeHost()) { if (!h.activeHost()) {
withBGApi { withBGApi {
val job = launch {
delay(500)
switchingUsersAndHosts.value = true
}
ModalManager.closeAllModalsEverywhere() ModalManager.closeAllModalsEverywhere()
if (h.sessionState != null) { if (h.sessionState != null) {
chatModel.controller.switchUIRemoteHost(h.remoteHostId) chatModel.controller.switchUIRemoteHost(h.remoteHostId)
} else { } else {
connectMobileDevice(h, connecting) connectMobileDevice(h, connecting)
} }
job.cancel()
switchingUsersAndHosts.value = false
} }
} else { } else {
connectMobileDevice(h, connecting) connectMobileDevice(h, connecting)

View File

@ -10,6 +10,7 @@ import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
import chat.simplex.common.platform.onRightClick import chat.simplex.common.platform.onRightClick
@ -202,13 +203,14 @@ fun SectionTextFooter(text: String) {
} }
@Composable @Composable
fun SectionTextFooter(text: AnnotatedString) { fun SectionTextFooter(text: AnnotatedString, textAlign: TextAlign = TextAlign.Start) {
Text( Text(
text, text,
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF).fillMaxWidth(0.9F), Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF).fillMaxWidth(0.9F),
color = MaterialTheme.colors.secondary, color = MaterialTheme.colors.secondary,
lineHeight = 18.sp, lineHeight = 18.sp,
fontSize = 14.sp fontSize = 14.sp,
textAlign = textAlign
) )
} }

View File

@ -182,6 +182,10 @@ private fun prepareChatBeforeAddressCreation(rhId: Long?) {
val user = chatModel.controller.apiGetActiveUser(rhId) ?: return@withApi val user = chatModel.controller.apiGetActiveUser(rhId) ?: return@withApi
chatModel.currentUser.value = user chatModel.currentUser.value = user
if (chatModel.users.isEmpty()) { 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) chatModel.controller.startChat(user)
} else { } else {
val users = chatModel.controller.listUsers(rhId) val users = chatModel.controller.listUsers(rhId)

View File

@ -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<String?>,
connecting: MutableState<Boolean>,
staleQrCode: MutableState<Boolean>,
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) }
}
}

View File

@ -3,6 +3,7 @@ package chat.simplex.common.views.onboarding
enum class OnboardingStage { enum class OnboardingStage {
Step1_SimpleXInfo, Step1_SimpleXInfo,
Step2_CreateProfile, Step2_CreateProfile,
LinkAMobile,
Step2_5_SetupDatabasePassphrase, Step2_5_SetupDatabasePassphrase,
Step3_CreateSimpleXAddress, Step3_CreateSimpleXAddress,
Step4_SetNotificationsMode, Step4_SetNotificationsMode,

View File

@ -43,7 +43,11 @@ fun SetupDatabasePassphrase(m: ChatModel) {
val newKey = rememberSaveable { mutableStateOf("") } val newKey = rememberSaveable { mutableStateOf("") }
val confirmNewKey = rememberSaveable { mutableStateOf("") } val confirmNewKey = rememberSaveable { mutableStateOf("") }
fun nextStep() { 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( SetupDatabasePassphraseLayout(
currentKey, currentKey,
@ -159,10 +163,7 @@ private fun SetupDatabasePassphraseLayout(
} }
}, },
isValid = { confirmNewKey.value == "" || newKey.value == confirmNewKey.value }, isValid = { confirmNewKey.value == "" || newKey.value == confirmNewKey.value },
keyboardActions = KeyboardActions(onDone = { keyboardActions = KeyboardActions(onDone = { defaultKeyboardAction(ImeAction.Done) }),
if (!disabled) onClickUpdate()
defaultKeyboardAction(ImeAction.Done)
}),
) )
Box(Modifier.align(Alignment.CenterHorizontally).padding(vertical = DEFAULT_PADDING)) { Box(Modifier.align(Alignment.CenterHorizontally).padding(vertical = DEFAULT_PADDING)) {

View File

@ -8,6 +8,7 @@ import androidx.compose.material.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
@ -99,26 +100,22 @@ private fun InfoRow(icon: Painter, titleId: StringResource, textId: StringResour
} }
@Composable @Composable
fun OnboardingActionButton(user: User?, onboardingStage: SharedPreference<OnboardingStage>, onclick: (() -> Unit)? = null) { expect fun OnboardingActionButton(user: User?, onboardingStage: SharedPreference<OnboardingStage>, 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)
}
}
@Composable @Composable
fun OnboardingActionButton( fun OnboardingActionButton(
labelId: StringResource, labelId: StringResource,
onboarding: OnboardingStage?, onboarding: OnboardingStage?,
border: Boolean, border: Boolean,
icon: Painter? = null,
iconColor: Color = MaterialTheme.colors.primary,
onclick: (() -> Unit)? onclick: (() -> Unit)?
) { ) {
val modifier = if (border) { val modifier = if (border) {
Modifier Modifier
.border(border = BorderStroke(1.dp, MaterialTheme.colors.primary), shape = RoundedCornerShape(50)) .border(border = BorderStroke(1.dp, MaterialTheme.colors.primary), shape = RoundedCornerShape(50))
.padding( .padding(
horizontal = DEFAULT_PADDING * 2, horizontal = if (icon == null) DEFAULT_PADDING * 2 else DEFAULT_PADDING_HALF,
vertical = 4.dp vertical = 4.dp
) )
} else { } else {
@ -131,6 +128,9 @@ fun OnboardingActionButton(
ChatController.appPrefs.onboardingStage.set(onboarding) ChatController.appPrefs.onboardingStage.set(onboarding)
} }
}, modifier) { }, 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) Text(stringResource(labelId), style = MaterialTheme.typography.h2, color = MaterialTheme.colors.primary, fontSize = 20.sp)
Icon( Icon(
painterResource(MR.images.ic_arrow_forward_ios), "next stage", tint = MaterialTheme.colors.primary, painterResource(MR.images.ic_arrow_forward_ios), "next stage", tint = MaterialTheme.colors.primary,

View File

@ -13,12 +13,14 @@ import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.* import androidx.compose.ui.text.font.*
import androidx.compose.ui.text.input.* import androidx.compose.ui.text.input.*
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import chat.simplex.common.model.* import chat.simplex.common.model.*
@ -97,9 +99,11 @@ fun ConnectMobileLayout(
SectionDividerSpaced(maxBottomPadding = false) SectionDividerSpaced(maxBottomPadding = false)
} }
SectionView(stringResource(MR.strings.devices).uppercase()) { SectionView(stringResource(MR.strings.devices).uppercase()) {
SettingsActionItemWithContent(text = stringResource(MR.strings.this_device), icon = painterResource(MR.images.ic_desktop), click = connectDesktop) { if (chatModel.localUserCreated.value == true) {
if (connectedHost.value == null) { SettingsActionItemWithContent(text = stringResource(MR.strings.this_device), icon = painterResource(MR.images.ic_desktop), click = connectDesktop) {
Icon(painterResource(MR.images.ic_done_filled), null, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground) 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 @Composable
private fun ConnectMobileViewLayout( private fun ConnectMobileViewLayout(
title: String, title: String?,
invitation: String?, invitation: String?,
deviceName: String?, deviceName: String?,
sessionCode: String?, sessionCode: String?,
port: String? port: String?,
staleQrCode: Boolean = false,
refreshQrCode: () -> Unit = {}
) { ) {
Column( Column(
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
AppBarTitle(title) if (title != null) {
AppBarTitle(title)
}
SectionView { SectionView {
if (invitation != null && sessionCode == null && port != null) { if (invitation != null && sessionCode == null && port != null) {
QRCode( Box {
invitation, Modifier QRCode(
.padding(start = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING_HALF) invitation, Modifier
.aspectRatio(1f) .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)) 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) { if (remember { controller.appPrefs.developerTools.state }.value) {
val clipboard = LocalClipboardManager.current val clipboard = LocalClipboardManager.current
@ -237,55 +252,72 @@ fun connectMobileDevice(rh: RemoteHostInfo, connecting: MutableState<Boolean>) {
private fun showAddingMobileDevice(connecting: MutableState<Boolean>) { private fun showAddingMobileDevice(connecting: MutableState<Boolean>) {
ModalManager.start.showModalCloseable { close -> ModalManager.start.showModalCloseable { close ->
val invitation = rememberSaveable { mutableStateOf<String?>(null) } AddingMobileDevice(true, remember { mutableStateOf(false) }, connecting, close)
val port = rememberSaveable { mutableStateOf<String?>(null) } }
val pairing = remember { chatModel.remoteHostPairing } }
val sessionCode = when (val state = pairing.value?.second) {
is RemoteHostSessionState.PendingConfirmation -> state.sessionCode @Composable
else -> null fun AddingMobileDevice(showTitle: Boolean, staleQrCode: MutableState<Boolean>, connecting: MutableState<Boolean>, close: () -> Unit) {
val invitation = rememberSaveable { mutableStateOf<String?>(null) }
val port = rememberSaveable { mutableStateOf<String?>(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<String?>(null) } val pairing = remember { chatModel.remoteHostPairing }
if (cachedSessionCode == null && sessionCode != null) { val sessionCode = when (val state = pairing.value?.second) {
cachedSessionCode = sessionCode is RemoteHostSessionState.PendingConfirmation -> state.sessionCode
} else -> null
val remoteDeviceName = pairing.value?.first?.hostDeviceName }
ConnectMobileViewLayout( /** It's needed to prevent screen flashes when [chatModel.newRemoteHostPairing] sets to null in background */
title = if (cachedSessionCode == null) stringResource(MR.strings.link_a_mobile) else stringResource(MR.strings.verify_connection), var cachedSessionCode by remember { mutableStateOf<String?>(null) }
invitation = invitation.value, if (cachedSessionCode == null && sessionCode != null) {
deviceName = remoteDeviceName, cachedSessionCode = sessionCode
sessionCode = cachedSessionCode, }
port = port.value val remoteDeviceName = pairing.value?.first?.hostDeviceName
) ConnectMobileViewLayout(
val oldRemoteHostId by remember { mutableStateOf(chatModel.currentRemoteHost.value?.remoteHostId) } title = if (!showTitle) null else if (cachedSessionCode == null) stringResource(MR.strings.link_a_mobile) else stringResource(MR.strings.verify_connection),
LaunchedEffect(remember { chatModel.currentRemoteHost }.value) { invitation = invitation.value,
if (chatModel.currentRemoteHost.value?.remoteHostId != null && chatModel.currentRemoteHost.value?.remoteHostId != oldRemoteHostId) { deviceName = remoteDeviceName,
close() sessionCode = cachedSessionCode,
} port = port.value,
} staleQrCode = staleQrCode.value,
KeyChangeEffect(pairing.value) { refreshQrCode = {
if (pairing.value == null) {
close()
}
}
DisposableEffect(Unit) {
withBGApi { withBGApi {
val r = chatModel.controller.startRemoteHost(null, controller.appPrefs.offerRemoteMulticast.get()) if (chatController.stopRemoteHost(null)) {
if (r != null) { startRemoteHost()
connecting.value = true staleQrCode.value = false
invitation.value = r.second
port.value = r.third
chatModel.remoteHostPairing.value = null to RemoteHostSessionState.Starting
} }
} }
onDispose { },
if (chatModel.currentRemoteHost.value?.remoteHostId == oldRemoteHostId) { )
withBGApi { val oldRemoteHostId by remember { mutableStateOf(chatModel.currentRemoteHost.value?.remoteHostId) }
chatController.stopRemoteHost(null) 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
} }
} }
} }

View File

@ -25,6 +25,7 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.platform.chatModel
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.item.ClickableText import chat.simplex.common.views.chat.item.ClickableText
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
@ -169,18 +170,20 @@ fun NetworkAndServersView(
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
AppBarTitle(stringResource(MR.strings.network_and_servers)) AppBarTitle(stringResource(MR.strings.network_and_servers))
SectionView(generalGetString(MR.strings.settings_section_title_messages)) { if (!chatModel.desktopNoUserNoRemote) {
SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.smp_servers), showCustomModal { m, close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.SMP, close) }) 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) { if (currentRemoteHost == null) {
UseSocksProxySwitch(networkUseSocksProxy, proxyPort, toggleSocksProxy, showSettingsModal) UseSocksProxySwitch(networkUseSocksProxy, proxyPort, toggleSocksProxy, showSettingsModal)
UseOnionHosts(onionHosts, networkUseSocksProxy, showSettingsModal, useOnion) UseOnionHosts(onionHosts, networkUseSocksProxy, showSettingsModal, useOnion)
if (developerTools) { if (developerTools) {
SessionModePicker(sessionMode, showSettingsModal, updateSessionMode) 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) { 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)) 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)) Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 24.dp, end = DEFAULT_PADDING_HALF, bottom = 30.dp))
} }

View File

@ -92,7 +92,6 @@ fun PrivacySettingsView(
chatModel.simplexLinkMode.value = it chatModel.simplexLinkMode.value = it
}) })
} }
SectionDividerSpaced()
val currentUser = chatModel.currentUser.value val currentUser = chatModel.currentUser.value
if (currentUser != null) { if (currentUser != null) {
@ -142,39 +141,42 @@ fun PrivacySettingsView(
} }
} }
DeliveryReceiptsSection( if (!chatModel.desktopNoUserNoRemote) {
currentUser = currentUser, SectionDividerSpaced()
setOrAskSendReceiptsContacts = { enable -> DeliveryReceiptsSection(
val contactReceiptsOverrides = chatModel.chats.fold(0) { count, chat -> currentUser = currentUser,
if (chat.chatInfo is ChatInfo.Direct) { setOrAskSendReceiptsContacts = { enable ->
val sendRcpts = chat.chatInfo.contact.chatSettings.sendRcpts val contactReceiptsOverrides = chatModel.chats.fold(0) { count, chat ->
count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1) 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 { } 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() SectionBottomSpacer()
} }

View File

@ -25,6 +25,7 @@ import androidx.compose.ui.unit.*
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.CreateProfile
import chat.simplex.common.views.database.DatabaseView import chat.simplex.common.views.database.DatabaseView
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.onboarding.SimpleXInfo import chat.simplex.common.views.onboarding.SimpleXInfo
@ -38,76 +39,39 @@ import kotlinx.coroutines.launch
fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, drawerState: DrawerState) { fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, drawerState: DrawerState) {
val user = chatModel.currentUser.value val user = chatModel.currentUser.value
val stopped = chatModel.chatRunning.value == false val stopped = chatModel.chatRunning.value == false
SettingsLayout(
if (user != null) { profile = user?.profile,
val requireAuth = remember { chatModel.controller.appPrefs.performLA.state } stopped,
SettingsLayout( chatModel.chatDbEncrypted.value == true,
profile = user.profile, remember { chatModel.controller.appPrefs.storeDBPassphrase.state }.value,
stopped, remember { chatModel.controller.appPrefs.notificationsMode.state },
chatModel.chatDbEncrypted.value == true, user?.displayName,
remember { chatModel.controller.appPrefs.storeDBPassphrase.state }.value, setPerformLA = setPerformLA,
remember { chatModel.controller.appPrefs.notificationsMode.state }, showModal = { modalView -> { ModalManager.start.showModal { modalView(chatModel) } } },
user.displayName, showSettingsModal = { modalView -> { ModalManager.start.showModal(true) { modalView(chatModel) } } },
setPerformLA = setPerformLA, showSettingsModalWithSearch = { modalView ->
showModal = { modalView -> { ModalManager.start.showModal { modalView(chatModel) } } }, ModalManager.start.showCustomModal { close ->
showSettingsModal = { modalView -> { ModalManager.start.showModal(true) { modalView(chatModel) } } }, val search = rememberSaveable { mutableStateOf("") }
showSettingsModalWithSearch = { modalView -> ModalView(
ModalManager.start.showCustomModal { close -> { close() },
val search = rememberSaveable { mutableStateOf("") } endButtons = {
ModalView( SearchTextField(Modifier.fillMaxWidth(), placeholder = stringResource(MR.strings.search_verb), alwaysVisible = true) { search.value = it }
{ close() }, },
endButtons = { content = { modalView(chatModel, search) })
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 = { withAuth = ::doWithAuth,
withApi { drawerState = drawerState,
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,
)
}
} }
val simplexTeamUri = val simplexTeamUri =
@ -115,12 +79,12 @@ val simplexTeamUri =
@Composable @Composable
fun SettingsLayout( fun SettingsLayout(
profile: LocalProfile, profile: LocalProfile?,
stopped: Boolean, stopped: Boolean,
encrypted: Boolean, encrypted: Boolean,
passphraseSaved: Boolean, passphraseSaved: Boolean,
notificationsMode: State<NotificationsMode>, notificationsMode: State<NotificationsMode>,
userDisplayName: String, userDisplayName: String?,
setPerformLA: (Boolean) -> Unit, setPerformLA: (Boolean) -> Unit,
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
@ -150,13 +114,22 @@ fun SettingsLayout(
AppBarTitle(stringResource(MR.strings.your_settings)) AppBarTitle(stringResource(MR.strings.your_settings))
SectionView(stringResource(MR.strings.settings_section_title_you)) { 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) } 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) if (profile != null) {
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) SectionItemView(showCustomModal { chatModel, close -> UserProfileView(chatModel, close) }, 80.dp, padding = PaddingValues(start = 16.dp, end = DEFAULT_PADDING), disabled = stopped) {
ChatPreferencesItem(showCustomModal, stopped = 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) { 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) 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 { } 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_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_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) 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() SectionDividerSpaced()
SectionView(stringResource(MR.strings.settings_section_title_help)) { 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_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_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) 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() 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) { private fun runAuth(title: String, desc: String, onFinish: (success: Boolean) -> Unit) {
authenticate( authenticate(
title, title,

View File

@ -21,6 +21,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.model.ChatModel.controller
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.item.ItemAction import chat.simplex.common.views.chat.item.ItemAction
@ -56,7 +57,9 @@ fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden:
ModalManager.end.closeModals() ModalManager.end.closeModals()
} }
withBGApi { 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 -> removeUser = { user ->

View File

@ -565,6 +565,7 @@
<string name="your_settings">Your settings</string> <string name="your_settings">Your settings</string>
<string name="your_simplex_contact_address">Your SimpleX address</string> <string name="your_simplex_contact_address">Your SimpleX address</string>
<string name="your_chat_profiles">Your chat profiles</string> <string name="your_chat_profiles">Your chat profiles</string>
<string name="create_chat_profile">Create chat profile</string>
<string name="database_passphrase_and_export">Database passphrase &amp; export</string> <string name="database_passphrase_and_export">Database passphrase &amp; export</string>
<string name="about_simplex_chat">About SimpleX Chat</string> <string name="about_simplex_chat">About SimpleX Chat</string>
<string name="how_to_use_simplex_chat">How to use it</string> <string name="how_to_use_simplex_chat">How to use it</string>
@ -1689,6 +1690,8 @@
<string name="paste_desktop_address">Paste desktop address</string> <string name="paste_desktop_address">Paste desktop address</string>
<string name="desktop_device">Desktop</string> <string name="desktop_device">Desktop</string>
<string name="not_compatible">Not compatible!</string> <string name="not_compatible">Not compatible!</string>
<string name="refresh_qr_code">Refresh</string>
<string name="no_connected_mobile">No connected mobile</string>
<!-- Under development --> <!-- Under development -->
<string name="in_developing_title">Coming soon!</string> <string name="in_developing_title">Coming soon!</string>

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480.433-164.5q-131.583 0-223.758-92.216Q164.5-348.932 164.5-480.082q0-131.149 92.175-223.284Q348.85-795.5 480.5-795.5q84 0 147.75 34.25T738.5-666.5v-129H796V-547H547.5v-57.5H713q-38.032-60.033-96.537-96.767Q557.959-738 480.539-738 372-738 297-663.015q-75 74.986-75 183.25 0 108.265 74.875 183.015Q371.75-222 480.331-222q82.298 0 150.734-47 68.435-47 95.623-124.5H786q-28.5 103-113.49 166-84.991 63-192.077 63Z"/></svg>

After

Width:  |  Height:  |  Size: 516 B

View File

@ -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<OnboardingStage>, 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)
}
}