Implement new about sheet.

This commit is contained in:
Alex Hart
2023-12-18 14:35:15 -04:00
committed by Clark Chen
parent 490d3549e2
commit 9924e293c9
11 changed files with 477 additions and 8 deletions

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.avatar
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.viewinterop.AndroidView
import org.thoughtcrime.securesms.components.AvatarImageView
import org.thoughtcrime.securesms.recipients.Recipient
@Composable
fun AvatarImage(
recipient: Recipient,
modifier: Modifier = Modifier
) {
if (LocalInspectionMode.current) {
Spacer(
modifier = modifier
.background(color = Color.Red, shape = CircleShape)
)
} else {
AndroidView(
factory = ::AvatarImageView,
modifier = modifier
) {
it.setAvatarUsingProfile(recipient)
}
}
}

View File

@@ -78,6 +78,7 @@ import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientExporter
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.recipients.ui.about.AboutSheet
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment
import org.thoughtcrime.securesms.stories.Stories
import org.thoughtcrime.securesms.stories.StoryViewerArgs
@@ -321,7 +322,11 @@ class ConversationSettingsFragment : DSLSettingsFragment(
)
state.withRecipientSettingsState {
customPref(BioTextPreference.RecipientModel(recipient = state.recipient))
customPref(
BioTextPreference.RecipientModel(recipient = state.recipient, onHeadlineClickListener = {
AboutSheet.create(state.recipient).show(parentFragmentManager, null)
})
)
}
state.withGroupSettingsState { groupState ->

View File

@@ -31,10 +31,13 @@ object BioTextPreference {
abstract fun getHeadlineText(context: Context): CharSequence
abstract fun getSubhead1Text(context: Context): String?
abstract fun getSubhead2Text(): String?
open val onHeadlineClickListener: () -> Unit = {}
}
class RecipientModel(
private val recipient: Recipient
private val recipient: Recipient,
override val onHeadlineClickListener: () -> Unit
) : BioTextPreferenceModel<RecipientModel>() {
override fun getHeadlineText(context: Context): CharSequence {
@@ -44,12 +47,18 @@ object BioTextPreference {
recipient.getDisplayNameOrUsername(context)
}
return if (recipient.showVerified()) {
SpannableStringBuilder(name).apply {
if (!recipient.showVerified() && !recipient.isIndividual) {
return name
}
return SpannableStringBuilder(name).apply {
if (recipient.showVerified()) {
SpanUtil.appendCenteredImageSpan(this, ContextUtil.requireDrawable(context, R.drawable.ic_official_28), 28, 28)
}
} else {
name
if (recipient.isIndividual) {
SpanUtil.appendCenteredImageSpan(this, ContextUtil.requireDrawable(context, R.drawable.symbol_chevron_right_24_color_on_secondary_container), 24, 24)
}
}
}
@@ -101,6 +110,7 @@ object BioTextPreference {
override fun bind(model: T) {
headline.text = model.getHeadlineText(context)
headline.setOnClickListener { model.onHeadlineClickListener() }
model.getSubhead1Text(context).let {
subhead1.text = it

View File

@@ -0,0 +1,294 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.recipients.ui.about
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.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
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.widget.TextViewCompat
import org.signal.core.ui.BottomSheets
import org.signal.core.ui.theme.SignalTheme
import org.signal.core.util.getParcelableCompat
import org.thoughtcrime.securesms.AvatarPreviewActivity
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.phonenumbers.PhoneNumberFormatter
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.stories.settings.my.SignalConnectionsBottomSheetDialogFragment
import org.thoughtcrime.securesms.util.viewModel
/**
* Displays all relevant context you know for a given user on the sheet.
*/
class AboutSheet : ComposeBottomSheetDialogFragment() {
companion object {
private const val RECIPIENT_ID = "recipient_id"
@JvmStatic
fun create(recipient: Recipient): AboutSheet {
return AboutSheet().apply {
arguments = bundleOf(
RECIPIENT_ID to recipient.id
)
}
}
}
override val peekHeightPercentage: Float = 1f
private val recipientId: RecipientId
get() = requireArguments().getParcelableCompat(RECIPIENT_ID, RecipientId::class.java)!!
private val viewModel by viewModel {
AboutSheetViewModel(recipientId)
}
@Composable
override fun SheetContent() {
val recipient by viewModel.recipient
val groupsInCommonCount by viewModel.groupsInCommonCount
if (recipient.isPresent) {
AboutSheetContent(
recipient = recipient.get(),
groupsInCommonCount = groupsInCommonCount,
onClickSignalConnections = this::openSignalConnectionsSheet,
onAvatarClicked = this::openProfilePhotoViewer
)
}
}
private fun openSignalConnectionsSheet() {
dismiss()
SignalConnectionsBottomSheetDialogFragment().show(parentFragmentManager, null)
}
private fun openProfilePhotoViewer() {
startActivity(AvatarPreviewActivity.intentFromRecipientId(requireContext(), recipientId))
}
}
@Preview
@Composable
private fun AboutSheetContentPreview() {
SignalTheme {
Surface {
AboutSheetContent(
recipient = Recipient.UNKNOWN,
groupsInCommonCount = 0,
onClickSignalConnections = {},
onAvatarClicked = {}
)
}
}
}
@Composable
private fun AboutSheetContent(
recipient: Recipient,
groupsInCommonCount: Int,
onClickSignalConnections: () -> Unit,
onAvatarClicked: () -> Unit
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxWidth()
) {
BottomSheets.Handle(modifier = Modifier.padding(top = 6.dp))
}
Column(horizontalAlignment = Alignment.CenterHorizontally) {
AvatarImage(
recipient = recipient,
modifier = Modifier
.padding(top = 56.dp)
.size(240.dp)
.clickable(onClick = onAvatarClicked)
)
Text(
text = "About",
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 32.dp)
.padding(top = 20.dp, bottom = 8.dp)
)
val context = LocalContext.current
val displayName = remember(recipient) { recipient.getDisplayName(context) }
AboutRow(
startIcon = painterResource(R.drawable.symbol_person_24),
text = displayName,
modifier = Modifier.fillMaxWidth()
)
if (recipient.about != null) {
AboutRow(
startIcon = painterResource(R.drawable.symbol_edit_24),
text = {
Row {
AndroidView(factory = ::EmojiTextView) {
it.text = recipient.combinedAboutAndEmoji
TextViewCompat.setTextAppearance(it, R.style.Signal_Text_BodyLarge)
}
}
},
modifier = Modifier.fillMaxWidth()
)
}
if (recipient.isProfileSharing) {
AboutRow(
startIcon = painterResource(id = R.drawable.symbol_connections_24),
text = stringResource(id = R.string.AboutSheet__signal_connection),
endIcon = painterResource(id = R.drawable.symbol_chevron_right_compact_bold_16),
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClickSignalConnections)
)
}
val shortName = remember(recipient) { recipient.getShortDisplayName(context) }
if (recipient.isSystemContact) {
AboutRow(
startIcon = painterResource(id = R.drawable.symbol_person_circle_24),
text = stringResource(id = R.string.AboutSheet__s_is_in_your_system_contacts, shortName),
modifier = Modifier.fillMaxWidth()
)
}
if (recipient.e164.isPresent) {
val e164 = remember(recipient.e164.get()) {
PhoneNumberFormatter.get(context).prettyPrintFormat(recipient.e164.get())
}
AboutRow(
startIcon = painterResource(R.drawable.symbol_phone_24),
text = e164,
modifier = Modifier.fillMaxWidth()
)
}
val groupsInCommonText = if (recipient.hasGroupsInCommon()) {
stringResource(id = R.string.AboutSheet__d_groups_in_common, groupsInCommonCount)
} else {
stringResource(id = R.string.AboutSheet__you_have_no_groups_in_common)
}
AboutRow(
startIcon = painterResource(R.drawable.symbol_group_24),
text = groupsInCommonText,
modifier = Modifier.fillMaxWidth()
)
if (!recipient.isProfileSharing) {
AboutRow(
startIcon = painterResource(R.drawable.symbol_error_circle_24),
text = stringResource(id = R.string.AboutSheet__review_requests_carefully),
modifier = Modifier.fillMaxWidth()
)
}
Spacer(modifier = Modifier.size(32.dp))
}
}
@Preview
@Composable
private fun AboutRowPreview() {
SignalTheme {
Surface {
AboutRow(
startIcon = painterResource(R.drawable.symbol_person_24),
text = "Maya Johnson",
endIcon = painterResource(id = R.drawable.symbol_chevron_right_compact_bold_16)
)
}
}
}
@Composable
private fun AboutRow(
startIcon: Painter,
text: String,
modifier: Modifier = Modifier,
endIcon: Painter? = null
) {
AboutRow(
startIcon = startIcon,
text = {
Text(
text = text,
style = MaterialTheme.typography.bodyLarge
)
},
modifier = modifier,
endIcon = endIcon
)
}
@Composable
private fun AboutRow(
startIcon: Painter,
text: @Composable () -> Unit,
modifier: Modifier = Modifier,
endIcon: Painter? = null
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
.padding(horizontal = 32.dp)
.padding(top = 12.dp)
) {
Icon(
painter = startIcon,
contentDescription = null,
modifier = Modifier
.padding(end = 16.dp)
.size(20.dp)
)
text()
if (endIcon != null) {
Icon(
painter = endIcon,
contentDescription = null,
tint = MaterialTheme.colorScheme.outline
)
}
}
}

View File

@@ -0,0 +1,19 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.recipients.ui.about
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.recipients.RecipientId
class AboutSheetRepository {
fun getGroupsInCommonCount(recipientId: RecipientId): Single<Int> {
return Single.fromCallable {
SignalDatabase.groups.getPushGroupsContainingMember(recipientId).size
}.subscribeOn(Schedulers.io())
}
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.recipients.ui.about
import androidx.compose.runtime.IntState
import androidx.compose.runtime.MutableIntState
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.kotlin.subscribeBy
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import java.util.Optional
class AboutSheetViewModel(
recipientId: RecipientId,
repository: AboutSheetRepository = AboutSheetRepository()
) : ViewModel() {
private val _recipient: MutableState<Optional<Recipient>> = mutableStateOf(Optional.empty())
val recipient: State<Optional<Recipient>> = _recipient
private val _groupsInCommonCount: MutableIntState = mutableIntStateOf(0)
val groupsInCommonCount: IntState = _groupsInCommonCount
private val recipientDisposable: Disposable = Recipient
.observable(recipientId)
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy {
_recipient.value = Optional.of(it)
}
private val groupsInCommonDisposable: Disposable = repository
.getGroupsInCommonCount(recipientId)
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy {
_groupsInCommonCount.intValue = it
}
override fun onCleared() {
recipientDisposable.dispose()
groupsInCommonDisposable.dispose()
}
}

View File

@@ -40,6 +40,7 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientExporter;
import org.thoughtcrime.securesms.recipients.RecipientId;
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.DrawableUtil;
@@ -192,7 +193,13 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF
} else if (recipient.showVerified()) {
SpanUtil.appendCenteredImageSpan(nameBuilder, ContextUtil.requireDrawable(requireContext(), R.drawable.ic_official_28), 28, 28);
}
SpanUtil.appendCenteredImageSpan(nameBuilder, ContextUtil.requireDrawable(requireContext(), R.drawable.symbol_chevron_right_24_color_on_secondary_container), 24, 24);
fullName.setText(nameBuilder);
fullName.setOnClickListener(v -> {
dismiss();
AboutSheet.create(recipient).show(getParentFragmentManager(), null);
});
String aboutText = recipient.getCombinedAboutAndEmoji();
if (recipient.isReleaseNotes()) {