Add create call link sheet.

This commit is contained in:
Alex Hart 2023-04-04 12:51:06 -03:00
parent d8ac5a390a
commit 9d575650d1
15 changed files with 717 additions and 9 deletions

View file

@ -0,0 +1,242 @@
package org.thoughtcrime.securesms.calls.links
import android.content.ActivityNotFoundException
import android.content.Intent
import android.widget.Toast
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
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.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.CircleShape
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.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Alignment.Companion.End
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.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.core.app.ShareCompat
import androidx.fragment.app.viewModels
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.compose.ComposeBottomSheetDialogFragment
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragment
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs
import org.thoughtcrime.securesms.sharing.MultiShareArgs
import org.thoughtcrime.securesms.util.Util
/**
* Bottom sheet for creating call links
*/
class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment() {
private val viewModel: CreateCallLinkViewModel by viewModels()
override val peekHeightPercentage: Float = 1f
@Composable
override fun SheetContent() {
Column(
modifier = Modifier
.fillMaxWidth()
.wrapContentSize(Alignment.Center)
) {
val callName: String by viewModel.callName
val callLink: String by viewModel.callLink
val approveAllMembers: Boolean by viewModel.approveAllMembers
Handle(modifier = Modifier.align(CenterHorizontally))
Spacer(modifier = Modifier.height(20.dp))
Text(
text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__create_call_link),
style = MaterialTheme.typography.titleLarge,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(24.dp))
SignalCallRow(
callName = callName,
callLink = callLink,
onJoinClicked = this@CreateCallLinkBottomSheetDialogFragment::onJoinClicked
)
Spacer(modifier = Modifier.height(12.dp))
Rows.TextRow(
text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__add_call_name),
modifier = Modifier.clickable(onClick = this@CreateCallLinkBottomSheetDialogFragment::onAddACallNameClicked)
)
Rows.ToggleRow(
checked = approveAllMembers,
text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__approve_all_members),
onCheckChanged = viewModel::setApproveAllMembers,
modifier = Modifier.clickable(onClick = viewModel::toggleApproveAllMembers)
)
Dividers.Default()
Rows.TextRow(
text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__share_link_via_signal),
icon = ImageVector.vectorResource(id = R.drawable.symbol_forward_24),
modifier = Modifier.clickable(onClick = this@CreateCallLinkBottomSheetDialogFragment::onShareViaSignalClicked)
)
Rows.TextRow(
text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__copy_link),
icon = ImageVector.vectorResource(id = R.drawable.symbol_copy_android_24),
modifier = Modifier.clickable(onClick = this@CreateCallLinkBottomSheetDialogFragment::onCopyLinkClicked)
)
Rows.TextRow(
text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__share_link),
icon = ImageVector.vectorResource(id = R.drawable.symbol_share_android_24),
modifier = Modifier.clickable(onClick = this@CreateCallLinkBottomSheetDialogFragment::onShareLinkClicked)
)
Buttons.MediumTonal(
onClick = this@CreateCallLinkBottomSheetDialogFragment::onDoneClicked,
modifier = Modifier
.padding(end = dimensionResource(id = R.dimen.core_ui__gutter))
.align(End)
) {
Text(text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__done))
}
Spacer(modifier = Modifier.size(16.dp))
}
}
private fun onAddACallNameClicked() {
EditCallLinkNameDialogFragment().show(childFragmentManager, null)
}
private fun onJoinClicked() {
}
private fun onDoneClicked() {
}
private fun onShareViaSignalClicked() {
val snapshot = viewModel.callLink.value
MultiselectForwardFragment.showFullScreen(
childFragmentManager,
MultiselectForwardFragmentArgs(
canSendToNonPush = false,
multiShareArgs = listOf(
MultiShareArgs.Builder()
.withDraftText(snapshot)
.build()
)
)
)
}
private fun onCopyLinkClicked() {
val snapshot = viewModel.callLink.value
Util.copyToClipboard(requireContext(), snapshot)
Toast.makeText(requireContext(), R.string.CreateCallLinkBottomSheetDialogFragment__copied_to_clipboard, Toast.LENGTH_LONG).show()
}
private fun onShareLinkClicked() {
val snapshot = viewModel.callLink.value
val mimeType = Intent.normalizeMimeType("text/plain")
val shareIntent = ShareCompat.IntentBuilder(requireContext())
.setText(snapshot)
.setType(mimeType)
.createChooserIntent()
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
try {
startActivity(shareIntent)
} catch (e: ActivityNotFoundException) {
Toast.makeText(requireContext(), R.string.CreateCallLinkBottomSheetDialogFragment__failed_to_open_share_sheet, Toast.LENGTH_LONG).show()
}
}
}
@Composable
private fun SignalCallRow(
callName: String,
callLink: String,
onJoinClicked: () -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
.border(
width = 1.25.dp,
color = MaterialTheme.colorScheme.outline,
shape = RoundedCornerShape(18.dp)
)
.padding(16.dp)
) {
Image(
imageVector = ImageVector.vectorResource(id = R.drawable.symbol_video_display_bold_40),
contentScale = ContentScale.Inside,
contentDescription = null,
colorFilter = ColorFilter.tint(Color(0xFF5151F6)),
modifier = Modifier
.size(64.dp)
.background(
color = Color(0xFFE5E5FE),
shape = CircleShape
)
)
Spacer(modifier = Modifier.width(10.dp))
Column(
modifier = Modifier
.weight(1f)
.align(CenterVertically)
) {
Text(
text = callName.ifEmpty { stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__signal_call) }
)
Text(
text = callLink,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Spacer(modifier = Modifier.width(10.dp))
Buttons.Small(
onClick = onJoinClicked,
modifier = Modifier.align(CenterVertically)
) {
Text(text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__join))
}
}
}

