mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-03-01 06:07:37 +00:00
Implement the majority of the new nicknames and notes feature.
This commit is contained in:
committed by
Nicholas Tinsley
parent
7a24554b68
commit
303929090b
@@ -14,6 +14,7 @@ import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.doOnPreDraw
|
||||
@@ -77,6 +78,7 @@ import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupsLearnMoreB
|
||||
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestRepository
|
||||
import org.thoughtcrime.securesms.nicknames.NicknameActivity
|
||||
import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientExporter
|
||||
@@ -92,6 +94,7 @@ import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.ContextUtil
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.Material3OnScrollHelper
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
@@ -149,6 +152,7 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
||||
private lateinit var toolbarTitle: TextView
|
||||
private lateinit var toolbarBackground: View
|
||||
private lateinit var addToGroupStoryDelegate: AddToGroupStoryDelegate
|
||||
private lateinit var nicknameLauncher: ActivityResultLauncher<NicknameActivity.Args>
|
||||
|
||||
private val navController get() = Navigation.findNavController(requireView())
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
@@ -216,6 +220,10 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
||||
}
|
||||
|
||||
override fun bindAdapter(adapter: MappingAdapter) {
|
||||
nicknameLauncher = registerForActivityResult(NicknameActivity.Contract()) {
|
||||
// Intentionally left blank
|
||||
}
|
||||
|
||||
val args = ConversationSettingsFragmentArgs.fromBundle(requireArguments())
|
||||
|
||||
BioTextPreference.register(adapter)
|
||||
@@ -495,6 +503,21 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
||||
)
|
||||
}
|
||||
|
||||
if (FeatureFlags.nicknames() && state.recipient.isIndividual) {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.NicknameActivity__nickname),
|
||||
icon = DSLSettingsIcon.from(R.drawable.symbol_edit_24),
|
||||
onClick = {
|
||||
nicknameLauncher.launch(
|
||||
NicknameActivity.Args(
|
||||
state.recipient.id,
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (!state.recipient.isReleaseNotes) {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__chat_color_and_wallpaper),
|
||||
|
||||
@@ -1739,6 +1739,20 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
||||
}
|
||||
}
|
||||
|
||||
fun setNicknameAndNote(id: RecipientId, nickname: ProfileName, note: String) {
|
||||
val contentValues = contentValuesOf(
|
||||
NICKNAME_GIVEN_NAME to nickname.givenName.nullIfBlank(),
|
||||
NICKNAME_FAMILY_NAME to nickname.familyName.nullIfBlank(),
|
||||
NICKNAME_JOINED_NAME to nickname.toString().nullIfBlank(),
|
||||
NOTE to note.nullIfBlank()
|
||||
)
|
||||
if (update(id, contentValues)) {
|
||||
rotateStorageId(id)
|
||||
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
|
||||
StorageSyncHelper.scheduleSyncForDataChange()
|
||||
}
|
||||
}
|
||||
|
||||
fun setProfileName(id: RecipientId, profileName: ProfileName) {
|
||||
val contentValues = ContentValues(1).apply {
|
||||
put(PROFILE_GIVEN_NAME, profileName.givenName.nullIfBlank())
|
||||
|
||||
@@ -0,0 +1,500 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.nicknames
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
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.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.os.bundleOf
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.Dialogs
|
||||
import org.signal.core.ui.DropdownMenus
|
||||
import org.signal.core.ui.Previews
|
||||
import org.signal.core.ui.Scaffolds
|
||||
import org.signal.core.ui.TextFields
|
||||
import org.signal.core.util.getParcelableCompat
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.avatar.AvatarImage
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
|
||||
/**
|
||||
* Fragment allowing a user to set a custom nickname for the given recipient.
|
||||
*/
|
||||
class NicknameActivity : PassphraseRequiredActivity(), NicknameContentCallback {
|
||||
|
||||
private val theme = DynamicNoActionBarTheme()
|
||||
|
||||
private val args: Args by lazy {
|
||||
Args.fromBundle(intent.extras!!)
|
||||
}
|
||||
|
||||
private val viewModel: NicknameViewModel by viewModel {
|
||||
NicknameViewModel(args.recipientId)
|
||||
}
|
||||
|
||||
override fun onPreCreate() {
|
||||
theme.onCreate(this)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
setContent {
|
||||
val state by viewModel.state
|
||||
|
||||
LaunchedEffect(state.formState) {
|
||||
if (state.formState == NicknameState.FormState.SAVED) {
|
||||
supportFinishAfterTransition()
|
||||
}
|
||||
}
|
||||
|
||||
NicknameContent(
|
||||
callback = remember { this },
|
||||
state = state,
|
||||
focusNoteFirst = args.focusNoteFirst
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
theme.onResume(this)
|
||||
}
|
||||
|
||||
override fun onNavigationClick() {
|
||||
supportFinishAfterTransition()
|
||||
}
|
||||
|
||||
override fun onSaveClick() {
|
||||
viewModel.save()
|
||||
}
|
||||
|
||||
override fun onDeleteClick() {
|
||||
viewModel.delete()
|
||||
}
|
||||
|
||||
override fun onFirstNameChanged(value: String) {
|
||||
viewModel.onFirstNameChanged(value)
|
||||
}
|
||||
|
||||
override fun onLastNameChanged(value: String) {
|
||||
viewModel.onLastNameChanged(value)
|
||||
}
|
||||
|
||||
override fun onNoteChanged(value: String) {
|
||||
viewModel.onNoteChanged(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param recipientId The recipient to edit the nickname and note for
|
||||
* @param focusNoteFirst Whether default focus should be on the edit note field
|
||||
*/
|
||||
data class Args(
|
||||
val recipientId: RecipientId,
|
||||
val focusNoteFirst: Boolean
|
||||
) {
|
||||
fun toBundle(): Bundle {
|
||||
return bundleOf(
|
||||
RECIPIENT_ID to recipientId,
|
||||
FOCUS_NOTE_FIRST to focusNoteFirst
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val RECIPIENT_ID = "recipient_id"
|
||||
private const val FOCUS_NOTE_FIRST = "focus_note_first"
|
||||
|
||||
fun fromBundle(bundle: Bundle): Args {
|
||||
return Args(
|
||||
recipientId = bundle.getParcelableCompat(RECIPIENT_ID, RecipientId::class.java)!!,
|
||||
focusNoteFirst = bundle.getBoolean(FOCUS_NOTE_FIRST)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches the nickname activity with the proper arguments.
|
||||
* Doesn't return a response, but is a helpful signal to know when to refresh UI.
|
||||
*/
|
||||
class Contract : ActivityResultContract<Args, Unit>() {
|
||||
override fun createIntent(context: Context, input: Args): Intent {
|
||||
return Intent(context, NicknameActivity::class.java).putExtras(input.toBundle())
|
||||
}
|
||||
|
||||
override fun parseResult(resultCode: Int, intent: Intent?) = Unit
|
||||
}
|
||||
}
|
||||
|
||||
private interface NicknameContentCallback {
|
||||
fun onNavigationClick()
|
||||
fun onSaveClick()
|
||||
fun onDeleteClick()
|
||||
fun onFirstNameChanged(value: String)
|
||||
fun onLastNameChanged(value: String)
|
||||
fun onNoteChanged(value: String)
|
||||
}
|
||||
|
||||
@Preview(name = "Light Theme", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
@Preview(name = "Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun NicknameContentPreview() {
|
||||
Previews.Preview {
|
||||
val callback = remember {
|
||||
object : NicknameContentCallback {
|
||||
override fun onNavigationClick() = Unit
|
||||
override fun onSaveClick() = Unit
|
||||
override fun onDeleteClick() = Unit
|
||||
override fun onFirstNameChanged(value: String) = Unit
|
||||
override fun onLastNameChanged(value: String) = Unit
|
||||
override fun onNoteChanged(value: String) = Unit
|
||||
}
|
||||
}
|
||||
|
||||
NicknameContent(
|
||||
callback = callback,
|
||||
state = NicknameState(
|
||||
isEditing = true,
|
||||
note = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod temor incididunt ut labore et dolore magna aliqua. Ut enim ad minimu"
|
||||
),
|
||||
focusNoteFirst = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NicknameContent(
|
||||
callback: NicknameContentCallback,
|
||||
state: NicknameState,
|
||||
focusNoteFirst: Boolean
|
||||
) {
|
||||
var displayDeletionDialog by remember { mutableStateOf(false) }
|
||||
|
||||
Scaffolds.Settings(
|
||||
title = stringResource(id = R.string.NicknameActivity__nickname),
|
||||
onNavigationClick = callback::onNavigationClick,
|
||||
navigationIconPainter = painterResource(id = R.drawable.symbol_arrow_left_24),
|
||||
actions = {
|
||||
if (state.isEditing) {
|
||||
val menuController = remember {
|
||||
DropdownMenus.MenuController()
|
||||
}
|
||||
|
||||
IconButton(onClick = { menuController.toggle() }) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.symbol_more_vertical_24),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
DropdownMenus.Menu(
|
||||
controller = menuController
|
||||
) {
|
||||
DropdownMenus.Item(
|
||||
text = {
|
||||
Text(text = stringResource(id = R.string.delete))
|
||||
},
|
||||
onClick = {
|
||||
displayDeletionDialog = true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
) { paddingValues ->
|
||||
|
||||
val firstNameFocusRequester = remember { FocusRequester() }
|
||||
val noteFocusRequester = remember { FocusRequester() }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
|
||||
) {
|
||||
LazyColumn(modifier = Modifier.weight(1f)) {
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(id = R.string.NicknameActivity__nicknames_amp_notes),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 24.dp)
|
||||
) {
|
||||
if (state.recipient != null) {
|
||||
AvatarImage(recipient = state.recipient, modifier = Modifier.size(80.dp))
|
||||
} else {
|
||||
Spacer(modifier = Modifier.size(80.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
ClearableTextField(
|
||||
value = state.firstName,
|
||||
hint = stringResource(id = R.string.NicknameActivity__first_name),
|
||||
clearContentDescription = stringResource(id = R.string.NicknameActivity__clear_first_name),
|
||||
enabled = true,
|
||||
singleLine = true,
|
||||
onValueChange = callback::onFirstNameChanged,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
|
||||
modifier = Modifier
|
||||
.focusRequester(firstNameFocusRequester)
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 20.dp)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
ClearableTextField(
|
||||
value = state.lastName,
|
||||
hint = stringResource(id = R.string.NicknameActivity__last_name),
|
||||
clearContentDescription = stringResource(id = R.string.NicknameActivity__clear_last_name),
|
||||
enabled = state.firstName.isNotBlank(),
|
||||
singleLine = true,
|
||||
onValueChange = callback::onLastNameChanged,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 20.dp)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
ClearableTextField(
|
||||
value = state.note,
|
||||
hint = stringResource(id = R.string.NicknameActivity__note),
|
||||
clearContentDescription = "",
|
||||
clearable = false,
|
||||
enabled = true,
|
||||
onValueChange = callback::onNoteChanged,
|
||||
keyboardActions = KeyboardActions(onDone = {
|
||||
callback.onSaveClick()
|
||||
}),
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
charactersRemaining = state.noteCharactersRemaining,
|
||||
modifier = Modifier
|
||||
.focusRequester(noteFocusRequester)
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
contentAlignment = Alignment.CenterEnd,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 20.dp)
|
||||
) {
|
||||
Buttons.LargeTonal(
|
||||
onClick = callback::onSaveClick,
|
||||
enabled = state.canSave
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.NicknameActivity__save)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (displayDeletionDialog) {
|
||||
Dialogs.SimpleAlertDialog(
|
||||
title = stringResource(id = R.string.NicknameActivity__delete_nickname),
|
||||
body = stringResource(id = R.string.NicknameActivity__this_will_permanently_delete_this_nickname_and_note),
|
||||
confirm = stringResource(id = R.string.delete),
|
||||
dismiss = stringResource(id = android.R.string.cancel),
|
||||
onConfirm = {
|
||||
callback.onDeleteClick()
|
||||
},
|
||||
onDismiss = { displayDeletionDialog = false }
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(state.hasBecomeReady) {
|
||||
if (state.hasBecomeReady) {
|
||||
if (focusNoteFirst) {
|
||||
noteFocusRequester.requestFocus()
|
||||
} else {
|
||||
firstNameFocusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(name = "Light Theme", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
@Preview(name = "Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ClearableTextFieldPreview() {
|
||||
Previews.Preview {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
ClearableTextField(
|
||||
value = "",
|
||||
hint = "Without content",
|
||||
enabled = true,
|
||||
onValueChange = {},
|
||||
clearContentDescription = ""
|
||||
)
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
ClearableTextField(
|
||||
value = "Test",
|
||||
hint = "With Content",
|
||||
enabled = true,
|
||||
onValueChange = {},
|
||||
clearContentDescription = ""
|
||||
)
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
ClearableTextField(
|
||||
value = "",
|
||||
hint = "Disabled",
|
||||
enabled = false,
|
||||
onValueChange = {},
|
||||
clearContentDescription = ""
|
||||
)
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
ClearableTextField(
|
||||
value = "",
|
||||
hint = "Focused",
|
||||
enabled = true,
|
||||
onValueChange = {},
|
||||
modifier = Modifier.focusRequester(focusRequester),
|
||||
clearContentDescription = ""
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun ClearableTextField(
|
||||
value: String,
|
||||
hint: String,
|
||||
clearContentDescription: String,
|
||||
enabled: Boolean,
|
||||
onValueChange: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
singleLine: Boolean = false,
|
||||
clearable: Boolean = true,
|
||||
charactersRemaining: Int = Int.MAX_VALUE,
|
||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default
|
||||
) {
|
||||
var focused by remember { mutableStateOf(false) }
|
||||
|
||||
val displayCountdown = charactersRemaining <= 100
|
||||
|
||||
val clearButton: @Composable () -> Unit = {
|
||||
ClearButton(
|
||||
visible = focused,
|
||||
onClick = { onValueChange("") },
|
||||
contentDescription = clearContentDescription
|
||||
)
|
||||
}
|
||||
|
||||
Box(modifier = modifier) {
|
||||
TextFields.TextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
label = {
|
||||
Text(text = hint)
|
||||
},
|
||||
enabled = enabled,
|
||||
singleLine = singleLine,
|
||||
keyboardActions = keyboardActions,
|
||||
keyboardOptions = keyboardOptions,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.onFocusChanged { focused = it.hasFocus && clearable },
|
||||
colors = TextFieldDefaults.colors(
|
||||
unfocusedLabelColor = MaterialTheme.colorScheme.outline,
|
||||
unfocusedIndicatorColor = MaterialTheme.colorScheme.outline
|
||||
),
|
||||
trailingIcon = if (clearable) clearButton else null,
|
||||
contentPadding = TextFieldDefaults.contentPaddingWithLabel(end = if (displayCountdown) 48.dp else 16.dp)
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = displayCountdown,
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(bottom = 10.dp, end = 12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "$charactersRemaining",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = if (charactersRemaining <= 5) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.outline
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ClearButton(
|
||||
visible: Boolean,
|
||||
onClick: () -> Unit,
|
||||
contentDescription: String
|
||||
) {
|
||||
AnimatedVisibility(visible = visible) {
|
||||
IconButton(
|
||||
onClick = onClick
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.symbol_x_circle_fill_24),
|
||||
contentDescription = contentDescription,
|
||||
tint = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.nicknames
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
data class NicknameState(
|
||||
val recipient: Recipient? = null,
|
||||
val firstName: String = "",
|
||||
val lastName: String = "",
|
||||
val note: String = "",
|
||||
val noteCharactersRemaining: Int = 0,
|
||||
val formState: FormState = FormState.LOADING,
|
||||
val hasBecomeReady: Boolean = false,
|
||||
val isEditing: Boolean = false
|
||||
) {
|
||||
|
||||
private val isFormBlank: Boolean = firstName.isBlank() && lastName.isBlank() && note.isBlank()
|
||||
private val hasFirstNameOrNote: Boolean = firstName.isNotBlank() || note.isNotBlank()
|
||||
private val isFormReady: Boolean = formState == FormState.READY
|
||||
private val isBlankFormDuringEdit: Boolean = isFormBlank && isEditing
|
||||
|
||||
val canSave: Boolean = isFormReady && (hasFirstNameOrNote || isBlankFormDuringEdit)
|
||||
enum class FormState {
|
||||
LOADING,
|
||||
READY,
|
||||
SAVING,
|
||||
SAVED
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.nicknames
|
||||
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.core.util.BreakIteratorCompat
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
class NicknameViewModel(
|
||||
private val recipientId: RecipientId
|
||||
) : ViewModel() {
|
||||
companion object {
|
||||
private const val NAME_MAX_LENGTH = 26
|
||||
private const val NOTE_MAX_LENGTH = 240
|
||||
}
|
||||
|
||||
private val internalState = mutableStateOf(NicknameState())
|
||||
private val iteratorCompat = BreakIteratorCompat.getInstance()
|
||||
|
||||
val state: MutableState<NicknameState> = internalState
|
||||
|
||||
private val recipientDisposable = Recipient.observable(recipientId)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { recipient ->
|
||||
internalState.value = if (state.value.formState == NicknameState.FormState.LOADING) {
|
||||
val noteLength = iteratorCompat.run {
|
||||
setText(recipient.note ?: "")
|
||||
countBreaks()
|
||||
}
|
||||
|
||||
NicknameState(
|
||||
recipient = recipient,
|
||||
firstName = recipient.nickname.givenName,
|
||||
lastName = recipient.nickname.familyName,
|
||||
note = recipient.note ?: "",
|
||||
noteCharactersRemaining = NOTE_MAX_LENGTH - noteLength,
|
||||
formState = NicknameState.FormState.READY,
|
||||
hasBecomeReady = true,
|
||||
isEditing = !recipient.nickname.isEmpty
|
||||
)
|
||||
} else {
|
||||
state.value.copy(recipient = recipient)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
recipientDisposable.dispose()
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun onFirstNameChanged(value: String) {
|
||||
iteratorCompat.setText(value)
|
||||
internalState.value = state.value.copy(firstName = iteratorCompat.take(NAME_MAX_LENGTH).toString())
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun onLastNameChanged(value: String) {
|
||||
iteratorCompat.setText(value)
|
||||
internalState.value = state.value.copy(lastName = iteratorCompat.take(NAME_MAX_LENGTH).toString())
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun onNoteChanged(value: String) {
|
||||
iteratorCompat.setText(value)
|
||||
val trimmed = iteratorCompat.take(NOTE_MAX_LENGTH)
|
||||
val count = iteratorCompat.run {
|
||||
setText(trimmed)
|
||||
countBreaks()
|
||||
}
|
||||
|
||||
internalState.value = state.value.copy(
|
||||
note = iteratorCompat.take(NOTE_MAX_LENGTH).toString(),
|
||||
noteCharactersRemaining = NOTE_MAX_LENGTH - count
|
||||
)
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun delete() {
|
||||
viewModelScope.launch {
|
||||
internalState.value = state.value.copy(formState = NicknameState.FormState.SAVING)
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
SignalDatabase.recipients.setNicknameAndNote(
|
||||
recipientId,
|
||||
ProfileName.EMPTY,
|
||||
""
|
||||
)
|
||||
}
|
||||
|
||||
internalState.value = state.value.copy(formState = NicknameState.FormState.SAVED)
|
||||
}
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun save() {
|
||||
viewModelScope.launch {
|
||||
val stateSnapshot = state.value.copy(formState = NicknameState.FormState.SAVING)
|
||||
internalState.value = stateSnapshot
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
SignalDatabase.recipients.setNicknameAndNote(
|
||||
recipientId,
|
||||
ProfileName.fromParts(stateSnapshot.firstName, stateSnapshot.lastName),
|
||||
stateSnapshot.note
|
||||
)
|
||||
}
|
||||
|
||||
internalState.value = state.value.copy(formState = NicknameState.FormState.SAVED)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.nicknames
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.util.Linkify
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
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.Color
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.text.method.LinkMovementMethodCompat
|
||||
import androidx.core.text.util.LinkifyCompat
|
||||
import org.signal.core.ui.BottomSheets
|
||||
import org.signal.core.ui.Previews
|
||||
import org.signal.core.util.getParcelableCompat
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
|
||||
/**
|
||||
* Allows user to view the full note for a given recipient.
|
||||
*/
|
||||
class ViewNoteSheet : ComposeBottomSheetDialogFragment() {
|
||||
|
||||
companion object {
|
||||
|
||||
private const val RECIPIENT_ID = "recipient_id"
|
||||
|
||||
@JvmStatic
|
||||
fun create(recipientId: RecipientId): ViewNoteSheet {
|
||||
return ViewNoteSheet().apply {
|
||||
arguments = bundleOf(
|
||||
RECIPIENT_ID to recipientId
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val recipientId: RecipientId by lazy {
|
||||
requireArguments().getParcelableCompat(RECIPIENT_ID, RecipientId::class.java)!!
|
||||
}
|
||||
|
||||
private val viewModel: ViewNoteSheetViewModel by viewModel {
|
||||
ViewNoteSheetViewModel(recipientId)
|
||||
}
|
||||
|
||||
private lateinit var editNoteLauncher: ActivityResultLauncher<NicknameActivity.Args>
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
editNoteLauncher = registerForActivityResult(NicknameActivity.Contract()) {}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun SheetContent() {
|
||||
val note by remember { viewModel.note }
|
||||
|
||||
ViewNoteBottomSheetContent(
|
||||
onEditNoteClick = this::onEditNoteClick,
|
||||
note = note
|
||||
)
|
||||
}
|
||||
|
||||
private fun onEditNoteClick() {
|
||||
editNoteLauncher.launch(
|
||||
NicknameActivity.Args(
|
||||
recipientId = recipientId,
|
||||
focusNoteFirst = true
|
||||
)
|
||||
)
|
||||
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun ViewNoteBottomSheetContentPreview() {
|
||||
Previews.Preview {
|
||||
ViewNoteBottomSheetContent(
|
||||
onEditNoteClick = {},
|
||||
note = "Lorem ipsum dolor sit amet\n\nWebsite: https://example.com"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun ViewNoteBottomSheetContent(
|
||||
onEditNoteClick: () -> Unit,
|
||||
note: String
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
|
||||
) {
|
||||
BottomSheets.Handle()
|
||||
|
||||
CenterAlignedTopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.ViewNoteSheet__note)
|
||||
)
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = onEditNoteClick) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.symbol_edit_24),
|
||||
contentDescription = stringResource(id = R.string.ViewNoteSheet__edit_note)
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
||||
containerColor = Color.Transparent,
|
||||
scrolledContainerColor = Color.Transparent
|
||||
)
|
||||
)
|
||||
|
||||
val mask = if (LocalInspectionMode.current) {
|
||||
Linkify.WEB_URLS
|
||||
} else {
|
||||
Linkify.WEB_URLS or Linkify.EMAIL_ADDRESSES or Linkify.PHONE_NUMBERS
|
||||
}
|
||||
|
||||
AndroidView(
|
||||
factory = { context ->
|
||||
val view = EmojiTextView(context)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
view.setTextAppearance(context, R.style.Signal_Text_BodyLarge)
|
||||
view.movementMethod = LinkMovementMethodCompat.getInstance()
|
||||
|
||||
view
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 48.dp)
|
||||
) {
|
||||
it.text = note
|
||||
|
||||
LinkifyCompat.addLinks(it, mask)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.nicknames
|
||||
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
class ViewNoteSheetViewModel(
|
||||
recipientId: RecipientId
|
||||
) : ViewModel() {
|
||||
private val internalNote = mutableStateOf("")
|
||||
val note: State<String> = internalNote
|
||||
|
||||
private val recipientDisposable = Recipient.observable(recipientId)
|
||||
.map { it.note ?: "" }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeBy { internalNote.value = it }
|
||||
|
||||
override fun onCleared() {
|
||||
recipientDisposable.dispose()
|
||||
}
|
||||
}
|
||||
@@ -505,6 +505,10 @@ public class Recipient {
|
||||
return contactUri;
|
||||
}
|
||||
|
||||
public @Nullable String getNote() {
|
||||
return note;
|
||||
}
|
||||
|
||||
public @Nullable String getGroupName(@NonNull Context context) {
|
||||
if (groupId != null && Util.isEmpty(this.groupName)) {
|
||||
RecipientId selfId = ApplicationDependencies.getRecipientCache().getSelfId();
|
||||
|
||||
@@ -10,6 +10,7 @@ import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@@ -17,6 +18,7 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
@@ -27,9 +29,11 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
@@ -44,6 +48,7 @@ import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.avatar.AvatarImage
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.nicknames.ViewNoteSheet
|
||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
@@ -101,10 +106,12 @@ class AboutSheet : ComposeBottomSheetDialogFragment() {
|
||||
},
|
||||
groupsInCommon = groupsInCommonCount,
|
||||
profileSharing = recipient.get().isProfileSharing,
|
||||
systemContact = recipient.get().isSystemContact
|
||||
systemContact = recipient.get().isSystemContact,
|
||||
note = recipient.get().note ?: ""
|
||||
),
|
||||
onClickSignalConnections = this::openSignalConnectionsSheet,
|
||||
onAvatarClicked = this::openProfilePhotoViewer
|
||||
onAvatarClicked = this::openProfilePhotoViewer,
|
||||
onNoteClicked = this::openNoteSheet
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -117,6 +124,11 @@ class AboutSheet : ComposeBottomSheetDialogFragment() {
|
||||
private fun openProfilePhotoViewer() {
|
||||
startActivity(AvatarPreviewActivity.intentFromRecipientId(requireContext(), recipientId))
|
||||
}
|
||||
|
||||
private fun openNoteSheet() {
|
||||
dismiss()
|
||||
ViewNoteSheet.create(recipientId).show(parentFragmentManager, null)
|
||||
}
|
||||
}
|
||||
|
||||
private data class AboutModel(
|
||||
@@ -130,14 +142,16 @@ private data class AboutModel(
|
||||
val formattedE164: String?,
|
||||
val profileSharing: Boolean,
|
||||
val systemContact: Boolean,
|
||||
val groupsInCommon: Int
|
||||
val groupsInCommon: Int,
|
||||
val note: String
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun Content(
|
||||
model: AboutModel,
|
||||
onClickSignalConnections: () -> Unit,
|
||||
onAvatarClicked: () -> Unit
|
||||
onAvatarClicked: () -> Unit,
|
||||
onNoteClicked: () -> Unit
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
@@ -180,12 +194,15 @@ private fun Content(
|
||||
)
|
||||
|
||||
if (model.about.isNotNullOrBlank()) {
|
||||
val textColor = LocalContentColor.current
|
||||
|
||||
AboutRow(
|
||||
startIcon = painterResource(R.drawable.symbol_edit_24),
|
||||
text = {
|
||||
Row {
|
||||
AndroidView(factory = ::EmojiTextView) {
|
||||
it.text = model.about
|
||||
it.setTextColor(textColor.toArgb())
|
||||
|
||||
TextViewCompat.setTextAppearance(it, R.style.Signal_Text_BodyLarge)
|
||||
}
|
||||
@@ -255,6 +272,16 @@ private fun Content(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
if (model.note.isNotBlank()) {
|
||||
AboutRow(
|
||||
startIcon = painterResource(id = R.drawable.symbol_note_light_24),
|
||||
text = model.note,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
endIcon = painterResource(id = R.drawable.symbol_chevron_right_compact_bold_16),
|
||||
onClick = onNoteClicked
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.size(26.dp))
|
||||
}
|
||||
}
|
||||
@@ -272,7 +299,10 @@ private fun AboutRow(
|
||||
text = {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f, false)
|
||||
)
|
||||
},
|
||||
modifier = modifier,
|
||||
@@ -284,7 +314,7 @@ private fun AboutRow(
|
||||
@Composable
|
||||
private fun AboutRow(
|
||||
startIcon: Painter,
|
||||
text: @Composable () -> Unit,
|
||||
text: @Composable RowScope.() -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
endIcon: Painter? = null,
|
||||
onClick: (() -> Unit)? = null
|
||||
@@ -346,10 +376,12 @@ private fun ContentPreviewDefault() {
|
||||
formattedE164 = "(123) 456-7890",
|
||||
profileSharing = true,
|
||||
systemContact = true,
|
||||
groupsInCommon = 0
|
||||
groupsInCommon = 0,
|
||||
note = "GET ME SPIDERMAN BEFORE I BLOW A DANG GASKET"
|
||||
),
|
||||
onClickSignalConnections = {},
|
||||
onAvatarClicked = {}
|
||||
onAvatarClicked = {},
|
||||
onNoteClicked = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -373,10 +405,12 @@ private fun ContentPreviewInContactsNotProfileSharing() {
|
||||
formattedE164 = null,
|
||||
profileSharing = false,
|
||||
systemContact = true,
|
||||
groupsInCommon = 3
|
||||
groupsInCommon = 3,
|
||||
note = "GET ME SPIDER MAN"
|
||||
),
|
||||
onClickSignalConnections = {},
|
||||
onAvatarClicked = {}
|
||||
onAvatarClicked = {},
|
||||
onNoteClicked = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -400,10 +434,12 @@ private fun ContentPreviewGroupsInCommonNoE164() {
|
||||
formattedE164 = null,
|
||||
profileSharing = true,
|
||||
systemContact = false,
|
||||
groupsInCommon = 3
|
||||
groupsInCommon = 3,
|
||||
note = "GET ME SPIDERMAN"
|
||||
),
|
||||
onClickSignalConnections = {},
|
||||
onAvatarClicked = {}
|
||||
onAvatarClicked = {},
|
||||
onNoteClicked = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -427,10 +463,12 @@ private fun ContentPreviewNotAConnection() {
|
||||
formattedE164 = null,
|
||||
profileSharing = false,
|
||||
systemContact = false,
|
||||
groupsInCommon = 3
|
||||
groupsInCommon = 3,
|
||||
note = "GET ME SPIDERMAN"
|
||||
),
|
||||
onClickSignalConnections = {},
|
||||
onAvatarClicked = {}
|
||||
onAvatarClicked = {},
|
||||
onNoteClicked = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
@@ -39,6 +40,7 @@ import org.thoughtcrime.securesms.contacts.avatars.FallbackPhoto80dp;
|
||||
import org.thoughtcrime.securesms.fonts.SignalSymbols;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.nicknames.NicknameActivity;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientExporter;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
@@ -46,6 +48,7 @@ import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.recipients.ui.about.AboutSheet;
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil;
|
||||
import org.thoughtcrime.securesms.util.ContextUtil;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.SpanUtil;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
@@ -74,6 +77,7 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF
|
||||
private AvatarView avatar;
|
||||
private TextView fullName;
|
||||
private TextView about;
|
||||
private TextView nickname;
|
||||
private TextView blockButton;
|
||||
private TextView unblockButton;
|
||||
private TextView addContactButton;
|
||||
@@ -92,6 +96,8 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF
|
||||
|
||||
private ButtonStripPreference.ViewHolder buttonStripViewHolder;
|
||||
|
||||
private ActivityResultLauncher<NicknameActivity.Args> nicknameLauncher;
|
||||
|
||||
public static void show(FragmentManager fragmentManager, @NonNull RecipientId recipientId, @Nullable GroupId groupId) {
|
||||
Recipient recipient = Recipient.resolved(recipientId);
|
||||
if (recipient.isSelf()) {
|
||||
@@ -127,6 +133,7 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF
|
||||
avatar = view.findViewById(R.id.rbs_recipient_avatar);
|
||||
fullName = view.findViewById(R.id.rbs_full_name);
|
||||
about = view.findViewById(R.id.rbs_about);
|
||||
nickname = view.findViewById(R.id.rbs_nickname_button);
|
||||
blockButton = view.findViewById(R.id.rbs_block_button);
|
||||
unblockButton = view.findViewById(R.id.rbs_unblock_button);
|
||||
addContactButton = view.findViewById(R.id.rbs_add_contact_button);
|
||||
@@ -150,6 +157,8 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF
|
||||
public void onViewCreated(@NonNull View fragmentView, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(fragmentView, savedInstanceState);
|
||||
|
||||
nicknameLauncher = registerForActivityResult(new NicknameActivity.Contract(), (b) -> {});
|
||||
|
||||
Bundle arguments = requireArguments();
|
||||
RecipientId recipientId = RecipientId.from(Objects.requireNonNull(arguments.getString(ARGS_RECIPIENT_ID)));
|
||||
GroupId groupId = GroupId.parseNullableOrThrow(arguments.getString(ARGS_GROUP_ID));
|
||||
@@ -214,6 +223,16 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF
|
||||
dismiss();
|
||||
AboutSheet.create(recipient).show(getParentFragmentManager(), null);
|
||||
});
|
||||
|
||||
if (FeatureFlags.nicknames() && groupId != null) {
|
||||
nickname.setVisibility(View.VISIBLE);
|
||||
nickname.setOnClickListener(v -> {
|
||||
nicknameLauncher.launch(new NicknameActivity.Args(
|
||||
recipientId,
|
||||
false
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
String aboutText = recipient.getCombinedAboutAndEmoji();
|
||||
|
||||
@@ -127,6 +127,7 @@ public final class FeatureFlags {
|
||||
private static final String RX_MESSAGE_SEND = "android.rxMessageSend";
|
||||
private static final String LINKED_DEVICE_LIFESPAN_SECONDS = "android.linkedDeviceLifespanSeconds";
|
||||
private static final String MESSAGE_BACKUPS = "android.messageBackups";
|
||||
private static final String NICKNAMES = "android.nicknames";
|
||||
|
||||
/**
|
||||
* We will only store remote values for flags in this set. If you want a flag to be controllable
|
||||
@@ -205,7 +206,8 @@ public final class FeatureFlags {
|
||||
PREKEY_FORCE_REFRESH_INTERVAL,
|
||||
CDSI_LIBSIGNAL_NET,
|
||||
RX_MESSAGE_SEND,
|
||||
LINKED_DEVICE_LIFESPAN_SECONDS
|
||||
LINKED_DEVICE_LIFESPAN_SECONDS,
|
||||
NICKNAMES
|
||||
);
|
||||
|
||||
@VisibleForTesting
|
||||
@@ -740,6 +742,11 @@ public final class FeatureFlags {
|
||||
return getBoolean(MESSAGE_BACKUPS, false);
|
||||
}
|
||||
|
||||
/** Whether or not the nicknames feature is available */
|
||||
public static boolean nicknames() {
|
||||
return getBoolean(NICKNAMES, false);
|
||||
}
|
||||
|
||||
/** Only for rendering debug info. */
|
||||
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
|
||||
return new TreeMap<>(REMOTE_VALUES);
|
||||
|
||||
Reference in New Issue
Block a user