From 9924e293c930008084369e9516abf7ac48fff4e8 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Mon, 18 Dec 2023 14:35:15 -0400 Subject: [PATCH] Implement new about sheet. --- .../securesms/avatar/AvatarImage.kt | 37 +++ .../ConversationSettingsFragment.kt | 7 +- .../preferences/BioTextPreference.kt | 20 +- .../recipients/ui/about/AboutSheet.kt | 294 ++++++++++++++++++ .../ui/about/AboutSheetRepository.kt | 19 ++ .../ui/about/AboutSheetViewModel.kt | 51 +++ .../RecipientBottomSheetDialogFragment.java | 7 + ..._right_24_color_on_secondary_container.xml | 10 + .../res/drawable/symbol_connections_24.xml | 24 ++ .../res/layout/recipient_bottom_sheet.xml | 4 +- app/src/main/res/values/strings.xml | 12 + 11 files changed, 477 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarImage.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/recipients/ui/about/AboutSheet.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/recipients/ui/about/AboutSheetRepository.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/recipients/ui/about/AboutSheetViewModel.kt create mode 100644 app/src/main/res/drawable/symbol_chevron_right_24_color_on_secondary_container.xml create mode 100644 app/src/main/res/drawable/symbol_connections_24.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarImage.kt b/app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarImage.kt new file mode 100644 index 0000000000..8e8daa1c52 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarImage.kt @@ -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) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt index adf8509b4c..4192f48468 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt @@ -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 -> diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/BioTextPreference.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/BioTextPreference.kt index 3be3d2f7e8..25f650c3d4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/BioTextPreference.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/BioTextPreference.kt @@ -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() { 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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/about/AboutSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/about/AboutSheet.kt new file mode 100644 index 0000000000..87b4621389 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/about/AboutSheet.kt @@ -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 + ) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/about/AboutSheetRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/about/AboutSheetRepository.kt new file mode 100644 index 0000000000..587638d63e --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/about/AboutSheetRepository.kt @@ -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 { + return Single.fromCallable { + SignalDatabase.groups.getPushGroupsContainingMember(recipientId).size + }.subscribeOn(Schedulers.io()) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/about/AboutSheetViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/about/AboutSheetViewModel.kt new file mode 100644 index 0000000000..1d936ffb4c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/about/AboutSheetViewModel.kt @@ -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> = mutableStateOf(Optional.empty()) + val recipient: State> = _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() + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.java index 3f620fda46..788e28500a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.java @@ -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()) { diff --git a/app/src/main/res/drawable/symbol_chevron_right_24_color_on_secondary_container.xml b/app/src/main/res/drawable/symbol_chevron_right_24_color_on_secondary_container.xml new file mode 100644 index 0000000000..ba3faecca6 --- /dev/null +++ b/app/src/main/res/drawable/symbol_chevron_right_24_color_on_secondary_container.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/symbol_connections_24.xml b/app/src/main/res/drawable/symbol_connections_24.xml new file mode 100644 index 0000000000..fb4e987dd8 --- /dev/null +++ b/app/src/main/res/drawable/symbol_connections_24.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/app/src/main/res/layout/recipient_bottom_sheet.xml b/app/src/main/res/layout/recipient_bottom_sheet.xml index 7f80e3f341..924ace1ae3 100644 --- a/app/src/main/res/layout/recipient_bottom_sheet.xml +++ b/app/src/main/res/layout/recipient_bottom_sheet.xml @@ -1,10 +1,10 @@ + android:layout_height="wrap_content" + tools:viewBindingIgnore="true"> Expand raised hand view + + + Signal connection + + %1$s is in your system contacts + + You have no groups in common + + Review requests carefully + + %1$d groups in common + In this call (%1$d)