From 35cd36e9feea46ba20e7a030603028e2301b0415 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Tue, 1 Mar 2022 12:41:45 -0400 Subject: [PATCH] Implement support for 'allows replies' toggle. --- .../database/DistributionListDatabaseTest.kt | 50 +++++++++++- .../ConversationParentFragment.java | 7 +- .../database/DistributionListDatabase.kt | 30 ++++++- .../securesms/database/MmsDatabase.java | 81 +++++++++---------- .../securesms/database/MmsSmsDatabase.java | 20 ++--- .../helpers/SignalDatabaseMigrations.kt | 7 +- .../model/DistributionListPartialRecord.kt | 3 +- .../database/model/DistributionListRecord.kt | 1 + .../database/model/MediaMmsMessageRecord.java | 10 ++- .../database/model/MmsMessageRecord.java | 8 +- .../model/NotificationMmsMessageRecord.java | 4 +- .../securesms/database/model/StoryType.kt | 36 +++++++++ .../jobs/PushDistributionListSendJob.java | 6 +- .../securesms/jobs/PushGroupSendJob.java | 7 +- .../securesms/jobs/RefreshAttributesJob.java | 2 +- .../mediasend/MediaSendActivityResult.java | 23 +++--- .../mediasend/v2/MediaSelectionRepository.kt | 23 +++++- .../messages/MessageContentProcessor.java | 25 ++++-- .../securesms/mms/IncomingMediaMessage.kt | 7 +- .../mms/OutgoingExpirationUpdateMessage.java | 5 +- .../mms/OutgoingGroupUpdateMessage.java | 3 +- .../securesms/mms/OutgoingMediaMessage.java | 19 ++--- .../mms/OutgoingSecureMediaMessage.java | 7 +- .../notifications/RemoteReplyReceiver.java | 3 +- .../securesms/sharing/MultiShareSender.java | 16 +++- .../custom/PrivateStorySettingsRepository.kt | 12 +++ .../custom/PrivateStorySettingsViewModel.kt | 9 ++- .../settings/my/MyStorySettingsRepository.kt | 15 +++- .../settings/my/MyStorySettingsViewModel.kt | 7 ++ .../stories/viewer/page/StoryPost.kt | 3 +- .../viewer/page/StoryViewerPageRepository.kt | 3 +- .../viewer/page/StoryViewerPageViewModel.kt | 6 +- .../direct/StoryDirectReplyRepository.kt | 3 +- .../reply/group/StoryGroupReplySender.kt | 3 +- .../securesms/database/MmsDatabaseTest.kt | 19 ++--- .../securesms/database/TestMms.kt | 7 +- .../api/SignalServiceMessageSender.java | 2 + .../api/messages/SignalServiceContent.java | 6 +- .../messages/SignalServiceStoryMessage.java | 19 +++-- .../src/main/proto/SignalService.proto | 5 +- 40 files changed, 374 insertions(+), 148 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/model/StoryType.kt diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/DistributionListDatabaseTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/database/DistributionListDatabaseTest.kt index 5407da2b66..40f7625e2e 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/DistributionListDatabaseTest.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/database/DistributionListDatabaseTest.kt @@ -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())) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java index 71d669d9a8..6bbf08915d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java @@ -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 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 future = new SettableFuture<>(); final Context context = requireContext().getApplicationContext(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/DistributionListDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/DistributionListDatabase.kt index 363205e536..2b4c57146c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/DistributionListDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/DistributionListDatabase.kt @@ -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 { 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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java index b884a8fd86..c3af11243e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -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 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 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 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 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 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 getMismatchedIdentities(String document) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java index ae627258ff..da5a917059 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -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 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; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt index 3f1ae5e2ab..83635b94b3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/DistributionListPartialRecord.kt b/app/src/main/java/org/thoughtcrime/securesms/database/model/DistributionListPartialRecord.kt index 5add270a56..06283263a4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/DistributionListPartialRecord.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/DistributionListPartialRecord.kt @@ -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 ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/DistributionListRecord.kt b/app/src/main/java/org/thoughtcrime/securesms/database/model/DistributionListRecord.kt index 8323d0cbd8..73af27788d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/DistributionListRecord.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/DistributionListRecord.kt @@ -10,5 +10,6 @@ data class DistributionListRecord( val id: DistributionListId, val name: String, val distributionId: DistributionId, + val allowsReplies: Boolean, val members: List ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java index d4b0d9f536..452be5c513 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java @@ -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 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 updateContacts(@NonNull List contacts, @NonNull Map attachmentIdMap) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java index 4c45a3e10a..3e45d8eaf7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java @@ -22,6 +22,7 @@ public abstract class MmsMessageRecord extends MessageRecord { private final @Nullable Quote quote; private final @NonNull List contacts = new LinkedList<>(); private final @NonNull List 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 contacts, @NonNull List linkPreviews, boolean unidentified, @NonNull List 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(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java index af51018e70..1513b7cdce 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java @@ -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; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/StoryType.kt b/app/src/main/java/org/thoughtcrime/securesms/database/model/StoryType.kt new file mode 100644 index 0000000000..e09ca3452b --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/StoryType.kt @@ -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 + } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDistributionListSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDistributionListSendJob.java index 7d992ee291..6ba3f060b8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDistributionListSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDistributionListSendJob.java @@ -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 existingNetworkFailures = message.getNetworkFailures(); Set 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); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java index de013b37cd..ca60b4fe23 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java @@ -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 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()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java index 4f898c8d73..f4c66ffe8e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java @@ -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(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivityResult.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivityResult.java index e0f1cc9093..32cc088f01 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivityResult.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivityResult.java @@ -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 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 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 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 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 CREATOR = new Creator() { @@ -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()); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionRepository.kt index 2c239fc2ff..7b5eabe984 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionRepository.kt @@ -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(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java index b3d344a4b1..57d86d20d7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java @@ -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(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/IncomingMediaMessage.kt b/app/src/main/java/org/thoughtcrime/securesms/mms/IncomingMediaMessage.kt index 654d454554..0ed07d511d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/IncomingMediaMessage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/IncomingMediaMessage.kt @@ -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, diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingExpirationUpdateMessage.java b/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingExpirationUpdateMessage.java index 2cb179dfa1..8b41cfd9de 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingExpirationUpdateMessage.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingExpirationUpdateMessage.java @@ -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(), + new LinkedList<>(), sentTimeMillis, ThreadDatabase.DistributionTypes.CONVERSATION, expiresIn, false, - false, + StoryType.NONE, null, null, Collections.emptyList(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingGroupUpdateMessage.java b/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingGroupUpdateMessage.java index c05bcd4604..29560abc1e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingGroupUpdateMessage.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingGroupUpdateMessage.java @@ -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, diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingMediaMessage.java b/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingMediaMessage.java index 40b34515fd..275fe12faa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingMediaMessage.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingMediaMessage.java @@ -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 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 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 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() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingSecureMediaMessage.java b/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingSecureMediaMessage.java index de81b58108..a5d994b155 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingSecureMediaMessage.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingSecureMediaMessage.java @@ -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 contacts, @NonNull List previews, @NonNull List 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(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java index 33fb8ea549..c5c0748263 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java @@ -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(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/sharing/MultiShareSender.java b/app/src/main/java/org/thoughtcrime/securesms/sharing/MultiShareSender.java index 41de138695..f153e7e358 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sharing/MultiShareSender.java +++ b/app/src/main/java/org/thoughtcrime/securesms/sharing/MultiShareSender.java @@ -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 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(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/custom/PrivateStorySettingsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/custom/PrivateStorySettingsRepository.kt index 747df52db5..a465a73f03 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/custom/PrivateStorySettingsRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/custom/PrivateStorySettingsRepository.kt @@ -26,4 +26,16 @@ class PrivateStorySettingsRepository { SignalDatabase.distributionLists.deleteList(distributionListId) }.subscribeOn(Schedulers.io()) } + + fun getRepliesAndReactionsEnabled(distributionListId: DistributionListId): Single { + 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()) + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/custom/PrivateStorySettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/custom/PrivateStorySettingsViewModel.kt index 83a784c667..054e78d491 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/custom/PrivateStorySettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/custom/PrivateStorySettingsViewModel.kt @@ -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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/my/MyStorySettingsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/my/MyStorySettingsRepository.kt index 5b5ce4f5db..59f31c52e0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/my/MyStorySettingsRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/my/MyStorySettingsRepository.kt @@ -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 { return Single.fromCallable { - SignalDatabase.distributionLists.getRawMemberCount(DistributionListId.from(DistributionListId.MY_STORY_ID)) + SignalDatabase.distributionLists.getRawMemberCount(DistributionListId.MY_STORY) + }.subscribeOn(Schedulers.io()) + } + + fun getRepliesAndReactionsEnabled(): Single { + 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()) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/my/MyStorySettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/my/MyStorySettingsViewModel.kt index 4419ab8ca0..eeeea61b49 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/my/MyStorySettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/my/MyStorySettingsViewModel.kt @@ -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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryPost.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryPost.kt index 5edd18624d..d243648db2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryPost.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryPost.kt @@ -16,5 +16,6 @@ class StoryPost( val replyCount: Int, val dateInMilliseconds: Long, val attachment: Attachment, - val conversationMessage: ConversationMessage + val conversationMessage: ConversationMessage, + val allowsReplies: Boolean ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageRepository.kt index 47a552f724..84ccc8640f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageRepository.kt @@ -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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageViewModel.kt index 8eeae989c1..64b871ceae 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageViewModel.kt @@ -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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/direct/StoryDirectReplyRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/direct/StoryDirectReplyRepository.kt index 0d6448d06c..90594246e1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/direct/StoryDirectReplyRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/direct/StoryDirectReplyRepository.kt @@ -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(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplySender.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplySender.kt index 183f7c4c8b..4d10143406 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplySender.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplySender.kt @@ -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(), diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/MmsDatabaseTest.kt b/app/src/test/java/org/thoughtcrime/securesms/database/MmsDatabaseTest.kt index 99b759fcf6..877427562d 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/database/MmsDatabaseTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/MmsDatabaseTest.kt @@ -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)) } } diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/TestMms.kt b/app/src/test/java/org/thoughtcrime/securesms/database/TestMms.kt index ce9fb7771e..2aff540d95 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/database/TestMms.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/TestMms.kt @@ -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) diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java index eb92888ef5..ef85c32af6 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java @@ -770,6 +770,8 @@ public class SignalServiceMessageSender { builder.setTextAttachment(createTextAttachment(message.getTextAttachment().get())); } + builder.setAllowsReplies(message.getAllowsReplies().or(true)); + return container.setStoryMessage(builder).build(); } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java index 4aea352036..6f83da0ca3 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java @@ -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()); } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceStoryMessage.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceStoryMessage.java index 55a0450271..42d41da7d6 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceStoryMessage.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceStoryMessage.java @@ -7,27 +7,32 @@ public class SignalServiceStoryMessage { private final Optional groupContext; private final Optional fileAttachment; private final Optional textAttachment; + private final Optional 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 getProfileKey() { @@ -45,4 +50,8 @@ public class SignalServiceStoryMessage { public Optional getTextAttachment() { return textAttachment; } + + public Optional getAllowsReplies() { + return allowsReplies; + } } diff --git a/libsignal/service/src/main/proto/SignalService.proto b/libsignal/service/src/main/proto/SignalService.proto index 972b324ca3..b9500c824e 100644 --- a/libsignal/service/src/main/proto/SignalService.proto +++ b/libsignal/service/src/main/proto/SignalService.proto @@ -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 {