Apply chat color tint and send icon to the media-send edit next button.

This commit is contained in:
Greyson Parrelli
2026-07-31 19:00:28 +00:00
committed by Alex Hart
parent 476ee40d5f
commit c3277fd2b1
7 changed files with 99 additions and 12 deletions
@@ -19,6 +19,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
import androidx.fragment.compose.AndroidFragment
@@ -27,6 +28,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.signal.core.ui.WindowBreakpoint
import org.signal.core.ui.compose.LocalChatColorProvider
import org.signal.core.ui.compose.LocalDisplayNameProvider
import org.signal.core.ui.getWindowBreakpoint
import org.signal.mediasend.HudCommand
@@ -145,6 +147,11 @@ class MediaSendV3Activity :
getDisplayName(context)
}
}
},
LocalChatColorProvider provides { id ->
rememberRecipientField(RecipientId.from(id)) {
Color(chatColors.asSingleColor())
}
}
) {
MediaSendScreen(
@@ -0,0 +1,27 @@
/*
* 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
import androidx.compose.ui.graphics.Color
/**
* Intended to be bound at the app level to allow for the production of an observable single-color representation of a
* recipient's chat color. Raw types are utilized here since RecipientId and ChatColors are both currently localized to
* the app module and are non-trivial to pick up and move.
*
* Gradient chat colors collapse to a single color, matching what the legacy send button did.
*/
val LocalChatColorProvider = compositionLocalOf<@Composable (Long) -> State<Color>> {
{ remember(it) { mutableStateOf(UnboundChatColor) } }
}
/** Stand-in used in previews and anywhere else the provider is left unbound. Matches the default (ultramarine) bubble. */
private val UnboundChatColor = Color(0xFF315FF4)
@@ -82,6 +82,7 @@ enum class SignalIcons(private val icon: SignalIcon) : SignalIcon by icon {
Redo(icon(R.drawable.symbol_redo_24)),
Save(icon(R.drawable.symbol_save_android_24)),
Search(icon(R.drawable.symbol_search_24)),
SendFill(icon(R.drawable.symbol_send_fill_24)),
Settings(icon(R.drawable.symbol_settings_android_24)),
Share(icon(R.drawable.symbol_share_android_24)),
SignalBackupsDisplay(icon(R.drawable.symbol_signal_backups_display_48)),
@@ -0,0 +1,9 @@
<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="M5.54 3.06c-0.83-0.4-1.79 0.2-1.79 1.13V8.9c0 0.58 0.4 1.08 0.96 1.21L12.59 12l-7.88 1.88C4.15 14 3.75 14.5 3.75 15.09v4.72c0 0.92 0.96 1.53 1.79 1.13l16.5-7.81c0.95-0.45 0.95-1.8 0-2.26L5.53 3.06Z"/>
</vector>
@@ -12,8 +12,6 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
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.MaterialTheme
@@ -23,6 +21,7 @@ import androidx.compose.runtime.compositionLocalOf
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.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
@@ -33,9 +32,13 @@ import org.signal.core.ui.compose.DropdownMenus
import org.signal.core.ui.compose.IconButtons
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalIcons
import org.signal.core.ui.compose.theme.SignalTheme
import org.signal.mediasend.R
import org.signal.mediasend.test.TestTags
/** Mirrors the legacy send button's disabled tint, which has no equivalent in the core-ui color scheme. */
private val DisabledNextButtonColor = Color(0xFF777777)
/**
* Because we need to be able to support stuff like mentions, styled text, and custom emoji, we need to allow
* the users of this feature to inject their own text-field.
@@ -51,6 +54,11 @@ val LocalAddAMessageRowTextField = compositionLocalOf<@Composable (CharSequence,
}
}
/**
* @param recipientChatColor The chat color of the single recipient this media is headed to, or null when the destination
* is still to be chosen. Non-null makes the trailing button a chat-color-tinted send button rather than a themed
* "next" arrow.
*/
@Composable
fun AddAMessageRow(
message: CharSequence?,
@@ -60,7 +68,8 @@ fun AddAMessageRow(
enabled: Boolean = true,
canScheduleSend: Boolean = false,
viewOnceAvailable: Boolean = false,
viewOnce: Boolean = false
viewOnce: Boolean = false,
recipientChatColor: Color? = null
) {
Row(
horizontalArrangement = Arrangement.Center,
@@ -132,20 +141,19 @@ fun AddAMessageRow(
onClick = onNextClick,
onLongClick = if (canScheduleSend) scheduleSendMenuController::show else null,
onLongClickLabel = stringResource(R.string.AddAMessageRow__schedule_send),
colors = IconButtons.iconButtonColors(
containerColor = recipientChatColor ?: MaterialTheme.colorScheme.onSecondaryContainer,
contentColor = if (recipientChatColor != null) SignalTheme.colors.colorOnCustom else MaterialTheme.colorScheme.secondaryContainer,
disabledContainerColor = DisabledNextButtonColor,
disabledContentColor = MaterialTheme.colorScheme.secondaryContainer
),
modifier = Modifier
.testTag(TestTags.ADD_A_MESSAGE_NEXT_BUTTON)
.padding(start = 12.dp)
.background(
color = MaterialTheme.colorScheme.primaryContainer,
shape = CircleShape
)
) {
Icon(
painter = SignalIcons.ArrowEnd.painter,
contentDescription = stringResource(R.string.AddAMessageRow__next),
modifier = Modifier
.size(40.dp)
.padding(8.dp)
painter = if (recipientChatColor != null) SignalIcons.SendFill.painter else SignalIcons.ArrowEnd.painter,
contentDescription = stringResource(if (recipientChatColor != null) R.string.AddAMessageRow__send else R.string.AddAMessageRow__next)
)
}
@@ -195,3 +203,30 @@ private fun AddAMessageRowViewOncePreview() {
)
}
}
@DayNightPreviews
@Composable
private fun AddAMessageRowKnownRecipientPreview() {
Previews.Preview {
AddAMessageRow(
message = null,
onEvent = {},
onNextClick = {},
recipientChatColor = Color(0xFF3B7845)
)
}
}
@DayNightPreviews
@Composable
private fun AddAMessageRowDisabledPreview() {
Previews.Preview {
AddAMessageRow(
message = null,
onEvent = {},
onNextClick = {},
enabled = false,
recipientChatColor = Color(0xFF3B7845)
)
}
}
@@ -42,6 +42,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.LocalChatColorProvider
import org.signal.core.ui.compose.LocalDisplayNameProvider
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.rememberWindowBreakpoint
@@ -231,6 +232,10 @@ internal fun MediaEditScreen(
val isTextEditing = imageController?.textEditingElement != null
// Null whenever the destination is still to be chosen, which is what turns the trailing button back into a
// "next" arrow.
val recipientChatColor: Color? = state.recipientId?.let { LocalChatColorProvider.current(it.id).value }
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
@@ -320,6 +325,7 @@ internal fun MediaEditScreen(
viewOnceAvailable = state.isViewOnceAvailable,
viewOnce = state.isViewOnceEnabled,
message = state.message,
recipientChatColor = recipientChatColor,
onEvent = onEvent,
onNextClick = { onEvent(MediaEditScreenEvent.NextClick) },
modifier = Modifier
@@ -24,6 +24,8 @@
<string name="AddAMessageRow__message">Message</string>
<!-- Accessibility description for the button that advances to the next step in the media send flow. -->
<string name="AddAMessageRow__next">Next</string>
<!-- Accessibility description for the button that sends the media to the already-known recipient. -->
<string name="AddAMessageRow__send">Send</string>
<!-- Accessibility description for long-pressing the send button to schedule the send for later. -->
<string name="AddAMessageRow__schedule_send">Schedule send</string>
<!-- Menu option that opens a picker for choosing the exact date and time to send at. -->