diff --git a/app/src/main/java/org/thoughtcrime/securesms/MainNavigator.java b/app/src/main/java/org/thoughtcrime/securesms/MainNavigator.java index 5a0f1b76a1..1008c63c8c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MainNavigator.java +++ b/app/src/main/java/org/thoughtcrime/securesms/MainNavigator.java @@ -41,9 +41,14 @@ public class MainNavigator { } public void goToConversation(@NonNull RecipientId recipientId, long threadId, int distributionType, int startingPosition) { + goToConversation(recipientId, threadId, distributionType, startingPosition, false); + } + + public void goToConversation(@NonNull RecipientId recipientId, long threadId, int distributionType, int startingPosition, boolean incognito) { Disposable disposable = ConversationIntents.createBuilder(activity, recipientId, threadId) .map(builder -> builder.withDistributionType(distributionType) .withStartingPosition(startingPosition) + .asIncognito(incognito) .toConversationArgs()) .subscribe(args -> viewModel.goTo(new MainNavigationDetailLocation.Chats.Conversation(args))); diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsEvents.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsEvents.kt index ef8e2e1bdd..d0791fbe71 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsEvents.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsEvents.kt @@ -8,4 +8,5 @@ package org.thoughtcrime.securesms.components.settings.app.labs sealed interface LabsSettingsEvents { data class ToggleIndividualChatPlaintextExport(val enabled: Boolean) : LabsSettingsEvents data class ToggleStoryArchive(val enabled: Boolean) : LabsSettingsEvents + data class ToggleIncognito(val enabled: Boolean) : LabsSettingsEvents } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsFragment.kt index da0d423f41..1849fec00c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsFragment.kt @@ -106,6 +106,15 @@ private fun LabsSettingsContent( onCheckChanged = { onEvent(LabsSettingsEvents.ToggleStoryArchive(it)) } ) } + + item { + Rows.ToggleRow( + checked = state.incognito, + text = "Incognito Mode", + label = "Adds an option to long-press a conversation to open it in incognito mode. Messages will not be marked as read and no read receipts will be sent.", + onCheckChanged = { onEvent(LabsSettingsEvents.ToggleIncognito(it)) } + ) + } } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsState.kt index e7b7c4a72f..f89b2e3a0c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsState.kt @@ -10,5 +10,6 @@ import androidx.compose.runtime.Immutable @Immutable data class LabsSettingsState( val individualChatPlaintextExport: Boolean = false, - val storyArchive: Boolean = false + val storyArchive: Boolean = false, + val incognito: Boolean = false ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsViewModel.kt index 6380264dad..480806a5eb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsViewModel.kt @@ -25,13 +25,18 @@ class LabsSettingsViewModel : ViewModel() { SignalStore.labs.storyArchive = event.enabled _state.value = _state.value.copy(storyArchive = event.enabled) } + is LabsSettingsEvents.ToggleIncognito -> { + SignalStore.labs.incognito = event.enabled + _state.value = _state.value.copy(incognito = event.enabled) + } } } private fun loadState(): LabsSettingsState { return LabsSettingsState( individualChatPlaintextExport = SignalStore.labs.individualChatPlaintextExport, - storyArchive = SignalStore.labs.storyArchive + storyArchive = SignalStore.labs.storyArchive, + incognito = SignalStore.labs.incognito ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationArgs.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationArgs.kt index fe3d526e5f..28594e6621 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationArgs.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationArgs.kt @@ -37,7 +37,8 @@ data class ConversationArgs( val isWithSearchOpen: Boolean, val giftBadge: Badge?, val shareDataTimestamp: Long, - val conversationScreenType: ConversationScreenType + val conversationScreenType: ConversationScreenType, + val isIncognito: Boolean = false ) : Parcelable { @IgnoredOnParcel val draftMediaType: SlideFactory.MediaType? = SlideFactory.MediaType.from(draftContentType) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationIntents.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationIntents.java index 3fe88ba09a..60e9cfdc95 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationIntents.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationIntents.java @@ -47,6 +47,7 @@ public class ConversationIntents { private static final String EXTRA_GIFT_BADGE = "gift_badge"; private static final String EXTRA_SHARE_DATA_TIMESTAMP = "share_data_timestamp"; private static final String EXTRA_CONVERSATION_TYPE = "conversation_type"; + private static final String EXTRA_INCOGNITO = "incognito"; private static final String INTENT_DATA = "intent_data"; private static final String INTENT_TYPE = "intent_type"; @@ -152,7 +153,8 @@ public class ConversationIntents { false, null, -1L, - ConversationScreenType.BUBBLE); + ConversationScreenType.BUBBLE, + false); } return new ConversationArgs(RecipientId.from(Objects.requireNonNull(arguments.getString(EXTRA_RECIPIENT))), @@ -169,7 +171,8 @@ public class ConversationIntents { arguments.getBoolean(EXTRA_WITH_SEARCH_OPEN, false), arguments.getParcelable(EXTRA_GIFT_BADGE), arguments.getLong(EXTRA_SHARE_DATA_TIMESTAMP, -1L), - ConversationScreenType.from(arguments.getInt(EXTRA_CONVERSATION_TYPE, 0))); + ConversationScreenType.from(arguments.getInt(EXTRA_CONVERSATION_TYPE, 0)), + arguments.getBoolean(EXTRA_INCOGNITO, false)); } public final static class Builder { @@ -191,6 +194,7 @@ public class ConversationIntents { private boolean withSearchOpen; private Badge giftBadge; private long shareDataTimestamp = -1L; + private boolean incognito; private Builder(@NonNull Context context, @NonNull Class conversationActivityClass, @@ -218,6 +222,7 @@ public class ConversationIntents { withSearchOpen = args.isWithSearchOpen(); giftBadge = args.getGiftBadge(); shareDataTimestamp = args.getShareDataTimestamp(); + incognito = args.isIncognito(); return this; } @@ -282,6 +287,11 @@ public class ConversationIntents { return this; } + public @NonNull Builder asIncognito(boolean incognito) { + this.incognito = incognito; + return this; + } + public @NonNull ConversationArgs toConversationArgs() { return new ConversationArgs( recipientId, @@ -298,7 +308,8 @@ public class ConversationIntents { withSearchOpen, giftBadge, shareDataTimestamp, - conversationScreenType + conversationScreenType, + incognito ); } @@ -329,6 +340,7 @@ public class ConversationIntents { intent.putExtra(EXTRA_GIFT_BADGE, giftBadge); intent.putExtra(EXTRA_SHARE_DATA_TIMESTAMP, shareDataTimestamp); intent.putExtra(EXTRA_CONVERSATION_TYPE, conversationScreenType.code); + intent.putExtra(EXTRA_INCOGNITO, incognito); if (draftText != null) { intent.putExtra(EXTRA_TEXT, draftText); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/MarkReadHelper.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/MarkReadHelper.java index 5686e261cf..ebdb231e45 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/MarkReadHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/MarkReadHelper.java @@ -42,18 +42,24 @@ public class MarkReadHelper { private final ConversationId conversationId; private final Context context; private final LifecycleOwner lifecycleOwner; + private final boolean incognito; private final Debouncer debouncer = new Debouncer(DEBOUNCE_TIMEOUT); private long latestTimestamp; private boolean ignoreViewReveals = false; public MarkReadHelper(@NonNull ConversationId conversationId, @NonNull Context context, @NonNull LifecycleOwner lifecycleOwner) { + this(conversationId, context, lifecycleOwner, false); + } + + public MarkReadHelper(@NonNull ConversationId conversationId, @NonNull Context context, @NonNull LifecycleOwner lifecycleOwner, boolean incognito) { this.conversationId = conversationId; this.context = context.getApplicationContext(); this.lifecycleOwner = lifecycleOwner; + this.incognito = incognito; } public void onViewsRevealed(long timestamp) { - if (timestamp <= latestTimestamp || lifecycleOwner.getLifecycle().getCurrentState() != Lifecycle.State.RESUMED || ignoreViewReveals) { + if (incognito || timestamp <= latestTimestamp || lifecycleOwner.getLifecycle().getCurrentState() != Lifecycle.State.RESUMED || ignoreViewReveals) { return; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt index f54b94d5d7..4d42def3b1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt @@ -659,7 +659,7 @@ class ConversationFragment : FullscreenHelper(requireActivity()).showSystemUI() } - markReadHelper = MarkReadHelper(ConversationId.forConversation(args.threadId), requireContext(), viewLifecycleOwner) + markReadHelper = MarkReadHelper(ConversationId.forConversation(args.threadId), requireContext(), viewLifecycleOwner, args.isIncognito) markReadHelper.ignoreViewReveals() attachmentManager = AttachmentManager(requireContext(), requireView(), AttachmentManagerListener()) @@ -670,7 +670,8 @@ class ConversationFragment : requireActivity(), binding.toolbarBackground, viewModel::wallpaperSnapshot, - viewLifecycleOwner + viewLifecycleOwner, + incognito = args.isIncognito ) conversationToolbarOnScrollHelper.attach(binding.conversationItemRecycler) presentWallpaper(args.wallpaper) @@ -1482,6 +1483,7 @@ class ConversationFragment : var inputDisabled = true when { inputReadyState.isClientExpired || inputReadyState.isUnauthorized -> disabledInputView.showAsExpiredOrUnauthorized(inputReadyState.isClientExpired, inputReadyState.isUnauthorized) + args.isIncognito -> disabledInputView.showAsIncognito() !inputReadyState.messageRequestState.isAccepted -> disabledInputView.showAsMessageRequest(inputReadyState.conversationRecipient, inputReadyState.messageRequestState) inputReadyState.isActiveGroup == false -> disabledInputView.showAsNoLongerAMember() inputReadyState.isRequestingMember == true -> disabledInputView.showAsRequestingMember() diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationToolbarOnScrollHelper.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationToolbarOnScrollHelper.kt index 700ea24dd3..13f22a1db0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationToolbarOnScrollHelper.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationToolbarOnScrollHelper.kt @@ -16,7 +16,8 @@ class ConversationToolbarOnScrollHelper( activity: FragmentActivity, toolbarBackground: View, private val wallpaperProvider: () -> ChatWallpaper?, - lifecycleOwner: LifecycleOwner + lifecycleOwner: LifecycleOwner, + private val incognito: Boolean = false ) : Material3OnScrollHelper( activity = activity, views = listOf(toolbarBackground), @@ -24,10 +25,10 @@ class ConversationToolbarOnScrollHelper( setStatusBarColor = {} ) { override val activeColorSet: ColorSet - get() = ColorSet(getActiveToolbarColor(wallpaperProvider() != null)) + get() = if (incognito) ColorSet(R.color.conversation_toolbar_color_incognito) else ColorSet(getActiveToolbarColor(wallpaperProvider() != null)) override val inactiveColorSet: ColorSet - get() = ColorSet(getInactiveToolbarColor(wallpaperProvider() != null)) + get() = if (incognito) ColorSet(R.color.conversation_toolbar_color_incognito) else ColorSet(getInactiveToolbarColor(wallpaperProvider() != null)) @ColorRes private fun getActiveToolbarColor(hasWallpaper: Boolean): Int { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/DisabledInputView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/DisabledInputView.kt index a89026f995..459c300c4d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/DisabledInputView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/DisabledInputView.kt @@ -48,6 +48,7 @@ class DisabledInputView @JvmOverloads constructor( private var announcementGroupOnly: TextView? = null private var inviteToSignal: View? = null private var releaseNoteChannel: View? = null + private var incognitoView: View? = null private var currentView: View? = null @@ -76,6 +77,13 @@ class DisabledInputView @JvmOverloads constructor( ) } + fun showAsIncognito() { + incognitoView = show( + existingView = incognitoView, + create = { inflater.inflate(R.layout.conversation_incognito_mode, this, false) } + ) + } + fun showAsMessageRequest(recipient: Recipient, messageRequestState: MessageRequestState) { messageRequestView = show( existingView = messageRequestView, @@ -210,6 +218,7 @@ class DisabledInputView @JvmOverloads constructor( noLongerAMember = null requestingGroup = null announcementGroupOnly = null + incognitoView = null } private fun show(existingView: V?, create: () -> V, bind: V.() -> Unit = {}): V { 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 8357361de3..3725e08ef1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java @@ -1234,6 +1234,22 @@ public class ConversationListFragment extends MainFragment implements Conversati }); } + private void handleOpenIncognito(@NonNull Conversation conversation) { + long threadId = conversation.getThreadRecord().getThreadId(); + Recipient recipient = conversation.getThreadRecord().getRecipient(); + int distributionType = conversation.getThreadRecord().getDistributionType(); + + SimpleTask.run(getLifecycle(), () -> { + ChatWallpaper wallpaper = recipient.resolve().getWallpaper(); + if (wallpaper != null && !wallpaper.prefetch(requireContext(), 250)) { + Log.w(TAG, "Failed to prefetch wallpaper."); + } + return null; + }, (nothing) -> { + getNavigator().goToConversation(recipient.getId(), threadId, distributionType, -1, true); + }); + } + private void startActionModeIfNotActive() { if (!mainToolbarViewModel.isInActionMode()) { startActionMode(); @@ -1327,6 +1343,10 @@ public class ConversationListFragment extends MainFragment implements Conversati } else { items.add(new ActionItem(R.drawable.symbol_bell_slash_24, getResources().getString(R.string.ConversationListFragment_mute), () -> handleMute(Collections.singleton(conversation)))); } + + if (SignalStore.labs().getIncognito()) { + items.add(new ActionItem(R.drawable.symbol_view_once_24, "Open Incognito", () -> handleOpenIncognito(conversation))); + } } if (!isFromSearch) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/LabsValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/LabsValues.kt index fafd0cfdca..49870d2588 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/LabsValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/LabsValues.kt @@ -6,6 +6,7 @@ class LabsValues internal constructor(store: KeyValueStore) : SignalStoreValues( companion object { const val INDIVIDUAL_CHAT_PLAINTEXT_EXPORT: String = "labs.individual_chat_plaintext_export" const val STORY_ARCHIVE: String = "labs.story_archive" + const val INCOGNITO: String = "labs.incognito" } public override fun onFirstEverAppLaunch() = Unit @@ -16,6 +17,8 @@ class LabsValues internal constructor(store: KeyValueStore) : SignalStoreValues( var storyArchive by booleanValue(STORY_ARCHIVE, true).falseForExternalUsers() + var incognito by booleanValue(INCOGNITO, true).falseForExternalUsers() + private fun SignalStoreValueDelegate.falseForExternalUsers(): SignalStoreValueDelegate { return this.map { actualValue -> RemoteConfig.internalUser && actualValue } } diff --git a/app/src/main/res/drawable/incognito_pill_background.xml b/app/src/main/res/drawable/incognito_pill_background.xml new file mode 100644 index 0000000000..127b1c68e2 --- /dev/null +++ b/app/src/main/res/drawable/incognito_pill_background.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/layout/conversation_incognito_mode.xml b/app/src/main/res/layout/conversation_incognito_mode.xml new file mode 100644 index 0000000000..6d0093b338 --- /dev/null +++ b/app/src/main/res/layout/conversation_incognito_mode.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/app/src/main/res/values-night/dark_colors.xml b/app/src/main/res/values-night/dark_colors.xml index 22d4ef6b02..5210c77d16 100644 --- a/app/src/main/res/values-night/dark_colors.xml +++ b/app/src/main/res/values-night/dark_colors.xml @@ -5,6 +5,7 @@ @color/signal_colorSurface @color/signal_colorTransparentInverse5 @color/signal_colorTransparentInverse5 + #4A2D73 @color/core_ultramarine_light @color/core_white diff --git a/app/src/main/res/values/light_colors.xml b/app/src/main/res/values/light_colors.xml index 7fb8c14c03..2eb00a503c 100644 --- a/app/src/main/res/values/light_colors.xml +++ b/app/src/main/res/values/light_colors.xml @@ -3,6 +3,7 @@ @color/signal_colorSurface @color/signal_colorTransparent5 @color/signal_colorTransparent5 + #C4A8E0 @color/core_ultramarine diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fbda6b0a9a..3359d339db 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -518,6 +518,7 @@ Attachment exceeds size limits for the type of message you\'re sending. Unable to record audio! You can\'t send messages to this group because you\'re no longer a member. + Incognito mode Only %1$s can send messages. admins Message an admin