From 15c8945fd3eb7937189d2e3a03b02bebe61b62dd Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Mon, 24 Oct 2022 18:45:12 +0300 Subject: [PATCH] android: Settings to auto-accept contact requests (#1250) * android: Settings to auto-accept contact requests * Link will not be recreated when not needed * Layout * Layout * Button rename Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- apps/android/.gitignore | 1 + .../simplex/app/views/helpers/ModalView.kt | 2 + .../simplex/app/views/helpers/SimpleButton.kt | 16 ++ .../app/views/newchat/AddContactView.kt | 10 +- .../newchat/ContactConnectionInfoView.kt | 13 +- .../app/views/newchat/CreateLinkView.kt | 22 ++- .../views/usersettings/AcceptRequestsView.kt | 167 ++++++++++++++++++ .../app/views/usersettings/SettingsView.kt | 30 ++++ .../app/views/usersettings/UserAddressView.kt | 40 +++-- .../app/src/main/res/values-de/strings.xml | 6 + .../app/src/main/res/values-ru/strings.xml | 6 + .../app/src/main/res/values/strings.xml | 6 + 12 files changed, 286 insertions(+), 33 deletions(-) create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/usersettings/AcceptRequestsView.kt diff --git a/apps/android/.gitignore b/apps/android/.gitignore index 644d967fb1..9ddf412fc2 100644 --- a/apps/android/.gitignore +++ b/apps/android/.gitignore @@ -10,6 +10,7 @@ /.idea/deploymentTargetDropDown.xml /.idea/misc.xml /.idea/uiDesigner.xml +/.idea/kotlinc.xml .DS_Store /build /captures diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ModalView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ModalView.kt index 9fc15c843d..1ad24b60fd 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ModalView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ModalView.kt @@ -60,6 +60,8 @@ class ModalManager { modalCount.value = modalViews.size - toRemove.size } + fun hasModalsOpen() = modalCount.value > 0 + fun closeModal() { if (modalViews.isNotEmpty()) { if (modalViews.lastOrNull()?.first == false) modalViews.removeAt(modalViews.lastIndex) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/SimpleButton.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/SimpleButton.kt index 392da7086f..d898126fef 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/SimpleButton.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/SimpleButton.kt @@ -28,6 +28,22 @@ fun SimpleButton(text: String, icon: ImageVector, } } +@Composable +fun SimpleButtonIconEnded( + text: String, + icon: ImageVector, + color: Color = MaterialTheme.colors.primary, + click: () -> Unit +) { + SimpleButtonFrame(click) { + Text(text, style = MaterialTheme.typography.caption, color = color) + Icon( + icon, text, tint = color, + modifier = Modifier.padding(start = 8.dp) + ) + } +} + @Composable fun SimpleButtonFrame(click: () -> Unit, disabled: Boolean = false, content: @Composable () -> Unit) { Surface(shape = RoundedCornerShape(20.dp)) { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddContactView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddContactView.kt index 8169e2b6fd..62b2752e18 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddContactView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddContactView.kt @@ -24,16 +24,8 @@ import chat.simplex.app.ui.theme.* import chat.simplex.app.views.helpers.* @Composable -fun AddContactView(chatModel: ChatModel, connReqInvitation: String, connIncognito: Boolean) { +fun AddContactView(connReqInvitation: String, connIncognito: Boolean) { val cxt = LocalContext.current - LaunchedEffect(connReqInvitation) { - if (connReqInvitation.isNotEmpty()) { - chatModel.connReqInv.value = connReqInvitation - } - } - DisposableEffect(Unit) { - onDispose { chatModel.connReqInv.value = null } - } AddContactLayout( connReq = connReqInvitation, connIncognito = connIncognito, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/ContactConnectionInfoView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/ContactConnectionInfoView.kt index 96acf498f6..dd6ec2b590 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/ContactConnectionInfoView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/ContactConnectionInfoView.kt @@ -30,16 +30,16 @@ fun ContactConnectionInfoView( focusAlias: Boolean, close: () -> Unit ) { - /** When [AddContactView] is open, we don't need to drop [chatModel.connReqInv]. It will be managed by [AddContactView] itself - * Otherwise it will be called here AFTER [AddContactView] is launched and will clear the value too soon */ - val allowDispose = remember { mutableStateOf(true) } LaunchedEffect(connReqInvitation) { - allowDispose.value = true chatModel.connReqInv.value = connReqInvitation } + /** When [AddContactView] is open, we don't need to drop [chatModel.connReqInv]. + * Otherwise, it will be called here AFTER [AddContactView] is launched and will clear the value too soon. + * It will be dropped automatically when connection established or when user goes away from this screen. + **/ DisposableEffect(Unit) { onDispose { - if (allowDispose.value) { + if (!ModalManager.shared.hasModalsOpen()) { chatModel.connReqInv.value = null } } @@ -54,7 +54,6 @@ fun ContactConnectionInfoView( deleteConnection = { deleteContactConnectionAlert(contactConnection, chatModel, close) }, onLocalAliasChanged = { setContactAlias(contactConnection, it, chatModel) }, showQr = { - allowDispose.value = false ModalManager.shared.showModal { Column( Modifier @@ -62,7 +61,7 @@ fun ContactConnectionInfoView( .padding(horizontal = DEFAULT_PADDING), verticalArrangement = Arrangement.SpaceBetween ) { - AddContactView(chatModel, connReqInvitation ?: return@showModal, contactConnection.incognito) + AddContactView(connReqInvitation ?: return@showModal, contactConnection.incognito) } } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/CreateLinkView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/CreateLinkView.kt index 2259b78104..5326e38bd5 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/CreateLinkView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/CreateLinkView.kt @@ -13,6 +13,7 @@ import chat.simplex.app.R import chat.simplex.app.model.ChatModel import chat.simplex.app.ui.theme.DEFAULT_PADDING import chat.simplex.app.ui.theme.HighOrLowlight +import chat.simplex.app.views.helpers.ModalManager import chat.simplex.app.views.helpers.withApi import chat.simplex.app.views.usersettings.UserAddressView @@ -23,16 +24,27 @@ enum class CreateLinkTab { @Composable fun CreateLinkView(m: ChatModel, initialSelection: CreateLinkTab) { val selection = remember { mutableStateOf(initialSelection) } - val connReqInvitation = rememberSaveable { mutableStateOf("") } + val connReqInvitation = rememberSaveable { m.connReqInv } val creatingConnReq = rememberSaveable { mutableStateOf(false) } LaunchedEffect(selection.value) { - if (selection.value == CreateLinkTab.ONE_TIME && connReqInvitation.value.isEmpty() && !creatingConnReq.value) { + if (selection.value == CreateLinkTab.ONE_TIME && connReqInvitation.value.isNullOrEmpty() && !creatingConnReq.value) { createInvitation(m, creatingConnReq, connReqInvitation) } } + /** When [AddContactView] is open, we don't need to drop [chatModel.connReqInv]. + * Otherwise, it will be called here AFTER [AddContactView] is launched and will clear the value too soon. + * It will be dropped automatically when connection established or when user goes away from this screen. + **/ + DisposableEffect(Unit) { + onDispose { + if (!ModalManager.shared.hasModalsOpen()) { + m.connReqInv.value = null + } + } + } val tabTitles = CreateLinkTab.values().map { when { - it == CreateLinkTab.ONE_TIME && connReqInvitation.value.isEmpty() -> stringResource(R.string.create_one_time_link) + it == CreateLinkTab.ONE_TIME && connReqInvitation.value.isNullOrEmpty() -> stringResource(R.string.create_one_time_link) it == CreateLinkTab.ONE_TIME -> stringResource(R.string.one_time_link) it == CreateLinkTab.LONG_TERM -> stringResource(R.string.your_contact_address) else -> "" @@ -47,7 +59,7 @@ fun CreateLinkView(m: ChatModel, initialSelection: CreateLinkTab) { Column(Modifier.weight(1f)) { when (selection.value) { CreateLinkTab.ONE_TIME -> { - AddContactView(m, connReqInvitation.value, m.incognito.value) + AddContactView(connReqInvitation.value ?: "", m.incognito.value) } CreateLinkTab.LONG_TERM -> { UserAddressView(m) @@ -80,7 +92,7 @@ fun CreateLinkView(m: ChatModel, initialSelection: CreateLinkTab) { } } -private fun createInvitation(m: ChatModel, creatingConnReq: MutableState, connReqInvitation: MutableState) { +private fun createInvitation(m: ChatModel, creatingConnReq: MutableState, connReqInvitation: MutableState) { creatingConnReq.value = true withApi { val connReq = m.controller.apiAddContact() diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/AcceptRequestsView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/AcceptRequestsView.kt new file mode 100644 index 0000000000..6404c02be1 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/AcceptRequestsView.kt @@ -0,0 +1,167 @@ +package chat.simplex.app.views.usersettings + +import SectionCustomFooter +import SectionDivider +import SectionItemView +import SectionView +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.TheaterComedy +import androidx.compose.material.icons.outlined.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import chat.simplex.app.R +import chat.simplex.app.model.* +import chat.simplex.app.ui.theme.* +import chat.simplex.app.views.helpers.* + +@Composable +fun AcceptRequestsView(m: ChatModel, contactLink: UserContactLinkRec) { + var contactLink by remember { mutableStateOf(contactLink) } + AcceptRequestsLayout( + contactLink, + saveState = { new: MutableState, old: MutableState -> + withApi { + val link = m.controller.userAddressAutoAccept(new.value.autoAccept) + if (link != null) { + contactLink = link + m.userAddress.value = link + old.value = new.value + } + } + } + ) +} + +@Composable +private fun AcceptRequestsLayout( + contactLink: UserContactLinkRec, + saveState: (new: MutableState, old: MutableState) -> Unit, +) { + Column( + Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), + ) { + AppBarTitle(stringResource(R.string.contact_requests)) + val autoAcceptState = remember { mutableStateOf(AutoAcceptState(contactLink)) } + val autoAcceptStateSaved = remember { mutableStateOf(autoAcceptState.value) } + SectionView(stringResource(R.string.accept_requests).uppercase()) { + SectionItemView { + PreferenceToggleWithIcon(stringResource(R.string.accept_automatically), Icons.Outlined.Check, checked = autoAcceptState.value.enable) { + autoAcceptState.value = if (!it) + AutoAcceptState() + else + AutoAcceptState(it, autoAcceptState.value.incognito, autoAcceptState.value.welcomeText) + } + } + if (autoAcceptState.value.enable) { + SectionDivider() + SectionItemView { + PreferenceToggleWithIcon( + stringResource(R.string.incognito), + if (autoAcceptState.value.incognito) Icons.Filled.TheaterComedy else Icons.Outlined.TheaterComedy, + if (autoAcceptState.value.incognito) Indigo else HighOrLowlight, + autoAcceptState.value.incognito, + ) { + autoAcceptState.value = AutoAcceptState(autoAcceptState.value.enable, it, autoAcceptState.value.welcomeText) + } + } + } + } + val welcomeText = remember { mutableStateOf(autoAcceptState.value.welcomeText) } + SectionCustomFooter(PaddingValues(horizontal = DEFAULT_PADDING)) { + ButtonsFooter( + cancel = { + autoAcceptState.value = autoAcceptStateSaved.value + welcomeText.value = autoAcceptStateSaved.value.welcomeText + }, + save = { saveState(autoAcceptState, autoAcceptStateSaved) }, + disabled = autoAcceptState.value == autoAcceptStateSaved.value + ) + } + Spacer(Modifier.height(DEFAULT_PADDING)) + if (autoAcceptState.value.enable) { + Text( + stringResource(R.string.section_title_welcome_text), color = HighOrLowlight, style = MaterialTheme.typography.body2, + modifier = Modifier.padding(start = DEFAULT_PADDING, bottom = 5.dp), fontSize = 12.sp + ) + TextEditor(Modifier.padding(horizontal = DEFAULT_PADDING).height(160.dp), text = welcomeText) + LaunchedEffect(welcomeText.value) { + if (welcomeText.value != autoAcceptState.value.welcomeText) { + autoAcceptState.value = AutoAcceptState(autoAcceptState.value.enable, autoAcceptState.value.incognito, welcomeText.value) + } + } + } + } +} + +@Composable +private fun ButtonsFooter(cancel: () -> Unit, save: () -> Unit, disabled: Boolean) { + Row( + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + FooterButton(Icons.Outlined.Replay, stringResource(R.string.cancel_verb), cancel, disabled) + FooterButton(Icons.Outlined.Check, stringResource(R.string.save_verb), save, disabled) + } +} + +private class AutoAcceptState { + var enable: Boolean = false + private set + var incognito: Boolean = false + private set + var welcomeText: String = "" + private set + + constructor(enable: Boolean = false, incognito: Boolean = false, welcomeText: String = "") { + this.enable = enable + this.incognito = incognito + this.welcomeText = welcomeText + } + + constructor(contactLink: UserContactLinkRec) { + contactLink.autoAccept?.let { aa -> + enable = true + incognito = aa.acceptIncognito + aa.autoReply?.let { msg -> + welcomeText = msg.text + } ?: run { + welcomeText = "" + } + } + } + + val autoAccept: AutoAccept? + get() { + if (enable) { + var autoReply: MsgContent? = null + val s = welcomeText.trim() + if (s != "") { + autoReply = MsgContent.MCText(s) + } + return AutoAccept(incognito, autoReply) + } + return null + } + + override fun equals(other: Any?): Boolean { + if (other !is AutoAcceptState) return false + return this.enable == other.enable && this.incognito == other.incognito && this.welcomeText == other.welcomeText + } + + override fun hashCode(): Int { + var result = enable.hashCode() + result = 31 * result + incognito.hashCode() + result = 31 * result + welcomeText.hashCode() + return result + } +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt index 2654acb8fa..012c77ba4f 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt @@ -342,6 +342,36 @@ fun SettingsPreferenceItemWithInfo( } } +@Composable +fun PreferenceToggleWithIcon( + text: String, + icon: ImageVector, + iconColor: Color = HighOrLowlight, + checked: Boolean, + onChange: (Boolean) -> Unit = {}, +) { + Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + Icon( + icon, + null, + tint = iconColor + ) + Spacer(Modifier.padding(horizontal = 4.dp)) + Text(text) + Spacer(Modifier.fillMaxWidth().weight(1f)) + Switch( + checked = checked, + onCheckedChange = { + onChange(it) + }, + colors = SwitchDefaults.colors( + checkedThumbColor = MaterialTheme.colors.primary, + uncheckedThumbColor = HighOrLowlight + ) + ) + } +} + @Preview(showBackground = true) @Preview( uiMode = Configuration.UI_MODE_NIGHT_YES, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserAddressView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserAddressView.kt index 34b117148a..c02ac72b97 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserAddressView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserAddressView.kt @@ -2,10 +2,13 @@ package chat.simplex.app.views.usersettings import android.content.res.Configuration import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -17,8 +20,7 @@ import androidx.compose.ui.unit.sp import chat.simplex.app.R import chat.simplex.app.model.ChatModel import chat.simplex.app.model.UserContactLinkRec -import chat.simplex.app.ui.theme.SimpleButton -import chat.simplex.app.ui.theme.SimpleXTheme +import chat.simplex.app.ui.theme.* import chat.simplex.app.views.helpers.* import chat.simplex.app.views.newchat.QRCode @@ -26,7 +28,7 @@ import chat.simplex.app.views.newchat.QRCode fun UserAddressView(chatModel: ChatModel) { val cxt = LocalContext.current UserAddressLayout( - userAddress = chatModel.userAddress.value, + userAddress = remember { chatModel.userAddress }.value, createAddress = { withApi { val connReqContact = chatModel.controller.apiCreateUserAddress() @@ -36,6 +38,11 @@ fun UserAddressView(chatModel: ChatModel) { } }, share = { userAddress: String -> shareText(cxt, userAddress) }, + acceptRequests = { + chatModel.userAddress.value?.let { address -> + ModalManager.shared.showModal(settings = true) { AcceptRequestsView(chatModel, address) } + } + }, deleteAddress = { AlertManager.shared.showAlertMsg( title = generalGetString(R.string.delete_address__question), @@ -57,9 +64,11 @@ fun UserAddressLayout( userAddress: UserContactLinkRec?, createAddress: () -> Unit, share: (String) -> Unit, + acceptRequests: () -> Unit, deleteAddress: () -> Unit ) { Column( + Modifier.verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.Top ) { @@ -70,24 +79,24 @@ fun UserAddressLayout( lineHeight = 22.sp ) Column( - Modifier.fillMaxWidth(), + Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.SpaceEvenly ) { if (userAddress == null) { Text( stringResource(R.string.if_you_later_delete_address_you_wont_lose_contacts), - Modifier.padding(bottom = 24.dp), + Modifier.align(Alignment.Start).padding(bottom = 24.dp), lineHeight = 22.sp ) SimpleButton(stringResource(R.string.create_address), icon = Icons.Outlined.QrCode, click = createAddress) } else { Text( stringResource(R.string.if_you_delete_address_you_wont_lose_contacts), - Modifier.padding(bottom = 24.dp), + Modifier.align(Alignment.Start).padding(bottom = 24.dp), lineHeight = 22.sp ) - QRCode(userAddress.connReqContact, Modifier.weight(1f, fill = false).aspectRatio(1f)) + QRCode(userAddress.connReqContact, Modifier.aspectRatio(1f)) Row( horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically, @@ -97,13 +106,18 @@ fun UserAddressLayout( stringResource(R.string.share_link), icon = Icons.Outlined.Share, click = { share(userAddress.connReqContact) }) - SimpleButton( - stringResource(R.string.delete_address), - icon = Icons.Outlined.Delete, - color = Color.Red, - click = deleteAddress + SimpleButtonIconEnded( + stringResource(R.string.contact_requests), + icon = Icons.Outlined.ChevronRight, + click = acceptRequests ) } + SimpleButton( + stringResource(R.string.delete_address), + icon = Icons.Outlined.Delete, + color = Color.Red, + click = deleteAddress + ) } } } @@ -122,6 +136,7 @@ fun PreviewUserAddressLayoutNoAddress() { userAddress = null, createAddress = {}, share = { _ -> }, + acceptRequests = {}, deleteAddress = {}, ) } @@ -140,6 +155,7 @@ fun PreviewUserAddressLayoutAddressCreated() { userAddress = UserContactLinkRec("https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D"), createAddress = {}, share = { _ -> }, + acceptRequests = {}, deleteAddress = {}, ) } diff --git a/apps/android/app/src/main/res/values-de/strings.xml b/apps/android/app/src/main/res/values-de/strings.xml index 0024defc52..6f58b0be3c 100644 --- a/apps/android/app/src/main/res/values-de/strings.xml +++ b/apps/android/app/src/main/res/values-de/strings.xml @@ -398,6 +398,12 @@ Wenn Sie diese Adresse später löschen, werden Sie Ihre mit dieser Adresse verbundenen Kontakte nicht verlieren. Link teilen Adresse löschen + Accept requests + + + Contact requests + Automatically + WELCOME TEXT Angezeigter Name: diff --git a/apps/android/app/src/main/res/values-ru/strings.xml b/apps/android/app/src/main/res/values-ru/strings.xml index ed2ca4312d..7e36ee734d 100644 --- a/apps/android/app/src/main/res/values-ru/strings.xml +++ b/apps/android/app/src/main/res/values-ru/strings.xml @@ -395,6 +395,12 @@ Вы можете удалить адрес, сохранив контакты, которые через него соединились. Поделиться\nссылкой Удалить\nадрес + Принимать запросы + + + Запросы контактов + Автоматически + ПРИВЕТСТВЕННЫЙ ТЕКСТ Имя профиля: diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index 322de86e92..d987aa1f87 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -398,6 +398,12 @@ If you delete it - you won\'t lose your contacts made via this address. Share link Delete address + Accept requests + + + Contact requests + Automatically + WELCOME TEXT Display name: