mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-20 08:39:22 +01:00
Implement Stories feature behind flag.
Co-Authored-By: Greyson Parrelli <37311915+greyson-signal@users.noreply.github.com> Co-Authored-By: Rashad Sookram <95182499+rashad-signal@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
package org.thoughtcrime.securesms.util
|
||||
|
||||
import android.content.Context
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.core.SingleEmitter
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.sms.MessageSender
|
||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask
|
||||
|
||||
object DeleteDialog {
|
||||
|
||||
fun show(
|
||||
context: Context,
|
||||
messageRecords: Set<MessageRecord>,
|
||||
title: CharSequence = context.resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageRecords.size, messageRecords.size),
|
||||
message: CharSequence? = null,
|
||||
forceRemoteDelete: Boolean = false
|
||||
): Single<Boolean> = Single.create { emitter ->
|
||||
val builder = MaterialAlertDialogBuilder(context)
|
||||
|
||||
builder.setTitle(title)
|
||||
builder.setMessage(message)
|
||||
builder.setCancelable(true)
|
||||
|
||||
if (forceRemoteDelete) {
|
||||
builder.setPositiveButton(R.string.ConversationFragment_delete_for_everyone) { _, _ -> deleteForEveryone(messageRecords, emitter) }
|
||||
} else {
|
||||
builder.setPositiveButton(R.string.ConversationFragment_delete_for_me) { _, _ ->
|
||||
DeleteProgressDialogAsyncTask(context, messageRecords, emitter::onSuccess).executeOnExecutor(SignalExecutors.BOUNDED)
|
||||
}
|
||||
|
||||
if (RemoteDeleteUtil.isValidSend(messageRecords, System.currentTimeMillis())) {
|
||||
builder.setNeutralButton(R.string.ConversationFragment_delete_for_everyone) { _, _ -> handleDeleteForEveryone(context, messageRecords, emitter) }
|
||||
}
|
||||
}
|
||||
|
||||
builder.setNegativeButton(android.R.string.cancel) { _, _ -> emitter.onSuccess(false) }
|
||||
builder.setOnCancelListener { emitter.onSuccess(false) }
|
||||
builder.show()
|
||||
}
|
||||
|
||||
private fun handleDeleteForEveryone(context: Context, messageRecords: Set<MessageRecord>, emitter: SingleEmitter<Boolean>) {
|
||||
if (SignalStore.uiHints().hasConfirmedDeleteForEveryoneOnce()) {
|
||||
deleteForEveryone(messageRecords, emitter)
|
||||
} else {
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setMessage(R.string.ConversationFragment_this_message_will_be_deleted_for_everyone_in_the_conversation)
|
||||
.setPositiveButton(R.string.ConversationFragment_delete_for_everyone) { _, _ ->
|
||||
SignalStore.uiHints().markHasConfirmedDeleteForEveryoneOnce()
|
||||
deleteForEveryone(messageRecords, emitter)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> emitter.onSuccess(false) }
|
||||
.setOnCancelListener { emitter.onSuccess(false) }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteForEveryone(messageRecords: Set<MessageRecord>, emitter: SingleEmitter<Boolean>) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
messageRecords.forEach { message ->
|
||||
MessageSender.sendRemoteDelete(ApplicationDependencies.getApplication(), message.id, message.isMms)
|
||||
}
|
||||
|
||||
emitter.onSuccess(false)
|
||||
}
|
||||
}
|
||||
|
||||
private class DeleteProgressDialogAsyncTask(
|
||||
context: Context,
|
||||
private val messageRecords: Set<MessageRecord>,
|
||||
private val onDeletionCompleted: ((Boolean) -> Unit)
|
||||
) : ProgressDialogAsyncTask<Void, Void, Boolean>(
|
||||
context,
|
||||
R.string.ConversationFragment_deleting,
|
||||
R.string.ConversationFragment_deleting_messages
|
||||
) {
|
||||
override fun doInBackground(vararg params: Void?): Boolean {
|
||||
return messageRecords.map { record ->
|
||||
if (record.isMms) {
|
||||
SignalDatabase.mms.deleteMessage(record.id)
|
||||
} else {
|
||||
SignalDatabase.sms.deleteMessage(record.id)
|
||||
}
|
||||
}.any { it }
|
||||
}
|
||||
|
||||
override fun onPostExecute(result: Boolean?) {
|
||||
super.onPostExecute(result)
|
||||
onDeletionCompleted(result == true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.thoughtcrime.securesms.util
|
||||
|
||||
/**
|
||||
* Treating an Enum as a circular list, returns the "next"
|
||||
* value after the caller, wrapping around to the first value
|
||||
* in the enum as necessary.
|
||||
*/
|
||||
inline fun <reified T : Enum<T>> T.next(): T {
|
||||
val values = enumValues<T>()
|
||||
val nextOrdinal = (ordinal + 1) % values.size
|
||||
|
||||
return values[nextOrdinal]
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import org.thoughtcrime.securesms.groups.SelectionLimits;
|
||||
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob;
|
||||
import org.thoughtcrime.securesms.jobs.RemoteConfigRefreshJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.keyvalue.StoryValues;
|
||||
import org.thoughtcrime.securesms.messageprocessingalarm.MessageProcessReceiver;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -91,6 +92,9 @@ public final class FeatureFlags {
|
||||
private static final String CDSH = "android.cdsh";
|
||||
private static final String HARDWARE_AEC_MODELS = "android.calling.hardwareAecModels";
|
||||
private static final String FORCE_DEFAULT_AEC = "android.calling.forceDefaultAec";
|
||||
private static final String STORIES = "android.stories";
|
||||
private static final String STORIES_TEXT_FUNCTIONS = "android.stories.text.functions";
|
||||
private static final String STORIES_TEXT_POSTS = "android.stories.text.posts";
|
||||
|
||||
/**
|
||||
* We will only store remote values for flags in this set. If you want a flag to be controllable
|
||||
@@ -134,7 +138,10 @@ public final class FeatureFlags {
|
||||
CHANGE_NUMBER_ENABLED,
|
||||
HARDWARE_AEC_MODELS,
|
||||
FORCE_DEFAULT_AEC,
|
||||
VALENTINES_DONATE_MEGAPHONE
|
||||
VALENTINES_DONATE_MEGAPHONE,
|
||||
STORIES,
|
||||
STORIES_TEXT_FUNCTIONS,
|
||||
STORIES_TEXT_POSTS
|
||||
);
|
||||
|
||||
@VisibleForTesting
|
||||
@@ -209,12 +216,13 @@ public final class FeatureFlags {
|
||||
* These can be called on any thread, including the main thread, so be careful!
|
||||
*
|
||||
* Also note that this doesn't play well with {@link #FORCED_VALUES} -- changes there will not
|
||||
* trigger changes in this map, so you'll have to do some manually hacking to get yourself in the
|
||||
* trigger changes in this map, so you'll have to do some manual hacking to get yourself in the
|
||||
* desired test state.
|
||||
*/
|
||||
private static final Map<String, OnFlagChange> FLAG_CHANGE_LISTENERS = new HashMap<String, OnFlagChange>() {{
|
||||
put(MESSAGE_PROCESSOR_ALARM_INTERVAL, change -> MessageProcessReceiver.startOrUpdateAlarm(ApplicationDependencies.getApplication()));
|
||||
put(SENDER_KEY, change -> ApplicationDependencies.getJobManager().add(new RefreshAttributesJob()));
|
||||
put(STORIES, change -> ApplicationDependencies.getJobManager().add(new RefreshAttributesJob()));
|
||||
}};
|
||||
|
||||
private static final Map<String, Object> REMOTE_VALUES = new TreeMap<>();
|
||||
@@ -429,6 +437,27 @@ public final class FeatureFlags {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not stories are available
|
||||
*/
|
||||
public static boolean stories() {
|
||||
return getBoolean(STORIES, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether users can apply alignment and scale to text posts
|
||||
*/
|
||||
public static boolean storiesTextFunctions() {
|
||||
return getBoolean(STORIES_TEXT_FUNCTIONS, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the user supports sending Story text posts
|
||||
*/
|
||||
public static boolean storiesTextPosts() {
|
||||
return getBoolean(STORIES_TEXT_POSTS, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not donor badges should be displayed throughout the app.
|
||||
*/
|
||||
|
||||
@@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.mms.MessageGroupContext;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingGroupUpdateMessage;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.whispersystems.libsignal.InvalidMessageException;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
@@ -52,6 +53,12 @@ public final class GroupUtil {
|
||||
content.getSyncMessage().get().getSent().get().getMessage().getGroupContext().isPresent())
|
||||
{
|
||||
return content.getSyncMessage().get().getSent().get().getMessage().getGroupContext().get();
|
||||
} else if (content.getStoryMessage().isPresent() && content.getStoryMessage().get().getGroupContext().isPresent()) {
|
||||
try {
|
||||
return SignalServiceGroupContext.create(null, content.getStoryMessage().get().getGroupContext().get());
|
||||
} catch (InvalidMessageException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.annimon.stream.Stream;
|
||||
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.DatabaseId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.whispersystems.libsignal.util.guava.Preconditions;
|
||||
|
||||
@@ -89,8 +90,8 @@ public final class SqlUtil {
|
||||
for (int i = 0; i < objects.length; i++) {
|
||||
if (objects[i] == null) {
|
||||
throw new NullPointerException("Cannot have null arg!");
|
||||
} else if (objects[i] instanceof RecipientId) {
|
||||
args[i] = ((RecipientId) objects[i]).serialize();
|
||||
} else if (objects[i] instanceof DatabaseId) {
|
||||
args[i] = ((DatabaseId) objects[i]).serialize();
|
||||
} else {
|
||||
args[i] = objects[i].toString();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package org.thoughtcrime.securesms.util
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.ContextThemeWrapper
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.fragment.app.Fragment
|
||||
|
||||
/**
|
||||
* "Mixin" to theme a Fragment with the given themeResId. This is making me wish Kotlin
|
||||
* had a stronger generic type system.
|
||||
*/
|
||||
object ThemedFragment {
|
||||
|
||||
private const val UNSET = -1
|
||||
private const val THEME_RES_ID = "ThemedFragment::theme_res_id"
|
||||
|
||||
@JvmStatic
|
||||
val Fragment.themeResId: Int
|
||||
get() = arguments?.getInt(THEME_RES_ID) ?: UNSET
|
||||
|
||||
@JvmStatic
|
||||
fun Fragment.themedInflate(@LayoutRes layoutId: Int, inflater: LayoutInflater, container: ViewGroup?): View? {
|
||||
return if (themeResId != UNSET) {
|
||||
inflater.cloneInContext(ContextThemeWrapper(inflater.context, themeResId)).inflate(layoutId, container, false)
|
||||
} else {
|
||||
inflater.inflate(layoutId, container, false)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun Fragment.withTheme(@StyleRes themeId: Int): Fragment {
|
||||
arguments = (arguments ?: Bundle()).apply {
|
||||
putInt(THEME_RES_ID, themeId)
|
||||
}
|
||||
return this
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import android.app.ProgressDialog;
|
||||
import android.content.Context;
|
||||
import android.os.AsyncTask;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
public abstract class ProgressDialogAsyncTask<Params, Progress, Result> extends AsyncTask<Params, Progress, Result> {
|
||||
@@ -30,6 +32,7 @@ public abstract class ProgressDialogAsyncTask<Params, Progress, Result> extends
|
||||
if (context != null) progress = ProgressDialog.show(context, title, message, true);
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
@Override
|
||||
protected void onPostExecute(Result result) {
|
||||
if (progress != null) progress.dismiss();
|
||||
|
||||
Reference in New Issue
Block a user