desktop: enhancements to remote hosts experience (#3428)

This commit is contained in:
Stanislav Dmitrenko 2023-11-22 22:35:32 +08:00 committed by GitHub
parent 9442121efa
commit 48d7afc959
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 99 additions and 51 deletions

View File

@ -126,7 +126,7 @@ fun processIntent(intent: Intent?) {
when (intent?.action) {
"android.intent.action.VIEW" -> {
val uri = intent.data
if (uri != null) connectIfOpenedViaUri(chatModel.remoteHostId, uri.toURI(), ChatModel)
if (uri != null) connectIfOpenedViaUri(chatModel.remoteHostId(), uri.toURI(), ChatModel)
}
}
}

View File

@ -57,7 +57,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
updatingChatsMutex.withLock {
kotlin.runCatching {
val currentUserId = chatModel.currentUser.value?.userId
val chats = ArrayList(chatController.apiGetChats(chatModel.remoteHostId))
val chats = ArrayList(chatController.apiGetChats(chatModel.remoteHostId()))
/** Active user can be changed in background while [ChatController.apiGetChats] is executing */
if (chatModel.currentUser.value?.userId == currentUserId) {
val currentChatId = chatModel.chatId.value

View File

@ -111,7 +111,8 @@ object ChatModel {
// remote controller
val remoteHosts = mutableStateListOf<RemoteHostInfo>()
val currentRemoteHost = mutableStateOf<RemoteHostInfo?>(null)
val remoteHostId: Long? get() = currentRemoteHost?.value?.remoteHostId
val remoteHostId: Long? @Composable get() = remember { currentRemoteHost }.value?.remoteHostId
fun remoteHostId(): Long? = currentRemoteHost.value?.remoteHostId
val newRemoteHostPairing = mutableStateOf<Pair<RemoteHostInfo?, RemoteHostSessionState>?>(null)
val remoteCtrlSession = mutableStateOf<RemoteCtrlSession?>(null)

View File

@ -1625,7 +1625,7 @@ object ChatController {
|| (mc is MsgContent.MCVoice && file.fileSize <= MAX_VOICE_SIZE_AUTO_RCV && file.fileStatus !is CIFileStatus.RcvAccepted))) {
withApi { receiveFile(rhId, r.user, file.fileId, encrypted = cItem.encryptLocalFile && chatController.appPrefs.privacyEncryptLocalFiles.get(), auto = true) }
}
if (cItem.showNotification && (allowedToShowNotification() || chatModel.chatId.value != cInfo.id || chatModel.remoteHostId != rhId)) {
if (cItem.showNotification && (allowedToShowNotification() || chatModel.chatId.value != cInfo.id || chatModel.remoteHostId() != rhId)) {
ntfManager.notifyMessageReceived(r.user, cInfo, cItem)
}
}
@ -1913,7 +1913,7 @@ object ChatController {
}
private fun activeUser(rhId: Long?, user: UserLike): Boolean =
rhId == chatModel.remoteHostId && user.userId == chatModel.currentUser.value?.userId
rhId == chatModel.remoteHostId() && user.userId == chatModel.currentUser.value?.userId
private fun withCall(r: CR, contact: Contact, perform: (Call) -> Unit) {
val call = chatModel.activeCall.value

View File

@ -54,7 +54,7 @@ private fun sendCommand(chatModel: ChatModel, composeState: MutableState<Compose
withApi {
// show "in progress"
// TODO show active remote host in chat console?
chatModel.controller.sendCmd(chatModel.remoteHostId, CC.Console(s))
chatModel.controller.sendCmd(chatModel.remoteHostId(), CC.Console(s))
composeState.value = ComposeState(useLinkPreviews = false)
// hide "in progress"
}

View File

@ -170,7 +170,7 @@ fun CreateFirstProfile(chatModel: ChatModel, close: () -> Unit) {
fun createProfileInProfiles(chatModel: ChatModel, displayName: String, close: () -> Unit) {
withApi {
val rhId = chatModel.remoteHostId
val rhId = chatModel.remoteHostId()
val user = chatModel.controller.apiCreateActiveUser(
rhId, Profile(displayName.trim(), "", null)
) ?: return@withApi

View File

@ -17,7 +17,7 @@ fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: ()
.fillMaxSize()
.padding(horizontal = DEFAULT_PADDING)
) {
AppBarTitle(stringResource(MR.strings.scan_code), false)
AppBarTitle(stringResource(MR.strings.scan_code), withPadding = false)
Box(
Modifier
.fillMaxWidth()

View File

@ -63,7 +63,7 @@ private fun VerifyCodeLayout(
.verticalScroll(rememberScrollState())
.padding(horizontal = DEFAULT_PADDING)
) {
AppBarTitle(stringResource(MR.strings.security_code), false)
AppBarTitle(stringResource(MR.strings.security_code), withPadding = false)
val splitCode = splitToParts(connectionCode, 24)
Row(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF), horizontalArrangement = Arrangement.Center) {
if (connectionVerified) {

View File

@ -53,7 +53,7 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
val url = chatModel.appOpenUrl.value
if (url != null) {
chatModel.appOpenUrl.value = null
connectIfOpenedViaUri(chatModel.remoteHostId, url, chatModel)
connectIfOpenedViaUri(chatModel.remoteHostId(), url, chatModel)
}
}
if (appPlatform.isDesktop) {

View File

@ -26,8 +26,7 @@ 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.remote.ConnectDesktopView
import chat.simplex.common.views.remote.connectMobileDevice
import chat.simplex.common.views.remote.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.delay
@ -84,7 +83,7 @@ fun UserPicker(
.filter { it }
.collect {
try {
val updatedUsers = chatModel.controller.listUsers(chatModel.remoteHostId).sortedByDescending { it.user.activeUser }
val updatedUsers = chatModel.controller.listUsers(chatModel.remoteHostId()).sortedByDescending { it.user.activeUser }
var same = users.size == updatedUsers.size
if (same) {
for (i in 0 until minOf(users.size, updatedUsers.size)) {
@ -213,6 +212,14 @@ fun UserPicker(
userPickerState.value = AnimatedViewState.GONE
}
Divider(Modifier.requiredHeight(1.dp))
} else if (remoteHosts.isEmpty()) {
LinkAMobilePickerItem {
ModalManager.start.showModal {
ConnectMobileView()
}
userPickerState.value = AnimatedViewState.GONE
}
Divider(Modifier.requiredHeight(1.dp))
}
if (showSettings) {
SettingsPickerItem(settingsClicked)
@ -384,6 +391,16 @@ private fun UseFromDesktopPickerItem(onClick: () -> Unit) {
}
}
@Composable
private fun LinkAMobilePickerItem(onClick: () -> Unit) {
SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) {
val text = generalGetString(MR.strings.link_a_mobile)
Icon(painterResource(MR.images.ic_smartphone_300), 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) {

View File

@ -627,7 +627,7 @@ private fun afterSetCiTTL(
try {
updatingChatsMutex.withLock {
// this is using current remote host on purpose - if it changes during update, it will load correct chats
val chats = m.controller.apiGetChats(m.remoteHostId)
val chats = m.controller.apiGetChats(m.remoteHostId())
m.updateChats(chats)
}
} catch (e: Exception) {

View File

@ -88,7 +88,7 @@ class AlertManager {
onDismissRequest = { onDismissRequest?.invoke(); hideAlert() },
title = alertTitle(title),
buttons = {
AlertContent(text, hostDevice) {
AlertContent(text, hostDevice, true) {
Row(
Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING),
horizontalArrangement = Arrangement.SpaceBetween

View File

@ -14,6 +14,8 @@ import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import chat.simplex.common.ui.theme.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
@Composable
fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}) {
@ -47,23 +49,38 @@ fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, endButtons: @
}
@Composable
fun AppBarTitle(title: String, withPadding: Boolean = true, bottomPadding: Dp = DEFAULT_PADDING * 1.5f) {
fun AppBarTitle(title: String, hostDevice: Pair<Long?, String>? = null, withPadding: Boolean = true, bottomPadding: Dp = DEFAULT_PADDING * 1.5f) {
val theme = CurrentColors.collectAsState()
val titleColor = CurrentColors.collectAsState().value.appColors.title
val brush = if (theme.value.base == DefaultTheme.SIMPLEX)
Brush.linearGradient(listOf(titleColor.darker(0.2f), titleColor.lighter(0.35f)), Offset(0f, Float.POSITIVE_INFINITY), Offset(Float.POSITIVE_INFINITY, 0f))
else // color is not updated when changing themes if I pass null here
Brush.linearGradient(listOf(titleColor, titleColor), Offset(0f, Float.POSITIVE_INFINITY), Offset(Float.POSITIVE_INFINITY, 0f))
Text(
title,
Modifier
.fillMaxWidth()
.padding(bottom = bottomPadding, start = if (withPadding) DEFAULT_PADDING else 0.dp, end = if (withPadding) DEFAULT_PADDING else 0.dp,),
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.h1.copy(brush = brush),
color = MaterialTheme.colors.primaryVariant,
textAlign = TextAlign.Center
)
Column {
Text(
title,
Modifier
.fillMaxWidth()
.padding(start = if (withPadding) DEFAULT_PADDING else 0.dp, end = if (withPadding) DEFAULT_PADDING else 0.dp,),
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.h1.copy(brush = brush),
color = MaterialTheme.colors.primaryVariant,
textAlign = TextAlign.Center
)
if (hostDevice != null) {
HostDeviceTitle(hostDevice)
}
Spacer(Modifier.height(bottomPadding))
}
}
@Composable
private fun HostDeviceTitle(hostDevice: Pair<Long?, String>, extraPadding: Boolean = false) {
Row(Modifier.fillMaxWidth().padding(top = 5.dp, bottom = if (extraPadding) DEFAULT_PADDING * 2 else DEFAULT_PADDING_HALF), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center) {
Icon(painterResource(if (hostDevice.first == null) MR.images.ic_desktop else MR.images.ic_smartphone_300), null, Modifier.size(15.dp), tint = MaterialTheme.colors.secondary)
Spacer(Modifier.width(10.dp))
Text(hostDevice.second, color = MaterialTheme.colors.secondary)
}
}
@Preview/*(

View File

@ -373,7 +373,7 @@ inline fun <reified T> serializableSaver(): Saver<T, *> = Saver(
fun UriHandler.openVerifiedSimplexUri(uri: String) {
val URI = try { URI.create(uri) } catch (e: Exception) { null }
if (URI != null) {
connectIfOpenedViaUri(chatModel.remoteHostId, URI, ChatModel)
connectIfOpenedViaUri(chatModel.remoteHostId(), URI, ChatModel)
}
}

View File

@ -79,7 +79,7 @@ fun AddContactLayout(
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.SpaceBetween,
) {
AppBarTitle(stringResource(MR.strings.add_contact))
AppBarTitle(stringResource(MR.strings.add_contact), hostDevice(rh?.remoteHostId))
SectionView(stringResource(MR.strings.one_time_link_short).uppercase()) {
if (connReq.isNotEmpty()) {

View File

@ -58,6 +58,7 @@ fun AddGroupView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit) {
}
},
incognitoPref = chatModel.controller.appPrefs.incognito,
rhId,
close
)
}
@ -66,6 +67,7 @@ fun AddGroupView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit) {
fun AddGroupLayout(
createGroup: (Boolean, GroupProfile) -> Unit,
incognitoPref: SharedPreference<Boolean>,
rhId: Long?,
close: () -> Unit
) {
val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
@ -98,7 +100,7 @@ fun AddGroupLayout(
.verticalScroll(rememberScrollState())
.padding(horizontal = DEFAULT_PADDING)
) {
AppBarTitle(stringResource(MR.strings.create_secret_group_title))
AppBarTitle(stringResource(MR.strings.create_secret_group_title), hostDevice(rhId))
Box(
Modifier
.fillMaxWidth()
@ -174,7 +176,8 @@ fun PreviewAddGroupLayout() {
AddGroupLayout(
createGroup = { _, _ -> },
incognitoPref = SharedPreference({ false }, {}),
close = {}
close = {},
rhId = null,
)
}
}

View File

@ -56,6 +56,7 @@ fun ContactConnectionInfoView(
connReq = connReqInvitation,
contactConnection = contactConnection,
focusAlias = focusAlias,
rhId = rhId,
deleteConnection = { deleteContactConnectionAlert(rhId, contactConnection, chatModel, close) },
onLocalAliasChanged = { setContactAlias(rhId, contactConnection, it, chatModel) },
share = { if (connReqInvitation != null) clipboard.shareText(connReqInvitation) },
@ -80,6 +81,7 @@ private fun ContactConnectionInfoLayout(
connReq: String?,
contactConnection: PendingContactConnection,
focusAlias: Boolean,
rhId: Long?,
deleteConnection: () -> Unit,
onLocalAliasChanged: (String) -> Unit,
share: () -> Unit,
@ -114,7 +116,8 @@ private fun ContactConnectionInfoLayout(
stringResource(
if (contactConnection.initiated) MR.strings.you_invited_a_contact
else MR.strings.you_accepted_connection
)
),
hostDevice(rhId)
)
Text(
stringResource(
@ -185,6 +188,7 @@ private fun PreviewContactConnectionInfoView() {
connReq = "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D",
contactConnection = PendingContactConnection.getSampleData(),
focusAlias = false,
rhId = null,
deleteConnection = {},
onLocalAliasChanged = {},
share = {},

View File

@ -67,7 +67,7 @@ fun PasteToConnectLayout(
Modifier.verticalScroll(rememberScrollState()).padding(horizontal = DEFAULT_PADDING),
verticalArrangement = Arrangement.SpaceBetween,
) {
AppBarTitle(stringResource(MR.strings.connect_via_link), false)
AppBarTitle(stringResource(MR.strings.connect_via_link), hostDevice(rhId), withPadding = false)
Box(Modifier.padding(top = DEFAULT_PADDING, bottom = 6.dp)) {
TextEditor(

View File

@ -469,7 +469,7 @@ fun ConnectContactLayout(
Modifier.verticalScroll(rememberScrollState()).padding(horizontal = DEFAULT_PADDING),
verticalArrangement = Arrangement.SpaceBetween
) {
AppBarTitle(stringResource(MR.strings.scan_QR_code), false)
AppBarTitle(stringResource(MR.strings.scan_QR_code), hostDevice(rh?.remoteHostId), withPadding = false)
Box(
Modifier
.fillMaxWidth()

View File

@ -26,7 +26,7 @@ fun HowItWorks(user: User?, onboardingStage: SharedPreference<OnboardingStage>?
.fillMaxWidth()
.padding(horizontal = DEFAULT_PADDING),
) {
AppBarTitle(stringResource(MR.strings.how_simplex_works), false)
AppBarTitle(stringResource(MR.strings.how_simplex_works), withPadding = false)
ReadableText(MR.strings.many_people_asked_how_can_it_deliver)
ReadableText(MR.strings.to_protect_privacy_simplex_has_ids_for_queues)
ReadableText(MR.strings.you_control_servers_to_receive_your_contacts_to_send)

View File

@ -36,12 +36,10 @@ import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
@Composable
fun ConnectMobileView(
m: ChatModel
) {
fun ConnectMobileView() {
val connecting = rememberSaveable() { mutableStateOf(false) }
val remoteHosts = remember { chatModel.remoteHosts }
val deviceName = m.controller.appPrefs.deviceNameForRemoteAccess
val deviceName = chatModel.controller.appPrefs.deviceNameForRemoteAccess
LaunchedEffect(Unit) {
controller.reloadRemoteHosts()
}
@ -49,11 +47,11 @@ fun ConnectMobileView(
deviceName = remember { deviceName.state },
remoteHosts = remoteHosts,
connecting,
connectedHost = remember { m.currentRemoteHost },
connectedHost = remember { chatModel.currentRemoteHost },
updateDeviceName = {
withBGApi {
if (it != "") {
m.controller.setLocalDeviceName(it)
chatModel.controller.setLocalDeviceName(it)
deviceName.set(it)
}
}

View File

@ -26,7 +26,7 @@ fun HelpLayout(userDisplayName: String) {
.verticalScroll(rememberScrollState())
.padding(horizontal = DEFAULT_PADDING),
){
AppBarTitle(String.format(stringResource(MR.strings.personal_welcome), userDisplayName), false)
AppBarTitle(String.format(stringResource(MR.strings.personal_welcome), userDisplayName), withPadding = false)
ChatHelpView()
}
}

View File

@ -29,13 +29,12 @@ import kotlinx.coroutines.launch
@Composable
fun ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: ServerProtocol, close: () -> Unit) {
// TODO close if remote host changes
var presetServers by remember { mutableStateOf(emptyList<String>()) }
var servers by remember {
var presetServers by remember(rhId) { mutableStateOf(emptyList<String>()) }
var servers by remember(rhId) {
mutableStateOf(m.userSMPServersUnsaved.value ?: emptyList())
}
val currServers = remember { mutableStateOf(servers) }
val testing = rememberSaveable { mutableStateOf(false) }
val currServers = remember(rhId) { mutableStateOf(servers) }
val testing = rememberSaveable(rhId) { mutableStateOf(false) }
val serversUnchanged = remember { derivedStateOf { servers == currServers.value || testing.value } }
val allServersDisabled = remember { derivedStateOf { servers.all { !it.enabled } } }
val saveDisabled = remember {
@ -51,7 +50,12 @@ fun ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: ServerProtoco
}
}
LaunchedEffect(Unit) {
KeyChangeEffect(rhId) {
m.userSMPServersUnsaved.value = null
servers = emptyList()
}
LaunchedEffect(rhId) {
val res = m.controller.getUserProtoServers(rhId, serverProtocol)
if (res != null) {
currServers.value = res.protoServers

View File

@ -22,7 +22,7 @@ fun ScanProtocolServerLayout(rhId: Long?, onNext: (ServerCfg) -> Unit) {
.fillMaxSize()
.padding(horizontal = DEFAULT_PADDING)
) {
AppBarTitle(stringResource(MR.strings.smp_servers_scan_qr), false)
AppBarTitle(stringResource(MR.strings.smp_servers_scan_qr), withPadding = false)
Box(
Modifier
.fillMaxWidth()

View File

@ -158,7 +158,7 @@ fun SettingsLayout(
SettingsActionItem(painterResource(MR.images.ic_qr_code), stringResource(MR.strings.your_simplex_contact_address), showCustomModal { it, close -> UserAddressView(it, it.currentUser.value?.remoteHostId, shareViaProfile = it.currentUser.value!!.addressShared, close = close) }, disabled = stopped, extraPadding = true)
ChatPreferencesItem(showCustomModal, stopped = stopped)
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(it) }, 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 {
SettingsActionItem(painterResource(MR.images.ic_desktop), stringResource(MR.strings.settings_section_title_use_from_desktop), showCustomModal{ it, close -> ConnectDesktopView(close) }, disabled = stopped, extraPadding = true)
}

View File

@ -65,6 +65,7 @@ fun UserAddressView(
UserAddressLayout(
userAddress = userAddress.value,
shareViaProfile,
rhId,
onCloseHandler,
createAddress = {
withApi {
@ -169,6 +170,7 @@ fun UserAddressView(
private fun UserAddressLayout(
userAddress: UserContactLinkRec?,
shareViaProfile: MutableState<Boolean>,
rhId: Long?,
onCloseHandler: MutableState<(close: () -> Unit) -> Unit>,
createAddress: () -> Unit,
learnMore: () -> Unit,
@ -181,7 +183,7 @@ private fun UserAddressLayout(
Column(
Modifier.verticalScroll(rememberScrollState()),
) {
AppBarTitle(stringResource(MR.strings.simplex_address), false)
AppBarTitle(stringResource(MR.strings.simplex_address), hostDevice(rhId), withPadding = false)
Column(
Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF),
horizontalAlignment = Alignment.CenterHorizontally,
@ -438,6 +440,7 @@ fun PreviewUserAddressLayoutNoAddress() {
setProfileAddress = { _ -> },
learnMore = {},
shareViaProfile = remember { mutableStateOf(false) },
rhId = null,
onCloseHandler = remember { mutableStateOf({}) },
sendEmail = {},
)
@ -471,6 +474,7 @@ fun PreviewUserAddressLayoutAddressCreated() {
setProfileAddress = { _ -> },
learnMore = {},
shareViaProfile = remember { mutableStateOf(false) },
rhId = null,
onCloseHandler = remember { mutableStateOf({}) },
sendEmail = {},
)

View File

@ -18,7 +18,7 @@ fun VersionInfoView(info: CoreVersionInfo) {
Column(
Modifier.padding(horizontal = DEFAULT_PADDING),
) {
AppBarTitle(stringResource(MR.strings.app_version_title), false)
AppBarTitle(stringResource(MR.strings.app_version_title), withPadding = false)
if (appPlatform.isAndroid) {
Text(String.format(stringResource(MR.strings.app_version_name), BuildConfigCommon.ANDROID_VERSION_NAME))
Text(String.format(stringResource(MR.strings.app_version_code), BuildConfigCommon.ANDROID_VERSION_CODE))