Merge branch 'master' into master-ghc8107

This commit is contained in:
Evgeny Poberezkin
2023-11-23 16:22:46 +00:00
60 changed files with 534 additions and 290 deletions
@@ -5,7 +5,7 @@
//#include <android/log.h>
// from the RTS
void hs_init(int * argc, char **argv[]);
void hs_init_with_rtsopts(int * argc, char **argv[]);
// from android-support
void setLineBuffering(void);
@@ -32,7 +32,17 @@ Java_chat_simplex_common_platform_CoreKt_pipeStdOutToSocket(JNIEnv *env, __unuse
JNIEXPORT void JNICALL
Java_chat_simplex_common_platform_CoreKt_initHS(__unused JNIEnv *env, __unused jclass clazz) {
hs_init(NULL, NULL);
int argc = 5;
char *argv[] = {
"simplex",
"+RTS", // requires `hs_init_with_rtsopts`
"-A16m", // chunk size for new allocations
"-H64m", // initial heap size
"-xn", // non-moving GC
NULL
};
char **pargv = argv;
hs_init_with_rtsopts(&argc, &pargv);
setLineBuffering();
}
@@ -4,11 +4,14 @@
#include <stdint.h>
// from the RTS
void hs_init(int * argc, char **argv[]);
void hs_init_with_rtsopts(int * argc, char **argv[]);
JNIEXPORT void JNICALL
Java_chat_simplex_common_platform_CoreKt_initHS(JNIEnv *env, jclass clazz) {
hs_init(NULL, NULL);
int argc = 5;
char *argv[] = {"simplex", "+RTS", "-A16m", "-H64m", "-xn", NULL}; // see android/simplex-api.c for details
char **pargv = argv;
hs_init_with_rtsopts(&argc, &pargv);
}
// from simplex-chat
@@ -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)
@@ -1252,7 +1253,7 @@ data class GroupMember (
fun canChangeRoleTo(groupInfo: GroupInfo): List<GroupMemberRole>? =
if (!canBeRemoved(groupInfo)) null
else groupInfo.membership.memberRole.let { userRole ->
GroupMemberRole.values().filter { it <= userRole }
GroupMemberRole.values().filter { it <= userRole && it != GroupMemberRole.Author }
}
val memberIncognito = memberProfile.profileId != memberContactProfileId
@@ -1294,12 +1295,14 @@ data class GroupMemberIds(
@Serializable
enum class GroupMemberRole(val memberRole: String) {
@SerialName("observer") Observer("observer"), // order matters in comparisons
@SerialName("author") Author("author"),
@SerialName("member") Member("member"),
@SerialName("admin") Admin("admin"),
@SerialName("owner") Owner("owner");
val text: String get() = when (this) {
Observer -> generalGetString(MR.strings.group_member_role_observer)
Author -> generalGetString(MR.strings.group_member_role_author)
Member -> generalGetString(MR.strings.group_member_role_member)
Admin -> generalGetString(MR.strings.group_member_role_admin)
Owner -> generalGetString(MR.strings.group_member_role_owner)
@@ -4,7 +4,6 @@ import chat.simplex.common.views.helpers.*
import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import chat.simplex.common.model.ChatModel.remoteHostId
import chat.simplex.common.model.ChatModel.updatingChatsMutex
import dev.icerock.moko.resources.compose.painterResource
import chat.simplex.common.platform.*
@@ -1393,10 +1392,10 @@ object ChatController {
chatModel.remoteHosts.addAll(hosts)
}
suspend fun startRemoteHost(rhId: Long?, multicast: Boolean = false): Pair<RemoteHostInfo?, String>? {
suspend fun startRemoteHost(rhId: Long?, multicast: Boolean = false): Triple<RemoteHostInfo?, String, String>? {
val r = sendCmd(null, CC.StartRemoteHost(rhId, multicast))
if (r is CR.RemoteHostStarted) return r.remoteHost_ to r.invitation
apiErrorAlert("listRemoteHosts", generalGetString(MR.strings.error_alert_title), r)
if (r is CR.RemoteHostStarted) return Triple(r.remoteHost_, r.invitation, r.ctrlPort)
apiErrorAlert("startRemoteHost", generalGetString(MR.strings.error_alert_title), r)
return null
}
@@ -1626,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)
}
}
@@ -1845,8 +1844,14 @@ object ChatController {
switchUIRemoteHost(r.remoteHost.remoteHostId)
}
is CR.RemoteHostStopped -> {
val disconnectedHost = chatModel.remoteHosts.firstOrNull { it.remoteHostId == r.remoteHostId_ }
chatModel.newRemoteHostPairing.value = null
if (chatModel.currentRemoteHost.value != null) {
if (disconnectedHost != null) {
showToast(
generalGetString(MR.strings.remote_host_was_disconnected_toast).format(disconnectedHost.hostDeviceName.ifEmpty { disconnectedHost.remoteHostId.toString() })
)
}
if (chatModel.remoteHostId() == r.remoteHostId_) {
chatModel.currentRemoteHost.value = null
switchUIRemoteHost(null)
}
@@ -1908,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
@@ -1968,6 +1973,9 @@ object ChatController {
suspend fun switchUIRemoteHost(rhId: Long?) {
// TODO lock the switch so that two switches can't run concurrently?
chatModel.chatId.value = null
ModalManager.center.closeModals()
ModalManager.end.closeModals()
AlertManager.shared.alertViews.clear()
chatModel.currentRemoteHost.value = switchRemoteHost(rhId)
reloadRemoteHosts()
val user = apiGetActiveUser(rhId)
@@ -3766,7 +3774,7 @@ sealed class CR {
// remote events (desktop)
@Serializable @SerialName("remoteHostList") class RemoteHostList(val remoteHosts: List<RemoteHostInfo>): CR()
@Serializable @SerialName("currentRemoteHost") class CurrentRemoteHost(val remoteHost_: RemoteHostInfo?): CR()
@Serializable @SerialName("remoteHostStarted") class RemoteHostStarted(val remoteHost_: RemoteHostInfo?, val invitation: String): CR()
@Serializable @SerialName("remoteHostStarted") class RemoteHostStarted(val remoteHost_: RemoteHostInfo?, val invitation: String, val ctrlPort: String): CR()
@Serializable @SerialName("remoteHostSessionCode") class RemoteHostSessionCode(val remoteHost_: RemoteHostInfo?, val sessionCode: String): CR()
@Serializable @SerialName("newRemoteHost") class NewRemoteHost(val remoteHost: RemoteHostInfo): CR()
@Serializable @SerialName("remoteHostConnected") class RemoteHostConnected(val remoteHost: RemoteHostInfo): CR()
@@ -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"
}
@@ -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
@@ -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()
@@ -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) {
@@ -205,7 +205,9 @@ private fun RoleSelectionRow(groupInfo: GroupInfo, selectedRole: MutableState<Gr
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
val values = GroupMemberRole.values().filter { it <= groupInfo.membership.memberRole }.map { it to it.text }
val values = GroupMemberRole.values()
.filter { it <= groupInfo.membership.memberRole && it != GroupMemberRole.Author }
.map { it to it.text }
ExposedDropDownSettingRow(
generalGetString(MR.strings.new_member_role),
values,
@@ -129,7 +129,7 @@ fun directChatAction(rhId: Long?, contact: Contact, chatModel: ChatModel) {
fun groupChatAction(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, inProgress: MutableState<Boolean>? = null) {
when (groupInfo.membership.memberStatus) {
GroupMemberStatus.MemInvited -> acceptGroupInvitationAlertDialog(rhId, groupInfo, chatModel, inProgress)
GroupMemberStatus.MemAccepted -> groupInvitationAcceptedAlert()
GroupMemberStatus.MemAccepted -> groupInvitationAcceptedAlert(rhId)
else -> withBGApi { openChat(rhId, ChatInfo.Group(groupInfo), chatModel) }
}
}
@@ -538,7 +538,8 @@ fun contactRequestAlertDialog(rhId: Long?, contactRequest: ChatInfo.ContactReque
Text(generalGetString(MR.strings.reject_contact_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red)
}
}
}
},
hostDevice = hostDevice(rhId),
)
}
@@ -644,7 +645,8 @@ fun askCurrentOrIncognitoProfileConnectContactViaAddress(
Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
}
}
},
hostDevice = hostDevice(rhId),
)
}
@@ -654,7 +656,8 @@ suspend fun connectContactViaAddress(chatModel: ChatModel, rhId: Long?, contactI
chatModel.updateContact(rhId, contact)
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.connection_request_sent),
text = generalGetString(MR.strings.you_will_be_connected_when_your_connection_request_is_accepted)
text = generalGetString(MR.strings.you_will_be_connected_when_your_connection_request_is_accepted),
hostDevice = hostDevice(rhId),
)
return true
}
@@ -674,7 +677,8 @@ fun acceptGroupInvitationAlertDialog(rhId: Long?, groupInfo: GroupInfo, chatMode
}
},
dismissText = generalGetString(MR.strings.delete_verb),
onDismiss = { deleteGroup(rhId, groupInfo, chatModel) }
onDismiss = { deleteGroup(rhId, groupInfo, chatModel) },
hostDevice = hostDevice(rhId),
)
}
@@ -700,10 +704,11 @@ fun deleteGroup(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) {
}
}
fun groupInvitationAcceptedAlert() {
fun groupInvitationAcceptedAlert(rhId: Long?) {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.joining_group),
generalGetString(MR.strings.youve_accepted_group_invitation_connecting_to_inviting_group_member)
generalGetString(MR.strings.youve_accepted_group_invitation_connecting_to_inviting_group_member),
hostDevice = hostDevice(rhId),
)
}
@@ -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) {
@@ -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) {
@@ -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) {
@@ -1,6 +1,5 @@
package chat.simplex.common.views.helpers
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -14,10 +13,12 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.*
import chat.simplex.common.model.ChatModel
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.StringResource
import dev.icerock.moko.resources.compose.painterResource
class AlertManager {
var alertViews = mutableStateListOf<(@Composable () -> Unit)>()
@@ -40,8 +41,11 @@ class AlertManager {
AlertDialog(
onDismissRequest = this::hideAlert,
title = alertTitle(title),
text = alertText(text),
buttons = buttons,
buttons = {
AlertContent(text, null, extraPadding = true) {
buttons()
}
},
shape = RoundedCornerShape(corner = CornerSize(25.dp))
)
}
@@ -51,30 +55,16 @@ class AlertManager {
title: String,
text: AnnotatedString? = null,
onDismissRequest: (() -> Unit)? = null,
hostDevice: Pair<Long?, String>? = null,
buttons: @Composable () -> Unit,
) {
showAlert {
AlertDialog(
onDismissRequest = { onDismissRequest?.invoke(); hideAlert() },
title = {
Text(
title,
Modifier.fillMaxWidth().padding(vertical = DEFAULT_PADDING),
textAlign = TextAlign.Center,
fontSize = 20.sp
)
},
title = alertTitle(title),
buttons = {
Column(
Modifier
.padding(bottom = DEFAULT_PADDING)
) {
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) {
if (text != null) {
Text(text, Modifier.fillMaxWidth().padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING * 1.5f), fontSize = 16.sp, textAlign = TextAlign.Center, color = MaterialTheme.colors.secondary)
}
buttons()
}
AlertContent(text, hostDevice, extraPadding = true) {
buttons()
}
},
shape = RoundedCornerShape(corner = CornerSize(25.dp))
@@ -90,30 +80,32 @@ class AlertManager {
dismissText: String = generalGetString(MR.strings.cancel_verb),
onDismiss: (() -> Unit)? = null,
onDismissRequest: (() -> Unit)? = null,
destructive: Boolean = false
destructive: Boolean = false,
hostDevice: Pair<Long?, String>? = null,
) {
showAlert {
AlertDialog(
onDismissRequest = { onDismissRequest?.invoke(); hideAlert() },
title = alertTitle(title),
text = alertText(text),
buttons = {
Row (
Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING_HALF),
horizontalArrangement = Arrangement.SpaceBetween
) {
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
focusRequester.requestFocus()
AlertContent(text, hostDevice, true) {
Row(
Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING),
horizontalArrangement = Arrangement.SpaceBetween
) {
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
TextButton(onClick = {
onDismiss?.invoke()
hideAlert()
}) { Text(dismissText) }
TextButton(onClick = {
onConfirm?.invoke()
hideAlert()
}, Modifier.focusRequester(focusRequester)) { Text(confirmText, color = if (destructive) MaterialTheme.colors.error else Color.Unspecified) }
}
TextButton(onClick = {
onDismiss?.invoke()
hideAlert()
}) { Text(dismissText) }
TextButton(onClick = {
onConfirm?.invoke()
hideAlert()
}, Modifier.focusRequester(focusRequester)) { Text(confirmText, color = if (destructive) MaterialTheme.colors.error else Color.Unspecified) }
}
},
shape = RoundedCornerShape(corner = CornerSize(25.dp))
@@ -135,20 +127,21 @@ class AlertManager {
AlertDialog(
onDismissRequest = { onDismissRequest?.invoke(); hideAlert() },
title = alertTitle(title),
text = alertText(text),
buttons = {
Column(
Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING_HALF).padding(top = DEFAULT_PADDING, bottom = 2.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
TextButton(onClick = {
onDismiss?.invoke()
hideAlert()
}) { Text(dismissText) }
TextButton(onClick = {
onConfirm?.invoke()
hideAlert()
}) { Text(confirmText, color = if (destructive) Color.Red else Color.Unspecified, textAlign = TextAlign.End) }
AlertContent(text, null) {
Column(
Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING_HALF).padding(top = DEFAULT_PADDING, bottom = 2.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
TextButton(onClick = {
onDismiss?.invoke()
hideAlert()
}) { Text(dismissText) }
TextButton(onClick = {
onConfirm?.invoke()
hideAlert()
}) { Text(confirmText, color = if (destructive) Color.Red else Color.Unspecified, textAlign = TextAlign.End) }
}
}
},
shape = RoundedCornerShape(corner = CornerSize(25.dp))
@@ -158,29 +151,31 @@ class AlertManager {
fun showAlertMsg(
title: String, text: String? = null,
confirmText: String = generalGetString(MR.strings.ok)
confirmText: String = generalGetString(MR.strings.ok),
hostDevice: Pair<Long?, String>? = null,
) {
showAlert {
AlertDialog(
onDismissRequest = this::hideAlert,
title = alertTitle(title),
text = alertText(text),
buttons = {
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
Row(
Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING_HALF),
horizontalArrangement = Arrangement.Center
) {
TextButton(
onClick = {
hideAlert()
},
Modifier.focusRequester(focusRequester)
AlertContent(text, hostDevice, extraPadding = true) {
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
Row(
Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING),
horizontalArrangement = Arrangement.Center
) {
Text(confirmText, color = Color.Unspecified)
TextButton(
onClick = {
hideAlert()
},
Modifier.focusRequester(focusRequester)
) {
Text(confirmText, color = Color.Unspecified)
}
}
}
},
@@ -191,16 +186,17 @@ class AlertManager {
fun showAlertMsgWithProgress(
title: String,
text: String? = null
text: String? = null,
) {
showAlert {
AlertDialog(
onDismissRequest = this::hideAlert,
title = alertTitle(title),
text = alertText(text),
buttons = {
Box(Modifier.fillMaxWidth().height(72.dp).padding(bottom = DEFAULT_PADDING * 2), contentAlignment = Alignment.Center) {
CircularProgressIndicator(Modifier.size(36.dp).padding(4.dp), color = MaterialTheme.colors.secondary, strokeWidth = 3.dp)
AlertContent(text, null) {
Box(Modifier.fillMaxWidth().height(72.dp).padding(bottom = DEFAULT_PADDING * 2), contentAlignment = Alignment.Center) {
CircularProgressIndicator(Modifier.size(36.dp).padding(4.dp), color = MaterialTheme.colors.secondary, strokeWidth = 3.dp)
}
}
}
)
@@ -211,7 +207,8 @@ class AlertManager {
title: StringResource,
text: StringResource? = null,
confirmText: StringResource = MR.strings.ok,
) = showAlertMsg(generalGetString(title), if (text != null) generalGetString(text) else null, generalGetString(confirmText))
hostDevice: Pair<Long?, String>? = null,
) = showAlertMsg(generalGetString(title), if (text != null) generalGetString(text) else null, generalGetString(confirmText), hostDevice)
@Composable
fun showInView() {
@@ -234,18 +231,75 @@ private fun alertTitle(title: String): (@Composable () -> Unit)? {
}
}
private fun alertText(text: String?): (@Composable () -> Unit)? {
return if (text == null) {
null
} else {
({
Text(
escapedHtmlToAnnotatedString(text, LocalDensity.current),
Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
fontSize = 16.sp,
color = MaterialTheme.colors.secondary
)
})
@Composable
private fun AlertContent(text: String?, hostDevice: Pair<Long?, String>?, extraPadding: Boolean = false, content: @Composable (() -> Unit)) {
Column(
Modifier
.padding(bottom = if (appPlatform.isDesktop) DEFAULT_PADDING else DEFAULT_PADDING_HALF)
) {
if (appPlatform.isDesktop) {
HostDeviceTitle(hostDevice, extraPadding = extraPadding)
} else {
Spacer(Modifier.size(DEFAULT_PADDING_HALF))
}
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) {
if (text != null) {
Text(
escapedHtmlToAnnotatedString(text, LocalDensity.current),
Modifier.fillMaxWidth().padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING * 1.5f),
fontSize = 16.sp,
textAlign = TextAlign.Center,
color = MaterialTheme.colors.secondary
)
}
}
content()
}
}
@Composable
private fun AlertContent(text: AnnotatedString?, hostDevice: Pair<Long?, String>?, extraPadding: Boolean = false, content: @Composable (() -> Unit)) {
Column(
Modifier
.padding(bottom = if (appPlatform.isDesktop) DEFAULT_PADDING else DEFAULT_PADDING_HALF)
) {
if (appPlatform.isDesktop) {
HostDeviceTitle(hostDevice, extraPadding = extraPadding)
} else {
Spacer(Modifier.size(DEFAULT_PADDING_HALF))
}
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) {
if (text != null) {
Text(
text,
Modifier.fillMaxWidth().padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING * 1.5f),
fontSize = 16.sp,
textAlign = TextAlign.Center,
color = MaterialTheme.colors.secondary
)
}
}
content()
}
}
fun hostDevice(rhId: Long?): Pair<Long?, String>? = if (rhId == null && chatModel.remoteHosts.isNotEmpty()) {
null to ChatModel.controller.appPrefs.deviceNameForRemoteAccess.get()!!
} else if (rhId == null) {
null
} else {
rhId to (chatModel.remoteHosts.firstOrNull { it.remoteHostId == rhId }?.hostDeviceName?.ifEmpty { rhId.toString() } ?: rhId.toString())
}
@Composable
private fun HostDeviceTitle(hostDevice: Pair<Long?, String>?, extraPadding: Boolean = false) {
if (hostDevice != null) {
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)
}
} else {
Spacer(Modifier.height(if (extraPadding) DEFAULT_PADDING * 2 else 0.dp))
}
}
@@ -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/*(
@@ -56,7 +56,7 @@ fun annotatedStringResource(id: StringResource): AnnotatedString {
fun annotatedStringResource(id: StringResource, vararg args: Any?): AnnotatedString {
val density = LocalDensity.current
return remember(id) {
escapedHtmlToAnnotatedString(id.localized().format(args), density)
escapedHtmlToAnnotatedString(id.localized().format(args = args), density)
}
}
@@ -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)
}
}
@@ -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()) {
@@ -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,
)
}
}
@@ -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 = {},
@@ -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(
@@ -4,7 +4,6 @@ import SectionBottomSpacer
import SectionItemView
import SectionTextFooter
import androidx.compose.desktop.ui.tooling.preview.Preview
import chat.simplex.common.platform.Log
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
@@ -17,7 +16,7 @@ import androidx.compose.ui.text.style.TextAlign
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.unit.dp
import chat.simplex.common.model.*
import chat.simplex.common.platform.TAG
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chatlist.*
import chat.simplex.common.views.helpers.*
@@ -65,6 +64,7 @@ suspend fun planAndConnect(
confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb),
onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) } },
destructive = true,
hostDevice = hostDevice(rhId),
)
} else {
askCurrentOrIncognitoProfileAlert(
@@ -82,12 +82,14 @@ suspend fun planAndConnect(
openKnownContact(chatModel, rhId, close, contact)
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.contact_already_exists),
String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), contact.displayName)
String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), contact.displayName),
hostDevice = hostDevice(rhId),
)
} else {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.connect_plan_already_connecting),
generalGetString(MR.strings.connect_plan_you_are_already_connecting_via_this_one_time_link)
generalGetString(MR.strings.connect_plan_you_are_already_connecting_via_this_one_time_link),
hostDevice = hostDevice(rhId),
)
}
}
@@ -97,7 +99,8 @@ suspend fun planAndConnect(
openKnownContact(chatModel, rhId, close, contact)
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.contact_already_exists),
String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName)
String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName),
hostDevice = hostDevice(rhId),
)
}
}
@@ -124,6 +127,7 @@ suspend fun planAndConnect(
confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb),
onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) } },
destructive = true,
hostDevice = hostDevice(rhId),
)
} else {
askCurrentOrIncognitoProfileAlert(
@@ -143,6 +147,7 @@ suspend fun planAndConnect(
confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb),
onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) } },
destructive = true,
hostDevice = hostDevice(rhId),
)
} else {
askCurrentOrIncognitoProfileAlert(
@@ -159,7 +164,8 @@ suspend fun planAndConnect(
openKnownContact(chatModel, rhId, close, contact)
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.contact_already_exists),
String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), contact.displayName)
String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), contact.displayName),
hostDevice = hostDevice(rhId),
)
}
is ContactAddressPlan.Known -> {
@@ -168,7 +174,8 @@ suspend fun planAndConnect(
openKnownContact(chatModel, rhId, close, contact)
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.contact_already_exists),
String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName)
String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName),
hostDevice = hostDevice(rhId),
)
}
is ContactAddressPlan.ContactViaAddress -> {
@@ -190,7 +197,8 @@ suspend fun planAndConnect(
title = generalGetString(MR.strings.connect_via_group_link),
text = generalGetString(MR.strings.you_will_join_group),
confirmText = if (incognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button),
onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) } }
onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) } },
hostDevice = hostDevice(rhId),
)
} else {
askCurrentOrIncognitoProfileAlert(
@@ -215,6 +223,7 @@ suspend fun planAndConnect(
confirmText = if (incognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button),
onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) } },
destructive = true,
hostDevice = hostDevice(rhId),
)
} else {
askCurrentOrIncognitoProfileAlert(
@@ -236,7 +245,8 @@ suspend fun planAndConnect(
} else {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.connect_plan_already_joining_the_group),
generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link)
generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link),
hostDevice = hostDevice(rhId),
)
}
}
@@ -246,7 +256,8 @@ suspend fun planAndConnect(
openKnownGroup(chatModel, rhId, close, groupInfo)
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.connect_plan_group_already_exists),
String.format(generalGetString(MR.strings.connect_plan_you_are_already_in_group_vName), groupInfo.displayName)
String.format(generalGetString(MR.strings.connect_plan_you_are_already_in_group_vName), groupInfo.displayName),
hostDevice = hostDevice(rhId),
)
}
}
@@ -284,7 +295,8 @@ suspend fun connectViaUri(
ConnectionLinkType.CONTACT -> generalGetString(MR.strings.you_will_be_connected_when_your_connection_request_is_accepted)
ConnectionLinkType.INVITATION -> generalGetString(MR.strings.you_will_be_connected_when_your_contacts_device_is_online)
ConnectionLinkType.GROUP -> generalGetString(MR.strings.you_will_be_connected_when_group_host_device_is_online)
}
},
hostDevice = hostDevice(rhId),
)
}
return r
@@ -336,7 +348,8 @@ fun askCurrentOrIncognitoProfileAlert(
Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
}
}
},
hostDevice = hostDevice(rhId),
)
}
@@ -411,7 +424,8 @@ fun ownGroupLinkConfirmConnect(
Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
}
}
},
hostDevice = hostDevice(rhId),
)
}
@@ -455,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()
@@ -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)
@@ -53,6 +53,7 @@ fun ConnectDesktopView(close: () -> Unit) {
ModalView(close = closeWithAlert) {
ConnectDesktopLayout(
deviceName = deviceName.value!!,
close
)
}
val ntfModeService = remember { chatModel.controller.appPrefs.notificationsMode.get() == NotificationsMode.SERVICE }
@@ -67,7 +68,7 @@ fun ConnectDesktopView(close: () -> Unit) {
}
@Composable
private fun ConnectDesktopLayout(deviceName: String) {
private fun ConnectDesktopLayout(deviceName: String, close: () -> Unit) {
val sessionAddress = remember { mutableStateOf("") }
val remoteCtrls = remember { mutableStateListOf<RemoteCtrlInfo>() }
val session = remember { chatModel.remoteCtrlSession }.value
@@ -89,7 +90,7 @@ private fun ConnectDesktopLayout(deviceName: String) {
}
}
is UIRemoteCtrlSessionState.Connected -> ActiveSession(session, session.sessionState.remoteCtrl)
is UIRemoteCtrlSessionState.Connected -> ActiveSession(session, session.sessionState.remoteCtrl, close)
}
} else {
ConnectDesktop(deviceName, remoteCtrls, sessionAddress)
@@ -205,7 +206,7 @@ private fun CtrlDeviceVersionText(session: RemoteCtrlSession) {
}
@Composable
private fun ActiveSession(session: RemoteCtrlSession, rc: RemoteCtrlInfo) {
private fun ActiveSession(session: RemoteCtrlSession, rc: RemoteCtrlInfo, close: () -> Unit) {
AppBarTitle(stringResource(MR.strings.connected_to_desktop))
SectionView(stringResource(MR.strings.connected_desktop).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
Text(rc.deviceViewName)
@@ -223,7 +224,7 @@ private fun ActiveSession(session: RemoteCtrlSession, rc: RemoteCtrlInfo) {
SectionSpacer()
SectionView {
DisconnectButton(::disconnectDesktop)
DisconnectButton { disconnectDesktop(close) }
}
}
@@ -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)
}
}
@@ -163,7 +161,8 @@ private fun ConnectMobileViewLayout(
title: String,
invitation: String?,
deviceName: String?,
sessionCode: String?
sessionCode: String?,
port: String?
) {
Column(
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
@@ -171,13 +170,14 @@ private fun ConnectMobileViewLayout(
) {
AppBarTitle(title)
SectionView {
if (invitation != null && sessionCode == null) {
if (invitation != null && sessionCode == null && port != null) {
QRCode(
invitation, Modifier
.padding(start = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING_HALF)
.aspectRatio(1f)
)
SectionTextFooter(annotatedStringResource(MR.strings.open_on_mobile_and_scan_qr_code))
SectionTextFooter(annotatedStringResource(MR.strings.waiting_for_mobile_to_connect_on_port, port))
if (remember { controller.appPrefs.developerTools.state }.value) {
val clipboard = LocalClipboardManager.current
@@ -234,6 +234,7 @@ fun connectMobileDevice(rh: RemoteHostInfo, connecting: MutableState<Boolean>) {
private fun showAddingMobileDevice(connecting: MutableState<Boolean>) {
ModalManager.start.showModalCloseable { close ->
val invitation = rememberSaveable { mutableStateOf<String?>(null) }
val port = rememberSaveable { mutableStateOf<String?>(null) }
val pairing = remember { chatModel.newRemoteHostPairing }
val sessionCode = when (val state = pairing.value?.second) {
is RemoteHostSessionState.PendingConfirmation -> state.sessionCode
@@ -249,7 +250,8 @@ private fun showAddingMobileDevice(connecting: MutableState<Boolean>) {
title = if (cachedSessionCode == null) stringResource(MR.strings.link_a_mobile) else stringResource(MR.strings.verify_connection),
invitation = invitation.value,
deviceName = remoteDeviceName,
sessionCode = cachedSessionCode
sessionCode = cachedSessionCode,
port = port.value
)
val oldRemoteHostId by remember { mutableStateOf(chatModel.currentRemoteHost.value?.remoteHostId) }
LaunchedEffect(remember { chatModel.currentRemoteHost }.value) {
@@ -268,6 +270,7 @@ private fun showAddingMobileDevice(connecting: MutableState<Boolean>) {
if (r != null) {
connecting.value = true
invitation.value = r.second
port.value = r.third
}
}
onDispose {
@@ -286,6 +289,7 @@ private fun showConnectMobileDevice(rh: RemoteHostInfo, connecting: MutableState
ModalManager.start.showModalCloseable { close ->
val pairing = remember { chatModel.newRemoteHostPairing }
val invitation = rememberSaveable { mutableStateOf<String?>(null) }
val port = rememberSaveable { mutableStateOf<String?>(null) }
val sessionCode = when (val state = pairing.value?.second) {
is RemoteHostSessionState.PendingConfirmation -> state.sessionCode
else -> null
@@ -300,6 +304,7 @@ private fun showConnectMobileDevice(rh: RemoteHostInfo, connecting: MutableState
invitation = invitation.value,
deviceName = pairing.value?.first?.hostDeviceName ?: rh.hostDeviceName,
sessionCode = cachedSessionCode,
port = port.value
)
var remoteHostId by rememberSaveable { mutableStateOf<Long?>(null) }
LaunchedEffect(Unit) {
@@ -309,6 +314,7 @@ private fun showConnectMobileDevice(rh: RemoteHostInfo, connecting: MutableState
connecting.value = true
remoteHostId = rh_?.remoteHostId
invitation.value = inv
port.value = r.third
}
}
LaunchedEffect(remember { chatModel.currentRemoteHost }.value) {
@@ -345,7 +351,8 @@ private fun showConnectedMobileDevice(rh: RemoteHostInfo, disconnectHost: () ->
title = stringResource(MR.strings.connected_to_mobile),
invitation = null,
deviceName = rh.hostDeviceName,
sessionCode = sessionCode
sessionCode = sessionCode,
port = null,
)
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
SectionItemView(disconnectHost) {
@@ -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()
}
}
@@ -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
@@ -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()
@@ -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)
}
@@ -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 = {},
)
@@ -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))
@@ -1178,6 +1178,7 @@
<!-- GroupMemberRole -->
<string name="group_member_role_observer">observer</string>
<string name="group_member_role_author">author</string>
<string name="group_member_role_member">member</string>
<string name="group_member_role_admin">admin</string>
<string name="group_member_role_owner">owner</string>
@@ -1608,16 +1609,6 @@
<string name="v5_3_simpler_incognito_mode_descr">Toggle incognito when connecting.</string>
<string name="v5_3_new_interface_languages">6 new interface languages</string>
<string name="v5_3_new_interface_languages_descr">Arabic, Bulgarian, Finnish, Hebrew, Thai and Ukrainian - thanks to the users and Weblate.</string>
<string name="v5_4_connect_desktop_mobile">Connect desktop and mobile!</string>
<string name="v5_4_connect_desktop_mobile_descr">Use your mobile app chat profile via desktop app.</string>
<string name="v5_4_group_improvements">Group improvements</string>
<string name="v5_4_group_improvements_descr">Create groups incognito, block group members, faster join via link, and more.</string>
<string name="v5_4_notify_contact_deletion">Notify about contact deletion</string>
<string name="v5_4_notify_contact_deletion_descr">You can optionally notify contacts when deleting them.</string>
<string name="v5_4_checking_simplex_links">Checking SimpleX links</string>
<string name="v5_4_checking_simplex_links_descr">Detection of previously used and your own SimpleX links.</string>
<string name="v5_4_spaces_in_profile_names">Spaces in profile names</string>
<string name="v5_4_spaces_in_profile_names_descr">You can now add spaces to your profile name.</string>
<string name="v5_4_link_mobile_desktop">Link mobile and desktop apps! 🔗</string>
<string name="v5_4_link_mobile_desktop_descr">Via secure quantum resistant protocol.</string>
<string name="v5_4_better_groups">Better groups</string>
@@ -1668,9 +1659,11 @@
<string name="unlink_desktop_question">Unlink desktop?</string>
<string name="unlink_desktop">Unlink</string>
<string name="disconnect_remote_host">Disconnect</string>
<string name="remote_host_was_disconnected_toast"><![CDATA[Mobile <b>%s</b> was disconnected]]></string>
<string name="disconnect_desktop_question">Disconnect desktop?</string>
<string name="only_one_device_can_work_at_the_same_time">Only one device can work at the same time</string>
<string name="open_on_mobile_and_scan_qr_code"><![CDATA[Open <i>Use from desktop</i> in mobile app and scan QR code]]></string>
<string name="open_on_mobile_and_scan_qr_code"><![CDATA[Open <i>Use from desktop</i> in mobile app and scan QR code.]]></string>
<string name="waiting_for_mobile_to_connect_on_port"><![CDATA[Waiting for mobile to connect on port <i>%s</i>]]></string>
<string name="bad_desktop_address">Bad desktop address</string>
<string name="desktop_incompatible_version">Incompatible version</string>
<string name="desktop_app_version_is_incompatible">Desktop app version %s is not compatible with this app.</string>
@@ -1465,7 +1465,7 @@
<string name="new_mobile_device">Nuovo dispositivo mobile</string>
<string name="desktop_address">Indirizzo desktop</string>
<string name="only_one_device_can_work_at_the_same_time">Solo un dispositivo può funzionare nello stesso momento</string>
<string name="open_on_mobile_and_scan_qr_code"><![CDATA[Apri <i>Usa dal desktop</i> nell\'app mobile e scansiona il codice QR]]></string>
<string name="open_on_mobile_and_scan_qr_code"><![CDATA[Apri <i>Usa dal desktop</i> nell\'app mobile e scansiona il codice QR.]]></string>
<string name="desktop_incompatible_version">Versione incompatibile</string>
<string name="new_desktop"><![CDATA[<i>(nuovo)</i>]]></string>
<string name="unlink_desktop_question">Scollegare il desktop?</string>
@@ -1463,7 +1463,7 @@
<string name="new_mobile_device">Nieuw mobiel apparaat</string>
<string name="desktop_address">Desktop adres</string>
<string name="only_one_device_can_work_at_the_same_time">Er kan slechts één apparaat tegelijkertijd werken</string>
<string name="open_on_mobile_and_scan_qr_code"><![CDATA[Open <i>Gebruik vanaf desktop</i> in de mobiele app en scan de QR-code]]></string>
<string name="open_on_mobile_and_scan_qr_code"><![CDATA[Open <i>Gebruik vanaf desktop</i> in de mobiele app en scan de QR-code.]]></string>
<string name="desktop_incompatible_version">Incompatibele versie</string>
<string name="new_desktop"><![CDATA[<i>(nieuw)</i>]]></string>
<string name="unlink_desktop_question">Desktop ontkoppelen?</string>
@@ -1465,7 +1465,7 @@
<string name="new_mobile_device">新移动设备</string>
<string name="desktop_address">桌面地址</string>
<string name="only_one_device_can_work_at_the_same_time">同一时刻只有一台设备可以工作</string>
<string name="open_on_mobile_and_scan_qr_code"><![CDATA[在移动应用中打开<i>从桌面使用</i>并扫描二维码]]></string>
<string name="open_on_mobile_and_scan_qr_code"><![CDATA[在移动应用中打开<i>从桌面使用</i>并扫描二维码.]]></string>
<string name="desktop_incompatible_version">不兼容的版本</string>
<string name="new_desktop"><![CDATA[<i>(新)</i>]]></string>
<string name="unlink_desktop_question">取消链接桌面端?</string>