mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 13:08:46 +00:00
Convert AddAllowedMembersFragment to compose.
This commit is contained in:
committed by
Jeffrey Starke
parent
958dde0f6e
commit
ecddf34083
@@ -2,32 +2,57 @@ package org.thoughtcrime.securesms.components.settings.app.notifications.profile
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import kotlinx.coroutines.rx3.asFlow
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.Rows
|
||||
import org.signal.core.ui.compose.Scaffolds
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.signal.core.ui.compose.Snackbars
|
||||
import org.signal.core.ui.compose.Texts
|
||||
import org.signal.core.ui.compose.horizontalGutters
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.models.NotificationProfileAddMembers
|
||||
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.models.NotificationProfileRecipient
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.RecipientPreference
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiStrings
|
||||
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.AddAllowedMembersViewModel.NotificationProfileAndRecipients
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.database.RecipientTable
|
||||
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
||||
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfileId
|
||||
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfileSchedule
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Show and allow addition of recipients to a profile during the create flow.
|
||||
*/
|
||||
class AddAllowedMembersFragment : DSLSettingsFragment(layoutId = R.layout.fragment_add_allowed_members) {
|
||||
class AddAllowedMembersFragment : ComposeFragment() {
|
||||
|
||||
private val viewModel: AddAllowedMembersViewModel by viewModels(factoryProducer = { AddAllowedMembersViewModel.Factory(profileId) })
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
@@ -37,90 +62,18 @@ class AddAllowedMembersFragment : DSLSettingsFragment(layoutId = R.layout.fragme
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
lifecycleDisposable.bindTo(viewLifecycleOwner.lifecycle)
|
||||
|
||||
view.findViewById<CircularProgressMaterialButton>(R.id.add_allowed_members_profile_next).apply {
|
||||
setOnClickListener {
|
||||
findNavController().safeNavigate(AddAllowedMembersFragmentDirections.actionAddAllowedMembersFragmentToEditNotificationProfileScheduleFragment(profileId, true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun bindAdapter(adapter: MappingAdapter) {
|
||||
NotificationProfileAddMembers.register(adapter)
|
||||
NotificationProfileRecipient.register(adapter)
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
val state by remember { viewModel.getProfile().map { GetProfileResult.Ready(it) }.asFlow() }
|
||||
.collectAsStateWithLifecycle(GetProfileResult.Loading)
|
||||
val callbacks = remember { Callbacks() }
|
||||
|
||||
lifecycleDisposable += viewModel.getProfile()
|
||||
.subscribeBy(
|
||||
onNext = { (profile, recipients) ->
|
||||
adapter.submitList(getConfiguration(profile, recipients).toMappingModelList())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun getConfiguration(profile: NotificationProfile, recipients: List<Recipient>): DSLConfiguration {
|
||||
return configure {
|
||||
sectionHeaderPref(R.string.AddAllowedMembers__allowed_notifications)
|
||||
|
||||
customPref(
|
||||
NotificationProfileAddMembers.Model(
|
||||
onClick = { id, currentSelection ->
|
||||
findNavController().safeNavigate(
|
||||
AddAllowedMembersFragmentDirections.actionAddAllowedMembersFragmentToSelectRecipientsFragment(id)
|
||||
.setCurrentSelection(currentSelection.toTypedArray())
|
||||
)
|
||||
},
|
||||
profileId = profile.id,
|
||||
currentSelection = profile.allowedMembers
|
||||
)
|
||||
)
|
||||
|
||||
for (member in recipients) {
|
||||
customPref(
|
||||
NotificationProfileRecipient.Model(
|
||||
recipientModel = RecipientPreference.Model(
|
||||
recipient = member,
|
||||
onClick = {}
|
||||
),
|
||||
onRemoveClick = { id ->
|
||||
lifecycleDisposable += viewModel.removeMember(id)
|
||||
.subscribeBy(
|
||||
onSuccess = { removed ->
|
||||
view?.let { view ->
|
||||
Snackbar.make(view, getString(R.string.NotificationProfileDetails__s_removed, removed.getDisplayName(requireContext())), Snackbar.LENGTH_LONG)
|
||||
.setAction(R.string.NotificationProfileDetails__undo) { undoRemove(id) }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
sectionHeaderPref(R.string.AddAllowedMembers__exceptions)
|
||||
|
||||
switchPref(
|
||||
title = DSLSettingsText.from(R.string.AddAllowedMembers__allow_all_calls),
|
||||
icon = DSLSettingsIcon.from(R.drawable.symbol_phone_24),
|
||||
isChecked = profile.allowAllCalls,
|
||||
onClick = {
|
||||
lifecycleDisposable += viewModel.toggleAllowAllCalls()
|
||||
.subscribeBy(
|
||||
onError = { Log.w(TAG, "Error updating profile", it) }
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
switchPref(
|
||||
title = DSLSettingsText.from(R.string.AddAllowedMembers__notify_for_all_mentions),
|
||||
icon = DSLSettingsIcon.from(R.drawable.symbol_at_24),
|
||||
isChecked = profile.allowAllMentions,
|
||||
onClick = {
|
||||
lifecycleDisposable += viewModel.toggleAllowAllMentions()
|
||||
.subscribeBy(
|
||||
onError = { Log.w(TAG, "Error updating profile", it) }
|
||||
)
|
||||
}
|
||||
if (state is GetProfileResult.Ready) {
|
||||
AddAllowedMembersContent(
|
||||
state = (state as GetProfileResult.Ready).notificationProfileAndRecipients,
|
||||
callbacks = callbacks
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -133,4 +86,199 @@ class AddAllowedMembersFragment : DSLSettingsFragment(layoutId = R.layout.fragme
|
||||
companion object {
|
||||
private val TAG = Log.tag(AddAllowedMembersFragment::class.java)
|
||||
}
|
||||
|
||||
private inner class Callbacks : AddAllowedMembersCallbacks {
|
||||
override fun onNavigationClick() {
|
||||
requireActivity().onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
|
||||
override fun onAllowAllCallsChanged(enabled: Boolean) {
|
||||
lifecycleDisposable += viewModel.toggleAllowAllCalls()
|
||||
.subscribeBy(
|
||||
onError = { Log.w(TAG, "Error updating profile", it) }
|
||||
)
|
||||
}
|
||||
|
||||
override fun onNotifyForAllMentionsChanged(enabled: Boolean) {
|
||||
lifecycleDisposable += viewModel.toggleAllowAllMentions()
|
||||
.subscribeBy(
|
||||
onError = { Log.w(TAG, "Error updating profile", it) }
|
||||
)
|
||||
}
|
||||
|
||||
override fun onAddMembersClick(id: Long, allowedMembers: Set<RecipientId>) {
|
||||
findNavController().safeNavigate(
|
||||
AddAllowedMembersFragmentDirections.actionAddAllowedMembersFragmentToSelectRecipientsFragment(id)
|
||||
.setCurrentSelection(allowedMembers.toTypedArray())
|
||||
)
|
||||
}
|
||||
|
||||
override fun onRemoveMemberClick(id: RecipientId) {
|
||||
lifecycleDisposable += viewModel.removeMember(id)
|
||||
.subscribeBy(
|
||||
onSuccess = { removed ->
|
||||
view?.let { view ->
|
||||
Snackbar.make(view, getString(R.string.NotificationProfileDetails__s_removed, removed.getDisplayName(requireContext())), Snackbar.LENGTH_LONG)
|
||||
.setAction(R.string.NotificationProfileDetails__undo) { undoRemove(id) }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onNextClick() {
|
||||
findNavController().safeNavigate(AddAllowedMembersFragmentDirections.actionAddAllowedMembersFragmentToEditNotificationProfileScheduleFragment(profileId, true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed interface GetProfileResult {
|
||||
data object Loading : GetProfileResult
|
||||
data class Ready(val notificationProfileAndRecipients: NotificationProfileAndRecipients) : GetProfileResult
|
||||
}
|
||||
|
||||
private interface AddAllowedMembersCallbacks {
|
||||
fun onNavigationClick() = Unit
|
||||
fun onAllowAllCallsChanged(enabled: Boolean) = Unit
|
||||
fun onNotifyForAllMentionsChanged(enabled: Boolean) = Unit
|
||||
fun onAddMembersClick(id: Long, allowedMembers: Set<RecipientId>) = Unit
|
||||
fun onRemoveMemberClick(id: RecipientId) = Unit
|
||||
fun onNextClick() = Unit
|
||||
|
||||
object Empty : AddAllowedMembersCallbacks
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AddAllowedMembersContent(
|
||||
state: NotificationProfileAndRecipients,
|
||||
callbacks: AddAllowedMembersCallbacks,
|
||||
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }
|
||||
) {
|
||||
Scaffolds.Settings(
|
||||
title = "",
|
||||
onNavigationClick = callbacks::onNavigationClick,
|
||||
navigationIcon = ImageVector.vectorResource(R.drawable.symbol_arrow_start_24),
|
||||
snackbarHost = {
|
||||
Snackbars.Host(snackbarHostState)
|
||||
}
|
||||
) { contentPadding ->
|
||||
Column(
|
||||
modifier = Modifier.padding(contentPadding)
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(R.string.AddAllowedMembers__allowed_notifications),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier
|
||||
.horizontalGutters()
|
||||
.padding(top = 20.dp)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.AddAllowedMembers__add_people_and_groups_you_want_notifications_and_calls_from_when_this_profile_is_on),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.horizontalGutters()
|
||||
.padding(top = 12.dp, bottom = 24.dp)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Texts.SectionHeader(
|
||||
text = stringResource(R.string.AddAllowedMembers__allowed_notifications)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
val callback = remember(state.profile.id, state.profile.allowedMembers) {
|
||||
{
|
||||
callbacks.onAddMembersClick(state.profile.id, state.profile.allowedMembers)
|
||||
}
|
||||
}
|
||||
|
||||
NotificationProfileAddMembers(onClick = callback)
|
||||
}
|
||||
|
||||
for (member in state.recipients) {
|
||||
item(key = member.id) {
|
||||
NotificationProfileRecipient(
|
||||
recipient = member,
|
||||
onRemoveClick = callbacks::onRemoveMemberClick
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Texts.SectionHeader(
|
||||
text = stringResource(R.string.AddAllowedMembers__exceptions)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.ToggleRow(
|
||||
checked = state.profile.allowAllCalls,
|
||||
text = stringResource(R.string.AddAllowedMembers__allow_all_calls),
|
||||
icon = ImageVector.vectorResource(R.drawable.symbol_phone_24),
|
||||
onCheckChanged = callbacks::onAllowAllCallsChanged
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Rows.ToggleRow(
|
||||
checked = state.profile.allowAllMentions,
|
||||
text = stringResource(R.string.AddAllowedMembers__notify_for_all_mentions),
|
||||
icon = ImageVector.vectorResource(R.drawable.symbol_at_24),
|
||||
onCheckChanged = callbacks::onNotifyForAllMentionsChanged
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Buttons.LargeTonal(
|
||||
onClick = callbacks::onNextClick,
|
||||
modifier = Modifier
|
||||
.align(Alignment.End)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(text = stringResource(R.string.EditNotificationProfileFragment__next))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun AddAllowedMembersContentPreview() {
|
||||
Previews.Preview {
|
||||
AddAllowedMembersContent(
|
||||
state = NotificationProfileAndRecipients(
|
||||
profile = NotificationProfile(
|
||||
id = 0L,
|
||||
name = "Test Profile",
|
||||
emoji = EmojiStrings.PHOTO,
|
||||
createdAt = System.currentTimeMillis(),
|
||||
schedule = NotificationProfileSchedule(
|
||||
id = 0L
|
||||
),
|
||||
notificationProfileId = NotificationProfileId(UUID.randomUUID())
|
||||
),
|
||||
recipients = (1..3).map {
|
||||
Recipient(
|
||||
id = RecipientId.from(it.toLong()),
|
||||
isResolving = false,
|
||||
registeredValue = RecipientTable.RegisteredState.REGISTERED,
|
||||
systemContactName = "Test User $it"
|
||||
)
|
||||
}
|
||||
),
|
||||
callbacks = AddAllowedMembersCallbacks.Empty
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,31 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.notifications.profiles
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
class AddAllowedMembersViewModel(private val profileId: Long, private val repository: NotificationProfilesRepository) : ViewModel() {
|
||||
|
||||
private val internalSnackbarRequests = MutableSharedFlow<Unit>()
|
||||
|
||||
val snackbarRequests: Flow<Unit> = internalSnackbarRequests
|
||||
|
||||
fun requestSnackbar() {
|
||||
viewModelScope.launch {
|
||||
internalSnackbarRequests.emit(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
fun getProfile(): Observable<NotificationProfileAndRecipients> {
|
||||
return repository.getProfile(profileId)
|
||||
.map { profile ->
|
||||
@@ -40,6 +55,7 @@ class AddAllowedMembersViewModel(private val profileId: Long, private val reposi
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class NotificationProfileAndRecipients(val profile: NotificationProfile, val recipients: List<Recipient>)
|
||||
|
||||
class Factory(private val profileId: Long) : ViewModelProvider.Factory {
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.notifications.profiles
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
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.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.Rows
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.signal.core.ui.compose.horizontalGutters
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.avatar.AvatarImage
|
||||
import org.thoughtcrime.securesms.components.emoji.Emojifier
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.BadgeImageMedium
|
||||
import org.thoughtcrime.securesms.database.RecipientTable
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.recipients.rememberRecipientField
|
||||
|
||||
@Composable
|
||||
fun NotificationProfileAddMembers(
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Rows.TextRow(
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_plus_24),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.background(
|
||||
color = SignalTheme.colors.colorSurface1,
|
||||
shape = CircleShape
|
||||
)
|
||||
.padding(8.dp)
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(R.string.AddAllowedMembers__add_people_or_groups),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
},
|
||||
onClick = onClick
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NotificationProfileRecipient(
|
||||
recipient: Recipient,
|
||||
onRemoveClick: (RecipientId) -> Unit
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.defaultMinSize(minHeight = 64.dp)
|
||||
.horizontalGutters()
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val featuredBadge by rememberRecipientField(recipient) { recipient.featuredBadge }
|
||||
val displayName by rememberRecipientField(recipient) { recipient.getDisplayName(context) }
|
||||
|
||||
Box(
|
||||
modifier = Modifier.padding(top = 6.dp)
|
||||
) {
|
||||
AvatarImage(
|
||||
recipient = recipient,
|
||||
modifier = Modifier.size(40.dp)
|
||||
)
|
||||
|
||||
BadgeImageMedium(
|
||||
badge = featuredBadge,
|
||||
modifier = Modifier
|
||||
.padding(top = 22.dp, start = 20.dp)
|
||||
.size(24.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.size(20.dp))
|
||||
|
||||
Emojifier(displayName) { string, map ->
|
||||
Text(
|
||||
text = string,
|
||||
inlineContent = map,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = {
|
||||
onRemoveClick(recipient.id)
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_minus_circle_20),
|
||||
contentDescription = stringResource(R.string.delete),
|
||||
tint = colorResource(R.color.core_grey_45)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
fun NotificationProfileAddMembersPreview() {
|
||||
Previews.Preview {
|
||||
NotificationProfileAddMembers(
|
||||
onClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
fun NotificationProfileRecipientPreview() {
|
||||
Previews.Preview {
|
||||
NotificationProfileRecipient(
|
||||
recipient = Recipient(
|
||||
id = RecipientId.from(1L),
|
||||
isResolving = false,
|
||||
registeredValue = RecipientTable.RegisteredState.REGISTERED,
|
||||
systemContactName = "Miles Morales"
|
||||
),
|
||||
onRemoveClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,7 @@ import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.signal.core.ui.compose.TextFields
|
||||
import org.signal.core.ui.compose.Tooltips
|
||||
import org.signal.core.ui.compose.circularReveal
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.avatar.AvatarImage
|
||||
import org.thoughtcrime.securesms.calls.log.CallLogFilter
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:viewBindingIgnore="true"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<org.thoughtcrime.securesms.util.views.DarkOverflowToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:theme="?attr/settingsToolbarStyle"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:navigationContentDescription="@string/DSLSettingsToolbar__navigate_up"
|
||||
app:navigationIcon="@drawable/symbol_arrow_start_24"
|
||||
app:titleTextAppearance="@style/Signal.Text.Title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/edit_notification_profile_schedule_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/AddAllowedMembers__allowed_notifications"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Title1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/edit_notification_profile_schedule_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/AddAllowedMembers__add_people_and_groups_you_want_notifications_and_calls_from_when_this_profile_is_on"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/edit_notification_profile_schedule_title" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="24dp"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:paddingBottom="60dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/edit_notification_profile_schedule_description" />
|
||||
|
||||
<View
|
||||
android:id="@+id/toolbar_shadow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="5dp"
|
||||
android:alpha="0"
|
||||
android:background="@drawable/toolbar_shadow"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/recycler" />
|
||||
|
||||
<org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton
|
||||
android:id="@+id/add_allowed_members_profile_next"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
app:circularProgressMaterialButton__label="@string/EditNotificationProfileFragment__next"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -973,8 +973,7 @@
|
||||
|
||||
<fragment
|
||||
android:id="@+id/addAllowedMembersFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.notifications.profiles.AddAllowedMembersFragment"
|
||||
tools:layout="@layout/fragment_add_allowed_members">
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.notifications.profiles.AddAllowedMembersFragment">
|
||||
|
||||
<argument
|
||||
android:name="profileId"
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.core.ui.compose
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.EnterExitState
|
||||
import androidx.compose.animation.EnterTransition
|
||||
import androidx.compose.animation.ExitTransition
|
||||
import androidx.compose.animation.core.LinearOutSlowInEasing
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Text
|
||||
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
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* Utilizes a circular reveal animation to display and hide the content given.
|
||||
* When the content is hidden via settings [isLoading] to true, we display a
|
||||
* circular progress indicator.
|
||||
*
|
||||
* This component will automatically size itself according to the content passed
|
||||
* in via [content]
|
||||
*/
|
||||
@Composable
|
||||
fun CircularProgressWrapper(
|
||||
isLoading: Boolean,
|
||||
content: @Composable () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = modifier
|
||||
) {
|
||||
var size by remember { mutableStateOf(IntSize.Zero) }
|
||||
|
||||
val dpSize = with(LocalDensity.current) {
|
||||
DpSize(size.width.toDp(), size.height.toDp())
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = isLoading,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut()
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier.size(dpSize)
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = !isLoading,
|
||||
enter = EnterTransition.None,
|
||||
exit = ExitTransition.None
|
||||
) {
|
||||
val visibility = transition.animateFloat(
|
||||
transitionSpec = { tween(durationMillis = 400, easing = LinearOutSlowInEasing) },
|
||||
label = "CircularProgressWrapper-Visibility"
|
||||
) { state ->
|
||||
if (state == EnterExitState.Visible) 1f else 0f
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.onSizeChanged { s ->
|
||||
size = s
|
||||
}
|
||||
.circularReveal(visibility)
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
fun CircularProgressWrapperPreview() {
|
||||
var isLoading by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
LaunchedEffect(isLoading) {
|
||||
if (isLoading) {
|
||||
delay(3.seconds)
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
Previews.Preview {
|
||||
CircularProgressWrapper(
|
||||
isLoading = isLoading,
|
||||
content = {
|
||||
Buttons.LargeTonal(onClick = {
|
||||
isLoading = true
|
||||
}) {
|
||||
Text(text = "Next")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.main
|
||||
package org.signal.core.ui.compose
|
||||
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -156,6 +156,7 @@ object Rows {
|
||||
onCheckChanged: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
label: String? = null,
|
||||
icon: ImageVector? = null,
|
||||
textColor: Color = MaterialTheme.colorScheme.onSurface,
|
||||
isLoading: Boolean = false
|
||||
) {
|
||||
@@ -168,6 +169,15 @@ object Rows {
|
||||
.padding(defaultPadding()),
|
||||
verticalAlignment = CenterVertically
|
||||
) {
|
||||
if (icon != null) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(24.dp))
|
||||
}
|
||||
|
||||
TextAndLabel(
|
||||
text = text,
|
||||
label = label,
|
||||
|
||||
Reference in New Issue
Block a user