diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/MmsDatabaseTest_stories.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/database/MmsDatabaseTest_stories.kt new file mode 100644 index 000000000..aafbf618e --- /dev/null +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/database/MmsDatabaseTest_stories.kt @@ -0,0 +1,141 @@ +package org.thoughtcrime.securesms.database + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.thoughtcrime.securesms.database.model.DistributionListId +import org.thoughtcrime.securesms.database.model.StoryType +import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.mms.IncomingMediaMessage +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.recipients.RecipientId +import org.whispersystems.signalservice.api.push.ACI +import org.whispersystems.signalservice.api.push.PNI +import org.whispersystems.signalservice.api.push.ServiceId +import java.util.UUID + +@Suppress("ClassName") +@RunWith(AndroidJUnit4::class) +class MmsDatabaseTest_stories { + + private lateinit var mms: MmsDatabase + + private val localAci = ACI.from(UUID.randomUUID()) + private val localPni = PNI.from(UUID.randomUUID()) + + private lateinit var myStory: Recipient + private lateinit var recipients: List + + @Before + fun setUp() { + mms = SignalDatabase.mms + + mms.deleteAllThreads() + + SignalStore.account().setAci(localAci) + SignalStore.account().setPni(localPni) + + myStory = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromDistributionListId(DistributionListId.MY_STORY)) + recipients = (0 until 5).map { SignalDatabase.recipients.getOrInsertFromServiceId(ServiceId.from(UUID.randomUUID())) } + } + + @Test + fun givenNoStories_whenIGetOrderedStoryRecipientsAndIds_thenIExpectAnEmptyList() { + // WHEN + val result = mms.orderedStoryRecipientsAndIds + + // THEN + assertEquals(0, result.size) + } + + @Test + fun givenOneOutgoingAndOneIncomingStory_whenIGetOrderedStoryRecipientsAndIds_thenIExpectIncomingThenOutgoing() { + // GIVEN + val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(myStory) + val sender = recipients[0] + + MmsHelper.insert( + recipient = myStory, + sentTimeMillis = 1, + storyType = StoryType.STORY_WITH_REPLIES, + threadId = threadId + ) + + MmsHelper.insert( + IncomingMediaMessage( + from = sender, + sentTimeMillis = 2, + serverTimeMillis = 2, + receivedTimeMillis = 2, + storyType = StoryType.STORY_WITH_REPLIES + ), + -1L + ) + + // WHEN + val result = mms.orderedStoryRecipientsAndIds + + // THEN + assertEquals(listOf(sender.toLong(), myStory.id.toLong()), result.map { it.recipientId.toLong() }) + } + + @Test + fun givenAStory_whenISetIncomingStoryMessageViewed_thenIExpectASetReceiptTimestamp() { + // GIVEN + val sender = recipients[0] + val messageId = MmsHelper.insert( + IncomingMediaMessage( + from = sender, + sentTimeMillis = 2, + serverTimeMillis = 2, + receivedTimeMillis = 2, + storyType = StoryType.STORY_WITH_REPLIES + ), + -1L + ).get().messageId + + val messageBeforeMark = SignalDatabase.mms.getMessageRecord(messageId) + assertFalse(messageBeforeMark.incomingStoryViewedAtTimestamp > 0) + + // WHEN + SignalDatabase.mms.setIncomingMessageViewed(messageId) + + // THEN + val messageAfterMark = SignalDatabase.mms.getMessageRecord(messageId) + assertTrue(messageAfterMark.incomingStoryViewedAtTimestamp > 0) + } + + @Test + fun given5ViewedStories_whenIGetOrderedStoryRecipientsAndIds_thenIExpectLatestViewedFirst() { + // GIVEN + val messageIds = recipients.take(5).map { + MmsHelper.insert( + IncomingMediaMessage( + from = it, + sentTimeMillis = 2, + serverTimeMillis = 2, + receivedTimeMillis = 2, + storyType = StoryType.STORY_WITH_REPLIES, + ), + -1L + ).get().messageId + } + + val randomizedOrderedIds = messageIds.shuffled() + randomizedOrderedIds.forEach { + SignalDatabase.mms.setIncomingMessageViewed(it) + Thread.sleep(5) + } + + // WHEN + val result = SignalDatabase.mms.orderedStoryRecipientsAndIds + val resultOrderedIds = result.map { it.messageId } + + // THEN + assertEquals(randomizedOrderedIds.reversed(), resultOrderedIds) + } +} diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/MmsHelper.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/database/MmsHelper.kt index ae85eee77..b566faa32 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/MmsHelper.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/database/MmsHelper.kt @@ -1,8 +1,10 @@ package org.thoughtcrime.securesms.database import org.thoughtcrime.securesms.database.model.StoryType +import org.thoughtcrime.securesms.mms.IncomingMediaMessage import org.thoughtcrime.securesms.mms.OutgoingMediaMessage import org.thoughtcrime.securesms.recipients.Recipient +import java.util.Optional /** * Helper methods for inserting an MMS message into the MMS table. @@ -52,4 +54,11 @@ object MmsHelper { ): Long { return SignalDatabase.mms.insertMessageOutbox(message, threadId, false, GroupReceiptDatabase.STATUS_UNKNOWN, null) } + + fun insert( + message: IncomingMediaMessage, + threadId: Long + ): Optional { + return SignalDatabase.mms.insertSecureDecryptedMessageInbox(message, threadId) + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/segmentedprogressbar/SegmentedProgressBar.kt b/app/src/main/java/org/thoughtcrime/securesms/components/segmentedprogressbar/SegmentedProgressBar.kt index 3d728e90f..af2acd54c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/segmentedprogressbar/SegmentedProgressBar.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/segmentedprogressbar/SegmentedProgressBar.kt @@ -318,7 +318,7 @@ class SegmentedProgressBar : View, ViewPager.OnPageChangeListener, View.OnTouchL if (nextSegmentIndex >= segmentCount) { this.listener?.onFinished() } else { - restartSegment() + loadSegment(offset = 0, userAction = false) } return } 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 91b88946e..4d1bf97df 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java @@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.database.documents.NetworkFailure; import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.SmsMessageRecord; +import org.thoughtcrime.securesms.database.model.StoryResult; import org.thoughtcrime.securesms.database.model.StoryViewState; import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange; import org.thoughtcrime.securesms.insights.InsightsConstants; @@ -185,8 +186,7 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns public abstract boolean isStory(long messageId); public abstract @NonNull Reader getOutgoingStoriesTo(@NonNull RecipientId recipientId); public abstract @NonNull Reader getAllOutgoingStories(boolean reverse); - public abstract @NonNull Reader getAllStories(); - public abstract @NonNull List getAllStoriesRecipientsList(); + public abstract @NonNull List getOrderedStoryRecipientsAndIds(); public abstract @NonNull Reader getAllStoriesFor(@NonNull RecipientId recipientId); public abstract @NonNull MessageId getStoryId(@NonNull RecipientId authorId, long sentTimestamp) throws NoSuchMessageException; public abstract int getNumberOfStoryReplies(long parentStoryId); 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 9e67ce209..e51ae6cce 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -57,6 +57,7 @@ import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord; import org.thoughtcrime.securesms.database.model.ParentStoryId; import org.thoughtcrime.securesms.database.model.Quote; import org.thoughtcrime.securesms.database.model.SmsMessageRecord; +import org.thoughtcrime.securesms.database.model.StoryResult; import org.thoughtcrime.securesms.database.model.StoryType; import org.thoughtcrime.securesms.database.model.StoryViewState; import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList; @@ -424,6 +425,7 @@ public class MmsDatabase extends MessageDatabase { ContentValues contentValues = new ContentValues(); contentValues.put(VIEWED_RECEIPT_COUNT, 1); + contentValues.put(RECEIPT_TIMESTAMP, System.currentTimeMillis()); database.update(TABLE_NAME, contentValues, ID_WHERE, SqlUtil.buildArgs(CursorUtil.requireLong(cursor, ID))); } @@ -577,11 +579,6 @@ public class MmsDatabase extends MessageDatabase { return new Reader(rawQuery(where, null, reverse, -1L)); } - @Override - public @NonNull MessageDatabase.Reader getAllStories() { - return new Reader(rawQuery(IS_STORY_CLAUSE, null, false, -1L)); - } - @Override public @NonNull MessageDatabase.Reader getAllStoriesFor(@NonNull RecipientId recipientId) { long threadId = SignalDatabase.threads().getThreadIdIfExistsFor(recipientId); @@ -653,25 +650,39 @@ public class MmsDatabase extends MessageDatabase { } @Override - public @NonNull List getAllStoriesRecipientsList() { - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - String query = "SELECT " + - "DISTINCT " + ThreadDatabase.RECIPIENT_ID + " " + - "FROM " + TABLE_NAME + " JOIN " + ThreadDatabase.TABLE_NAME + " " + - "ON " + TABLE_NAME + "." + THREAD_ID + " = " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ID + " " + - "WHERE " + IS_STORY_CLAUSE + " " + - "ORDER BY " + TABLE_NAME + "." + DATE_SENT + " DESC, " + TABLE_NAME + "." + VIEWED_RECEIPT_COUNT + " ASC"; - List recipientIds; + public @NonNull List getOrderedStoryRecipientsAndIds() { + SQLiteDatabase db = getReadableDatabase(); + String query = "SELECT\n" + + " mms.date AS sent_timestamp,\n" + + " mms._id AS mms_id,\n" + + " thread_recipient_id,\n" + + " (" + getOutgoingTypeClause() + ") AS is_outgoing,\n" + + " viewed_receipt_count,\n" + + " mms.date,\n" + + " receipt_timestamp,\n" + + " (" + getOutgoingTypeClause() + ") = 0 AND viewed_receipt_count = 0 AS is_unread\n" + + "FROM mms\n" + + "JOIN thread\n" + + "ON mms.thread_id = thread._id\n" + + "WHERE is_story > 0 AND remote_deleted = 0\n" + + "ORDER BY\n" + + "is_unread DESC,\n" + + "CASE WHEN is_outgoing = 0 AND viewed_receipt_count = 0 THEN mms.date END DESC,\n" + + "CASE WHEN is_outgoing = 0 AND viewed_receipt_count > 0 THEN receipt_timestamp END DESC,\n" + + "CASE WHEN is_outgoing = 1 THEN mms.date END DESC"; + List results; try (Cursor cursor = db.rawQuery(query, null)) { if (cursor != null) { - recipientIds = new ArrayList<>(cursor.getCount()); + results = new ArrayList<>(cursor.getCount()); while (cursor.moveToNext()) { - recipientIds.add(RecipientId.from(CursorUtil.requireLong(cursor, ThreadDatabase.RECIPIENT_ID))); + results.add(new StoryResult(RecipientId.from(CursorUtil.requireLong(cursor, ThreadDatabase.RECIPIENT_ID)), + CursorUtil.requireLong(cursor, "mms_id"), + CursorUtil.requireLong(cursor, "sent_timestamp"))); } - return recipientIds; + return results; } } 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 f5062fcf0..717e9d9a5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -41,6 +41,7 @@ import org.thoughtcrime.securesms.database.model.GroupCallUpdateDetailsUtil; import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.SmsMessageRecord; +import org.thoughtcrime.securesms.database.model.StoryResult; import org.thoughtcrime.securesms.database.model.StoryViewState; import org.thoughtcrime.securesms.database.model.databaseprotos.GroupCallUpdateDetails; import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails; @@ -1405,12 +1406,7 @@ public class SmsDatabase extends MessageDatabase { } @Override - public @NonNull MessageDatabase.Reader getAllStories() { - throw new UnsupportedOperationException(); - } - - @Override - public @NonNull List getAllStoriesRecipientsList() { + public @NonNull List getOrderedStoryRecipientsAndIds() { throw new UnsupportedOperationException(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java index e34e7a643..94ba3b8a6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java @@ -657,6 +657,15 @@ public abstract class MessageRecord extends DisplayRecord { return notifiedTimestamp; } + @VisibleForTesting + public long getIncomingStoryViewedAtTimestamp() { + if (isOutgoing()) { + return -1L; + } else { + return receiptTimestamp; + } + } + public long getReceiptTimestamp() { if (!isOutgoing()) { return getDateSent(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/StoryResult.kt b/app/src/main/java/org/thoughtcrime/securesms/database/model/StoryResult.kt new file mode 100644 index 000000000..9df014f02 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/StoryResult.kt @@ -0,0 +1,9 @@ +package org.thoughtcrime.securesms.database.model + +import org.thoughtcrime.securesms.recipients.RecipientId + +class StoryResult( + val recipientId: RecipientId, + val messageId: Long, + val messageSentTimestamp: Long +) 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 2ecb4e5fc..f8b875e15 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 @@ -110,7 +110,7 @@ class StoriesLandingFragment : DSLSettingsFragment(layoutId = R.layout.stories_l if (state.displayMyStoryItem) { customPref( MyStoriesItem.Model( - state.hasOutgoingGroupStories, + state.hasOutgoingStories, onClick = { if (it) { startActivity(Intent(requireContext(), MyStoriesActivity::class.java)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingRepository.kt index b9e2f3938..0eadeaebd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingRepository.kt @@ -9,6 +9,7 @@ import org.thoughtcrime.securesms.database.DatabaseObserver import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.model.DistributionListId import org.thoughtcrime.securesms.database.model.MessageRecord +import org.thoughtcrime.securesms.database.model.StoryResult import org.thoughtcrime.securesms.database.model.StoryViewState import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.recipients.Recipient @@ -27,35 +28,21 @@ class StoriesLandingRepository(context: Context) { } fun getStories(): Observable { - return Observable.create> { emitter -> - val myStoriesId = SignalDatabase.recipients.getOrInsertFromDistributionListId(DistributionListId.MY_STORY) - val myStories = Recipient.resolved(myStoriesId) - + val storyRecipients: Observable>> = Observable.create { emitter -> fun refresh() { - val storyMap = mutableMapOf>() - var hasOutgoingGroupStories = false - SignalDatabase.mms.allStories.use { - while (it.next != null) { - val messageRecord = it.current - val recipient = if (messageRecord.isOutgoing && !messageRecord.recipient.isGroup) { + val myStoriesId = SignalDatabase.recipients.getOrInsertFromDistributionListId(DistributionListId.MY_STORY) + val myStories = Recipient.resolved(myStoriesId) + + emitter.onNext( + SignalDatabase.mms.orderedStoryRecipientsAndIds.groupBy { + val recipient = Recipient.resolved(it.recipientId) + if (recipient.isDistributionList) { myStories - } else if (messageRecord.isOutgoing && messageRecord.recipient.isGroup) { - hasOutgoingGroupStories = true - messageRecord.recipient } else { - SignalDatabase.threads.getRecipientForThreadId(messageRecord.threadId)!! + recipient } - - storyMap[recipient] = (storyMap[recipient] ?: emptyList()) + messageRecord } - } - - val data: List> = storyMap.map { (sender, records) -> createStoriesLandingItemData(sender, records) } - if (data.isEmpty()) { - emitter.onNext(Observable.just(StoriesResult(emptyList(), false))) - } else { - emitter.onNext(Observable.combineLatest(data) { StoriesResult(it.toList() as List, hasOutgoingGroupStories) }) - } + ) } val observer = DatabaseObserver.Observer { @@ -68,7 +55,40 @@ class StoriesLandingRepository(context: Context) { } refresh() - }.switchMap { it }.subscribeOn(Schedulers.io()) + } + + val storiesLandingItemData = storyRecipients.switchMap { map -> + val observables = map.map { (recipient, results) -> + val messages = results + .sortedBy { it.messageSentTimestamp } + .reversed() + .take(if (recipient.isMyStory) 2 else 1) + .map { + SignalDatabase.mms.getMessageRecord(it.messageId) + } + + createStoriesLandingItemData(recipient, messages) + } + + Observable.combineLatest(observables) { + it.toList() as List + } + } + + val hasOutgoingStories: Observable = storyRecipients.concatMap { + Observable.fromCallable { + SignalDatabase.mms.getAllOutgoingStories(false).use { + it.next != null + } + } + } + + return Observable.combineLatest( + storiesLandingItemData, + hasOutgoingStories + ) { data, outgoingStories -> + StoriesResult(data, outgoingStories) + }.observeOn(Schedulers.io()) } private fun createStoriesLandingItemData(sender: Recipient, messageRecords: List): Observable { @@ -125,6 +145,6 @@ class StoriesLandingRepository(context: Context) { data class StoriesResult( val data: List, - val hasOutgoingGroupStories: Boolean + val hasOutgoingStories: Boolean ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingState.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingState.kt index 509c66eb4..28ba53ce6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingState.kt @@ -4,7 +4,7 @@ data class StoriesLandingState( val storiesLandingItems: List = emptyList(), val displayMyStoryItem: Boolean = false, val isHiddenContentVisible: Boolean = false, - val hasOutgoingGroupStories: Boolean = false, + val hasOutgoingStories: Boolean = false, val loadingState: LoadingState = LoadingState.INIT ) { enum class LoadingState { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingViewModel.kt index a7369e2ba..63dcf4597 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingViewModel.kt @@ -23,7 +23,7 @@ class StoriesLandingViewModel(private val storiesLandingRepository: StoriesLandi loadingState = StoriesLandingState.LoadingState.LOADED, storiesLandingItems = stories.sorted(), displayMyStoryItem = stories.isEmpty() || stories.none { it.storyRecipient.isMyStory }, - hasOutgoingGroupStories = hasOutgoingGroupStories + hasOutgoingStories = hasOutgoingGroupStories ) } } 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 119713f60..a4f246dd7 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 @@ -64,6 +64,10 @@ class StoryViewerFragment : Fragment(R.layout.stories_viewer_fragment), StoryVie storyPager.unregisterOnPageChangeCallback(onPageChanged) } + override fun onGoToPreviousStory(recipientId: RecipientId) { + viewModel.onGoToPreviousStory(recipientId) + } + override fun onFinishedPosts(recipientId: RecipientId) { viewModel.onFinishedPosts(recipientId) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerRepository.kt index 259caf6fb..b06ae8a8f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerRepository.kt @@ -4,14 +4,15 @@ import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.schedulers.Schedulers import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.model.DistributionListId +import org.thoughtcrime.securesms.database.model.StoryResult import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId class StoryViewerRepository { fun getStories(): Single> { return Single.fromCallable { - val recipients = SignalDatabase.mms.allStoriesRecipientsList - val resolved = recipients.map { Recipient.resolved(it) } + val storyResults: List = SignalDatabase.mms.orderedStoryRecipientsAndIds.distinctBy { it.recipientId } + val resolved = storyResults.map { Recipient.resolved(it.recipientId) } val doNotCollapse: List = resolved .filterNot { it.isDistributionList || it.shouldHideStory() } 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 957207d31..e377a39ae 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 @@ -8,6 +8,7 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.kotlin.plusAssign import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.util.livedata.Store +import kotlin.math.min class StoryViewerViewModel( private val startRecipientId: RecipientId, @@ -75,6 +76,16 @@ class StoryViewerViewModel( } } + fun onGoToPreviousStory(recipientId: RecipientId) { + store.update { + if (it.pages[it.page] == recipientId) { + updatePages(it, min(0, it.page - 1)) + } else { + it + } + } + } + fun onRecipientHidden() { refresh() } 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 33ae6620c..c19b97d76 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 @@ -256,7 +256,7 @@ class StoryViewerPageFragment : LiveDataReactiveStreams .fromPublisher(viewModel.state.observeOn(AndroidSchedulers.mainThread())) .observe(viewLifecycleOwner) { state -> - if (state.posts.isNotEmpty() && state.selectedPostIndex < state.posts.size) { + if (state.posts.isNotEmpty() && state.selectedPostIndex in state.posts.indices) { val post = state.posts[state.selectedPostIndex] presentViewsAndReplies(post) @@ -289,6 +289,8 @@ class StoryViewerPageFragment : viewModel.setAreSegmentsInitialized(true) } else if (state.selectedPostIndex >= state.posts.size) { callback.onFinishedPosts(storyRecipientId) + } else if (state.selectedPostIndex < 0) { + callback.onGoToPreviousStory(storyRecipientId) } } @@ -873,6 +875,7 @@ class StoryViewerPageFragment : } interface Callback { + fun onGoToPreviousStory(recipientId: RecipientId) fun onFinishedPosts(recipientId: RecipientId) fun onStoryHidden(recipientId: RecipientId) } 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 00c819a29..6fc2f5f1d 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 @@ -111,7 +111,7 @@ class StoryViewerPageViewModel( } val postIndex = store.state.selectedPostIndex - setSelectedPostIndex(max(0, postIndex - 1)) + setSelectedPostIndex(max(-1, postIndex - 1)) } fun getRestartIndex(): Int { diff --git a/app/src/test/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageViewModelTest.kt b/app/src/test/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageViewModelTest.kt index 8ccc422d4..bd20c6d97 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageViewModelTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageViewModelTest.kt @@ -101,6 +101,24 @@ class StoryViewerPageViewModelTest { testSubscriber.assertValueAt(0) { it.selectedPostIndex == 2 } } + @Test + fun `Given a single story, when I goToPrevious, then I expect storyIndex to be -1`() { + // GIVEN + val storyPosts = createStoryPosts(1) + whenever(repository.getStoryPostsFor(any())).thenReturn(Observable.just(storyPosts)) + + // WHEN + val testSubject = createTestSubject() + testScheduler.triggerActions() + testSubject.goToPreviousPost() + testScheduler.triggerActions() + + // THEN + val testSubscriber = testSubject.state.test() + + testSubscriber.assertValueAt(0) { it.selectedPostIndex == -1 } + } + private fun createTestSubject(): StoryViewerPageViewModel { return StoryViewerPageViewModel( RecipientId.from(1),