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:
Alex Hart
2022-02-24 13:40:28 -04:00
parent 765185952e
commit 174cd860a0
416 changed files with 19506 additions and 857 deletions

View File

@@ -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)
}
}
}

View File

@@ -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]
}

View File

@@ -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.
*/

View File

@@ -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;
}

View File

@@ -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();
}

View File

@@ -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
}
}

View File

@@ -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();