Show some more info in the about sheet.

This commit is contained in:
Greyson Parrelli 2024-02-13 12:27:09 -05:00 committed by Cody Henthorne
parent 47cdc50a81
commit 57ac7cb328
7 changed files with 251 additions and 58 deletions

View file

@ -5,6 +5,7 @@
package org.thoughtcrime.securesms.recipients.ui.about
import android.content.res.Configuration
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@ -26,7 +27,6 @@ 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.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
@ -38,6 +38,7 @@ 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.signal.core.util.isNotNullOrBlank
import org.thoughtcrime.securesms.AvatarPreviewActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.avatar.AvatarImage
@ -81,11 +82,27 @@ class AboutSheet : ComposeBottomSheetDialogFragment() {
override fun SheetContent() {
val recipient by viewModel.recipient
val groupsInCommonCount by viewModel.groupsInCommonCount
val verified by viewModel.verified
if (recipient.isPresent) {
AboutSheetContent(
recipient = recipient.get(),
groupsInCommonCount = groupsInCommonCount,
Content(
model = AboutModel(
isSelf = recipient.get().isSelf,
hasAvatar = recipient.get().profileAvatarFileDetails.hasFile(),
displayName = recipient.get().getDisplayName(requireContext()),
shortName = recipient.get().getShortDisplayName(requireContext()),
about = recipient.get().about,
verified = verified,
recipientForAvatar = recipient.get(),
formattedE164 = if (recipient.get().hasE164() && recipient.get().shouldShowE164()) {
PhoneNumberFormatter.get(requireContext()).prettyPrintFormat(recipient.get().requireE164())
} else {
null
},
groupsInCommon = groupsInCommonCount,
profileSharing = recipient.get().isProfileSharing,
systemContact = recipient.get().isSystemContact
),
onClickSignalConnections = this::openSignalConnectionsSheet,
onAvatarClicked = this::openProfilePhotoViewer
)
@ -102,25 +119,23 @@ class AboutSheet : ComposeBottomSheetDialogFragment() {
}
}
@Preview
@Composable
private fun AboutSheetContentPreview() {
SignalTheme {
Surface {
AboutSheetContent(
recipient = Recipient.UNKNOWN,
groupsInCommonCount = 0,
onClickSignalConnections = {},
onAvatarClicked = {}
)
}
}
}
private data class AboutModel(
val isSelf: Boolean,
val displayName: String,
val shortName: String,
val about: String?,
val verified: Boolean,
val hasAvatar: Boolean,
val recipientForAvatar: Recipient,
val formattedE164: String?,
val profileSharing: Boolean,
val systemContact: Boolean,
val groupsInCommon: Int
)
@Composable
private fun AboutSheetContent(
recipient: Recipient,
groupsInCommonCount: Int,
private fun Content(
model: AboutModel,
onClickSignalConnections: () -> Unit,
onAvatarClicked: () -> Unit
) {
@ -131,8 +146,8 @@ private fun AboutSheetContent(
BottomSheets.Handle(modifier = Modifier.padding(top = 6.dp))
}
val avatarOnClick = remember(recipient.profileAvatarFileDetails.hasFile()) {
if (recipient.profileAvatarFileDetails.hasFile()) {
val avatarOnClick = remember(model.hasAvatar) {
if (model.hasAvatar) {
onAvatarClicked
} else {
{ }
@ -141,7 +156,7 @@ private fun AboutSheetContent(
Column(horizontalAlignment = Alignment.CenterHorizontally) {
AvatarImage(
recipient = recipient,
recipient = model.recipientForAvatar,
modifier = Modifier
.padding(top = 56.dp)
.size(240.dp)
@ -150,7 +165,7 @@ private fun AboutSheetContent(
)
Text(
text = stringResource(id = if (recipient.isSelf) R.string.AboutSheet__you else R.string.AboutSheet__about),
text = stringResource(id = if (model.isSelf) R.string.AboutSheet__you else R.string.AboutSheet__about),
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier
.fillMaxWidth()
@ -158,22 +173,19 @@ private fun AboutSheetContent(
.padding(top = 20.dp, bottom = 14.dp)
)
val context = LocalContext.current
val displayName = remember(recipient) { recipient.getDisplayName(context) }
AboutRow(
startIcon = painterResource(R.drawable.symbol_person_24),
text = displayName,
text = model.displayName,
modifier = Modifier.fillMaxWidth()
)
if (!recipient.about.isNullOrBlank()) {
if (model.about.isNotNullOrBlank()) {
AboutRow(
startIcon = painterResource(R.drawable.symbol_edit_24),
text = {
Row {
AndroidView(factory = ::EmojiTextView) {
it.text = recipient.combinedAboutAndEmoji
it.text = model.about
TextViewCompat.setTextAppearance(it, R.style.Signal_Text_BodyLarge)
}
@ -183,7 +195,16 @@ private fun AboutSheetContent(
)
}
if (recipient.isProfileSharing) {
if (model.verified) {
AboutRow(
startIcon = painterResource(id = R.drawable.check),
text = stringResource(id = R.string.AboutSheet__verified),
modifier = Modifier.align(alignment = Alignment.Start),
onClick = onClickSignalConnections
)
}
if (model.profileSharing || model.systemContact) {
AboutRow(
startIcon = painterResource(id = R.drawable.symbol_connections_24),
text = stringResource(id = R.string.AboutSheet__signal_connection),
@ -191,36 +212,38 @@ private fun AboutSheetContent(
modifier = Modifier.align(alignment = Alignment.Start),
onClick = onClickSignalConnections
)
} else {
AboutRow(
startIcon = painterResource(id = R.drawable.chat_x),
text = stringResource(id = R.string.AboutSheet__no_direct_message, model.shortName),
modifier = Modifier.align(alignment = Alignment.Start),
onClick = onClickSignalConnections
)
}
val shortName = remember(recipient) { recipient.getShortDisplayName(context) }
if (recipient.isSystemContact) {
if (model.systemContact) {
AboutRow(
startIcon = painterResource(id = R.drawable.symbol_person_circle_24),
text = stringResource(id = R.string.AboutSheet__s_is_in_your_system_contacts, shortName),
text = stringResource(id = R.string.AboutSheet__s_is_in_your_system_contacts, model.shortName),
modifier = Modifier.fillMaxWidth()
)
}
if (recipient.e164.isPresent && recipient.shouldShowE164()) {
val e164 = remember(recipient.e164.get()) {
PhoneNumberFormatter.get(context).prettyPrintFormat(recipient.e164.get())
}
if (model.formattedE164.isNotNullOrBlank()) {
AboutRow(
startIcon = painterResource(R.drawable.symbol_phone_24),
text = e164,
text = model.formattedE164,
modifier = Modifier.fillMaxWidth()
)
}
val groupsInCommonText = if (recipient.hasGroupsInCommon()) {
pluralStringResource(id = R.plurals.AboutSheet__d_groups_in, groupsInCommonCount, groupsInCommonCount)
val groupsInCommonText = if (model.groupsInCommon > 0) {
pluralStringResource(id = R.plurals.AboutSheet__d_groups_in, model.groupsInCommon, model.groupsInCommon)
} else {
stringResource(id = R.string.AboutSheet__you_have_no_groups_in_common)
}
val groupsInCommonIcon = if (!recipient.isProfileSharing && groupsInCommonCount == 0) {
val groupsInCommonIcon = if (!model.profileSharing && model.groupsInCommon == 0) {
painterResource(R.drawable.symbol_error_circle_24)
} else {
painterResource(R.drawable.symbol_group_24)
@ -236,20 +259,6 @@ private fun AboutSheetContent(
}
}
@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,
@ -318,3 +327,126 @@ private fun AboutRow(
}
}
}
@Preview(name = "Light Theme", group = "content", uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark Theme", group = "content", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ContentPreviewDefault() {
SignalTheme {
Surface {
Content(
model = AboutModel(
isSelf = false,
hasAvatar = true,
displayName = "Peter Parker",
shortName = "Peter",
about = "Photographer for the Daily Bugle.",
verified = true,
recipientForAvatar = Recipient.UNKNOWN,
formattedE164 = "(123) 456-7890",
profileSharing = true,
systemContact = true,
groupsInCommon = 0
),
onClickSignalConnections = {},
onAvatarClicked = {}
)
}
}
}
@Preview(name = "Light Theme", group = "content", uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark Theme", group = "content", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ContentPreviewInContactsNotProfileSharing() {
SignalTheme {
Surface {
Content(
model = AboutModel(
isSelf = false,
hasAvatar = true,
displayName = "Peter Parker",
shortName = "Peter",
about = "Photographer for the Daily Bugle.",
verified = false,
recipientForAvatar = Recipient.UNKNOWN,
formattedE164 = null,
profileSharing = false,
systemContact = true,
groupsInCommon = 3
),
onClickSignalConnections = {},
onAvatarClicked = {}
)
}
}
}
@Preview(name = "Light Theme", group = "content", uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark Theme", group = "content", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ContentPreviewGroupsInCommonNoE164() {
SignalTheme {
Surface {
Content(
model = AboutModel(
isSelf = false,
hasAvatar = true,
displayName = "Peter Parker",
shortName = "Peter",
about = "Photographer for the Daily Bugle.",
verified = false,
recipientForAvatar = Recipient.UNKNOWN,
formattedE164 = null,
profileSharing = true,
systemContact = false,
groupsInCommon = 3
),
onClickSignalConnections = {},
onAvatarClicked = {}
)
}
}
}
@Preview(name = "Light Theme", group = "content", uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark Theme", group = "content", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ContentPreviewNotAConnection() {
SignalTheme {
Surface {
Content(
model = AboutModel(
isSelf = false,
hasAvatar = true,
displayName = "Peter Parker",
shortName = "Peter",
about = "Photographer for the Daily Bugle.",
verified = false,
recipientForAvatar = Recipient.UNKNOWN,
formattedE164 = null,
profileSharing = false,
systemContact = false,
groupsInCommon = 3
),
onClickSignalConnections = {},
onAvatarClicked = {}
)
}
}
}
@Preview(name = "Light Theme", group = "about row", uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark Theme", group = "about row", uiMode = Configuration.UI_MODE_NIGHT_YES)
@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)
)
}
}
}

View file

@ -7,7 +7,9 @@ package org.thoughtcrime.securesms.recipients.ui.about
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.thoughtcrime.securesms.database.IdentityTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.recipients.RecipientId
class AboutSheetRepository {
@ -16,4 +18,11 @@ class AboutSheetRepository {
SignalDatabase.groups.getPushGroupsContainingMember(recipientId).size
}.subscribeOn(Schedulers.io())
}
fun getVerified(recipientId: RecipientId): Single<Boolean> {
return Single.fromCallable {
val identityRecord = ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecord(recipientId)
identityRecord.isPresent && identityRecord.get().verifiedStatus == IdentityTable.VerifiedStatus.VERIFIED
}.subscribeOn(Schedulers.io())
}
}

View file

@ -30,6 +30,9 @@ class AboutSheetViewModel(
private val _groupsInCommonCount: MutableIntState = mutableIntStateOf(0)
val groupsInCommonCount: IntState = _groupsInCommonCount
private val _verified: MutableState<Boolean> = mutableStateOf(false)
val verified: State<Boolean> = _verified
private val recipientDisposable: Disposable = Recipient
.observable(recipientId)
.observeOn(AndroidSchedulers.mainThread())
@ -44,6 +47,13 @@ class AboutSheetViewModel(
_groupsInCommonCount.intValue = it
}
private val verifiedDisposable: Disposable = repository
.getVerified(recipientId)
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy {
_verified.value = it
}
override fun onCleared() {
recipientDisposable.dispose()
groupsInCommonDisposable.dispose()

View file

@ -0,0 +1,17 @@
<!--
~ Copyright 2024 Signal Messenger, LLC
~ SPDX-License-Identifier: AGPL-3.0-only
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M8.26 8.26c0.34-0.35 0.9-0.35 1.23 0l2.51 2.5 2.5-2.5c0.35-0.35 0.9-0.35 1.24 0 0.35 0.34 0.35 0.9 0 1.23L13.24 12l2.5 2.5c0.35 0.35 0.35 0.9 0 1.24-0.34 0.35-0.9 0.35-1.23 0L12 13.24l-2.5 2.5c-0.35 0.35-0.9 0.35-1.24 0-0.35-0.34-0.35-0.9 0-1.23l2.5-2.51-2.5-2.5c-0.35-0.35-0.35-0.9 0-1.24Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M1.88 12C1.88 6.4 6.4 1.87 12 1.87c5.6 0 10.13 4.54 10.13 10.13 0 5.6-4.54 10.13-10.13 10.13-1.67 0-3.25-0.41-4.64-1.13L3.3 22.46c-1.1 0.4-2.16-0.66-1.76-1.76L3 16.64C2.28 15.25 1.88 13.67 1.88 12ZM12 3.62c-4.63 0-8.37 3.75-8.37 8.38 0 1.43 0.35 2.78 0.99 3.95 0.18 0.34 0.22 0.75 0.08 1.13l-1.25 3.47 3.47-1.25c0.38-0.14 0.79-0.1 1.13 0.09 1.17 0.63 2.52 0.98 3.95 0.98 4.63 0 8.38-3.74 8.38-8.37 0-4.63-3.75-8.38-8.38-8.38Z"/>
</vector>

View file

@ -0,0 +1,14 @@
<!--
~ Copyright 2024 Signal Messenger, LLC
~ SPDX-License-Identifier: AGPL-3.0-only
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M18.97 5.26c0.4 0.26 0.53 0.8 0.27 1.21l-8.32 13c-0.16 0.24-0.42 0.4-0.7 0.4-0.28 0.02-0.55-0.1-0.73-0.33l-4.68-5.98c-0.3-0.38-0.23-0.93 0.15-1.23 0.38-0.3 0.93-0.23 1.23 0.15l3.92 5 7.65-11.95c0.26-0.4 0.8-0.53 1.21-0.27Z"/>
</vector>

View file

@ -1965,6 +1965,10 @@
<!-- AboutSheet -->
<!-- Displayed in a sheet row and allows user to open signal connection explanation on tap -->
<string name="AboutSheet__signal_connection">Signal connection</string>
<!-- Displayed in a sheet row describing that the user has marked this contact as 'verified' from within the app -->
<string name="AboutSheet__verified">Verified</string>
<!-- Displayed in bottom sheet describing that the user has no direct messages with this person. The placeholder is a person's name. -->
<string name="AboutSheet__no_direct_message">No direct messages with %1$s</string>
<!-- Explains that the given user (placeholder is short name) is in the users system contact -->
<string name="AboutSheet__s_is_in_your_system_contacts">%1$s is in your system contacts</string>
<!-- Notice in a row when user has no groups in common -->

View file

@ -1,5 +1,8 @@
package org.signal.core.util
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
/**
* Treats the string as a serialized list of tokens and tells you if an item is present in the list.
* In addition to exact matches, this handles wildcards at the end of an item.
@ -55,6 +58,10 @@ fun String?.nullIfBlank(): String? {
}
}
@OptIn(ExperimentalContracts::class)
fun CharSequence?.isNotNullOrBlank(): Boolean {
contract {
returns(true) implies (this@isNotNullOrBlank != null)
}
return !this.isNullOrBlank()
}