mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-15 07:28:30 +00:00
Display member label on recipient details sheet.
This commit is contained in:
committed by
Greyson Parrelli
parent
fae4ca91bd
commit
d5b2f4fdd3
@@ -0,0 +1,97 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.groups.memberlabel
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.compositeOver
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import org.signal.core.ui.compose.DayNightPreviews
|
||||||
|
import org.signal.core.ui.compose.Previews
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays member label text with an optional emoji.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun MemberLabelPill(
|
||||||
|
emoji: String?,
|
||||||
|
text: String,
|
||||||
|
tintColor: Color,
|
||||||
|
modifier: Modifier = Modifier.padding(horizontal = 12.dp, vertical = 2.dp),
|
||||||
|
textStyle: TextStyle = MaterialTheme.typography.bodyLarge
|
||||||
|
) {
|
||||||
|
val isDark = isSystemInDarkTheme()
|
||||||
|
val backgroundColor = tintColor.copy(alpha = if (isDark) 0.32f else 0.10f)
|
||||||
|
|
||||||
|
val textColor = if (isDark) {
|
||||||
|
Color.White.copy(alpha = 0.25f).compositeOver(tintColor)
|
||||||
|
} else {
|
||||||
|
Color.Black.copy(alpha = 0.30f).compositeOver(tintColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
color = backgroundColor,
|
||||||
|
shape = RoundedCornerShape(percent = 50)
|
||||||
|
)
|
||||||
|
.then(modifier),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
if (!emoji.isNullOrEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = emoji,
|
||||||
|
style = textStyle,
|
||||||
|
modifier = Modifier.padding(end = 5.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.isNotEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
color = textColor,
|
||||||
|
style = textStyle,
|
||||||
|
maxLines = 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@DayNightPreviews
|
||||||
|
@Composable
|
||||||
|
private fun MemberLabelWithEmojiPreview() = Previews.Preview {
|
||||||
|
Box(modifier = Modifier.width(200.dp)) {
|
||||||
|
MemberLabelPill(
|
||||||
|
emoji = "🧠",
|
||||||
|
text = "Zero-Knowledge Know-It-All",
|
||||||
|
tintColor = Color(0xFF7A3DF5)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@DayNightPreviews
|
||||||
|
@Composable
|
||||||
|
private fun MemberLabelTextOnlyPreview() = Previews.Preview {
|
||||||
|
Box(modifier = Modifier.width(200.dp)) {
|
||||||
|
MemberLabelPill(
|
||||||
|
emoji = null,
|
||||||
|
text = "Zero-Knowledge Know-It-All",
|
||||||
|
tintColor = Color(0xFF7A3DF5)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.groups.memberlabel
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import androidx.annotation.ColorInt
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.AbstractComposeView
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see MemberLabelPill
|
||||||
|
*/
|
||||||
|
class MemberLabelPillView : AbstractComposeView {
|
||||||
|
constructor(context: Context) : super(context)
|
||||||
|
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||||
|
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||||
|
|
||||||
|
private var memberLabel: MemberLabel? by mutableStateOf(null)
|
||||||
|
private var tintColor: Color by mutableStateOf(Color.Unspecified)
|
||||||
|
|
||||||
|
fun setLabel(label: MemberLabel, tintColor: Color) {
|
||||||
|
this.memberLabel = label
|
||||||
|
this.tintColor = tintColor
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setLabel(label: MemberLabel, @ColorInt tintColor: Int) {
|
||||||
|
this.memberLabel = label
|
||||||
|
this.tintColor = Color(tintColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun Content() {
|
||||||
|
memberLabel?.let { label ->
|
||||||
|
MemberLabelPill(
|
||||||
|
emoji = label.emoji,
|
||||||
|
text = label.text,
|
||||||
|
tintColor = tintColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,9 @@
|
|||||||
package org.thoughtcrime.securesms.groups.memberlabel
|
package org.thoughtcrime.securesms.groups.memberlabel
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.signal.core.models.ServiceId
|
import org.signal.core.models.ServiceId
|
||||||
import org.signal.core.util.orNull
|
import org.signal.core.util.orNull
|
||||||
@@ -22,26 +24,45 @@ import org.thoughtcrime.securesms.util.RemoteConfig
|
|||||||
/**
|
/**
|
||||||
* Handles the retrieval and modification of group member labels.
|
* Handles the retrieval and modification of group member labels.
|
||||||
*/
|
*/
|
||||||
class MemberLabelRepository(
|
class MemberLabelRepository private constructor(
|
||||||
private val groupId: GroupId.V2,
|
|
||||||
private val context: Context = AppDependencies.application,
|
private val context: Context = AppDependencies.application,
|
||||||
private val groupsTable: GroupTable = SignalDatabase.groups
|
private val groupsTable: GroupTable = SignalDatabase.groups
|
||||||
) {
|
) {
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
val instance: MemberLabelRepository by lazy { MemberLabelRepository() }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the member label for a specific recipient in the group.
|
* Gets the member label for a specific recipient in the group.
|
||||||
*/
|
*/
|
||||||
suspend fun getLabel(recipientId: RecipientId): MemberLabel? = withContext(Dispatchers.IO) {
|
suspend fun getLabel(groupId: GroupId.V2, recipientId: RecipientId): MemberLabel? = withContext(Dispatchers.IO) {
|
||||||
val recipient = Recipient.resolved(recipientId)
|
getLabel(groupId, Recipient.resolved(recipientId))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the member label for a specific recipient in the group (blocking version for Java compatibility).
|
||||||
|
*/
|
||||||
|
@WorkerThread
|
||||||
|
fun getLabelJava(groupId: GroupId.V2, recipient: Recipient): MemberLabel? = runBlocking { getLabel(groupId, recipient) }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the member label for a specific recipient in the group.
|
||||||
|
*/
|
||||||
|
suspend fun getLabel(groupId: GroupId.V2, recipient: Recipient): MemberLabel? = withContext(Dispatchers.IO) {
|
||||||
|
if (!RemoteConfig.receiveMemberLabels) {
|
||||||
|
return@withContext null
|
||||||
|
}
|
||||||
|
|
||||||
val aci = recipient.serviceId.orNull() as? ServiceId.ACI ?: return@withContext null
|
val aci = recipient.serviceId.orNull() as? ServiceId.ACI ?: return@withContext null
|
||||||
val groupRecord = groupsTable.getGroup(groupId).orNull() ?: return@withContext null
|
val groupRecord = groupsTable.getGroup(groupId).orNull() ?: return@withContext null
|
||||||
|
|
||||||
return@withContext groupRecord.requireV2GroupProperties().memberLabel(aci)
|
return@withContext groupRecord.requireV2GroupProperties().memberLabel(aci)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the group member label for the current user.
|
* Sets the group member label for the current user.
|
||||||
*/
|
*/
|
||||||
suspend fun setLabel(label: MemberLabel): Unit = withContext(Dispatchers.IO) {
|
suspend fun setLabel(groupId: GroupId.V2, label: MemberLabel): Unit = withContext(Dispatchers.IO) {
|
||||||
if (!RemoteConfig.sendMemberLabels) {
|
if (!RemoteConfig.sendMemberLabels) {
|
||||||
throw IllegalStateException("Set member label not allowed due to remote config.")
|
throw IllegalStateException("Set member label not allowed due to remote config.")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,18 +21,11 @@ private const val MIN_LABEL_TEXT_LENGTH = 1
|
|||||||
private const val MAX_LABEL_TEXT_LENGTH = 24
|
private const val MAX_LABEL_TEXT_LENGTH = 24
|
||||||
|
|
||||||
class MemberLabelViewModel(
|
class MemberLabelViewModel(
|
||||||
private val memberLabelRepo: MemberLabelRepository,
|
private val memberLabelRepo: MemberLabelRepository = MemberLabelRepository.instance,
|
||||||
|
private val groupId: GroupId.V2,
|
||||||
private val recipientId: RecipientId
|
private val recipientId: RecipientId
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
constructor(
|
|
||||||
groupId: GroupId.V2,
|
|
||||||
recipientId: RecipientId
|
|
||||||
) : this(
|
|
||||||
memberLabelRepo = MemberLabelRepository(groupId = groupId),
|
|
||||||
recipientId = recipientId
|
|
||||||
)
|
|
||||||
|
|
||||||
private var originalLabelEmoji: String = ""
|
private var originalLabelEmoji: String = ""
|
||||||
private var originalLabelText: String = ""
|
private var originalLabelText: String = ""
|
||||||
|
|
||||||
@@ -45,7 +38,7 @@ class MemberLabelViewModel(
|
|||||||
|
|
||||||
private fun loadExistingLabel() {
|
private fun loadExistingLabel() {
|
||||||
viewModelScope.launch(SignalDispatchers.IO) {
|
viewModelScope.launch(SignalDispatchers.IO) {
|
||||||
val memberLabel = memberLabelRepo.getLabel(recipientId)
|
val memberLabel = memberLabelRepo.getLabel(groupId, recipientId)
|
||||||
originalLabelEmoji = memberLabel?.emoji.orEmpty()
|
originalLabelEmoji = memberLabel?.emoji.orEmpty()
|
||||||
originalLabelText = memberLabel?.text.orEmpty()
|
originalLabelText = memberLabel?.text.orEmpty()
|
||||||
|
|
||||||
@@ -103,6 +96,7 @@ class MemberLabelViewModel(
|
|||||||
|
|
||||||
val currentState = internalUiState.value
|
val currentState = internalUiState.value
|
||||||
memberLabelRepo.setLabel(
|
memberLabelRepo.setLabel(
|
||||||
|
groupId = groupId,
|
||||||
label = MemberLabel(
|
label = MemberLabel(
|
||||||
emoji = currentState.labelEmoji.ifEmpty { null },
|
emoji = currentState.labelEmoji.ifEmpty { null },
|
||||||
text = currentState.labelText
|
text = currentState.labelText
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import org.thoughtcrime.securesms.components.settings.conversation.preferences.B
|
|||||||
import org.thoughtcrime.securesms.conversation.v2.data.AvatarDownloadStateCache
|
import org.thoughtcrime.securesms.conversation.v2.data.AvatarDownloadStateCache
|
||||||
import org.thoughtcrime.securesms.fonts.SignalSymbols
|
import org.thoughtcrime.securesms.fonts.SignalSymbols
|
||||||
import org.thoughtcrime.securesms.groups.GroupId
|
import org.thoughtcrime.securesms.groups.GroupId
|
||||||
|
import org.thoughtcrime.securesms.groups.memberlabel.MemberLabelPillView
|
||||||
import org.thoughtcrime.securesms.nicknames.NicknameActivity
|
import org.thoughtcrime.securesms.nicknames.NicknameActivity
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientExporter
|
import org.thoughtcrime.securesms.recipients.RecipientExporter
|
||||||
@@ -108,7 +109,8 @@ class RecipientBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFr
|
|||||||
|
|
||||||
val avatar: AvatarView = view.findViewById(R.id.rbs_recipient_avatar)
|
val avatar: AvatarView = view.findViewById(R.id.rbs_recipient_avatar)
|
||||||
val fullName: TextView = view.findViewById(R.id.rbs_full_name)
|
val fullName: TextView = view.findViewById(R.id.rbs_full_name)
|
||||||
val about: TextView = view.findViewById(R.id.rbs_about)
|
val memberLabelView: MemberLabelPillView = view.findViewById(R.id.rbs_member_label)
|
||||||
|
val aboutView: TextView = view.findViewById(R.id.rbs_about)
|
||||||
val nickname: TextView = view.findViewById(R.id.rbs_nickname_button)
|
val nickname: TextView = view.findViewById(R.id.rbs_nickname_button)
|
||||||
val blockButton: TextView = view.findViewById(R.id.rbs_block_button)
|
val blockButton: TextView = view.findViewById(R.id.rbs_block_button)
|
||||||
val unblockButton: TextView = view.findViewById(R.id.rbs_unblock_button)
|
val unblockButton: TextView = view.findViewById(R.id.rbs_unblock_button)
|
||||||
@@ -148,6 +150,7 @@ class RecipientBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFr
|
|||||||
AvatarDownloadStateCache.forRecipient(recipient.id).collect {
|
AvatarDownloadStateCache.forRecipient(recipient.id).collect {
|
||||||
when (it) {
|
when (it) {
|
||||||
AvatarDownloadStateCache.DownloadState.NONE -> {}
|
AvatarDownloadStateCache.DownloadState.NONE -> {}
|
||||||
|
|
||||||
AvatarDownloadStateCache.DownloadState.IN_PROGRESS -> {
|
AvatarDownloadStateCache.DownloadState.IN_PROGRESS -> {
|
||||||
if (inProgress) {
|
if (inProgress) {
|
||||||
return@collect
|
return@collect
|
||||||
@@ -159,12 +162,14 @@ class RecipientBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFr
|
|||||||
delay(LOADING_DELAY)
|
delay(LOADING_DELAY)
|
||||||
progressBar.visible = AvatarDownloadStateCache.getDownloadState(recipient) == AvatarDownloadStateCache.DownloadState.IN_PROGRESS
|
progressBar.visible = AvatarDownloadStateCache.getDownloadState(recipient) == AvatarDownloadStateCache.DownloadState.IN_PROGRESS
|
||||||
}
|
}
|
||||||
|
|
||||||
AvatarDownloadStateCache.DownloadState.FINISHED -> {
|
AvatarDownloadStateCache.DownloadState.FINISHED -> {
|
||||||
AvatarDownloadStateCache.set(recipient, AvatarDownloadStateCache.DownloadState.NONE)
|
AvatarDownloadStateCache.set(recipient, AvatarDownloadStateCache.DownloadState.NONE)
|
||||||
viewModel.refreshGroupId(groupId)
|
viewModel.refreshGroupId(groupId)
|
||||||
inProgress = false
|
inProgress = false
|
||||||
progressBar.visible = false
|
progressBar.visible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
AvatarDownloadStateCache.DownloadState.FAILED -> {
|
AvatarDownloadStateCache.DownloadState.FAILED -> {
|
||||||
AvatarDownloadStateCache.set(recipient, AvatarDownloadStateCache.DownloadState.NONE)
|
AvatarDownloadStateCache.set(recipient, AvatarDownloadStateCache.DownloadState.NONE)
|
||||||
avatar.displayGradientBlur(recipient)
|
avatar.displayGradientBlur(recipient)
|
||||||
@@ -256,18 +261,6 @@ class RecipientBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFr
|
|||||||
fullName.text = name
|
fullName.text = name
|
||||||
}
|
}
|
||||||
|
|
||||||
var aboutText = recipient.combinedAboutAndEmoji
|
|
||||||
if (recipient.isReleaseNotes) {
|
|
||||||
aboutText = getString(R.string.ReleaseNotes__signal_release_notes_and_news)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!aboutText.isNullOrEmpty()) {
|
|
||||||
about.text = aboutText
|
|
||||||
about.visible = true
|
|
||||||
} else {
|
|
||||||
about.visible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
noteToSelfDescription.visible = recipient.isSelf
|
noteToSelfDescription.visible = recipient.isSelf
|
||||||
|
|
||||||
if (RecipientUtil.isBlockable(recipient)) {
|
if (RecipientUtil.isBlockable(recipient)) {
|
||||||
@@ -339,6 +332,10 @@ class RecipientBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viewModel.recipientDetails.observe(viewLifecycleOwner) { state ->
|
||||||
|
updateRecipientDetails(state, memberLabelView, aboutView)
|
||||||
|
}
|
||||||
|
|
||||||
viewModel.canAddToAGroup.observe(getViewLifecycleOwner()) { canAdd: Boolean ->
|
viewModel.canAddToAGroup.observe(getViewLifecycleOwner()) { canAdd: Boolean ->
|
||||||
addToGroupButton.setText(if (groupId == null) R.string.RecipientBottomSheet_add_to_a_group else R.string.RecipientBottomSheet_add_to_another_group)
|
addToGroupButton.setText(if (groupId == null) R.string.RecipientBottomSheet_add_to_a_group else R.string.RecipientBottomSheet_add_to_another_group)
|
||||||
addToGroupButton.visible = canAdd
|
addToGroupButton.visible = canAdd
|
||||||
@@ -449,6 +446,31 @@ class RecipientBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFr
|
|||||||
animator.start()
|
animator.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateRecipientDetails(
|
||||||
|
state: RecipientDetailsState,
|
||||||
|
memberLabelView: MemberLabelPillView,
|
||||||
|
aboutView: TextView
|
||||||
|
) {
|
||||||
|
when {
|
||||||
|
state.memberLabel != null -> {
|
||||||
|
memberLabelView.setLabel(state.memberLabel.label, state.memberLabel.tintColor)
|
||||||
|
memberLabelView.visible = true
|
||||||
|
aboutView.visible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
!state.aboutText.isNullOrBlank() -> {
|
||||||
|
aboutView.text = state.aboutText
|
||||||
|
aboutView.visible = true
|
||||||
|
memberLabelView.visible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
memberLabelView.visible = false
|
||||||
|
aboutView.visible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface Callback {
|
interface Callback {
|
||||||
fun onRecipientBottomSheetDismissed()
|
fun onRecipientBottomSheetDismissed()
|
||||||
fun onMessageClicked()
|
fun onMessageClicked()
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package org.thoughtcrime.securesms.recipients.ui.bottomsheet
|
||||||
|
|
||||||
|
import androidx.annotation.ColorInt
|
||||||
|
import org.thoughtcrime.securesms.groups.memberlabel.MemberLabel
|
||||||
|
|
||||||
|
data class RecipientDetailsState(
|
||||||
|
val memberLabel: StyledMemberLabel?,
|
||||||
|
val aboutText: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class StyledMemberLabel(
|
||||||
|
val label: MemberLabel,
|
||||||
|
@param:ColorInt val tintColor: Int
|
||||||
|
)
|
||||||
@@ -21,18 +21,23 @@ import org.signal.core.util.concurrent.SignalExecutors;
|
|||||||
import org.thoughtcrime.securesms.BlockUnblockDialog;
|
import org.thoughtcrime.securesms.BlockUnblockDialog;
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsActivity;
|
import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsActivity;
|
||||||
|
import org.thoughtcrime.securesms.conversation.colors.Colorizer;
|
||||||
import org.thoughtcrime.securesms.database.GroupTable;
|
import org.thoughtcrime.securesms.database.GroupTable;
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||||
|
import org.thoughtcrime.securesms.database.model.GroupRecord;
|
||||||
import org.thoughtcrime.securesms.database.model.IdentityRecord;
|
import org.thoughtcrime.securesms.database.model.IdentityRecord;
|
||||||
import org.thoughtcrime.securesms.database.model.StoryViewState;
|
import org.thoughtcrime.securesms.database.model.StoryViewState;
|
||||||
import org.thoughtcrime.securesms.groups.GroupId;
|
import org.thoughtcrime.securesms.groups.GroupId;
|
||||||
import org.thoughtcrime.securesms.groups.LiveGroup;
|
import org.thoughtcrime.securesms.groups.LiveGroup;
|
||||||
|
import org.thoughtcrime.securesms.groups.memberlabel.MemberLabel;
|
||||||
|
import org.thoughtcrime.securesms.groups.memberlabel.MemberLabelRepository;
|
||||||
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
|
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
|
||||||
import org.thoughtcrime.securesms.groups.ui.GroupErrors;
|
import org.thoughtcrime.securesms.groups.ui.GroupErrors;
|
||||||
import org.thoughtcrime.securesms.groups.ui.addtogroup.AddToGroupsActivity;
|
import org.thoughtcrime.securesms.groups.ui.addtogroup.AddToGroupsActivity;
|
||||||
import org.thoughtcrime.securesms.jobs.AvatarGroupsV2DownloadJob;
|
import org.thoughtcrime.securesms.jobs.AvatarGroupsV2DownloadJob;
|
||||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob;
|
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob;
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
|
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||||
@@ -44,6 +49,7 @@ import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
|||||||
import org.thoughtcrime.securesms.verify.VerifyIdentityActivity;
|
import org.thoughtcrime.securesms.verify.VerifyIdentityActivity;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
import kotlin.Pair;
|
import kotlin.Pair;
|
||||||
|
|
||||||
@@ -52,16 +58,17 @@ import io.reactivex.rxjava3.disposables.Disposable;
|
|||||||
|
|
||||||
final class RecipientDialogViewModel extends ViewModel {
|
final class RecipientDialogViewModel extends ViewModel {
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final RecipientDialogRepository recipientDialogRepository;
|
private final RecipientDialogRepository recipientDialogRepository;
|
||||||
private final LiveData<Recipient> recipient;
|
private final LiveData<Recipient> recipient;
|
||||||
private final MutableLiveData<IdentityRecord> identity;
|
private final MutableLiveData<IdentityRecord> identity;
|
||||||
private final LiveData<AdminActionStatus> adminActionStatus;
|
private final LiveData<AdminActionStatus> adminActionStatus;
|
||||||
private final LiveData<Boolean> canAddToAGroup;
|
private final LiveData<Boolean> canAddToAGroup;
|
||||||
private final MutableLiveData<Boolean> adminActionBusy;
|
private final MutableLiveData<Boolean> adminActionBusy;
|
||||||
private final MutableLiveData<StoryViewState> storyViewState;
|
private final MutableLiveData<StoryViewState> storyViewState;
|
||||||
private final CompositeDisposable disposables;
|
private final MutableLiveData<RecipientDetailsState> recipientDetailsState;
|
||||||
private final boolean isDeprecatedOrUnregistered;
|
private final CompositeDisposable disposables;
|
||||||
|
private final boolean isDeprecatedOrUnregistered;
|
||||||
private RecipientDialogViewModel(@NonNull Context context,
|
private RecipientDialogViewModel(@NonNull Context context,
|
||||||
@NonNull RecipientDialogRepository recipientDialogRepository)
|
@NonNull RecipientDialogRepository recipientDialogRepository)
|
||||||
{
|
{
|
||||||
@@ -70,12 +77,14 @@ final class RecipientDialogViewModel extends ViewModel {
|
|||||||
this.identity = new MutableLiveData<>();
|
this.identity = new MutableLiveData<>();
|
||||||
this.adminActionBusy = new MutableLiveData<>(false);
|
this.adminActionBusy = new MutableLiveData<>(false);
|
||||||
this.storyViewState = new MutableLiveData<>();
|
this.storyViewState = new MutableLiveData<>();
|
||||||
|
this.recipientDetailsState = new MutableLiveData<>();
|
||||||
this.disposables = new CompositeDisposable();
|
this.disposables = new CompositeDisposable();
|
||||||
this.isDeprecatedOrUnregistered = SignalStore.misc().isClientDeprecated() || TextSecurePreferences.isUnauthorizedReceived(context);
|
this.isDeprecatedOrUnregistered = SignalStore.misc().isClientDeprecated() || TextSecurePreferences.isUnauthorizedReceived(context);
|
||||||
|
|
||||||
boolean recipientIsSelf = recipientDialogRepository.getRecipientId().equals(Recipient.self().getId());
|
boolean recipientIsSelf = recipientDialogRepository.getRecipientId().equals(Recipient.self().getId());
|
||||||
|
|
||||||
recipient = Recipient.live(recipientDialogRepository.getRecipientId()).getLiveData();
|
final LiveRecipient liveRecipient = Recipient.live(recipientDialogRepository.getRecipientId());
|
||||||
|
recipient = liveRecipient.getLiveData();
|
||||||
|
|
||||||
if (recipientDialogRepository.getGroupId() != null && recipientDialogRepository.getGroupId().isV2() && !recipientIsSelf) {
|
if (recipientDialogRepository.getGroupId() != null && recipientDialogRepository.getGroupId().isV2() && !recipientIsSelf) {
|
||||||
LiveGroup source = new LiveGroup(recipientDialogRepository.getGroupId());
|
LiveGroup source = new LiveGroup(recipientDialogRepository.getGroupId());
|
||||||
@@ -114,6 +123,35 @@ final class RecipientDialogViewModel extends ViewModel {
|
|||||||
.subscribe(storyViewState::postValue);
|
.subscribe(storyViewState::postValue);
|
||||||
|
|
||||||
disposables.add(storyViewStateDisposable);
|
disposables.add(storyViewStateDisposable);
|
||||||
|
|
||||||
|
Disposable recipientDisposable = liveRecipient.observable().subscribe(this::updateRecipientDetailsState);
|
||||||
|
disposables.add(recipientDisposable);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateRecipientDetailsState(@NonNull Recipient recipient) {
|
||||||
|
GroupId groupId = recipientDialogRepository.getGroupId();
|
||||||
|
String aboutText = recipient.isReleaseNotes() ? context.getString(R.string.ReleaseNotes__signal_release_notes_and_news) : recipient.getCombinedAboutAndEmoji();
|
||||||
|
|
||||||
|
if (groupId != null && groupId.isV2() && recipient.isIndividual() && !recipient.isSelf()) {
|
||||||
|
SignalExecutors.BOUNDED.execute(() -> {
|
||||||
|
GroupId.V2 v2GroupId = (GroupId.V2) groupId;
|
||||||
|
MemberLabel label = MemberLabelRepository.getInstance().getLabelJava(v2GroupId, recipient);
|
||||||
|
StyledMemberLabel styledLabel = null;
|
||||||
|
|
||||||
|
if (label != null) {
|
||||||
|
Colorizer colorizer = new Colorizer();
|
||||||
|
Optional<GroupRecord> groupRecord = SignalDatabase.groups().getGroup(v2GroupId);
|
||||||
|
if (groupRecord.isPresent()) {
|
||||||
|
colorizer.onGroupMembershipChanged(groupRecord.get().requireV2GroupProperties().getMemberServiceIds());
|
||||||
|
}
|
||||||
|
styledLabel = new StyledMemberLabel(label, colorizer.getIncomingGroupSenderColor(context, recipient));
|
||||||
|
}
|
||||||
|
|
||||||
|
recipientDetailsState.postValue(new RecipientDetailsState(styledLabel, aboutText));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
recipientDetailsState.setValue(new RecipientDetailsState(null, aboutText));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override protected void onCleared() {
|
@Override protected void onCleared() {
|
||||||
@@ -149,6 +187,10 @@ final class RecipientDialogViewModel extends ViewModel {
|
|||||||
return adminActionBusy;
|
return adminActionBusy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LiveData<RecipientDetailsState> getRecipientDetails() {
|
||||||
|
return recipientDetailsState;
|
||||||
|
}
|
||||||
|
|
||||||
void onNoteToSelfClicked(@NonNull Activity activity) {
|
void onNoteToSelfClicked(@NonNull Activity activity) {
|
||||||
if (storyViewState.getValue() == null || storyViewState.getValue() == StoryViewState.NONE) {
|
if (storyViewState.getValue() == null || storyViewState.getValue() == StoryViewState.NONE) {
|
||||||
onMessageClicked(activity);
|
onMessageClicked(activity);
|
||||||
|
|||||||
@@ -305,11 +305,13 @@ object RemoteConfig {
|
|||||||
val newKey = key.removePrefix("android.libsignal.")
|
val newKey = key.removePrefix("android.libsignal.")
|
||||||
when (value) {
|
when (value) {
|
||||||
is String -> newKey to value
|
is String -> newKey to value
|
||||||
|
|
||||||
// The server is currently synthesizing "true" / "false" values
|
// The server is currently synthesizing "true" / "false" values
|
||||||
// for RemoteConfigs that are otherwise empty string values.
|
// for RemoteConfigs that are otherwise empty string values.
|
||||||
// Libsignal expects that disabled values are simply absent from the
|
// Libsignal expects that disabled values are simply absent from the
|
||||||
// map, so we map true to "true" and otherwise omit disabled values.
|
// map, so we map true to "true" and otherwise omit disabled values.
|
||||||
is Boolean -> if (value) newKey to "true" else null
|
is Boolean -> if (value) newKey to "true" else null
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
val type = value?.let { value::class.simpleName }
|
val type = value?.let { value::class.simpleName }
|
||||||
Log.w(TAG, "[libsignal] Unexpected type for $newKey! Was a $type")
|
Log.w(TAG, "[libsignal] Unexpected type for $newKey! Was a $type")
|
||||||
@@ -1237,6 +1239,15 @@ object RemoteConfig {
|
|||||||
hotSwappable = true
|
hotSwappable = true
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to receive and display group member labels.
|
||||||
|
*/
|
||||||
|
val receiveMemberLabels: Boolean by remoteBoolean(
|
||||||
|
key = "android.receiveMemberLabels",
|
||||||
|
defaultValue = false,
|
||||||
|
hotSwappable = true
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether to enable modifying group member labels.
|
* Whether to enable modifying group member labels.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -18,10 +18,10 @@
|
|||||||
android:layout_gravity="center_horizontal"
|
android:layout_gravity="center_horizontal"
|
||||||
android:layout_marginTop="10dp"
|
android:layout_marginTop="10dp"
|
||||||
android:importantForAccessibility="no"
|
android:importantForAccessibility="no"
|
||||||
app:srcCompat="@drawable/bottom_sheet_handle"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"/>
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:srcCompat="@drawable/bottom_sheet_handle" />
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.avatar.view.AvatarView
|
<org.thoughtcrime.securesms.avatar.view.AvatarView
|
||||||
android:id="@+id/rbs_recipient_avatar"
|
android:id="@+id/rbs_recipient_avatar"
|
||||||
@@ -58,17 +58,17 @@
|
|||||||
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
android:id="@+id/rbs_progress_bar"
|
android:id="@+id/rbs_progress_bar"
|
||||||
|
style="?android:attr/progressBarStyleLarge"
|
||||||
android:layout_width="48dp"
|
android:layout_width="48dp"
|
||||||
android:layout_height="48dp"
|
android:layout_height="48dp"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:visibility="gone"
|
|
||||||
android:indeterminate="true"
|
android:indeterminate="true"
|
||||||
style="?android:attr/progressBarStyleLarge"
|
|
||||||
android:indeterminateTint="@color/signal_colorOnSurfaceVariant"
|
android:indeterminateTint="@color/signal_colorOnSurfaceVariant"
|
||||||
app:layout_constraintTop_toTopOf="@id/rbs_recipient_avatar"
|
android:visibility="gone"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/rbs_recipient_avatar"
|
app:layout_constraintBottom_toBottomOf="@id/rbs_recipient_avatar"
|
||||||
app:layout_constraintStart_toStartOf="@id/rbs_recipient_avatar"
|
|
||||||
app:layout_constraintEnd_toEndOf="@id/rbs_recipient_avatar"
|
app:layout_constraintEnd_toEndOf="@id/rbs_recipient_avatar"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/rbs_recipient_avatar"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/rbs_recipient_avatar"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
@@ -115,6 +115,17 @@
|
|||||||
app:layout_constraintTop_toBottomOf="@id/rbs_recipient_avatar"
|
app:layout_constraintTop_toBottomOf="@id/rbs_recipient_avatar"
|
||||||
tools:text="Gwen Stacy" />
|
tools:text="Gwen Stacy" />
|
||||||
|
|
||||||
|
<org.thoughtcrime.securesms.groups.memberlabel.MemberLabelPillView
|
||||||
|
android:id="@+id/rbs_member_label"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/rbs_full_name"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||||
android:id="@+id/rbs_about"
|
android:id="@+id/rbs_about"
|
||||||
style="@style/Signal.Text.BodyLarge"
|
style="@style/Signal.Text.BodyLarge"
|
||||||
@@ -125,7 +136,7 @@
|
|||||||
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:textColor="@color/signal_text_secondary"
|
android:textColor="@color/signal_text_secondary"
|
||||||
app:layout_constraintTop_toBottomOf="@id/rbs_full_name"
|
app:layout_constraintTop_toBottomOf="@id/rbs_member_label"
|
||||||
tools:text="🕷🕷🕷 Hangin' on the web 🕷🕷🕷" />
|
tools:text="🕷🕷🕷 Hangin' on the web 🕷🕷🕷" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import org.junit.Assert.assertFalse
|
|||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupId
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import org.thoughtcrime.securesms.testing.CoroutineDispatcherRule
|
import org.thoughtcrime.securesms.testing.CoroutineDispatcherRule
|
||||||
|
|
||||||
@@ -26,13 +27,14 @@ class MemberLabelViewModelTest {
|
|||||||
val dispatcherRule = CoroutineDispatcherRule(testDispatcher)
|
val dispatcherRule = CoroutineDispatcherRule(testDispatcher)
|
||||||
|
|
||||||
private val memberLabelRepo = mockk<MemberLabelRepository>(relaxUnitFun = true)
|
private val memberLabelRepo = mockk<MemberLabelRepository>(relaxUnitFun = true)
|
||||||
|
private val groupId = mockk<GroupId.V2>()
|
||||||
private val recipientId = RecipientId.from(1L)
|
private val recipientId = RecipientId.from(1L)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `isSaveEnabled returns true when label text is different from the original value`() {
|
fun `isSaveEnabled returns true when label text is different from the original value`() {
|
||||||
coEvery { memberLabelRepo.getLabel(any<RecipientId>()) } returns MemberLabel(emoji = null, text = "Original")
|
coEvery { memberLabelRepo.getLabel(groupId, any<RecipientId>()) } returns MemberLabel(emoji = null, text = "Original")
|
||||||
|
|
||||||
val viewModel = MemberLabelViewModel(memberLabelRepo, recipientId)
|
val viewModel = MemberLabelViewModel(memberLabelRepo, groupId, recipientId)
|
||||||
viewModel.onLabelTextChanged("Modified")
|
viewModel.onLabelTextChanged("Modified")
|
||||||
|
|
||||||
assertTrue(viewModel.uiState.value.isSaveEnabled)
|
assertTrue(viewModel.uiState.value.isSaveEnabled)
|
||||||
@@ -40,9 +42,9 @@ class MemberLabelViewModelTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `isSaveEnabled returns false when label text is the same as the original value`() {
|
fun `isSaveEnabled returns false when label text is the same as the original value`() {
|
||||||
coEvery { memberLabelRepo.getLabel(any<RecipientId>()) } returns MemberLabel(emoji = null, text = "Original")
|
coEvery { memberLabelRepo.getLabel(groupId, any<RecipientId>()) } returns MemberLabel(emoji = null, text = "Original")
|
||||||
|
|
||||||
val viewModel = MemberLabelViewModel(memberLabelRepo, recipientId)
|
val viewModel = MemberLabelViewModel(memberLabelRepo, groupId, recipientId)
|
||||||
viewModel.onLabelTextChanged("Original")
|
viewModel.onLabelTextChanged("Original")
|
||||||
|
|
||||||
assertFalse(viewModel.uiState.value.isSaveEnabled)
|
assertFalse(viewModel.uiState.value.isSaveEnabled)
|
||||||
@@ -50,9 +52,9 @@ class MemberLabelViewModelTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `isSaveEnabled returns true when label text is valid and the emoji is different from the original value`() = runTest(testDispatcher) {
|
fun `isSaveEnabled returns true when label text is valid and the emoji is different from the original value`() = runTest(testDispatcher) {
|
||||||
coEvery { memberLabelRepo.getLabel(any<RecipientId>()) } returns MemberLabel(emoji = null, text = "Label")
|
coEvery { memberLabelRepo.getLabel(groupId, any<RecipientId>()) } returns MemberLabel(emoji = null, text = "Label")
|
||||||
|
|
||||||
val viewModel = MemberLabelViewModel(memberLabelRepo, recipientId)
|
val viewModel = MemberLabelViewModel(memberLabelRepo, groupId, recipientId)
|
||||||
viewModel.onLabelEmojiChanged("🎉")
|
viewModel.onLabelEmojiChanged("🎉")
|
||||||
|
|
||||||
assertTrue(viewModel.uiState.value.isSaveEnabled)
|
assertTrue(viewModel.uiState.value.isSaveEnabled)
|
||||||
@@ -60,18 +62,18 @@ class MemberLabelViewModelTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `isSaveEnabled returns false when the label and emoji are not changed`() = runTest(testDispatcher) {
|
fun `isSaveEnabled returns false when the label and emoji are not changed`() = runTest(testDispatcher) {
|
||||||
coEvery { memberLabelRepo.getLabel(any<RecipientId>()) } returns MemberLabel(emoji = "🎉", text = "Label")
|
coEvery { memberLabelRepo.getLabel(groupId, any<RecipientId>()) } returns MemberLabel(emoji = "🎉", text = "Label")
|
||||||
|
|
||||||
val viewModel = MemberLabelViewModel(memberLabelRepo, recipientId)
|
val viewModel = MemberLabelViewModel(memberLabelRepo, groupId, recipientId)
|
||||||
|
|
||||||
assertFalse(viewModel.uiState.value.isSaveEnabled)
|
assertFalse(viewModel.uiState.value.isSaveEnabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `isSaveEnabled returns false when the label and emoji are changed to the original value`() = runTest(testDispatcher) {
|
fun `isSaveEnabled returns false when the label and emoji are changed to the original value`() = runTest(testDispatcher) {
|
||||||
coEvery { memberLabelRepo.getLabel(any<RecipientId>()) } returns MemberLabel(emoji = "🎉", text = "Original")
|
coEvery { memberLabelRepo.getLabel(groupId, any<RecipientId>()) } returns MemberLabel(emoji = "🎉", text = "Original")
|
||||||
|
|
||||||
val viewModel = MemberLabelViewModel(memberLabelRepo, recipientId)
|
val viewModel = MemberLabelViewModel(memberLabelRepo, groupId, recipientId)
|
||||||
|
|
||||||
viewModel.onLabelEmojiChanged("🫢")
|
viewModel.onLabelEmojiChanged("🫢")
|
||||||
viewModel.onLabelTextChanged("Modified")
|
viewModel.onLabelTextChanged("Modified")
|
||||||
@@ -84,9 +86,9 @@ class MemberLabelViewModelTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `isSaveEnabled returns false when label is too short`() = runTest(testDispatcher) {
|
fun `isSaveEnabled returns false when label is too short`() = runTest(testDispatcher) {
|
||||||
coEvery { memberLabelRepo.getLabel(any<RecipientId>()) } returns MemberLabel(emoji = null, text = "Label")
|
coEvery { memberLabelRepo.getLabel(groupId, any<RecipientId>()) } returns MemberLabel(emoji = null, text = "Label")
|
||||||
|
|
||||||
val viewModel = MemberLabelViewModel(memberLabelRepo, recipientId)
|
val viewModel = MemberLabelViewModel(memberLabelRepo, groupId, recipientId)
|
||||||
viewModel.onLabelTextChanged("")
|
viewModel.onLabelTextChanged("")
|
||||||
viewModel.onLabelEmojiChanged("🎉")
|
viewModel.onLabelEmojiChanged("🎉")
|
||||||
|
|
||||||
@@ -95,9 +97,9 @@ class MemberLabelViewModelTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `isSaveEnabled returns true when clearLabel is called with existing label and emoji`() = runTest(testDispatcher) {
|
fun `isSaveEnabled returns true when clearLabel is called with existing label and emoji`() = runTest(testDispatcher) {
|
||||||
coEvery { memberLabelRepo.getLabel(any<RecipientId>()) } returns MemberLabel(emoji = "🎉", text = "Original")
|
coEvery { memberLabelRepo.getLabel(groupId, any<RecipientId>()) } returns MemberLabel(emoji = "🎉", text = "Original")
|
||||||
|
|
||||||
val viewModel = MemberLabelViewModel(memberLabelRepo, recipientId)
|
val viewModel = MemberLabelViewModel(memberLabelRepo, groupId, recipientId)
|
||||||
viewModel.clearLabel()
|
viewModel.clearLabel()
|
||||||
|
|
||||||
assertTrue(viewModel.uiState.value.isSaveEnabled)
|
assertTrue(viewModel.uiState.value.isSaveEnabled)
|
||||||
@@ -105,9 +107,9 @@ class MemberLabelViewModelTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `isSaveEnabled returns true when clearLabel is called with existing label without emoji`() = runTest(testDispatcher) {
|
fun `isSaveEnabled returns true when clearLabel is called with existing label without emoji`() = runTest(testDispatcher) {
|
||||||
coEvery { memberLabelRepo.getLabel(any<RecipientId>()) } returns MemberLabel(emoji = null, text = "Original")
|
coEvery { memberLabelRepo.getLabel(groupId, any<RecipientId>()) } returns MemberLabel(emoji = null, text = "Original")
|
||||||
|
|
||||||
val viewModel = MemberLabelViewModel(memberLabelRepo, recipientId)
|
val viewModel = MemberLabelViewModel(memberLabelRepo, groupId, recipientId)
|
||||||
viewModel.clearLabel()
|
viewModel.clearLabel()
|
||||||
|
|
||||||
assertTrue(viewModel.uiState.value.isSaveEnabled)
|
assertTrue(viewModel.uiState.value.isSaveEnabled)
|
||||||
@@ -115,9 +117,9 @@ class MemberLabelViewModelTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `isSaveEnabled returns false when clearLabel is called with no existing label`() = runTest(testDispatcher) {
|
fun `isSaveEnabled returns false when clearLabel is called with no existing label`() = runTest(testDispatcher) {
|
||||||
coEvery { memberLabelRepo.getLabel(any<RecipientId>()) } returns null
|
coEvery { memberLabelRepo.getLabel(groupId, any<RecipientId>()) } returns null
|
||||||
|
|
||||||
val viewModel = MemberLabelViewModel(memberLabelRepo, recipientId)
|
val viewModel = MemberLabelViewModel(memberLabelRepo, groupId, recipientId)
|
||||||
viewModel.clearLabel()
|
viewModel.clearLabel()
|
||||||
|
|
||||||
assertFalse(viewModel.uiState.value.isSaveEnabled)
|
assertFalse(viewModel.uiState.value.isSaveEnabled)
|
||||||
@@ -125,9 +127,9 @@ class MemberLabelViewModelTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `isSaveEnabled returns true when both emoji and label are modified`() = runTest(testDispatcher) {
|
fun `isSaveEnabled returns true when both emoji and label are modified`() = runTest(testDispatcher) {
|
||||||
coEvery { memberLabelRepo.getLabel(any<RecipientId>()) } returns MemberLabel(emoji = "🎉", text = "Original")
|
coEvery { memberLabelRepo.getLabel(groupId, any<RecipientId>()) } returns MemberLabel(emoji = "🎉", text = "Original")
|
||||||
|
|
||||||
val viewModel = MemberLabelViewModel(memberLabelRepo, recipientId)
|
val viewModel = MemberLabelViewModel(memberLabelRepo, groupId, recipientId)
|
||||||
viewModel.onLabelTextChanged("New Label")
|
viewModel.onLabelTextChanged("New Label")
|
||||||
viewModel.onLabelEmojiChanged("🚀")
|
viewModel.onLabelEmojiChanged("🚀")
|
||||||
|
|
||||||
@@ -136,9 +138,9 @@ class MemberLabelViewModelTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `isSaveEnabled returns false when only emoji is changed without an existing label`() = runTest(testDispatcher) {
|
fun `isSaveEnabled returns false when only emoji is changed without an existing label`() = runTest(testDispatcher) {
|
||||||
coEvery { memberLabelRepo.getLabel(any<RecipientId>()) } returns null
|
coEvery { memberLabelRepo.getLabel(groupId, any<RecipientId>()) } returns null
|
||||||
|
|
||||||
val viewModel = MemberLabelViewModel(memberLabelRepo, recipientId)
|
val viewModel = MemberLabelViewModel(memberLabelRepo, groupId, recipientId)
|
||||||
viewModel.onLabelEmojiChanged("🎉")
|
viewModel.onLabelEmojiChanged("🎉")
|
||||||
|
|
||||||
assertFalse(viewModel.uiState.value.isSaveEnabled)
|
assertFalse(viewModel.uiState.value.isSaveEnabled)
|
||||||
@@ -146,76 +148,77 @@ class MemberLabelViewModelTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `save does not call setLabel when isSaveEnabled is false`() = runTest(testDispatcher) {
|
fun `save does not call setLabel when isSaveEnabled is false`() = runTest(testDispatcher) {
|
||||||
coEvery { memberLabelRepo.getLabel(any<RecipientId>()) } returns MemberLabel(emoji = null, text = "Label")
|
coEvery { memberLabelRepo.getLabel(groupId, any<RecipientId>()) } returns MemberLabel(emoji = null, text = "Label")
|
||||||
|
|
||||||
val viewModel = MemberLabelViewModel(memberLabelRepo, recipientId)
|
val viewModel = MemberLabelViewModel(memberLabelRepo, groupId, recipientId)
|
||||||
viewModel.save()
|
viewModel.save()
|
||||||
|
|
||||||
coVerify(exactly = 0) { memberLabelRepo.setLabel(any()) }
|
coVerify(exactly = 0) { memberLabelRepo.setLabel(groupId, any()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `save does not call setLabel when label is less than 1 character`() = runTest(testDispatcher) {
|
fun `save does not call setLabel when label is less than 1 character`() = runTest(testDispatcher) {
|
||||||
coEvery { memberLabelRepo.getLabel(any<RecipientId>()) } returns MemberLabel(emoji = null, text = "Label")
|
coEvery { memberLabelRepo.getLabel(groupId, any<RecipientId>()) } returns MemberLabel(emoji = null, text = "Label")
|
||||||
|
|
||||||
val viewModel = MemberLabelViewModel(memberLabelRepo, recipientId)
|
val viewModel = MemberLabelViewModel(memberLabelRepo, groupId, recipientId)
|
||||||
viewModel.onLabelTextChanged("")
|
viewModel.onLabelTextChanged("")
|
||||||
viewModel.onLabelEmojiChanged("🎉")
|
viewModel.onLabelEmojiChanged("🎉")
|
||||||
viewModel.save()
|
viewModel.save()
|
||||||
|
|
||||||
coVerify(exactly = 0) { memberLabelRepo.setLabel(any()) }
|
coVerify(exactly = 0) { memberLabelRepo.setLabel(groupId, any()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `save calls setLabel with truncated label when label exceeds max length`() = runTest(testDispatcher) {
|
fun `save calls setLabel with truncated label when label exceeds max length`() = runTest(testDispatcher) {
|
||||||
coEvery { memberLabelRepo.getLabel(any<RecipientId>()) } returns null
|
coEvery { memberLabelRepo.getLabel(groupId, any<RecipientId>()) } returns null
|
||||||
|
|
||||||
val viewModel = MemberLabelViewModel(memberLabelRepo, recipientId)
|
val viewModel = MemberLabelViewModel(memberLabelRepo, groupId, recipientId)
|
||||||
viewModel.onLabelTextChanged("A".repeat(30))
|
viewModel.onLabelTextChanged("A".repeat(30))
|
||||||
viewModel.save()
|
viewModel.save()
|
||||||
|
|
||||||
coVerify(exactly = 1) {
|
coVerify(exactly = 1) {
|
||||||
memberLabelRepo.setLabel(
|
memberLabelRepo.setLabel(
|
||||||
match { it.text.length == 24 }
|
groupId = groupId,
|
||||||
|
label = match { it.text.length == 24 }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `save does not call setLabel when emoji is set with no label`() = runTest(testDispatcher) {
|
fun `save does not call setLabel when emoji is set with no label`() = runTest(testDispatcher) {
|
||||||
coEvery { memberLabelRepo.getLabel(any<RecipientId>()) } returns null
|
coEvery { memberLabelRepo.getLabel(groupId, any<RecipientId>()) } returns null
|
||||||
|
|
||||||
val viewModel = MemberLabelViewModel(memberLabelRepo, recipientId)
|
val viewModel = MemberLabelViewModel(memberLabelRepo, groupId, recipientId)
|
||||||
viewModel.onLabelEmojiChanged("🎉")
|
viewModel.onLabelEmojiChanged("🎉")
|
||||||
viewModel.save()
|
viewModel.save()
|
||||||
|
|
||||||
coVerify(exactly = 0) { memberLabelRepo.setLabel(any()) }
|
coVerify(exactly = 0) { memberLabelRepo.setLabel(groupId, any()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `save calls setLabel when label change is valid`() = runTest(testDispatcher) {
|
fun `save calls setLabel when label change is valid`() = runTest(testDispatcher) {
|
||||||
coEvery { memberLabelRepo.getLabel(any<RecipientId>()) } returns MemberLabel(emoji = null, text = "Original")
|
coEvery { memberLabelRepo.getLabel(groupId, any<RecipientId>()) } returns MemberLabel(emoji = null, text = "Original")
|
||||||
|
|
||||||
val viewModel = MemberLabelViewModel(memberLabelRepo, recipientId)
|
val viewModel = MemberLabelViewModel(memberLabelRepo, groupId, recipientId)
|
||||||
viewModel.onLabelTextChanged("New Label")
|
viewModel.onLabelTextChanged("New Label")
|
||||||
viewModel.onLabelEmojiChanged("🎉")
|
viewModel.onLabelEmojiChanged("🎉")
|
||||||
viewModel.save()
|
viewModel.save()
|
||||||
|
|
||||||
coVerify(exactly = 1) {
|
coVerify(exactly = 1) {
|
||||||
memberLabelRepo.setLabel(MemberLabel(text = "New Label", emoji = "🎉"))
|
memberLabelRepo.setLabel(groupId, MemberLabel(text = "New Label", emoji = "🎉"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `save calls setLabel with cleared values when clearLabel is called`() = runTest(testDispatcher) {
|
fun `save calls setLabel with cleared values when clearLabel is called`() = runTest(testDispatcher) {
|
||||||
coEvery { memberLabelRepo.getLabel(any<RecipientId>()) } returns MemberLabel(emoji = "🎉", text = "Original")
|
coEvery { memberLabelRepo.getLabel(groupId, any<RecipientId>()) } returns MemberLabel(emoji = "🎉", text = "Original")
|
||||||
|
|
||||||
val viewModel = MemberLabelViewModel(memberLabelRepo, recipientId)
|
val viewModel = MemberLabelViewModel(memberLabelRepo, groupId, recipientId)
|
||||||
viewModel.clearLabel()
|
viewModel.clearLabel()
|
||||||
viewModel.save()
|
viewModel.save()
|
||||||
|
|
||||||
coVerify(exactly = 1) {
|
coVerify(exactly = 1) {
|
||||||
memberLabelRepo.setLabel(MemberLabel(text = "", emoji = null))
|
memberLabelRepo.setLabel(groupId, MemberLabel(text = "", emoji = null))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user