View file

@ -0,0 +1,32 @@
package org.thoughtcrime.securesms.calls.links
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
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
fun setApproveAllMembers(approveAllMembers: Boolean) {
_approveAllMembers.value = approveAllMembers
}
fun toggleApproveAllMembers() {
_approveAllMembers.value = !_approveAllMembers.value
}
fun setCallName(callName: String) {
_callName.value = callName
}
fun setCallLink(callLink: String) {
_callLink.value = callLink
}
}

View file

@ -0,0 +1,114 @@
package org.thoughtcrime.securesms.calls.links
import android.app.Dialog
import android.os.Bundle
import android.view.WindowManager
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
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
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment.Companion.End
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
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 org.signal.core.ui.Buttons
import org.signal.core.ui.Scaffolds
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.ComposeDialogFragment
class EditCallLinkNameDialogFragment : ComposeDialogFragment() {
private val viewModel: CreateCallLinkViewModel by viewModels(
ownerProducer = { requireParentFragment() }
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_FRAME, R.style.Signal_DayNight_Dialog_FullScreen)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState)
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
return dialog
}
@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
override fun DialogContent() {
val viewModelCallName by viewModel.callName
var callName by remember {
mutableStateOf(
TextFieldValue(
text = viewModelCallName,
selection = TextRange(viewModelCallName.length)
)
)
}
Scaffolds.Settings(
title = stringResource(id = R.string.EditCallLinkNameDialogFragment__edit_call_name),
onNavigationClick = this::dismiss,
navigationIconPainter = painterResource(id = R.drawable.ic_arrow_left_24),
navigationContentDescription = stringResource(id = R.string.Material3SearchToolbar__close)
) { paddingValues ->
val focusRequester = remember { FocusRequester() }
Surface(modifier = Modifier.padding(paddingValues)) {
Column(
modifier = Modifier
.padding(
horizontal = dimensionResource(id = org.signal.core.ui.R.dimen.core_ui__gutter)
)
.padding(top = 20.dp, bottom = 16.dp)
) {
TextField(
value = callName,
label = {
Text(text = stringResource(id = R.string.EditCallLinkNameDialogFragment__call_name))
},
onValueChange = { callName = it },
singleLine = true,
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester)
)
Spacer(modifier = Modifier.weight(1f))
Buttons.MediumTonal(
onClick = {
viewModel.setCallName(callName.text)
dismiss()
},
modifier = Modifier.align(End)
) {
Text(text = stringResource(id = R.string.EditCallLinkNameDialogFragment__save))
}
}
}
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
}
}
}

View file

