diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchData.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchData.kt index c7651fcc3..c057c0999 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchData.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchData.kt @@ -13,7 +13,11 @@ sealed class ContactSearchData(val contactSearchKey: ContactSearchKey) { * * Note that if the recipient is a group, it's participant list size is used instead of viewerCount. */ - data class Story(val recipient: Recipient, val viewerCount: Int, val privacyMode: DistributionListPrivacyMode) : ContactSearchData(ContactSearchKey.RecipientSearchKey.Story(recipient.id)) + data class Story( + val recipient: Recipient, + val viewerCount: Int, + val privacyMode: DistributionListPrivacyMode + ) : ContactSearchData(ContactSearchKey.RecipientSearchKey.Story(recipient.id)) /** * A row displaying a known recipient. diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSource.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSource.kt index 77989df6e..8d5892ce9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSource.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchPagedDataSource.kt @@ -261,7 +261,6 @@ class ContactSearchPagedDataSource( rhs.recipient.isMyStory -> 1 lhsActiveRank < rhsActiveRank -> -1 lhsActiveRank > rhsActiveRank -> 1 - lhsActiveRank == rhsActiveRank -> -1 else -> 0 } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchRepository.kt index 6a1c3a0cd..5cebb5916 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchRepository.kt @@ -1,16 +1,20 @@ package org.thoughtcrime.securesms.contacts.paged +import androidx.annotation.CheckResult import io.reactivex.rxjava3.core.Completable import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.schedulers.Schedulers +import org.thoughtcrime.securesms.database.GroupDatabase import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.model.DistributionListId import org.thoughtcrime.securesms.groups.GroupId import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.storage.StorageSyncHelper import org.thoughtcrime.securesms.stories.Stories class ContactSearchRepository { + @CheckResult fun filterOutUnselectableContactSearchKeys(contactSearchKeys: Set): Single> { return Single.fromCallable { contactSearchKeys.map { @@ -35,12 +39,25 @@ class ContactSearchRepository { } } - fun unmarkDisplayAsStory(groupId: GroupId): Completable { + @CheckResult + fun markDisplayAsStory(recipientIds: Collection): Completable { return Completable.fromAction { - SignalDatabase.groups.markDisplayAsStory(groupId, false) + SignalDatabase.groups.setShowAsStoryState(recipientIds, GroupDatabase.ShowAsStoryState.ALWAYS) + SignalDatabase.recipients.markNeedsSync(recipientIds) + StorageSyncHelper.scheduleSyncForDataChange() }.subscribeOn(Schedulers.io()) } + @CheckResult + fun unmarkDisplayAsStory(groupId: GroupId): Completable { + return Completable.fromAction { + SignalDatabase.groups.setShowAsStoryState(groupId, GroupDatabase.ShowAsStoryState.NEVER) + SignalDatabase.recipients.markNeedsSync(Recipient.externalGroupExact(groupId).id) + StorageSyncHelper.scheduleSyncForDataChange() + }.subscribeOn(Schedulers.io()) + } + + @CheckResult fun deletePrivateStory(distributionListId: DistributionListId): Completable { return Completable.fromAction { SignalDatabase.distributionLists.deleteList(distributionListId) diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchViewModel.kt index 9aaa25506..585996cc3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchViewModel.kt @@ -99,13 +99,15 @@ class ContactSearchViewModel( } fun addToVisibleGroupStories(groupStories: Set) { - configurationStore.update { state -> - state.copy( - groupStories = state.groupStories + groupStories.map { - val recipient = Recipient.resolved(it.recipientId) - ContactSearchData.Story(recipient, recipient.participantIds.size, DistributionListPrivacyMode.ALL) - } - ) + disposables += contactSearchRepository.markDisplayAsStory(groupStories.map { it.recipientId }).subscribe { + configurationStore.update { state -> + state.copy( + groupStories = state.groupStories + groupStories.map { + val recipient = Recipient.resolved(it.recipientId) + ContactSearchData.Story(recipient, recipient.participantIds.size, DistributionListPrivacyMode.ALL) + } + ) + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java index 7193725d6..7d3ac4cbe 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -14,6 +14,7 @@ import com.annimon.stream.Stream; import com.google.protobuf.InvalidProtocolBufferException; import org.signal.core.util.CursorUtil; +import org.signal.core.util.SQLiteDatabaseExtensionsKt; import org.signal.core.util.SetUtil; import org.signal.core.util.SqlUtil; import org.signal.core.util.logging.Log; @@ -37,6 +38,7 @@ import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; +import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil; import org.whispersystems.signalservice.api.groupsv2.GroupChangeReconstruct; @@ -45,6 +47,7 @@ import org.whispersystems.signalservice.api.push.ACI; import org.whispersystems.signalservice.api.push.DistributionId; import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.util.UuidUtil; +import org.whispersystems.signalservice.internal.push.exceptions.GroupNotFoundException; import java.io.Closeable; import java.security.SecureRandom; @@ -84,7 +87,7 @@ public class GroupDatabase extends Database { private static final String EXPECTED_V2_ID = "expected_v2_id"; private static final String UNMIGRATED_V1_MEMBERS = "former_v1_members"; private static final String DISTRIBUTION_ID = "distribution_id"; - private static final String DISPLAY_AS_STORY = "display_as_story"; + private static final String SHOW_AS_STORY_STATE = "display_as_story"; /** Was temporarily used for PNP accept by pni but is no longer needed/updated */ @Deprecated @@ -118,7 +121,7 @@ public class GroupDatabase extends Database { EXPECTED_V2_ID + " TEXT DEFAULT NULL, " + UNMIGRATED_V1_MEMBERS + " TEXT DEFAULT NULL, " + DISTRIBUTION_ID + " TEXT DEFAULT NULL, " + - DISPLAY_AS_STORY + " INTEGER DEFAULT 0, " + + SHOW_AS_STORY_STATE + " INTEGER DEFAULT 0, " + AUTH_SERVICE_ID + " TEXT DEFAULT NULL);"; public static final String[] CREATE_INDEXS = { @@ -1462,11 +1465,20 @@ public class GroupDatabase extends Database { } public @NonNull List getGroupsToDisplayAsStories() throws BadGroupIdException { - String[] selection = SqlUtil.buildArgs(GROUP_ID); - String where = DISPLAY_AS_STORY + " = ? AND " + ACTIVE + " = ?"; - String[] whereArgs = SqlUtil.buildArgs(1, 1); + String query = "SELECT " + GROUP_ID + ", (" + + "SELECT " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.DATE_RECEIVED + " FROM " + MmsDatabase.TABLE_NAME + + " WHERE " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.RECIPIENT_ID + " = " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.RECIPIENT_ID + + " AND " + MmsDatabase.STORY_TYPE + " > 1 ORDER BY " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.DATE_RECEIVED + " DESC LIMIT 1" + + ") as active_timestamp" + + " FROM " + TABLE_NAME + + " INNER JOIN " + ThreadDatabase.TABLE_NAME + " ON " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.RECIPIENT_ID + " = " + TABLE_NAME + "." + RECIPIENT_ID + + " WHERE " + ACTIVE + " = 1 " + + " AND (" + + SHOW_AS_STORY_STATE + " = " + ShowAsStoryState.ALWAYS.code + + " OR (" + SHOW_AS_STORY_STATE + " = " + ShowAsStoryState.IF_ACTIVE.code + " AND active_timestamp IS NOT NULL)" + + ") ORDER BY active_timestamp DESC"; - try (Cursor cursor = getReadableDatabase().query(TABLE_NAME, selection, where, whereArgs, null, null, null, null)) { + try (Cursor cursor = getReadableDatabase().query(query)) { if (cursor == null || cursor.getCount() == 0) { return Collections.emptyList(); } @@ -1480,17 +1492,43 @@ public class GroupDatabase extends Database { } } - public void markDisplayAsStory(@NonNull GroupId groupId) { - markDisplayAsStory(groupId, true); + public @NonNull ShowAsStoryState getShowAsStoryState(@NonNull GroupId groupId) { + String[] projection = SqlUtil.buildArgs(SHOW_AS_STORY_STATE); + String where = GROUP_ID + " = ?"; + String[] whereArgs = SqlUtil.buildArgs(groupId.toString()); + + try (Cursor cursor = getReadableDatabase().query(TABLE_NAME, projection, where, whereArgs, null, null, null)) { + if (!cursor.moveToFirst()) { + throw new AssertionError("Group does not exist."); + } + + int serializedState = CursorUtil.requireInt(cursor, SHOW_AS_STORY_STATE); + return ShowAsStoryState.deserialize(serializedState); + } } - public void markDisplayAsStory(@NonNull GroupId groupId, boolean displayAsStory) { + public void setShowAsStoryState(@NonNull GroupId groupId, @NonNull ShowAsStoryState showAsStoryState) { ContentValues contentValues = new ContentValues(1); - contentValues.put(DISPLAY_AS_STORY, displayAsStory); + contentValues.put(SHOW_AS_STORY_STATE, showAsStoryState.code); getWritableDatabase().update(TABLE_NAME, contentValues, GROUP_ID + " = ?", SqlUtil.buildArgs(groupId.toString())); } + public void setShowAsStoryState(@NonNull Collection recipientIds, @NonNull ShowAsStoryState showAsStoryState) { + ContentValues contentValues = new ContentValues(1); + List queries = SqlUtil.buildCollectionQuery(RECIPIENT_ID, recipientIds); + + contentValues.put(SHOW_AS_STORY_STATE, showAsStoryState.code); + + SQLiteDatabaseExtensionsKt.withinTransaction(getWritableDatabase(), db -> { + for (SqlUtil.Query query : queries) { + db.update(TABLE_NAME, contentValues, query.getWhere(), query.getWhereArgs()); + } + + return null; + }); + } + public enum MemberSet { FULL_MEMBERS_INCLUDING_SELF(true, false), FULL_MEMBERS_EXCLUDING_SELF(false, false), @@ -1506,6 +1544,45 @@ public class GroupDatabase extends Database { } } + /** + * State object describing whether or not to display a story in a list. + */ + public enum ShowAsStoryState { + /** + * The default value. Display the group as a story if the group has stories in it currently. + */ + IF_ACTIVE(0), + /** + * Always display the group as a story unless explicitly removed. This state is entered if the + * user sends a story to a group or otherwise explicitly selects it to appear. + */ + ALWAYS(1), + /** + * Never display the story as a group. This state is entered if the user removes the group from + * their list, and is only navigated away from if the user explicitly adds the group again. + */ + NEVER(2); + + private final int code; + + ShowAsStoryState(int code) { + this.code = code; + } + + private static @NonNull ShowAsStoryState deserialize(int code) { + switch (code) { + case 0: + return IF_ACTIVE; + case 1: + return ALWAYS; + case 2: + return NEVER; + default: + throw new IllegalArgumentException("Unknown code: " + code); + } + } + } + public enum MemberLevel { NOT_A_MEMBER(false), PENDING_MEMBER(false), diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt index 67f910f25..058fd2272 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt @@ -50,6 +50,7 @@ import org.thoughtcrime.securesms.conversation.colors.ChatColorsMapper.getChatCo import org.thoughtcrime.securesms.crypto.ProfileKeyUtil import org.thoughtcrime.securesms.database.GroupDatabase.LegacyGroupInsertException import org.thoughtcrime.securesms.database.GroupDatabase.MissedGroupMigrationInsertException +import org.thoughtcrime.securesms.database.GroupDatabase.ShowAsStoryState import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus import org.thoughtcrime.securesms.database.SignalDatabase.Companion.distributionLists import org.thoughtcrime.securesms.database.SignalDatabase.Companion.groups @@ -110,6 +111,7 @@ import org.whispersystems.signalservice.api.storage.SignalContactRecord import org.whispersystems.signalservice.api.storage.SignalGroupV1Record import org.whispersystems.signalservice.api.storage.SignalGroupV2Record import org.whispersystems.signalservice.api.storage.StorageId +import org.whispersystems.signalservice.internal.storage.protos.GroupV2Record import java.io.Closeable import java.io.IOException import java.util.Arrays @@ -1076,6 +1078,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : .build() ) + groups.setShowAsStoryState(groupId, insert.storySendMode.toShowAsStoryState()) updateExtras(recipient.id) { it.setHideStory(insert.shouldHideStory()) } @@ -1095,12 +1098,14 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : } val masterKey = update.old.masterKeyOrThrow - val recipient = Recipient.externalGroupExact(GroupId.v2(masterKey)) + val groupId = GroupId.v2(masterKey) + val recipient = Recipient.externalGroupExact(groupId) updateExtras(recipient.id) { it.setHideStory(update.new.shouldHideStory()) } + groups.setShowAsStoryState(groupId, update.new.storySendMode.toShowAsStoryState()) threads.applyStorageSyncUpdate(recipient.id, update.new) recipient.live().refresh() } @@ -1209,6 +1214,15 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : } } + private fun GroupV2Record.StorySendMode.toShowAsStoryState(): ShowAsStoryState { + return when (this) { + GroupV2Record.StorySendMode.DEFAULT -> ShowAsStoryState.IF_ACTIVE + GroupV2Record.StorySendMode.DISABLED -> ShowAsStoryState.NEVER + GroupV2Record.StorySendMode.ENABLED -> ShowAsStoryState.ALWAYS + GroupV2Record.StorySendMode.UNRECOGNIZED -> ShowAsStoryState.IF_ACTIVE + } + } + private fun getRecordForSync(query: String?, args: Array?): List { val table = """ 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 365a41c6a..089060ae2 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 @@ -239,10 +239,6 @@ class MediaSelectionRepository(context: Context) { val recipient = Recipient.resolved(contact.recipientId) val isStory = contact.isStory || recipient.isDistributionList - if (isStory && recipient.isActiveGroup && recipient.isGroup) { - SignalDatabase.groups.markDisplayAsStory(recipient.requireGroupId()) - } - if (isStory && !recipient.isMyStory) { SignalStore.storyValues().setLatestStorySend(StorySend.newSend(recipient)) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/send/TextStoryPostSendRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/send/TextStoryPostSendRepository.kt index 92e22921d..c957f7cda 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/send/TextStoryPostSendRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/send/TextStoryPostSendRepository.kt @@ -68,10 +68,6 @@ class TextStoryPostSendRepository { val recipient = Recipient.resolved(contact.requireShareContact().recipientId.get()) val isStory = contact is ContactSearchKey.RecipientSearchKey.Story || recipient.isDistributionList - if (isStory && recipient.isActiveGroup && recipient.isGroup) { - SignalDatabase.groups.markDisplayAsStory(recipient.requireGroupId()) - } - if (isStory && !recipient.isMyStory) { SignalStore.storyValues().setLatestStorySend(StorySend.newSend(recipient)) } 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 cc8b17add..e00744075 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sharing/MultiShareSender.java +++ b/app/src/main/java/org/thoughtcrime/securesms/sharing/MultiShareSender.java @@ -21,6 +21,7 @@ import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey; import org.thoughtcrime.securesms.conversation.MessageSendType; import org.thoughtcrime.securesms.conversation.colors.ChatColors; import org.thoughtcrime.securesms.database.AttachmentDatabase; +import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.database.model.Mention; @@ -232,10 +233,6 @@ public final class MultiShareSender { storyType = StoryType.STORY_WITH_REPLIES; } - if (recipient.isActiveGroup() && recipient.isGroup()) { - SignalDatabase.groups().markDisplayAsStory(recipient.requireGroupId()); - } - if (!recipient.isMyStory()) { SignalStore.storyValues().setLatestStorySend(StorySend.newSend(recipient)); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/GroupV2RecordProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/storage/GroupV2RecordProcessor.java index 0745b822b..04b339cc3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/GroupV2RecordProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/GroupV2RecordProcessor.java @@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.groups.GroupsV1MigrationUtil; import org.thoughtcrime.securesms.recipients.RecipientId; import org.whispersystems.signalservice.api.storage.SignalGroupV2Record; +import org.whispersystems.signalservice.internal.storage.protos.GroupV2Record; import java.io.IOException; import java.util.Arrays; @@ -66,17 +67,18 @@ public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor< @Override @NonNull SignalGroupV2Record merge(@NonNull SignalGroupV2Record remote, @NonNull SignalGroupV2Record local, @NonNull StorageKeyGenerator keyGenerator) { - byte[] unknownFields = remote.serializeUnknownFields(); - boolean blocked = remote.isBlocked(); - boolean profileSharing = remote.isProfileSharingEnabled(); - boolean archived = remote.isArchived(); - boolean forcedUnread = remote.isForcedUnread(); - long muteUntil = remote.getMuteUntil(); - boolean notifyForMentionsWhenMuted = remote.notifyForMentionsWhenMuted(); - boolean hideStory = remote.shouldHideStory(); + byte[] unknownFields = remote.serializeUnknownFields(); + boolean blocked = remote.isBlocked(); + boolean profileSharing = remote.isProfileSharingEnabled(); + boolean archived = remote.isArchived(); + boolean forcedUnread = remote.isForcedUnread(); + long muteUntil = remote.getMuteUntil(); + boolean notifyForMentionsWhenMuted = remote.notifyForMentionsWhenMuted(); + boolean hideStory = remote.shouldHideStory(); + GroupV2Record.StorySendMode storySendMode = remote.getStorySendMode(); - boolean matchesRemote = doParamsMatch(remote, unknownFields, blocked, profileSharing, archived, forcedUnread, muteUntil, notifyForMentionsWhenMuted, hideStory); - boolean matchesLocal = doParamsMatch(local, unknownFields, blocked, profileSharing, archived, forcedUnread, muteUntil, notifyForMentionsWhenMuted, hideStory); + boolean matchesRemote = doParamsMatch(remote, unknownFields, blocked, profileSharing, archived, forcedUnread, muteUntil, notifyForMentionsWhenMuted, hideStory, storySendMode); + boolean matchesLocal = doParamsMatch(local, unknownFields, blocked, profileSharing, archived, forcedUnread, muteUntil, notifyForMentionsWhenMuted, hideStory, storySendMode); if (matchesRemote) { return remote; @@ -91,6 +93,7 @@ public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor< .setMuteUntil(muteUntil) .setNotifyForMentionsWhenMuted(notifyForMentionsWhenMuted) .setHideStory(hideStory) + .setStorySendMode(storySendMode) .build(); } } @@ -139,7 +142,8 @@ public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor< boolean forcedUnread, long muteUntil, boolean notifyForMentionsWhenMuted, - boolean hideStory) + boolean hideStory, + @NonNull GroupV2Record.StorySendMode storySendMode) { return Arrays.equals(unknownFields, group.serializeUnknownFields()) && blocked == group.isBlocked() && @@ -148,6 +152,7 @@ public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor< forcedUnread == group.isForcedUnread() && muteUntil == group.getMuteUntil() && notifyForMentionsWhenMuted == group.notifyForMentionsWhenMuted() && - hideStory == group.shouldHideStory(); + hideStory == group.shouldHideStory() && + storySendMode == group.getStorySendMode(); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncModels.java b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncModels.java index e80756658..66051d605 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncModels.java +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncModels.java @@ -6,6 +6,7 @@ import androidx.annotation.Nullable; import com.annimon.stream.Stream; import org.signal.libsignal.zkgroup.groups.GroupMasterKey; +import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.IdentityDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.database.SignalDatabase; @@ -31,6 +32,7 @@ import org.whispersystems.signalservice.api.subscriptions.SubscriberId; import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.internal.storage.protos.AccountRecord; import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState; +import org.whispersystems.signalservice.internal.storage.protos.GroupV2Record; import java.util.Collections; import java.util.List; @@ -163,7 +165,20 @@ public final class StorageSyncModels { throw new AssertionError("Group master key not on recipient record"); } - boolean hideStory = recipient.getExtras() != null && recipient.getExtras().hideStory(); + boolean hideStory = recipient.getExtras() != null && recipient.getExtras().hideStory(); + GroupDatabase.ShowAsStoryState showAsStoryState = SignalDatabase.groups().getShowAsStoryState(groupId); + GroupV2Record.StorySendMode storySendMode; + + switch (showAsStoryState) { + case ALWAYS: + storySendMode = GroupV2Record.StorySendMode.ENABLED; + break; + case NEVER: + storySendMode = GroupV2Record.StorySendMode.DISABLED; + break; + default: + storySendMode = GroupV2Record.StorySendMode.DEFAULT; + } return new SignalGroupV2Record.Builder(rawStorageId, groupMasterKey, recipient.getSyncExtras().getStorageProto()) .setBlocked(recipient.isBlocked()) @@ -173,6 +188,7 @@ public final class StorageSyncModels { .setMuteUntil(recipient.getMuteUntil()) .setNotifyForMentionsWhenMuted(recipient.getMentionSetting() == RecipientDatabase.MentionSetting.ALWAYS_NOTIFY) .setHideStory(hideStory) + .setStorySendMode(storySendMode) .build(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/group/GroupStorySettingsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/group/GroupStorySettingsRepository.kt index 2507b64a7..99d534e06 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/group/GroupStorySettingsRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/group/GroupStorySettingsRepository.kt @@ -3,13 +3,18 @@ package org.thoughtcrime.securesms.stories.settings.group import io.reactivex.rxjava3.core.Completable import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.schedulers.Schedulers +import org.thoughtcrime.securesms.database.GroupDatabase import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.groups.GroupId +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.storage.StorageSyncHelper class GroupStorySettingsRepository { fun unmarkAsGroupStory(groupId: GroupId): Completable { return Completable.fromAction { - SignalDatabase.groups.markDisplayAsStory(groupId, false) + SignalDatabase.groups.setShowAsStoryState(groupId, GroupDatabase.ShowAsStoryState.NEVER) + SignalDatabase.recipients.markNeedsSync(Recipient.externalGroupExact(groupId).id) + StorageSyncHelper.scheduleSyncForDataChange() }.subscribeOn(Schedulers.io()) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsRepository.kt index 9acd109c0..ca97f0616 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsRepository.kt @@ -2,18 +2,20 @@ package org.thoughtcrime.securesms.stories.settings.story import io.reactivex.rxjava3.core.Completable import io.reactivex.rxjava3.schedulers.Schedulers +import org.thoughtcrime.securesms.database.GroupDatabase import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.storage.StorageSyncHelper import org.thoughtcrime.securesms.stories.Stories class StoriesPrivacySettingsRepository { fun markGroupsAsStories(groups: List): Completable { return Completable.fromCallable { - groups - .map { Recipient.resolved(it) } - .forEach { SignalDatabase.groups.markDisplayAsStory(it.requireGroupId()) } + SignalDatabase.groups.setShowAsStoryState(groups, GroupDatabase.ShowAsStoryState.ALWAYS) + SignalDatabase.recipients.markNeedsSync(groups) + StorageSyncHelper.scheduleSyncForDataChange() } } diff --git a/core-util/src/main/java/org/signal/core/util/SqlUtil.kt b/core-util/src/main/java/org/signal/core/util/SqlUtil.kt index ef27c5eaa..3af308683 100644 --- a/core-util/src/main/java/org/signal/core/util/SqlUtil.kt +++ b/core-util/src/main/java/org/signal/core/util/SqlUtil.kt @@ -1,12 +1,9 @@ package org.signal.core.util -import androidx.sqlite.db.SupportSQLiteDatabase import android.content.ContentValues import android.text.TextUtils import androidx.annotation.VisibleForTesting -import java.lang.NullPointerException -import java.lang.StringBuilder -import java.util.ArrayList +import androidx.sqlite.db.SupportSQLiteDatabase import java.util.LinkedList import java.util.Locale import java.util.stream.Collectors diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalGroupV2Record.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalGroupV2Record.java index 3b23ed6d3..9e057b7e1 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalGroupV2Record.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalGroupV2Record.java @@ -82,6 +82,10 @@ public final class SignalGroupV2Record implements SignalRecord { diff.add("HideStory"); } + if (!Objects.equals(this.getStorySendMode(), that.getStorySendMode())) { + diff.add("StorySendMode"); + } + if (!Objects.equals(this.hasUnknownFields(), that.hasUnknownFields())) { diff.add("UnknownFields"); } @@ -140,6 +144,10 @@ public final class SignalGroupV2Record implements SignalRecord { return proto.getHideStory(); } + public GroupV2Record.StorySendMode getStorySendMode() { + return proto.getStorySendMode(); + } + public GroupV2Record toProto() { return proto; } @@ -213,6 +221,11 @@ public final class SignalGroupV2Record implements SignalRecord { return this; } + public Builder setStorySendMode(GroupV2Record.StorySendMode storySendMode) { + builder.setStorySendMode(storySendMode); + return this; + } + private static GroupV2Record.Builder parseUnknowns(byte[] serializedUnknowns) { try { return GroupV2Record.parseFrom(serializedUnknowns).toBuilder(); diff --git a/libsignal/service/src/main/proto/StorageService.proto b/libsignal/service/src/main/proto/StorageService.proto index 238d2dbd3..ed0dad6ef 100644 --- a/libsignal/service/src/main/proto/StorageService.proto +++ b/libsignal/service/src/main/proto/StorageService.proto @@ -100,14 +100,22 @@ message GroupV1Record { } message GroupV2Record { - bytes masterKey = 1; - bool blocked = 2; - bool whitelisted = 3; - bool archived = 4; - bool markedUnread = 5; - uint64 mutedUntilTimestamp = 6; - bool dontNotifyForMentionsIfMuted = 7; - bool hideStory = 8; + enum StorySendMode { + DEFAULT = 0; + DISABLED = 1; + ENABLED = 2; + } + + bytes masterKey = 1; + bool blocked = 2; + bool whitelisted = 3; + bool archived = 4; + bool markedUnread = 5; + uint64 mutedUntilTimestamp = 6; + bool dontNotifyForMentionsIfMuted = 7; + bool hideStory = 8; + reserved /* storySendEnabled */ 9; + StorySendMode storySendMode = 10; } message Payments {