mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-26 20:55:10 +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 = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.main
|
||||
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawWithCache
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.drawscope.clipPath
|
||||
import kotlin.math.sqrt
|
||||
|
||||
/**
|
||||
* Circle Reveal Modifiers found here:
|
||||
* https://gist.github.com/darvld/eb3844474baf2f3fc6d3ab44a4b4b5f8
|
||||
*
|
||||
* A modifier that clips the composable content using a circular shape. The radius of the circle
|
||||
* will be determined by the [transitionProgress].
|
||||
*
|
||||
* The values of the progress should be between 0 and 1.
|
||||
*
|
||||
* By default, the circle is centered in the content, but custom positions may be specified using
|
||||
* [revealFrom]. Specified offsets should be between 0 (left/top) and 1 (right/bottom).
|
||||
*/
|
||||
fun Modifier.circularReveal(
|
||||
transitionProgress: State<Float>,
|
||||
revealFrom: Offset = Offset(0.5f, 0.5f)
|
||||
): Modifier {
|
||||
return drawWithCache {
|
||||
val path = Path()
|
||||
|
||||
val center = revealFrom.mapTo(size)
|
||||
val radius = calculateRadius(revealFrom, size)
|
||||
|
||||
path.addOval(Rect(center, radius * transitionProgress.value))
|
||||
|
||||
onDrawWithContent {
|
||||
clipPath(path) { this@onDrawWithContent.drawContent() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Offset.mapTo(size: Size): Offset {
|
||||
return Offset(x * size.width, y * size.height)
|
||||
}
|
||||
|
||||
private fun calculateRadius(normalizedOrigin: Offset, size: Size) = with(normalizedOrigin) {
|
||||
val x = (if (x > 0.5f) x else 1 - x) * size.width
|
||||
val y = (if (y > 0.5f) y else 1 - y) * size.height
|
||||
|
||||
sqrt(x * x + y * y)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user