@ -9,6 +9,7 @@ import androidx.core.widget.TextViewCompat
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.database.CallTable
import org.thoughtcrime.securesms.databinding.CallLogAdapterItemBinding
import org.thoughtcrime.securesms.databinding.CallLogCreateCallLinkItemBinding
import org.thoughtcrime.securesms.databinding.ConversationListItemClearFilterBinding
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.recipients.Recipient
@ -51,6 +52,13 @@ class CallLogAdapter(
inflater = ConversationListItemClearFilterBinding::inflate
)
)
registerFactory(
CreateCallLinkModel::class.java,
BindingFactory(
creator = { CreateCallLinkViewHolder(it, callbacks::onCreateACallLinkClicked) },
inflater = CallLogCreateCallLinkItemBinding::inflate
)
)
}
fun submitCallRows(
@ -65,6 +73,7 @@ class CallLogAdapter(
when (it) {
is CallLogRow.Call -> CallModel(it, selectionState, itemCount)
is CallLogRow.ClearFilter -> ClearFilterModel()
is CallLogRow.CreateCallLink -> CreateCallLinkModel()
}
}
@ -112,6 +121,12 @@ class CallLogAdapter(
override fun areContentsTheSame(newItem: ClearFilterModel): Boolean = true
}
private class CreateCallLinkModel : MappingModel<CreateCallLinkModel> {
override fun areItemsTheSame(newItem: CreateCallLinkModel): Boolean = true
override fun areContentsTheSame(newItem: CreateCallLinkModel): Boolean = true
}
private class CallModelViewHolder(
binding: CallLogAdapterItemBinding,
private val onCallClicked: (CallLogRow.Call) -> Unit,
@ -234,7 +249,23 @@ class CallLogAdapter(
override fun bind(model: ClearFilterModel) = Unit
}
private class CreateCallLinkViewHolder(
binding: CallLogCreateCallLinkItemBinding,
onClick: () -> Unit
) : BindingViewHolder<CreateCallLinkModel, CallLogCreateCallLinkItemBinding>(binding) {
init {
binding.root.setOnClickListener { onClick() }
}
override fun bind(model: CreateCallLinkModel) = Unit
}
interface Callbacks {
/**
* Invoked when 'Create a call link' is clicked
*/
fun onCreateACallLinkClicked()
/**
* Invoked when a call row is clicked
*/

View file

@ -14,6 +14,7 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.MenuProvider
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@ -268,6 +269,10 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
}
}
override fun onCreateACallLinkClicked() {
findNavController().navigate(R.id.createCallLinkBottomSheet)
}
override fun onCallClicked(callLogRow: CallLogRow.Call) {
if (viewModel.selectionStateSnapshot.isNotEmpty(binding.recycler.adapter!!.itemCount)) {
viewModel.toggleSelected(callLogRow.id)

View file

@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.calls.log
import org.signal.paging.PagedDataSource
import org.thoughtcrime.securesms.util.FeatureFlags
class CallLogPagedDataSource(
private val query: String?,
@ -9,16 +10,24 @@ class CallLogPagedDataSource(
) : PagedDataSource<CallLogRow.Id, CallLogRow> {
private val hasFilter = filter == CallLogFilter.MISSED
private val hasCallLinkRow = FeatureFlags.adHocCalling() && filter == CallLogFilter.ALL && query.isNullOrEmpty()
var callsCount = 0
private var callsCount = 0
override fun size(): Int {
callsCount = repository.getCallsCount(query, filter)
return callsCount + (if (hasFilter) 1 else 0)
return callsCount + hasFilter.toInt() + hasCallLinkRow.toInt()
}
override fun load(start: Int, length: Int, cancellationSignal: PagedDataSource.CancellationSignal): MutableList<CallLogRow> {
val calls: MutableList<CallLogRow> = repository.getCalls(query, filter, start, length).toMutableList()
val calls = mutableListOf<CallLogRow>()
val callLimit = length - hasCallLinkRow.toInt()
if (start == 0 && length >= 1 && hasCallLinkRow) {
calls.add(CallLogRow.CreateCallLink)
}
calls.addAll(repository.getCalls(query, filter, start, callLimit).toMutableList())
if (calls.size < length && hasFilter) {
calls.add(CallLogRow.ClearFilter)
@ -31,6 +40,10 @@ class CallLogPagedDataSource(
override fun load(key: CallLogRow.Id?): CallLogRow = error("Not supported")
private fun Boolean.toInt(): Int {
return if (this) 1 else 0
}
interface CallRepository {
fun getCallsCount(query: String?, filter: CallLogFilter): Int
fun getCalls(query: String?, filter: CallLogFilter, start: Int, length: Int): List<CallLogRow>

View file

@ -27,8 +27,13 @@ sealed class CallLogRow {
override val id: Id = Id.ClearFilter
}
object CreateCallLink : CallLogRow() {
override val id: Id = Id.CreateCallLink
}
sealed class Id {
data class Call(val callId: Long) : Id()
object ClearFilter : Id()
object CreateCallLink : Id()
}
}

View file

@ -57,9 +57,9 @@ abstract class ComposeBottomSheetDialogFragment : FixedRoundedCornerBottomSheetD
* ```
*/
@Composable
protected fun Handle() {
protected fun Handle(modifier: Modifier = Modifier) {
Box(
modifier = Modifier
modifier = modifier
.size(width = 48.dp, height = 22.dp)
.padding(vertical = 10.dp)
.clip(RoundedCornerShape(1000.dp))

View file

@ -0,0 +1,34 @@
package org.thoughtcrime.securesms.compose
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.DialogFragment
import org.signal.core.ui.theme.SignalTheme
import org.thoughtcrime.securesms.util.DynamicTheme
/**
* Generic ComposeFragment which can be subclassed to build UI with compose.
*/
abstract class ComposeDialogFragment : DialogFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
SignalTheme(
isDarkMode = DynamicTheme.isDarkTheme(LocalContext.current)
) {
DialogContent()
}
}
}
}
@Composable
abstract fun DialogContent()
}

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M16.1 12.88c-0.35 0.34-0.35 0.9 0 1.24 0.34 0.34 0.89 0.34 1.23 0l3-3c1.03-1.03 1.55-2.38 1.55-3.73 0-2.9-2.36-5.26-5.27-5.26-1.35 0-2.7 0.51-3.73 1.54l-3 3C9.54 7 9.54 7.57 9.88 7.9c0.34 0.34 0.9 0.34 1.24 0l3-3c0.69-0.7 1.58-1.04 2.49-1.04 1.94 0 3.52 1.58 3.52 3.52 0 0.9-0.35 1.8-1.04 2.5l-3 3Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M15.6 8.37c0.35 0.34 0.35 0.9 0 1.23l-6 6c-0.34 0.35-0.9 0.35-1.23 0-0.35-0.34-0.35-0.9 0-1.23l6-6c0.34-0.35 0.9-0.35 1.23 0Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M7.9 9.88c0.35 0.34 0.35 0.9 0 1.24l-3 3C4.23 14.8 3.88 15.7 3.88 16.6c0 1.94 1.58 3.52 3.52 3.52 0.9 0 1.8-0.35 2.5-1.04l3-3c0.33-0.34 0.89-0.34 1.23 0 0.34 0.35 0.34 0.9 0 1.24l-3 3c-1.03 1.03-2.38 1.55-3.73 1.55-2.9 0-5.26-2.36-5.26-5.27 0-1.35 0.51-2.7 1.54-3.73l3-3c0.34-0.34 0.9-0.34 1.24 0Z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="40"
android:viewportHeight="40">
<path
android:fillColor="#FF000000"
android:pathData="M11.95 6.25h8.1c1.36 0 2.45 0 3.34 0.07 0.9 0.08 1.7 0.23 2.45 0.61 1.17 0.6 2.13 1.56 2.73 2.73 0.38 0.74 0.53 1.54 0.6 2.45 0.08 0.89 0.08 1.98 0.08 3.34v0.03l5.3-5.3c1.74-1.74 4.7-0.5 4.7 1.94v15.76c0 2.45-2.96 3.68-4.7 1.94l-5.3-5.3v0.03c0 1.36 0 2.45-0.07 3.34-0.08 0.9-0.23 1.7-0.61 2.45-0.6 1.17-1.56 2.13-2.73 2.73-0.74 0.38-1.54 0.53-2.45 0.6-0.89 0.08-1.98 0.08-3.34 0.08h-8.1c-1.36 0-2.45 0-3.34-0.07-0.9-0.08-1.7-0.23-2.45-0.61-1.17-0.6-2.13-1.56-2.73-2.73-0.38-0.74-0.53-1.54-0.6-2.45C2.74 27 2.74 25.9 2.74 24.55v-9.1c0-1.36 0-2.45 0.07-3.34 0.08-0.9 0.23-1.7 0.61-2.45C4.03 8.5 5 7.53 6.16 6.93 6.9 6.55 7.7 6.4 8.61 6.33c0.89-0.08 1.98-0.08 3.34-0.08Zm14.8 9.25c0-1.42 0-2.41-0.06-3.18-0.07-0.76-0.18-1.2-0.35-1.52-0.36-0.7-0.93-1.28-1.64-1.64-0.33-0.17-0.76-0.28-1.52-0.35-0.77-0.06-1.76-0.06-3.18-0.06h-8c-1.42 0-2.41 0-3.18 0.06C8.06 8.88 7.62 9 7.3 9.16c-0.7 0.36-1.28 0.93-1.64 1.64-0.17 0.33-0.28 0.76-0.35 1.52-0.06 0.77-0.06 1.76-0.06 3.18v9c0 1.42 0 2.41 0.06 3.18 0.07 0.76 0.18 1.2 0.35 1.52 0.36 0.7 0.93 1.28 1.64 1.64 0.33 0.17 0.76 0.28 1.52 0.35 0.77 0.06 1.76 0.06 3.18 0.06h8c1.42 0 2.41 0 3.18-0.06 0.76-0.07 1.2-0.18 1.52-0.35 0.7-0.36 1.28-0.93 1.64-1.64 0.17-0.33 0.28-0.76 0.35-1.52 0.06-0.77 0.06-1.76 0.06-3.18v-9Zm2.5 4.5c0 0.63 0.25 1.23 0.7 1.68l6.37 6.38c0.05 0.05 0.1 0.06 0.13 0.07 0.04 0 0.1 0 0.15-0.02 0.05-0.02 0.09-0.06 0.11-0.09 0.02-0.03 0.04-0.07 0.04-0.14V12.12c0-0.07-0.02-0.11-0.04-0.14-0.02-0.03-0.06-0.07-0.11-0.09-0.06-0.02-0.1-0.03-0.15-0.02-0.03 0-0.08 0.02-0.13 0.07l-6.38 6.38c-0.44 0.45-0.69 1.05-0.69 1.68Z"/>
</vector>

View file

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="12dp"
android:layout_marginVertical="2dp"
android:background="@drawable/selectable_list_item_background"
android:minHeight="60dp">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/call_link_icon"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="12dp"
android:background="@color/signal_colorSecondaryContainer"
android:scaleType="centerInside"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Signal.Circle"
app:srcCompat="@drawable/symbol_link_24"
app:tint="@color/signal_colorOnPrimaryContainer" />
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/call_link_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:gravity="start|center_vertical"
android:maxLines="2"
android:text="@string/CreateCallLink__create_a_call_link"
android:textAlignment="viewStart"
android:textAppearance="@style/Signal.Text.BodyLarge"
app:layout_constraintBottom_toTopOf="@+id/call_link_description"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@+id/call_link_icon"
app:layout_constraintTop_toTopOf="@+id/call_link_icon" />
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/call_link_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:drawablePadding="6dp"
android:ellipsize="end"
android:gravity="start|center_vertical"
android:maxLines="1"
android:text="@string/CreateCallLink__share_a_link_for"
android:textAlignment="viewStart"
android:textAppearance="@style/Signal.Text.BodyMedium"
android:textColor="@color/signal_text_secondary"
app:layout_constraintBottom_toBottomOf="@+id/call_link_icon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@+id/call_link_icon"
app:layout_constraintTop_toBottomOf="@+id/call_link_title" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -44,6 +44,14 @@
<action
android:id="@+id/action_callLogFragment_to_storiesLandingFragment"
app:destination="@id/storiesLandingFragment" />
<action
android:id="@+id/action_callLogFragment_to_createCallLinkBottomSheet"
app:destination="@id/createCallLinkBottomSheet" />
</fragment>
<dialog
android:id="@+id/createCallLinkBottomSheet"
android:name="org.thoughtcrime.securesms.calls.links.CreateCallLinkBottomSheetDialogFragment"
android:label="create_call_link_bottom_sheet" />
</navigation>

View file

@ -5859,5 +5859,43 @@
<!-- Outgoing group call in call info -->
<string name="CallPreference__outgoing_group_call">Outgoing group call</string>
<!-- CreateCallLink -->
<!-- Call link creation item title on calls tab -->
<string name="CreateCallLink__create_a_call_link">Create a Call Link</string>
<!-- Call link creation item description on calls tab -->
<string name="CreateCallLink__share_a_link_for">Share a link for a Signal call</string>
<!-- CreateCallLinkBottomSheetDialogFragment -->
<!-- Fragment title -->
<string name="CreateCallLinkBottomSheetDialogFragment__create_call_link">Create call link</string>
<!-- Displayed as a default name for the signal call -->
<string name="CreateCallLinkBottomSheetDialogFragment__signal_call">Signal call</string>
<!-- Displayed on a small button to allow user to instantly join call -->
<string name="CreateCallLinkBottomSheetDialogFragment__join">Join</string>
<!-- Option to open a full screen dialog to enter a call name -->
<string name="CreateCallLinkBottomSheetDialogFragment__add_call_name">Add call name</string>
<!-- Toggle to require approval for all members before joining -->
<string name="CreateCallLinkBottomSheetDialogFragment__approve_all_members">Approve all members</string>
<!-- Row label to share the link via Signal -->
<string name="CreateCallLinkBottomSheetDialogFragment__share_link_via_signal">Share link via Signal</string>
<!-- Row label to copy the link to the clipboard -->
<string name="CreateCallLinkBottomSheetDialogFragment__copy_link">Copy link</string>
<!-- Row label to share the link with the external share sheet -->
<string name="CreateCallLinkBottomSheetDialogFragment__share_link">Share link</string>
<!-- Button text to dismiss the sheet and add it as an upcoming call -->
<string name="CreateCallLinkBottomSheetDialogFragment__done">Done</string>
<!-- Displayed when we can't find a suitable way to open the system share picker -->
<string name="CreateCallLinkBottomSheetDialogFragment__failed_to_open_share_sheet">Failed to open share sheet.</string>
<!-- Displayed when we copy the call link to the clipboard -->
<string name="CreateCallLinkBottomSheetDialogFragment__copied_to_clipboard">Copied to clipboard</string>
<!-- EditCallLinkNameDialogFragment -->
<!-- App bar title for editing a call name -->
<string name="EditCallLinkNameDialogFragment__edit_call_name">Edit call name</string>
<!-- Text on button to confirm edit -->
<string name="EditCallLinkNameDialogFragment__save">Save</string>
<!-- Placeholder text on input field when editing call name -->
<string name="EditCallLinkNameDialogFragment__call_name">Call name</string>
<!-- EOF -->
</resources>

View file

@ -2,11 +2,16 @@ package org.signal.core.ui
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -14,7 +19,9 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
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.vector.ImageVector
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@ -22,6 +29,7 @@ import androidx.compose.ui.unit.sp
import org.signal.core.ui.theme.SignalTheme
object Rows {
/**
* A row consisting of a radio button and text, which takes up the full
* width of the screen.
@ -36,10 +44,7 @@ object Rows {
Row(
modifier = modifier
.fillMaxWidth()
.padding(
horizontal = dimensionResource(id = R.dimen.core_ui__gutter),
vertical = 16.dp
),
.padding(defaultPadding()),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
@ -65,6 +70,76 @@ object Rows {
}
}
}
@Composable
fun ToggleRow(
checked: Boolean,
text: String,
onCheckChanged: (Boolean) -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier
.fillMaxWidth()
.padding(defaultPadding())
) {
Text(
text = text,
modifier = Modifier
.weight(1f)
.align(CenterVertically)
)
Switch(
checked = checked,
onCheckedChange = onCheckChanged,
modifier = Modifier.align(CenterVertically)
)
}
}
@Composable
fun TextRow(
text: String,
modifier: Modifier = Modifier,
icon: ImageVector? = null
) {
if (icon != null) {
Row(
modifier = modifier
.fillMaxWidth()
.padding(defaultPadding())
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurface
)
Spacer(modifier = Modifier.width(24.dp))
Text(
text = text,
modifier = Modifier.weight(1f)
)
}
} else {
Text(
text = text,
modifier = modifier
.fillMaxWidth()
.padding(defaultPadding())
)
}
}
@Composable
private fun defaultPadding(): PaddingValues {
return PaddingValues(
horizontal = dimensionResource(id = R.dimen.core_ui__gutter),
vertical = 16.dp
)
}
}
@Preview
@ -83,3 +158,28 @@ private fun RadioRowPreview() {
)
}
}
@Preview
@Composable
private fun ToggleRowPreview() {
SignalTheme(isDarkMode = false) {
var checked by remember { mutableStateOf(false) }
Rows.ToggleRow(
checked = checked,
text = "ToggleRow",
onCheckChanged = {
checked = it
}
)
}
}
@Preview
@Composable
private fun TextRowPreview() {
SignalTheme(isDarkMode = false) {
Rows.TextRow(text = "TextRow")
Rows.TextRow(text = "TextRow")
}
}