diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4f5e6d02ad..df33d2b0e3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -406,7 +406,14 @@ android:screenOrientation="portrait" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" android:theme="@style/TextSecure.DarkNoActionBar.StoryViewer" - android:windowSoftInputMode="stateAlwaysHidden|adjustNothing" /> + android:launchMode="singleTask" + android:windowSoftInputMode="stateAlwaysHidden|adjustNothing" + android:parentActivityName=".MainActivity"> + + + CREATOR = new Creator() { + @Override + public BlurHash createFromParcel(Parcel in) { + return new BlurHash(in); + } + + @Override + public BlurHash[] newArray(int size) { + return new BlurHash[size]; + } + }; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsRepository.kt index e582a1d536..45a625d5d8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsRepository.kt @@ -11,7 +11,7 @@ import org.thoughtcrime.securesms.emoji.EmojiFiles import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob import org.thoughtcrime.securesms.jobs.CreateReleaseChannelJob import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.thoughtcrime.securesms.notifications.v2.NotificationThread +import org.thoughtcrime.securesms.notifications.v2.ConversationId import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.releasechannel.ReleaseChannel @@ -54,7 +54,7 @@ class InternalSettingsRepository(context: Context) { SignalDatabase.attachments.getAttachmentsForMessage(insertResult.messageId) .forEach { ApplicationDependencies.getJobManager().add(AttachmentDownloadJob(insertResult.messageId, it.attachmentId, false)) } - ApplicationDependencies.getMessageNotifier().updateNotification(context, NotificationThread.forConversation(insertResult.threadId)) + ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(insertResult.threadId)) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt index 5f42ec3123..9e9b56b143 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt @@ -76,6 +76,7 @@ import org.thoughtcrime.securesms.recipients.RecipientExporter import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment import org.thoughtcrime.securesms.stories.Stories +import org.thoughtcrime.securesms.stories.StoryViewerArgs import org.thoughtcrime.securesms.stories.dialogs.StoryDialogs import org.thoughtcrime.securesms.stories.viewer.StoryViewerActivity import org.thoughtcrime.securesms.util.CommunicationActions @@ -275,7 +276,13 @@ class ConversationSettingsFragment : DSLSettingsFragment( val viewAvatarTransitionBundle = AvatarPreviewActivity.createTransitionBundle(requireActivity(), avatar) if (Stories.isFeatureEnabled() && avatar.hasStory()) { - val viewStoryIntent = StoryViewerActivity.createIntent(requireContext(), state.recipient.id) + val viewStoryIntent = StoryViewerActivity.createIntent( + requireContext(), + StoryViewerArgs( + recipientId = state.recipient.id, + isInHiddenStoryMode = state.recipient.shouldHideStory() + ) + ) StoryDialogs.displayStoryOrProfileImage( context = requireContext(), onViewStory = { startActivity(viewStoryIntent) }, diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscovery.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscovery.kt index 2b27db7f21..8354ecd5d5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscovery.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscovery.kt @@ -20,7 +20,7 @@ import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.notifications.NotificationChannels -import org.thoughtcrime.securesms.notifications.v2.NotificationThread +import org.thoughtcrime.securesms.notifications.v2.ConversationId import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter import org.thoughtcrime.securesms.profiles.ProfileName @@ -215,7 +215,7 @@ object ContactDiscovery { .forEach { result -> val hour = Calendar.getInstance()[Calendar.HOUR_OF_DAY] if (hour in 9..22) { - ApplicationDependencies.getMessageNotifier().updateNotification(context, NotificationThread.forConversation(result.threadId), true) + ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(result.threadId), true) } else { Log.i(TAG, "Not notifying of a new user due to the time of day. (Hour: $hour)") } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java index e58463b5e4..b0216278ff 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java @@ -141,6 +141,7 @@ import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.TextSlide; import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile; +import org.thoughtcrime.securesms.notifications.v2.ConversationId; import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter; import org.thoughtcrime.securesms.providers.BlobProvider; @@ -157,6 +158,7 @@ import org.thoughtcrime.securesms.sms.MessageSender; import org.thoughtcrime.securesms.sms.OutgoingTextMessage; import org.thoughtcrime.securesms.stickers.StickerLocator; import org.thoughtcrime.securesms.stickers.StickerPackPreviewActivity; +import org.thoughtcrime.securesms.stories.StoryViewerArgs; import org.thoughtcrime.securesms.stories.viewer.StoryViewerActivity; import org.thoughtcrime.securesms.util.CachedInflater; import org.thoughtcrime.securesms.util.CommunicationActions; @@ -670,7 +672,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect this.recipient = Recipient.live(conversationViewModel.getArgs().getRecipientId()); this.threadId = conversationViewModel.getArgs().getThreadId(); - this.markReadHelper = new MarkReadHelper(threadId, requireContext(), getViewLifecycleOwner()); + this.markReadHelper = new MarkReadHelper(ConversationId.forConversation(threadId), requireContext(), getViewLifecycleOwner()); conversationViewModel.onConversationDataAvailable(recipient.getId(), threadId, startingPosition); messageCountsViewModel.setThreadId(threadId); @@ -917,7 +919,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect snapToTopDataObserver.requestScrollPosition(0); conversationViewModel.onConversationDataAvailable(recipient.getId(), threadId, -1); messageCountsViewModel.setThreadId(threadId); - markReadHelper = new MarkReadHelper(threadId, requireContext(), getViewLifecycleOwner()); + markReadHelper = new MarkReadHelper(ConversationId.forConversation(threadId), requireContext(), getViewLifecycleOwner()); initializeListAdapter(); initializeTypingObserver(); } @@ -1628,15 +1630,9 @@ public class ConversationFragment extends LoggingFragment implements Multiselect if (messageRecord.getParentStoryId() != null) { startActivity(StoryViewerActivity.createIntent( requireContext(), - messageRecord.getQuote().getAuthor(), - messageRecord.getParentStoryId().asMessageId().getId(), - Recipient.resolved(messageRecord.getQuote().getAuthor()).shouldHideStory(), - null, - null, - null, - Collections.emptyList() - )); - + new StoryViewerArgs.Builder(messageRecord.getQuote().getAuthor(), Recipient.resolved(messageRecord.getQuote().getAuthor()).shouldHideStory()) + .withStoryId(messageRecord.getParentStoryId().asMessageId().getId()) + .build())); return; } 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 7f59666e86..373c557bc8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java @@ -240,7 +240,7 @@ import org.thoughtcrime.securesms.mms.SlideFactory.MediaType; import org.thoughtcrime.securesms.mms.StickerSlide; import org.thoughtcrime.securesms.mms.VideoSlide; import org.thoughtcrime.securesms.notifications.NotificationChannels; -import org.thoughtcrime.securesms.notifications.v2.NotificationThread; +import org.thoughtcrime.securesms.notifications.v2.ConversationId; import org.thoughtcrime.securesms.payments.CanNotSendPaymentDialog; import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.profiles.spoofing.ReviewBannerView; @@ -267,6 +267,7 @@ import org.thoughtcrime.securesms.stickers.StickerLocator; import org.thoughtcrime.securesms.stickers.StickerManagementActivity; import org.thoughtcrime.securesms.stickers.StickerPackInstallEvent; import org.thoughtcrime.securesms.stickers.StickerSearchRepository; +import org.thoughtcrime.securesms.stories.StoryViewerArgs; import org.thoughtcrime.securesms.stories.viewer.StoryViewerActivity; import org.thoughtcrime.securesms.util.AsynchronousCallback; import org.thoughtcrime.securesms.util.Base64; @@ -885,7 +886,7 @@ public class ConversationParentFragment extends Fragment private void setVisibleThread(long threadId) { if (!isInBubble()) { // TODO [alex] LargeScreenSupport -- Inform MainActivityViewModel that the conversation was opened. - ApplicationDependencies.getMessageNotifier().setVisibleThread(NotificationThread.forConversation(threadId)); + ApplicationDependencies.getMessageNotifier().setVisibleThread(ConversationId.forConversation(threadId)); } } @@ -1249,7 +1250,10 @@ public class ConversationParentFragment extends Fragment } private void handleStoryRingClick() { - startActivity(StoryViewerActivity.createIntent(requireContext(), recipient.getId(), -1L, recipient.get().shouldHideStory(), null, null, null, Collections.emptyList())); + startActivity(StoryViewerActivity.createIntent( + requireContext(), + new StoryViewerArgs.Builder(recipient.getId(), recipient.get().shouldHideStory()) + .build())); } private void handleConversationSettings() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/MarkReadHelper.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/MarkReadHelper.java index 2b8fb6b4d1..e316215843 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/MarkReadHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/MarkReadHelper.java @@ -13,26 +13,27 @@ import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.notifications.MarkReadReceiver; +import org.thoughtcrime.securesms.notifications.v2.ConversationId; import org.thoughtcrime.securesms.util.Debouncer; import org.thoughtcrime.securesms.util.concurrent.SerialMonoLifoExecutor; import java.util.List; import java.util.concurrent.Executor; -class MarkReadHelper { +public class MarkReadHelper { private static final String TAG = Log.tag(MarkReadHelper.class); private static final long DEBOUNCE_TIMEOUT = 100; private static final Executor EXECUTOR = new SerialMonoLifoExecutor(SignalExecutors.BOUNDED); - private final long threadId; + private final ConversationId conversationId; private final Context context; - private final LifecycleOwner lifecycleOwner; - private final Debouncer debouncer = new Debouncer(DEBOUNCE_TIMEOUT); - private long latestTimestamp; + private final LifecycleOwner lifecycleOwner; + private final Debouncer debouncer = new Debouncer(DEBOUNCE_TIMEOUT); + private long latestTimestamp; - MarkReadHelper(long threadId, @NonNull Context context, @NonNull LifecycleOwner lifecycleOwner) { - this.threadId = threadId; + public MarkReadHelper(@NonNull ConversationId conversationId, @NonNull Context context, @NonNull LifecycleOwner lifecycleOwner) { + this.conversationId = conversationId; this.context = context.getApplicationContext(); this.lifecycleOwner = lifecycleOwner; } @@ -47,7 +48,7 @@ class MarkReadHelper { debouncer.publish(() -> { EXECUTOR.execute(() -> { ThreadDatabase threadDatabase = SignalDatabase.threads(); - List infos = threadDatabase.setReadSince(threadId, false, timestamp); + List infos = threadDatabase.setReadSince(conversationId, false, timestamp); Log.d(TAG, "Marking " + infos.size() + " messages as read."); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java index c2476f3392..bf1ef65c76 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java @@ -197,6 +197,7 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns public abstract @NonNull List getUnreadStoryThreadRecipientIds(); public abstract boolean containsStories(long threadId); public abstract boolean hasSelfReplyInStory(long parentStoryId); + public abstract boolean hasSelfReplyInGroupStory(long parentStoryId); public abstract @NonNull Cursor getStoryReplies(long parentStoryId); public abstract @Nullable Long getOldestStorySendTimestamp(); public abstract int deleteStoriesOlderThan(long timestamp); @@ -204,6 +205,7 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns public abstract @Nullable ParentStoryId.GroupReply getParentStoryIdForGroupReply(long messageId); public abstract void deleteGroupStoryReplies(long parentStoryId); public abstract boolean isOutgoingStoryAlreadyInDatabase(@NonNull RecipientId recipientId, long sentTimestamp); + public abstract @NonNull List setGroupStoryMessagesReadSince(long threadId, long groupStoryId, long sinceTimestamp); public abstract @NonNull StoryViewState getStoryViewState(@NonNull RecipientId recipientId); public abstract void updateViewedStories(@NonNull Set syncMessageIds); 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 ff41ed0795..b0705c5661 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -801,7 +801,7 @@ public class MmsDatabase extends MessageDatabase { String where = PARENT_STORY_ID + " = ?"; String[] whereArgs = SqlUtil.buildArgs(parentStoryId); - return rawQuery(where, whereArgs, true, 0); + return rawQuery(where, whereArgs, false, 0); } @Override @@ -840,6 +840,18 @@ public class MmsDatabase extends MessageDatabase { } } + @Override + public boolean hasSelfReplyInGroupStory(long parentStoryId) { + SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); + String[] columns = new String[]{"COUNT(*)"}; + String where = PARENT_STORY_ID + " = ? AND (" + getOutgoingTypeClause() + ")"; + String[] whereArgs = SqlUtil.buildArgs(parentStoryId); + + try (Cursor cursor = db.query(TABLE_NAME, columns, where, whereArgs, null, null, null, null)) { + return cursor != null && cursor.moveToNext() && cursor.getInt(0) > 0; + } + } + @Override public @Nullable Long getOldestStorySendTimestamp() { SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); @@ -1381,6 +1393,15 @@ public class MmsDatabase extends MessageDatabase { } } + @Override + public @NonNull List setGroupStoryMessagesReadSince(long threadId, long groupStoryId, long sinceTimestamp) { + if (sinceTimestamp == -1) { + return setMessagesRead(THREAD_ID + " = ? AND " + STORY_TYPE + " = 0 AND " + PARENT_STORY_ID + " = ? AND (" + READ + " = 0 OR (" + REACTIONS_UNREAD + " = 1 AND (" + getOutgoingTypeClause() + ")))", SqlUtil.buildArgs(threadId, groupStoryId)); + } else { + return setMessagesRead(THREAD_ID + " = ? AND " + STORY_TYPE + " = 0 AND " + PARENT_STORY_ID + " = ? AND (" + READ + " = 0 OR (" + REACTIONS_UNREAD + " = 1 AND ( " + getOutgoingTypeClause() + " ))) AND " + DATE_RECEIVED + " <= ?", SqlUtil.buildArgs(threadId, groupStoryId, sinceTimestamp)); + } + } + @Override public List setEntireThreadRead(long threadId) { return setMessagesRead(THREAD_ID + " = ? AND " + STORY_TYPE + " = 0 AND " + PARENT_STORY_ID + " <= 0", new String[] { String.valueOf(threadId)}); 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 6100af9d0f..537e8dfd73 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -32,7 +32,6 @@ import org.signal.libsignal.protocol.util.Pair; import org.thoughtcrime.securesms.database.MessageDatabase.MessageUpdate; import org.thoughtcrime.securesms.database.MessageDatabase.SyncMessageId; import org.thoughtcrime.securesms.database.model.MessageRecord; -import org.thoughtcrime.securesms.database.model.StoryViewState; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.notifications.v2.MessageNotifierV2; import org.thoughtcrime.securesms.recipients.Recipient; @@ -259,12 +258,12 @@ public class MmsSmsDatabase extends Database { } stickyQuery.append("(") .append(MmsSmsColumns.THREAD_ID + " = ") - .append(stickyThread.getNotificationThread().getThreadId()) + .append(stickyThread.getConversationId().getThreadId()) .append(" AND ") .append(MmsSmsColumns.NORMALIZED_DATE_RECEIVED) .append(" >= ") .append(stickyThread.getEarliestTimestamp()) - .append(getStickyWherePartForParentStoryId(stickyThread.getNotificationThread().getGroupStoryId())) + .append(getStickyWherePartForParentStoryId(stickyThread.getConversationId().getGroupStoryId())) .append(")"); } @@ -654,6 +653,11 @@ public class MmsSmsDatabase extends Database { return SignalDatabase.sms().hasReceivedAnyCallsSince(threadId, timestamp); } + + public int getMessagePositionInConversation(long threadId, long receivedTimestamp) { + return getMessagePositionInConversation(threadId, 0, receivedTimestamp); + } + /** * Retrieves the position of the message with the provided timestamp in the query results you'd * get from calling {@link #getConversation(long)}. @@ -661,12 +665,24 @@ public class MmsSmsDatabase extends Database { * Note: This could give back incorrect results in the situation where multiple messages have the * same received timestamp. However, because this was designed to determine where to scroll to, * you'll still wind up in about the right spot. + * + * @param groupStoryId Ignored if passed value is <= 0 */ - public int getMessagePositionInConversation(long threadId, long receivedTimestamp) { - String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC"; - String selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + - MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " > " + receivedTimestamp + " AND " + - MmsDatabase.STORY_TYPE + " = 0 AND " + MmsDatabase.PARENT_STORY_ID + " <= 0"; + public int getMessagePositionInConversation(long threadId, long groupStoryId, long receivedTimestamp) { + final String order; + final String selection; + + if (groupStoryId > 0) { + order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " ASC"; + selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " < " + receivedTimestamp + " AND " + + MmsDatabase.STORY_TYPE + " = 0 AND " + MmsDatabase.PARENT_STORY_ID + " = " + groupStoryId; + } else { + order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC"; + selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " > " + receivedTimestamp + " AND " + + MmsDatabase.STORY_TYPE + " = 0 AND " + MmsDatabase.PARENT_STORY_ID + " <= 0"; + } try (Cursor cursor = queryTables(new String[]{ "COUNT(*)" }, selection, order, null, false)) { if (cursor != null && cursor.moveToFirst()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java index bf513ffeca..4d2519078d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -1446,6 +1446,11 @@ public class SmsDatabase extends MessageDatabase { throw new UnsupportedOperationException(); } + @Override + public boolean hasSelfReplyInGroupStory(long parentStoryId) { + throw new UnsupportedOperationException(); + } + @Override public @NonNull Cursor getStoryReplies(long parentStoryId) { throw new UnsupportedOperationException(); @@ -1481,6 +1486,11 @@ public class SmsDatabase extends MessageDatabase { throw new UnsupportedOperationException(); } + @Override + public @NonNull List setGroupStoryMessagesReadSince(long threadId, long groupStoryId, long sinceTimestamp) { + throw new UnsupportedOperationException(); + } + @Override public void deleteGroupStoryReplies(long parentStoryId) { throw new UnsupportedOperationException(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java index 8f6cd644ed..017121daa3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -31,6 +31,8 @@ import com.annimon.stream.Stream; import com.fasterxml.jackson.annotation.JsonProperty; import org.jsoup.helper.StringUtil; +import org.signal.core.util.CursorUtil; +import org.signal.core.util.SqlUtil; import org.signal.core.util.logging.Log; import org.signal.libsignal.zkgroup.InvalidInputException; import org.signal.libsignal.zkgroup.groups.GroupMasterKey; @@ -46,15 +48,14 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.mms.StickerSlide; +import org.thoughtcrime.securesms.notifications.v2.ConversationId; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientDetails; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.util.ConversationUtil; -import org.signal.core.util.CursorUtil; import org.thoughtcrime.securesms.util.JsonUtils; -import org.signal.core.util.SqlUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.signalservice.api.push.ServiceId; @@ -402,6 +403,22 @@ public class ThreadDatabase extends Database { return setReadSince(Collections.singletonMap(threadId, -1L), lastSeen); } + public List setRead(@NonNull ConversationId conversationId, boolean lastSeen) { + if (conversationId.getGroupStoryId() == null) { + return setRead(conversationId.getThreadId(), lastSeen); + } else { + return setGroupStoryReadSince(conversationId.getThreadId(), conversationId.getGroupStoryId(), System.currentTimeMillis()); + } + } + + public List setReadSince(@NonNull ConversationId conversationId, boolean lastSeen, long sinceTimestamp) { + if (conversationId.getGroupStoryId() != null) { + return setGroupStoryReadSince(conversationId.getThreadId(), conversationId.getGroupStoryId(), sinceTimestamp); + } else { + return setReadSince(conversationId.getThreadId(), lastSeen, sinceTimestamp); + } + } + public List setReadSince(long threadId, boolean lastSeen, long sinceTimestamp) { return setReadSince(Collections.singletonMap(threadId, sinceTimestamp), lastSeen); } @@ -410,6 +427,10 @@ public class ThreadDatabase extends Database { return setReadSince(Stream.of(threadIds).collect(Collectors.toMap(t -> t, t -> -1L)), lastSeen); } + public List setGroupStoryReadSince(long threadId, long groupStoryId, long sinceTimestamp) { + return SignalDatabase.mms().setGroupStoryMessagesReadSince(threadId, groupStoryId, sinceTimestamp); + } + public List setReadSince(Map threadIdToSinceTimestamp, boolean lastSeen) { SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); @@ -1252,7 +1273,7 @@ public class ThreadDatabase extends Database { pinnedPosition++; } - + db.setTransactionSuccessful(); } finally { db.endTransaction(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupV1MessageProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupV1MessageProcessor.java index 7e6b95b30e..b2a898dd02 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupV1MessageProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupV1MessageProcessor.java @@ -19,7 +19,7 @@ import org.thoughtcrime.securesms.jobs.AvatarGroupsV1DownloadJob; import org.thoughtcrime.securesms.jobs.PushGroupUpdateJob; import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.OutgoingGroupUpdateMessage; -import org.thoughtcrime.securesms.notifications.v2.NotificationThread; +import org.thoughtcrime.securesms.notifications.v2.ConversationId; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.sms.IncomingGroupUpdateMessage; @@ -255,7 +255,7 @@ public final class GroupV1MessageProcessor { Optional insertResult = smsDatabase.insertMessageInbox(groupMessage); if (insertResult.isPresent()) { - ApplicationDependencies.getMessageNotifier().updateNotification(context, NotificationThread.forConversation(insertResult.get().getThreadId())); + ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(insertResult.get().getThreadId())); return insertResult.get().getThreadId(); } else { return null; diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java index 3ba675445d..f81465fec6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java @@ -22,7 +22,7 @@ import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.JobLogger; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.mms.MmsException; -import org.thoughtcrime.securesms.notifications.v2.NotificationThread; +import org.thoughtcrime.securesms.notifications.v2.ConversationId; import org.thoughtcrime.securesms.releasechannel.ReleaseChannel; import org.thoughtcrime.securesms.s3.S3; import org.thoughtcrime.securesms.transport.RetryLaterException; @@ -120,7 +120,7 @@ public final class AttachmentDownloadJob extends BaseJob { doWork(); if (!SignalDatabase.mms().isStory(messageId)) { - ApplicationDependencies.getMessageNotifier().updateNotification(context, NotificationThread.forConversation(0)); + ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(0)); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AutomaticSessionResetJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/AutomaticSessionResetJob.java index dee161789a..81382da3d4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AutomaticSessionResetJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AutomaticSessionResetJob.java @@ -11,7 +11,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.impl.DecryptionsDrainedConstraint; -import org.thoughtcrime.securesms.notifications.v2.NotificationThread; +import org.thoughtcrime.securesms.notifications.v2.ConversationId; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; @@ -122,7 +122,7 @@ public class AutomaticSessionResetJob extends BaseJob { private void insertLocalMessage() { MessageDatabase.InsertResult result = SignalDatabase.sms().insertChatSessionRefreshedMessage(recipientId, deviceId, sentTimestamp); - ApplicationDependencies.getMessageNotifier().updateNotification(context, NotificationThread.forConversation(result.getThreadId())); + ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(result.getThreadId())); } private void sendNullMessage() throws IOException { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java index 8c62ee7f0b..62b4bf8462 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java @@ -32,7 +32,7 @@ import org.thoughtcrime.securesms.mms.IncomingMediaMessage; import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.MmsRadioException; import org.thoughtcrime.securesms.mms.PartParser; -import org.thoughtcrime.securesms.notifications.v2.NotificationThread; +import org.thoughtcrime.securesms.notifications.v2.ConversationId; import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; @@ -175,7 +175,7 @@ public class MmsDownloadJob extends BaseJob { if (automatic) { database.markIncomingNotificationReceived(threadId); - ApplicationDependencies.getMessageNotifier().updateNotification(context, NotificationThread.forConversation(threadId)); + ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(threadId)); } } @@ -255,7 +255,7 @@ public class MmsDownloadJob extends BaseJob { if (insertResult.isPresent()) { database.deleteMessage(messageId); - ApplicationDependencies.getMessageNotifier().updateNotification(context, NotificationThread.forConversation(insertResult.get().getThreadId())); + ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(insertResult.get().getThreadId())); } } @@ -267,7 +267,7 @@ public class MmsDownloadJob extends BaseJob { if (automatic) { db.markIncomingNotificationReceived(threadId); - ApplicationDependencies.getMessageNotifier().updateNotification(context, NotificationThread.forConversation(threadId)); + ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(threadId)); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsSendJob.java index 3736cda932..e05f237223 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsSendJob.java @@ -45,7 +45,7 @@ import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.MmsSendResult; import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; import org.thoughtcrime.securesms.mms.PartAuthority; -import org.thoughtcrime.securesms.notifications.v2.NotificationThread; +import org.thoughtcrime.securesms.notifications.v2.ConversationId; import org.thoughtcrime.securesms.phonenumbers.NumberUtil; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException; @@ -348,7 +348,7 @@ public final class MmsSendJob extends SendJob { Recipient recipient = SignalDatabase.threads().getRecipientForThreadId(threadId); if (recipient != null) { - ApplicationDependencies.getMessageNotifier().notifyMessageDeliveryFailed(context, recipient, NotificationThread.forConversation(threadId)); + ApplicationDependencies.getMessageNotifier().notifyMessageDeliveryFailed(context, recipient, ConversationId.forConversation(threadId)); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java index 3a96efeb7b..d145b429ae 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java @@ -44,7 +44,7 @@ import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.mms.QuoteModel; import org.thoughtcrime.securesms.net.NotPushRegisteredException; -import org.thoughtcrime.securesms.notifications.v2.NotificationThread; +import org.thoughtcrime.securesms.notifications.v2.ConversationId; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; @@ -296,7 +296,7 @@ public abstract class PushSendJob extends SendJob { ParentStoryId.GroupReply groupReplyStoryId = SignalDatabase.mms().getParentStoryIdForGroupReply(messageId); if (threadId != -1 && recipient != null) { - ApplicationDependencies.getMessageNotifier().notifyMessageDeliveryFailed(context, recipient, NotificationThread.fromThreadAndReply(threadId, groupReplyStoryId)); + ApplicationDependencies.getMessageNotifier().notifyMessageDeliveryFailed(context, recipient, ConversationId.fromThreadAndReply(threadId, groupReplyStoryId)); } } @@ -515,7 +515,7 @@ public abstract class PushSendJob extends SendJob { if (recipient != null) { ParentStoryId.GroupReply groupReply = SignalDatabase.mms().getParentStoryIdForGroupReply(messageId); - ApplicationDependencies.getMessageNotifier().notifyProofRequired(context, recipient, NotificationThread.fromThreadAndReply(threadId, groupReply)); + ApplicationDependencies.getMessageNotifier().notifyProofRequired(context, recipient, ConversationId.fromThreadAndReply(threadId, groupReply)); } else { Log.w(TAG, "[Proof Required] No recipient! Couldn't notify."); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java index 562c2bf0f9..44c85a9dd3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java @@ -15,7 +15,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.keyvalue.SignalStore; -import org.thoughtcrime.securesms.notifications.v2.NotificationThread; +import org.thoughtcrime.securesms.notifications.v2.ConversationId; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; @@ -127,7 +127,7 @@ public class PushTextSendJob extends PushSendJob { } catch (InsecureFallbackApprovalException e) { warn(TAG, String.valueOf(record.getDateSent()), "Failure", e); database.markAsPendingInsecureSmsFallback(record.getId()); - ApplicationDependencies.getMessageNotifier().notifyMessageDeliveryFailed(context, record.getRecipient(), NotificationThread.forConversation(record.getThreadId())); + ApplicationDependencies.getMessageNotifier().notifyMessageDeliveryFailed(context, record.getRecipient(), ConversationId.forConversation(record.getThreadId())); ApplicationDependencies.getJobManager().add(new DirectoryRefreshJob(false)); } catch (UntrustedIdentityException e) { warn(TAG, String.valueOf(record.getDateSent()), "Failure", e); @@ -157,7 +157,7 @@ public class PushTextSendJob extends PushSendJob { Recipient recipient = SignalDatabase.threads().getRecipientForThreadId(threadId); if (threadId != -1 && recipient != null) { - ApplicationDependencies.getMessageNotifier().notifyMessageDeliveryFailed(context, recipient, NotificationThread.forConversation(threadId)); + ApplicationDependencies.getMessageNotifier().notifyMessageDeliveryFailed(context, recipient, ConversationId.forConversation(threadId)); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveRemoteAnnouncementsJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveRemoteAnnouncementsJob.kt index 822acf6f65..4b0c3c7fd8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveRemoteAnnouncementsJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveRemoteAnnouncementsJob.kt @@ -19,7 +19,7 @@ import org.thoughtcrime.securesms.jobmanager.Data import org.thoughtcrime.securesms.jobmanager.Job import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.thoughtcrime.securesms.notifications.v2.NotificationThread +import org.thoughtcrime.securesms.notifications.v2.ConversationId import org.thoughtcrime.securesms.providers.BlobProvider import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.releasechannel.ReleaseChannel @@ -214,7 +214,7 @@ class RetrieveRemoteAnnouncementsJob private constructor(private val force: Bool SignalDatabase.attachments.getAttachmentsForMessage(insertResult.messageId) .forEach { ApplicationDependencies.getJobManager().add(AttachmentDownloadJob(insertResult.messageId, it.attachmentId, false)) } - ApplicationDependencies.getMessageNotifier().updateNotification(context, NotificationThread.forConversation(insertResult.threadId)) + ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(insertResult.threadId)) TrimThreadJob.enqueueAsync(insertResult.threadId) highestVersion = max(highestVersion, note.releaseNote.androidMinVersion!!.toInt()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SmsReceiveJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SmsReceiveJob.java index 8e636fe419..e1ab737601 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SmsReceiveJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SmsReceiveJob.java @@ -26,7 +26,7 @@ import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraint; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.notifications.NotificationIds; -import org.thoughtcrime.securesms.notifications.v2.NotificationThread; +import org.thoughtcrime.securesms.notifications.v2.ConversationId; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.service.VerificationCodeParser; import org.thoughtcrime.securesms.sms.IncomingTextMessage; @@ -124,7 +124,7 @@ public class SmsReceiveJob extends BaseJob { Optional insertResult = storeMessage(message.get()); if (insertResult.isPresent()) { - ApplicationDependencies.getMessageNotifier().updateNotification(context, NotificationThread.forConversation(insertResult.get().getThreadId())); + ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(insertResult.get().getThreadId())); } } else if (message.isPresent()) { Log.w(TAG, "Received an SMS from a blocked user. Ignoring."); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SmsSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SmsSendJob.java index 20826f3486..fa3305452b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SmsSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SmsSendJob.java @@ -20,7 +20,7 @@ import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.impl.NetworkOrCellServiceConstraint; import org.thoughtcrime.securesms.keyvalue.SignalStore; -import org.thoughtcrime.securesms.notifications.v2.NotificationThread; +import org.thoughtcrime.securesms.notifications.v2.ConversationId; import org.thoughtcrime.securesms.phonenumbers.NumberUtil; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.service.SmsDeliveryListener; @@ -98,7 +98,7 @@ public class SmsSendJob extends SendJob { } catch (UndeliverableMessageException ude) { warn(TAG, ude); SignalDatabase.sms().markAsSentFailed(record.getId()); - ApplicationDependencies.getMessageNotifier().notifyMessageDeliveryFailed(context, record.getRecipient(), NotificationThread.fromMessageRecord(record)); + ApplicationDependencies.getMessageNotifier().notifyMessageDeliveryFailed(context, record.getRecipient(), ConversationId.fromMessageRecord(record)); } } @@ -116,7 +116,7 @@ public class SmsSendJob extends SendJob { SignalDatabase.sms().markAsSentFailed(messageId); if (threadId != -1 && recipient != null) { - ApplicationDependencies.getMessageNotifier().notifyMessageDeliveryFailed(context, recipient, NotificationThread.forConversation(threadId)); + ApplicationDependencies.getMessageNotifier().notifyMessageDeliveryFailed(context, recipient, ConversationId.forConversation(threadId)); } else { Log.w(TAG, "Could not find message! threadId: " + threadId + ", recipient: " + (recipient != null ? recipient.getId().toString() : "null")); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SmsSentJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SmsSentJob.java index 23dcc2a2d3..334bd56797 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SmsSentJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SmsSentJob.java @@ -13,7 +13,7 @@ import org.thoughtcrime.securesms.database.model.SmsMessageRecord; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; -import org.thoughtcrime.securesms.notifications.v2.NotificationThread; +import org.thoughtcrime.securesms.notifications.v2.ConversationId; import org.thoughtcrime.securesms.service.SmsDeliveryListener; public class SmsSentJob extends BaseJob { @@ -109,7 +109,7 @@ public class SmsSentJob extends BaseJob { if (isMultipart) { Log.w(TAG, "Service connectivity problem, but not retrying due to multipart"); database.markAsSentFailed(messageId); - ApplicationDependencies.getMessageNotifier().notifyMessageDeliveryFailed(context, record.getRecipient(), NotificationThread.forConversation(record.getThreadId())); + ApplicationDependencies.getMessageNotifier().notifyMessageDeliveryFailed(context, record.getRecipient(), ConversationId.forConversation(record.getThreadId())); } else { Log.w(TAG, "Service connectivity problem, requeuing..."); ApplicationDependencies.getJobManager().add(new SmsSendJob(messageId, record.getIndividualRecipient(), runAttempt + 1)); @@ -117,7 +117,7 @@ public class SmsSentJob extends BaseJob { break; default: database.markAsSentFailed(messageId); - ApplicationDependencies.getMessageNotifier().notifyMessageDeliveryFailed(context, record.getRecipient(), NotificationThread.forConversation(record.getThreadId())); + ApplicationDependencies.getMessageNotifier().notifyMessageDeliveryFailed(context, record.getRecipient(), ConversationId.forConversation(record.getThreadId())); } } catch (NoSuchMessageException e) { Log.w(TAG, e); 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 26262911c8..dd568c3179 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java @@ -112,7 +112,7 @@ import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.mms.StickerSlide; import org.thoughtcrime.securesms.notifications.MarkReadReceiver; import org.thoughtcrime.securesms.notifications.MessageNotifier; -import org.thoughtcrime.securesms.notifications.v2.NotificationThread; +import org.thoughtcrime.securesms.notifications.v2.ConversationId; import org.thoughtcrime.securesms.payments.MobileCoinPublicAddress; import org.thoughtcrime.securesms.ratelimit.RateLimitUtil; import org.thoughtcrime.securesms.recipients.Recipient; @@ -411,7 +411,7 @@ public final class MessageContentProcessor { if (threadId != null) { ThreadDatabase.ConversationMetadata metadata = SignalDatabase.threads().getConversationMetadata(threadId); - long visibleThread = ApplicationDependencies.getMessageNotifier().getVisibleThread().map(NotificationThread::getThreadId).orElse(-1L); + long visibleThread = ApplicationDependencies.getMessageNotifier().getVisibleThread().map(ConversationId::getThreadId).orElse(-1L); if (threadId != visibleThread && metadata.getLastSeen() > 0 && metadata.getLastSeen() < pending.getReceivedTimestamp()) { receivedTime = pending.getReceivedTimestamp(); @@ -754,7 +754,7 @@ public final class MessageContentProcessor { ApplicationDependencies.getProtocolStore().aci().deleteAllSessions(content.getSender().getIdentifier()); SecurityEvent.broadcastSecurityUpdateEvent(context); - ApplicationDependencies.getMessageNotifier().updateNotification(context, NotificationThread.forConversation(insertResult.get().getThreadId())); + ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(insertResult.get().getThreadId())); return new MessageId(insertResult.get().getMessageId(), true); } else { @@ -982,7 +982,7 @@ public final class MessageContentProcessor { } else { ReactionRecord reactionRecord = new ReactionRecord(reaction.getEmoji(), senderRecipient.getId(), message.getTimestamp(), System.currentTimeMillis()); SignalDatabase.reactions().addReaction(targetMessageId, reactionRecord); - ApplicationDependencies.getMessageNotifier().updateNotification(context, NotificationThread.fromMessageRecord(targetMessage), false); + ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.fromMessageRecord(targetMessage), false); } return new MessageId(targetMessage.getId(), targetMessage.isMms()); @@ -998,7 +998,7 @@ public final class MessageContentProcessor { if (targetMessage != null && RemoteDeleteUtil.isValidReceive(targetMessage, senderRecipient, content.getServerReceivedTimestamp())) { MessageDatabase db = targetMessage.isMms() ? SignalDatabase.mms() : SignalDatabase.sms(); db.markAsRemoteDelete(targetMessage.getId()); - ApplicationDependencies.getMessageNotifier().updateNotification(context, NotificationThread.fromMessageRecord(targetMessage), false); + ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.fromMessageRecord(targetMessage), false); return new MessageId(targetMessage.getId(), targetMessage.isMms()); } else if (targetMessage == null) { warn(String.valueOf(content.getTimestamp()), "[handleRemoteDelete] Could not find matching message! timestamp: " + delete.getTargetSentTimestamp() + " author: " + senderRecipient.getId()); @@ -1613,6 +1613,14 @@ public final class MessageContentProcessor { if (insertResult.isPresent()) { database.setTransactionSuccessful(); + + if (parentStoryId.isGroupReply()) { + ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.fromThreadAndReply(insertResult.get().getThreadId(), (ParentStoryId.GroupReply) parentStoryId)); + } else { + ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(insertResult.get().getThreadId())); + TrimThreadJob.enqueueAsync(insertResult.get().getThreadId()); + } + if (parentStoryId.isDirectReply()) { return MessageId.fromNullable(insertResult.get().getMessageId(), true); } else { @@ -1704,6 +1712,14 @@ public final class MessageContentProcessor { if (insertResult.isPresent()) { database.setTransactionSuccessful(); + + if (parentStoryId.isGroupReply()) { + ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.fromThreadAndReply(insertResult.get().getThreadId(), (ParentStoryId.GroupReply) parentStoryId)); + } else { + ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(insertResult.get().getThreadId())); + TrimThreadJob.enqueueAsync(insertResult.get().getThreadId()); + } + if (parentStoryId.isDirectReply()) { return MessageId.fromNullable(insertResult.get().getMessageId(), true); } else { @@ -1776,7 +1792,7 @@ public final class MessageContentProcessor { } if (insertResult.isPresent()) { - ApplicationDependencies.getMessageNotifier().updateNotification(context, NotificationThread.forConversation(insertResult.get().getThreadId())); + ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(insertResult.get().getThreadId())); TrimThreadJob.enqueueAsync(insertResult.get().getThreadId()); return new MessageId(insertResult.get().getMessageId(), true); @@ -1860,7 +1876,7 @@ public final class MessageContentProcessor { ApplicationDependencies.getJobManager().add(new AttachmentDownloadJob(insertResult.get().getMessageId(), attachment.getAttachmentId(), false)); } - ApplicationDependencies.getMessageNotifier().updateNotification(context, NotificationThread.forConversation(insertResult.get().getThreadId())); + ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(insertResult.get().getThreadId())); TrimThreadJob.enqueueAsync(insertResult.get().getThreadId()); if (message.isViewOnce()) { @@ -2329,7 +2345,7 @@ public final class MessageContentProcessor { } if (insertResult.isPresent()) { - ApplicationDependencies.getMessageNotifier().updateNotification(context, NotificationThread.forConversation(insertResult.get().getThreadId())); + ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(insertResult.get().getThreadId())); return new MessageId(insertResult.get().getMessageId(), false); } else { return null; @@ -2416,7 +2432,7 @@ public final class MessageContentProcessor { if (insertResult.isPresent()) { smsDatabase.markAsInvalidVersionKeyExchange(insertResult.get().getMessageId()); - ApplicationDependencies.getMessageNotifier().updateNotification(context, NotificationThread.forConversation(insertResult.get().getThreadId())); + ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(insertResult.get().getThreadId())); } } else { smsDatabase.markAsInvalidVersionKeyExchange(smsMessageId.get()); @@ -2435,7 +2451,7 @@ public final class MessageContentProcessor { if (insertResult.isPresent()) { smsDatabase.markAsDecryptFailed(insertResult.get().getMessageId()); - ApplicationDependencies.getMessageNotifier().updateNotification(context, NotificationThread.forConversation(insertResult.get().getThreadId())); + ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(insertResult.get().getThreadId())); } } else { smsDatabase.markAsDecryptFailed(smsMessageId.get()); @@ -2457,7 +2473,7 @@ public final class MessageContentProcessor { if (insertResult.isPresent()) { smsDatabase.markAsUnsupportedProtocolVersion(insertResult.get().getMessageId()); - ApplicationDependencies.getMessageNotifier().updateNotification(context, NotificationThread.forConversation(insertResult.get().getThreadId())); + ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(insertResult.get().getThreadId())); } } else { smsDatabase.markAsNoSession(smsMessageId.get()); @@ -2479,7 +2495,7 @@ public final class MessageContentProcessor { if (insertResult.isPresent()) { smsDatabase.markAsInvalidMessage(insertResult.get().getMessageId()); - ApplicationDependencies.getMessageNotifier().updateNotification(context, NotificationThread.forConversation(insertResult.get().getThreadId())); + ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(insertResult.get().getThreadId())); } } else { smsDatabase.markAsNoSession(smsMessageId.get()); @@ -2498,7 +2514,7 @@ public final class MessageContentProcessor { if (insertResult.isPresent()) { smsDatabase.markAsLegacyVersion(insertResult.get().getMessageId()); - ApplicationDependencies.getMessageNotifier().updateNotification(context, NotificationThread.forConversation(insertResult.get().getThreadId())); + ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(insertResult.get().getThreadId())); } } else { smsDatabase.markAsLegacyVersion(smsMessageId.get()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/DeleteNotificationReceiver.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/DeleteNotificationReceiver.java index c0f9c6eef6..d2522f486a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/DeleteNotificationReceiver.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/DeleteNotificationReceiver.java @@ -8,7 +8,7 @@ import android.content.Intent; import org.signal.core.util.concurrent.SignalExecutors; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; -import org.thoughtcrime.securesms.notifications.v2.NotificationThread; +import org.thoughtcrime.securesms.notifications.v2.ConversationId; import java.util.ArrayList; @@ -27,11 +27,11 @@ public class DeleteNotificationReceiver extends BroadcastReceiver { notifier.clearReminder(context); final long[] ids = intent.getLongArrayExtra(EXTRA_IDS); - final boolean[] mms = intent.getBooleanArrayExtra(EXTRA_MMS); - final ArrayList threads = intent.getParcelableArrayListExtra(EXTRA_THREADS); + final boolean[] mms = intent.getBooleanArrayExtra(EXTRA_MMS); + final ArrayList threads = intent.getParcelableArrayListExtra(EXTRA_THREADS); if (threads != null) { - for (NotificationThread thread : threads) { + for (ConversationId thread : threads) { notifier.removeStickyThread(thread); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java index c8789f18b9..8239eec74b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java @@ -19,7 +19,7 @@ import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobs.MultiDeviceReadUpdateJob; import org.thoughtcrime.securesms.jobs.SendReadReceiptJob; -import org.thoughtcrime.securesms.notifications.v2.NotificationThread; +import org.thoughtcrime.securesms.notifications.v2.ConversationId; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.service.ExpiringMessageManager; @@ -41,11 +41,11 @@ public class MarkReadReceiver extends BroadcastReceiver { if (!CLEAR_ACTION.equals(intent.getAction())) return; - final ArrayList threads = intent.getParcelableArrayListExtra(THREADS_EXTRA); + final ArrayList threads = intent.getParcelableArrayListExtra(THREADS_EXTRA); if (threads != null) { MessageNotifier notifier = ApplicationDependencies.getMessageNotifier(); - for (NotificationThread thread : threads) { + for (ConversationId thread : threads) { notifier.removeStickyThread(thread); } @@ -55,9 +55,9 @@ public class MarkReadReceiver extends BroadcastReceiver { SignalExecutors.BOUNDED.execute(() -> { List messageIdsCollection = new LinkedList<>(); - for (NotificationThread thread : threads) { + for (ConversationId thread : threads) { Log.i(TAG, "Marking as read: " + thread); - List messageIds = SignalDatabase.threads().setRead(thread.getThreadId(), true); + List messageIds = SignalDatabase.threads().setRead(thread, true); messageIdsCollection.addAll(messageIds); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/MessageNotifier.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/MessageNotifier.java index 5e1b4a10a6..ffa98a5aaf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/MessageNotifier.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/MessageNotifier.java @@ -9,28 +9,28 @@ import androidx.annotation.Nullable; import org.signal.core.util.concurrent.SignalExecutors; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; -import org.thoughtcrime.securesms.notifications.v2.NotificationThread; +import org.thoughtcrime.securesms.notifications.v2.ConversationId; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.util.BubbleUtil; import java.util.Optional; public interface MessageNotifier { - void setVisibleThread(@Nullable NotificationThread notificationThread); - @NonNull Optional getVisibleThread(); + void setVisibleThread(@Nullable ConversationId conversationId); + @NonNull Optional getVisibleThread(); void clearVisibleThread(); void setLastDesktopActivityTimestamp(long timestamp); - void notifyMessageDeliveryFailed(@NonNull Context context, @NonNull Recipient recipient, @NonNull NotificationThread notificationThread); - void notifyProofRequired(@NonNull Context context, @NonNull Recipient recipient, @NonNull NotificationThread notificationThread); + void notifyMessageDeliveryFailed(@NonNull Context context, @NonNull Recipient recipient, @NonNull ConversationId conversationId); + void notifyProofRequired(@NonNull Context context, @NonNull Recipient recipient, @NonNull ConversationId conversationId); void cancelDelayedNotifications(); void updateNotification(@NonNull Context context); - void updateNotification(@NonNull Context context, @NonNull NotificationThread notificationThread); - void updateNotification(@NonNull Context context, @NonNull NotificationThread notificationThread, @NonNull BubbleUtil.BubbleState defaultBubbleState); - void updateNotification(@NonNull Context context, @NonNull NotificationThread notificationThread, boolean signal); - void updateNotification(@NonNull Context context, @Nullable NotificationThread notificationThread, boolean signal, int reminderCount, @NonNull BubbleUtil.BubbleState defaultBubbleState); + void updateNotification(@NonNull Context context, @NonNull ConversationId conversationId); + void updateNotification(@NonNull Context context, @NonNull ConversationId conversationId, @NonNull BubbleUtil.BubbleState defaultBubbleState); + void updateNotification(@NonNull Context context, @NonNull ConversationId conversationId, boolean signal); + void updateNotification(@NonNull Context context, @Nullable ConversationId conversationId, boolean signal, int reminderCount, @NonNull BubbleUtil.BubbleState defaultBubbleState); void clearReminder(@NonNull Context context); - void addStickyThread(@NonNull NotificationThread notificationThread, long earliestTimestamp); - void removeStickyThread(@NonNull NotificationThread notificationThread); + void addStickyThread(@NonNull ConversationId conversationId, long earliestTimestamp); + void removeStickyThread(@NonNull ConversationId conversationId); class ReminderReceiver extends BroadcastReceiver { diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationCancellationHelper.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationCancellationHelper.java index db7db3452e..f63aa021c5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationCancellationHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationCancellationHelper.java @@ -15,7 +15,7 @@ import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.notifications.v2.MessageNotifierV2; -import org.thoughtcrime.securesms.notifications.v2.NotificationThread; +import org.thoughtcrime.securesms.notifications.v2.ConversationId; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.BubbleUtil; import org.thoughtcrime.securesms.util.ConversationUtil; @@ -185,10 +185,10 @@ public final class NotificationCancellationHelper { return true; } - Long threadId = SignalDatabase.threads().getThreadIdFor(recipientId); - Optional focusedThread = ApplicationDependencies.getMessageNotifier().getVisibleThread(); - Long focusedThreadId = focusedThread.map(NotificationThread::getThreadId).orElse(null); - Long focusedGroupStoryId = focusedThread.map(NotificationThread::getGroupStoryId).orElse(null); + Long threadId = SignalDatabase.threads().getThreadIdFor(recipientId); + Optional focusedThread = ApplicationDependencies.getMessageNotifier().getVisibleThread(); + Long focusedThreadId = focusedThread.map(ConversationId::getThreadId).orElse(null); + Long focusedGroupStoryId = focusedThread.map(ConversationId::getGroupStoryId).orElse(null); if (Objects.equals(threadId, focusedThreadId) && focusedGroupStoryId == null) { Log.d(TAG, "isCancellable: user entered full screen thread."); diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationIds.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationIds.java index 07bd9a454d..703e649c86 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationIds.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationIds.java @@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.notifications; import androidx.annotation.NonNull; -import org.thoughtcrime.securesms.notifications.v2.NotificationThread; +import org.thoughtcrime.securesms.notifications.v2.ConversationId; public final class NotificationIds { @@ -25,19 +25,19 @@ public final class NotificationIds { private NotificationIds() { } - public static int getNotificationIdForThread(@NonNull NotificationThread notificationThread) { - if (notificationThread.getGroupStoryId() != null) { - return STORY_THREAD + notificationThread.getGroupStoryId().intValue(); + public static int getNotificationIdForThread(@NonNull ConversationId conversationId) { + if (conversationId.getGroupStoryId() != null) { + return STORY_THREAD + conversationId.getGroupStoryId().intValue(); } else { - return THREAD + (int) notificationThread.getThreadId(); + return THREAD + (int) conversationId.getThreadId(); } } - public static int getNotificationIdForMessageDeliveryFailed(@NonNull NotificationThread notificationThread) { - if (notificationThread.getGroupStoryId() != null) { - return STORY_MESSAGE_DELIVERY_FAILURE + notificationThread.getGroupStoryId().intValue(); + public static int getNotificationIdForMessageDeliveryFailed(@NonNull ConversationId conversationId) { + if (conversationId.getGroupStoryId() != null) { + return STORY_MESSAGE_DELIVERY_FAILURE + conversationId.getGroupStoryId().intValue(); } else { - return MESSAGE_DELIVERY_FAILURE + (int) notificationThread.getThreadId(); + return MESSAGE_DELIVERY_FAILURE + (int) conversationId.getThreadId(); } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/OptimizedMessageNotifier.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/OptimizedMessageNotifier.java index e77558f583..1d4b185517 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/OptimizedMessageNotifier.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/OptimizedMessageNotifier.java @@ -11,7 +11,7 @@ import androidx.annotation.Nullable; import org.signal.core.util.ExceptionUtil; import org.signal.core.util.concurrent.SignalExecutors; import org.thoughtcrime.securesms.notifications.v2.MessageNotifierV2; -import org.thoughtcrime.securesms.notifications.v2.NotificationThread; +import org.thoughtcrime.securesms.notifications.v2.ConversationId; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.util.BubbleUtil; import org.thoughtcrime.securesms.util.LeakyBucketLimiter; @@ -33,12 +33,12 @@ public class OptimizedMessageNotifier implements MessageNotifier { } @Override - public void setVisibleThread(@Nullable NotificationThread notificationThread) { - getNotifier().setVisibleThread(notificationThread); + public void setVisibleThread(@Nullable ConversationId conversationId) { + getNotifier().setVisibleThread(conversationId); } @Override - public @NonNull Optional getVisibleThread() { + public @NonNull Optional getVisibleThread() { return getNotifier().getVisibleThread(); } @@ -53,13 +53,13 @@ public class OptimizedMessageNotifier implements MessageNotifier { } @Override - public void notifyMessageDeliveryFailed(@NonNull Context context, @NonNull Recipient recipient, @NonNull NotificationThread notificationThread) { - getNotifier().notifyMessageDeliveryFailed(context, recipient, notificationThread); + public void notifyMessageDeliveryFailed(@NonNull Context context, @NonNull Recipient recipient, @NonNull ConversationId conversationId) { + getNotifier().notifyMessageDeliveryFailed(context, recipient, conversationId); } @Override - public void notifyProofRequired(@NonNull Context context, @NonNull Recipient recipient, @NonNull NotificationThread notificationThread) { - getNotifier().notifyProofRequired(context, recipient, notificationThread); + public void notifyProofRequired(@NonNull Context context, @NonNull Recipient recipient, @NonNull ConversationId conversationId) { + getNotifier().notifyProofRequired(context, recipient, conversationId); } @Override @@ -73,23 +73,23 @@ public class OptimizedMessageNotifier implements MessageNotifier { } @Override - public void updateNotification(@NonNull Context context, @NonNull NotificationThread notificationThread) { - runOnLimiter(() -> getNotifier().updateNotification(context, notificationThread)); + public void updateNotification(@NonNull Context context, @NonNull ConversationId conversationId) { + runOnLimiter(() -> getNotifier().updateNotification(context, conversationId)); } @Override - public void updateNotification(@NonNull Context context, @NonNull NotificationThread notificationThread, @NonNull BubbleUtil.BubbleState defaultBubbleState) { - runOnLimiter(() -> getNotifier().updateNotification(context, notificationThread, defaultBubbleState)); + public void updateNotification(@NonNull Context context, @NonNull ConversationId conversationId, @NonNull BubbleUtil.BubbleState defaultBubbleState) { + runOnLimiter(() -> getNotifier().updateNotification(context, conversationId, defaultBubbleState)); } @Override - public void updateNotification(@NonNull Context context, @NonNull NotificationThread notificationThread, boolean signal) { - runOnLimiter(() -> getNotifier().updateNotification(context, notificationThread, signal)); + public void updateNotification(@NonNull Context context, @NonNull ConversationId conversationId, boolean signal) { + runOnLimiter(() -> getNotifier().updateNotification(context, conversationId, signal)); } @Override - public void updateNotification(@NonNull Context context, @Nullable NotificationThread notificationThread, boolean signal, int reminderCount, @NonNull BubbleUtil.BubbleState defaultBubbleState) { - runOnLimiter(() -> getNotifier().updateNotification(context, notificationThread, signal, reminderCount, defaultBubbleState)); + public void updateNotification(@NonNull Context context, @Nullable ConversationId conversationId, boolean signal, int reminderCount, @NonNull BubbleUtil.BubbleState defaultBubbleState) { + runOnLimiter(() -> getNotifier().updateNotification(context, conversationId, signal, reminderCount, defaultBubbleState)); } @Override @@ -98,13 +98,13 @@ public class OptimizedMessageNotifier implements MessageNotifier { } @Override - public void addStickyThread(@NonNull NotificationThread notificationThread, long earliestTimestamp) { - getNotifier().addStickyThread(notificationThread, earliestTimestamp); + public void addStickyThread(@NonNull ConversationId conversationId, long earliestTimestamp) { + getNotifier().addStickyThread(conversationId, earliestTimestamp); } @Override - public void removeStickyThread(@NonNull NotificationThread notificationThread) { - getNotifier().removeStickyThread(notificationThread); + public void removeStickyThread(@NonNull ConversationId conversationId) { + getNotifier().removeStickyThread(conversationId); } private void runOnLimiter(@NonNull Runnable runnable) { 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 78ec008d19..4908192508 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java @@ -33,7 +33,7 @@ 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; -import org.thoughtcrime.securesms.notifications.v2.NotificationThread; +import org.thoughtcrime.securesms.notifications.v2.ConversationId; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.sms.MessageSender; @@ -120,7 +120,7 @@ public class RemoteReplyReceiver extends BroadcastReceiver { } ApplicationDependencies.getMessageNotifier() - .addStickyThread(new NotificationThread(threadId, groupStoryId != Long.MIN_VALUE ? groupStoryId : null), + .addStickyThread(new ConversationId(threadId, groupStoryId != Long.MIN_VALUE ? groupStoryId : null), intent.getLongExtra(EARLIEST_TIMESTAMP, System.currentTimeMillis())); List messageIds = SignalDatabase.threads().setRead(threadId, true); diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationThread.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/ConversationId.kt similarity index 60% rename from app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationThread.kt rename to app/src/main/java/org/thoughtcrime/securesms/notifications/v2/ConversationId.kt index edc92dfddc..2806d1fe54 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationThread.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/ConversationId.kt @@ -10,27 +10,27 @@ import org.thoughtcrime.securesms.database.model.ParentStoryId * Represents a "thread" that a notification can belong to. */ @Parcelize -data class NotificationThread( +data class ConversationId( val threadId: Long, val groupStoryId: Long? ) : Parcelable { companion object { @JvmStatic - fun forConversation(threadId: Long): NotificationThread { - return NotificationThread( + fun forConversation(threadId: Long): ConversationId { + return ConversationId( threadId = threadId, groupStoryId = null ) } @JvmStatic - fun fromMessageRecord(record: MessageRecord): NotificationThread { - return NotificationThread(record.threadId, ((record as? MmsMessageRecord)?.parentStoryId as? ParentStoryId.GroupReply)?.serialize()) + fun fromMessageRecord(record: MessageRecord): ConversationId { + return ConversationId(record.threadId, ((record as? MmsMessageRecord)?.parentStoryId as? ParentStoryId.GroupReply)?.serialize()) } @JvmStatic - fun fromThreadAndReply(threadId: Long, groupReply: ParentStoryId.GroupReply?): NotificationThread { - return NotificationThread(threadId, groupReply?.serialize()) + fun fromThreadAndReply(threadId: Long, groupReply: ParentStoryId.GroupReply?): ConversationId { + return ConversationId(threadId, groupReply?.serialize()) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/MessageNotifierV2.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/MessageNotifierV2.kt index 7d45a091f5..5cf090e59a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/MessageNotifierV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/MessageNotifierV2.kt @@ -45,7 +45,7 @@ import kotlin.math.max * MessageNotifier implementation using the new system for creating and showing notifications. */ class MessageNotifierV2(context: Application) : MessageNotifier { - @Volatile private var visibleThread: NotificationThread? = null + @Volatile private var visibleThread: ConversationId? = null @Volatile private var lastDesktopActivityTimestamp: Long = -1 @Volatile private var lastAudibleNotification: Long = -1 @Volatile private var lastScheduledReminder: Long = 0 @@ -53,17 +53,17 @@ class MessageNotifierV2(context: Application) : MessageNotifier { @Volatile private var previousPrivacyPreference: NotificationPrivacyPreference = SignalStore.settings().messageNotificationsPrivacy @Volatile private var previousState: NotificationStateV2 = NotificationStateV2.EMPTY - private val threadReminders: MutableMap = ConcurrentHashMap() - private val stickyThreads: MutableMap = mutableMapOf() + private val threadReminders: MutableMap = ConcurrentHashMap() + private val stickyThreads: MutableMap = mutableMapOf() private val executor = CancelableExecutor() - override fun setVisibleThread(notificationThread: NotificationThread?) { - visibleThread = notificationThread - stickyThreads.remove(notificationThread) + override fun setVisibleThread(conversationId: ConversationId?) { + visibleThread = conversationId + stickyThreads.remove(conversationId) } - override fun getVisibleThread(): Optional { + override fun getVisibleThread(): Optional { return Optional.ofNullable(visibleThread) } @@ -75,12 +75,12 @@ class MessageNotifierV2(context: Application) : MessageNotifier { lastDesktopActivityTimestamp = timestamp } - override fun notifyMessageDeliveryFailed(context: Context, recipient: Recipient, notificationThread: NotificationThread) { - NotificationFactory.notifyMessageDeliveryFailed(context, recipient, notificationThread, visibleThread) + override fun notifyMessageDeliveryFailed(context: Context, recipient: Recipient, conversationId: ConversationId) { + NotificationFactory.notifyMessageDeliveryFailed(context, recipient, conversationId, visibleThread) } - override fun notifyProofRequired(context: Context, recipient: Recipient, notificationThread: NotificationThread) { - NotificationFactory.notifyProofRequired(context, recipient, notificationThread, visibleThread) + override fun notifyProofRequired(context: Context, recipient: Recipient, conversationId: ConversationId) { + NotificationFactory.notifyProofRequired(context, recipient, conversationId, visibleThread) } override fun cancelDelayedNotifications() { @@ -91,21 +91,21 @@ class MessageNotifierV2(context: Application) : MessageNotifier { updateNotification(context, null, false, 0, BubbleState.HIDDEN) } - override fun updateNotification(context: Context, notificationThread: NotificationThread) { + override fun updateNotification(context: Context, conversationId: ConversationId) { if (System.currentTimeMillis() - lastDesktopActivityTimestamp < DESKTOP_ACTIVITY_PERIOD) { Log.i(TAG, "Scheduling delayed notification...") - executor.enqueue(context, notificationThread) + executor.enqueue(context, conversationId) } else { - updateNotification(context, notificationThread, true) + updateNotification(context, conversationId, true) } } - override fun updateNotification(context: Context, notificationThread: NotificationThread, defaultBubbleState: BubbleState) { - updateNotification(context, notificationThread, false, 0, defaultBubbleState) + override fun updateNotification(context: Context, conversationId: ConversationId, defaultBubbleState: BubbleState) { + updateNotification(context, conversationId, false, 0, defaultBubbleState) } - override fun updateNotification(context: Context, notificationThread: NotificationThread, signal: Boolean) { - updateNotification(context, notificationThread, signal, 0, BubbleState.HIDDEN) + override fun updateNotification(context: Context, conversationId: ConversationId, signal: Boolean) { + updateNotification(context, conversationId, signal, 0, BubbleState.HIDDEN) } /** @@ -114,7 +114,7 @@ class MessageNotifierV2(context: Application) : MessageNotifier { */ override fun updateNotification( context: Context, - notificationThread: NotificationThread?, + conversationId: ConversationId?, signal: Boolean, reminderCount: Int, defaultBubbleState: BubbleState @@ -164,7 +164,7 @@ class MessageNotifierV2(context: Application) : MessageNotifier { val displayedNotifications: Set? = ServiceUtil.getNotificationManager(context).getDisplayedNotificationIds().getOrNull() if (displayedNotifications != null) { - val cleanedUpThreads: MutableSet = mutableSetOf() + val cleanedUpThreads: MutableSet = mutableSetOf() state.conversations.filterNot { it.hasNewNotifications() || displayedNotifications.contains(it.notificationId) } .forEach { conversation -> cleanedUpThreads += conversation.thread @@ -179,7 +179,7 @@ class MessageNotifierV2(context: Application) : MessageNotifier { } } - val retainStickyThreadIds: Set = state.getThreadsWithMostRecentNotificationFromSelf() + val retainStickyThreadIds: Set = state.getThreadsWithMostRecentNotificationFromSelf() stickyThreads.keys.retainAll { retainStickyThreadIds.contains(it) } if (state.isEmpty) { @@ -190,13 +190,13 @@ class MessageNotifierV2(context: Application) : MessageNotifier { return } - val alertOverrides: Set = threadReminders.filter { (_, reminder) -> reminder.lastNotified < System.currentTimeMillis() - REMINDER_TIMEOUT }.keys + val alertOverrides: Set = threadReminders.filter { (_, reminder) -> reminder.lastNotified < System.currentTimeMillis() - REMINDER_TIMEOUT }.keys - val threadsThatAlerted: Set = NotificationFactory.notify( + val threadsThatAlerted: Set = NotificationFactory.notify( context = ContextThemeWrapper(context, R.style.TextSecure_LightTheme), state = state, visibleThread = visibleThread, - targetThread = notificationThread, + targetThread = conversationId, defaultBubbleState = defaultBubbleState, lastAudibleNotification = lastAudibleNotification, notificationConfigurationChanged = notificationConfigurationChanged, @@ -238,23 +238,23 @@ class MessageNotifierV2(context: Application) : MessageNotifier { // Intentionally left blank } - override fun addStickyThread(notificationThread: NotificationThread, earliestTimestamp: Long) { - stickyThreads[notificationThread] = StickyThread(notificationThread, NotificationIds.getNotificationIdForThread(notificationThread), earliestTimestamp) + override fun addStickyThread(conversationId: ConversationId, earliestTimestamp: Long) { + stickyThreads[conversationId] = StickyThread(conversationId, NotificationIds.getNotificationIdForThread(conversationId), earliestTimestamp) } - override fun removeStickyThread(notificationThread: NotificationThread) { - stickyThreads.remove(notificationThread) + override fun removeStickyThread(conversationId: ConversationId) { + stickyThreads.remove(conversationId) } - private fun updateReminderTimestamps(context: Context, alertOverrides: Set, threadsThatAlerted: Set) { + private fun updateReminderTimestamps(context: Context, alertOverrides: Set, threadsThatAlerted: Set) { if (SignalStore.settings().messageNotificationsRepeatAlerts == 0) { return } - val iterator: MutableIterator> = threadReminders.iterator() + val iterator: MutableIterator> = threadReminders.iterator() while (iterator.hasNext()) { - val entry: MutableEntry = iterator.next() - val (id: NotificationThread, reminder: Reminder) = entry + val entry: MutableEntry = iterator.next() + val (id: ConversationId, reminder: Reminder) = entry if (alertOverrides.contains(id)) { val notifyCount: Int = reminder.count + 1 if (notifyCount >= SignalStore.settings().messageNotificationsRepeatAlerts) { @@ -265,7 +265,7 @@ class MessageNotifierV2(context: Application) : MessageNotifier { } } - for (alertedThreadId: NotificationThread in threadsThatAlerted) { + for (alertedThreadId: ConversationId in threadsThatAlerted) { threadReminders[alertedThreadId] = Reminder(lastAudibleNotification) } @@ -319,7 +319,7 @@ class MessageNotifierV2(context: Application) : MessageNotifier { } } - data class StickyThread(val notificationThread: NotificationThread, val notificationId: Int, val earliestTimestamp: Long) + data class StickyThread(val conversationId: ConversationId, val notificationId: Int, val earliestTimestamp: Long) private data class Reminder(val lastNotified: Long, val count: Int = 0) } @@ -368,8 +368,8 @@ private class CancelableExecutor { private val executor: Executor = Executors.newSingleThreadExecutor() private val tasks: MutableSet = mutableSetOf() - fun enqueue(context: Context, notificationThread: NotificationThread) { - execute(DelayedNotification(context, notificationThread)) + fun enqueue(context: Context, conversationId: ConversationId) { + execute(DelayedNotification(context, conversationId)) } private fun execute(runnable: DelayedNotification) { @@ -389,7 +389,7 @@ private class CancelableExecutor { } } - private class DelayedNotification constructor(private val context: Context, private val thread: NotificationThread) : Runnable { + private class DelayedNotification constructor(private val context: Context, private val thread: ConversationId) : Runnable { private val canceled = AtomicBoolean(false) private val delayUntil: Long = System.currentTimeMillis() + DELAY diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationConversation.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationConversation.kt index 16dd4398fd..779cb0eec7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationConversation.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationConversation.kt @@ -7,6 +7,7 @@ import android.graphics.drawable.Drawable import android.net.Uri import android.text.SpannableStringBuilder import androidx.core.app.TaskStackBuilder +import org.signal.core.util.PendingIntentFlags import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.contacts.TurnOffContactJoinedNotificationsActivity import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto @@ -22,6 +23,7 @@ import org.thoughtcrime.securesms.notifications.ReplyMethod import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.service.KeyCachingService +import org.thoughtcrime.securesms.stories.StoryViewerArgs import org.thoughtcrime.securesms.stories.viewer.StoryViewerActivity import org.thoughtcrime.securesms.util.Util @@ -31,7 +33,7 @@ import org.thoughtcrime.securesms.util.Util */ data class NotificationConversation( val recipient: Recipient, - val thread: NotificationThread, + val thread: ConversationId, val notificationItems: List ) { val mostRecentNotification: NotificationItemV2 = notificationItems.last() @@ -43,7 +45,7 @@ data class NotificationConversation( fun getContentTitle(context: Context): CharSequence { return if (SignalStore.settings().messageNotificationsPrivacy.isDisplayContact) { - recipient.getDisplayName(context) + getDisplayName(context) } else { context.getString(R.string.SingleRecipientNotificationBuilder_signal) } @@ -82,7 +84,7 @@ data class NotificationConversation( fun getConversationTitle(context: Context): CharSequence? { if (SignalStore.settings().messageNotificationsPrivacy.isDisplayContact) { - return if (isGroup) recipient.getDisplayName(context) else null + return if (isGroup) getDisplayName(context) else null } return context.getString(R.string.SingleRecipientNotificationBuilder_signal) } @@ -113,13 +115,21 @@ data class NotificationConversation( fun getPendingIntent(context: Context): PendingIntent { val intent: Intent = if (thread.groupStoryId != null) { - StoryViewerActivity.createIntent(context, recipient.id, thread.groupStoryId, recipient.shouldHideStory()) + StoryViewerActivity.createIntent( + context, + StoryViewerArgs( + recipientId = recipient.id, + storyId = thread.groupStoryId, + isInHiddenStoryMode = recipient.shouldHideStory(), + isFromNotification = true, + groupReplyStartPosition = mostRecentNotification.getStartingPosition(context) + ) + ) } else { ConversationIntents.createBuilder(context, recipient.id, thread.threadId) .withStartingPosition(mostRecentNotification.getStartingPosition(context)) .build() - .makeUniqueToPreventMerging() - } + }.makeUniqueToPreventMerging() return TaskStackBuilder.create(context) .addNextIntentWithParentStack(intent) @@ -141,7 +151,7 @@ data class NotificationConversation( .putParcelableArrayListExtra(DeleteNotificationReceiver.EXTRA_THREADS, arrayListOf(thread)) .makeUniqueToPreventMerging() - return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) + return PendingIntent.getBroadcast(context, 0, intent, PendingIntentFlags.updateCurrent()) } fun getMarkAsReadIntent(context: Context): PendingIntent { @@ -151,7 +161,7 @@ data class NotificationConversation( .putExtra(MarkReadReceiver.NOTIFICATION_ID_EXTRA, notificationId) .makeUniqueToPreventMerging() - return PendingIntent.getBroadcast(context, (thread.threadId * 2).toInt(), intent, PendingIntent.FLAG_UPDATE_CURRENT) + return PendingIntent.getBroadcast(context, (thread.threadId * 2).toInt(), intent, PendingIntentFlags.updateCurrent()) } fun getQuickReplyIntent(context: Context): PendingIntent { @@ -159,7 +169,7 @@ data class NotificationConversation( .build() .makeUniqueToPreventMerging() - return PendingIntent.getActivity(context, (thread.threadId * 2).toInt() + 1, intent, PendingIntent.FLAG_UPDATE_CURRENT) + return PendingIntent.getActivity(context, (thread.threadId * 2).toInt() + 1, intent, PendingIntentFlags.updateCurrent()) } fun getRemoteReplyIntent(context: Context, replyMethod: ReplyMethod): PendingIntent { @@ -171,7 +181,7 @@ data class NotificationConversation( .putExtra(RemoteReplyReceiver.GROUP_STORY_ID_EXTRA, notificationItems.first().thread.groupStoryId ?: Long.MIN_VALUE) .makeUniqueToPreventMerging() - return PendingIntent.getBroadcast(context, (thread.threadId * 2).toInt() + 1, intent, PendingIntent.FLAG_UPDATE_CURRENT) + return PendingIntent.getBroadcast(context, (thread.threadId * 2).toInt() + 1, intent, PendingIntentFlags.updateCurrent()) } fun getTurnOffJoinedNotificationsIntent(context: Context): PendingIntent { @@ -179,10 +189,18 @@ data class NotificationConversation( context, 0, TurnOffContactJoinedNotificationsActivity.newIntent(context, thread.threadId), - PendingIntent.FLAG_UPDATE_CURRENT + PendingIntentFlags.updateCurrent() ) } + private fun getDisplayName(context: Context): String { + return if (thread.groupStoryId != null) { + context.getString(R.string.SingleRecipientNotificationBuilder__s_dot_story, recipient.getDisplayName(context)) + } else { + recipient.getDisplayName(context) + } + } + override fun toString(): String { return "NotificationConversation(thread=$thread, notificationItems=$notificationItems, messageCount=$messageCount, hasNewNotifications=${hasNewNotifications()})" } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationFactory.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationFactory.kt index 7dc1e824f3..d4b961da58 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationFactory.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationFactory.kt @@ -27,7 +27,6 @@ import org.thoughtcrime.securesms.notifications.NotificationChannels import org.thoughtcrime.securesms.notifications.NotificationIds import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.stories.my.MyStoriesActivity -import org.thoughtcrime.securesms.stories.viewer.StoryViewerActivity import org.thoughtcrime.securesms.util.BubbleUtil import org.thoughtcrime.securesms.util.ConversationUtil import org.thoughtcrime.securesms.util.ServiceUtil @@ -43,14 +42,14 @@ object NotificationFactory { fun notify( context: Context, state: NotificationStateV2, - visibleThread: NotificationThread?, - targetThread: NotificationThread?, + visibleThread: ConversationId?, + targetThread: ConversationId?, defaultBubbleState: BubbleUtil.BubbleState, lastAudibleNotification: Long, notificationConfigurationChanged: Boolean, - alertOverrides: Set, + alertOverrides: Set, previousState: NotificationStateV2 - ): Set { + ): Set { if (state.isEmpty) { Log.d(TAG, "State is empty, bailing") return emptySet() @@ -87,14 +86,14 @@ object NotificationFactory { private fun notify19( context: Context, state: NotificationStateV2, - visibleThread: NotificationThread?, - targetThread: NotificationThread?, + visibleThread: ConversationId?, + targetThread: ConversationId?, defaultBubbleState: BubbleUtil.BubbleState, lastAudibleNotification: Long, - alertOverrides: Set, + alertOverrides: Set, nonVisibleThreadCount: Int - ): Set { - val threadsThatNewlyAlerted: MutableSet = mutableSetOf() + ): Set { + val threadsThatNewlyAlerted: MutableSet = mutableSetOf() state.conversations.find { it.thread == visibleThread }?.let { conversation -> if (conversation.hasNewNotifications()) { @@ -129,16 +128,16 @@ object NotificationFactory { private fun notify24( context: Context, state: NotificationStateV2, - visibleThread: NotificationThread?, - targetThread: NotificationThread?, + visibleThread: ConversationId?, + targetThread: ConversationId?, defaultBubbleState: BubbleUtil.BubbleState, lastAudibleNotification: Long, notificationConfigurationChanged: Boolean, - alertOverrides: Set, + alertOverrides: Set, nonVisibleThreadCount: Int, previousState: NotificationStateV2 - ): Set { - val threadsThatNewlyAlerted: MutableSet = mutableSetOf() + ): Set { + val threadsThatNewlyAlerted: MutableSet = mutableSetOf() state.conversations.forEach { conversation -> if (conversation.thread == visibleThread && conversation.hasNewNotifications()) { @@ -169,7 +168,7 @@ object NotificationFactory { private fun notifyForConversation( context: Context, conversation: NotificationConversation, - targetThread: NotificationThread?, + targetThread: ConversationId?, defaultBubbleState: BubbleUtil.BubbleState, shouldAlert: Boolean ) { @@ -189,8 +188,12 @@ object NotificationFactory { setContentTitle(conversation.getContentTitle(context)) setLargeIcon(conversation.getContactLargeIcon(context).toLargeBitmap(context)) addPerson(conversation.recipient) - setShortcutId(ConversationUtil.getShortcutId(conversation.recipient)) - setLocusId(ConversationUtil.getShortcutId(conversation.recipient)) + + if (conversation.thread.groupStoryId == null) { + setShortcutId(ConversationUtil.getShortcutId(conversation.recipient)) + setLocusId(ConversationUtil.getShortcutId(conversation.recipient)) + } + setContentInfo(conversation.messageCount.toString()) setNumber(conversation.messageCount) setContentText(conversation.getContentText(context)) @@ -292,22 +295,18 @@ object NotificationFactory { ringtone.play() } - fun notifyMessageDeliveryFailed(context: Context, recipient: Recipient, thread: NotificationThread, visibleThread: NotificationThread?) { + fun notifyMessageDeliveryFailed(context: Context, recipient: Recipient, thread: ConversationId, visibleThread: ConversationId?) { if (thread == visibleThread) { notifyInThread(context, recipient, 0) return } - val intent: Intent = if (recipient.isDistributionList) { + val intent: Intent = if (recipient.isDistributionList || thread.groupStoryId != null) { Intent(context, MyStoriesActivity::class.java) - .makeUniqueToPreventMerging() - } else if (thread.groupStoryId != null) { - StoryViewerActivity.createIntent(context, recipient.id, thread.groupStoryId, recipient.shouldHideStory()) } else { ConversationIntents.createBuilder(context, recipient.id, thread.threadId) .build() - .makeUniqueToPreventMerging() - } + }.makeUniqueToPreventMerging() val builder: NotificationBuilder = NotificationBuilder.create(context) @@ -326,22 +325,18 @@ object NotificationFactory { NotificationManagerCompat.from(context).safelyNotify(context, recipient, NotificationIds.getNotificationIdForMessageDeliveryFailed(thread), builder.build()) } - fun notifyProofRequired(context: Context, recipient: Recipient, thread: NotificationThread, visibleThread: NotificationThread?) { + fun notifyProofRequired(context: Context, recipient: Recipient, thread: ConversationId, visibleThread: ConversationId?) { if (thread == visibleThread) { notifyInThread(context, recipient, 0) return } - val intent: Intent = if (recipient.isDistributionList) { + val intent: Intent = if (recipient.isDistributionList || thread.groupStoryId != null) { Intent(context, MyStoriesActivity::class.java) - .makeUniqueToPreventMerging() - } else if (thread.groupStoryId != null) { - StoryViewerActivity.createIntent(context, recipient.id, thread.groupStoryId, recipient.shouldHideStory()) } else { ConversationIntents.createBuilder(context, recipient.id, thread.threadId) .build() - .makeUniqueToPreventMerging() - } + }.makeUniqueToPreventMerging() val builder: NotificationBuilder = NotificationBuilder.create(context) @@ -366,7 +361,7 @@ object NotificationFactory { val conversation = NotificationConversation( recipient = recipient, - thread = NotificationThread.forConversation(threadId), + thread = ConversationId.forConversation(threadId), notificationItems = listOf( MessageNotification( threadRecipient = recipient, diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationItemV2.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationItemV2.kt index 32e93cae58..92ce7cc5e3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationItemV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationItemV2.kt @@ -40,7 +40,7 @@ private const val MAX_DISPLAY_LENGTH = 500 sealed class NotificationItemV2(val threadRecipient: Recipient, protected val record: MessageRecord) : Comparable { val id: Long = record.id - val thread = NotificationThread.fromMessageRecord(record) + val thread = ConversationId.fromMessageRecord(record) val isMms: Boolean = record.isMms val slideDeck: SlideDeck? = if (record.isViewOnce) null else (record as? MmsMessageRecord)?.slideDeck val isJoined: Boolean = record.isJoined @@ -201,7 +201,11 @@ class MessageNotification(threadRecipient: Recipient, record: MessageRecord) : N } override fun getStartingPosition(context: Context): Int { - return -1 + return if (thread.groupStoryId != null) { + SignalDatabase.mmsSms.getMessagePositionInConversation(thread.threadId, thread.groupStoryId, record.dateReceived) + } else { + -1 + } } override fun getLargeIconUri(): Uri? { @@ -304,7 +308,7 @@ class ReactionNotification(threadRecipient: Recipient, record: MessageRecord, va } override fun getStartingPosition(context: Context): Int { - return SignalDatabase.mmsSms.getMessagePositionInConversation(thread.threadId, record.dateReceived) + return SignalDatabase.mmsSms.getMessagePositionInConversation(thread.threadId, thread.groupStoryId ?: 0L, record.dateReceived) } override fun getLargeIconUri(): Uri? = null diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationStateProvider.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationStateProvider.kt index e60100bcf5..20db05068d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationStateProvider.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationStateProvider.kt @@ -21,7 +21,7 @@ object NotificationStateProvider { private val TAG = Log.tag(NotificationStateProvider::class.java) @WorkerThread - fun constructNotificationState(stickyThreads: Map, notificationProfile: NotificationProfile?): NotificationStateV2 { + fun constructNotificationState(stickyThreads: Map, notificationProfile: NotificationProfile?): NotificationStateV2 { val messages: MutableList = mutableListOf() SignalDatabase.mmsSms.getMessagesForNotificationState(stickyThreads.values).use { unreadMessages -> @@ -35,16 +35,27 @@ object NotificationStateProvider { val threadRecipient: Recipient? = SignalDatabase.threads.getRecipientForThreadId(record.threadId) if (threadRecipient != null) { val hasUnreadReactions = CursorUtil.requireInt(unreadMessages, MmsSmsColumns.REACTIONS_UNREAD) == 1 + val conversationId = ConversationId.fromMessageRecord(record) + + val parentRecord = conversationId.groupStoryId?.let { + SignalDatabase.mms.getMessageRecord(it) + } + + val hasSelfRepliedToGroupStory = conversationId.groupStoryId?.let { + SignalDatabase.mms.hasSelfReplyInGroupStory(it) + } messages += NotificationMessage( messageRecord = record, reactions = if (hasUnreadReactions) SignalDatabase.reactions.getReactions(MessageId(record.id, record.isMms)) else emptyList(), threadRecipient = threadRecipient, - thread = NotificationThread.fromMessageRecord(record), - stickyThread = stickyThreads.containsKey(NotificationThread.fromMessageRecord(record)), + thread = conversationId, + stickyThread = stickyThreads.containsKey(conversationId), isUnreadMessage = CursorUtil.requireInt(unreadMessages, MmsSmsColumns.READ) == 0, hasUnreadReactions = hasUnreadReactions, - lastReactionRead = CursorUtil.requireLong(unreadMessages, MmsSmsColumns.REACTIONS_LAST_SEEN) + lastReactionRead = CursorUtil.requireLong(unreadMessages, MmsSmsColumns.REACTIONS_LAST_SEEN), + isParentStorySentBySelf = parentRecord?.isOutgoing ?: false, + hasSelfRepliedToStory = hasSelfRepliedToGroupStory ?: false ) } try { @@ -104,16 +115,20 @@ object NotificationStateProvider { val messageRecord: MessageRecord, val reactions: List, val threadRecipient: Recipient, - val thread: NotificationThread, + val thread: ConversationId, val stickyThread: Boolean, val isUnreadMessage: Boolean, val hasUnreadReactions: Boolean, - val lastReactionRead: Long + val lastReactionRead: Long, + val isParentStorySentBySelf: Boolean, + val hasSelfRepliedToStory: Boolean ) { - private val isUnreadIncoming: Boolean = isUnreadMessage && !messageRecord.isOutgoing + private val isGroupStoryReply: Boolean = thread.groupStoryId != null + private val isUnreadIncoming: Boolean = isUnreadMessage && !messageRecord.isOutgoing && !isGroupStoryReply + private val isNotifiableGroupStoryMessage: Boolean = isUnreadMessage && !messageRecord.isOutgoing && isGroupStoryReply && (isParentStorySentBySelf || hasSelfRepliedToStory) fun includeMessage(notificationProfile: NotificationProfile?): MessageInclusion { - return if (isUnreadIncoming || stickyThread) { + return if (isUnreadIncoming || stickyThread || isNotifiableGroupStoryMessage) { if (threadRecipient.isMuted && (threadRecipient.isDoNotNotifyMentions || !messageRecord.hasSelfMention())) { MessageInclusion.MUTE_FILTERED } else if (notificationProfile != null && !notificationProfile.isRecipientAllowed(threadRecipient.id) && !(notificationProfile.allowAllMentions && messageRecord.hasSelfMention())) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationStateV2.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationStateV2.kt index 1dd9f89a96..6749f4edc8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationStateV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationStateV2.kt @@ -39,18 +39,18 @@ data class NotificationStateV2(val conversations: List val mostRecentSender: Recipient? get() = mostRecentNotification?.individualRecipient - fun getNonVisibleConversation(visibleThread: NotificationThread?): List { + fun getNonVisibleConversation(visibleThread: ConversationId?): List { return conversations.filterNot { it.thread == visibleThread } } - fun getConversation(notificationThread: NotificationThread): NotificationConversation? { - return conversations.firstOrNull { it.thread == notificationThread } + fun getConversation(conversationId: ConversationId): NotificationConversation? { + return conversations.firstOrNull { it.thread == conversationId } } fun getDeleteIntent(context: Context): PendingIntent? { val ids = LongArray(messageCount) val mms = BooleanArray(ids.size) - val threads: MutableList = mutableListOf() + val threads: MutableList = mutableListOf() conversations.forEach { conversation -> threads += conversation.thread @@ -79,7 +79,7 @@ data class NotificationStateV2(val conversations: List return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) } - fun getThreadsWithMostRecentNotificationFromSelf(): Set { + fun getThreadsWithMostRecentNotificationFromSelf(): Set { return conversations.filter { it.mostRecentNotification.individualRecipient.isSelf } .map { it.thread } .toSet() diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogViewModel.java index 5aafd18b0e..2062130510 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogViewModel.java @@ -32,12 +32,12 @@ import org.thoughtcrime.securesms.groups.ui.addtogroup.AddToGroupsActivity; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; +import org.thoughtcrime.securesms.stories.StoryViewerArgs; import org.thoughtcrime.securesms.stories.viewer.StoryViewerActivity; import org.thoughtcrime.securesms.util.CommunicationActions; import org.thoughtcrime.securesms.util.livedata.LiveDataUtil; import org.thoughtcrime.securesms.verify.VerifyIdentityActivity; -import java.util.Collections; import java.util.Objects; import io.reactivex.rxjava3.disposables.CompositeDisposable; @@ -141,7 +141,10 @@ final class RecipientDialogViewModel extends ViewModel { if (storyViewState.getValue() == null || storyViewState.getValue() == StoryViewState.NONE) { onMessageClicked(activity); } else { - activity.startActivity(StoryViewerActivity.createIntent(activity, recipientDialogRepository.getRecipientId(), -1L, recipient.getValue().shouldHideStory(), null, null, null, Collections.emptyList())); + activity.startActivity(StoryViewerActivity.createIntent( + activity, + new StoryViewerArgs.Builder(recipientDialogRepository.getRecipientId(), recipient.getValue().shouldHideStory()) + .build())); } } @@ -177,7 +180,10 @@ final class RecipientDialogViewModel extends ViewModel { if (storyViewState.getValue() == null || storyViewState.getValue() == StoryViewState.NONE) { activity.startActivity(ConversationSettingsActivity.forRecipient(activity, recipientDialogRepository.getRecipientId())); } else { - activity.startActivity(StoryViewerActivity.createIntent(activity, recipientDialogRepository.getRecipientId(), -1L, recipient.getValue().shouldHideStory(), null, null, null, Collections.emptyList())); + activity.startActivity(StoryViewerActivity.createIntent( + activity, + new StoryViewerArgs.Builder(recipientDialogRepository.getRecipientId(), recipient.getValue().shouldHideStory()) + .build())); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java index 9feac0037d..0b1c729cb9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java @@ -40,7 +40,7 @@ import org.thoughtcrime.securesms.jobs.GroupCallUpdateSendJob; import org.thoughtcrime.securesms.jobs.RetrieveProfileJob; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.messages.GroupSendUtil; -import org.thoughtcrime.securesms.notifications.v2.NotificationThread; +import org.thoughtcrime.securesms.notifications.v2.ConversationId; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; @@ -351,7 +351,7 @@ private void processStateless(@NonNull Function1 messageAndThreadId = SignalDatabase.sms().insertMissedCall(remotePeer.getId(), timestamp, isVideoOffer); ApplicationDependencies.getMessageNotifier() - .updateNotification(context, NotificationThread.forConversation(messageAndThreadId.second()), signal); + .updateNotification(context, ConversationId.forConversation(messageAndThreadId.second()), signal); } public void insertReceivedCall(@NonNull RemotePeer remotePeer, boolean signal, boolean isVideoOffer) { Pair messageAndThreadId = SignalDatabase.sms().insertReceivedCall(remotePeer.getId(), isVideoOffer); ApplicationDependencies.getMessageNotifier() - .updateNotification(context, NotificationThread.forConversation(messageAndThreadId.second()), signal); + .updateNotification(context, ConversationId.forConversation(messageAndThreadId.second()), signal); } public void retrieveTurnServers(@NonNull RemotePeer remotePeer) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/StoryViewerArgs.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/StoryViewerArgs.kt new file mode 100644 index 0000000000..1c2ccf0e46 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/StoryViewerArgs.kt @@ -0,0 +1,84 @@ +package org.thoughtcrime.securesms.stories + +import android.net.Uri +import android.os.Parcelable +import kotlinx.parcelize.Parcelize +import org.thoughtcrime.securesms.blurhash.BlurHash +import org.thoughtcrime.securesms.recipients.RecipientId + +/** + * Arguments for launching the story viewer, prefilled with sensible defaults. + */ +@Parcelize +data class StoryViewerArgs( + val recipientId: RecipientId, + val isInHiddenStoryMode: Boolean, + val storyId: Long = -1L, + val storyThumbTextModel: StoryTextPostModel? = null, + val storyThumbUri: Uri? = null, + val storyThumbBlur: BlurHash? = null, + val recipientIds: List = emptyList(), + val isFromNotification: Boolean = false, + val groupReplyStartPosition: Int = -1 +) : Parcelable { + + class Builder(private val recipientId: RecipientId, private val isInHiddenStoryMode: Boolean) { + + private var storyId: Long = -1L + private var storyThumbTextModel: StoryTextPostModel? = null + private var storyThumbUri: Uri? = null + private var storyThumbBlur: BlurHash? = null + private var recipientIds: List = emptyList() + private var isFromNotification: Boolean = false + private var groupReplyStartPosition: Int = -1 + + fun withStoryId(storyId: Long): Builder { + this.storyId = storyId + return this + } + + fun withStoryThumbTextModel(storyThumbTextModel: StoryTextPostModel?): Builder { + this.storyThumbTextModel = storyThumbTextModel + return this + } + + fun withStoryThumbUri(storyThumbUri: Uri?): Builder { + this.storyThumbUri = storyThumbUri + return this + } + + fun withStoryThumbBlur(storyThumbBlur: BlurHash?): Builder { + this.storyThumbBlur = storyThumbBlur + return this + } + + fun withRecipientIds(recipientIds: List): Builder { + this.recipientIds = recipientIds + return this + } + + fun isFromNotification(isFromNotification: Boolean): Builder { + this.isFromNotification = isFromNotification + return this + } + + fun withGroupReplyStartPosition(groupReplyStartPosition: Int): Builder { + this.groupReplyStartPosition = groupReplyStartPosition + return this + } + + fun build(): StoryViewerArgs { + return StoryViewerArgs( + recipientId = recipientId, + isInHiddenStoryMode = isInHiddenStoryMode, + storyId = storyId, + storyThumbTextModel = storyThumbTextModel, + storyThumbUri = storyThumbUri, + storyThumbBlur = storyThumbBlur, + recipientIds = recipientIds, + isFromNotification = isFromNotification, + groupReplyStartPosition = groupReplyStartPosition + ) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingFragment.kt index 86950e8dc2..2c7db44d5d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingFragment.kt @@ -38,6 +38,7 @@ import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.stories.StoryTextPostModel +import org.thoughtcrime.securesms.stories.StoryViewerArgs import org.thoughtcrime.securesms.stories.dialogs.StoryContextMenu import org.thoughtcrime.securesms.stories.dialogs.StoryDialogs import org.thoughtcrime.securesms.stories.my.MyStoriesActivity @@ -209,13 +210,15 @@ class StoriesLandingFragment : DSLSettingsFragment(layoutId = R.layout.stories_l startActivityIfAble( StoryViewerActivity.createIntent( context = requireContext(), - recipientId = model.data.storyRecipient.id, - storyId = -1L, - onlyIncludeHiddenStories = model.data.isHidden, - storyThumbTextModel = text, - storyThumbUri = image, - storyThumbBlur = blur, - recipientIds = viewModel.getRecipientIds(model.data.isHidden) + storyViewerArgs = StoryViewerArgs( + recipientId = model.data.storyRecipient.id, + storyId = -1L, + isInHiddenStoryMode = model.data.isHidden, + storyThumbTextModel = text, + storyThumbUri = image, + storyThumbBlur = blur, + recipientIds = viewModel.getRecipientIds(model.data.isHidden) + ) ), options.toBundle() ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesFragment.kt index 6f13e248bc..6a48367780 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesFragment.kt @@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.stories.StoryTextPostModel +import org.thoughtcrime.securesms.stories.StoryViewerArgs import org.thoughtcrime.securesms.stories.dialogs.StoryContextMenu import org.thoughtcrime.securesms.stories.dialogs.StoryDialogs import org.thoughtcrime.securesms.stories.viewer.StoryViewerActivity @@ -101,7 +102,20 @@ class MyStoriesFragment : DSLSettingsFragment( } val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(), preview, ViewCompat.getTransitionName(preview) ?: "") - startActivity(StoryViewerActivity.createIntent(requireContext(), recipient.id, conversationMessage.messageRecord.id, recipient.shouldHideStory(), text, image, blur), options.toBundle()) + startActivity( + StoryViewerActivity.createIntent( + context = requireContext(), + storyViewerArgs = StoryViewerArgs( + recipientId = recipient.id, + storyId = conversationMessage.messageRecord.id, + isInHiddenStoryMode = recipient.shouldHideStory(), + storyThumbTextModel = text, + storyThumbUri = image, + storyThumbBlur = blur + ) + ), + options.toBundle() + ) } }, onLongClick = { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerActivity.kt index 124cffc200..898e4bef79 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerActivity.kt @@ -2,17 +2,14 @@ package org.thoughtcrime.securesms.stories.viewer import android.content.Context import android.content.Intent -import android.net.Uri import android.os.Build import android.os.Bundle import androidx.appcompat.app.AppCompatDelegate import org.thoughtcrime.securesms.PassphraseRequiredActivity import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.blurhash.BlurHash import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner -import org.thoughtcrime.securesms.recipients.RecipientId -import org.thoughtcrime.securesms.stories.StoryTextPostModel +import org.thoughtcrime.securesms.stories.StoryViewerArgs class StoryViewerActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner { @@ -32,57 +29,40 @@ class StoryViewerActivity : PassphraseRequiredActivity(), VoiceNoteMediaControll voiceNoteMediaController = VoiceNoteMediaController(this) if (savedInstanceState == null) { - supportFragmentManager.beginTransaction() - .replace( - R.id.fragment_container, - StoryViewerFragment.create( - intent.getParcelableExtra(ARG_START_RECIPIENT_ID)!!, - intent.getLongExtra(ARG_START_STORY_ID, -1L), - intent.getBooleanExtra(ARG_HIDDEN_STORIES, false), - intent.getParcelableExtra(ARG_CROSSFADE_TEXT_MODEL), - intent.getParcelableExtra(ARG_CROSSFADE_IMAGE_URI), - intent.getStringExtra(ARG_CROSSFADE_IMAGE_BLUR), - intent.getParcelableArrayListExtra(ARG_RECIPIENT_IDS)!! - ) - ) - .commit() + replaceStoryViewerFragment() } } + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + setIntent(intent) + replaceStoryViewerFragment() + } + override fun onEnterAnimationComplete() { if (Build.VERSION.SDK_INT >= 21) { window.transitionBackgroundFadeDuration = 100 } } + private fun replaceStoryViewerFragment() { + supportFragmentManager.beginTransaction() + .replace( + R.id.fragment_container, + StoryViewerFragment.create(intent.getParcelableExtra(ARGS)!!) + ) + .commit() + } + companion object { - private const val ARG_START_RECIPIENT_ID = "start.recipient.id" - private const val ARG_START_STORY_ID = "start.story.id" - private const val ARG_HIDDEN_STORIES = "hidden_stories" - private const val ARG_CROSSFADE_TEXT_MODEL = "crossfade.text.model" - private const val ARG_CROSSFADE_IMAGE_URI = "crossfade.image.uri" - private const val ARG_CROSSFADE_IMAGE_BLUR = "crossfade.image.blur" - private const val ARG_RECIPIENT_IDS = "recipient_ids" + private const val ARGS = "story.viewer.args" @JvmStatic fun createIntent( context: Context, - recipientId: RecipientId, - storyId: Long = -1L, - onlyIncludeHiddenStories: Boolean = false, - storyThumbTextModel: StoryTextPostModel? = null, - storyThumbUri: Uri? = null, - storyThumbBlur: BlurHash? = null, - recipientIds: List = emptyList() + storyViewerArgs: StoryViewerArgs ): Intent { - return Intent(context, StoryViewerActivity::class.java) - .putExtra(ARG_START_RECIPIENT_ID, recipientId) - .putExtra(ARG_START_STORY_ID, storyId) - .putExtra(ARG_HIDDEN_STORIES, onlyIncludeHiddenStories) - .putExtra(ARG_CROSSFADE_TEXT_MODEL, storyThumbTextModel) - .putExtra(ARG_CROSSFADE_IMAGE_URI, storyThumbUri) - .putExtra(ARG_CROSSFADE_IMAGE_BLUR, storyThumbBlur?.hash) - .putParcelableArrayListExtra(ARG_RECIPIENT_IDS, ArrayList(recipientIds)) + return Intent(context, StoryViewerActivity::class.java).putExtra(ARGS, storyViewerArgs) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerFragment.kt index 560c757d58..07f0b85184 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerFragment.kt @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.stories.viewer -import android.net.Uri import android.os.Bundle import android.view.View import androidx.fragment.app.Fragment @@ -8,9 +7,8 @@ import androidx.fragment.app.viewModels import androidx.lifecycle.LiveDataReactiveStreams import androidx.viewpager2.widget.ViewPager2 import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.blurhash.BlurHash import org.thoughtcrime.securesms.recipients.RecipientId -import org.thoughtcrime.securesms.stories.StoryTextPostModel +import org.thoughtcrime.securesms.stories.StoryViewerArgs import org.thoughtcrime.securesms.stories.viewer.page.StoryViewerPageFragment /** @@ -24,35 +22,16 @@ class StoryViewerFragment : Fragment(R.layout.stories_viewer_fragment), StoryVie private val viewModel: StoryViewerViewModel by viewModels( factoryProducer = { - StoryViewerViewModel.Factory(storyRecipientId, onlyIncludeHiddenStories, storyThumbTextModel, storyThumbUri, storuThumbBlur, recipientIds, StoryViewerRepository()) + StoryViewerViewModel.Factory(storyViewerArgs, StoryViewerRepository()) } ) - private val storyRecipientId: RecipientId - get() = requireArguments().getParcelable(ARG_START_RECIPIENT_ID)!! - - private val storyId: Long - get() = requireArguments().getLong(ARG_START_STORY_ID, -1L) - - private val onlyIncludeHiddenStories: Boolean - get() = requireArguments().getBoolean(ARG_HIDDEN_STORIES) - - private val storyThumbTextModel: StoryTextPostModel? - get() = requireArguments().getParcelable(ARG_CROSSFADE_TEXT_MODEL) - - private val storyThumbUri: Uri? - get() = requireArguments().getParcelable(ARG_CROSSFADE_IMAGE_URI) - - private val storuThumbBlur: BlurHash? - get() = requireArguments().getString(ARG_CROSSFADE_IMAGE_BLUR)?.let { BlurHash.parseOrNull(it) } - - private val recipientIds: List - get() = requireArguments().getParcelableArrayList(ARG_RECIPIENT_IDS)!! + private val storyViewerArgs: StoryViewerArgs by lazy { requireArguments().getParcelable(ARGS)!! } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { storyPager = view.findViewById(R.id.story_item_pager) - val adapter = StoryViewerPagerAdapter(this, storyId) + val adapter = StoryViewerPagerAdapter(this, storyViewerArgs.storyId, storyViewerArgs.isFromNotification, storyViewerArgs.groupReplyStartPosition) storyPager.adapter = adapter viewModel.isChildScrolling.observe(viewLifecycleOwner) { @@ -110,32 +89,12 @@ class StoryViewerFragment : Fragment(R.layout.stories_viewer_fragment), StoryVie } companion object { - private const val ARG_START_RECIPIENT_ID = "start.recipient.id" - private const val ARG_START_STORY_ID = "start.story.id" - private const val ARG_HIDDEN_STORIES = "hidden_stories" - private const val ARG_CROSSFADE_TEXT_MODEL = "crossfade.text.model" - private const val ARG_CROSSFADE_IMAGE_URI = "crossfade.image.uri" - private const val ARG_CROSSFADE_IMAGE_BLUR = "crossfade.image.blur" - private const val ARG_RECIPIENT_IDS = "start.recipient.ids" + private const val ARGS = "args" - fun create( - storyRecipientId: RecipientId, - storyId: Long, - onlyIncludeHiddenStories: Boolean, - storyThumbTextModel: StoryTextPostModel? = null, - storyThumbUri: Uri? = null, - storyThumbBlur: String? = null, - recipientIds: List = emptyList() - ): Fragment { + fun create(storyViewerArgs: StoryViewerArgs): Fragment { return StoryViewerFragment().apply { arguments = Bundle().apply { - putParcelable(ARG_START_RECIPIENT_ID, storyRecipientId) - putLong(ARG_START_STORY_ID, storyId) - putBoolean(ARG_HIDDEN_STORIES, onlyIncludeHiddenStories) - putParcelable(ARG_CROSSFADE_TEXT_MODEL, storyThumbTextModel) - putParcelable(ARG_CROSSFADE_IMAGE_URI, storyThumbUri) - putString(ARG_CROSSFADE_IMAGE_BLUR, storyThumbBlur) - putParcelableArrayList(ARG_RECIPIENT_IDS, ArrayList(recipientIds)) + putParcelable(ARGS, storyViewerArgs) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerPagerAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerPagerAdapter.kt index bd8883a6e6..d93c313286 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerPagerAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerPagerAdapter.kt @@ -6,7 +6,12 @@ import androidx.viewpager2.adapter.FragmentStateAdapter import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.stories.viewer.page.StoryViewerPageFragment -class StoryViewerPagerAdapter(fragment: Fragment, private val initialStoryId: Long) : FragmentStateAdapter(fragment) { +class StoryViewerPagerAdapter( + fragment: Fragment, + private val initialStoryId: Long, + private val isFromNotification: Boolean, + private val groupReplyStartPosition: Int +) : FragmentStateAdapter(fragment) { private var pages: List = emptyList() @@ -21,7 +26,7 @@ class StoryViewerPagerAdapter(fragment: Fragment, private val initialStoryId: Lo override fun getItemCount(): Int = pages.size override fun createFragment(position: Int): Fragment { - return StoryViewerPageFragment.create(pages[position], initialStoryId) + return StoryViewerPageFragment.create(pages[position], initialStoryId, isFromNotification, groupReplyStartPosition) } private class Callback( diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerViewModel.kt index 977d891712..268f61fbc5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerViewModel.kt @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.stories.viewer -import android.net.Uri import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel @@ -9,27 +8,21 @@ import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.kotlin.plusAssign -import org.thoughtcrime.securesms.blurhash.BlurHash import org.thoughtcrime.securesms.recipients.RecipientId -import org.thoughtcrime.securesms.stories.StoryTextPostModel +import org.thoughtcrime.securesms.stories.StoryViewerArgs import org.thoughtcrime.securesms.util.rx.RxStore import kotlin.math.max class StoryViewerViewModel( - private val startRecipientId: RecipientId, - private val onlyIncludeHiddenStories: Boolean, - storyThumbTextModel: StoryTextPostModel?, - storyThumbUri: Uri?, - storyThumbBlur: BlurHash?, - private val recipientIds: List, + private val storyViewerArgs: StoryViewerArgs, private val repository: StoryViewerRepository, ) : ViewModel() { private val store = RxStore( StoryViewerState( crossfadeSource = when { - storyThumbTextModel != null -> StoryViewerState.CrossfadeSource.TextModel(storyThumbTextModel) - storyThumbUri != null -> StoryViewerState.CrossfadeSource.ImageUri(storyThumbUri, storyThumbBlur) + storyViewerArgs.storyThumbTextModel != null -> StoryViewerState.CrossfadeSource.TextModel(storyViewerArgs.storyThumbTextModel) + storyViewerArgs.storyThumbUri != null -> StoryViewerState.CrossfadeSource.ImageUri(storyViewerArgs.storyThumbUri, storyViewerArgs.storyThumbBlur) else -> StoryViewerState.CrossfadeSource.None } ) @@ -46,10 +39,17 @@ class StoryViewerViewModel( private val childScrollStatePublisher: MutableLiveData = MutableLiveData(false) val isChildScrolling: LiveData = childScrollStatePublisher + var hasConsumedInitialState = false + private set + init { refresh() } + fun consumeInitialState() { + hasConsumedInitialState = true + } + fun setContentIsReady() { store.update { it.copy(loadState = it.loadState.copy(isContentReady = true)) @@ -67,10 +67,10 @@ class StoryViewerViewModel( } private fun getStories(): Single> { - return if (recipientIds.isNotEmpty()) { - Single.just(recipientIds) + return if (storyViewerArgs.recipientIds.isNotEmpty()) { + Single.just(storyViewerArgs.recipientIds) } else { - repository.getStories(onlyIncludeHiddenStories) + repository.getStories(storyViewerArgs.isInHiddenStoryMode) } } @@ -148,7 +148,7 @@ class StoryViewerViewModel( return if (page > -1) { page } else { - val indexOfStartRecipient = recipientIds.indexOf(startRecipientId) + val indexOfStartRecipient = recipientIds.indexOf(storyViewerArgs.recipientId) if (indexOfStartRecipient == -1) { 0 } else { @@ -162,23 +162,13 @@ class StoryViewerViewModel( } class Factory( - private val startRecipientId: RecipientId, - private val onlyIncludeHiddenStories: Boolean, - private val storyThumbTextModel: StoryTextPostModel?, - private val storyThumbUri: Uri?, - private val storyThumbBlur: BlurHash?, - private val recipientIds: List, + private val storyViewerArgs: StoryViewerArgs, private val repository: StoryViewerRepository ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { return modelClass.cast( StoryViewerViewModel( - startRecipientId, - onlyIncludeHiddenStories, - storyThumbTextModel, - storyThumbUri, - storyThumbBlur, - recipientIds, + storyViewerArgs, repository ) ) as T diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt index 6ff3f28c1c..c7bad611f3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt @@ -23,7 +23,6 @@ import androidx.core.view.doOnNextLayout import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels -import androidx.lifecycle.LiveDataReactiveStreams import com.google.android.material.button.MaterialButton import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Observable @@ -115,6 +114,12 @@ class StoryViewerPageFragment : private val initialStoryId: Long get() = requireArguments().getLong(ARG_STORY_ID, -1L) + private val isFromNotification: Boolean + get() = requireArguments().getBoolean(ARG_IS_FROM_NOTIFICATION, false) + + private val groupReplyStartPosition: Int + get() = requireArguments().getInt(ARG_GROUP_REPLY_START_POSITION, -1) + @SuppressLint("ClickableViewAccessibility") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { callback = requireListener() @@ -249,7 +254,7 @@ class StoryViewerPageFragment : viewModel.setIsUserScrollingParent(isScrolling) } - LiveDataReactiveStreams.fromPublisher(sharedViewModel.state.distinctUntilChanged()).observe(viewLifecycleOwner) { parentState -> + lifecycleDisposable += sharedViewModel.state.distinctUntilChanged().observeOn(AndroidSchedulers.mainThread()).subscribe { parentState -> if (parentState.pages.size <= parentState.page) { viewModel.setIsSelectedPage(false) } else if (storyRecipientId == parentState.pages[parentState.page]) { @@ -270,51 +275,54 @@ class StoryViewerPageFragment : } } - LiveDataReactiveStreams - .fromPublisher(viewModel.state.observeOn(AndroidSchedulers.mainThread())) - .observe(viewLifecycleOwner) { state -> - if (state.posts.isNotEmpty() && state.selectedPostIndex in state.posts.indices) { - val post = state.posts[state.selectedPostIndex] + lifecycleDisposable += viewModel.state.observeOn(AndroidSchedulers.mainThread()).subscribe { state -> + if (state.posts.isNotEmpty() && state.selectedPostIndex in state.posts.indices) { + val post = state.posts[state.selectedPostIndex] - presentViewsAndReplies(post, state.replyState) - presentSenderAvatar(senderAvatar, post) - presentGroupAvatar(groupAvatar, post) - presentFrom(from, post) - presentDate(date, post) - presentDistributionList(distributionList, post) - presentCaption(caption, largeCaption, largeCaptionOverlay, post) - presentBlur(blurContainer, post) + presentViewsAndReplies(post, state.replyState) + presentSenderAvatar(senderAvatar, post) + presentGroupAvatar(groupAvatar, post) + presentFrom(from, post) + presentDate(date, post) + presentDistributionList(distributionList, post) + presentCaption(caption, largeCaption, largeCaptionOverlay, post) + presentBlur(blurContainer, post) - val durations: Map = state.posts - .mapIndexed { index, storyPost -> - index to when { - storyPost.content.isVideo() -> -1L - storyPost.content is StoryPost.Content.TextContent -> calculateDurationForText(storyPost.content) - else -> DEFAULT_DURATION - } + val durations: Map = state.posts + .mapIndexed { index, storyPost -> + index to when { + storyPost.content.isVideo() -> -1L + storyPost.content is StoryPost.Content.TextContent -> calculateDurationForText(storyPost.content) + else -> DEFAULT_DURATION } - .toMap() - - if (progressBar.segmentCount != state.posts.size || progressBar.segmentDurations != durations) { - progressBar.segmentCount = state.posts.size - progressBar.segmentDurations = durations } + .toMap() - presentStory(post, state.selectedPostIndex) - presentSlate(post) - - if (!storyCrossfader.setTargetView(post.conversationMessage.messageRecord as MmsMessageRecord)) { - onReadyToAnimate() - } - - viewModel.setAreSegmentsInitialized(true) - } else if (state.selectedPostIndex >= state.posts.size) { - callback.onFinishedPosts(storyRecipientId) - } else if (state.selectedPostIndex < 0) { - callback.onGoToPreviousStory(storyRecipientId) + if (progressBar.segmentCount != state.posts.size || progressBar.segmentDurations != durations) { + progressBar.segmentCount = state.posts.size + progressBar.segmentDurations = durations } + + presentStory(post, state.selectedPostIndex) + presentSlate(post) + + if (!storyCrossfader.setTargetView(post.conversationMessage.messageRecord as MmsMessageRecord)) { + onReadyToAnimate() + } + + viewModel.setAreSegmentsInitialized(true) + } else if (state.selectedPostIndex >= state.posts.size) { + callback.onFinishedPosts(storyRecipientId) + } else if (state.selectedPostIndex < 0) { + callback.onGoToPreviousStory(storyRecipientId) } + if (state.isDisplayingInitialState && isFromNotification && !sharedViewModel.hasConsumedInitialState) { + sharedViewModel.consumeInitialState() + startReply(isFromNotification = true, groupReplyStartPosition = groupReplyStartPosition) + } + } + viewModel.storyViewerPlaybackState.observe(viewLifecycleOwner) { state -> if (state.isPaused) { pauseProgress() @@ -484,13 +492,25 @@ class StoryViewerPageFragment : videoControlsDelegate.pause() } - private fun startReply() { + private fun startReply(isFromNotification: Boolean = false, groupReplyStartPosition: Int = -1) { + val storyPostId: Long = viewModel.getPost().id val replyFragment: DialogFragment = when (viewModel.getSwipeToReplyState()) { StoryViewerPageState.ReplyState.NONE -> return - StoryViewerPageState.ReplyState.SELF -> StoryViewsBottomSheetDialogFragment.create(viewModel.getPost().id) - StoryViewerPageState.ReplyState.GROUP -> StoryGroupReplyBottomSheetDialogFragment.create(viewModel.getPost().id, viewModel.getPost().group!!.id) - StoryViewerPageState.ReplyState.PRIVATE -> StoryDirectReplyDialogFragment.create(viewModel.getPost().id) - StoryViewerPageState.ReplyState.GROUP_SELF -> StoryViewsAndRepliesDialogFragment.create(viewModel.getPost().id, viewModel.getPost().group!!.id, getViewsAndRepliesDialogStartPage()) + StoryViewerPageState.ReplyState.SELF -> StoryViewsBottomSheetDialogFragment.create(storyPostId) + StoryViewerPageState.ReplyState.GROUP -> StoryGroupReplyBottomSheetDialogFragment.create( + storyPostId, + viewModel.getPost().group!!.id, + isFromNotification, + groupReplyStartPosition + ) + StoryViewerPageState.ReplyState.PRIVATE -> StoryDirectReplyDialogFragment.create(storyPostId) + StoryViewerPageState.ReplyState.GROUP_SELF -> StoryViewsAndRepliesDialogFragment.create( + storyPostId, + viewModel.getPost().group!!.id, + if (isFromNotification) StoryViewsAndRepliesDialogFragment.StartPage.REPLIES else getViewsAndRepliesDialogStartPage(), + isFromNotification, + groupReplyStartPosition + ) } if (viewModel.getSwipeToReplyState() == StoryViewerPageState.ReplyState.PRIVATE) { @@ -805,12 +825,16 @@ class StoryViewerPageFragment : private const val ARG_STORY_RECIPIENT_ID = "arg.story.recipient.id" private const val ARG_STORY_ID = "arg.story.id" + private const val ARG_IS_FROM_NOTIFICATION = "is_from_notification" + private const val ARG_GROUP_REPLY_START_POSITION = "group_reply_start_position" - fun create(recipientId: RecipientId, initialStoryId: Long): Fragment { + fun create(recipientId: RecipientId, initialStoryId: Long, isFromNotification: Boolean, groupReplyStartPosition: Int): Fragment { return StoryViewerPageFragment().apply { arguments = Bundle().apply { putParcelable(ARG_STORY_RECIPIENT_ID, recipientId) putLong(ARG_STORY_ID, initialStoryId) + putBoolean(ARG_IS_FROM_NOTIFICATION, isFromNotification) + putInt(ARG_GROUP_REPLY_START_POSITION, groupReplyStartPosition) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageState.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageState.kt index d3c1fe6034..52e8f47c69 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageState.kt @@ -4,7 +4,8 @@ data class StoryViewerPageState( val posts: List = emptyList(), val selectedPostIndex: Int = 0, val replyState: ReplyState = ReplyState.NONE, - val isFirstPage: Boolean = false + val isFirstPage: Boolean = false, + val isDisplayingInitialState: Boolean = false ) { /** * Indicates which Reply method is available when the user swipes on the dialog 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 f9ebe2132e..ffd0162f8e 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 @@ -49,8 +49,10 @@ class StoryViewerPageViewModel( disposables.clear() disposables += repository.getStoryPostsFor(recipientId).subscribe { posts -> store.update { state -> + var isDisplayingInitialState = false val startIndex = if (state.posts.isEmpty() && initialStoryId > 0) { val initialIndex = posts.indexOfFirst { it.id == initialStoryId } + isDisplayingInitialState = initialIndex > -1 initialIndex.takeIf { it > -1 } ?: state.selectedPostIndex } else if (state.posts.isEmpty()) { val initialPost = getNextUnreadPost(posts) @@ -63,7 +65,8 @@ class StoryViewerPageViewModel( state.copy( posts = posts, replyState = resolveSwipeToReplyState(state, startIndex), - selectedPostIndex = startIndex + selectedPostIndex = startIndex, + isDisplayingInitialState = isDisplayingInitialState ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyBottomSheetDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyBottomSheetDialogFragment.kt index 793fe132bc..d403adffb1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyBottomSheetDialogFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyBottomSheetDialogFragment.kt @@ -34,6 +34,12 @@ class StoryGroupReplyBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDi private val groupRecipientId: RecipientId get() = requireArguments().getParcelable(ARG_GROUP_RECIPIENT_ID)!! + private val isFromNotification: Boolean + get() = requireArguments().getBoolean(ARG_IS_FROM_NOTIFICATION, false) + + private val groupReplyStartPosition: Int + get() = requireArguments().getInt(ARG_GROUP_REPLY_START_POSITION, -1) + override val peekHeightPercentage: Float = 1f private val lifecycleDisposable = LifecycleDisposable() @@ -52,7 +58,7 @@ class StoryGroupReplyBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDi lifecycleDisposable.bindTo(viewLifecycleOwner) if (savedInstanceState == null) { childFragmentManager.beginTransaction() - .replace(R.id.fragment_container, StoryGroupReplyFragment.create(storyId, groupRecipientId)) + .replace(R.id.fragment_container, StoryGroupReplyFragment.create(storyId, groupRecipientId, isFromNotification, groupReplyStartPosition)) .commitAllowingStateLoss() } @@ -109,12 +115,16 @@ class StoryGroupReplyBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDi companion object { private const val ARG_STORY_ID = "arg.story.id" private const val ARG_GROUP_RECIPIENT_ID = "arg.group.recipient.id" + private const val ARG_IS_FROM_NOTIFICATION = "is_from_notification" + private const val ARG_GROUP_REPLY_START_POSITION = "group_reply_start_position" - fun create(storyId: Long, groupRecipientId: RecipientId): DialogFragment { + fun create(storyId: Long, groupRecipientId: RecipientId, isFromNotification: Boolean, groupReplyStartPosition: Int): DialogFragment { return StoryGroupReplyBottomSheetDialogFragment().apply { arguments = Bundle().apply { putLong(ARG_STORY_ID, storyId) putParcelable(ARG_GROUP_RECIPIENT_ID, groupRecipientId) + putBoolean(ARG_IS_FROM_NOTIFICATION, isFromNotification) + putInt(ARG_GROUP_REPLY_START_POSITION, groupReplyStartPosition) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyFragment.kt index 939fc68f7d..48d8c5e5b3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyFragment.kt @@ -22,16 +22,19 @@ import org.thoughtcrime.securesms.components.emoji.MediaKeyboard import org.thoughtcrime.securesms.components.mention.MentionAnnotation import org.thoughtcrime.securesms.components.settings.DSLConfiguration import org.thoughtcrime.securesms.components.settings.configure +import org.thoughtcrime.securesms.conversation.MarkReadHelper import org.thoughtcrime.securesms.conversation.colors.Colorizer import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerFragment import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerViewModel import org.thoughtcrime.securesms.database.model.Mention +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.jobs.RetrieveProfileJob import org.thoughtcrime.securesms.keyboard.KeyboardPage import org.thoughtcrime.securesms.keyboard.KeyboardPagerViewModel import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardCallback import org.thoughtcrime.securesms.mediasend.v2.UntrustedRecords +import org.thoughtcrime.securesms.notifications.v2.ConversationId import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDialogFragment import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId @@ -44,7 +47,7 @@ import org.thoughtcrime.securesms.util.DeleteDialog import org.thoughtcrime.securesms.util.FragmentDialogs.displayInDialogAboveAnchor import org.thoughtcrime.securesms.util.LifecycleDisposable import org.thoughtcrime.securesms.util.ServiceUtil -import org.thoughtcrime.securesms.util.SnapToTopDataObserver +import org.thoughtcrime.securesms.util.ViewUtil import org.thoughtcrime.securesms.util.adapter.mapping.PagingMappingAdapter import org.thoughtcrime.securesms.util.fragments.findListener import org.thoughtcrime.securesms.util.fragments.requireListener @@ -97,10 +100,19 @@ class StoryGroupReplyFragment : private val groupRecipientId: RecipientId get() = requireArguments().getParcelable(ARG_GROUP_RECIPIENT_ID)!! + private val isFromNotification: Boolean + get() = requireArguments().getBoolean(ARG_IS_FROM_NOTIFICATION, false) + + private val groupReplyStartPosition: Int + get() = requireArguments().getInt(ARG_GROUP_REPLY_START_POSITION, -1) + private lateinit var recyclerView: RecyclerView private lateinit var adapter: PagingMappingAdapter - private lateinit var snapToTopDataObserver: SnapToTopDataObserver + private lateinit var dataObserver: RecyclerView.AdapterDataObserver private lateinit var composer: StoryReplyComposer + + private var markReadHelper: MarkReadHelper? = null + private var currentChild: StoryViewsAndRepliesPagerParent.Child? = null private var resendBody: CharSequence? = null @@ -120,7 +132,7 @@ class StoryGroupReplyFragment : val emptyNotice: View = requireView().findViewById(R.id.empty_notice) adapter = PagingMappingAdapter() - val layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, true) + val layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false) recyclerView.layoutManager = layoutManager recyclerView.adapter = adapter recyclerView.itemAnimator = null @@ -131,6 +143,18 @@ class StoryGroupReplyFragment : onPageSelected(findListener()?.selectedChild ?: StoryViewsAndRepliesPagerParent.Child.REPLIES) viewModel.state.observe(viewLifecycleOwner) { state -> + if (markReadHelper == null && state.threadId > 0L) { + if (isResumed) { + ApplicationDependencies.getMessageNotifier().setVisibleThread(ConversationId(state.threadId, storyId)) + } + + markReadHelper = MarkReadHelper(ConversationId(state.threadId, storyId), requireContext(), viewLifecycleOwner) + + if (isFromNotification) { + markReadHelper?.onViewsRevealed(System.currentTimeMillis()) + } + } + emptyNotice.visible = state.noReplies && state.loadState == StoryGroupReplyState.LoadState.READY colorizer.onNameColorsChanged(state.nameColors) } @@ -139,26 +163,39 @@ class StoryGroupReplyFragment : adapter.setPagingController(controller) } + var consumed = false viewModel.pageData.observe(viewLifecycleOwner) { pageData -> - adapter.submitList(getConfiguration(pageData).toMappingModelList()) + adapter.submitList(getConfiguration(pageData).toMappingModelList()) { + if (!consumed && groupReplyStartPosition >= 0 && adapter.hasItem(groupReplyStartPosition)) { + consumed = true + recyclerView.post { recyclerView.scrollToPosition(groupReplyStartPosition) } + } + } } - snapToTopDataObserver = SnapToTopDataObserver( - recyclerView, - object : SnapToTopDataObserver.ScrollRequestValidator { - override fun isPositionStillValid(position: Int): Boolean { - return position >= 0 && position < adapter.itemCount - } - - override fun isItemAtPositionLoaded(position: Int): Boolean { - return adapter.hasItem(position) - } - }, - null - ) - adapter.registerAdapterDataObserver(snapToTopDataObserver) + dataObserver = GroupDataObserver() + adapter.registerAdapterDataObserver(dataObserver) initializeMentions() + + if (savedInstanceState == null && isFromNotification) { + ViewUtil.focusAndShowKeyboard(composer) + } + + recyclerView.addOnScrollListener(GroupReplyScrollObserver()) + } + + override fun onResume() { + super.onResume() + val threadId = viewModel.stateSnapshot.threadId + if (threadId != 0L) { + ApplicationDependencies.getMessageNotifier().setVisibleThread(ConversationId(threadId, storyId)) + } + } + + override fun onPause() { + super.onPause() + ApplicationDependencies.getMessageNotifier().setVisibleThread(null) } override fun onDestroyView() { @@ -168,9 +205,23 @@ class StoryGroupReplyFragment : composer.input.setMentionValidator(null) } + private fun postMarkAsReadRequest() { + if (adapter.itemCount == 0 || markReadHelper == null) { + return + } + + val lastVisibleItem = (recyclerView.layoutManager as LinearLayoutManager).findLastVisibleItemPosition() + val adapterItem = adapter.getItem(lastVisibleItem) + if (adapterItem == null || adapterItem !is StoryGroupReplyItem.DataWrapper) { + return + } + + markReadHelper?.onViewsRevealed(adapterItem.storyGroupReplyItemData.sentAtMillis) + } + private fun getConfiguration(pageData: List): DSLConfiguration { return configure { - pageData.filterNotNull().forEach { + pageData.forEach { when (it.replyBody) { is StoryGroupReplyItemData.ReplyBody.Text -> { customPref( @@ -294,9 +345,6 @@ class StoryGroupReplyFragment : Toast.makeText(context, R.string.message_details_recipient__failed_to_send, Toast.LENGTH_SHORT).show() } } - }, - onComplete = { - snapToTopDataObserver.requestScrollPosition(0) } ) } @@ -379,17 +427,47 @@ class StoryGroupReplyFragment : } } + private inner class GroupReplyScrollObserver : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + postMarkAsReadRequest() + } + + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + postMarkAsReadRequest() + } + } + + private inner class GroupDataObserver : RecyclerView.AdapterDataObserver() { + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + if (itemCount == 0) { + return + } + + val item = adapter.getItem(positionStart) + if (positionStart == adapter.itemCount - 1 && item is StoryGroupReplyItem.DataWrapper) { + val isOutgoing = item.storyGroupReplyItemData.sender == Recipient.self() + if (isOutgoing || (!isOutgoing && !recyclerView.canScrollVertically(1))) { + recyclerView.post { recyclerView.scrollToPosition(positionStart) } + } + } + } + } + companion object { private val TAG = Log.tag(StoryGroupReplyFragment::class.java) private const val ARG_STORY_ID = "arg.story.id" private const val ARG_GROUP_RECIPIENT_ID = "arg.group.recipient.id" + private const val ARG_IS_FROM_NOTIFICATION = "is_from_notification" + private const val ARG_GROUP_REPLY_START_POSITION = "group_reply_start_position" - fun create(storyId: Long, groupRecipientId: RecipientId): Fragment { + fun create(storyId: Long, groupRecipientId: RecipientId, isFromNotification: Boolean, groupReplyStartPosition: Int): Fragment { return StoryGroupReplyFragment().apply { arguments = Bundle().apply { putLong(ARG_STORY_ID, storyId) putParcelable(ARG_GROUP_RECIPIENT_ID, groupRecipientId) + putBoolean(ARG_IS_FROM_NOTIFICATION, isFromNotification) + putInt(ARG_GROUP_REPLY_START_POSITION, groupReplyStartPosition) } } } @@ -412,9 +490,6 @@ class StoryGroupReplyFragment : Toast.makeText(context, R.string.message_details_recipient__failed_to_send, Toast.LENGTH_SHORT).show() } } - }, - onComplete = { - snapToTopDataObserver.requestScrollPosition(0) } ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyItem.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyItem.kt index 0460903104..9fd0eddeae 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyItem.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyItem.kt @@ -43,13 +43,13 @@ object StoryGroupReplyItem { } class TextModel( - val storyGroupReplyItemData: StoryGroupReplyItemData, + override val storyGroupReplyItemData: StoryGroupReplyItemData, val text: StoryGroupReplyItemData.ReplyBody.Text, @ColorInt val nameColor: Int, val onCopyClick: (TextModel) -> Unit, val onDeleteClick: (TextModel) -> Unit, val onMentionClick: (RecipientId) -> Unit - ) : PreferenceModel() { + ) : PreferenceModel(), DataWrapper { override fun areItemsTheSame(newItem: TextModel): Boolean { return storyGroupReplyItemData.sender == newItem.storyGroupReplyItemData.sender && storyGroupReplyItemData.sentAtMillis == newItem.storyGroupReplyItemData.sentAtMillis @@ -76,11 +76,11 @@ object StoryGroupReplyItem { } class RemoteDeleteModel( - val storyGroupReplyItemData: StoryGroupReplyItemData, + override val storyGroupReplyItemData: StoryGroupReplyItemData, val remoteDelete: StoryGroupReplyItemData.ReplyBody.RemoteDelete, val onDeleteClick: (RemoteDeleteModel) -> Unit, @ColorInt val nameColor: Int - ) : MappingModel { + ) : MappingModel, DataWrapper { override fun areItemsTheSame(newItem: RemoteDeleteModel): Boolean { return storyGroupReplyItemData.sender == newItem.storyGroupReplyItemData.sender && storyGroupReplyItemData.sentAtMillis == newItem.storyGroupReplyItemData.sentAtMillis @@ -105,10 +105,10 @@ object StoryGroupReplyItem { } class ReactionModel( - val storyGroupReplyItemData: StoryGroupReplyItemData, + override val storyGroupReplyItemData: StoryGroupReplyItemData, val reaction: StoryGroupReplyItemData.ReplyBody.Reaction, @ColorInt val nameColor: Int - ) : PreferenceModel() { + ) : PreferenceModel(), DataWrapper { override fun areItemsTheSame(newItem: ReactionModel): Boolean { return storyGroupReplyItemData.sender == newItem.storyGroupReplyItemData.sender && storyGroupReplyItemData.sentAtMillis == newItem.storyGroupReplyItemData.sentAtMillis @@ -134,6 +134,10 @@ object StoryGroupReplyItem { } } + interface DataWrapper { + val storyGroupReplyItemData: StoryGroupReplyItemData + } + private abstract class BaseViewHolder(itemView: View) : MappingViewHolder(itemView) { protected val avatar: AvatarImageView = itemView.findViewById(R.id.avatar) protected val name: FromTextView = itemView.findViewById(R.id.name) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyRepository.kt index 85c1024904..aca01125a7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyRepository.kt @@ -13,6 +13,12 @@ import org.thoughtcrime.securesms.recipients.RecipientId class StoryGroupReplyRepository { + fun getThreadId(storyId: Long): Single { + return Single.fromCallable { + SignalDatabase.mms.getThreadIdForMessage(storyId) + }.subscribeOn(Schedulers.io()) + } + fun getPagedReplies(parentStoryId: Long): Observable> { return Observable.create> { emitter -> fun refresh() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyState.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyState.kt index cd7cd1ded5..ef66710fed 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyState.kt @@ -4,6 +4,7 @@ import org.thoughtcrime.securesms.conversation.colors.NameColor import org.thoughtcrime.securesms.recipients.RecipientId data class StoryGroupReplyState( + val threadId: Long = 0L, val noReplies: Boolean = true, val nameColors: Map = emptyMap(), val loadState: LoadState = LoadState.INIT diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyViewModel.kt index 553f8e6064..49f366d9b1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyViewModel.kt @@ -21,6 +21,7 @@ class StoryGroupReplyViewModel(storyId: Long, repository: StoryGroupReplyReposit private val store = Store(StoryGroupReplyState()) private val disposables = CompositeDisposable() + val stateSnapshot: StoryGroupReplyState = store.state val state: LiveData = store.stateLiveData private val pagedData: MutableLiveData> = MutableLiveData() @@ -29,6 +30,10 @@ class StoryGroupReplyViewModel(storyId: Long, repository: StoryGroupReplyReposit val pageData: LiveData> init { + disposables += repository.getThreadId(storyId).subscribe { threadId -> + store.update { it.copy(threadId = threadId) } + } + disposables += repository.getPagedReplies(storyId).subscribe { pagedData.postValue(it) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/tabs/StoryViewsAndRepliesDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/tabs/StoryViewsAndRepliesDialogFragment.kt index cc09411f54..480d3c58c1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/tabs/StoryViewsAndRepliesDialogFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/tabs/StoryViewsAndRepliesDialogFragment.kt @@ -44,6 +44,12 @@ class StoryViewsAndRepliesDialogFragment : FixedRoundedCornerBottomSheetDialogFr private val startPageIndex: Int get() = requireArguments().getInt(ARG_START_PAGE) + private val isFromNotification: Boolean + get() = requireArguments().getBoolean(ARG_IS_FROM_NOTIFICATION, false) + + private val groupReplyStartPosition: Int + get() = requireArguments().getInt(ARG_GROUP_REPLY_START_POSITION, -1) + override val peekHeightPercentage: Float = 1f private lateinit var pager: ViewPager2 @@ -84,7 +90,7 @@ class StoryViewsAndRepliesDialogFragment : FixedRoundedCornerBottomSheetDialogFr val tabs: TabLayout = view.findViewById(R.id.tab_layout) ViewCompat.setNestedScrollingEnabled(tabs, false) - pager.adapter = StoryViewsAndRepliesPagerAdapter(this, storyId, groupRecipientId) + pager.adapter = StoryViewsAndRepliesPagerAdapter(this, storyId, groupRecipientId, isFromNotification, groupReplyStartPosition) pager.setCurrentItem(startPageIndex, false) TabLayoutMediator(tabs, pager) { tab, position -> @@ -168,13 +174,17 @@ class StoryViewsAndRepliesDialogFragment : FixedRoundedCornerBottomSheetDialogFr private const val ARG_STORY_ID = "arg.story.id" private const val ARG_START_PAGE = "arg.start.page" private const val ARG_GROUP_RECIPIENT_ID = "arg.group.recipient.id" + private const val ARG_IS_FROM_NOTIFICATION = "is_from_notification" + private const val ARG_GROUP_REPLY_START_POSITION = "group_reply_start_position" - fun create(storyId: Long, groupRecipientId: RecipientId, startPage: StartPage): DialogFragment { + fun create(storyId: Long, groupRecipientId: RecipientId, startPage: StartPage, isFromNotification: Boolean, groupReplyStartPosition: Int): DialogFragment { return StoryViewsAndRepliesDialogFragment().apply { arguments = Bundle().apply { putLong(ARG_STORY_ID, storyId) putInt(ARG_START_PAGE, startPage.index) putParcelable(ARG_GROUP_RECIPIENT_ID, groupRecipientId) + putBoolean(ARG_IS_FROM_NOTIFICATION, isFromNotification) + putInt(ARG_GROUP_REPLY_START_POSITION, groupReplyStartPosition) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/tabs/StoryViewsAndRepliesPagerAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/tabs/StoryViewsAndRepliesPagerAdapter.kt index 0001eebf9d..f04634a0b3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/tabs/StoryViewsAndRepliesPagerAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/tabs/StoryViewsAndRepliesPagerAdapter.kt @@ -10,7 +10,9 @@ import org.thoughtcrime.securesms.stories.viewer.views.StoryViewsFragment class StoryViewsAndRepliesPagerAdapter( fragment: Fragment, private val storyId: Long, - private val groupRecipientId: RecipientId + private val groupRecipientId: RecipientId, + private val isFromNotification: Boolean, + private val groupReplyStartPosition: Int ) : FragmentStateAdapter(fragment) { override fun getItemCount(): Int = 2 @@ -22,7 +24,7 @@ class StoryViewsAndRepliesPagerAdapter( override fun createFragment(position: Int): Fragment { return when (position) { 0 -> StoryViewsFragment.create(storyId) - 1 -> StoryGroupReplyFragment.create(storyId, groupRecipientId) + 1 -> StoryGroupReplyFragment.create(storyId, groupRecipientId, isFromNotification, groupReplyStartPosition) else -> throw IndexOutOfBoundsException() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/BubbleUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/BubbleUtil.java index 012c6ca39b..4e4c392547 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/BubbleUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/BubbleUtil.java @@ -20,7 +20,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.notifications.NotificationIds; import org.thoughtcrime.securesms.notifications.v2.NotificationFactory; -import org.thoughtcrime.securesms.notifications.v2.NotificationThread; +import org.thoughtcrime.securesms.notifications.v2.ConversationId; import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; @@ -76,12 +76,12 @@ public final class BubbleUtil { */ public static void displayAsBubble(@NonNull Context context, @NonNull RecipientId recipientId, long threadId) { if (Build.VERSION.SDK_INT >= CONVERSATION_SUPPORT_VERSION) { - NotificationThread notificationThread = NotificationThread.forConversation(threadId); + ConversationId conversationId = ConversationId.forConversation(threadId); SignalExecutors.BOUNDED.execute(() -> { if (canBubble(context, recipientId, threadId)) { NotificationManager notificationManager = ServiceUtil.getNotificationManager(context); StatusBarNotification[] notifications = notificationManager.getActiveNotifications(); - int threadNotificationId = NotificationIds.getNotificationIdForThread(notificationThread); + int threadNotificationId = NotificationIds.getNotificationIdForThread(conversationId); Notification activeThreadNotification = Stream.of(notifications) .filter(n -> n.getId() == threadNotificationId) .findFirst() @@ -89,7 +89,7 @@ public final class BubbleUtil { .orElse(null); if (activeThreadNotification != null && activeThreadNotification.deleteIntent != null) { - ApplicationDependencies.getMessageNotifier().updateNotification(context, notificationThread, BubbleState.SHOWN); + ApplicationDependencies.getMessageNotifier().updateNotification(context, conversationId, BubbleState.SHOWN); } else { Recipient recipient = Recipient.resolved(recipientId); NotificationFactory.notifyToBubbleConversation(context, recipient, threadId); diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/IdentityUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/IdentityUtil.java index 7c396365ba..bfe554e567 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/IdentityUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/IdentityUtil.java @@ -22,7 +22,7 @@ import org.thoughtcrime.securesms.database.MessageDatabase.InsertResult; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.model.IdentityRecord; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; -import org.thoughtcrime.securesms.notifications.v2.NotificationThread; +import org.thoughtcrime.securesms.notifications.v2.ConversationId; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.sms.IncomingIdentityDefaultMessage; @@ -141,7 +141,7 @@ public final class IdentityUtil { Optional insertResult = smsDatabase.insertMessageInbox(individualUpdate); if (insertResult.isPresent()) { - ApplicationDependencies.getMessageNotifier().updateNotification(context, NotificationThread.forConversation(insertResult.get().getThreadId())); + ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(insertResult.get().getThreadId())); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/adapter/mapping/PagingMappingAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/util/adapter/mapping/PagingMappingAdapter.java index 3b624dabee..f4dd63b666 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/adapter/mapping/PagingMappingAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/adapter/mapping/PagingMappingAdapter.java @@ -33,7 +33,7 @@ public class PagingMappingAdapter extends MappingAdapter { } @Override - protected @Nullable MappingModel getItem(int position) { + public @Nullable MappingModel getItem(int position) { if (pagingController != null) { pagingController.onDataNeededAroundIndex(position); } diff --git a/app/src/main/res/layout/stories_group_replies_fragment.xml b/app/src/main/res/layout/stories_group_replies_fragment.xml index 13cf9644f6..949c32e2a5 100644 --- a/app/src/main/res/layout/stories_group_replies_fragment.xml +++ b/app/src/main/res/layout/stories_group_replies_fragment.xml @@ -22,6 +22,7 @@ android:layout_width="0dp" android:layout_height="0dp" android:orientation="vertical" + android:scrollbars="vertical" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toTopOf="@+id/composer" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout/stories_views_fragment.xml b/app/src/main/res/layout/stories_views_fragment.xml index f9b036a969..479da1b1b3 100644 --- a/app/src/main/res/layout/stories_views_fragment.xml +++ b/app/src/main/res/layout/stories_views_fragment.xml @@ -22,6 +22,7 @@ android:layout_width="0dp" android:layout_height="0dp" android:orientation="vertical" + android:scrollbars="vertical" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 83ae54ba9f..136723428e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1952,6 +1952,8 @@ New message Message request You + + %1$s • Story Play video diff --git a/app/src/test/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerViewModelTest.kt b/app/src/test/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerViewModelTest.kt index 8585510b16..a2ef1ceea3 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerViewModelTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerViewModelTest.kt @@ -14,6 +14,7 @@ import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.stories.StoryViewerArgs class StoryViewerViewModelTest { private val testScheduler = TestScheduler() @@ -39,7 +40,14 @@ class StoryViewerViewModelTest { val injectedStories: List = (6L..10L).map(RecipientId::from) // WHEN - val testSubject = StoryViewerViewModel(injectedStories.first(), false, null, null, null, injectedStories, repository) + val testSubject = StoryViewerViewModel( + StoryViewerArgs( + recipientId = injectedStories.first(), + isInHiddenStoryMode = false, + recipientIds = injectedStories + ), + repository + ) testScheduler.triggerActions() // THEN @@ -55,7 +63,13 @@ class StoryViewerViewModelTest { whenever(repository.getStories(any())).doReturn(Single.just(stories)) // WHEN - val testSubject = StoryViewerViewModel(startStory, false, null, null, null, emptyList(), repository) + val testSubject = StoryViewerViewModel( + StoryViewerArgs( + recipientId = startStory, + isInHiddenStoryMode = false + ), + repository + ) testScheduler.triggerActions() // THEN @@ -71,7 +85,13 @@ class StoryViewerViewModelTest { val stories: List = (1L..5L).map(RecipientId::from) val startStory = RecipientId.from(1L) whenever(repository.getStories(any())).doReturn(Single.just(stories)) - val testSubject = StoryViewerViewModel(startStory, false, null, null, null, emptyList(), repository) + val testSubject = StoryViewerViewModel( + StoryViewerArgs( + recipientId = startStory, + isInHiddenStoryMode = false, + ), + repository + ) testScheduler.triggerActions() // WHEN @@ -91,7 +111,13 @@ class StoryViewerViewModelTest { val stories: List = (1L..5L).map(RecipientId::from) val startStory = stories.last() whenever(repository.getStories(any())).doReturn(Single.just(stories)) - val testSubject = StoryViewerViewModel(startStory, false, null, null, null, emptyList(), repository) + val testSubject = StoryViewerViewModel( + StoryViewerArgs( + recipientId = startStory, + isInHiddenStoryMode = false, + ), + repository + ) testScheduler.triggerActions() // WHEN @@ -111,7 +137,13 @@ class StoryViewerViewModelTest { val stories: List = (1L..5L).map(RecipientId::from) val startStory = stories.last() whenever(repository.getStories(any())).doReturn(Single.just(stories)) - val testSubject = StoryViewerViewModel(startStory, false, null, null, null, emptyList(), repository) + val testSubject = StoryViewerViewModel( + StoryViewerArgs( + recipientId = startStory, + isInHiddenStoryMode = false, + ), + repository + ) testScheduler.triggerActions() // WHEN @@ -131,7 +163,13 @@ class StoryViewerViewModelTest { val stories: List = (1L..5L).map(RecipientId::from) val startStory = stories.first() whenever(repository.getStories(any())).doReturn(Single.just(stories)) - val testSubject = StoryViewerViewModel(startStory, false, null, null, null, emptyList(), repository) + val testSubject = StoryViewerViewModel( + StoryViewerArgs( + recipientId = startStory, + isInHiddenStoryMode = false, + ), + repository + ) testScheduler.triggerActions() // WHEN @@ -151,7 +189,13 @@ class StoryViewerViewModelTest { val stories: List = (1L..5L).map(RecipientId::from) val startStory = stories.first() whenever(repository.getStories(any())).doReturn(Single.just(stories)) - val testSubject = StoryViewerViewModel(startStory, false, null, null, null, emptyList(), repository) + val testSubject = StoryViewerViewModel( + StoryViewerArgs( + recipientId = startStory, + isInHiddenStoryMode = false, + ), + repository + ) testScheduler.triggerActions() // WHEN