kopia lustrzana https://github.com/ryukoposting/Signal-Android
Implement proper story viewer ordering.
rodzic
157198fd17
commit
469879c211
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
|
@ -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))
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() }
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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),
|
||||||
|
|
Ładowanie…
Reference in New Issue