mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-08-06 05:14:50 +01:00
Add media summary pill.
This commit is contained in:
@@ -15,12 +15,14 @@ import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.fragment.compose.AndroidFragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.core.ui.compose.LocalDisplayNameProvider
|
||||
import org.signal.mediasend.HudCommand
|
||||
import org.signal.mediasend.MediaSendActivityContract
|
||||
import org.signal.mediasend.MediaSendRecipient
|
||||
@@ -37,6 +39,7 @@ import org.thoughtcrime.securesms.mediasend.v2.review.AddMessageDialogFragment
|
||||
import org.thoughtcrime.securesms.mediasend.v2.text.TextStoryPostCreationFragment
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.recipients.rememberRecipientField
|
||||
import org.thoughtcrime.securesms.registration.olddevice.QuickTransferOldDeviceActivity
|
||||
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
@@ -79,6 +82,8 @@ class MediaSendV3Activity : PassphraseRequiredActivity(), SafetyNumberBottomShee
|
||||
}
|
||||
|
||||
setContent {
|
||||
val context = LocalContext.current
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalAddAMessageRowTextField provides { message, modifier ->
|
||||
AndroidView(
|
||||
@@ -88,6 +93,15 @@ class MediaSendV3Activity : PassphraseRequiredActivity(), SafetyNumberBottomShee
|
||||
},
|
||||
modifier = modifier
|
||||
)
|
||||
},
|
||||
LocalDisplayNameProvider provides { id ->
|
||||
rememberRecipientField(RecipientId.from(id)) {
|
||||
if (isUnknown) {
|
||||
""
|
||||
} else {
|
||||
getDisplayName(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
MediaSendScreen(
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.core.ui.compose
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
|
||||
/**
|
||||
* Intended to be bound at the app level to allow for the production of an observable display name of a recipient. Raw types are utilized here since
|
||||
* RecipientId and Recipient are both currently localized to the app module and are non-trivial to pick up and move.
|
||||
*/
|
||||
val LocalDisplayNameProvider = compositionLocalOf<@Composable (Long) -> State<String>> {
|
||||
{ remember(it) { mutableStateOf("DisplayName($it)") } }
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
@@ -39,6 +40,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import org.signal.core.ui.WindowBreakpoint
|
||||
import org.signal.core.ui.compose.AllDevicePreviews
|
||||
import org.signal.core.ui.compose.LocalDisplayNameProvider
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.rememberWindowBreakpoint
|
||||
import org.signal.glide.compose.GlideImage
|
||||
@@ -55,6 +57,7 @@ import org.signal.mediasend.edit.image.RotationDial
|
||||
import org.signal.mediasend.edit.video.VideoEditorFragment
|
||||
import org.signal.mediasend.edit.video.VideoEditorToolbar
|
||||
import org.signal.mediasend.edit.video.VideoEditorViewModel
|
||||
import org.signal.mediasend.rememberPreviewState
|
||||
|
||||
@Composable
|
||||
fun MediaEditScreen(
|
||||
@@ -292,6 +295,19 @@ fun MediaEditScreen(
|
||||
.then(if (isTextEditing) Modifier.imePadding() else Modifier)
|
||||
)
|
||||
}
|
||||
|
||||
val displayNameState = state.recipientId?.let { LocalDisplayNameProvider.current(it.id) } ?: remember { mutableStateOf(null) }
|
||||
val displayName: String? by displayNameState
|
||||
|
||||
MediaEditSummaryPill(
|
||||
displayName = displayName,
|
||||
selectedMedia = state.selectedMedia,
|
||||
selectedPage = pagerState.currentPage,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.padding(top = 10.dp)
|
||||
.systemBarsPadding()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,7 +327,7 @@ private fun MediaEditScreenPreview() {
|
||||
|
||||
Previews.Preview {
|
||||
MediaEditScreen(
|
||||
state = MediaSendState(
|
||||
state = rememberPreviewState().copy(
|
||||
selectedMedia = selectedMedia,
|
||||
focusedMedia = selectedMedia.first(),
|
||||
editorStateMap = mutableMapOf(
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.mediasend.edit
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.models.media.Media
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
import org.signal.core.util.ContentTypeUtil
|
||||
import org.signal.core.util.isNotNullOrBlank
|
||||
import org.signal.mediasend.R
|
||||
|
||||
@Composable
|
||||
internal fun MediaEditSummaryPill(
|
||||
displayName: String?,
|
||||
selectedMedia: List<Media>,
|
||||
selectedPage: Int,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val selectedMediaText = rememberSelectedMediaText(selectedMedia, selectedPage)
|
||||
|
||||
val targetState = remember(displayName, selectedMediaText) {
|
||||
val hasDisplayName = displayName.isNotNullOrBlank()
|
||||
val hasSelectedMedia = selectedMediaText.isNotNullOrBlank()
|
||||
|
||||
when {
|
||||
hasDisplayName && hasSelectedMedia -> {
|
||||
MediaEditSummaryPillTargetState.MultiRow(
|
||||
row1 = displayName,
|
||||
row2 = selectedMediaText
|
||||
)
|
||||
}
|
||||
|
||||
hasDisplayName -> {
|
||||
MediaEditSummaryPillTargetState.SingleRow(
|
||||
text = displayName
|
||||
)
|
||||
}
|
||||
|
||||
hasSelectedMedia -> {
|
||||
MediaEditSummaryPillTargetState.SingleRow(
|
||||
text = selectedMediaText
|
||||
)
|
||||
}
|
||||
|
||||
else -> MediaEditSummaryPillTargetState.None
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
) {
|
||||
when (targetState) {
|
||||
is MediaEditSummaryPillTargetState.MultiRow -> MultiRowPill(targetState)
|
||||
MediaEditSummaryPillTargetState.None -> Unit
|
||||
is MediaEditSummaryPillTargetState.SingleRow -> SingleRowPill(targetState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SingleRowPill(singleRow: MediaEditSummaryPillTargetState.SingleRow) {
|
||||
Pill(
|
||||
modifier = Modifier.padding(
|
||||
horizontal = 12.dp,
|
||||
vertical = 4.dp
|
||||
)
|
||||
) {
|
||||
Text(text = singleRow.text, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MultiRowPill(multiRow: MediaEditSummaryPillTargetState.MultiRow) {
|
||||
Pill(
|
||||
modifier = Modifier.padding(
|
||||
horizontal = 16.dp,
|
||||
vertical = 4.dp
|
||||
)
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(text = multiRow.row1, style = MaterialTheme.typography.bodyMedium)
|
||||
Text(text = multiRow.row2, style = MaterialTheme.typography.labelSmall.copy(fontWeight = FontWeight(400)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Pill(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = SignalTheme.colors.colorSurface5,
|
||||
shape = CircleShape
|
||||
)
|
||||
.then(modifier)
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberSelectedMediaText(selectedMedia: List<Media>, selectedPage: Int): String {
|
||||
if (selectedMedia.isEmpty()) {
|
||||
return ""
|
||||
}
|
||||
|
||||
if (selectedMedia.size > 1) {
|
||||
return stringResource(R.string.MediaEditScreen__d_of_d, selectedPage + 1, selectedMedia.size)
|
||||
}
|
||||
|
||||
val media = selectedMedia.first()
|
||||
val contentType = media.contentType
|
||||
|
||||
val label = when {
|
||||
ContentTypeUtil.isGif(contentType) || media.isVideoGif -> R.plurals.MediaEditScreen__gif
|
||||
ContentTypeUtil.isImageType(contentType) -> R.plurals.MediaEditScreen__photo
|
||||
ContentTypeUtil.isVideoType(contentType) -> R.plurals.MediaEditScreen__video
|
||||
ContentTypeUtil.isDocumentType(contentType) -> R.plurals.MediaEditScreen__document
|
||||
else -> R.plurals.MediaEditScreen__item
|
||||
}
|
||||
|
||||
return pluralStringResource(label, selectedMedia.size, selectedMedia.size)
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun SingleRowPillPreview() {
|
||||
Previews.Preview {
|
||||
SingleRowPill(singleRow = MediaEditSummaryPillTargetState.SingleRow(text = "Single Row"))
|
||||
}
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun MultiRowPillPreview() {
|
||||
Previews.Preview {
|
||||
MultiRowPill(multiRow = MediaEditSummaryPillTargetState.MultiRow(row1 = "Top Row", row2 = "Bottom Row"))
|
||||
}
|
||||
}
|
||||
|
||||
private sealed interface MediaEditSummaryPillTargetState {
|
||||
data class SingleRow(val text: String) : MediaEditSummaryPillTargetState
|
||||
data class MultiRow(val row1: String, val row2: String) : MediaEditSummaryPillTargetState
|
||||
data object None : MediaEditSummaryPillTargetState
|
||||
}
|
||||
@@ -116,4 +116,32 @@
|
||||
<string name="LinkedDeviceScannedDialog__it_looks_like_youre_trying">It looks like you\'re trying to link a Signal device. Tap continue and then tap \"Link a New Device\" and scan the QR code again.</string>
|
||||
<!-- The label of a dialog asking the user if they would like to continue to the linked device settings screen. -->
|
||||
<string name="LinkedDeviceScannedDialog__device_link_dialog_continue">Continue</string>
|
||||
|
||||
<!-- Gif label displayed in a pill in the editor specifying the type of media being sent. -->
|
||||
<plurals name="MediaEditScreen__gif">
|
||||
<item quantity="one">%1$d Gif</item>
|
||||
<item quantity="other">%1$d Gifs</item>
|
||||
</plurals>
|
||||
<!-- Photo label displayed in a pill in the editor specifying the type of media being sent. -->
|
||||
<plurals name="MediaEditScreen__photo">
|
||||
<item quantity="one">%1$d Photo</item>
|
||||
<item quantity="other">%1$d Photos</item>
|
||||
</plurals>
|
||||
<!-- Video label displayed in a pill in the editor specifying the type of media being sent. -->
|
||||
<plurals name="MediaEditScreen__video">
|
||||
<item quantity="one">%1$d Video</item>
|
||||
<item quantity="other">%1$d Videos</item>
|
||||
</plurals>
|
||||
<!-- Document label displayed in a pill in the editor specifying the type of media being sent. -->
|
||||
<plurals name="MediaEditScreen__document">
|
||||
<item quantity="one">%1$d Document</item>
|
||||
<item quantity="other">%1$d Documents</item>
|
||||
</plurals>
|
||||
<!-- Item label displayed in a pill in the editor specifying the type of media being sent. -->
|
||||
<plurals name="MediaEditScreen__item">
|
||||
<item quantity="one">%1$d Item</item>
|
||||
<item quantity="other">%1$d Items</item>
|
||||
</plurals>
|
||||
<!-- x of y label displayed in a pill in the editor specifying the type of media being sent. First placeholder is selected page, second is total count. -->
|
||||
<string name="MediaEditScreen__d_of_d">%1$d of %2$d</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user