Merge branch 'master' into master-ghc8107

This commit is contained in:
Evgeny Poberezkin
2023-10-18 22:44:27 +01:00
41 changed files with 703 additions and 305 deletions
@@ -53,6 +53,7 @@ object ChatModel {
// current chat
val chatId = mutableStateOf<String?>(null)
val chatItems = mutableStateListOf<ChatItem>()
val chatItemStatuses = mutableMapOf<Long, CIStatus>()
val groupMembers = mutableStateListOf<GroupMember>()
val terminalItems = mutableStateListOf<TerminalItem>()
@@ -135,6 +136,7 @@ object ChatModel {
fun hasChat(id: String): Boolean = chats.toList().firstOrNull { it.id == id } != null
fun getChat(id: String): Chat? = chats.toList().firstOrNull { it.id == id }
fun getContactChat(contactId: Long): Chat? = chats.toList().firstOrNull { it.chatInfo is ChatInfo.Direct && it.chatInfo.apiId == contactId }
fun getGroupChat(groupId: Long): Chat? = chats.toList().firstOrNull { it.chatInfo is ChatInfo.Group && it.chatInfo.apiId == groupId }
private fun getChatIndex(id: String): Int = chats.toList().indexOfFirst { it.id == id }
fun addChat(chat: Chat) = chats.add(index = 0, chat)
@@ -272,7 +274,13 @@ object ChatModel {
Log.d(TAG, "TODOCHAT: upsertChatItem: updated in chat $chatId from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
false
} else {
chatItems.add(cItem)
val status = chatItemStatuses.remove(cItem.id)
val ci = if (status != null && cItem.meta.itemStatus is CIStatus.SndNew) {
cItem.copy(meta = cItem.meta.copy(itemStatus = status))
} else {
cItem
}
chatItems.add(ci)
Log.d(TAG, "TODOCHAT: upsertChatItem: added to chat $chatId from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
true
}
@@ -282,13 +290,15 @@ object ChatModel {
}
}
suspend fun updateChatItem(cInfo: ChatInfo, cItem: ChatItem) {
suspend fun updateChatItem(cInfo: ChatInfo, cItem: ChatItem, status: CIStatus? = null) {
withContext(Dispatchers.Main) {
if (chatId.value == cInfo.id) {
val itemIndex = chatItems.indexOfFirst { it.id == cItem.id }
if (itemIndex >= 0) {
chatItems[itemIndex] = cItem
}
} else if (status != null) {
chatItemStatuses[cItem.id] = status
}
}
}
@@ -326,6 +336,7 @@ object ChatModel {
}
// clear current chat
if (chatId.value == cInfo.id) {
chatItemStatuses.clear()
chatItems.clear()
}
}
@@ -2416,7 +2427,7 @@ sealed class Format {
@Serializable @SerialName("secret") class Secret: Format()
@Serializable @SerialName("colored") class Colored(val color: FormatColor): Format()
@Serializable @SerialName("uri") class Uri: Format()
@Serializable @SerialName("simplexLink") class SimplexLink(val linkType: SimplexLinkType, val simplexUri: String, val trustedUri: Boolean, val smpHosts: List<String>): Format()
@Serializable @SerialName("simplexLink") class SimplexLink(val linkType: SimplexLinkType, val simplexUri: String, val smpHosts: List<String>): Format()
@Serializable @SerialName("email") class Email: Format()
@Serializable @SerialName("phone") class Phone: Format()
@@ -855,6 +855,14 @@ object ChatController {
return null
}
suspend fun apiConnectPlan(connReq: String): ConnectionPlan? {
val userId = kotlin.runCatching { currentUserId("apiConnectPlan") }.getOrElse { return null }
val r = sendCmd(CC.APIConnectPlan(userId, connReq))
if (r is CR.CRConnectionPlan) return r.connectionPlan
Log.e(TAG, "apiConnectPlan bad response: ${r.responseType} ${r.details}")
return null
}
suspend fun apiConnect(incognito: Boolean, connReq: String): Boolean {
val userId = chatModel.currentUser.value?.userId ?: run {
Log.e(TAG, "apiConnect: no current user")
@@ -1489,11 +1497,8 @@ object ChatController {
is CR.ChatItemStatusUpdated -> {
val cInfo = r.chatItem.chatInfo
val cItem = r.chatItem.chatItem
if (!cItem.isDeletedContent) {
val added = if (active(r.user)) chatModel.upsertChatItem(cInfo, cItem) else true
if (added && cItem.showNotification) {
ntfManager.notifyMessageReceived(r.user, cInfo, cItem)
}
if (!cItem.isDeletedContent && active(r.user)) {
chatModel.updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus)
}
}
is CR.ChatItemUpdated ->
@@ -1917,6 +1922,7 @@ sealed class CC {
class APIVerifyGroupMember(val groupId: Long, val groupMemberId: Long, val connectionCode: String?): CC()
class APIAddContact(val userId: Long, val incognito: Boolean): CC()
class ApiSetConnectionIncognito(val connId: Long, val incognito: Boolean): CC()
class APIConnectPlan(val userId: Long, val connReq: String): CC()
class APIConnect(val userId: Long, val incognito: Boolean, val connReq: String): CC()
class ApiDeleteChat(val type: ChatType, val id: Long): CC()
class ApiClearChat(val type: ChatType, val id: Long): CC()
@@ -2026,6 +2032,7 @@ sealed class CC {
is APIVerifyGroupMember -> "/_verify code #$groupId $groupMemberId" + if (connectionCode != null) " $connectionCode" else ""
is APIAddContact -> "/_connect $userId incognito=${onOff(incognito)}"
is ApiSetConnectionIncognito -> "/_set incognito :$connId ${onOff(incognito)}"
is APIConnectPlan -> "/_connect plan $userId $connReq"
is APIConnect -> "/_connect $userId incognito=${onOff(incognito)} $connReq"
is ApiDeleteChat -> "/_delete ${chatRef(type, id)}"
is ApiClearChat -> "/_clear chat ${chatRef(type, id)}"
@@ -2127,6 +2134,7 @@ sealed class CC {
is APIVerifyGroupMember -> "apiVerifyGroupMember"
is APIAddContact -> "apiAddContact"
is ApiSetConnectionIncognito -> "apiSetConnectionIncognito"
is APIConnectPlan -> "apiConnectPlan"
is APIConnect -> "apiConnect"
is ApiDeleteChat -> "apiDeleteChat"
is ApiClearChat -> "apiClearChat"
@@ -3340,6 +3348,7 @@ sealed class CR {
@Serializable @SerialName("connectionVerified") class ConnectionVerified(val user: UserRef, val verified: Boolean, val expectedCode: String): CR()
@Serializable @SerialName("invitation") class Invitation(val user: UserRef, val connReqInvitation: String, val connection: PendingContactConnection): CR()
@Serializable @SerialName("connectionIncognitoUpdated") class ConnectionIncognitoUpdated(val user: UserRef, val toConnection: PendingContactConnection): CR()
@Serializable @SerialName("connectionPlan") class CRConnectionPlan(val user: UserRef, val connectionPlan: ConnectionPlan): CR()
@Serializable @SerialName("sentConfirmation") class SentConfirmation(val user: UserRef): CR()
@Serializable @SerialName("sentInvitation") class SentInvitation(val user: UserRef): CR()
@Serializable @SerialName("contactAlreadyExists") class ContactAlreadyExists(val user: UserRef, val contact: Contact): CR()
@@ -3475,6 +3484,7 @@ sealed class CR {
is ConnectionVerified -> "connectionVerified"
is Invitation -> "invitation"
is ConnectionIncognitoUpdated -> "connectionIncognitoUpdated"
is CRConnectionPlan -> "connectionPlan"
is SentConfirmation -> "sentConfirmation"
is SentInvitation -> "sentInvitation"
is ContactAlreadyExists -> "contactAlreadyExists"
@@ -3605,6 +3615,7 @@ sealed class CR {
is ConnectionVerified -> withUser(user, "verified: $verified\nconnectionCode: $expectedCode")
is Invitation -> withUser(user, connReqInvitation)
is ConnectionIncognitoUpdated -> withUser(user, json.encodeToString(toConnection))
is CRConnectionPlan -> withUser(user, json.encodeToString(connectionPlan))
is SentConfirmation -> withUser(user, noDetails())
is SentInvitation -> withUser(user, noDetails())
is ContactAlreadyExists -> withUser(user, json.encodeToString(contact))
@@ -3718,6 +3729,39 @@ fun chatError(r: CR): ChatErrorType? {
)
}
@Serializable
sealed class ConnectionPlan {
@Serializable @SerialName("invitationLink") class InvitationLink(val invitationLinkPlan: InvitationLinkPlan): ConnectionPlan()
@Serializable @SerialName("contactAddress") class ContactAddress(val contactAddressPlan: ContactAddressPlan): ConnectionPlan()
@Serializable @SerialName("groupLink") class GroupLink(val groupLinkPlan: GroupLinkPlan): ConnectionPlan()
}
@Serializable
sealed class InvitationLinkPlan {
@Serializable @SerialName("ok") object Ok: InvitationLinkPlan()
@Serializable @SerialName("ownLink") object OwnLink: InvitationLinkPlan()
@Serializable @SerialName("connecting") class Connecting(val contact_: Contact? = null): InvitationLinkPlan()
@Serializable @SerialName("known") class Known(val contact: Contact): InvitationLinkPlan()
}
@Serializable
sealed class ContactAddressPlan {
@Serializable @SerialName("ok") object Ok: ContactAddressPlan()
@Serializable @SerialName("ownLink") object OwnLink: ContactAddressPlan()
@Serializable @SerialName("connectingConfirmReconnect") object ConnectingConfirmReconnect: ContactAddressPlan()
@Serializable @SerialName("connectingProhibit") class ConnectingProhibit(val contact: Contact): ContactAddressPlan()
@Serializable @SerialName("known") class Known(val contact: Contact): ContactAddressPlan()
}
@Serializable
sealed class GroupLinkPlan {
@Serializable @SerialName("ok") object Ok: GroupLinkPlan()
@Serializable @SerialName("ownLink") class OwnLink(val groupInfo: GroupInfo): GroupLinkPlan()
@Serializable @SerialName("connectingConfirmReconnect") object ConnectingConfirmReconnect: GroupLinkPlan()
@Serializable @SerialName("connectingProhibit") class ConnectingProhibit(val groupInfo_: GroupInfo? = null): GroupLinkPlan()
@Serializable @SerialName("known") class Known(val groupInfo: GroupInfo): GroupLinkPlan()
}
abstract class TerminalItem {
abstract val id: Long
val date: Instant = Clock.System.now()
@@ -3877,6 +3921,7 @@ sealed class ChatErrorType {
is ChatNotStarted -> "chatNotStarted"
is ChatNotStopped -> "chatNotStopped"
is ChatStoreChanged -> "chatStoreChanged"
is ConnectionPlanChatError -> "connectionPlan"
is InvalidConnReq -> "invalidConnReq"
is InvalidChatMessage -> "invalidChatMessage"
is ContactNotReady -> "contactNotReady"
@@ -3953,6 +3998,7 @@ sealed class ChatErrorType {
@Serializable @SerialName("chatNotStarted") object ChatNotStarted: ChatErrorType()
@Serializable @SerialName("chatNotStopped") object ChatNotStopped: ChatErrorType()
@Serializable @SerialName("chatStoreChanged") object ChatStoreChanged: ChatErrorType()
@Serializable @SerialName("connectionPlan") class ConnectionPlanChatError(val connectionPlan: ConnectionPlan): ChatErrorType()
@Serializable @SerialName("invalidConnReq") object InvalidConnReq: ChatErrorType()
@Serializable @SerialName("invalidChatMessage") class InvalidChatMessage(val connection: Connection, val message: String): ChatErrorType()
@Serializable @SerialName("contactNotReady") class ContactNotReady(val contact: Contact): ChatErrorType()
@@ -31,10 +31,10 @@ import androidx.compose.ui.unit.sp
import chat.simplex.common.model.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.newchat.QRCode
import chat.simplex.common.views.usersettings.*
import chat.simplex.common.platform.*
import chat.simplex.common.views.chatlist.updateChatSettings
import chat.simplex.common.views.newchat.*
import chat.simplex.res.MR
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
@@ -309,9 +309,9 @@ fun ChatInfoLayout(
if (contact.contactLink != null) {
SectionView(stringResource(MR.strings.address_section_title).uppercase()) {
QRCode(contact.contactLink, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f))
SimpleXLinkQRCode(contact.contactLink, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f))
val clipboard = LocalClipboardManager.current
ShareAddressButton { clipboard.shareText(contact.contactLink) }
ShareAddressButton { clipboard.shareText(simplexChatLink(contact.contactLink)) }
SectionTextFooter(stringResource(MR.strings.you_can_share_this_address_with_your_contacts).format(contact.displayName))
}
SectionDividerSpaced()
@@ -19,7 +19,7 @@ import chat.simplex.common.model.*
import chat.simplex.common.platform.shareText
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.newchat.QRCode
import chat.simplex.common.views.newchat.*
import chat.simplex.res.MR
@Composable
@@ -44,14 +44,12 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St
createLink()
}
}
val clipboard = LocalClipboardManager.current
GroupLinkLayout(
groupLink = groupLink,
groupInfo,
groupLinkMemberRole,
creatingLink,
createLink = ::createLink,
share = { clipboard.shareText(groupLink ?: return@GroupLinkLayout) },
updateLink = {
val role = groupLinkMemberRole.value
if (role != null) {
@@ -95,7 +93,6 @@ fun GroupLinkLayout(
groupLinkMemberRole: MutableState<GroupMemberRole?>,
creatingLink: Boolean,
createLink: () -> Unit,
share: () -> Unit,
updateLink: () -> Unit,
deleteLink: () -> Unit
) {
@@ -125,16 +122,17 @@ fun GroupLinkLayout(
}
initialLaunch = false
}
QRCode(groupLink, Modifier.aspectRatio(1f).padding(horizontal = DEFAULT_PADDING))
SimpleXLinkQRCode(groupLink, Modifier.aspectRatio(1f).padding(horizontal = DEFAULT_PADDING))
Row(
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = DEFAULT_PADDING, vertical = 10.dp)
) {
val clipboard = LocalClipboardManager.current
SimpleButton(
stringResource(MR.strings.share_link),
icon = painterResource(MR.images.ic_share),
click = share
click = { clipboard.shareText(simplexChatLink(groupLink)) }
)
SimpleButton(
stringResource(MR.strings.delete_link),
@@ -73,6 +73,7 @@ fun GroupMemberInfoView(
chatModel.addChat(c)
}
chatModel.chatItems.clear()
chatModel.chatItemStatuses.clear()
chatModel.chatItems.addAll(c.chatItems)
chatModel.chatId.value = c.id
closeAll()
@@ -283,9 +284,9 @@ fun GroupMemberInfoLayout(
if (member.contactLink != null) {
SectionView(stringResource(MR.strings.address_section_title).uppercase()) {
QRCode(member.contactLink, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f))
SimpleXLinkQRCode(member.contactLink, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f))
val clipboard = LocalClipboardManager.current
ShareAddressButton { clipboard.shareText(member.contactLink) }
ShareAddressButton { clipboard.shareText(simplexChatLink(member.contactLink)) }
if (contactId != null) {
if (knownDirectChat(contactId) == null && !groupInfo.fullGroupPreferences.directMessages.on) {
ConnectViaAddressButton(onClick = { connectViaAddress(member.contactLink) })
@@ -472,43 +473,17 @@ private fun updateMemberRoleDialog(
}
fun connectViaMemberAddressAlert(connReqUri: String) {
AlertManager.shared.showAlertDialogButtonsColumn(
title = generalGetString(MR.strings.connect_via_member_address_alert_title),
text = AnnotatedString(generalGetString(MR.strings.connect_via_member_address_alert_desc)),
buttons = {
Column {
SectionItemView({
AlertManager.shared.hideAlert()
val uri = URI(connReqUri)
withUriAction(uri) { linkType ->
withApi {
Log.d(TAG, "connectViaUri: connecting")
connectViaUri(chatModel, linkType, uri, incognito = false)
}
}
}) {
Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
SectionItemView({
AlertManager.shared.hideAlert()
val uri = URI(connReqUri)
withUriAction(uri) { linkType ->
withApi {
Log.d(TAG, "connectViaUri: connecting incognito")
connectViaUri(chatModel, linkType, uri, incognito = true)
}
}
}) {
Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
SectionItemView({
AlertManager.shared.hideAlert()
}) {
Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
}
try {
val uri = URI(connReqUri)
withApi {
planAndConnect(chatModel, uri, incognito = null, close = { ModalManager.closeAllModalsEverywhere() })
}
)
} catch (e: RuntimeException) {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.invalid_connection_link),
text = generalGetString(MR.strings.this_string_is_not_a_connection_link)
)
}
}
@Preview
@@ -103,12 +103,11 @@ private fun GroupPreferencesLayout(
FeatureSection(GroupFeature.Voice, allowVoice, groupInfo, preferences, onTTLUpdated) {
applyPrefs(preferences.copy(voice = GroupPreference(enable = it)))
}
// TODO uncomment in 5.3
// SectionDividerSpaced(true, maxBottomPadding = false)
// val allowFiles = remember(preferences) { mutableStateOf(preferences.files.enable) }
// FeatureSection(GroupFeature.Files, allowFiles, groupInfo, preferences, onTTLUpdated) {
// applyPrefs(preferences.copy(files = GroupPreference(enable = it)))
// }
SectionDividerSpaced(true, maxBottomPadding = false)
val allowFiles = remember(preferences) { mutableStateOf(preferences.files.enable) }
FeatureSection(GroupFeature.Files, allowFiles, groupInfo, preferences, onTTLUpdated) {
applyPrefs(preferences.copy(files = GroupPreference(enable = it)))
}
if (groupInfo.canEdit) {
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
ResetSaveButtons(
@@ -136,12 +136,8 @@ fun MarkdownText (
val link = ft.link(linkMode)
if (link != null) {
hasLinks = true
val ftStyle = if (ft.format is Format.SimplexLink && !ft.format.trustedUri && linkMode == SimplexLinkMode.BROWSER) {
SpanStyle(color = Color.Red, textDecoration = TextDecoration.Underline)
} else {
ft.format.style
}
withAnnotation(tag = if (ft.format is Format.SimplexLink && linkMode != SimplexLinkMode.BROWSER) "SIMPLEX_URL" else "URL", annotation = link) {
val ftStyle = ft.format.style
withAnnotation(tag = if (ft.format is Format.SimplexLink) "SIMPLEX_URL" else "URL", annotation = link) {
withStyle(ftStyle) { append(ft.viewText(linkMode)) }
}
} else {
@@ -130,6 +130,13 @@ suspend fun openDirectChat(contactId: Long, chatModel: ChatModel) {
}
}
suspend fun openGroupChat(groupId: Long, chatModel: ChatModel) {
val chat = chatModel.controller.apiGetChat(ChatType.Group, groupId)
if (chat != null) {
openChat(chat, chatModel)
}
}
suspend fun openChat(chatInfo: ChatInfo, chatModel: ChatModel) {
Log.d(TAG, "TODOCHAT: openChat: opening ${chatInfo.id}, current chatId ${ChatModel.chatId.value}, size ${ChatModel.chatItems.size}")
val chat = chatModel.controller.apiGetChat(chatInfo.chatType, chatInfo.apiId)
@@ -141,6 +148,7 @@ suspend fun openChat(chatInfo: ChatInfo, chatModel: ChatModel) {
suspend fun openChat(chat: Chat, chatModel: ChatModel) {
chatModel.chatItems.clear()
chatModel.chatItemStatuses.clear()
chatModel.chatItems.addAll(chat.chatItems)
chatModel.chatId.value = chat.chatInfo.id
}
@@ -316,46 +316,8 @@ fun connectIfOpenedViaUri(uri: URI, chatModel: ChatModel) {
if (chatModel.currentUser.value == null) {
chatModel.appOpenUrl.value = uri
} else {
withUriAction(uri) { linkType ->
val title = when (linkType) {
ConnectionLinkType.CONTACT -> generalGetString(MR.strings.connect_via_contact_link)
ConnectionLinkType.INVITATION -> generalGetString(MR.strings.connect_via_invitation_link)
ConnectionLinkType.GROUP -> generalGetString(MR.strings.connect_via_group_link)
}
AlertManager.shared.showAlertDialogButtonsColumn(
title = title,
text = if (linkType == ConnectionLinkType.GROUP)
AnnotatedString(generalGetString(MR.strings.you_will_join_group))
else
AnnotatedString(generalGetString(MR.strings.profile_will_be_sent_to_contact_sending_link)),
buttons = {
Column {
SectionItemView({
AlertManager.shared.hideAlert()
withApi {
Log.d(TAG, "connectIfOpenedViaUri: connecting")
connectViaUri(chatModel, linkType, uri, incognito = false)
}
}) {
Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
SectionItemView({
AlertManager.shared.hideAlert()
withApi {
Log.d(TAG, "connectIfOpenedViaUri: connecting incognito")
connectViaUri(chatModel, linkType, uri, incognito = true)
}
}) {
Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
SectionItemView({
AlertManager.shared.hideAlert()
}) {
Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
}
}
)
withApi {
planAndConnect(chatModel, uri, incognito = null, close = null)
}
}
}
@@ -34,7 +34,6 @@ fun AddContactView(
incognitoPref = chatModel.controller.appPrefs.incognito,
connReq = connReqInvitation,
contactConnection = contactConnection,
share = { clipboard.shareText(connReqInvitation) },
learnMore = {
ModalManager.center.showModal {
Column(
@@ -56,7 +55,6 @@ fun AddContactLayout(
incognitoPref: SharedPreference<Boolean>,
connReq: String,
contactConnection: MutableState<PendingContactConnection?>,
share: () -> Unit,
learnMore: () -> Unit
) {
val incognito = remember { mutableStateOf(incognitoPref.get()) }
@@ -82,7 +80,7 @@ fun AddContactLayout(
SectionView(stringResource(MR.strings.one_time_link_short).uppercase()) {
if (connReq.isNotEmpty()) {
QRCode(
SimpleXLinkQRCode(
connReq, Modifier
.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF)
.aspectRatio(1f)
@@ -99,7 +97,7 @@ fun AddContactLayout(
}
IncognitoToggle(incognitoPref, incognito) { ModalManager.start.showModal { IncognitoView() } }
ShareLinkButton(share)
ShareLinkButton(connReq)
OneTimeLinkLearnMoreButton(learnMore)
}
SectionTextFooter(sharedProfileInfo(chatModel, incognito.value))
@@ -109,11 +107,12 @@ fun AddContactLayout(
}
@Composable
fun ShareLinkButton(onClick: () -> Unit) {
fun ShareLinkButton(connReqInvitation: String) {
val clipboard = LocalClipboardManager.current
SettingsActionItem(
painterResource(MR.images.ic_share),
stringResource(MR.strings.share_invitation_link),
onClick,
click = { clipboard.shareText(simplexChatLink(connReqInvitation)) },
iconColor = MaterialTheme.colors.primary,
textColor = MaterialTheme.colors.primary,
)
@@ -177,7 +176,6 @@ fun PreviewAddContactView() {
incognitoPref = SharedPreference({ false }, {}),
connReq = "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D",
contactConnection = mutableStateOf(PendingContactConnection.getSampleData()),
share = {},
learnMore = {},
)
}
@@ -41,6 +41,7 @@ fun AddGroupView(chatModel: ChatModel, close: () -> Unit) {
if (groupInfo != null) {
chatModel.addChat(Chat(chatInfo = ChatInfo.Group(groupInfo), chatItems = listOf()))
chatModel.chatItems.clear()
chatModel.chatItemStatuses.clear()
chatModel.chatId.value = groupInfo.id
setGroupMembers(groupInfo, chatModel)
close.invoke()
@@ -131,13 +131,13 @@ private fun ContactConnectionInfoLayout(
SectionView {
if (!connReq.isNullOrEmpty() && contactConnection.initiated) {
QRCode(
SimpleXLinkQRCode(
connReq, Modifier
.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF)
.aspectRatio(1f)
)
incognitoEnabled()
ShareLinkButton(share)
ShareLinkButton(connReq)
OneTimeLinkLearnMoreButton(learnMore)
} else {
incognitoEnabled()
@@ -53,32 +53,14 @@ fun PasteToConnectLayout(
fun connectViaLink(connReqUri: String) {
try {
val uri = URI(connReqUri)
withUriAction(uri) { linkType ->
val action = suspend {
Log.d(TAG, "connectViaUri: connecting")
if (connectViaUri(chatModel, linkType, uri, incognito = incognito.value)) {
close()
}
}
if (linkType == ConnectionLinkType.GROUP) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.connect_via_group_link),
text = generalGetString(MR.strings.you_will_join_group),
confirmText = if (incognito.value) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb),
onConfirm = { withApi { action() } }
)
} else action()
withApi {
planAndConnect(chatModel, uri, incognito = incognito.value, close)
}
} catch (e: RuntimeException) {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.invalid_connection_link),
text = generalGetString(MR.strings.this_string_is_not_a_connection_link)
)
} catch (e: URISyntaxException) {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.invalid_connection_link),
text = generalGetString(MR.strings.this_string_is_not_a_connection_link)
)
}
}
@@ -115,6 +97,7 @@ fun PasteToConnectLayout(
painterResource(MR.images.ic_link),
stringResource(MR.strings.connect_button),
click = { connectViaLink(connectionLink.value) },
disabled = connectionLink.value.isEmpty() || connectionLink.value.trim().contains(" ")
)
IncognitoToggle(incognitoPref, incognito) { ModalManager.start.showModal { IncognitoView() } }
@@ -19,6 +19,29 @@ import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import kotlinx.coroutines.launch
@Composable
fun SimpleXLinkQRCode(
connReq: String,
modifier: Modifier = Modifier,
tintColor: Color = Color(0xff062d56),
withLogo: Boolean = true
) {
QRCode(
simplexChatLink(connReq),
modifier,
tintColor,
withLogo
)
}
fun simplexChatLink(uri: String): String {
return if (uri.startsWith("simplex:/")) {
uri.replace("simplex:/", "https://simplex.chat/")
} else {
uri
}
}
@Composable
fun QRCode(
connReq: String,
@@ -1,6 +1,7 @@
package chat.simplex.common.views.newchat
import SectionBottomSpacer
import SectionItemView
import SectionTextFooter
import androidx.compose.desktop.ui.tooling.preview.Preview
import chat.simplex.common.platform.Log
@@ -10,66 +11,265 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.buildAnnotatedString
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.ui.theme.*
import chat.simplex.common.views.chatlist.openDirectChat
import chat.simplex.common.views.chatlist.openGroupChat
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.usersettings.*
import chat.simplex.res.MR
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.net.URI
@Composable
expect fun ScanToConnectView(chatModel: ChatModel, close: () -> Unit)
enum class ConnectionLinkType {
CONTACT, INVITATION, GROUP
INVITATION, CONTACT, GROUP
}
@Serializable
sealed class CReqClientData {
@Serializable @SerialName("group") data class Group(val groupLinkId: String): CReqClientData()
}
fun withUriAction(uri: URI, run: suspend (ConnectionLinkType) -> Unit) {
val action = uri.path?.drop(1)?.replace("/", "")
val data = URI(uri.toString().replaceFirst("#/", "/")).getQueryParameter("data")
val type = when {
data != null -> {
val parsed = runCatching {
json.decodeFromString(CReqClientData.serializer(), data)
suspend fun planAndConnect(
chatModel: ChatModel,
uri: URI,
incognito: Boolean?,
close: (() -> Unit)?
) {
val connectionPlan = chatModel.controller.apiConnectPlan(uri.toString())
if (connectionPlan != null) {
when (connectionPlan) {
is ConnectionPlan.InvitationLink -> when (connectionPlan.invitationLinkPlan) {
InvitationLinkPlan.Ok -> {
Log.d(TAG, "planAndConnect, .InvitationLink, .Ok, incognito=$incognito")
if (incognito != null) {
connectViaUri(chatModel, uri, incognito, connectionPlan, close)
} else {
askCurrentOrIncognitoProfileAlert(
chatModel, uri, connectionPlan, close,
title = generalGetString(MR.strings.connect_via_invitation_link),
text = AnnotatedString(generalGetString(MR.strings.profile_will_be_sent_to_contact_sending_link)),
connectDestructive = false
)
}
}
InvitationLinkPlan.OwnLink -> {
Log.d(TAG, "planAndConnect, .InvitationLink, .OwnLink, incognito=$incognito")
if (incognito != null) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.connect_plan_connect_to_yourself),
text = generalGetString(MR.strings.connect_plan_this_is_your_own_one_time_link),
confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb),
onConfirm = { withApi { connectViaUri(chatModel, uri, incognito, connectionPlan, close) } },
destructive = true,
)
} else {
askCurrentOrIncognitoProfileAlert(
chatModel, uri, connectionPlan, close,
title = generalGetString(MR.strings.connect_plan_connect_to_yourself),
text = AnnotatedString(generalGetString(MR.strings.connect_plan_this_is_your_own_one_time_link)),
connectDestructive = true
)
}
}
is InvitationLinkPlan.Connecting -> {
Log.d(TAG, "planAndConnect, .InvitationLink, .Connecting, incognito=$incognito")
val contact = connectionPlan.invitationLinkPlan.contact_
if (contact != null) {
openKnownContact(chatModel, 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)
)
} 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)
)
}
}
is InvitationLinkPlan.Known -> {
Log.d(TAG, "planAndConnect, .InvitationLink, .Known, incognito=$incognito")
val contact = connectionPlan.invitationLinkPlan.contact
openKnownContact(chatModel, 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)
)
}
}
when {
parsed.getOrNull() is CReqClientData.Group -> ConnectionLinkType.GROUP
else -> null
is ConnectionPlan.ContactAddress -> when (connectionPlan.contactAddressPlan) {
ContactAddressPlan.Ok -> {
Log.d(TAG, "planAndConnect, .ContactAddress, .Ok, incognito=$incognito")
if (incognito != null) {
connectViaUri(chatModel, uri, incognito, connectionPlan, close)
} else {
askCurrentOrIncognitoProfileAlert(
chatModel, uri, connectionPlan, close,
title = generalGetString(MR.strings.connect_via_contact_link),
text = AnnotatedString(generalGetString(MR.strings.profile_will_be_sent_to_contact_sending_link)),
connectDestructive = false
)
}
}
ContactAddressPlan.OwnLink -> {
Log.d(TAG, "planAndConnect, .ContactAddress, .OwnLink, incognito=$incognito")
if (incognito != null) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.connect_plan_connect_to_yourself),
text = generalGetString(MR.strings.connect_plan_this_is_your_own_simplex_address),
confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb),
onConfirm = { withApi { connectViaUri(chatModel, uri, incognito, connectionPlan, close) } },
destructive = true,
)
} else {
askCurrentOrIncognitoProfileAlert(
chatModel, uri, connectionPlan, close,
title = generalGetString(MR.strings.connect_plan_connect_to_yourself),
text = AnnotatedString(generalGetString(MR.strings.connect_plan_this_is_your_own_simplex_address)),
connectDestructive = true
)
}
}
ContactAddressPlan.ConnectingConfirmReconnect -> {
Log.d(TAG, "planAndConnect, .ContactAddress, .ConnectingConfirmReconnect, incognito=$incognito")
if (incognito != null) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.connect_plan_repeat_connection_request),
text = generalGetString(MR.strings.connect_plan_you_have_already_requested_connection_via_this_address),
confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb),
onConfirm = { withApi { connectViaUri(chatModel, uri, incognito, connectionPlan, close) } },
destructive = true,
)
} else {
askCurrentOrIncognitoProfileAlert(
chatModel, uri, connectionPlan, close,
title = generalGetString(MR.strings.connect_plan_repeat_connection_request),
text = AnnotatedString(generalGetString(MR.strings.connect_plan_you_have_already_requested_connection_via_this_address)),
connectDestructive = true
)
}
}
is ContactAddressPlan.ConnectingProhibit -> {
Log.d(TAG, "planAndConnect, .ContactAddress, .ConnectingProhibit, incognito=$incognito")
val contact = connectionPlan.contactAddressPlan.contact
openKnownContact(chatModel, 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)
)
}
is ContactAddressPlan.Known -> {
Log.d(TAG, "planAndConnect, .ContactAddress, .Known, incognito=$incognito")
val contact = connectionPlan.contactAddressPlan.contact
openKnownContact(chatModel, 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)
)
}
}
is ConnectionPlan.GroupLink -> when (connectionPlan.groupLinkPlan) {
GroupLinkPlan.Ok -> {
Log.d(TAG, "planAndConnect, .GroupLink, .Ok, incognito=$incognito")
if (incognito != null) {
AlertManager.shared.showAlertDialog(
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, uri, incognito, connectionPlan, close) } }
)
} else {
askCurrentOrIncognitoProfileAlert(
chatModel, uri, connectionPlan, close,
title = generalGetString(MR.strings.connect_via_group_link),
text = AnnotatedString(generalGetString(MR.strings.you_will_join_group)),
connectDestructive = false
)
}
}
is GroupLinkPlan.OwnLink -> {
Log.d(TAG, "planAndConnect, .GroupLink, .OwnLink, incognito=$incognito")
val groupInfo = connectionPlan.groupLinkPlan.groupInfo
ownGroupLinkConfirmConnect(chatModel, uri, incognito, connectionPlan, groupInfo, close)
}
GroupLinkPlan.ConnectingConfirmReconnect -> {
Log.d(TAG, "planAndConnect, .GroupLink, .ConnectingConfirmReconnect, incognito=$incognito")
if (incognito != null) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.connect_plan_repeat_join_request),
text = generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link),
confirmText = if (incognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button),
onConfirm = { withApi { connectViaUri(chatModel, uri, incognito, connectionPlan, close) } },
destructive = true,
)
} else {
askCurrentOrIncognitoProfileAlert(
chatModel, uri, connectionPlan, close,
title = generalGetString(MR.strings.connect_plan_repeat_join_request),
text = AnnotatedString(generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link)),
connectDestructive = true
)
}
}
is GroupLinkPlan.ConnectingProhibit -> {
Log.d(TAG, "planAndConnect, .GroupLink, .ConnectingProhibit, incognito=$incognito")
val groupInfo = connectionPlan.groupLinkPlan.groupInfo_
if (groupInfo != null) {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.connect_plan_group_already_exists),
String.format(generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_vName), groupInfo.displayName)
)
} 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)
)
}
}
is GroupLinkPlan.Known -> {
Log.d(TAG, "planAndConnect, .GroupLink, .Known, incognito=$incognito")
val groupInfo = connectionPlan.groupLinkPlan.groupInfo
openKnownGroup(chatModel, 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)
)
}
}
}
action == "contact" -> ConnectionLinkType.CONTACT
action == "invitation" -> ConnectionLinkType.INVITATION
else -> null
}
if (type != null) {
withApi { run(type) }
} else {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.invalid_contact_link),
text = generalGetString(MR.strings.this_link_is_not_a_valid_connection_link)
)
Log.d(TAG, "planAndConnect, plan error")
if (incognito != null) {
connectViaUri(chatModel, uri, incognito, connectionPlan = null, close)
} else {
askCurrentOrIncognitoProfileAlert(
chatModel, uri, connectionPlan = null, close,
title = generalGetString(MR.strings.connect_plan_connect_via_link),
connectDestructive = false
)
}
}
}
suspend fun connectViaUri(chatModel: ChatModel, action: ConnectionLinkType, uri: URI, incognito: Boolean): Boolean {
suspend fun connectViaUri(
chatModel: ChatModel,
uri: URI,
incognito: Boolean,
connectionPlan: ConnectionPlan?,
close: (() -> Unit)?
): Boolean {
val r = chatModel.controller.apiConnect(incognito, uri.toString())
val connLinkType = if (connectionPlan != null) planToConnectionLinkType(connectionPlan) else ConnectionLinkType.INVITATION
if (r) {
close?.invoke()
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.connection_request_sent),
text =
when (action) {
when (connLinkType) {
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)
@@ -79,6 +279,139 @@ suspend fun connectViaUri(chatModel: ChatModel, action: ConnectionLinkType, uri:
return r
}
fun planToConnectionLinkType(connectionPlan: ConnectionPlan): ConnectionLinkType {
return when(connectionPlan) {
is ConnectionPlan.InvitationLink -> ConnectionLinkType.INVITATION
is ConnectionPlan.ContactAddress -> ConnectionLinkType.CONTACT
is ConnectionPlan.GroupLink -> ConnectionLinkType.GROUP
}
}
fun askCurrentOrIncognitoProfileAlert(
chatModel: ChatModel,
uri: URI,
connectionPlan: ConnectionPlan?,
close: (() -> Unit)?,
title: String,
text: AnnotatedString? = null,
connectDestructive: Boolean,
) {
AlertManager.shared.showAlertDialogButtonsColumn(
title = title,
text = text,
buttons = {
Column {
val connectColor = if (connectDestructive) MaterialTheme.colors.error else MaterialTheme.colors.primary
SectionItemView({
AlertManager.shared.hideAlert()
withApi {
connectViaUri(chatModel, uri, incognito = false, connectionPlan, close)
}
}) {
Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = connectColor)
}
SectionItemView({
AlertManager.shared.hideAlert()
withApi {
connectViaUri(chatModel, uri, incognito = true, connectionPlan, close)
}
}) {
Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = connectColor)
}
SectionItemView({
AlertManager.shared.hideAlert()
}) {
Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
}
}
)
}
fun openKnownContact(chatModel: ChatModel, close: (() -> Unit)?, contact: Contact) {
withApi {
val c = chatModel.getContactChat(contact.contactId)
if (c != null) {
close?.invoke()
openDirectChat(contact.contactId, chatModel)
}
}
}
fun ownGroupLinkConfirmConnect(
chatModel: ChatModel,
uri: URI,
incognito: Boolean?,
connectionPlan: ConnectionPlan?,
groupInfo: GroupInfo,
close: (() -> Unit)?,
) {
AlertManager.shared.showAlertDialogButtonsColumn(
title = generalGetString(MR.strings.connect_plan_join_your_group),
text = AnnotatedString(String.format(generalGetString(MR.strings.connect_plan_this_is_your_link_for_group_vName), groupInfo.displayName)),
buttons = {
Column {
// Open group
SectionItemView({
AlertManager.shared.hideAlert()
openKnownGroup(chatModel, close, groupInfo)
}) {
Text(generalGetString(MR.strings.connect_plan_open_group), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
if (incognito != null) {
// Join incognito / Join with current profile
SectionItemView({
AlertManager.shared.hideAlert()
withApi {
connectViaUri(chatModel, uri, incognito, connectionPlan, close)
}
}) {
Text(
if (incognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button),
Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error
)
}
} else {
// Use current profile
SectionItemView({
AlertManager.shared.hideAlert()
withApi {
connectViaUri(chatModel, uri, incognito = false, connectionPlan, close)
}
}) {
Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
}
// Use new incognito profile
SectionItemView({
AlertManager.shared.hideAlert()
withApi {
connectViaUri(chatModel, uri, incognito = true, connectionPlan, close)
}
}) {
Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
}
}
// Cancel
SectionItemView({
AlertManager.shared.hideAlert()
}) {
Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
}
}
)
}
fun openKnownGroup(chatModel: ChatModel, close: (() -> Unit)?, groupInfo: GroupInfo) {
withApi {
val g = chatModel.getGroupChat(groupInfo.groupId)
if (g != null) {
close?.invoke()
openGroupChat(groupInfo.groupId, chatModel)
}
}
}
@Composable
fun ConnectContactLayout(
chatModel: ChatModel,
@@ -92,21 +425,8 @@ fun ConnectContactLayout(
QRCodeScanner { connReqUri ->
try {
val uri = URI(connReqUri)
withUriAction(uri) { linkType ->
val action = suspend {
Log.d(TAG, "connectViaUri: connecting")
if (connectViaUri(ChatModel, linkType, uri, incognito = incognito.value)) {
close()
}
}
if (linkType == ConnectionLinkType.GROUP) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.connect_via_group_link),
text = generalGetString(MR.strings.you_will_join_group),
confirmText = if (incognito.value) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb),
onConfirm = { withApi { action() } }
)
} else action()
withApi {
planAndConnect(chatModel, uri, incognito = incognito.value, close)
}
} catch (e: RuntimeException) {
AlertManager.shared.showAlertMsg(
@@ -18,7 +18,8 @@ import chat.simplex.common.model.*
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.newchat.QRCode
import chat.simplex.common.views.newchat.SimpleXLinkQRCode
import chat.simplex.common.views.newchat.simplexChatLink
import chat.simplex.res.MR
@Composable
@@ -38,7 +39,7 @@ fun CreateSimpleXAddress(m: ChatModel) {
sendEmail = { address ->
uriHandler.sendEmail(
generalGetString(MR.strings.email_invite_subject),
generalGetString(MR.strings.email_invite_body).format(address.connReqContact)
generalGetString(MR.strings.email_invite_body).format(simplexChatLink(address.connReqContact))
)
},
createAddress = {
@@ -91,8 +92,8 @@ private fun CreateSimpleXAddressLayout(
Spacer(Modifier.weight(1f))
if (userAddress != null) {
QRCode(userAddress.connReqContact, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f))
ShareAddressButton { share(userAddress.connReqContact) }
SimpleXLinkQRCode(userAddress.connReqContact, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f))
ShareAddressButton { share(simplexChatLink(userAddress.connReqContact)) }
Spacer(Modifier.weight(1f))
ShareViaEmailButton { sendEmail(userAddress) }
Spacer(Modifier.weight(1f))
@@ -92,9 +92,6 @@ fun PrivacySettingsView(
chatModel.simplexLinkMode.value = it
})
}
if (chatModel.simplexLinkMode.value == SimplexLinkMode.BROWSER) {
SectionTextFooter(stringResource(MR.strings.simplex_link_mode_browser_warning))
}
SectionDividerSpaced()
val currentUser = chatModel.currentUser.value
@@ -185,8 +182,10 @@ fun PrivacySettingsView(
@Composable
private fun SimpleXLinkOptions(simplexLinkModeState: State<SimplexLinkMode>, onSelected: (SimplexLinkMode) -> Unit) {
val modeValues = listOf(SimplexLinkMode.DESCRIPTION, SimplexLinkMode.FULL)
val pickerValues = modeValues + if (modeValues.contains(simplexLinkModeState.value)) emptyList() else listOf(simplexLinkModeState.value)
val values = remember {
SimplexLinkMode.values().map {
pickerValues.map {
when (it) {
SimplexLinkMode.DESCRIPTION -> it to generalGetString(MR.strings.simplex_link_mode_description)
SimplexLinkMode.FULL -> it to generalGetString(MR.strings.simplex_link_mode_full)
@@ -24,10 +24,10 @@ import chat.simplex.common.model.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.ShareAddressButton
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.newchat.QRCode
import chat.simplex.common.model.ChatModel
import chat.simplex.common.model.MsgContent
import chat.simplex.common.platform.*
import chat.simplex.common.views.newchat.*
import chat.simplex.res.MR
@Composable
@@ -100,7 +100,7 @@ fun UserAddressView(
sendEmail = { userAddress ->
uriHandler.sendEmail(
generalGetString(MR.strings.email_invite_subject),
generalGetString(MR.strings.email_invite_body).format(userAddress.connReqContact)
generalGetString(MR.strings.email_invite_body).format(simplexChatLink( userAddress.connReqContact))
)
},
setProfileAddress = ::setProfileAddress,
@@ -201,8 +201,8 @@ private fun UserAddressLayout(
val autoAcceptState = remember { mutableStateOf(AutoAcceptState(userAddress)) }
val autoAcceptStateSaved = remember { mutableStateOf(autoAcceptState.value) }
SectionView(stringResource(MR.strings.address_section_title).uppercase()) {
QRCode(userAddress.connReqContact, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f))
ShareAddressButton { share(userAddress.connReqContact) }
SimpleXLinkQRCode(userAddress.connReqContact, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f))
ShareAddressButton { share(simplexChatLink(userAddress.connReqContact)) }
ShareViaEmailButton { sendEmail(userAddress) }
ShareWithContactsButton(shareViaProfile, setProfileAddress)
AutoAcceptToggle(autoAcceptState) { saveAas(autoAcceptState.value, autoAcceptStateSaved) }
@@ -4,13 +4,13 @@
<string name="thousand_abbreviation">k</string>
<!-- Connect via Link - MainActivity.kt -->
<string name="connect_via_contact_link">Connect via contact link?</string>
<string name="connect_via_invitation_link">Connect via invitation link?</string>
<string name="connect_via_group_link">Connect via group link?</string>
<string name="connect_via_contact_link">Connect via contact address?</string>
<string name="connect_via_invitation_link">Connect via one-time link?</string>
<string name="connect_via_group_link">Join group?</string>
<string name="connect_use_current_profile">Use current profile</string>
<string name="connect_use_new_incognito_profile">Use new incognito profile</string>
<string name="profile_will_be_sent_to_contact_sending_link">Your profile will be sent to the contact that you received this link from.</string>
<string name="you_will_join_group">You will join a group this link refers to and connect to its group members.</string>
<string name="you_will_join_group">You will connect to all group members.</string>
<string name="connect_via_link_verb">Connect</string>
<string name="connect_via_link_incognito">Connect incognito</string>
@@ -1600,4 +1600,25 @@
<!-- Under development -->
<string name="in_developing_title">Coming soon!</string>
<string name="in_developing_desc">This feature is not yet supported. Try the next release.</string>
<!-- Connection plan -->
<string name="connect_plan_connect_to_yourself">Connect to yourself?</string>
<string name="connect_plan_this_is_your_own_one_time_link">This is your own one-time link!</string>
<string name="connect_plan_you_are_already_connecting_to_vName">You are already connecting to %1$s.</string>
<string name="connect_plan_already_connecting">Already connecting!</string>
<string name="connect_plan_you_are_already_connecting_via_this_one_time_link">You are already connecting via this one-time link!</string>
<string name="connect_plan_this_is_your_own_simplex_address">This is your own SimpleX address!</string>
<string name="connect_plan_repeat_connection_request">Repeat connection request?</string>
<string name="connect_plan_you_have_already_requested_connection_via_this_address">You have already requested connection via this address!</string>
<string name="connect_plan_join_your_group">Join your group?</string>
<string name="connect_plan_this_is_your_link_for_group_vName">This is your link for group %1$s!</string>
<string name="connect_plan_open_group">Open group</string>
<string name="connect_plan_repeat_join_request">Repeat join request?</string>
<string name="connect_plan_you_are_already_joining_the_group_via_this_link">You are already joining the group via this link!</string>
<string name="connect_plan_group_already_exists">Group already exists!</string>
<string name="connect_plan_you_are_already_joining_the_group_vName">You are already joining the group %1$s.</string>
<string name="connect_plan_already_joining_the_group">Already joining the group!</string>
<string name="connect_plan_you_are_already_joining_the_group_via_this_link">You are already joining the group via this link.</string>
<string name="connect_plan_you_are_already_in_group_vName">You are already in group %1$s.</string>
<string name="connect_plan_connect_via_link">Connect via link?</string>
</resources>