Implement support for 'allows replies' toggle.

This commit is contained in:
Alex Hart
2022-03-01 12:41:45 -04:00
parent ee176cbe3d
commit 35cd36e9fe
40 changed files with 374 additions and 148 deletions

View File

@@ -5,8 +5,10 @@ import org.junit.Before
import org.junit.Test
import org.thoughtcrime.securesms.database.model.DistributionListId
import org.thoughtcrime.securesms.database.model.DistributionListRecord
import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.push.ACI
import java.lang.IllegalStateException
import java.util.UUID
class DistributionListDatabaseTest {
@@ -60,9 +62,53 @@ class DistributionListDatabaseTest {
Assert.assertEquals(members, foundMembers)
}
fun createRecipients(count: Int) {
@Test
fun givenStoryExists_getStoryType_returnsStoryWithReplies() {
val id: DistributionListId? = distributionDatabase.createList("test", recipientList(1, 2, 3))
Assert.assertNotNull(id)
val storyType = distributionDatabase.getStoryType(id!!)
Assert.assertEquals(StoryType.STORY_WITH_REPLIES, storyType)
}
@Test
fun givenStoryExistsAndMarkedNoReplies_getStoryType_returnsStoryWithoutReplies() {
val id: DistributionListId? = distributionDatabase.createList("test", recipientList(1, 2, 3))
Assert.assertNotNull(id)
distributionDatabase.setAllowsReplies(id!!, false)
val storyType = distributionDatabase.getStoryType(id)
Assert.assertEquals(StoryType.STORY_WITHOUT_REPLIES, storyType)
}
@Test
fun givenStoryExistsAndMarkedNoReplies_getAllListsForContactSelectionUi_returnsStoryWithoutReplies() {
val id: DistributionListId? = distributionDatabase.createList("test", recipientList(1, 2, 3))
Assert.assertNotNull(id)
distributionDatabase.setAllowsReplies(id!!, false)
val records = distributionDatabase.getAllListsForContactSelectionUi(null, false)
Assert.assertFalse(records.first().allowsReplies)
}
@Test
fun givenStoryExists_getAllListsForContactSelectionUi_returnsStoryWithReplies() {
val id: DistributionListId? = distributionDatabase.createList("test", recipientList(1, 2, 3))
Assert.assertNotNull(id)
val records = distributionDatabase.getAllListsForContactSelectionUi(null, false)
Assert.assertTrue(records.first().allowsReplies)
}
@Test(expected = IllegalStateException::class)
fun givenStoryDoesNotExist_getStoryType_throwsIllegalStateException() {
distributionDatabase.getStoryType(DistributionListId.from(12))
Assert.fail("Expected an assertion error.")
}
private fun createRecipients(count: Int) {
for (i in 0 until count) {
SignalDatabase.recipients.getOrInsertFromAci(ACI.from(UUID.randomUUID()))
SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID()))
}
}

View File

@@ -109,6 +109,7 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.ShortcutLauncherActivity;
import org.thoughtcrime.securesms.TransportOption;
import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel;
import org.thoughtcrime.securesms.database.model.StoryType;
import org.thoughtcrime.securesms.verify.VerifyIdentityActivity;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.TombstoneAttachment;
@@ -291,7 +292,6 @@ import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.views.Stub;
import org.thoughtcrime.securesms.verify.VerifyIdentityActivity;
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
import org.thoughtcrime.securesms.wallpaper.ChatWallpaperDimLevelUtil;
import org.whispersystems.libsignal.InvalidMessageException;
@@ -302,7 +302,6 @@ import org.whispersystems.signalservice.api.SignalSessionLock;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
@@ -2961,7 +2960,7 @@ public class ConversationParentFragment extends Fragment
long expiresIn = TimeUnit.SECONDS.toMillis(recipient.get().getExpiresInSeconds());
QuoteModel quote = result.isViewOnce() ? null : inputPanel.getQuote().orNull();
List<Mention> mentions = new ArrayList<>(result.getMentions());
OutgoingMediaMessage message = new OutgoingMediaMessage(recipient.get(), new SlideDeck(), result.getBody(), System.currentTimeMillis(), -1, expiresIn, result.isViewOnce(), distributionType, result.isStory(), null, quote, Collections.emptyList(), Collections.emptyList(), mentions);
OutgoingMediaMessage message = new OutgoingMediaMessage(recipient.get(), new SlideDeck(), result.getBody(), System.currentTimeMillis(), -1, expiresIn, result.isViewOnce(), distributionType, result.getStoryType(), null, quote, Collections.emptyList(), Collections.emptyList(), mentions);
OutgoingMediaMessage secureMessage = new OutgoingSecureMediaMessage(message);
final Context context = requireContext().getApplicationContext();
@@ -3037,7 +3036,7 @@ public class ConversationParentFragment extends Fragment
}
}
OutgoingMediaMessage outgoingMessageCandidate = new OutgoingMediaMessage(Recipient.resolved(recipientId), slideDeck, body, System.currentTimeMillis(), subscriptionId, expiresIn, viewOnce, distributionType, false, null, quote, contacts, previews, mentions);
OutgoingMediaMessage outgoingMessageCandidate = new OutgoingMediaMessage(Recipient.resolved(recipientId), slideDeck, body, System.currentTimeMillis(), subscriptionId, expiresIn, viewOnce, distributionType, StoryType.NONE, null, quote, contacts, previews, mentions);
final SettableFuture<Void> future = new SettableFuture<>();
final Context context = requireContext().getApplicationContext();

View File

