Add username link share sheet.
This commit is contained in:
parent
5698e0deda
commit
96333b616b
8 changed files with 242 additions and 28 deletions
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
) {
|
||||
|
|
|
@ -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) },
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Reference in a new issue