Add call link details screen scaffolding.

This commit is contained in:
Alex Hart 2023-04-21 10:48:42 -03:00
parent 5f7414e84c
commit bff8fc8230
14 changed files with 371 additions and 46 deletions

View file

@ -0,0 +1,8 @@
package org.thoughtcrime.securesms.calls.links
/**
* Utility object for call links to try to keep some common logic in one place.
*/
object CallLinks {
fun url(identifier: String) = "https://calls.signal.org/#$identifier"
}

View file

@ -28,18 +28,21 @@ import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.fragment.app.viewModels
import androidx.core.os.bundleOf
import androidx.fragment.app.setFragmentResult
import androidx.navigation.fragment.navArgs
import org.signal.core.ui.Buttons
import org.signal.core.ui.Scaffolds
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.calls.links.create.CreateCallLinkViewModel
import org.thoughtcrime.securesms.compose.ComposeDialogFragment
class EditCallLinkNameDialogFragment : ComposeDialogFragment() {
private val viewModel: CreateCallLinkViewModel by viewModels(
ownerProducer = { requireParentFragment() }
)
companion object {
const val RESULT_KEY = "edit_call_link_name"
}
private val args: EditCallLinkNameDialogFragmentArgs by navArgs()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -57,12 +60,11 @@ class EditCallLinkNameDialogFragment : ComposeDialogFragment() {
@Preview
@Composable
override fun DialogContent() {
val viewModelCallName by viewModel.callName
var callName by remember {
mutableStateOf(
TextFieldValue(
text = viewModelCallName,
selection = TextRange(viewModelCallName.length)
text = args.name,
selection = TextRange(args.name.length)
)
)
}
@ -97,7 +99,7 @@ class EditCallLinkNameDialogFragment : ComposeDialogFragment() {
Spacer(modifier = Modifier.weight(1f))
Buttons.MediumTonal(
onClick = {
viewModel.setCallName(callName.text)
setFragmentResult(RESULT_KEY, bundleOf(RESULT_KEY to callName.text))
dismiss()
},
modifier = Modifier.align(End)

View file

@ -15,12 +15,14 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
@ -29,14 +31,25 @@ import androidx.compose.ui.unit.dp
import org.signal.core.ui.Buttons
import org.signal.core.ui.theme.SignalTheme
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
import org.thoughtcrime.securesms.conversation.colors.AvatarColorPair
import org.thoughtcrime.securesms.database.CallLinkTable
@Preview
@Composable
private fun SignalCallRowPreview() {
val avatarColor = remember { AvatarColor.random() }
val callLink = remember {
CallLinkTable.CallLink(
name = "Call Name",
identifier = "blahblahblah",
avatarColor = avatarColor,
isApprovalRequired = false
)
}
SignalTheme(false) {
SignalCallRow(
callName = "Call Name",
callLink = "https://call.signal.org#blahblahblah",
callLink = callLink,
onJoinClicked = {}
)
}
@ -44,8 +57,7 @@ private fun SignalCallRowPreview() {
@Composable
fun SignalCallRow(
callName: String,
callLink: String,
callLink: CallLinkTable.CallLink,
onJoinClicked: () -> Unit,
modifier: Modifier = Modifier
) {
@ -60,15 +72,17 @@ fun SignalCallRow(
)
.padding(16.dp)
) {
val callColorPair = AvatarColorPair.create(LocalContext.current, callLink.avatarColor)
Image(
imageVector = ImageVector.vectorResource(id = R.drawable.symbol_video_display_bold_40),
contentScale = ContentScale.Inside,
contentDescription = null,
colorFilter = ColorFilter.tint(Color(0xFF5151F6)),
colorFilter = ColorFilter.tint(Color(callColorPair.foregroundColor)),
modifier = Modifier
.size(64.dp)
.background(
color = Color(0xFFE5E5FE),
color = Color(callColorPair.backgroundColor),
shape = CircleShape
)
)
@ -81,10 +95,10 @@ fun SignalCallRow(
.align(CenterVertically)
) {
Text(
text = callName.ifEmpty { stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__signal_call) }
text = callLink.name.ifEmpty { stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__signal_call) }
)
Text(
text = callLink,
text = CallLinks.url(callLink.identifier),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)

View file

@ -2,6 +2,8 @@ package org.thoughtcrime.securesms.calls.links.create
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
@ -25,15 +27,18 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.core.app.ShareCompat
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import org.signal.core.ui.Buttons
import org.signal.core.ui.Dividers
import org.signal.core.ui.Rows
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.calls.links.CallLinks
import org.thoughtcrime.securesms.calls.links.EditCallLinkNameDialogFragment
import org.thoughtcrime.securesms.calls.links.SignalCallRow
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragment
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs
import org.thoughtcrime.securesms.database.CallLinkTable
import org.thoughtcrime.securesms.sharing.MultiShareArgs
import org.thoughtcrime.securesms.util.Util
@ -46,6 +51,14 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment
override val peekHeightPercentage: Float = 1f
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
parentFragmentManager.setFragmentResultListener(EditCallLinkNameDialogFragment.RESULT_KEY, viewLifecycleOwner) { resultKey, bundle ->
if (bundle.containsKey(resultKey)) {
viewModel.setCallName(bundle.getString(resultKey)!!)
}
}
}
@Composable
override fun SheetContent() {
Column(
@ -53,9 +66,7 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment
.fillMaxWidth()
.wrapContentSize(Alignment.Center)
) {
val callName: String by viewModel.callName
val callLink: String by viewModel.callLink
val approveAllMembers: Boolean by viewModel.approveAllMembers
val callLink: CallLinkTable.CallLink by viewModel.callLink
Handle(modifier = Modifier.align(Alignment.CenterHorizontally))
@ -71,7 +82,6 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment
Spacer(modifier = Modifier.height(24.dp))
SignalCallRow(
callName = callName,
callLink = callLink,
onJoinClicked = this@CreateCallLinkBottomSheetDialogFragment::onJoinClicked
)
@ -84,7 +94,7 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment
)
Rows.ToggleRow(
checked = approveAllMembers,
checked = callLink.isApprovalRequired,
text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__approve_all_members),
onCheckChanged = viewModel::setApproveAllMembers,
modifier = Modifier.clickable(onClick = viewModel::toggleApproveAllMembers)
@ -124,7 +134,10 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment
}
private fun onAddACallNameClicked() {
EditCallLinkNameDialogFragment().show(childFragmentManager, null)
val snapshot = viewModel.callLink.value
findNavController().navigate(
CreateCallLinkBottomSheetDialogFragmentDirections.actionCreateCallLinkBottomSheetToEditCallLinkNameDialogFragment(snapshot.name)
)
}
private fun onJoinClicked() {
@ -142,7 +155,7 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment
canSendToNonPush = false,
multiShareArgs = listOf(
MultiShareArgs.Builder()
.withDraftText(snapshot)
.withDraftText(snapshot.identifier)
.build()
)
)
@ -151,7 +164,7 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment
private fun onCopyLinkClicked() {
val snapshot = viewModel.callLink.value
Util.copyToClipboard(requireContext(), snapshot)
Util.copyToClipboard(requireContext(), CallLinks.url(snapshot.identifier))
Toast.makeText(requireContext(), R.string.CreateCallLinkBottomSheetDialogFragment__copied_to_clipboard, Toast.LENGTH_LONG).show()
}
@ -159,7 +172,7 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment
val snapshot = viewModel.callLink.value
val mimeType = Intent.normalizeMimeType("text/plain")
val shareIntent = ShareCompat.IntentBuilder(requireContext())
.setText(snapshot)
.setText(CallLinks.url(snapshot.identifier))
.setType(mimeType)
.createChooserIntent()
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)

View file

@ -4,29 +4,24 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
import org.thoughtcrime.securesms.database.CallLinkTable
class CreateCallLinkViewModel : ViewModel() {
private val _callName: MutableState<String> = mutableStateOf("")
private val _callLink: MutableState<String> = mutableStateOf("")
private val _approveAllMembers: MutableState<Boolean> = mutableStateOf(false)
val callName: State<String> = _callName
val callLink: State<String> = _callLink
val approveAllMembers: State<Boolean> = _approveAllMembers
private val _callLink: MutableState<CallLinkTable.CallLink> = mutableStateOf(
CallLinkTable.CallLink("", "", AvatarColor.random(), false)
)
val callLink: State<CallLinkTable.CallLink> = _callLink
fun setApproveAllMembers(approveAllMembers: Boolean) {
_approveAllMembers.value = approveAllMembers
_callLink.value = _callLink.value.copy(isApprovalRequired = approveAllMembers)
}
fun toggleApproveAllMembers() {
_approveAllMembers.value = !_approveAllMembers.value
_callLink.value = _callLink.value.copy(isApprovalRequired = _callLink.value.isApprovalRequired)
}
fun setCallName(callName: String) {
_callName.value = callName
}
fun setCallLink(callLink: String) {
_callLink.value = callLink
_callLink.value = _callLink.value.copy(name = callName)
}
}

View file

@ -0,0 +1,10 @@
package org.thoughtcrime.securesms.calls.links.details
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.NavHostFragment
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.FragmentWrapperActivity
class CallLinkDetailsActivity : FragmentWrapperActivity() {
override fun getFragment(): Fragment = NavHostFragment.create(R.navigation.call_link_details)
}

View file

@ -0,0 +1,179 @@
package org.thoughtcrime.securesms.calls.links.details
import android.os.Bundle
import android.view.View
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.fragment.app.setFragmentResultListener
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import org.signal.core.ui.Dividers
import org.signal.core.ui.Rows
import org.signal.core.ui.Scaffolds
import org.signal.core.ui.theme.SignalTheme
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.calls.links.EditCallLinkNameDialogFragment
import org.thoughtcrime.securesms.calls.links.SignalCallRow
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
import org.thoughtcrime.securesms.database.CallLinkTable
/**
* Provides detailed info about a call link and allows the owner of that link
* to modify call properties.
*/
class CallLinkDetailsFragment : ComposeFragment(), CallLinkDetailsCallback {
private val viewModel: CallLinkViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
parentFragmentManager.setFragmentResultListener(EditCallLinkNameDialogFragment.RESULT_KEY, viewLifecycleOwner) { resultKey, bundle ->
if (bundle.containsKey(resultKey)) {
viewModel.setName(bundle.getString(resultKey)!!)
}
}
}
@Composable
override fun FragmentContent() {
val isLoading by viewModel.isLoading
val callLink by viewModel.callLink
CallLinkDetails(
isLoading,
callLink,
this
)
}
override fun onNavigationClicked() {
findNavController().popBackStack()
}
override fun onJoinClicked() {
// TODO("Not yet implemented")
}
override fun onEditNameClicked() {
val name = viewModel.callLink.value.name
findNavController().navigate(
CallLinkDetailsFragmentDirections.actionCallLinkDetailsFragmentToEditCallLinkNameDialogFragment(name)
)
}
override fun onShareClicked() {
// TODO("Not yet implemented")
}
override fun onDeleteClicked() {
// TODO("Not yet implemented")
}
override fun onApproveAllMembersChanged(checked: Boolean) {
// TODO("Not yet implemented")
}
}
private interface CallLinkDetailsCallback {
fun onNavigationClicked()
fun onJoinClicked()
fun onEditNameClicked()
fun onShareClicked()
fun onDeleteClicked()
fun onApproveAllMembersChanged(checked: Boolean)
}
@Preview
@Composable
private fun CallLinkDetailsPreview() {
val avatarColor = remember {
AvatarColor.random()
}
val callLink = remember {
CallLinkTable.CallLink(
name = "Call Name",
identifier = "call-id-1",
isApprovalRequired = false,
avatarColor = avatarColor
)
}
SignalTheme(false) {
CallLinkDetails(
false,
callLink,
object : CallLinkDetailsCallback {
override fun onNavigationClicked() = Unit
override fun onJoinClicked() = Unit
override fun onEditNameClicked() = Unit
override fun onShareClicked() = Unit
override fun onDeleteClicked() = Unit
override fun onApproveAllMembersChanged(checked: Boolean) = Unit
}
)
}
}
@Composable
private fun CallLinkDetails(
isLoading: Boolean,
callLink: CallLinkTable.CallLink,
callback: CallLinkDetailsCallback
) {
Scaffolds.Settings(
title = stringResource(id = R.string.CallLinkDetailsFragment__call_details),
onNavigationClick = callback::onNavigationClicked,
navigationIconPainter = painterResource(id = R.drawable.ic_arrow_left_24)
) { paddingValues ->
if (isLoading) {
return@Settings
}
Column(modifier = Modifier.padding(paddingValues)) {
SignalCallRow(
callLink = callLink,
onJoinClicked = callback::onJoinClicked,
modifier = Modifier.padding(top = 16.dp, bottom = 12.dp)
)
Rows.TextRow(
text = stringResource(id = R.string.CallLinkDetailsFragment__add_call_name),
modifier = Modifier.clickable(onClick = callback::onEditNameClicked)
)
Rows.ToggleRow(
checked = callLink.isApprovalRequired,
text = stringResource(id = R.string.CallLinkDetailsFragment__approve_all_members),
onCheckChanged = callback::onApproveAllMembersChanged
)
Dividers.Default()
Rows.TextRow(
text = stringResource(id = R.string.CallLinkDetailsFragment__share_link),
icon = ImageVector.vectorResource(id = R.drawable.symbol_link_24),
modifier = Modifier.clickable(onClick = callback::onShareClicked)
)
Rows.TextRow(
text = stringResource(id = R.string.CallLinkDetailsFragment__delete_call_link),
icon = ImageVector.vectorResource(id = R.drawable.symbol_trash_24),
foregroundTint = MaterialTheme.colorScheme.error,
modifier = Modifier.clickable(onClick = callback::onDeleteClicked)
)
}
}
}

View file

@ -0,0 +1,22 @@
package org.thoughtcrime.securesms.calls.links.details
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
import org.thoughtcrime.securesms.database.CallLinkTable
class CallLinkViewModel : ViewModel() {
private val isLoadingState: MutableState<Boolean> = mutableStateOf(true)
val isLoading: State<Boolean> = isLoadingState
private val callLinkState: MutableState<CallLinkTable.CallLink> = mutableStateOf(
CallLinkTable.CallLink("", "", AvatarColor.A120, false)
)
val callLink: State<CallLinkTable.CallLink> = callLinkState
fun setName(name: String) {
callLinkState.value = callLinkState.value.copy(name = name)
}
}

View file

@ -29,4 +29,22 @@ class AvatarColorPair private constructor(
}
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as AvatarColorPair
if (foregroundColor != other.foregroundColor) return false
if (backgroundColor != other.backgroundColor) return false
return true
}
override fun hashCode(): Int {
var result = foregroundColor
result = 31 * result + backgroundColor
return result
}
}

View file

@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.database
import android.content.Context
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
/**
* Table containing ad-hoc call link details
@ -21,4 +22,11 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
)
"""
}
data class CallLink(
val name: String,
val identifier: String,
val avatarColor: AvatarColor,
val isApprovalRequired: Boolean
)
}

View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/call_link_details"
app:startDestination="@id/callLinkDetailsFragment">
<fragment
android:id="@+id/callLinkDetailsFragment"
android:name="org.thoughtcrime.securesms.calls.links.details.CallLinkDetailsFragment"
android:label="call_link_details">
<action
android:id="@+id/action_callLinkDetailsFragment_to_editCallLinkNameDialogFragment"
app:destination="@id/editCallLinkNameDialogFragment" />
</fragment>
<dialog
android:id="@+id/editCallLinkNameDialogFragment"
android:name="org.thoughtcrime.securesms.calls.links.EditCallLinkNameDialogFragment"
android:label="edit_call_link_name_dialog">
<argument
android:name="name"
app:argType="string"
app:nullable="false" />
</dialog>
</navigation>

View file

@ -31,7 +31,7 @@
<fragment
android:id="@+id/storiesLandingFragment"
android:name="org.thoughtcrime.securesms.stories.landing.StoriesLandingFragment"
android:label="stories_landing_fragment" >
android:label="stories_landing_fragment">
<action
android:id="@+id/action_storiesLandingFragment_to_callLogFragment"
app:destination="@id/callLogFragment" />
@ -40,7 +40,7 @@
<fragment
android:id="@+id/callLogFragment"
android:name="org.thoughtcrime.securesms.calls.log.CallLogFragment"
android:label="call_log_fragment" >
android:label="call_log_fragment">
<action
android:id="@+id/action_callLogFragment_to_storiesLandingFragment"
app:destination="@id/storiesLandingFragment" />
@ -52,6 +52,21 @@
<dialog
android:id="@+id/createCallLinkBottomSheet"
android:name="org.thoughtcrime.securesms.calls.links.create.CreateCallLinkBottomSheetDialogFragment"
android:label="create_call_link_bottom_sheet" />
android:label="create_call_link_bottom_sheet">
<action
android:id="@+id/action_createCallLinkBottomSheet_to_editCallLinkNameDialogFragment"
app:destination="@id/editCallLinkNameDialogFragment" />
</dialog>
<dialog
android:id="@+id/editCallLinkNameDialogFragment"
android:name="org.thoughtcrime.securesms.calls.links.EditCallLinkNameDialogFragment"
android:label="edit_call_link_name_fragment">
<argument
android:name="name"
app:argType="string"
app:nullable="false" />
</dialog>
</navigation>

View file

@ -5960,5 +5960,17 @@
<!-- Title shown at top of bottom sheet dialog for displaying a message\'s edit history -->
<string name="EditMessageHistoryDialog_title">Edit history</string>
<!-- CallLinkDetailsFragment -->
<!-- Displayed in action bar at the top of the fragment -->
<string name="CallLinkDetailsFragment__call_details">Call details</string>
<!-- Displayed in a text row, allowing the user to click and add a call name -->
<string name="CallLinkDetailsFragment__add_call_name">Add call name</string>
<!-- Displayed in a toggle row, allowing the user to click to enable or disable member approval -->
<string name="CallLinkDetailsFragment__approve_all_members">Approve all members</string>
<!-- Displayed in a text row, allowing the user to share the call link -->
<string name="CallLinkDetailsFragment__share_link">Share link</string>
<!-- Displayed in a text row, allowing the user to delete the call link -->
<string name="CallLinkDetailsFragment__delete_call_link">Delete call link</string>
<!-- EOF -->
</resources>

View file

@ -21,6 +21,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.tooling.preview.Preview
@ -102,7 +103,8 @@ object Rows {
fun TextRow(
text: String,
modifier: Modifier = Modifier,
icon: ImageVector? = null
icon: ImageVector? = null,
foregroundTint: Color = MaterialTheme.colorScheme.onSurface
) {
if (icon != null) {
Row(
@ -113,14 +115,15 @@ object Rows {
Icon(
imageVector = icon,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurface
tint = foregroundTint
)
Spacer(modifier = Modifier.width(24.dp))
Text(
text = text,
modifier = Modifier.weight(1f)
modifier = Modifier.weight(1f),
color = foregroundTint
)
}
} else {