@@ -7,6 +7,7 @@ import androidx.core.content.contentValuesOf
import org.thoughtcrime.securesms.database.model.DistributionListId
import org.thoughtcrime.securesms.database.model.DistributionListPartialRecord
import org.thoughtcrime.securesms.database.model.DistributionListRecord
import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.thoughtcrime.securesms.util.Base64
@@ -55,13 +56,15 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
const val NAME = "name"
const val DISTRIBUTION_ID = "distribution_id"
const val RECIPIENT_ID = "recipient_id"
const val ALLOWS_REPLIES = "allows_replies"
const val CREATE_TABLE = """
CREATE TABLE $TABLE_NAME (
$ID INTEGER PRIMARY KEY AUTOINCREMENT,
$NAME TEXT UNIQUE NOT NULL,
$DISTRIBUTION_ID TEXT UNIQUE NOT NULL,
$RECIPIENT_ID INTEGER UNIQUE REFERENCES ${RecipientDatabase.TABLE_NAME} (${RecipientDatabase.ID})
$RECIPIENT_ID INTEGER UNIQUE REFERENCES ${RecipientDatabase.TABLE_NAME} (${RecipientDatabase.ID}),
$ALLOWS_REPLIES INTEGER DEFAULT 1
)
"""
}
@@ -106,6 +109,7 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
DistributionListPartialRecord(
id = DistributionListId.from(CursorUtil.requireLong(it, ListTable.ID)),
name = CursorUtil.requireString(it, ListTable.NAME),
allowsReplies = CursorUtil.requireBoolean(it, ListTable.ALLOWS_REPLIES),
recipientId = RecipientId.from(CursorUtil.requireLong(it, ListTable.RECIPIENT_ID))
)
)
@@ -117,7 +121,7 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
fun getAllListsForContactSelectionUiCursor(query: String?, includeMyStory: Boolean): Cursor? {
val db = readableDatabase
val projection = arrayOf(ListTable.ID, ListTable.NAME, ListTable.RECIPIENT_ID)
val projection = arrayOf(ListTable.ID, ListTable.NAME, ListTable.RECIPIENT_ID, ListTable.ALLOWS_REPLIES)
val where = when {
query.isNullOrEmpty() && includeMyStory -> null
@@ -137,7 +141,7 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
fun getCustomListsForUi(): List<DistributionListPartialRecord> {
val db = readableDatabase
val projection = SqlUtil.buildArgs(ListTable.ID, ListTable.NAME, ListTable.RECIPIENT_ID)
val projection = SqlUtil.buildArgs(ListTable.ID, ListTable.NAME, ListTable.RECIPIENT_ID, ListTable.ALLOWS_REPLIES)
val selection = "${ListTable.ID} != ${DistributionListId.MY_STORY_ID}"
return db.query(ListTable.TABLE_NAME, projection, selection, null, null, null, null)?.use {
@@ -147,6 +151,7 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
DistributionListPartialRecord(
id = DistributionListId.from(CursorUtil.requireLong(it, ListTable.ID)),
name = CursorUtil.requireString(it, ListTable.NAME),
allowsReplies = CursorUtil.requireBoolean(it, ListTable.ALLOWS_REPLIES),
recipientId = RecipientId.from(CursorUtil.requireLong(it, ListTable.RECIPIENT_ID))
)
)
@@ -194,6 +199,24 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
}
}
fun getStoryType(listId: DistributionListId): StoryType {
readableDatabase.query(ListTable.TABLE_NAME, arrayOf(ListTable.ALLOWS_REPLIES), "${ListTable.ID} = ?", SqlUtil.buildArgs(listId), null, null, null).use { cursor ->
return if (cursor.moveToFirst()) {
if (CursorUtil.requireBoolean(cursor, ListTable.ALLOWS_REPLIES)) {
StoryType.STORY_WITH_REPLIES
} else {
StoryType.STORY_WITHOUT_REPLIES
}
} else {
error("Distribution list not in database.")
}
}
}
fun setAllowsReplies(listId: DistributionListId, allowsReplies: Boolean) {
writableDatabase.update(ListTable.TABLE_NAME, contentValuesOf(ListTable.ALLOWS_REPLIES to allowsReplies), "${ListTable.ID} = ?", SqlUtil.buildArgs(listId))
}
fun getList(listId: DistributionListId): DistributionListRecord? {
readableDatabase.query(ListTable.TABLE_NAME, null, "${ListTable.ID} = ?", SqlUtil.buildArgs(listId), null, null, null).use { cursor ->
return if (cursor.moveToFirst()) {
@@ -203,6 +226,7 @@ class DistributionListDatabase constructor(context: Context?, databaseHelper: Si
id = id,
name = cursor.requireNonNullString(ListTable.NAME),
distributionId = DistributionId.from(cursor.requireNonNullString(ListTable.DISTRIBUTION_ID)),
allowsReplies = CursorUtil.requireBoolean(cursor, ListTable.ALLOWS_REPLIES),
members = getMembers(id)
)
} else {

View File

@@ -51,10 +51,11 @@ import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.database.model.MessageId;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.ParentStoryId;
import org.thoughtcrime.securesms.database.model.Quote;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.database.model.StoryType;
import org.thoughtcrime.securesms.database.model.StoryViewState;
import org.thoughtcrime.securesms.database.model.ParentStoryId;
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange;
@@ -132,7 +133,7 @@ public class MmsDatabase extends MessageDatabase {
static final String MESSAGE_RANGES = "ranges";
public static final String VIEW_ONCE = "reveal_duration";
static final String IS_STORY = "is_story";
static final String STORY_TYPE = "is_story";
static final String PARENT_STORY_ID = "parent_story_id";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
@@ -179,7 +180,7 @@ public class MmsDatabase extends MessageDatabase {
SERVER_GUID + " TEXT DEFAULT NULL, " +
RECEIPT_TIMESTAMP + " INTEGER DEFAULT -1, " +
MESSAGE_RANGES + " BLOB DEFAULT NULL, " +
IS_STORY + " INTEGER DEFAULT 0, " +
STORY_TYPE + " INTEGER DEFAULT 0, " +
PARENT_STORY_ID + " INTEGER DEFAULT 0);";
public static final String[] CREATE_INDEXS = {
@@ -189,7 +190,7 @@ public class MmsDatabase extends MessageDatabase {
"CREATE INDEX IF NOT EXISTS mms_date_server_index ON " + TABLE_NAME + " (" + DATE_SERVER + ");",
"CREATE INDEX IF NOT EXISTS mms_thread_date_index ON " + TABLE_NAME + " (" + THREAD_ID + ", " + DATE_RECEIVED + ");",
"CREATE INDEX IF NOT EXISTS mms_reactions_unread_index ON " + TABLE_NAME + " (" + REACTIONS_UNREAD + ");",
"CREATE INDEX IF NOT EXISTS mms_is_story_index ON " + TABLE_NAME + " (" + IS_STORY + ");",
"CREATE INDEX IF NOT EXISTS mms_is_story_index ON " + TABLE_NAME + " (" + STORY_TYPE + ");",
"CREATE INDEX IF NOT EXISTS mms_parent_story_id_index ON " + TABLE_NAME + " (" + PARENT_STORY_ID + ");"
};
@@ -206,7 +207,7 @@ public class MmsDatabase extends MessageDatabase {
EXPIRES_IN, EXPIRE_STARTED, NOTIFIED, QUOTE_ID, QUOTE_AUTHOR, QUOTE_BODY, QUOTE_ATTACHMENT, QUOTE_MISSING, QUOTE_MENTIONS,
SHARED_CONTACTS, LINK_PREVIEWS, UNIDENTIFIED, VIEW_ONCE, REACTIONS_UNREAD, REACTIONS_LAST_SEEN,
REMOTE_DELETED, MENTIONS_SELF, NOTIFIED_TIMESTAMP, VIEWED_RECEIPT_COUNT, RECEIPT_TIMESTAMP, MESSAGE_RANGES,
IS_STORY, PARENT_STORY_ID,
STORY_TYPE, PARENT_STORY_ID,
"json_group_array(json_object(" +
"'" + AttachmentDatabase.ROW_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + ", " +
"'" + AttachmentDatabase.UNIQUE_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UNIQUE_ID + ", " +
@@ -237,9 +238,9 @@ public class MmsDatabase extends MessageDatabase {
"'" + AttachmentDatabase.DISPLAY_ORDER + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DISPLAY_ORDER + ", " +
"'" + AttachmentDatabase.UPLOAD_TIMESTAMP + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UPLOAD_TIMESTAMP +
")) AS " + AttachmentDatabase.ATTACHMENT_JSON_ALIAS,
};
};
private static final String IS_STORY_CLAUSE = IS_STORY + " = ? AND " + REMOTE_DELETED + " = ?";
private static final String IS_STORY_CLAUSE = STORY_TYPE + " > 0 AND " + REMOTE_DELETED + " = 0";
private static final String RAW_ID_WHERE = TABLE_NAME + "._id = ?";
@@ -538,7 +539,7 @@ public class MmsDatabase extends MessageDatabase {
SQLiteDatabase database = databaseHelper.getSignalReadableDatabase();
String[] projection = new String[]{"1"};
String where = IS_STORY_CLAUSE + " AND " + ID + " = ?";
String[] whereArgs = SqlUtil.buildArgs(1, 0, messageId);
String[] whereArgs = SqlUtil.buildArgs(messageId);
try (Cursor cursor = database.query(TABLE_NAME, projection, where, whereArgs, null, null, null, "1")) {
return cursor != null && cursor.moveToFirst();
@@ -559,7 +560,7 @@ public class MmsDatabase extends MessageDatabase {
final String[] whereArgs;
if (threadId == null) {
where += " AND " + RECIPIENT_ID + " = ?";
whereArgs = SqlUtil.buildArgs(1, 0, recipientId);
whereArgs = SqlUtil.buildArgs(recipientId);
} else {
where += " AND " + THREAD_ID_WHERE;
whereArgs = SqlUtil.buildArgs(1, 0, threadId);
@@ -571,25 +572,20 @@ public class MmsDatabase extends MessageDatabase {
@Override
public @NonNull MessageDatabase.Reader getAllOutgoingStories() {
String where = IS_STORY_CLAUSE + " AND (" + getOutgoingTypeClause() + ")";
String[] whereArgs = SqlUtil.buildArgs(1, 0);
return new Reader(rawQuery(where, whereArgs, true, -1L));
return new Reader(rawQuery(where, null, true, -1L));
}
@Override
public @NonNull MessageDatabase.Reader getAllStories() {
String where = IS_STORY_CLAUSE;
String[] whereArgs = SqlUtil.buildArgs(1, 0);
Cursor cursor = rawQuery(where, whereArgs, true, -1L);
return new Reader(cursor);
return new Reader(rawQuery(IS_STORY_CLAUSE, null, true, -1L));
}
@Override
public @NonNull MessageDatabase.Reader getAllStoriesFor(@NonNull RecipientId recipientId) {
long threadId = SignalDatabase.threads().getThreadIdIfExistsFor(recipientId);
String where = IS_STORY_CLAUSE + " AND " + THREAD_ID_WHERE;
String[] whereArgs = SqlUtil.buildArgs(1, 0, threadId);
String[] whereArgs = SqlUtil.buildArgs(threadId);
Cursor cursor = rawQuery(where, whereArgs, true, -1L);
return new Reader(cursor);
@@ -609,7 +605,7 @@ public class MmsDatabase extends MessageDatabase {
@VisibleForTesting
@NonNull StoryViewState getStoryViewState(long threadId) {
final String hasStoryQuery = "SELECT EXISTS(SELECT 1 FROM " + TABLE_NAME + " WHERE " + IS_STORY_CLAUSE + " AND " + THREAD_ID_WHERE + " LIMIT 1)";
final String[] hasStoryArgs = SqlUtil.buildArgs(1, 0, threadId);
final String[] hasStoryArgs = SqlUtil.buildArgs(threadId);
final boolean hasStories;
try (Cursor cursor = getReadableDatabase().rawQuery(hasStoryQuery, hasStoryArgs)) {
@@ -621,7 +617,7 @@ public class MmsDatabase extends MessageDatabase {
}
final String hasUnviewedStoriesQuery = "SELECT EXISTS(SELECT 1 FROM " + TABLE_NAME + " WHERE " + IS_STORY_CLAUSE + " AND " + THREAD_ID_WHERE + " AND " + VIEWED_RECEIPT_COUNT + " = ? " + "AND NOT (" + getOutgoingTypeClause() + ") LIMIT 1)";
final String[] hasUnviewedStoriesArgs = SqlUtil.buildArgs(1, 0, threadId, 0);
final String[] hasUnviewedStoriesArgs = SqlUtil.buildArgs(threadId, 0);
final boolean hasUnviewedStories;
try (Cursor cursor = getReadableDatabase().rawQuery(hasUnviewedStoriesQuery, hasUnviewedStoriesArgs)) {
@@ -640,7 +636,7 @@ public class MmsDatabase extends MessageDatabase {
SQLiteDatabase database = databaseHelper.getSignalReadableDatabase();
String[] projection = new String[]{ID, RECIPIENT_ID};
String where = IS_STORY_CLAUSE + " AND " + DATE_SENT + " = ?";
String[] whereArgs = SqlUtil.buildArgs(1, 0, sentTimestamp);
String[] whereArgs = SqlUtil.buildArgs(sentTimestamp);
try (Cursor cursor = database.query(TABLE_NAME, projection, where, whereArgs, null, null, null, "1")) {
if (cursor != null && cursor.moveToFirst()) {
@@ -664,10 +660,9 @@ public class MmsDatabase extends MessageDatabase {
"ON " + TABLE_NAME + "." + THREAD_ID + " = " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ID + " " +
"WHERE " + IS_STORY_CLAUSE + " " +
"ORDER BY " + ThreadDatabase.RECIPIENT_ID + " DESC";
String[] args = SqlUtil.buildArgs(1, 0);
List<RecipientId> recipientIds;
try (Cursor cursor = db.rawQuery(query, args)) {
try (Cursor cursor = db.rawQuery(query, null)) {
if (cursor != null) {
recipientIds = new ArrayList<>(cursor.getCount());
@@ -694,9 +689,8 @@ public class MmsDatabase extends MessageDatabase {
public long getUnreadStoryCount() {
String[] columns = new String[]{"COUNT(*)"};
String where = IS_STORY_CLAUSE + " AND NOT (" + getOutgoingTypeClause() + ") AND " + VIEWED_RECEIPT_COUNT + " = 0";
String[] whereArgs = SqlUtil.buildArgs(1, 0);
try (Cursor cursor = getReadableDatabase().query(TABLE_NAME, columns, where, whereArgs, null, null, null, null)) {
try (Cursor cursor = getReadableDatabase().query(TABLE_NAME, columns, where, null, null, null, null, null)) {
return cursor != null && cursor.moveToFirst() ? cursor.getInt(0) : 0;
}
}
@@ -730,11 +724,10 @@ public class MmsDatabase extends MessageDatabase {
SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
String[] columns = new String[]{DATE_SENT};
String where = IS_STORY_CLAUSE;
String[] whereArgs = SqlUtil.buildArgs(1, 0);
String orderBy = DATE_SENT + " ASC";
String limit = "1";
try (Cursor cursor = db.query(TABLE_NAME, columns, where, whereArgs, null, null, orderBy, limit)) {
try (Cursor cursor = db.query(TABLE_NAME, columns, where, null, null, null, orderBy, limit)) {
return cursor != null && cursor.moveToNext() ? cursor.getLong(0) : null;
}
}
@@ -746,7 +739,7 @@ public class MmsDatabase extends MessageDatabase {
db.beginTransaction();
try {
String storiesBeforeTimestampWhere = IS_STORY_CLAUSE + " AND " + DATE_SENT + " < ?";
String[] sharedArgs = SqlUtil.buildArgs(1, 0, timestamp);
String[] sharedArgs = SqlUtil.buildArgs(timestamp);
String deleteStoryRepliesQuery = "DELETE FROM " + TABLE_NAME + " " +
"WHERE " + PARENT_STORY_ID + " IN (" +
"SELECT " + ID + " " +
@@ -814,7 +807,7 @@ public class MmsDatabase extends MessageDatabase {
public int getMessageCountForThread(long threadId) {
SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
String query = THREAD_ID + " = ? AND " + IS_STORY + " = ? AND " + PARENT_STORY_ID + " <= ?";
String query = THREAD_ID + " = ? AND " + STORY_TYPE + " = ? AND " + PARENT_STORY_ID + " <= ?";
String[] args = SqlUtil.buildArgs(threadId, 0, 0);
try (Cursor cursor = db.query(TABLE_NAME, COUNT, query, args, null, null, null)) {
@@ -830,7 +823,7 @@ public class MmsDatabase extends MessageDatabase {
public int getMessageCountForThread(long threadId, long beforeTime) {
SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
String query = THREAD_ID + " = ? AND " + DATE_RECEIVED + " < ? AND " + IS_STORY + " = ? AND " + PARENT_STORY_ID + " <= ?";
String query = THREAD_ID + " = ? AND " + DATE_RECEIVED + " < ? AND " + STORY_TYPE + " = ? AND " + PARENT_STORY_ID + " <= ?";
String[] args = SqlUtil.buildArgs(threadId, beforeTime, 0, 0);
try (Cursor cursor = db.query(TABLE_NAME, COUNT, query, args, null, null, null)) {
@@ -1208,20 +1201,20 @@ public class MmsDatabase extends MessageDatabase {
@Override
public List<MarkedMessageInfo> setMessagesReadSince(long threadId, long sinceTimestamp) {
if (sinceTimestamp == -1) {
return setMessagesRead(THREAD_ID + " = ? AND " + IS_STORY + " = 0 AND " + PARENT_STORY_ID + " <= 0 AND (" + READ + " = 0 OR (" + REACTIONS_UNREAD + " = 1 AND (" + getOutgoingTypeClause() + ")))", new String[] {String.valueOf(threadId)});
return setMessagesRead(THREAD_ID + " = ? AND " + STORY_TYPE + " = 0 AND " + PARENT_STORY_ID + " <= 0 AND (" + READ + " = 0 OR (" + REACTIONS_UNREAD + " = 1 AND (" + getOutgoingTypeClause() + ")))", new String[] { String.valueOf(threadId)});
} else {
return setMessagesRead(THREAD_ID + " = ? AND " + IS_STORY + " = 0 AND " + PARENT_STORY_ID + " <= 0 AND (" + READ + " = 0 OR (" + REACTIONS_UNREAD + " = 1 AND ( " + getOutgoingTypeClause() + " ))) AND " + DATE_RECEIVED + " <= ?", new String[]{String.valueOf(threadId), String.valueOf(sinceTimestamp)});
return setMessagesRead(THREAD_ID + " = ? AND " + STORY_TYPE + " = 0 AND " + PARENT_STORY_ID + " <= 0 AND (" + READ + " = 0 OR (" + REACTIONS_UNREAD + " = 1 AND ( " + getOutgoingTypeClause() + " ))) AND " + DATE_RECEIVED + " <= ?", new String[]{ String.valueOf(threadId), String.valueOf(sinceTimestamp)});
}
}
@Override
public List<MarkedMessageInfo> setEntireThreadRead(long threadId) {
return setMessagesRead(THREAD_ID + " = ? AND " + IS_STORY + " = 0 AND " + PARENT_STORY_ID + " <= 0", new String[] {String.valueOf(threadId)});
return setMessagesRead(THREAD_ID + " = ? AND " + STORY_TYPE + " = 0 AND " + PARENT_STORY_ID + " <= 0", new String[] { String.valueOf(threadId)});
}
@Override
public List<MarkedMessageInfo> setAllMessagesRead() {
return setMessagesRead(IS_STORY+ " = 0 AND " + PARENT_STORY_ID + " <= 0 AND (" + READ + " = 0 OR (" + REACTIONS_UNREAD + " = 1 AND (" + getOutgoingTypeClause() + ")))", null);
return setMessagesRead(STORY_TYPE + " = 0 AND " + PARENT_STORY_ID + " <= 0 AND (" + READ + " = 0 OR (" + REACTIONS_UNREAD + " = 1 AND (" + getOutgoingTypeClause() + ")))", null);
}
private List<MarkedMessageInfo> setMessagesRead(String where, String[] arguments) {
@@ -1419,7 +1412,7 @@ public class MmsDatabase extends MessageDatabase {
int distributionType = SignalDatabase.threads().getDistributionType(threadId);
String mismatchDocument = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.MISMATCHED_IDENTITIES));
String networkDocument = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.NETWORK_FAILURE));
boolean isStory = CursorUtil.requireBoolean(cursor, IS_STORY);
StoryType storyType = StoryType.fromCode(CursorUtil.requireInt(cursor, STORY_TYPE));
ParentStoryId parentStoryId = ParentStoryId.deserialize(CursorUtil.requireLong(cursor, PARENT_STORY_ID));
long quoteId = cursor.getLong(cursor.getColumnIndexOrThrow(QUOTE_ID));
@@ -1469,7 +1462,7 @@ public class MmsDatabase extends MessageDatabase {
return new OutgoingExpirationUpdateMessage(recipient, timestamp, expiresIn);
}
OutgoingMediaMessage message = new OutgoingMediaMessage(recipient, body, attachments, timestamp, subscriptionId, expiresIn, viewOnce, distributionType, isStory, parentStoryId, quote, contacts, previews, mentions, networkFailures, mismatches);
OutgoingMediaMessage message = new OutgoingMediaMessage(recipient, body, attachments, timestamp, subscriptionId, expiresIn, viewOnce, distributionType, storyType, parentStoryId, quote, contacts, previews, mentions, networkFailures, mismatches);
if (Types.isSecureType(outboxType)) {
return new OutgoingSecureMediaMessage(message);
@@ -1590,7 +1583,7 @@ public class MmsDatabase extends MessageDatabase {
contentValues.put(SUBSCRIPTION_ID, retrieved.getSubscriptionId());
contentValues.put(EXPIRES_IN, retrieved.getExpiresIn());
contentValues.put(VIEW_ONCE, retrieved.isViewOnce() ? 1 : 0);
contentValues.put(IS_STORY, retrieved.isStory() ? 1 : 0);
contentValues.put(STORY_TYPE, retrieved.getStoryType().getCode());
contentValues.put(PARENT_STORY_ID, retrieved.getParentStoryId() != null ? retrieved.getParentStoryId().serialize() : 0);
contentValues.put(READ, retrieved.isExpirationUpdate() ? 1 : 0);
contentValues.put(UNIDENTIFIED, retrieved.isUnidentified());
@@ -1623,7 +1616,7 @@ public class MmsDatabase extends MessageDatabase {
long messageId = insertMediaMessage(threadId, retrieved.getBody(), retrieved.getAttachments(), quoteAttachments, retrieved.getSharedContacts(), retrieved.getLinkPreviews(), retrieved.getMentions(), retrieved.getMessageRanges(), contentValues, null, true);
if (!Types.isExpirationTimerUpdate(mailbox) && !retrieved.isStory() && retrieved.getParentStoryId() == null) {
if (!Types.isExpirationTimerUpdate(mailbox) && !retrieved.getStoryType().isStory() && retrieved.getParentStoryId() == null) {
SignalDatabase.threads().incrementUnread(threadId, 1);
SignalDatabase.threads().update(threadId, true);
}
@@ -1785,7 +1778,7 @@ public class MmsDatabase extends MessageDatabase {
contentValues.put(RECIPIENT_ID, message.getRecipient().getId().serialize());
contentValues.put(DELIVERY_RECEIPT_COUNT, Stream.of(earlyDeliveryReceipts.values()).mapToLong(EarlyReceiptCache.Receipt::getCount).sum());
contentValues.put(RECEIPT_TIMESTAMP, Stream.of(earlyDeliveryReceipts.values()).mapToLong(EarlyReceiptCache.Receipt::getTimestamp).max().orElse(-1));
contentValues.put(IS_STORY, message.isStory() ? 1 : 0);
contentValues.put(STORY_TYPE, message.getStoryType().getCode());
contentValues.put(PARENT_STORY_ID, message.getParentStoryId() != null ? message.getParentStoryId().serialize() : 0);
if (message.getRecipient().isSelf() && hasAudioAttachment(message.getAttachments())) {
@@ -1840,7 +1833,7 @@ public class MmsDatabase extends MessageDatabase {
SignalDatabase.threads().updateLastSeenAndMarkSentAndLastScrolledSilenty(threadId);
if (!message.isStory()) {
if (!message.getStoryType().isStory()) {
ApplicationDependencies.getDatabaseObserver().notifyMessageInsertObservers(threadId, new MessageId(messageId, true));
} else {
ApplicationDependencies.getDatabaseObserver().notifyStoryObservers(message.getRecipient().getId());
@@ -2276,7 +2269,8 @@ public class MmsDatabase extends MessageDatabase {
0,
0,
-1,
null);
null,
message.getStoryType());
}
}
@@ -2329,6 +2323,7 @@ public class MmsDatabase extends MessageDatabase {
int subscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.SUBSCRIPTION_ID));
int viewedReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.VIEWED_RECEIPT_COUNT));
long receiptTimestamp = CursorUtil.requireLong(cursor, MmsSmsColumns.RECEIPT_TIMESTAMP);
StoryType storyType = StoryType.fromCode(CursorUtil.requireInt(cursor, STORY_TYPE));
if (!TextSecurePreferences.isReadReceiptsEnabled(context)) {
readReceiptCount = 0;
@@ -2350,7 +2345,7 @@ public class MmsDatabase extends MessageDatabase {
addressDeviceId, dateSent, dateReceived, deliveryReceiptCount, threadId,
contentLocationBytes, messageSize, expiry, status,
transactionIdBytes, mailbox, subscriptionId, slideDeck,
readReceiptCount, viewedReceiptCount, receiptTimestamp);
readReceiptCount, viewedReceiptCount, receiptTimestamp, storyType);
}
private MediaMmsMessageRecord getMediaMmsMessageRecord(Cursor cursor) {
@@ -2379,6 +2374,7 @@ public class MmsDatabase extends MessageDatabase {
int viewedReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.VIEWED_RECEIPT_COUNT));
long receiptTimestamp = CursorUtil.requireLong(cursor, MmsSmsColumns.RECEIPT_TIMESTAMP);
byte[] messageRangesData = CursorUtil.requireBlob(cursor, MESSAGE_RANGES);
StoryType storyType = StoryType.fromCode(CursorUtil.requireInt(cursor, STORY_TYPE));
if (!TextSecurePreferences.isReadReceiptsEnabled(context)) {
readReceiptCount = 0;
@@ -2413,7 +2409,8 @@ public class MmsDatabase extends MessageDatabase {
threadId, body, slideDeck, partCount, box, mismatches,
networkFailures, subscriptionId, expiresIn, expireStarted,
isViewOnce, readReceiptCount, quote, contacts, previews, unidentified, Collections.emptyList(),
remoteDelete, mentionsSelf, notifiedTimestamp, viewedReceiptCount, receiptTimestamp, messageRanges);
remoteDelete, mentionsSelf, notifiedTimestamp, viewedReceiptCount, receiptTimestamp, messageRanges,
storyType);
}
private Set<IdentityKeyMismatch> getMismatchedIdentities(String document) {

View File

@@ -111,14 +111,14 @@ public class MmsSmsDatabase extends Database {
MmsSmsColumns.VIEWED_RECEIPT_COUNT,
MmsSmsColumns.RECEIPT_TIMESTAMP,
MmsDatabase.MESSAGE_RANGES,
MmsDatabase.IS_STORY,
MmsDatabase.STORY_TYPE,
MmsDatabase.PARENT_STORY_ID};
private static final String SNIPPET_QUERY = "SELECT " + MmsSmsColumns.ID + ", 0 AS " + TRANSPORT + ", " + SmsDatabase.TYPE + " AS " + MmsSmsColumns.NORMALIZED_TYPE + ", " + SmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " FROM " + SmsDatabase.TABLE_NAME + " " +
"WHERE " + MmsSmsColumns.THREAD_ID + " = ? AND " + SmsDatabase.TYPE + " NOT IN (" + SmsDatabase.Types.PROFILE_CHANGE_TYPE + ", " + SmsDatabase.Types.GV1_MIGRATION_TYPE + ", " + SmsDatabase.Types.CHANGE_NUMBER_TYPE + ", " + SmsDatabase.Types.BOOST_REQUEST_TYPE + ") AND " + SmsDatabase.TYPE + " & " + GROUP_V2_LEAVE_BITS + " != " + GROUP_V2_LEAVE_BITS + " " +
"UNION ALL " +
"SELECT " + MmsSmsColumns.ID + ", 1 AS " + TRANSPORT + ", " + MmsDatabase.MESSAGE_BOX + " AS " + MmsSmsColumns.NORMALIZED_TYPE + ", " + MmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " FROM " + MmsDatabase.TABLE_NAME + " " +
"WHERE " + MmsSmsColumns.THREAD_ID + " = ? AND " + MmsDatabase.MESSAGE_BOX + " & " + GROUP_V2_LEAVE_BITS + " != " + GROUP_V2_LEAVE_BITS + " AND " + MmsDatabase.IS_STORY + " = 0 AND " + MmsDatabase.PARENT_STORY_ID + " <= 0 " +
"WHERE " + MmsSmsColumns.THREAD_ID + " = ? AND " + MmsDatabase.MESSAGE_BOX + " & " + GROUP_V2_LEAVE_BITS + " != " + GROUP_V2_LEAVE_BITS + " AND " + MmsDatabase.STORY_TYPE + " = 0 AND " + MmsDatabase.PARENT_STORY_ID + " <= 0 " +
"ORDER BY " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC " +
"LIMIT 1";
@@ -202,7 +202,7 @@ public class MmsSmsDatabase extends Database {
public Cursor getConversation(long threadId, long offset, long limit) {
SQLiteDatabase db = databaseHelper.getSignalReadableDatabase();
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC";
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + MmsDatabase.IS_STORY + " = 0 AND " + MmsDatabase.PARENT_STORY_ID + " <= 0";
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + MmsDatabase.STORY_TYPE + " = 0 AND " + MmsDatabase.PARENT_STORY_ID + " <= 0";
String limitStr = limit > 0 || offset > 0 ? offset + ", " + limit : null;
String query = buildQuery(PROJECTION, selection, order, limitStr, false);
@@ -264,13 +264,13 @@ public class MmsSmsDatabase extends Database {
}
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " ASC";
String selection = MmsSmsColumns.NOTIFIED + " = 0 AND " + MmsDatabase.IS_STORY + " = 0 AND " + MmsDatabase.PARENT_STORY_ID + " <= 0 AND (" + MmsSmsColumns.READ + " = 0 OR " + MmsSmsColumns.REACTIONS_UNREAD + " = 1" + (stickyQuery.length() > 0 ? " OR (" + stickyQuery.toString() + ")" : "") + ")";
String selection = MmsSmsColumns.NOTIFIED + " = 0 AND " + MmsDatabase.STORY_TYPE + " = 0 AND " + MmsDatabase.PARENT_STORY_ID + " <= 0 AND (" + MmsSmsColumns.READ + " = 0 OR " + MmsSmsColumns.REACTIONS_UNREAD + " = 1" + (stickyQuery.length() > 0 ? " OR (" + stickyQuery.toString() + ")" : "") + ")";
return queryTables(PROJECTION, selection, order, null);
}
public int getUnreadCount(long threadId) {
String selection = MmsSmsColumns.READ + " = 0 AND " + MmsDatabase.IS_STORY + " = 0 AND " + MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + MmsDatabase.PARENT_STORY_ID + " <= 0";
String selection = MmsSmsColumns.READ + " = 0 AND " + MmsDatabase.STORY_TYPE + " = 0 AND " + MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + MmsDatabase.PARENT_STORY_ID + " <= 0";
try (Cursor cursor = queryTables(PROJECTION, selection, null, null)) {
return cursor != null ? cursor.getCount() : 0;
@@ -729,7 +729,7 @@ public class MmsSmsDatabase extends Database {
MmsSmsColumns.VIEWED_RECEIPT_COUNT,
MmsSmsColumns.RECEIPT_TIMESTAMP,
MmsDatabase.MESSAGE_RANGES,
MmsDatabase.IS_STORY,
MmsDatabase.STORY_TYPE,
MmsDatabase.PARENT_STORY_ID};
String[] smsProjection = {SmsDatabase.DATE_SENT + " AS " + MmsSmsColumns.NORMALIZED_DATE_SENT,
@@ -765,8 +765,8 @@ public class MmsSmsDatabase extends Database {
MmsSmsColumns.VIEWED_RECEIPT_COUNT,
MmsSmsColumns.RECEIPT_TIMESTAMP,
MmsDatabase.MESSAGE_RANGES,
"0 AS " + MmsDatabase.IS_STORY,
"0 AS " + MmsDatabase.PARENT_STORY_ID };
"0 AS " + MmsDatabase.STORY_TYPE,
"0 AS " + MmsDatabase.PARENT_STORY_ID};
SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder();
SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder();
@@ -829,7 +829,7 @@ public class MmsSmsDatabase extends Database {
mmsColumnsPresent.add(MmsSmsColumns.VIEWED_RECEIPT_COUNT);
mmsColumnsPresent.add(MmsSmsColumns.RECEIPT_TIMESTAMP);
mmsColumnsPresent.add(MmsDatabase.MESSAGE_RANGES);
mmsColumnsPresent.add(MmsDatabase.IS_STORY);
mmsColumnsPresent.add(MmsDatabase.STORY_TYPE);
mmsColumnsPresent.add(MmsDatabase.PARENT_STORY_ID);
Set<String> smsColumnsPresent = new HashSet<>();
@@ -858,7 +858,7 @@ public class MmsSmsDatabase extends Database {
smsColumnsPresent.add(MmsSmsColumns.REMOTE_DELETED);
smsColumnsPresent.add(MmsSmsColumns.NOTIFIED_TIMESTAMP);
smsColumnsPresent.add(MmsSmsColumns.RECEIPT_TIMESTAMP);
smsColumnsPresent.add("0 AS " + MmsDatabase.IS_STORY);
smsColumnsPresent.add("0 AS " + MmsDatabase.STORY_TYPE);
smsColumnsPresent.add("0 AS " + MmsDatabase.PARENT_STORY_ID);
String mmsGroupBy = includeAttachments ? MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID : null;

View File

@@ -192,8 +192,9 @@ object SignalDatabaseMigrations {
private const val PNI_STORES = 130
private const val DONATION_RECEIPTS = 131
private const val STORIES = 132
private const val ALLOW_STORY_REPLIES = 133
const val DATABASE_VERSION = 132
const val DATABASE_VERSION = 133
@JvmStatic
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
@@ -2471,6 +2472,10 @@ object SignalDatabaseMigrations {
)
)
}
if (oldVersion < ALLOW_STORY_REPLIES) {
db.execSQL("ALTER TABLE distribution_list ADD COLUMN allows_replies INTEGER DEFAULT 1")
}
}
@JvmStatic

View File

@@ -5,5 +5,6 @@ import org.thoughtcrime.securesms.recipients.RecipientId
data class DistributionListPartialRecord(
val id: DistributionListId,
val name: CharSequence,
val recipientId: RecipientId
val recipientId: RecipientId,
val allowsReplies: Boolean
)

View File

@@ -10,5 +10,6 @@ data class DistributionListRecord(
val id: DistributionListId,
val name: String,
val distributionId: DistributionId,
val allowsReplies: Boolean,
val members: List<RecipientId>
)

View File

@@ -91,12 +91,14 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
long notifiedTimestamp,
int viewedReceiptCount,
long receiptTimestamp,
@Nullable BodyRangeList messageRanges)
@Nullable BodyRangeList messageRanges,
@NonNull StoryType storyType)
{
super(id, body, conversationRecipient, individualRecipient, recipientDeviceId, dateSent,
dateReceived, dateServer, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox, mismatches, failures,
subscriptionId, expiresIn, expireStarted, viewOnce, slideDeck,
readReceiptCount, quote, contacts, linkPreviews, unidentified, reactions, remoteDelete, notifiedTimestamp, viewedReceiptCount, receiptTimestamp);
readReceiptCount, quote, contacts, linkPreviews, unidentified, reactions, remoteDelete, notifiedTimestamp, viewedReceiptCount, receiptTimestamp,
storyType);
this.partCount = partCount;
this.mentionsSelf = mentionsSelf;
this.messageRanges = messageRanges;
@@ -150,7 +152,7 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
return new MediaMmsMessageRecord(getId(), getRecipient(), getIndividualRecipient(), getRecipientDeviceId(), getDateSent(), getDateReceived(), getServerTimestamp(), getDeliveryReceiptCount(), getThreadId(), getBody(), getSlideDeck(),
getPartCount(), getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(),
getReadReceiptCount(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), reactions, isRemoteDelete(), mentionsSelf,
getNotifiedTimestamp(), getViewedReceiptCount(), getReceiptTimestamp(), getMessageRanges());
getNotifiedTimestamp(), getViewedReceiptCount(), getReceiptTimestamp(), getMessageRanges(), getStoryType());
}
public @NonNull MediaMmsMessageRecord withAttachments(@NonNull Context context, @NonNull List<DatabaseAttachment> attachments) {
@@ -171,7 +173,7 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
return new MediaMmsMessageRecord(getId(), getRecipient(), getIndividualRecipient(), getRecipientDeviceId(), getDateSent(), getDateReceived(), getServerTimestamp(), getDeliveryReceiptCount(), getThreadId(), getBody(), slideDeck,
getPartCount(), getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(),
getReadReceiptCount(), quote, contacts, linkPreviews, isUnidentified(), getReactions(), isRemoteDelete(), mentionsSelf,
getNotifiedTimestamp(), getViewedReceiptCount(), getReceiptTimestamp(), getMessageRanges());
getNotifiedTimestamp(), getViewedReceiptCount(), getReceiptTimestamp(), getMessageRanges(), getStoryType());
}
private static @NonNull List<Contact> updateContacts(@NonNull List<Contact> contacts, @NonNull Map<AttachmentId, DatabaseAttachment> attachmentIdMap) {

View File

@@ -22,6 +22,7 @@ public abstract class MmsMessageRecord extends MessageRecord {
private final @Nullable Quote quote;
private final @NonNull List<Contact> contacts = new LinkedList<>();
private final @NonNull List<LinkPreview> linkPreviews = new LinkedList<>();
private final @NonNull StoryType storyType;
private final boolean viewOnce;
@@ -35,7 +36,7 @@ public abstract class MmsMessageRecord extends MessageRecord {
@Nullable Quote quote, @NonNull List<Contact> contacts,
@NonNull List<LinkPreview> linkPreviews, boolean unidentified,
@NonNull List<ReactionRecord> reactions, boolean remoteDelete, long notifiedTimestamp,
int viewedReceiptCount, long receiptTimestamp)
int viewedReceiptCount, long receiptTimestamp, @NonNull StoryType storyType)
{
super(id, body, conversationRecipient, individualRecipient, recipientDeviceId,
dateSent, dateReceived, dateServer, threadId, deliveryStatus, deliveryReceiptCount,
@@ -45,6 +46,7 @@ public abstract class MmsMessageRecord extends MessageRecord {
this.slideDeck = slideDeck;
this.quote = quote;
this.viewOnce = viewOnce;
this.storyType = storyType;
this.contacts.addAll(contacts);
this.linkPreviews.addAll(linkPreviews);
@@ -76,6 +78,10 @@ public abstract class MmsMessageRecord extends MessageRecord {
return viewOnce;
}
public @NonNull StoryType getStoryType() {
return storyType;
}
public boolean containsMediaSlide() {
return slideDeck.containsMediaSlide();
}

View File

@@ -53,13 +53,13 @@ public class NotificationMmsMessageRecord extends MmsMessageRecord {
long threadId, byte[] contentLocation, long messageSize,
long expiry, int status, byte[] transactionId, long mailbox,
int subscriptionId, SlideDeck slideDeck, int readReceiptCount,
int viewedReceiptCount, long receiptTimestamp)
int viewedReceiptCount, long receiptTimestamp, @NonNull StoryType storyType)
{
super(id, "", conversationRecipient, individualRecipient, recipientDeviceId,
dateSent, dateReceived, -1, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox,
new HashSet<>(), new HashSet<>(), subscriptionId,
0, 0, false, slideDeck, readReceiptCount, null, Collections.emptyList(), Collections.emptyList(), false,
Collections.emptyList(), false, 0, viewedReceiptCount, receiptTimestamp);
Collections.emptyList(), false, 0, viewedReceiptCount, receiptTimestamp, storyType);
this.contentLocation = contentLocation;
this.messageSize = messageSize;

View File

@@ -0,0 +1,36 @@
package org.thoughtcrime.securesms.database.model
/**
* Represents whether a given story can be replied to.
*/
enum class StoryType(val code: Int) {
/**
* Not a story.
*/
NONE(0),
/**
* User can send replies to this story.
*/
STORY_WITH_REPLIES(1),
/**
* User cannot send replies to this story.
*/
STORY_WITHOUT_REPLIES(2);
val isStory get() = this == STORY_WITH_REPLIES || this == STORY_WITHOUT_REPLIES
val isStoryWithReplies get() = this == STORY_WITH_REPLIES
companion object {
@JvmStatic
fun fromCode(code: Int): StoryType {
return when (code) {
1 -> STORY_WITH_REPLIES
2 -> STORY_WITHOUT_REPLIES
else -> NONE
}
}
}
}

View File

@@ -87,7 +87,7 @@ public final class PushDistributionListSendJob extends PushSendJob {
OutgoingMediaMessage message = SignalDatabase.mms().getOutgoingMessage(messageId);
if (!message.isStory()) {
if (!message.getStoryType().isStory()) {
throw new AssertionError("Only story messages are currently supported! MessageId: " + messageId);
}
@@ -125,7 +125,7 @@ public final class PushDistributionListSendJob extends PushSendJob {
Set<NetworkFailure> existingNetworkFailures = message.getNetworkFailures();
Set<IdentityKeyMismatch> existingIdentityMismatches = message.getIdentityKeyMismatches();
if (!message.isStory()) {
if (!message.getStoryType().isStory()) {
throw new MmsException("Only story sends are currently supported!");
}
@@ -176,7 +176,7 @@ public final class PushDistributionListSendJob extends PushSendJob {
boolean isRecipientUpdate = Stream.of(SignalDatabase.groupReceipts().getGroupReceiptInfo(messageId))
.anyMatch(info -> info.getStatus() > GroupReceiptDatabase.STATUS_UNDELIVERED);
SignalServiceStoryMessage storyMessage = SignalServiceStoryMessage.forFileAttachment(Recipient.self().getProfileKey(), null, attachmentPointers.get(0));
SignalServiceStoryMessage storyMessage = SignalServiceStoryMessage.forFileAttachment(Recipient.self().getProfileKey(), null, attachmentPointers.get(0), message.getStoryType().isStoryWithReplies());
return GroupSendUtil.sendStoryMessage(context, message.getRecipient().requireDistributionListId(), destinations, isRecipientUpdate, new MessageId(messageId, true), message.getSentTimeMillis(), storyMessage);
} catch (ServerRejectedException e) {
throw new UndeliverableMessageException(e);

View File

@@ -51,7 +51,6 @@ import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Quote;
import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
import org.whispersystems.signalservice.api.messages.SignalServicePreview;
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage;
@@ -231,7 +230,7 @@ public final class PushGroupSendJob extends PushSendJob {
boolean isRecipientUpdate = Stream.of(SignalDatabase.groupReceipts().getGroupReceiptInfo(messageId))
.anyMatch(info -> info.getStatus() > GroupReceiptDatabase.STATUS_UNDELIVERED);
if (message.isStory()) {
if (message.getStoryType().isStory()) {
// TODO [stories] Filter based off of stories capability
Optional<GroupDatabase.GroupRecord> groupRecord = SignalDatabase.groups().getGroup(groupId);
@@ -241,7 +240,7 @@ public final class PushGroupSendJob extends PushSendJob {
.withRevision(v2GroupProperties.getGroupRevision())
.build();
SignalServiceStoryMessage storyMessage = SignalServiceStoryMessage.forFileAttachment(Recipient.self().getProfileKey(), groupContext, attachmentPointers.get(0));
SignalServiceStoryMessage storyMessage = SignalServiceStoryMessage.forFileAttachment(Recipient.self().getProfileKey(), groupContext, attachmentPointers.get(0), message.getStoryType().isStoryWithReplies());
return GroupSendUtil.sendGroupStoryMessage(context, groupId.requireV2(), destinations, isRecipientUpdate, new MessageId(messageId, true), message.getSentTimeMillis(), storyMessage);
} else {
@@ -391,7 +390,7 @@ public final class PushGroupSendJob extends PushSendJob {
SignalDatabase.attachments().deleteAttachmentFilesForViewOnceMessage(messageId);
}
if (message.isStory()) {
if (message.getStoryType().isStory()) {
ApplicationDependencies.getExpireStoriesManager().scheduleIfNecessary();
}
} else if (!identityMismatches.isEmpty()) {

View File

@@ -111,7 +111,7 @@ public class RefreshAttributesJob extends BaseJob {
"\n Sender Key? " + capabilities.isSenderKey() +
"\n Announcement Groups? " + capabilities.isAnnouncementGroup() +
"\n Change Number? " + capabilities.isChangeNumber() +
"\n Stories? " + capabilities.isChangeNumber() +
"\n Stories? " + capabilities.isStories() +
"\n UUID? " + capabilities.isUuid());
SignalServiceAccountManager signalAccountManager = ApplicationDependencies.getSignalServiceAccountManager();

View File

@@ -9,6 +9,7 @@ import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.TransportOption;
import org.thoughtcrime.securesms.conversation.ConversationActivity;
import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.database.model.StoryType;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.sms.MessageSender.PreUploadResult;
import org.thoughtcrime.securesms.util.ParcelUtil;
@@ -32,7 +33,7 @@ public class MediaSendActivityResult implements Parcelable {
private final TransportOption transport;
private final boolean viewOnce;
private final Collection<Mention> mentions;
private final boolean isStory;
private final StoryType storyType;
public static @NonNull MediaSendActivityResult fromData(@NonNull Intent data) {
MediaSendActivityResult result = data.getParcelableExtra(MediaSendActivityResult.EXTRA_RESULT);
@@ -49,10 +50,10 @@ public class MediaSendActivityResult implements Parcelable {
@NonNull TransportOption transport,
boolean viewOnce,
@NonNull List<Mention> mentions,
boolean isStory)
@NonNull StoryType storyType)
{
Preconditions.checkArgument(uploadResults.size() > 0, "Must supply uploadResults!");
return new MediaSendActivityResult(recipientId, uploadResults, Collections.emptyList(), body, transport, viewOnce, mentions, isStory);
return new MediaSendActivityResult(recipientId, uploadResults, Collections.emptyList(), body, transport, viewOnce, mentions, storyType);
}
public static @NonNull MediaSendActivityResult forTraditionalSend(@NonNull RecipientId recipientId,
@@ -61,10 +62,10 @@ public class MediaSendActivityResult implements Parcelable {
@NonNull TransportOption transport,
boolean viewOnce,
@NonNull List<Mention> mentions,
boolean isStory)
@NonNull StoryType storyType)
{
Preconditions.checkArgument(nonUploadedMedia.size() > 0, "Must supply media!");
return new MediaSendActivityResult(recipientId, Collections.emptyList(), nonUploadedMedia, body, transport, viewOnce, mentions, isStory);
return new MediaSendActivityResult(recipientId, Collections.emptyList(), nonUploadedMedia, body, transport, viewOnce, mentions, storyType);
}
private MediaSendActivityResult(@NonNull RecipientId recipientId,
@@ -74,7 +75,7 @@ public class MediaSendActivityResult implements Parcelable {
@NonNull TransportOption transport,
boolean viewOnce,
@NonNull List<Mention> mentions,
boolean isStory)
@NonNull StoryType storyType)
{
this.recipientId = recipientId;
this.uploadResults = uploadResults;
@@ -83,7 +84,7 @@ public class MediaSendActivityResult implements Parcelable {
this.transport = transport;
this.viewOnce = viewOnce;
this.mentions = mentions;
this.isStory = isStory;
this.storyType = storyType;
}
private MediaSendActivityResult(Parcel in) {
@@ -94,7 +95,7 @@ public class MediaSendActivityResult implements Parcelable {
this.transport = in.readParcelable(TransportOption.class.getClassLoader());
this.viewOnce = ParcelUtil.readBoolean(in);
this.mentions = ParcelUtil.readParcelableCollection(in, Mention.class);
this.isStory = ParcelUtil.readBoolean(in);
this.storyType = StoryType.fromCode(in.readInt());
}
public @NonNull RecipientId getRecipientId() {
@@ -129,8 +130,8 @@ public class MediaSendActivityResult implements Parcelable {
return mentions;
}
public boolean isStory() {
return isStory;
public @NonNull StoryType getStoryType() {
return storyType;
}
public static final Creator<MediaSendActivityResult> CREATOR = new Creator<MediaSendActivityResult>() {
@@ -159,6 +160,6 @@ public class MediaSendActivityResult implements Parcelable {
dest.writeParcelable(transport, 0);
ParcelUtil.writeBoolean(dest, viewOnce);
ParcelUtil.writeParcelableCollection(dest, mentions);
ParcelUtil.writeBoolean(dest, isStory);
dest.writeInt(storyType.getCode());
}
}

View File

@@ -14,8 +14,10 @@ import org.thoughtcrime.securesms.TransportOption
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
import org.thoughtcrime.securesms.contacts.paged.RecipientSearchKey
import org.thoughtcrime.securesms.database.AttachmentDatabase.TransformProperties
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.database.model.Mention
import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.mediasend.CompositeMediaTransform
import org.thoughtcrime.securesms.mediasend.ImageEditorModelRenderMediaTransform
import org.thoughtcrime.securesms.mediasend.Media
@@ -95,9 +97,15 @@ class MediaSelectionRepository(context: Context) {
}
val singleRecipient: Recipient? = singleContact?.let { Recipient.resolved(it.recipientId) }
val storyType: StoryType = if (singleRecipient?.isDistributionList == true) {
SignalDatabase.distributionLists.getStoryType(singleRecipient.requireDistributionListId())
} else {
StoryType.NONE
}
if (isSms || MessageSender.isLocalSelfSend(context, singleRecipient, isSms)) {
Log.i(TAG, "SMS or local self-send. Skipping pre-upload.")
emitter.onSuccess(MediaSendActivityResult.forTraditionalSend(singleRecipient!!.id, updatedMedia, trimmedBody, transport, isViewOnce, trimmedMentions, false))
emitter.onSuccess(MediaSendActivityResult.forTraditionalSend(singleRecipient!!.id, updatedMedia, trimmedBody, transport, isViewOnce, trimmedMentions, StoryType.NONE))
} else {
val splitMessage = MessageUtil.getSplitMessage(context, trimmedBody, transport.calculateCharacters(trimmedBody).maxPrimaryMessageSize)
val splitBody = splitMessage.body
@@ -126,10 +134,10 @@ class MediaSelectionRepository(context: Context) {
uploadRepository.deleteAbandonedAttachments()
emitter.onComplete()
} else if (uploadResults.isNotEmpty()) {
emitter.onSuccess(MediaSendActivityResult.forPreUpload(singleRecipient!!.id, uploadResults, splitBody, transport, isViewOnce, trimmedMentions, singleContact.isStory))
emitter.onSuccess(MediaSendActivityResult.forPreUpload(singleRecipient!!.id, uploadResults, splitBody, transport, isViewOnce, trimmedMentions, storyType))
} else {
Log.w(TAG, "Got empty upload results! isSms: $isSms, updatedMedia.size(): ${updatedMedia.size}, isViewOnce: $isViewOnce, target: $singleContact")
emitter.onSuccess(MediaSendActivityResult.forTraditionalSend(singleRecipient!!.id, updatedMedia, trimmedBody, transport, isViewOnce, trimmedMentions, singleContact.isStory))
emitter.onSuccess(MediaSendActivityResult.forTraditionalSend(singleRecipient!!.id, updatedMedia, trimmedBody, transport, isViewOnce, trimmedMentions, storyType))
}
}
}
@@ -196,6 +204,13 @@ class MediaSelectionRepository(context: Context) {
for (contact in contacts) {
val recipient = Recipient.resolved(contact.recipientId)
val isStory = contact is ContactSearchKey.Story || recipient.isDistributionList
val storyType: StoryType = when {
recipient.isDistributionList -> SignalDatabase.distributionLists.getStoryType(recipient.requireDistributionListId())
isStory -> StoryType.STORY_WITH_REPLIES
else -> StoryType.NONE
}
val message = OutgoingMediaMessage(
recipient,
body,
@@ -205,7 +220,7 @@ class MediaSelectionRepository(context: Context) {
TimeUnit.SECONDS.toMillis(recipient.expiresInSeconds.toLong()),
isViewOnce,
ThreadDatabase.DistributionTypes.DEFAULT,
isStory,
storyType,
null,
null,
emptyList(),

View File

@@ -50,6 +50,7 @@ import org.thoughtcrime.securesms.database.model.ParentStoryId;
import org.thoughtcrime.securesms.database.model.PendingRetryReceiptModel;
import org.thoughtcrime.securesms.database.model.ReactionRecord;
import org.thoughtcrime.securesms.database.model.StickerRecord;
import org.thoughtcrime.securesms.database.model.StoryType;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.BadGroupIdException;
@@ -830,7 +831,7 @@ public final class MessageContentProcessor {
content.getTimestamp(),
content.getServerReceivedTimestamp(),
receivedTime,
false,
StoryType.NONE,
null,
-1,
expiresInSeconds * 1000L,
@@ -1324,11 +1325,18 @@ public final class MessageContentProcessor {
database.beginTransaction();
try {
final StoryType storyType;
if (message.getAllowsReplies().or(false)) {
storyType = StoryType.STORY_WITH_REPLIES;
} else {
storyType = StoryType.STORY_WITHOUT_REPLIES;
}
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(senderRecipient.getId(),
content.getTimestamp(),
content.getServerReceivedTimestamp(),
System.currentTimeMillis(),
true,
storyType,
null,
-1,
0,
@@ -1388,6 +1396,11 @@ public final class MessageContentProcessor {
} else {
MmsMessageRecord story = (MmsMessageRecord) database.getMessageRecord(storyMessageId.getId());
if (!story.getStoryType().isStoryWithReplies()) {
warn(content.getTimestamp(), "Story has replies disabled. Dropping reply.");
return;
}
parentStoryId = new ParentStoryId.DirectReply(storyMessageId.getId());
quoteModel = new QuoteModel(storyContext.getSentTimestamp(), storyAuthorRecipient, "", false, story.getSlideDeck().asAttachments(), Collections.emptyList());
}
@@ -1400,7 +1413,7 @@ public final class MessageContentProcessor {
content.getTimestamp(),
content.getServerReceivedTimestamp(),
System.currentTimeMillis(),
false,
StoryType.NONE,
parentStoryId,
-1,
0,
@@ -1457,7 +1470,7 @@ public final class MessageContentProcessor {
message.getTimestamp(),
content.getServerReceivedTimestamp(),
receivedTime,
false,
StoryType.NONE,
null,
-1,
TimeUnit.SECONDS.toMillis(message.getExpiresInSeconds()),
@@ -1563,7 +1576,7 @@ public final class MessageContentProcessor {
TimeUnit.SECONDS.toMillis(message.getMessage().getExpiresInSeconds()),
viewOnce,
ThreadDatabase.DistributionTypes.DEFAULT,
false,
StoryType.NONE,
null,
quote.orNull(),
sharedContacts.or(Collections.emptyList()),
@@ -1757,7 +1770,7 @@ public final class MessageContentProcessor {
expiresInMillis,
false,
ThreadDatabase.DistributionTypes.DEFAULT,
false,
StoryType.NONE,
null,
null,
Collections.emptyList(),

View File

@@ -5,6 +5,7 @@ import org.thoughtcrime.securesms.attachments.PointerAttachment
import org.thoughtcrime.securesms.contactshare.Contact
import org.thoughtcrime.securesms.database.model.Mention
import org.thoughtcrime.securesms.database.model.ParentStoryId
import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.linkpreview.LinkPreview
@@ -19,7 +20,7 @@ class IncomingMediaMessage(
val groupId: GroupId? = null,
val body: String? = null,
val isPushMessage: Boolean = false,
val isStory: Boolean = false,
val storyType: StoryType = StoryType.NONE,
val parentStoryId: ParentStoryId? = null,
val sentTimeMillis: Long,
val serverTimeMillis: Long,
@@ -83,7 +84,7 @@ class IncomingMediaMessage(
sentTimeMillis: Long,
serverTimeMillis: Long,
receivedTimeMillis: Long,
isStory: Boolean,
storyType: StoryType,
parentStoryId: ParentStoryId?,
subscriptionId: Int,
expiresIn: Long,
@@ -104,7 +105,7 @@ class IncomingMediaMessage(
groupId = if (group.isPresent) GroupUtil.idFromGroupContextOrThrow(group.get()) else null,
body = body.orNull(),
isPushMessage = true,
isStory = isStory,
storyType = storyType,
parentStoryId = parentStoryId,
sentTimeMillis = sentTimeMillis,
serverTimeMillis = serverTimeMillis,

View File

@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.mms;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.model.StoryType;
import org.thoughtcrime.securesms.recipients.Recipient;
import java.util.Collections;
@@ -12,12 +13,12 @@ public class OutgoingExpirationUpdateMessage extends OutgoingSecureMediaMessage
public OutgoingExpirationUpdateMessage(Recipient recipient, long sentTimeMillis, long expiresIn) {
super(recipient,
"",
new LinkedList<Attachment>(),
new LinkedList<>(),
sentTimeMillis,
ThreadDatabase.DistributionTypes.CONVERSATION,
expiresIn,
false,
false,
StoryType.NONE,
null,
null,
Collections.emptyList(),

View File

@@ -7,6 +7,7 @@ import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.database.model.StoryType;
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -38,7 +39,7 @@ public final class OutgoingGroupUpdateMessage extends OutgoingSecureMediaMessage
ThreadDatabase.DistributionTypes.CONVERSATION,
expiresIn,
viewOnce,
false,
StoryType.NONE,
null,
quote,
contacts,

View File

@@ -11,6 +11,7 @@ import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.database.model.ParentStoryId;
import org.thoughtcrime.securesms.database.model.StoryType;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -30,7 +31,7 @@ public class OutgoingMediaMessage {
private final long expiresIn;
private final boolean viewOnce;
private final QuoteModel outgoingQuote;
private final boolean isStory;
private final StoryType storyType;
private final ParentStoryId parentStoryId;
private final Set<NetworkFailure> networkFailures = new HashSet<>();
@@ -47,7 +48,7 @@ public class OutgoingMediaMessage {
long expiresIn,
boolean viewOnce,
int distributionType,
boolean isStory,
@NonNull StoryType storyType,
@Nullable ParentStoryId parentStoryId,
@Nullable QuoteModel outgoingQuote,
@NonNull List<Contact> contacts,
@@ -65,7 +66,7 @@ public class OutgoingMediaMessage {
this.expiresIn = expiresIn;
this.viewOnce = viewOnce;
this.outgoingQuote = outgoingQuote;
this.isStory = isStory;
this.storyType = storyType;
this.parentStoryId = parentStoryId;
this.contacts.addAll(contacts);
@@ -83,7 +84,7 @@ public class OutgoingMediaMessage {
long expiresIn,
boolean viewOnce,
int distributionType,
boolean isStory,
@NonNull StoryType storyType,
@Nullable ParentStoryId parentStoryId,
@Nullable QuoteModel outgoingQuote,
@NonNull List<Contact> contacts,
@@ -98,7 +99,7 @@ public class OutgoingMediaMessage {
expiresIn,
viewOnce,
distributionType,
isStory,
storyType,
parentStoryId,
outgoingQuote,
contacts,
@@ -118,7 +119,7 @@ public class OutgoingMediaMessage {
this.expiresIn = that.expiresIn;
this.viewOnce = that.viewOnce;
this.outgoingQuote = that.outgoingQuote;
this.isStory = that.isStory;
this.storyType = that.storyType;
this.parentStoryId = that.parentStoryId;
this.identityKeyMismatches.addAll(that.identityKeyMismatches);
@@ -138,7 +139,7 @@ public class OutgoingMediaMessage {
expiresIn,
viewOnce,
distributionType,
isStory,
storyType,
parentStoryId,
outgoingQuote,
contacts,
@@ -193,8 +194,8 @@ public class OutgoingMediaMessage {
return viewOnce;
}
public boolean isStory() {
return isStory;
public @NonNull StoryType getStoryType() {
return storyType;
}
public @Nullable ParentStoryId getParentStoryId() {

View File

@@ -7,6 +7,7 @@ import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.database.model.ParentStoryId;
import org.thoughtcrime.securesms.database.model.StoryType;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -22,14 +23,14 @@ public class OutgoingSecureMediaMessage extends OutgoingMediaMessage {
int distributionType,
long expiresIn,
boolean viewOnce,
boolean isStory,
@NonNull StoryType storyType,
@Nullable ParentStoryId parentStoryId,
@Nullable QuoteModel quote,
@NonNull List<Contact> contacts,
@NonNull List<LinkPreview> previews,
@NonNull List<Mention> mentions)
{
super(recipient, body, attachments, sentTimeMillis, -1, expiresIn, viewOnce, distributionType, isStory, parentStoryId, quote, contacts, previews, mentions, Collections.emptySet(), Collections.emptySet());
super(recipient, body, attachments, sentTimeMillis, -1, expiresIn, viewOnce, distributionType, storyType, parentStoryId, quote, contacts, previews, mentions, Collections.emptySet(), Collections.emptySet());
}
public OutgoingSecureMediaMessage(OutgoingMediaMessage base) {
@@ -50,7 +51,7 @@ public class OutgoingSecureMediaMessage extends OutgoingMediaMessage {
getDistributionType(),
expiresIn,
isViewOnce(),
isStory(),
getStoryType(),
getParentStoryId(),
getOutgoingQuote(),
getSharedContacts(),

View File

@@ -28,6 +28,7 @@ import androidx.core.app.RemoteInput;
import org.signal.core.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.database.MessageDatabase.MarkedMessageInfo;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.StoryType;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.notifications.v2.MessageNotifierV2;
@@ -86,7 +87,7 @@ public class RemoteReplyReceiver extends BroadcastReceiver {
expiresIn,
false,
0,
false,
StoryType.NONE,
null,
null,
Collections.emptyList(),

View File

@@ -14,8 +14,10 @@ import org.signal.core.util.ThreadUtil;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.TransportOption;
import org.thoughtcrime.securesms.TransportOptions;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.database.model.StoryType;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
@@ -30,6 +32,7 @@ import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.sms.OutgoingEncryptedMessage;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.util.MessageUtil;
import org.thoughtcrime.securesms.util.ParcelUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
@@ -170,7 +173,16 @@ public final class MultiShareSender {
List<OutgoingMediaMessage> outgoingMessages = new ArrayList<>();
StoryType storyType = StoryType.NONE;
if (isStory && recipient.isDistributionList()) {
storyType = SignalDatabase.distributionLists().getStoryType(recipient.requireDistributionListId());
}
if (isStory && slideDeck.getSlides().size() > 1) {
if (storyType == StoryType.NONE) {
storyType = StoryType.STORY_WITH_REPLIES;
}
for (final Slide slide : slideDeck.getSlides()) {
SlideDeck singletonDeck = new SlideDeck();
singletonDeck.addSlide(slide);
@@ -183,7 +195,7 @@ public final class MultiShareSender {
expiresIn,
isViewOnce,
ThreadDatabase.DistributionTypes.DEFAULT,
true,
storyType,
null,
null,
Collections.emptyList(),
@@ -206,7 +218,7 @@ public final class MultiShareSender {
expiresIn,
isViewOnce,
ThreadDatabase.DistributionTypes.DEFAULT,
isStory,
storyType,
null,
null,
Collections.emptyList(),

View File

@@ -26,4 +26,16 @@ class PrivateStorySettingsRepository {
SignalDatabase.distributionLists.deleteList(distributionListId)
}.subscribeOn(Schedulers.io())
}
fun getRepliesAndReactionsEnabled(distributionListId: DistributionListId): Single<Boolean> {
return Single.fromCallable {
SignalDatabase.distributionLists.getStoryType(distributionListId).isStoryWithReplies
}.subscribeOn(Schedulers.io())
}
fun setRepliesAndReactionsEnabled(distributionListId: DistributionListId, repliesAndReactionsEnabled: Boolean): Completable {
return Completable.fromAction {
SignalDatabase.distributionLists.setAllowsReplies(distributionListId, repliesAndReactionsEnabled)
}.subscribeOn(Schedulers.io())
}
}

View File

@@ -23,10 +23,15 @@ class PrivateStorySettingsViewModel(private val distributionListId: Distribution
}
fun refresh() {
disposables.clear()
disposables += repository.getRecord(distributionListId)
.subscribe { record ->
store.update { it.copy(privateStory = record) }
}
disposables += repository.getRepliesAndReactionsEnabled(distributionListId)
.subscribe { repliesAndReactionsEnabled ->
store.update { it.copy(areRepliesAndReactionsEnabled = repliesAndReactionsEnabled) }
}
}
fun getName(): String {
@@ -41,7 +46,9 @@ class PrivateStorySettingsViewModel(private val distributionListId: Distribution
}
fun setRepliesAndReactionsEnabled(repliesAndReactionsEnabled: Boolean) {
// TODO [stories] impl
disposables += repository.setRepliesAndReactionsEnabled(distributionListId, repliesAndReactionsEnabled)
.observeOn(AndroidSchedulers.mainThread())
.subscribe { refresh() }
}
fun delete(): Completable {

View File

@@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.stories.settings.my
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.thoughtcrime.securesms.database.SignalDatabase
@@ -9,7 +10,19 @@ class MyStorySettingsRepository {
fun getHiddenRecipientCount(): Single<Int> {
return Single.fromCallable {
SignalDatabase.distributionLists.getRawMemberCount(DistributionListId.from(DistributionListId.MY_STORY_ID))
SignalDatabase.distributionLists.getRawMemberCount(DistributionListId.MY_STORY)
}.subscribeOn(Schedulers.io())
}
fun getRepliesAndReactionsEnabled(): Single<Boolean> {
return Single.fromCallable {
SignalDatabase.distributionLists.getStoryType(DistributionListId.MY_STORY).isStoryWithReplies
}.subscribeOn(Schedulers.io())
}
fun setRepliesAndReactionsEnabled(repliesAndReactionsEnabled: Boolean): Completable {
return Completable.fromAction {
SignalDatabase.distributionLists.setAllowsReplies(DistributionListId.MY_STORY, repliesAndReactionsEnabled)
}.subscribeOn(Schedulers.io())
}
}

View File

@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.stories.settings.my
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
import org.thoughtcrime.securesms.util.livedata.Store
@@ -18,11 +19,17 @@ class MyStorySettingsViewModel(private val repository: MyStorySettingsRepository
}
fun refresh() {
disposables.clear()
disposables += repository.getHiddenRecipientCount()
.subscribe { count -> store.update { it.copy(hiddenStoryFromCount = count) } }
disposables += repository.getRepliesAndReactionsEnabled()
.subscribe { repliesAndReactionsEnabled -> store.update { it.copy(areRepliesAndReactionsEnabled = repliesAndReactionsEnabled) } }
}
fun setRepliesAndReactionsEnabled(repliesAndReactionsEnabled: Boolean) {
disposables += repository.setRepliesAndReactionsEnabled(repliesAndReactionsEnabled)
.observeOn(AndroidSchedulers.mainThread())
.subscribe { refresh() }
}
class Factory(private val repository: MyStorySettingsRepository) : ViewModelProvider.Factory {

View File

@@ -16,5 +16,6 @@ class StoryPost(
val replyCount: Int,
val dateInMilliseconds: Long,
val attachment: Attachment,
val conversationMessage: ConversationMessage
val conversationMessage: ConversationMessage,
val allowsReplies: Boolean
)

View File

@@ -70,7 +70,8 @@ class StoryViewerPageRepository(context: Context) {
replyCount = SignalDatabase.mms.getNumberOfStoryReplies(record.id),
dateInMilliseconds = record.dateSent,
attachment = (record as MmsMessageRecord).slideDeck.firstSlide!!.asAttachment(),
conversationMessage = ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(context, record)
conversationMessage = ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(context, record),
allowsReplies = record.storyType.isStoryWithReplies
)
emitter.onNext(story)

View File

@@ -146,7 +146,11 @@ class StoryViewerPageViewModel(
val isFromSelf = post.sender.isSelf
val isToGroup = post.group != null
return StoryViewerPageState.ReplyState.resolve(isFromSelf, isToGroup)
return when {
post.allowsReplies -> StoryViewerPageState.ReplyState.resolve(isFromSelf, isToGroup)
isFromSelf -> StoryViewerPageState.ReplyState.SELF
else -> StoryViewerPageState.ReplyState.NONE
}
}
fun getPostAt(index: Int): StoryPost {

View File

@@ -8,6 +8,7 @@ import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.ParentStoryId
import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage
import org.thoughtcrime.securesms.mms.QuoteModel
import org.thoughtcrime.securesms.recipients.Recipient
@@ -53,7 +54,7 @@ class StoryDirectReplyRepository {
0L,
false,
0,
false,
StoryType.NONE,
ParentStoryId.DirectReply(storyId),
QuoteModel(message.dateSent, quoteAuthor.id, "", false, message.slideDeck.asAttachments(), null),
emptyList(),

View File

@@ -6,6 +6,7 @@ import io.reactivex.rxjava3.schedulers.Schedulers
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.Mention
import org.thoughtcrime.securesms.database.model.ParentStoryId
import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage
import org.thoughtcrime.securesms.sms.MessageSender
@@ -30,7 +31,7 @@ object StoryGroupReplySender {
0L,
false,
0,
false,
StoryType.NONE,
ParentStoryId.GroupReply(message.id),
null,
emptyList(),

View File

@@ -13,6 +13,7 @@ import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types
import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.database.model.StoryViewState
import org.thoughtcrime.securesms.testing.TestDatabaseUtil
@@ -80,35 +81,35 @@ class MmsDatabaseTest {
@Test
fun `Given stories in database not in thread 1, when I getStoryViewState for thread 1, then I expect NONE`() {
TestMms.insert(db, threadId = 2, isStory = true)
TestMms.insert(db, threadId = 2, isStory = true)
TestMms.insert(db, threadId = 2, storyType = StoryType.STORY_WITH_REPLIES)
TestMms.insert(db, threadId = 2, storyType = StoryType.STORY_WITH_REPLIES)
assertEquals(StoryViewState.NONE, mmsDatabase.getStoryViewState(1))
}
@Test
fun `Given viewed incoming stories in database, when I getStoryViewState, then I expect VIEWED`() {
TestMms.insert(db, threadId = 1, isStory = true, viewed = true)
TestMms.insert(db, threadId = 1, isStory = true, viewed = true)
TestMms.insert(db, threadId = 1, storyType = StoryType.STORY_WITH_REPLIES, viewed = true)
TestMms.insert(db, threadId = 1, storyType = StoryType.STORY_WITH_REPLIES, viewed = true)
assertEquals(StoryViewState.VIEWED, mmsDatabase.getStoryViewState(1))
}
@Test
fun `Given unviewed incoming stories in database, when I getStoryViewState, then I expect UNVIEWED`() {
TestMms.insert(db, threadId = 1, isStory = true, viewed = false)
TestMms.insert(db, threadId = 1, isStory = true, viewed = false)
TestMms.insert(db, threadId = 1, storyType = StoryType.STORY_WITH_REPLIES, viewed = false)
TestMms.insert(db, threadId = 1, storyType = StoryType.STORY_WITH_REPLIES, viewed = false)
assertEquals(StoryViewState.UNVIEWED, mmsDatabase.getStoryViewState(1))
}
@Test
fun `Given mix of viewed and unviewed incoming stories in database, when I getStoryViewState, then I expect UNVIEWED`() {
TestMms.insert(db, threadId = 1, isStory = true, viewed = true)
TestMms.insert(db, threadId = 1, isStory = true, viewed = false)
TestMms.insert(db, threadId = 1, storyType = StoryType.STORY_WITH_REPLIES, viewed = true)
TestMms.insert(db, threadId = 1, storyType = StoryType.STORY_WITH_REPLIES, viewed = false)
assertEquals(StoryViewState.UNVIEWED, mmsDatabase.getStoryViewState(1))
}
@Test
fun `Given only outgoing story in database, when I getStoryViewState, then I expect VIEWED`() {
TestMms.insert(db, threadId = 1, isStory = true, type = Types.BASE_OUTBOX_TYPE)
TestMms.insert(db, threadId = 1, storyType = StoryType.STORY_WITH_REPLIES, type = Types.BASE_OUTBOX_TYPE)
assertEquals(StoryViewState.VIEWED, mmsDatabase.getStoryViewState(1))
}
}

View File

@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.database
import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase
import com.google.android.mms.pdu_alt.PduHeaders
import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage
import org.thoughtcrime.securesms.recipients.Recipient
@@ -25,7 +26,7 @@ object TestMms {
unread: Boolean = false,
viewed: Boolean = false,
threadId: Long = 1,
isStory: Boolean = false
storyType: StoryType = StoryType.NONE
): Long {
val message = OutgoingMediaMessage(
recipient,
@@ -36,7 +37,7 @@ object TestMms {
expiresIn,
viewOnce,
distributionType,
isStory,
storyType,
null,
null,
emptyList(),
@@ -83,7 +84,7 @@ object TestMms {
put(MmsSmsColumns.DELIVERY_RECEIPT_COUNT, 0)
put(MmsSmsColumns.RECEIPT_TIMESTAMP, 0)
put(MmsSmsColumns.VIEWED_RECEIPT_COUNT, if (viewed) 1 else 0)
put(MmsDatabase.IS_STORY, if (message.isStory) 1 else 0)
put(MmsDatabase.STORY_TYPE, message.storyType.code)
put(MmsSmsColumns.BODY, body)
put(MmsDatabase.PART_COUNT, 0)

View File

@@ -770,6 +770,8 @@ public class SignalServiceMessageSender {
builder.setTextAttachment(createTextAttachment(message.getTextAttachment().get()));
}
builder.setAllowsReplies(message.getAllowsReplies().or(true));
return container.setStoryMessage(builder).build();
}

View File

@@ -958,11 +958,13 @@ public final class SignalServiceContent {
if (content.hasFileAttachment()) {
return SignalServiceStoryMessage.forFileAttachment(profileKey,
createGroupV2Info(content),
createAttachmentPointer(content.getFileAttachment()));
createAttachmentPointer(content.getFileAttachment()),
content.getAllowsReplies());
} else {
return SignalServiceStoryMessage.forTextAttachment(profileKey,
createGroupV2Info(content),
createTextAttachment(content.getTextAttachment()));
createTextAttachment(content.getTextAttachment()),
content.getAllowsReplies());
}
}

View File

@@ -7,27 +7,32 @@ public class SignalServiceStoryMessage {
private final Optional<SignalServiceGroupV2> groupContext;
private final Optional<SignalServiceAttachment> fileAttachment;
private final Optional<SignalServiceTextAttachment> textAttachment;
private final Optional<Boolean> allowsReplies;
private SignalServiceStoryMessage(byte[] profileKey,
SignalServiceGroupV2 groupContext,
SignalServiceAttachment fileAttachment,
SignalServiceTextAttachment textAttachment) {
SignalServiceTextAttachment textAttachment,
boolean allowsReplies) {
this.profileKey = Optional.fromNullable(profileKey);
this.groupContext = Optional.fromNullable(groupContext);
this.fileAttachment = Optional.fromNullable(fileAttachment);
this.textAttachment = Optional.fromNullable(textAttachment);
this.allowsReplies = Optional.of(allowsReplies);
}
public static SignalServiceStoryMessage forFileAttachment(byte[] profileKey,
SignalServiceGroupV2 groupContext,
SignalServiceAttachment fileAttachment) {
return new SignalServiceStoryMessage(profileKey, groupContext, fileAttachment, null);
SignalServiceAttachment fileAttachment,
boolean allowsReplies) {
return new SignalServiceStoryMessage(profileKey, groupContext, fileAttachment, null, allowsReplies);
}
public static SignalServiceStoryMessage forTextAttachment(byte[] profileKey,
SignalServiceGroupV2 groupContext,
SignalServiceTextAttachment textAttachment) {
return new SignalServiceStoryMessage(profileKey, groupContext, null, textAttachment);
SignalServiceTextAttachment textAttachment,
boolean allowsReplies) {
return new SignalServiceStoryMessage(profileKey, groupContext, null, textAttachment, allowsReplies);
}
public Optional<byte[]> getProfileKey() {
@@ -45,4 +50,8 @@ public class SignalServiceStoryMessage {
public Optional<SignalServiceTextAttachment> getTextAttachment() {
return textAttachment;
}
public Optional<Boolean> getAllowsReplies() {
return allowsReplies;
}
}

View File

@@ -350,12 +350,13 @@ message TypingMessage {
}
message StoryMessage {
optional bytes profileKey = 1;
optional GroupContextV2 group = 2;
optional bytes profileKey = 1;
optional GroupContextV2 group = 2;
oneof attachment {
AttachmentPointer fileAttachment = 3;
TextAttachment textAttachment = 4;
}
optional bool allowsReplies = 5;
}
message Preview {