diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java
index a28b727ba4..be8d41d662 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java
@@ -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)));
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewFragment.kt
index f92b2e15b7..86d3fa2405 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewFragment.kt
@@ -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) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v3/MediaSendV3Activity.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v3/MediaSendV3Activity.kt
index 3aa6cbe251..7a9cfc39ad 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v3/MediaSendV3Activity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v3/MediaSendV3Activity.kt
@@ -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))
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v3/MediaSendV3ForwardFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v3/MediaSendV3ForwardFragment.kt
index c00199aadc..c559a9664d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v3/MediaSendV3ForwardFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v3/MediaSendV3ForwardFragment.kt
@@ -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
)
)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/revealable/ViewOnceMessageView.java b/app/src/main/java/org/thoughtcrime/securesms/revealable/ViewOnceMessageView.java
index 234b4d2f34..00c6ce8895 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/revealable/ViewOnceMessageView.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/revealable/ViewOnceMessageView.java
@@ -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("");
diff --git a/core/ui/src/main/java/org/signal/core/ui/compose/SignalIcons.kt b/core/ui/src/main/java/org/signal/core/ui/compose/SignalIcons.kt
index 6a1416fb37..0380a7de05 100644
--- a/core/ui/src/main/java/org/signal/core/ui/compose/SignalIcons.kt
+++ b/core/ui/src/main/java/org/signal/core/ui/compose/SignalIcons.kt
@@ -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))
}
diff --git a/app/src/main/res/drawable/symbol_view_once_24.xml b/core/ui/src/main/res/drawable/symbol_view_once_24.xml
similarity index 100%
rename from app/src/main/res/drawable/symbol_view_once_24.xml
rename to core/ui/src/main/res/drawable/symbol_view_once_24.xml
diff --git a/app/src/main/res/drawable/symbol_view_once_infinite_24.xml b/core/ui/src/main/res/drawable/symbol_view_once_infinite_24.xml
similarity index 100%
rename from app/src/main/res/drawable/symbol_view_once_infinite_24.xml
rename to core/ui/src/main/res/drawable/symbol_view_once_infinite_24.xml
diff --git a/feature/media-send/src/main/java/org/signal/mediasend/MediaSendState.kt b/feature/media-send/src/main/java/org/signal/mediasend/MediaSendState.kt
index 9119d78ac4..f2c81400e0 100644
--- a/feature/media-send/src/main/java/org/signal/mediasend/MediaSendState.kt
+++ b/feature/media-send/src/main/java/org/signal/mediasend/MediaSendState.kt
@@ -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()
}
diff --git a/feature/media-send/src/main/java/org/signal/mediasend/MediaSendViewModel.kt b/feature/media-send/src/main/java/org/signal/mediasend/MediaSendViewModel.kt
index 59a9863409..9acb9dc436 100644
--- a/feature/media-send/src/main/java/org/signal/mediasend/MediaSendViewModel.kt
+++ b/feature/media-send/src/main/java/org/signal/mediasend/MediaSendViewModel.kt
@@ -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
diff --git a/feature/media-send/src/main/java/org/signal/mediasend/edit/AddAMessageRow.kt b/feature/media-send/src/main/java/org/signal/mediasend/edit/AddAMessageRow.kt
index a1741fe003..f17d5529e3 100644
--- a/feature/media-send/src/main/java/org/signal/mediasend/edit/AddAMessageRow.kt
+++ b/feature/media-send/src/main/java/org/signal/mediasend/edit/AddAMessageRow.kt
@@ -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
+ )
+ }
+}
diff --git a/feature/media-send/src/main/java/org/signal/mediasend/edit/MediaEditScreen.kt b/feature/media-send/src/main/java/org/signal/mediasend/edit/MediaEditScreen.kt
index 036481d3d5..682381f8ef 100644
--- a/feature/media-send/src/main/java/org/signal/mediasend/edit/MediaEditScreen.kt
+++ b/feature/media-send/src/main/java/org/signal/mediasend/edit/MediaEditScreen.kt
@@ -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) },
diff --git a/feature/media-send/src/main/java/org/signal/mediasend/edit/MediaEditScreenEvent.kt b/feature/media-send/src/main/java/org/signal/mediasend/edit/MediaEditScreenEvent.kt
index cf1f994261..fe4b64d3d4 100644
--- a/feature/media-send/src/main/java/org/signal/mediasend/edit/MediaEditScreenEvent.kt
+++ b/feature/media-send/src/main/java/org/signal/mediasend/edit/MediaEditScreenEvent.kt
@@ -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
diff --git a/feature/media-send/src/main/java/org/signal/mediasend/edit/MediaEditorToolbar.kt b/feature/media-send/src/main/java/org/signal/mediasend/edit/MediaEditorToolbar.kt
index 5deaff46ae..c504260282 100644
--- a/feature/media-send/src/main/java/org/signal/mediasend/edit/MediaEditorToolbar.kt
+++ b/feature/media-send/src/main/java/org/signal/mediasend/edit/MediaEditorToolbar.kt
@@ -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) }
+ )
+ }
}
diff --git a/feature/media-send/src/main/res/values/strings.xml b/feature/media-send/src/main/res/values/strings.xml
index 6af09fa7fb..9b22359f90 100644
--- a/feature/media-send/src/main/res/values/strings.xml
+++ b/feature/media-send/src/main/res/values/strings.xml
@@ -28,6 +28,10 @@
Schedule send
Pick Date & Time
+
+ View once media
+
+ Toggle view once
High
@@ -147,6 +151,10 @@
Error while saving attachment to storage!
Unable to save to external storage without permissions
+
+ Photo set to view once
+
+ Video set to view once
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\".