mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-05-03 23:15:44 +01:00
Add member labels education sheet.
This commit is contained in:
committed by
Cody Henthorne
parent
955bcde062
commit
0b2d3edcce
@@ -244,6 +244,10 @@ sealed class GroupId(private val encodedId: String) : DatabaseId, Parcelable {
|
||||
return this as V2
|
||||
}
|
||||
|
||||
fun v2OrNull(): V2? {
|
||||
return if (isV2) (this as V2) else null
|
||||
}
|
||||
|
||||
fun requirePush(): Push {
|
||||
assert(this is Push)
|
||||
return this as Push
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.groups.memberlabel
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import org.signal.core.util.getParcelableExtraCompat
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
|
||||
/**
|
||||
* Hosts [MemberLabelFragment], allowing navigation to the member label editor from any context.
|
||||
*/
|
||||
class MemberLabelActivity : PassphraseRequiredActivity() {
|
||||
companion object {
|
||||
private const val EXTRA_GROUP_ID = "group_id"
|
||||
|
||||
fun createIntent(context: Context, groupId: GroupId.V2): Intent {
|
||||
return Intent(context, MemberLabelActivity::class.java).apply {
|
||||
putExtra(EXTRA_GROUP_ID, groupId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
super.onCreate(savedInstanceState, ready)
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
val groupId = intent.getParcelableExtraCompat(EXTRA_GROUP_ID, GroupId.V2::class.java)!!
|
||||
val fragment = MemberLabelFragment.newInstance(groupId)
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(android.R.id.content, fragment)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.groups.memberlabel
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
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.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.signal.core.ui.compose.AllDevicePreviews
|
||||
import org.signal.core.ui.compose.BottomSheets
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.ComposeBottomSheetDialogFragment
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.util.requireParcelableCompat
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
|
||||
/**
|
||||
* Explains what member labels are and provides options to edit the current user's label.
|
||||
*/
|
||||
class MemberLabelEducationSheet : ComposeBottomSheetDialogFragment() {
|
||||
companion object {
|
||||
const val RESULT_EDIT_MEMBER_LABEL = "edit_member_label"
|
||||
const val KEY_GROUP_ID = "group_id"
|
||||
|
||||
private const val FRAGMENT_TAG = "MemberLabelEducationSheet"
|
||||
private const val ARGS_GROUP_ID = "group_id"
|
||||
|
||||
fun show(fragmentManager: FragmentManager, groupId: GroupId.V2) {
|
||||
val fragment = MemberLabelEducationSheet().apply {
|
||||
arguments = bundleOf(ARGS_GROUP_ID to groupId)
|
||||
}
|
||||
fragment.show(fragmentManager, FRAGMENT_TAG)
|
||||
}
|
||||
}
|
||||
|
||||
private val groupId: GroupId.V2 by lazy {
|
||||
requireArguments().requireParcelableCompat(ARGS_GROUP_ID, GroupId.V2::class.java)
|
||||
}
|
||||
|
||||
private val viewModel: MemberLabelEducationViewModel by viewModel {
|
||||
MemberLabelEducationViewModel(groupId)
|
||||
}
|
||||
|
||||
override val peekHeightPercentage: Float = 1f
|
||||
|
||||
@Composable
|
||||
override fun SheetContent() {
|
||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
val callbacks = remember {
|
||||
object : MemberLabelEducationUiCallbacks {
|
||||
override fun onSetLabelClicked() {
|
||||
setFragmentResult(RESULT_EDIT_MEMBER_LABEL, bundleOf(KEY_GROUP_ID to groupId))
|
||||
dismiss()
|
||||
}
|
||||
|
||||
override fun onDismiss() = dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
MemberLabelEducationSheetContent(
|
||||
state = state,
|
||||
callbacks = callbacks
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MemberLabelEducationSheetContent(
|
||||
state: MemberLabelEducationViewModel.UiState,
|
||||
callbacks: MemberLabelEducationUiCallbacks = MemberLabelEducationUiCallbacks.Empty
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.padding(top = 4.dp, bottom = 28.dp, start = 28.dp, end = 28.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
BottomSheets.Handle()
|
||||
|
||||
Image(
|
||||
painter = painterResource(R.drawable.symbol_tag_filled_64),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(top = 24.dp)
|
||||
.size(64.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.MemberLabelsEducation__title),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.padding(top = 16.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.MemberLabelsEducation__body),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(top = 12.dp)
|
||||
)
|
||||
|
||||
if (state.selfCanSetLabel) {
|
||||
TextButton(
|
||||
onClick = callbacks::onSetLabelClicked,
|
||||
modifier = Modifier.padding(top = 56.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(
|
||||
if (state.selfHasLabel) R.string.MemberLabelsEducation__edit_label
|
||||
else R.string.MemberLabelsEducation__set_label
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Spacer(modifier = Modifier.height(56.dp))
|
||||
}
|
||||
|
||||
Buttons.LargeTonal(
|
||||
onClick = callbacks::onDismiss,
|
||||
modifier = Modifier
|
||||
.padding(top = 16.dp)
|
||||
.defaultMinSize(minWidth = 220.dp)
|
||||
) {
|
||||
Text(text = stringResource(android.R.string.ok))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private interface MemberLabelEducationUiCallbacks {
|
||||
fun onSetLabelClicked()
|
||||
fun onDismiss()
|
||||
|
||||
object Empty : MemberLabelEducationUiCallbacks {
|
||||
override fun onSetLabelClicked() = Unit
|
||||
override fun onDismiss() = Unit
|
||||
}
|
||||
}
|
||||
|
||||
@AllDevicePreviews
|
||||
@Composable
|
||||
private fun MemberLabelEducationSheetPreviewCanSetNoLabel() = Previews.Preview {
|
||||
MemberLabelEducationSheetContent(
|
||||
state = MemberLabelEducationViewModel.UiState(
|
||||
selfHasLabel = false,
|
||||
selfCanSetLabel = true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun MemberLabelEducationSheetPreviewCanSetHasLabel() = Previews.Preview {
|
||||
MemberLabelEducationSheetContent(
|
||||
state = MemberLabelEducationViewModel.UiState(
|
||||
selfHasLabel = true,
|
||||
selfCanSetLabel = true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun MemberLabelEducationSheetPreviewCannotSet() = Previews.Preview {
|
||||
MemberLabelEducationSheetContent(
|
||||
state = MemberLabelEducationViewModel.UiState(
|
||||
selfHasLabel = false,
|
||||
selfCanSetLabel = false
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.groups.memberlabel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.signal.core.util.concurrent.SignalDispatchers
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
class MemberLabelEducationViewModel(
|
||||
private val groupId: GroupId.V2,
|
||||
private val repository: MemberLabelRepository = MemberLabelRepository.instance
|
||||
) : ViewModel() {
|
||||
|
||||
data class UiState(
|
||||
val selfHasLabel: Boolean = false,
|
||||
val selfCanSetLabel: Boolean = false
|
||||
)
|
||||
|
||||
private val _uiState = MutableStateFlow(UiState())
|
||||
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
|
||||
|
||||
init {
|
||||
viewModelScope.launch(SignalDispatchers.IO) {
|
||||
val self = Recipient.self()
|
||||
val selfMemberLabel = repository.getLabel(groupId, self.id)
|
||||
val selfCanSetLabel = repository.canSetLabel(groupId, self)
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
selfHasLabel = selfMemberLabel != null,
|
||||
selfCanSetLabel = selfCanSetLabel
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,8 +38,8 @@ import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import org.signal.core.ui.compose.AllDevicePreviews
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.ClearableTextField
|
||||
@@ -48,6 +48,7 @@ import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.Scaffolds
|
||||
import org.signal.core.ui.compose.SignalIcons
|
||||
import org.signal.core.util.isNotNullOrBlank
|
||||
import org.signal.core.util.requireParcelableCompat
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.groups.memberlabel.MemberLabelUiState.SaveState
|
||||
@@ -62,12 +63,22 @@ import org.thoughtcrime.securesms.util.viewModel
|
||||
class MemberLabelFragment : ComposeFragment(), ReactWithAnyEmojiBottomSheetDialogFragment.Callback {
|
||||
companion object {
|
||||
private const val EMOJI_PICKER_DIALOG_TAG = "emoji_picker_dialog"
|
||||
private const val ARG_GROUP_ID = "group_id"
|
||||
|
||||
fun newInstance(groupId: GroupId.V2): MemberLabelFragment {
|
||||
return MemberLabelFragment().apply {
|
||||
arguments = bundleOf(ARG_GROUP_ID to groupId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val groupId: GroupId.V2 by lazy {
|
||||
requireArguments().requireParcelableCompat(ARG_GROUP_ID, GroupId.V2::class.java)
|
||||
}
|
||||
|
||||
private val args: MemberLabelFragmentArgs by navArgs()
|
||||
private val viewModel: MemberLabelViewModel by viewModel {
|
||||
MemberLabelViewModel(
|
||||
groupId = (args.groupId as GroupId).requireV2(),
|
||||
groupId = groupId,
|
||||
recipientId = Recipient.self().id
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user