Implement proper story viewer ordering.

fork-5.53.8
Alex Hart 2022-04-01 15:03:31 -03:00 zatwierdzone przez Cody Henthorne
rodzic 157198fd17
commit 469879c211
18 zmienionych plików z 291 dodań i 59 usunięć

Wyświetl plik

@ -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<RecipientId>
@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)
}
}

Wyświetl plik

@ -1,8 +1,10 @@
package org.thoughtcrime.securesms.database package org.thoughtcrime.securesms.database
import org.thoughtcrime.securesms.database.model.StoryType import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage import org.thoughtcrime.securesms.mms.OutgoingMediaMessage
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import java.util.Optional
/** /**
* Helper methods for inserting an MMS message into the MMS table. * Helper methods for inserting an MMS message into the MMS table.
@ -52,4 +54,11 @@ object MmsHelper {
): Long { ): Long {
return SignalDatabase.mms.insertMessageOutbox(message, threadId, false, GroupReceiptDatabase.STATUS_UNKNOWN, null) return SignalDatabase.mms.insertMessageOutbox(message, threadId, false, GroupReceiptDatabase.STATUS_UNKNOWN, null)
} }
fun insert(
message: IncomingMediaMessage,
threadId: Long
): Optional<MessageDatabase.InsertResult> {
return SignalDatabase.mms.insertSecureDecryptedMessageInbox(message, threadId)
}
} }

Wyświetl plik

@ -318,7 +318,7 @@ class SegmentedProgressBar : View, ViewPager.OnPageChangeListener, View.OnTouchL
if (nextSegmentIndex >= segmentCount) { if (nextSegmentIndex >= segmentCount) {
this.listener?.onFinished() this.listener?.onFinished()
} else { } else {
restartSegment() loadSegment(offset = 0, userAction = false)
} }
return return
} }

Wyświetl plik

@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.database.documents.NetworkFailure;
import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.database.model.MessageId;
import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord; 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.StoryViewState;
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange; import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange;
import org.thoughtcrime.securesms.insights.InsightsConstants; 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 boolean isStory(long messageId);
public abstract @NonNull Reader getOutgoingStoriesTo(@NonNull RecipientId recipientId); public abstract @NonNull Reader getOutgoingStoriesTo(@NonNull RecipientId recipientId);
public abstract @NonNull Reader getAllOutgoingStories(boolean reverse); public abstract @NonNull Reader getAllOutgoingStories(boolean reverse);
public abstract @NonNull Reader getAllStories(); public abstract @NonNull List<StoryResult> getOrderedStoryRecipientsAndIds();
public abstract @NonNull List<RecipientId> getAllStoriesRecipientsList();
public abstract @NonNull Reader getAllStoriesFor(@NonNull RecipientId recipientId); public abstract @NonNull Reader getAllStoriesFor(@NonNull RecipientId recipientId);
public abstract @NonNull MessageId getStoryId(@NonNull RecipientId authorId, long sentTimestamp) throws NoSuchMessageException; public abstract @NonNull MessageId getStoryId(@NonNull RecipientId authorId, long sentTimestamp) throws NoSuchMessageException;
public abstract int getNumberOfStoryReplies(long parentStoryId); public abstract int getNumberOfStoryReplies(long parentStoryId);

Wyświetl plik

@ -57,6 +57,7 @@ import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.ParentStoryId; import org.thoughtcrime.securesms.database.model.ParentStoryId;
import org.thoughtcrime.securesms.database.model.Quote; import org.thoughtcrime.securesms.database.model.Quote;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord; 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.StoryType;
import org.thoughtcrime.securesms.database.model.StoryViewState; import org.thoughtcrime.securesms.database.model.StoryViewState;
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList; import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList;
@ -424,6 +425,7 @@ public class MmsDatabase extends MessageDatabase {
ContentValues contentValues = new ContentValues(); ContentValues contentValues = new ContentValues();
contentValues.put(VIEWED_RECEIPT_COUNT, 1); 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))); 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)); 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 @Override
public @NonNull MessageDatabase.Reader getAllStoriesFor(@NonNull RecipientId recipientId) { public @NonNull MessageDatabase.Reader getAllStoriesFor(@NonNull RecipientId recipientId) {
long threadId = SignalDatabase.threads().getThreadIdIfExistsFor(recipientId); long threadId = SignalDatabase.threads().getThreadIdIfExistsFor(recipientId);
@ -653,25 +650,39 @@ public class MmsDatabase extends MessageDatabase {
} }
@Override @Override
public @NonNull List<RecipientId> getAllStoriesRecipientsList() { public @NonNull List<StoryResult> getOrderedStoryRecipientsAndIds() {
SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); SQLiteDatabase db = getReadableDatabase();
String query = "SELECT " + String query = "SELECT\n"
"DISTINCT " + ThreadDatabase.RECIPIENT_ID + " " + + " mms.date AS sent_timestamp,\n"
"FROM " + TABLE_NAME + " JOIN " + ThreadDatabase.TABLE_NAME + " " + + " mms._id AS mms_id,\n"
"ON " + TABLE_NAME + "." + THREAD_ID + " = " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ID + " " + + " thread_recipient_id,\n"
"WHERE " + IS_STORY_CLAUSE + " " + + " (" + getOutgoingTypeClause() + ") AS is_outgoing,\n"
"ORDER BY " + TABLE_NAME + "." + DATE_SENT + " DESC, " + TABLE_NAME + "." + VIEWED_RECEIPT_COUNT + " ASC"; + " viewed_receipt_count,\n"
List<RecipientId> recipientIds; + " 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<StoryResult> results;
try (Cursor cursor = db.rawQuery(query, null)) { try (Cursor cursor = db.rawQuery(query, null)) {
if (cursor != null) { if (cursor != null) {
recipientIds = new ArrayList<>(cursor.getCount()); results = new ArrayList<>(cursor.getCount());
while (cursor.moveToNext()) { 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;
} }
} }

Wyświetl plik

@ -41,6 +41,7 @@ import org.thoughtcrime.securesms.database.model.GroupCallUpdateDetailsUtil;
import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.database.model.MessageId;
import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord; 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.StoryViewState;
import org.thoughtcrime.securesms.database.model.databaseprotos.GroupCallUpdateDetails; import org.thoughtcrime.securesms.database.model.databaseprotos.GroupCallUpdateDetails;
import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails; import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails;
@ -1405,12 +1406,7 @@ public class SmsDatabase extends MessageDatabase {
} }
@Override @Override
public @NonNull MessageDatabase.Reader getAllStories() { public @NonNull List<StoryResult> getOrderedStoryRecipientsAndIds() {
throw new UnsupportedOperationException();
}
@Override
public @NonNull List<RecipientId> getAllStoriesRecipientsList() {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }

Wyświetl plik

@ -657,6 +657,15 @@ public abstract class MessageRecord extends DisplayRecord {
return notifiedTimestamp; return notifiedTimestamp;
} }
@VisibleForTesting
public long getIncomingStoryViewedAtTimestamp() {
if (isOutgoing()) {
return -1L;
} else {
return receiptTimestamp;
}
}
public long getReceiptTimestamp() { public long getReceiptTimestamp() {
if (!isOutgoing()) { if (!isOutgoing()) {
return getDateSent(); return getDateSent();

Wyświetl plik

@ -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
)

Wyświetl plik

@ -110,7 +110,7 @@ class StoriesLandingFragment : DSLSettingsFragment(layoutId = R.layout.stories_l
if (state.displayMyStoryItem) { if (state.displayMyStoryItem) {
customPref( customPref(
MyStoriesItem.Model( MyStoriesItem.Model(
state.hasOutgoingGroupStories, state.hasOutgoingStories,
onClick = { onClick = {
if (it) { if (it) {
startActivity(Intent(requireContext(), MyStoriesActivity::class.java)) startActivity(Intent(requireContext(), MyStoriesActivity::class.java))

Wyświetl plik

@ -9,6 +9,7 @@ import org.thoughtcrime.securesms.database.DatabaseObserver
import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.DistributionListId import org.thoughtcrime.securesms.database.model.DistributionListId
import org.thoughtcrime.securesms.database.model.MessageRecord 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.database.model.StoryViewState
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
@ -27,35 +28,21 @@ class StoriesLandingRepository(context: Context) {
} }
fun getStories(): Observable<StoriesResult> { fun getStories(): Observable<StoriesResult> {
return Observable.create<Observable<StoriesResult>> { emitter -> val storyRecipients: Observable<Map<Recipient, List<StoryResult>>> = Observable.create { emitter ->
fun refresh() {
val myStoriesId = SignalDatabase.recipients.getOrInsertFromDistributionListId(DistributionListId.MY_STORY) val myStoriesId = SignalDatabase.recipients.getOrInsertFromDistributionListId(DistributionListId.MY_STORY)
val myStories = Recipient.resolved(myStoriesId) val myStories = Recipient.resolved(myStoriesId)
fun refresh() { emitter.onNext(
val storyMap = mutableMapOf<Recipient, List<MessageRecord>>() SignalDatabase.mms.orderedStoryRecipientsAndIds.groupBy {
var hasOutgoingGroupStories = false val recipient = Recipient.resolved(it.recipientId)
SignalDatabase.mms.allStories.use { if (recipient.isDistributionList) {
while (it.next != null) {
val messageRecord = it.current
val recipient = if (messageRecord.isOutgoing && !messageRecord.recipient.isGroup) {
myStories myStories
} else if (messageRecord.isOutgoing && messageRecord.recipient.isGroup) {
hasOutgoingGroupStories = true
messageRecord.recipient
} else { } else {
SignalDatabase.threads.getRecipientForThreadId(messageRecord.threadId)!! recipient
}
storyMap[recipient] = (storyMap[recipient] ?: emptyList()) + messageRecord
} }
} }
)
val data: List<Observable<StoriesLandingItemData>> = 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<StoriesLandingItemData>, hasOutgoingGroupStories) })
}
} }
val observer = DatabaseObserver.Observer { val observer = DatabaseObserver.Observer {
@ -68,7 +55,40 @@ class StoriesLandingRepository(context: Context) {
} }
refresh() 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<StoriesLandingItemData>
}
}
val hasOutgoingStories: Observable<Boolean> = 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<MessageRecord>): Observable<StoriesLandingItemData> { private fun createStoriesLandingItemData(sender: Recipient, messageRecords: List<MessageRecord>): Observable<StoriesLandingItemData> {
@ -125,6 +145,6 @@ class StoriesLandingRepository(context: Context) {
data class StoriesResult( data class StoriesResult(
val data: List<StoriesLandingItemData>, val data: List<StoriesLandingItemData>,
val hasOutgoingGroupStories: Boolean val hasOutgoingStories: Boolean
) )
} }

Wyświetl plik

@ -4,7 +4,7 @@ data class StoriesLandingState(
val storiesLandingItems: List<StoriesLandingItemData> = emptyList(), val storiesLandingItems: List<StoriesLandingItemData> = emptyList(),
val displayMyStoryItem: Boolean = false, val displayMyStoryItem: Boolean = false,
val isHiddenContentVisible: Boolean = false, val isHiddenContentVisible: Boolean = false,
val hasOutgoingGroupStories: Boolean = false, val hasOutgoingStories: Boolean = false,
val loadingState: LoadingState = LoadingState.INIT val loadingState: LoadingState = LoadingState.INIT
) { ) {
enum class LoadingState { enum class LoadingState {

Wyświetl plik

@ -23,7 +23,7 @@ class StoriesLandingViewModel(private val storiesLandingRepository: StoriesLandi
loadingState = StoriesLandingState.LoadingState.LOADED, loadingState = StoriesLandingState.LoadingState.LOADED,
storiesLandingItems = stories.sorted(), storiesLandingItems = stories.sorted(),
displayMyStoryItem = stories.isEmpty() || stories.none { it.storyRecipient.isMyStory }, displayMyStoryItem = stories.isEmpty() || stories.none { it.storyRecipient.isMyStory },
hasOutgoingGroupStories = hasOutgoingGroupStories hasOutgoingStories = hasOutgoingGroupStories
) )
} }
} }

Wyświetl plik

@ -64,6 +64,10 @@ class StoryViewerFragment : Fragment(R.layout.stories_viewer_fragment), StoryVie
storyPager.unregisterOnPageChangeCallback(onPageChanged) storyPager.unregisterOnPageChangeCallback(onPageChanged)
} }
override fun onGoToPreviousStory(recipientId: RecipientId) {
viewModel.onGoToPreviousStory(recipientId)
}
override fun onFinishedPosts(recipientId: RecipientId) { override fun onFinishedPosts(recipientId: RecipientId) {
viewModel.onFinishedPosts(recipientId) viewModel.onFinishedPosts(recipientId)
} }

Wyświetl plik

@ -4,14 +4,15 @@ import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.DistributionListId 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.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.recipients.RecipientId
class StoryViewerRepository { class StoryViewerRepository {
fun getStories(): Single<List<RecipientId>> { fun getStories(): Single<List<RecipientId>> {
return Single.fromCallable { return Single.fromCallable {
val recipients = SignalDatabase.mms.allStoriesRecipientsList val storyResults: List<StoryResult> = SignalDatabase.mms.orderedStoryRecipientsAndIds.distinctBy { it.recipientId }
val resolved = recipients.map { Recipient.resolved(it) } val resolved = storyResults.map { Recipient.resolved(it.recipientId) }
val doNotCollapse: List<RecipientId> = resolved val doNotCollapse: List<RecipientId> = resolved
.filterNot { it.isDistributionList || it.shouldHideStory() } .filterNot { it.isDistributionList || it.shouldHideStory() }

Wyświetl plik

@ -8,6 +8,7 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign import io.reactivex.rxjava3.kotlin.plusAssign
import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.livedata.Store import org.thoughtcrime.securesms.util.livedata.Store
import kotlin.math.min
class StoryViewerViewModel( class StoryViewerViewModel(
private val startRecipientId: RecipientId, 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() { fun onRecipientHidden() {
refresh() refresh()
} }

Wyświetl plik

@ -256,7 +256,7 @@ class StoryViewerPageFragment :
LiveDataReactiveStreams LiveDataReactiveStreams
.fromPublisher(viewModel.state.observeOn(AndroidSchedulers.mainThread())) .fromPublisher(viewModel.state.observeOn(AndroidSchedulers.mainThread()))
.observe(viewLifecycleOwner) { state -> .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] val post = state.posts[state.selectedPostIndex]
presentViewsAndReplies(post) presentViewsAndReplies(post)
@ -289,6 +289,8 @@ class StoryViewerPageFragment :
viewModel.setAreSegmentsInitialized(true) viewModel.setAreSegmentsInitialized(true)
} else if (state.selectedPostIndex >= state.posts.size) { } else if (state.selectedPostIndex >= state.posts.size) {
callback.onFinishedPosts(storyRecipientId) callback.onFinishedPosts(storyRecipientId)
} else if (state.selectedPostIndex < 0) {
callback.onGoToPreviousStory(storyRecipientId)
} }
} }
@ -873,6 +875,7 @@ class StoryViewerPageFragment :
} }
interface Callback { interface Callback {
fun onGoToPreviousStory(recipientId: RecipientId)
fun onFinishedPosts(recipientId: RecipientId) fun onFinishedPosts(recipientId: RecipientId)
fun onStoryHidden(recipientId: RecipientId) fun onStoryHidden(recipientId: RecipientId)
} }

Wyświetl plik

@ -111,7 +111,7 @@ class StoryViewerPageViewModel(
} }
val postIndex = store.state.selectedPostIndex val postIndex = store.state.selectedPostIndex
setSelectedPostIndex(max(0, postIndex - 1)) setSelectedPostIndex(max(-1, postIndex - 1))
} }
fun getRestartIndex(): Int { fun getRestartIndex(): Int {

Wyświetl plik

@ -101,6 +101,24 @@ class StoryViewerPageViewModelTest {
testSubscriber.assertValueAt(0) { it.selectedPostIndex == 2 } 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 { private fun createTestSubject(): StoryViewerPageViewModel {
return StoryViewerPageViewModel( return StoryViewerPageViewModel(
RecipientId.from(1), RecipientId.from(1),