mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-02 16:32:57 +01:00
Add ability to open a chat incognito.
This commit is contained in:
committed by
Michelle Tang
parent
db5cced91b
commit
b62b5ea8ef
@@ -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)));
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<? extends Activity> 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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 <V : View> show(existingView: V?, create: () -> V, bind: V.() -> Unit = {}): V {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<Boolean>.falseForExternalUsers(): SignalStoreValueDelegate<Boolean> {
|
||||
return this.map { actualValue -> RemoteConfig.internalUser && actualValue }
|
||||
}
|
||||
|
||||
6
app/src/main/res/drawable/incognito_pill_background.xml
Normal file
6
app/src/main/res/drawable/incognito_pill_background.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/conversation_toolbar_color_incognito" />
|
||||
<corners android:radius="16dp" />
|
||||
</shape>
|
||||
22
app/src/main/res/layout/conversation_incognito_mode.xml
Normal file
22
app/src/main/res/layout/conversation_incognito_mode.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:viewBindingIgnore="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="12dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:background="@drawable/incognito_pill_background"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:text="@string/DisabledInputView__incognito_mode"
|
||||
android:textColor="@color/signal_colorOnSurface"
|
||||
android:textSize="14sp" />
|
||||
|
||||
</FrameLayout>
|
||||
@@ -5,6 +5,7 @@
|
||||
<color name="conversation_toolbar_color">@color/signal_colorSurface</color>
|
||||
<color name="conversation_toolbar_color_wallpaper">@color/signal_colorTransparentInverse5</color>
|
||||
<color name="conversation_toolbar_color_wallpaper_scrolled">@color/signal_colorTransparentInverse5</color>
|
||||
<color name="conversation_toolbar_color_incognito">#4A2D73</color>
|
||||
|
||||
<color name="signal_accent_primary">@color/core_ultramarine_light</color>
|
||||
<color name="signal_inverse_primary">@color/core_white</color>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<color name="conversation_toolbar_color">@color/signal_colorSurface</color>
|
||||
<color name="conversation_toolbar_color_wallpaper">@color/signal_colorTransparent5</color>
|
||||
<color name="conversation_toolbar_color_wallpaper_scrolled">@color/signal_colorTransparent5</color>
|
||||
<color name="conversation_toolbar_color_incognito">#C4A8E0</color>
|
||||
|
||||
<color name="signal_accent_primary">@color/core_ultramarine</color>
|
||||
|
||||
|
||||
@@ -518,6 +518,7 @@
|
||||
<string name="ConversationActivity_attachment_exceeds_size_limits">Attachment exceeds size limits for the type of message you\'re sending.</string>
|
||||
<string name="ConversationActivity_unable_to_record_audio">Unable to record audio!</string>
|
||||
<string name="ConversationActivity_you_cant_send_messages_to_this_group">You can\'t send messages to this group because you\'re no longer a member.</string>
|
||||
<string name="DisabledInputView__incognito_mode">Incognito mode</string>
|
||||
<string name="ConversationActivity_only_s_can_send_messages">Only %1$s can send messages.</string>
|
||||
<string name="ConversationActivity_admins">admins</string>
|
||||
<string name="ConversationActivity_message_an_admin">Message an admin</string>
|
||||
|
||||
Reference in New Issue
Block a user