Add username link share sheet.

This commit is contained in:
Greyson Parrelli 2023-11-09 10:10:42 -05:00
parent 5698e0deda
commit 96333b616b
8 changed files with 242 additions and 28 deletions

View file

@ -42,7 +42,9 @@ abstract class FixedRoundedCornerBottomSheetDialogFragment : BottomSheetDialogFr
override fun onResume() {
super.onResume()
WindowUtil.initializeScreenshotSecurity(requireContext(), dialog!!.window!!)
dialog?.window?.let { window ->
WindowUtil.initializeScreenshotSecurity(requireContext(), window)
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {

View file

@ -51,7 +51,7 @@ fun QrCodeBadge(
username: String,
modifier: Modifier = Modifier,
usernameCopyable: Boolean = false,
onClick: (() -> Unit) = {}
onClick: ((String) -> Unit) = {}
) {
val borderColor by animateColorAsState(targetValue = colorScheme.borderColor, label = "border")
val foregroundColor by animateColorAsState(targetValue = colorScheme.foregroundColor, label = "foreground")
@ -132,7 +132,7 @@ fun QrCodeBadge(
.clip(RoundedCornerShape(8.dp))
.clickable(
enabled = usernameCopyable,
onClick = onClick
onClick = { onClick(username) }
)
.padding(8.dp)
) {

View file

@ -30,6 +30,7 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@ -43,6 +44,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.app.ShareCompat
import androidx.fragment.app.setFragmentResultListener
import androidx.fragment.app.viewModels
import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController
@ -59,19 +61,24 @@ import org.signal.core.util.concurrent.LifecycleDisposable
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.main.UsernameLinkSettingsState.ActiveTab
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.compose.ScreenshotController
import org.thoughtcrime.securesms.providers.BlobProvider
import java.io.ByteArrayOutputStream
import java.util.UUID
@OptIn(
ExperimentalPermissionsApi::class
)
@OptIn(ExperimentalPermissionsApi::class)
class UsernameLinkSettingsFragment : ComposeFragment() {
private val viewModel: UsernameLinkSettingsViewModel by viewModels()
private val disposables: LifecycleDisposable = LifecycleDisposable()
private val screenshotController = ScreenshotController()
override fun onStart() {
super.onStart()
setFragmentResultListener(UsernameLinkShareBottomSheet.REQUEST_KEY) { key, bundle ->
if (bundle.getBoolean(UsernameLinkShareBottomSheet.KEY_COPY)) {
viewModel.onLinkCopied()
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -83,6 +90,15 @@ class UsernameLinkSettingsFragment : ComposeFragment() {
var showResetDialog: Boolean by remember { mutableStateOf(false) }
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
val cameraPermissionState: PermissionState = rememberPermissionState(permission = android.Manifest.permission.CAMERA)
val linkCopiedEvent: UUID? by viewModel.linkCopiedEvent
val linkCopiedString = stringResource(R.string.UsernameLinkSettings_link_copied_toast)
LaunchedEffect(linkCopiedEvent) {
if (linkCopiedEvent != null) {
snackbarHostState.showSnackbar(linkCopiedString)
}
}
Scaffold(
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },

View file

@ -10,6 +10,7 @@ import android.graphics.Rect
import android.graphics.RectF
import android.graphics.Typeface
import android.os.Build
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.geometry.Size
@ -39,6 +40,7 @@ import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.NetworkUtil
import org.whispersystems.signalservice.api.push.UsernameLinkComponents
import java.util.Optional
import java.util.UUID
class UsernameLinkSettingsViewModel : ViewModel() {
@ -58,6 +60,9 @@ class UsernameLinkSettingsViewModel : ViewModel() {
private val disposable: CompositeDisposable = CompositeDisposable()
private val usernameLink: BehaviorSubject<Optional<UsernameLinkComponents>> = BehaviorSubject.createDefault(Optional.ofNullable(SignalStore.account().usernameLink))
private val _linkCopiedEvent: MutableState<UUID?> = mutableStateOf(null)
val linkCopiedEvent: State<UUID?> get() = _linkCopiedEvent
init {
disposable += usernameLink
.observeOn(Schedulers.io())
@ -177,6 +182,10 @@ class UsernameLinkSettingsViewModel : ViewModel() {
)
}
fun onLinkCopied() {
_linkCopiedEvent.value = UUID.randomUUID()
}
private fun generateQrCodeData(url: Optional<String>): Single<Optional<QrCodeData>> {
return Single.fromCallable {
url.map { QrCodeData.forData(it, 64) }

View file

@ -0,0 +1,180 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.settings.app.usernamelinks.main
import android.content.Intent
import android.content.res.Configuration
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.os.bundleOf
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.setFragmentResult
import kotlinx.coroutines.CoroutineScope
import org.signal.core.ui.BottomSheets
import org.signal.core.ui.theme.SignalTheme
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.webrtc.requests.CallLinkIncomingRequestSheet
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository.toLink
import org.thoughtcrime.securesms.util.BottomSheetUtil
import org.thoughtcrime.securesms.util.Util
class UsernameLinkShareBottomSheet : ComposeBottomSheetDialogFragment() {
companion object {
const val REQUEST_KEY = "link_share_bottom_sheet"
const val KEY_COPY = "copy"
@JvmStatic
fun show(fragmentManager: FragmentManager) {
CallLinkIncomingRequestSheet().show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
}
}
@Composable
override fun SheetContent() {
val scope = rememberCoroutineScope()
val snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }
Content(
usernameLink = SignalStore.account().usernameLink?.toLink() ?: "",
scope = scope,
snackbarHostState = snackbarHostState,
dismissDialog = { didCopy ->
setFragmentResult(REQUEST_KEY, bundleOf(KEY_COPY to didCopy))
dismiss()
}
)
}
}
@Composable
private fun Content(
usernameLink: String,
scope: CoroutineScope,
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
dismissDialog: (Boolean) -> Unit = {}
) {
val context = LocalContext.current
val usernameCopiedString = stringResource(id = R.string.UsernameLinkSettings_username_copied_toast)
Column(horizontalAlignment = Alignment.CenterHorizontally) {
BottomSheets.Handle()
Text(
text = stringResource(R.string.UsernameLinkShareBottomSheet_title),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier
.padding(horizontal = 41.dp, vertical = 24.dp)
)
Text(
text = usernameLink,
modifier = Modifier
.padding(horizontal = 24.dp)
.border(
width = 1.dp,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f),
shape = RoundedCornerShape(12.dp)
)
.padding(all = 16.dp)
)
ButtonRow(
icon = painterResource(R.drawable.symbol_copy_android_24),
text = stringResource(R.string.UsernameLinkShareBottomSheet_copy_link),
modifier = Modifier.padding(top = 12.dp),
onClick = {
Util.copyToClipboard(context, usernameLink)
dismissDialog(true)
}
)
ButtonRow(
icon = painterResource(R.drawable.symbol_share_android_24),
text = stringResource(R.string.UsernameLinkShareBottomSheet_share),
modifier = Modifier.padding(bottom = 12.dp),
onClick = {
dismissDialog(false)
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, usernameLink)
}
context.startActivity(Intent.createChooser(sendIntent, null))
}
)
}
}
@Composable
private fun ButtonRow(icon: Painter, text: String, modifier: Modifier = Modifier, onClick: () -> Unit = {}) {
Row(
modifier = modifier
.fillMaxWidth()
.clickable { onClick() }
) {
Icon(
painter = icon,
contentDescription = text,
tint = MaterialTheme.colorScheme.onSurface,
modifier = Modifier
.padding(horizontal = 24.dp, vertical = 16.dp)
)
Text(
text = text,
modifier = Modifier
.padding(vertical = 16.dp)
)
}
}
@Preview(name = "Light Theme", group = "content", uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark Theme", group = "content", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ContentPreview() {
SignalTheme {
Surface {
Content(
usernameLink = "https://signal.me#eufzLWmFFUYAOqnVJ4Zlt0KqXf87r59FC1hZ3r7WipjKvgzMBg7DBlY5DB5hQTjsw0",
scope = rememberCoroutineScope()
)
}
}
}
@Preview(name = "Light Theme", group = "button row", uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark Theme", group = "button row", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ButtonRowPreview() {
SignalTheme {
Surface {
ButtonRow(icon = painterResource(R.drawable.symbol_share_android_24), text = "Share")
}
}
}

View file

@ -59,6 +59,8 @@ fun UsernameLinkShareScreen(
modifier: Modifier = Modifier,
onResetClicked: () -> Unit
) {
val context = LocalContext.current
when (state.usernameLinkResetResult) {
UsernameLinkResetResult.NetworkUnavailable -> {
ResetLinkResultDialog(stringResource(R.string.UsernameLinkSettings_reset_link_result_network_unavailable), onDismiss = onLinkResultHandled)
@ -81,7 +83,8 @@ fun UsernameLinkShareScreen(
username = state.username,
usernameCopyable = true,
modifier = Modifier.padding(horizontal = 58.dp, vertical = 24.dp),
onClick = {
onClick = { username ->
Util.copyToClipboard(context, username)
scope.launch {
snackbarHostState.showSnackbar(usernameCopiedString)
}
@ -95,8 +98,9 @@ fun UsernameLinkShareScreen(
LinkRow(
linkState = state.usernameLinkState,
snackbarHostState = snackbarHostState,
scope = scope
onClick = {
navController.safeNavigate(UsernameLinkSettingsFragmentDirections.actionUsernameLinkSettingsFragmentToUsernameLinkShareBottomSheet())
}
)
Text(
@ -142,9 +146,7 @@ private fun ButtonBar(onShareClicked: () -> Unit, onColorClicked: () -> Unit) {
}
@Composable
private fun LinkRow(linkState: UsernameLinkState, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
val context = LocalContext.current
val copyMessage = stringResource(R.string.UsernameLinkSettings_link_copied_toast)
private fun LinkRow(linkState: UsernameLinkState, onClick: () -> Unit = {}) {
Row(
modifier = Modifier
.fillMaxWidth()
@ -161,11 +163,7 @@ private fun LinkRow(linkState: UsernameLinkState, snackbarHostState: SnackbarHos
shape = RoundedCornerShape(12.dp)
)
.clickable(enabled = linkState is UsernameLinkState.Present) {
Util.copyToClipboard(context, (linkState as UsernameLinkState.Present).link)
scope.launch {
snackbarHostState.showSnackbar(copyMessage)
}
onClick()
}
.padding(horizontal = 26.dp, vertical = 16.dp)
.alpha(if (linkState is UsernameLinkState.Present) 1.0f else 0.6f)
@ -225,19 +223,13 @@ private fun LinkRowPreview() {
Surface {
Column(modifier = Modifier.padding(8.dp)) {
LinkRow(
linkState = UsernameLinkState.Present("https://signal.me/#eu/asdfasdfasdfasdfasdfasdfasdfasdfasdfasdf"),
snackbarHostState = SnackbarHostState(),
scope = rememberCoroutineScope()
linkState = UsernameLinkState.Present("https://signal.me/#eu/asdfasdfasdfasdfasdfasdfasdfasdfasdfasdf")
)
LinkRow(
linkState = UsernameLinkState.NotSet,
snackbarHostState = SnackbarHostState(),
scope = rememberCoroutineScope()
linkState = UsernameLinkState.NotSet
)
LinkRow(
linkState = UsernameLinkState.Resetting,
snackbarHostState = SnackbarHostState(),
scope = rememberCoroutineScope()
linkState = UsernameLinkState.Resetting
)
}
}

View file

@ -20,10 +20,18 @@
app:exitAnim="@anim/fragment_open_exit"
app:popEnterAnim="@anim/fragment_close_enter"
app:popExitAnim="@anim/fragment_close_exit" />
<action
android:id="@+id/action_usernameLinkSettingsFragment_to_usernameLinkShareBottomSheet"
app:destination="@id/usernameLinkShareBottomSheet" />
</fragment>
<fragment
android:id="@+id/usernameLinkQrColorPickerFragment"
android:name="org.thoughtcrime.securesms.components.settings.app.usernamelinks.colorpicker.UsernameLinkQrColorPickerFragment" />
<dialog
android:id="@+id/usernameLinkShareBottomSheet"
android:name="org.thoughtcrime.securesms.components.settings.app.usernamelinks.main.UsernameLinkShareBottomSheet" />
</navigation>

View file

@ -6401,6 +6401,13 @@
<!-- Body of a dialog that is displayed when we failed to reset your username link because of a transient network issue. -->
<string name="UsernameLinkSettings_reset_link_result_network_error">A network error occurred while trying to reset your link. Try again later.</string>
<!-- Explanatory text at the top of a bottom sheet describing how username links work -->
<string name="UsernameLinkShareBottomSheet_title">Anyone with this link can view your username and start a chat with you. Only share it with people you trust.</string>
<!-- A button label for a button that, when pressed, will copy your username link to the clipboard -->
<string name="UsernameLinkShareBottomSheet_copy_link">Copy link</string>
<!-- A button label for a button that, when pressed, will open a share sheet for sharing your username link -->
<string name="UsernameLinkShareBottomSheet_share">Share</string>
<!-- PendingParticipantsView -->
<!-- Displayed in the popup card when a remote user attempts to join a call link -->
<string name="PendingParticipantsView__would_like_to_join">Would like to join…</string>