diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/links/CreateCallLinkBottomSheetDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/links/CreateCallLinkBottomSheetDialogFragment.kt new file mode 100644 index 0000000000..5f65fc5b3c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/links/CreateCallLinkBottomSheetDialogFragment.kt @@ -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)) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/links/CreateCallLinkViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/links/CreateCallLinkViewModel.kt new file mode 100644 index 0000000000..be9d693f80 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/links/CreateCallLinkViewModel.kt @@ -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 = mutableStateOf("") + private val _callLink: MutableState = mutableStateOf("") + private val _approveAllMembers: MutableState = mutableStateOf(false) + + val callName: State = _callName + val callLink: State = _callLink + val approveAllMembers: State = _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 + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/links/EditCallLinkNameDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/links/EditCallLinkNameDialogFragment.kt new file mode 100644 index 0000000000..344f2959d0 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/links/EditCallLinkNameDialogFragment.kt @@ -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() + } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogAdapter.kt index ceaf1c1685..44c95f83b2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogAdapter.kt @@ -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 { + 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(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 */ diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogFragment.kt index 2b02a9a16b..685d529c80 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogFragment.kt @@ -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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogPagedDataSource.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogPagedDataSource.kt index f840d1c2b1..464f03a16a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogPagedDataSource.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogPagedDataSource.kt @@ -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 { 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 { - val calls: MutableList = repository.getCalls(query, filter, start, length).toMutableList() + val calls = mutableListOf() + 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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogRow.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogRow.kt index b279cb7b45..ad540bb2dd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogRow.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogRow.kt @@ -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() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/compose/ComposeBottomSheetDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/compose/ComposeBottomSheetDialogFragment.kt index 01728a00d4..56e32043f5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/compose/ComposeBottomSheetDialogFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/compose/ComposeBottomSheetDialogFragment.kt @@ -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)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/compose/ComposeDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/compose/ComposeDialogFragment.kt new file mode 100644 index 0000000000..a27dcd422a --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/compose/ComposeDialogFragment.kt @@ -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() +} diff --git a/app/src/main/res/drawable/symbol_link_24.xml b/app/src/main/res/drawable/symbol_link_24.xml new file mode 100644 index 0000000000..3f1c198a65 --- /dev/null +++ b/app/src/main/res/drawable/symbol_link_24.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/symbol_video_display_bold_40.xml b/app/src/main/res/drawable/symbol_video_display_bold_40.xml new file mode 100644 index 0000000000..cbdb0f91cd --- /dev/null +++ b/app/src/main/res/drawable/symbol_video_display_bold_40.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/call_log_create_call_link_item.xml b/app/src/main/res/layout/call_log_create_call_link_item.xml new file mode 100644 index 0000000000..09a15ff023 --- /dev/null +++ b/app/src/main/res/layout/call_log_create_call_link_item.xml @@ -0,0 +1,62 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/main_activity_list.xml b/app/src/main/res/navigation/main_activity_list.xml index 469e332fef..f09e338adb 100644 --- a/app/src/main/res/navigation/main_activity_list.xml +++ b/app/src/main/res/navigation/main_activity_list.xml @@ -44,6 +44,14 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6c4dd5e039..0e36fb5d54 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -5859,5 +5859,43 @@ Outgoing group call + + + Create a Call Link + + Share a link for a Signal call + + + + Create call link + + Signal call + + Join + + Add call name + + Approve all members + + Share link via Signal + + Copy link + + Share link + + Done + + Failed to open share sheet. + + Copied to clipboard + + + + Edit call name + + Save + + Call name + diff --git a/core-ui/src/main/java/org/signal/core/ui/Rows.kt b/core-ui/src/main/java/org/signal/core/ui/Rows.kt index 7e2b9ab689..0132d19024 100644 --- a/core-ui/src/main/java/org/signal/core/ui/Rows.kt +++ b/core-ui/src/main/java/org/signal/core/ui/Rows.kt @@ -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") + } +}