mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-08-05 21:07:49 +01:00
Add view-once affordance and indication to the V3 media edit screen.
This commit is contained in:
committed by
Alex Hart
parent
b9eeb2fe84
commit
e8f23a5015
+1
-1
@@ -1430,7 +1430,7 @@ public class ConversationListFragment extends MainFragment implements Conversati
|
||||
}
|
||||
|
||||
if (SignalStore.labs().getIncognito()) {
|
||||
items.add(new ActionItem(R.drawable.symbol_view_once_24, "Open Incognito (Labs)", () -> handleOpenIncognito(conversation)));
|
||||
items.add(new ActionItem(org.signal.core.ui.R.drawable.symbol_view_once_24, "Open Incognito (Labs)", () -> handleOpenIncognito(conversation)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -416,7 +416,7 @@ class MediaReviewFragment : Fragment(R.layout.v2_media_review_fragment), Schedul
|
||||
getString(R.string.MediaReviewFragment__photo_set_to_view_once)
|
||||
}
|
||||
|
||||
MediaReviewToastPopupWindow.show(controls, R.drawable.symbol_view_once_24, description)
|
||||
MediaReviewToastPopupWindow.show(controls, CoreUiR.drawable.symbol_view_once_24, description)
|
||||
}
|
||||
|
||||
private fun presentQualityToggleToast(state: MediaSelectionState) {
|
||||
|
||||
@@ -100,8 +100,7 @@ class MediaSendV3Activity :
|
||||
|
||||
supportFragmentManager.setFragmentResultListener(AddMessageDialogFragment.REQUEST_KEY, this) { _, bundle ->
|
||||
if (bundle.getBoolean(AddMessageDialogFragment.RESULT_INCREMENT_VIEW_ONCE_STATE)) {
|
||||
viewModel.setMessage(null)
|
||||
viewModel.incrementViewOnceState()
|
||||
viewModel.toggleViewOnce()
|
||||
} else {
|
||||
viewModel.setMessage(bundle.getCharSequence(AddMessageDialogFragment.RESULT_MESSAGE, null))
|
||||
}
|
||||
|
||||
+1
-2
@@ -15,7 +15,6 @@ import org.signal.core.util.getParcelableArrayListCompat
|
||||
import org.signal.mediasend.MediaRecipientId
|
||||
import org.signal.mediasend.MediaSendActivityContract
|
||||
import org.signal.mediasend.MediaSendRecipient
|
||||
import org.signal.mediasend.MediaSendState
|
||||
import org.signal.mediasend.MediaSendViewModel
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
|
||||
@@ -47,7 +46,7 @@ class MediaSendV3ForwardFragment : Fragment(R.layout.multiselect_forward_activit
|
||||
title = R.string.MediaReviewFragment__send_to,
|
||||
storySendRequirements = state.storySendRequirements.toAppSendRequirements(),
|
||||
isSearchEnabled = !state.isStory,
|
||||
isViewOnce = state.viewOnceToggleState == MediaSendState.ViewOnceToggleState.ONCE
|
||||
isViewOnce = state.isViewOnceEnabled
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -123,12 +123,12 @@ public class ViewOnceMessageView extends LinearLayout {
|
||||
} else {
|
||||
iconColor = unopenedIconColor;
|
||||
text.setText(R.string.RevealableMessageView_media);
|
||||
icon.setImageResource(R.drawable.symbol_view_once_24);
|
||||
icon.setImageResource(org.signal.core.ui.R.drawable.symbol_view_once_24);
|
||||
}
|
||||
} else if (ViewOnceUtil.isViewable(messageRecord)) {
|
||||
iconColor = unopenedIconColor;
|
||||
text.setText(getDescriptionId(messageRecord));
|
||||
icon.setImageResource(R.drawable.symbol_view_once_24);
|
||||
icon.setImageResource(org.signal.core.ui.R.drawable.symbol_view_once_24);
|
||||
} else if (networkInProgress(messageRecord)) {
|
||||
iconColor = unopenedIconColor;
|
||||
text.setText("");
|
||||
|
||||
@@ -92,6 +92,8 @@ enum class SignalIcons(private val icon: SignalIcon) : SignalIcon by icon {
|
||||
TransferDisplay(icon(R.drawable.symbol_transfer_display_48)),
|
||||
Trash(icon(R.drawable.symbol_trash_24)),
|
||||
Undo(icon(R.drawable.symbol_undo_24)),
|
||||
ViewOnce(icon(R.drawable.symbol_view_once_24)),
|
||||
ViewOnceInfinite(icon(R.drawable.symbol_view_once_infinite_24)),
|
||||
X(icon(R.drawable.symbol_x_24)),
|
||||
XCircleFill(icon(R.drawable.symbol_x_circle_fill_24))
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import org.signal.camera.CameraDependencies
|
||||
import org.signal.core.models.media.Media
|
||||
import org.signal.core.models.media.MediaFolder
|
||||
import org.signal.core.models.parcelers.NullableCharSequenceParceler
|
||||
import org.signal.core.util.ContentTypeUtil
|
||||
import org.signal.mediasend.edit.image.BrushWidths
|
||||
import org.signal.mediasend.edit.video.VideoTrimData
|
||||
import kotlin.time.Duration
|
||||
@@ -140,6 +141,19 @@ data class MediaSendState(
|
||||
val brushWidths: BrushWidths = MediaSendDependencies.mediaSendRepository.brushWidths
|
||||
) : Parcelable {
|
||||
|
||||
/**
|
||||
* View-once only makes sense for a single, non-document attachment that isn't headed for a story.
|
||||
*/
|
||||
val isViewOnceAvailable: Boolean
|
||||
get() = selectedMedia.size == 1 && !isStory && !ContentTypeUtil.isDocumentType(focusedMedia?.contentType)
|
||||
|
||||
/**
|
||||
* Whether the current selection will actually be sent as view-once. [viewOnceToggleState] is sticky, so it can
|
||||
* outlive the conditions that allowed it to be set, and must always be read alongside [isViewOnceAvailable].
|
||||
*/
|
||||
val isViewOnceEnabled: Boolean
|
||||
get() = isViewOnceAvailable && viewOnceToggleState == ViewOnceToggleState.ONCE
|
||||
|
||||
fun getOrCreateVideoTrimData(uri: Uri): VideoTrimData {
|
||||
return (editorStateMap[uri] as? EditorState.VideoTrim)?.videoTrimData ?: VideoTrimData()
|
||||
}
|
||||
|
||||
@@ -292,7 +292,7 @@ class MediaSendViewModel(
|
||||
HudCommand.ShowAddAMessageDialog(
|
||||
message = snapshot.message ?: "",
|
||||
startWithEmojiKeyboard = mediaEditScreenEvent.startWithEmojiKeyboard,
|
||||
isViewOnceAvailable = snapshot.selectedMedia.size == 1 && !snapshot.isStory && !ContentTypeUtil.isDocumentType(snapshot.focusedMedia?.contentType)
|
||||
isViewOnceAvailable = snapshot.isViewOnceAvailable
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -309,6 +309,10 @@ class MediaSendViewModel(
|
||||
setSentMediaQuality(state.value.sentMediaQuality.next())
|
||||
}
|
||||
|
||||
MediaEditScreenEvent.ToggleViewOnce -> {
|
||||
toggleViewOnce()
|
||||
}
|
||||
|
||||
MediaEditScreenEvent.SaveMedia -> {
|
||||
saveFocusedMediaToStorage()
|
||||
}
|
||||
@@ -969,14 +973,37 @@ class MediaSendViewModel(
|
||||
|
||||
//region View Once
|
||||
|
||||
fun incrementViewOnceState() {
|
||||
updateState { copy(viewOnceToggleState = viewOnceToggleState.next()) }
|
||||
fun isViewOnceEnabled(): Boolean {
|
||||
return internalState.value.isViewOnceEnabled
|
||||
}
|
||||
|
||||
fun isViewOnceEnabled(): Boolean {
|
||||
val snapshot = internalState.value
|
||||
return snapshot.selectedMedia.size == 1 &&
|
||||
snapshot.viewOnceToggleState == MediaSendState.ViewOnceToggleState.ONCE
|
||||
/**
|
||||
* Flips view-once. Turning it on drops any message the user had already typed, since a view-once send cannot carry
|
||||
* a body, and confirms the change with a snackbar.
|
||||
*/
|
||||
fun toggleViewOnce() {
|
||||
updateState { copy(viewOnceToggleState = viewOnceToggleState.next()) }
|
||||
|
||||
if (!internalState.value.isViewOnceEnabled) {
|
||||
return
|
||||
}
|
||||
|
||||
setMessage(null)
|
||||
|
||||
val focusedMedia = internalState.value.focusedMedia
|
||||
val isVideo = focusedMedia != null &&
|
||||
ContentTypeUtil.isVideoType(focusedMedia.contentType) &&
|
||||
!focusedMedia.isVideoGif
|
||||
|
||||
internalSnackbarEvents.trySend(
|
||||
SnackbarEvent(
|
||||
message = if (isVideo) {
|
||||
R.string.MediaSendViewModel__video_set_to_view_once
|
||||
} else {
|
||||
R.string.MediaSendViewModel__photo_set_to_view_once
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
//endregion
|
||||
|
||||
@@ -26,6 +26,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.DropdownMenus
|
||||
@@ -57,7 +58,9 @@ fun AddAMessageRow(
|
||||
onNextClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
canScheduleSend: Boolean = false
|
||||
canScheduleSend: Boolean = false,
|
||||
viewOnceAvailable: Boolean = false,
|
||||
viewOnce: Boolean = false
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
@@ -70,24 +73,55 @@ fun AddAMessageRow(
|
||||
.background(color = MaterialTheme.colorScheme.surfaceVariant, shape = RoundedCornerShape(percent = 50))
|
||||
.weight(1f)
|
||||
.heightIn(min = 40.dp)
|
||||
.clickable(enabled = enabled, onClickLabel = stringResource(R.string.AddAMessageRow__add_a_message), onClick = { onEvent(MediaEditScreenEvent.AddMessageClick()) }, role = Role.Button)
|
||||
.then(
|
||||
if (viewOnce) {
|
||||
// A view-once send cannot carry a body, so the row becomes a static label rather than an entry point.
|
||||
Modifier
|
||||
} else {
|
||||
Modifier.clickable(enabled = enabled, onClickLabel = stringResource(R.string.AddAMessageRow__add_a_message), onClick = { onEvent(MediaEditScreenEvent.AddMessageClick()) }, role = Role.Button)
|
||||
}
|
||||
)
|
||||
) {
|
||||
IconButtons.IconButton(
|
||||
enabled = enabled,
|
||||
onClick = { onEvent(MediaEditScreenEvent.AddMessageClick(startWithEmojiKeyboard = true)) }
|
||||
) {
|
||||
Icon(
|
||||
painter = SignalIcons.Emoji.painter,
|
||||
contentDescription = stringResource(R.string.AddAMessageRow__open_emoji_keyboard)
|
||||
if (viewOnce) {
|
||||
Text(
|
||||
text = stringResource(R.string.AddAMessageRow__view_once_media),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(horizontal = 16.dp)
|
||||
)
|
||||
} else {
|
||||
IconButtons.IconButton(
|
||||
enabled = enabled,
|
||||
onClick = { onEvent(MediaEditScreenEvent.AddMessageClick(startWithEmojiKeyboard = true)) }
|
||||
) {
|
||||
Icon(
|
||||
painter = SignalIcons.Emoji.painter,
|
||||
contentDescription = stringResource(R.string.AddAMessageRow__open_emoji_keyboard)
|
||||
)
|
||||
}
|
||||
|
||||
LocalAddAMessageRowTextField.current(
|
||||
message ?: stringResource(R.string.AddAMessageRow__message),
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.padding(end = if (viewOnceAvailable) 0.dp else 16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
LocalAddAMessageRowTextField.current(
|
||||
message ?: stringResource(R.string.AddAMessageRow__message),
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 16.dp)
|
||||
)
|
||||
if (viewOnceAvailable) {
|
||||
IconButtons.IconButton(
|
||||
enabled = enabled,
|
||||
onClick = { onEvent(MediaEditScreenEvent.ToggleViewOnce) }
|
||||
) {
|
||||
Icon(
|
||||
painter = if (viewOnce) SignalIcons.ViewOnce.painter else SignalIcons.ViewOnceInfinite.painter,
|
||||
contentDescription = stringResource(R.string.AddAMessageRow__toggle_view_once)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Box {
|
||||
@@ -134,3 +168,30 @@ private fun AddAMessageRowPreview() {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun AddAMessageRowViewOnceAvailablePreview() {
|
||||
Previews.Preview {
|
||||
AddAMessageRow(
|
||||
message = null,
|
||||
onEvent = {},
|
||||
onNextClick = {},
|
||||
viewOnceAvailable = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun AddAMessageRowViewOncePreview() {
|
||||
Previews.Preview {
|
||||
AddAMessageRow(
|
||||
message = null,
|
||||
onEvent = {},
|
||||
onNextClick = {},
|
||||
viewOnceAvailable = true,
|
||||
viewOnce = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,6 +317,8 @@ internal fun MediaEditScreen(
|
||||
AddAMessageRow(
|
||||
enabled = !isInteracting && !state.isSending,
|
||||
canScheduleSend = !state.isStory,
|
||||
viewOnceAvailable = state.isViewOnceAvailable,
|
||||
viewOnce = state.isViewOnceEnabled,
|
||||
message = state.message,
|
||||
onEvent = onEvent,
|
||||
onNextClick = { onEvent(MediaEditScreenEvent.NextClick) },
|
||||
|
||||
@@ -20,6 +20,7 @@ sealed interface MediaEditScreenEvent {
|
||||
data object NavigateBack : MediaEditScreenEvent
|
||||
data object NavigateToGallery : MediaEditScreenEvent
|
||||
data object ToggleMediaQuality : MediaEditScreenEvent
|
||||
data object ToggleViewOnce : MediaEditScreenEvent
|
||||
data class BrushWidthChanged(val tool: BrushTool, val fraction: Float) : MediaEditScreenEvent
|
||||
data object SaveMedia : MediaEditScreenEvent
|
||||
data class VideoTrimChanged(val videoTrimData: VideoTrimData, val editingComplete: Boolean) : MediaEditScreenEvent
|
||||
|
||||
@@ -106,8 +106,11 @@ internal fun MediaEditorToolbarSharedButtons(
|
||||
)
|
||||
}
|
||||
|
||||
MediaEditorToolbarButton(
|
||||
imageVector = SignalIcons.Plus.imageVector, // TODO [alex] - wrong art asset
|
||||
onClick = { onEvent(MediaEditScreenEvent.NavigateToGallery) }
|
||||
)
|
||||
// Adding a second attachment would silently drop view-once, so the entry point goes away while it is on.
|
||||
if (!state.isViewOnceEnabled) {
|
||||
MediaEditorToolbarButton(
|
||||
imageVector = SignalIcons.Plus.imageVector, // TODO [alex] - wrong art asset
|
||||
onClick = { onEvent(MediaEditScreenEvent.NavigateToGallery) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,10 @@
|
||||
<string name="AddAMessageRow__schedule_send">Schedule send</string>
|
||||
<!-- Menu option that opens a picker for choosing the exact date and time to send at. -->
|
||||
<string name="ScheduleSendMenu__pick_date_and_time">Pick Date & Time</string>
|
||||
<!-- Label shown in place of the add-a-message row when the media has been set to view once, indicating that no message can be attached. -->
|
||||
<string name="AddAMessageRow__view_once_media">View once media</string>
|
||||
<!-- Accessibility label for the button at the end of the add-a-message row that turns view-once mode on and off. -->
|
||||
<string name="AddAMessageRow__toggle_view_once">Toggle view once</string>
|
||||
<!-- Setting option that can be selected to default media to be sent as high quality by default -->
|
||||
<string name="SentMediaQuality__high">High</string>
|
||||
<!-- Setting option that can be selected to default media to be sent as standard quality by default -->
|
||||
@@ -147,6 +151,10 @@
|
||||
<string name="MediaSendViewModel__error_saving_media">Error while saving attachment to storage!</string>
|
||||
<!-- Displayed when the media being edited cannot be saved because the storage permission was denied -->
|
||||
<string name="MediaSendViewModel__unable_to_save_without_storage_permission">Unable to save to external storage without permissions</string>
|
||||
<!-- Small notification presented to the user when they set their photo to view-once mode -->
|
||||
<string name="MediaSendViewModel__photo_set_to_view_once">Photo set to view once</string>
|
||||
<!-- Small notification presented to the user when they set their video to view-once mode -->
|
||||
<string name="MediaSendViewModel__video_set_to_view_once">Video set to view once</string>
|
||||
<!-- Message shown when saving the media being edited requires the storage permission, but that permission has been permanently denied by the user. -->
|
||||
<string name="MediaSendViewModel__signal_needs_the_storage_permission">Signal needs the Storage permission in order to save to external storage, but it has been permanently denied. Please continue to app settings, select \"Permissions\", and enable \"Storage\".</string>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user