Apply new story list ordering rules.

Co-authored-by: Cody Henthorne <cody@signal.org>
fork-5.53.8
Alex Hart 2022-06-14 12:50:53 -03:00 zatwierdzone przez Greyson Parrelli
rodzic 3b07f4a8ca
commit 88a66b49ff
16 zmienionych plików z 319 dodań i 77 usunięć

Wyświetl plik

@ -0,0 +1,6 @@
package org.thoughtcrime.securesms.contacts.paged
/**
* Number of active contacts for a given section, handed to the expand config.
*/
typealias ActiveContactCount = Int

Wyświetl plik

@ -83,7 +83,7 @@ class ContactSearchConfiguration private constructor(
*/ */
data class ExpandConfig( data class ExpandConfig(
val isExpanded: Boolean, val isExpanded: Boolean,
val maxCountWhenNotExpanded: Int = 2 val maxCountWhenNotExpanded: (ActiveContactCount) -> Int = { 2 }
) )
/** /**

Wyświetl plik

@ -3,6 +3,8 @@ package org.thoughtcrime.securesms.contacts.paged
import android.database.Cursor import android.database.Cursor
import org.signal.paging.PagedDataSource import org.signal.paging.PagedDataSource
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyvalue.StorySend
import java.util.concurrent.TimeUnit
import kotlin.math.min import kotlin.math.min
/** /**
@ -13,6 +15,14 @@ class ContactSearchPagedDataSource(
private val contactSearchPagedDataSourceRepository: ContactSearchPagedDataSourceRepository = ContactSearchPagedDataSourceRepository(ApplicationDependencies.getApplication()) private val contactSearchPagedDataSourceRepository: ContactSearchPagedDataSourceRepository = ContactSearchPagedDataSourceRepository(ApplicationDependencies.getApplication())
) : PagedDataSource<ContactSearchKey, ContactSearchData> { ) : PagedDataSource<ContactSearchKey, ContactSearchData> {
companion object {
private val ACTIVE_STORY_CUTOFF_DURATION = TimeUnit.DAYS.toMillis(1)
}
private val latestStorySends: List<StorySend> = contactSearchPagedDataSourceRepository.getLatestStorySends(ACTIVE_STORY_CUTOFF_DURATION)
private val activeStoryCount = latestStorySends.size
override fun size(): Int { override fun size(): Int {
return contactConfiguration.sections.sumOf { return contactConfiguration.sections.sumOf {
getSectionSize(it, contactConfiguration.query) getSectionSize(it, contactConfiguration.query)
@ -67,27 +77,26 @@ class ContactSearchPagedDataSource(
} }
private fun getSectionSize(section: ContactSearchConfiguration.Section, query: String?): Int { private fun getSectionSize(section: ContactSearchConfiguration.Section, query: String?): Int {
val cursor: Cursor = when (section) { when (section) {
is ContactSearchConfiguration.Section.Individuals -> getNonGroupContactsCursor(section, query) is ContactSearchConfiguration.Section.Individuals -> getNonGroupContactsCursor(section, query)
is ContactSearchConfiguration.Section.Groups -> contactSearchPagedDataSourceRepository.getGroupContacts(section, query) is ContactSearchConfiguration.Section.Groups -> contactSearchPagedDataSourceRepository.getGroupContacts(section, query)
is ContactSearchConfiguration.Section.Recents -> getRecentsCursor(section, query) is ContactSearchConfiguration.Section.Recents -> getRecentsCursor(section, query)
is ContactSearchConfiguration.Section.Stories -> getStoriesCursor(query) is ContactSearchConfiguration.Section.Stories -> getStoriesCursor(query)
}!! }!!.use { cursor ->
val extras: List<ContactSearchData> = when (section) { val extras: List<ContactSearchData> = when (section) {
is ContactSearchConfiguration.Section.Stories -> getFilteredGroupStories(section, query) is ContactSearchConfiguration.Section.Stories -> getFilteredGroupStories(section, query)
else -> emptyList() else -> emptyList()
} }
val collection = ResultsCollection( val collection = createResultsCollection(
section = section, section = section,
cursor = cursor, cursor = cursor,
extraData = extras, extraData = extras,
cursorMapper = { error("Unsupported") } cursorMapper = { error("Unsupported") }
) )
return collection.getSize() return collection.getSize()
} }
}
private fun getFilteredGroupStories(section: ContactSearchConfiguration.Section.Stories, query: String?): List<ContactSearchData> { private fun getFilteredGroupStories(section: ContactSearchConfiguration.Section.Stories, query: String?): List<ContactSearchData> {
return (contactSearchPagedDataSourceRepository.getGroupStories() + section.groupStories) return (contactSearchPagedDataSourceRepository.getGroupStories() + section.groupStories)
@ -133,7 +142,7 @@ class ContactSearchPagedDataSource(
): List<ContactSearchData> { ): List<ContactSearchData> {
val results = mutableListOf<ContactSearchData>() val results = mutableListOf<ContactSearchData>()
val collection = ResultsCollection(section, cursor, extraData, cursorRowToData) val collection = createResultsCollection(section, cursor, extraData, cursorRowToData)
results.addAll(collection.getSublist(startIndex, endIndex)) results.addAll(collection.getSublist(startIndex, endIndex))
return results return results
@ -201,14 +210,27 @@ class ContactSearchPagedDataSource(
} ?: emptyList() } ?: emptyList()
} }
private fun createResultsCollection(
section: ContactSearchConfiguration.Section,
cursor: Cursor,
extraData: List<ContactSearchData>,
cursorMapper: (Cursor) -> ContactSearchData
): ResultsCollection {
return when (section) {
is ContactSearchConfiguration.Section.Stories -> StoriesCollection(section, cursor, extraData, cursorMapper, activeStoryCount, StoryComparator(latestStorySends))
else -> ResultsCollection(section, cursor, extraData, cursorMapper, 0)
}
}
/** /**
* We assume that the collection is [cursor contents] + [extraData contents] * We assume that the collection is [cursor contents] + [extraData contents]
*/ */
private data class ResultsCollection( private open class ResultsCollection(
val section: ContactSearchConfiguration.Section, val section: ContactSearchConfiguration.Section,
val cursor: Cursor, val cursor: Cursor,
val extraData: List<ContactSearchData>, val extraData: List<ContactSearchData>,
val cursorMapper: (Cursor) -> ContactSearchData val cursorMapper: (Cursor) -> ContactSearchData,
val activeContactCount: Int
) { ) {
private val contentSize = cursor.count + extraData.count() private val contentSize = cursor.count + extraData.count()
@ -216,7 +238,7 @@ class ContactSearchPagedDataSource(
fun getSize(): Int { fun getSize(): Int {
val contentsAndExpand = min( val contentsAndExpand = min(
section.expandConfig?.let { section.expandConfig?.let {
if (it.isExpanded) Int.MAX_VALUE else (it.maxCountWhenNotExpanded + 1) if (it.isExpanded) Int.MAX_VALUE else (it.maxCountWhenNotExpanded(activeContactCount) + 1)
} ?: Int.MAX_VALUE, } ?: Int.MAX_VALUE,
contentSize contentSize
) )
@ -239,7 +261,13 @@ class ContactSearchPagedDataSource(
index == getSize() - 1 && shouldDisplayExpandRow() -> ContactSearchData.Expand(section.sectionKey) index == getSize() - 1 && shouldDisplayExpandRow() -> ContactSearchData.Expand(section.sectionKey)
else -> { else -> {
val correctedIndex = if (section.includeHeader) index - 1 else index val correctedIndex = if (section.includeHeader) index - 1 else index
if (correctedIndex < cursor.count) { return getItemAtCorrectedIndex(correctedIndex)
}
}
}
protected open fun getItemAtCorrectedIndex(correctedIndex: Int): ContactSearchData {
return if (correctedIndex < cursor.count) {
cursor.moveToPosition(correctedIndex) cursor.moveToPosition(correctedIndex)
cursorMapper.invoke(cursor) cursorMapper.invoke(cursor)
} else { } else {
@ -247,14 +275,60 @@ class ContactSearchPagedDataSource(
extraData[extraIndex] extraData[extraIndex]
} }
} }
}
}
private fun shouldDisplayExpandRow(): Boolean { private fun shouldDisplayExpandRow(): Boolean {
val expandConfig = section.expandConfig val expandConfig = section.expandConfig
return when { return when {
expandConfig == null || expandConfig.isExpanded -> false expandConfig == null || expandConfig.isExpanded -> false
else -> contentSize > expandConfig.maxCountWhenNotExpanded + 1 else -> contentSize > expandConfig.maxCountWhenNotExpanded(activeContactCount) + 1
}
}
}
private class StoriesCollection(
section: ContactSearchConfiguration.Section,
cursor: Cursor,
extraData: List<ContactSearchData>,
cursorMapper: (Cursor) -> ContactSearchData,
activeContactCount: Int,
val storyComparator: StoryComparator
) : ResultsCollection(section, cursor, extraData, cursorMapper, activeContactCount) {
private val aggregateStoryData: List<ContactSearchData.Story> by lazy {
if (section !is ContactSearchConfiguration.Section.Stories) {
error("Aggregate data creation is only necessary for stories.")
}
val cursorContacts: List<ContactSearchData> = (0 until cursor.count).map {
cursor.moveToPosition(it)
cursorMapper(cursor)
}
(cursorContacts + extraData)
.filterIsInstance(ContactSearchData.Story::class.java)
.sortedWith(storyComparator)
}
override fun getItemAtCorrectedIndex(correctedIndex: Int): ContactSearchData {
return aggregateStoryData[correctedIndex]
}
}
/**
* StoryComparator
*/
private class StoryComparator(private val latestStorySends: List<StorySend>) : Comparator<ContactSearchData.Story> {
override fun compare(lhs: ContactSearchData.Story, rhs: ContactSearchData.Story): Int {
val lhsActiveRank = latestStorySends.indexOfFirst { it.identifier.matches(lhs.recipient) }.let { if (it == -1) Int.MAX_VALUE else it }
val rhsActiveRank = latestStorySends.indexOfFirst { it.identifier.matches(rhs.recipient) }.let { if (it == -1) Int.MAX_VALUE else it }
return when {
lhs.recipient.isMyStory && rhs.recipient.isMyStory -> 0
lhs.recipient.isMyStory -> -1
rhs.recipient.isMyStory -> 1
lhsActiveRank < rhsActiveRank -> -1
lhsActiveRank > rhsActiveRank -> 1
lhsActiveRank == rhsActiveRank -> -1
else -> 0
} }
} }
} }

Wyświetl plik

@ -9,6 +9,8 @@ import org.thoughtcrime.securesms.database.DistributionListDatabase
import org.thoughtcrime.securesms.database.GroupDatabase import org.thoughtcrime.securesms.database.GroupDatabase
import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.keyvalue.StorySend
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.recipients.RecipientId
@ -22,6 +24,11 @@ open class ContactSearchPagedDataSourceRepository(
private val contactRepository = ContactRepository(context, context.getString(R.string.note_to_self)) private val contactRepository = ContactRepository(context, context.getString(R.string.note_to_self))
open fun getLatestStorySends(activeStoryCutoffDuration: Long): List<StorySend> {
return SignalStore.storyValues()
.getLatestActiveStorySendTimestamps(System.currentTimeMillis() - activeStoryCutoffDuration)
}
open fun querySignalContacts(query: String?, includeSelf: Boolean): Cursor? { open fun querySignalContacts(query: String?, includeSelf: Boolean): Cursor? {
return contactRepository.querySignalContacts(query ?: "", includeSelf) return contactRepository.querySignalContacts(query ?: "", includeSelf)
} }

Wyświetl plik

@ -354,7 +354,8 @@ class MultiselectForwardFragment :
if (Stories.isFeatureEnabled() && isSelectedMediaValidForStories()) { if (Stories.isFeatureEnabled() && isSelectedMediaValidForStories()) {
val expandedConfig: ContactSearchConfiguration.ExpandConfig? = if (isSelectedMediaValidForNonStories()) { val expandedConfig: ContactSearchConfiguration.ExpandConfig? = if (isSelectedMediaValidForNonStories()) {
ContactSearchConfiguration.ExpandConfig( ContactSearchConfiguration.ExpandConfig(
isExpanded = contactSearchState.expandedSections.contains(ContactSearchConfiguration.SectionKey.STORIES) isExpanded = contactSearchState.expandedSections.contains(ContactSearchConfiguration.SectionKey.STORIES),
maxCountWhenNotExpanded = { it + 1 }
) )
} else { } else {
null null

Wyświetl plik

@ -35,6 +35,15 @@ public final class DistributionListId implements DatabaseId, Parcelable {
} }
} }
public static @NonNull DistributionListId from(@NonNull String serializedId) {
try {
long id = Long.parseLong(serializedId);
return from(id);
} catch (NumberFormatException e) {
throw new IllegalArgumentException(e);
}
}
private DistributionListId(long id) { private DistributionListId(long id) {
this.id = id; this.id = id;
} }

Wyświetl plik

@ -260,7 +260,8 @@ public final class PushGroupSendJob extends PushSendJob {
final SignalServiceStoryMessage storyMessage; final SignalServiceStoryMessage storyMessage;
if (message.getStoryType().isTextStory()) { if (message.getStoryType().isTextStory()) {
storyMessage = SignalServiceStoryMessage.forTextAttachment(Recipient.self().getProfileKey(), groupContext, StorySendUtil.deserializeBodyToStoryTextAttachment(message, this::getPreviewsFor), message.getStoryType().isStoryWithReplies()); storyMessage = SignalServiceStoryMessage.forTextAttachment(Recipient.self().getProfileKey(), groupContext, StorySendUtil.deserializeBodyToStoryTextAttachment(message, this::getPreviewsFor), message.getStoryType()
.isStoryWithReplies());
} else if (!attachmentPointers.isEmpty()) { } else if (!attachmentPointers.isEmpty()) {
storyMessage = SignalServiceStoryMessage.forFileAttachment(Recipient.self().getProfileKey(), groupContext, attachmentPointers.get(0), message.getStoryType().isStoryWithReplies()); storyMessage = SignalServiceStoryMessage.forFileAttachment(Recipient.self().getProfileKey(), groupContext, attachmentPointers.get(0), message.getStoryType().isStoryWithReplies());
} else { } else {
@ -309,7 +310,7 @@ public final class PushGroupSendJob extends PushSendJob {
SignalServiceDataMessage.Builder groupMessageBuilder = builder.withAttachments(attachmentPointers) SignalServiceDataMessage.Builder groupMessageBuilder = builder.withAttachments(attachmentPointers)
.withBody(message.getBody()) .withBody(message.getBody())
.withExpiration((int)(message.getExpiresIn() / 1000)) .withExpiration((int) (message.getExpiresIn() / 1000))
.withViewOnce(message.isViewOnce()) .withViewOnce(message.isViewOnce())
.asExpirationUpdate(message.isExpirationUpdate()) .asExpirationUpdate(message.isExpirationUpdate())
.withProfileKey(profileKey.orElse(null)) .withProfileKey(profileKey.orElse(null))
@ -377,7 +378,8 @@ public final class PushGroupSendJob extends PushSendJob {
RecipientAccessList accessList = new RecipientAccessList(target); RecipientAccessList accessList = new RecipientAccessList(target);
List<NetworkFailure> networkFailures = Stream.of(results).filter(SendMessageResult::isNetworkFailure).map(result -> new NetworkFailure(accessList.requireIdByAddress(result.getAddress()))).toList(); List<NetworkFailure> networkFailures = Stream.of(results).filter(SendMessageResult::isNetworkFailure).map(result -> new NetworkFailure(accessList.requireIdByAddress(result.getAddress()))).toList();
List<IdentityKeyMismatch> identityMismatches = Stream.of(results).filter(result -> result.getIdentityFailure() != null).map(result -> new IdentityKeyMismatch(accessList.requireIdByAddress(result.getAddress()), result.getIdentityFailure().getIdentityKey())).toList(); List<IdentityKeyMismatch> identityMismatches = Stream.of(results).filter(result -> result.getIdentityFailure() != null)
.map(result -> new IdentityKeyMismatch(accessList.requireIdByAddress(result.getAddress()), result.getIdentityFailure().getIdentityKey())).toList();
ProofRequiredException proofRequired = Stream.of(results).filter(r -> r.getProofRequiredFailure() != null).findLast().map(SendMessageResult::getProofRequiredFailure).orElse(null); ProofRequiredException proofRequired = Stream.of(results).filter(r -> r.getProofRequiredFailure() != null).findLast().map(SendMessageResult::getProofRequiredFailure).orElse(null);
List<SendMessageResult> successes = Stream.of(results).filter(result -> result.getSuccess() != null).toList(); List<SendMessageResult> successes = Stream.of(results).filter(result -> result.getSuccess() != null).toList();
List<Pair<RecipientId, Boolean>> successUnidentifiedStatus = Stream.of(successes).map(result -> new Pair<>(accessList.requireIdByAddress(result.getAddress()), result.getSuccess().isUnidentified())).toList(); List<Pair<RecipientId, Boolean>> successUnidentifiedStatus = Stream.of(successes).map(result -> new Pair<>(accessList.requireIdByAddress(result.getAddress()), result.getSuccess().isUnidentified())).toList();

Wyświetl plik

@ -13,6 +13,7 @@ import com.annimon.stream.Stream;
import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode; import org.greenrobot.eventbus.ThreadMode;
import org.signal.core.util.Hex;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.signal.libsignal.metadata.certificate.InvalidCertificateException; import org.signal.libsignal.metadata.certificate.InvalidCertificateException;
import org.signal.libsignal.metadata.certificate.SenderCertificate; import org.signal.libsignal.metadata.certificate.SenderCertificate;
@ -54,7 +55,6 @@ import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.BitmapDecodingException; import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.FeatureFlags;
import org.signal.core.util.Hex;
import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;

Wyświetl plik

@ -2,7 +2,13 @@ package org.thoughtcrime.securesms.keyvalue;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.google.protobuf.InvalidProtocolBufferException;
import org.thoughtcrime.securesms.database.model.databaseprotos.SignalStoreList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
abstract class SignalStoreValues { abstract class SignalStoreValues {
@ -44,6 +50,25 @@ abstract class SignalStoreValues {
return store.getBlob(key, defaultValue); return store.getBlob(key, defaultValue);
} }
<T> List<T> getList(@NonNull String key, @NonNull Serializer<T> serializer) {
byte[] blob = getBlob(key, null);
if (blob == null) {
return Collections.emptyList();
}
try {
SignalStoreList signalStoreList = SignalStoreList.parseFrom(blob);
return signalStoreList.getContentsList()
.stream()
.map(serializer::deserialize)
.collect(Collectors.toList());
} catch (InvalidProtocolBufferException e) {
throw new IllegalArgumentException(e);
}
}
void putBlob(@NonNull String key, byte[] value) { void putBlob(@NonNull String key, byte[] value) {
store.beginWrite().putBlob(key, value).apply(); store.beginWrite().putBlob(key, value).apply();
} }
@ -68,7 +93,21 @@ abstract class SignalStoreValues {
store.beginWrite().putString(key, value).apply(); store.beginWrite().putString(key, value).apply();
} }
<T> void putList(@NonNull String key, @NonNull List<T> values, @NonNull Serializer<T> serializer) {
putBlob(key, SignalStoreList.newBuilder()
.addAllContents(values.stream()
.map(serializer::serialize)
.collect(Collectors.toList()))
.build()
.toByteArray());
}
void remove(@NonNull String key) { void remove(@NonNull String key) {
store.beginWrite().remove(key).apply(); store.beginWrite().remove(key).apply();
} }
interface Serializer<T> {
@NonNull String serialize(@NonNull T data);
T deserialize(@NonNull String data);
}
} }

Wyświetl plik

@ -0,0 +1,33 @@
package org.thoughtcrime.securesms.keyvalue
import org.thoughtcrime.securesms.database.model.DistributionListId
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.recipients.Recipient
data class StorySend(
val timestamp: Long,
val identifier: Identifier
) {
companion object {
@JvmStatic
fun newSend(recipient: Recipient): StorySend {
return if (recipient.isGroup) {
StorySend(System.currentTimeMillis(), Identifier.Group(recipient.requireGroupId()))
} else {
StorySend(System.currentTimeMillis(), Identifier.DistributionList(recipient.requireDistributionListId()))
}
}
}
sealed class Identifier {
data class Group(val groupId: GroupId) : Identifier() {
override fun matches(recipient: Recipient) = recipient.groupId.orElse(null) == groupId
}
data class DistributionList(val distributionListId: DistributionListId) : Identifier() {
override fun matches(recipient: Recipient) = recipient.distributionListId.orElse(null) == distributionListId
}
abstract fun matches(recipient: Recipient): Boolean
}
}

Wyświetl plik

@ -1,5 +1,9 @@
package org.thoughtcrime.securesms.keyvalue package org.thoughtcrime.securesms.keyvalue
import org.json.JSONObject
import org.thoughtcrime.securesms.database.model.DistributionListId
import org.thoughtcrime.securesms.groups.GroupId
internal class StoryValues(store: KeyValueStore) : SignalStoreValues(store) { internal class StoryValues(store: KeyValueStore) : SignalStoreValues(store) {
companion object { companion object {
@ -14,6 +18,11 @@ internal class StoryValues(store: KeyValueStore) : SignalStoreValues(store) {
* Used to check whether we should display certain dialogs. * Used to check whether we should display certain dialogs.
*/ */
private const val USER_HAS_ADDED_TO_A_STORY = "user.has.added.to.a.story" private const val USER_HAS_ADDED_TO_A_STORY = "user.has.added.to.a.story"
/**
* Rolling window of latest two private or group stories a user has sent to.
*/
private const val LATEST_STORY_SENDS = "latest.story.sends"
} }
override fun onFirstEverAppLaunch() = Unit override fun onFirstEverAppLaunch() = Unit
@ -25,4 +34,44 @@ internal class StoryValues(store: KeyValueStore) : SignalStoreValues(store) {
var lastFontVersionCheck: Long by longValue(LAST_FONT_VERSION_CHECK, 0) var lastFontVersionCheck: Long by longValue(LAST_FONT_VERSION_CHECK, 0)
var userHasBeenNotifiedAboutStories: Boolean by booleanValue(USER_HAS_ADDED_TO_A_STORY, false) var userHasBeenNotifiedAboutStories: Boolean by booleanValue(USER_HAS_ADDED_TO_A_STORY, false)
fun setLatestStorySend(storySend: StorySend) {
synchronized(this) {
val storySends: List<StorySend> = getList(LATEST_STORY_SENDS, StorySendSerializer)
val newStorySends: List<StorySend> = listOf(storySend) + storySends.take(1)
putList(LATEST_STORY_SENDS, newStorySends, StorySendSerializer)
}
}
fun getLatestActiveStorySendTimestamps(activeCutoffTimestamp: Long): List<StorySend> {
val storySends: List<StorySend> = getList(LATEST_STORY_SENDS, StorySendSerializer)
return storySends.filter { it.timestamp >= activeCutoffTimestamp }
}
private object StorySendSerializer : Serializer<StorySend> {
override fun serialize(data: StorySend): String {
return JSONObject()
.put("timestamp", data.timestamp)
.put("groupId", if (data.identifier is StorySend.Identifier.Group) data.identifier.groupId.toString() else null)
.put("distributionListId", if (data.identifier is StorySend.Identifier.DistributionList) data.identifier.distributionListId.serialize() else null)
.toString()
}
override fun deserialize(data: String): StorySend {
val jsonData = JSONObject(data)
val timestamp = jsonData.getLong("timestamp")
val identifier = if (jsonData.has("groupId")) {
val group = jsonData.getString("groupId")
StorySend.Identifier.Group(GroupId.parse(group))
} else {
val distributionListId = jsonData.getString("distributionListId")
StorySend.Identifier.DistributionList(DistributionListId.from(distributionListId))
}
return StorySend(timestamp, identifier)
}
}
} }

Wyświetl plik

@ -18,6 +18,8 @@ import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.database.model.Mention import org.thoughtcrime.securesms.database.model.Mention
import org.thoughtcrime.securesms.database.model.StoryType import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.keyvalue.StorySend
import org.thoughtcrime.securesms.mediasend.CompositeMediaTransform import org.thoughtcrime.securesms.mediasend.CompositeMediaTransform
import org.thoughtcrime.securesms.mediasend.ImageEditorModelRenderMediaTransform import org.thoughtcrime.securesms.mediasend.ImageEditorModelRenderMediaTransform
import org.thoughtcrime.securesms.mediasend.Media import org.thoughtcrime.securesms.mediasend.Media
@ -217,10 +219,14 @@ class MediaSelectionRepository(context: Context) {
val recipient = Recipient.resolved(contact.recipientId) val recipient = Recipient.resolved(contact.recipientId)
val isStory = contact.isStory || recipient.isDistributionList val isStory = contact.isStory || recipient.isDistributionList
if (isStory && recipient.isActiveGroup) { if (isStory && recipient.isActiveGroup && recipient.isGroup) {
SignalDatabase.groups.markDisplayAsStory(recipient.requireGroupId()) SignalDatabase.groups.markDisplayAsStory(recipient.requireGroupId())
} }
if (isStory && !recipient.isMyStory) {
SignalStore.storyValues().setLatestStorySend(StorySend.newSend(recipient))
}
val storyType: StoryType = when { val storyType: StoryType = when {
recipient.isDistributionList -> SignalDatabase.distributionLists.getStoryType(recipient.requireDistributionListId()) recipient.isDistributionList -> SignalDatabase.distributionLists.getStoryType(recipient.requireDistributionListId())
isStory -> StoryType.STORY_WITH_REPLIES isStory -> StoryType.STORY_WITH_REPLIES

Wyświetl plik

@ -9,6 +9,8 @@ import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.database.model.StoryType import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.database.model.databaseprotos.StoryTextPost import org.thoughtcrime.securesms.database.model.databaseprotos.StoryTextPost
import org.thoughtcrime.securesms.fonts.TextFont import org.thoughtcrime.securesms.fonts.TextFont
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.keyvalue.StorySend
import org.thoughtcrime.securesms.linkpreview.LinkPreview import org.thoughtcrime.securesms.linkpreview.LinkPreview
import org.thoughtcrime.securesms.mediasend.v2.UntrustedRecords import org.thoughtcrime.securesms.mediasend.v2.UntrustedRecords
import org.thoughtcrime.securesms.mediasend.v2.text.TextStoryPostCreationState import org.thoughtcrime.securesms.mediasend.v2.text.TextStoryPostCreationState
@ -52,10 +54,14 @@ class TextStoryPostSendRepository {
val recipient = Recipient.resolved(contact.requireShareContact().recipientId.get()) val recipient = Recipient.resolved(contact.requireShareContact().recipientId.get())
val isStory = contact is ContactSearchKey.RecipientSearchKey.Story || recipient.isDistributionList val isStory = contact is ContactSearchKey.RecipientSearchKey.Story || recipient.isDistributionList
if (isStory && recipient.isActiveGroup) { if (isStory && recipient.isActiveGroup && recipient.isGroup) {
SignalDatabase.groups.markDisplayAsStory(recipient.requireGroupId()) SignalDatabase.groups.markDisplayAsStory(recipient.requireGroupId())
} }
if (isStory && !recipient.isMyStory) {
SignalStore.storyValues().setLatestStorySend(StorySend.newSend(recipient))
}
val storyType: StoryType = when { val storyType: StoryType = when {
recipient.isDistributionList -> SignalDatabase.distributionLists.getStoryType(recipient.requireDistributionListId()) recipient.isDistributionList -> SignalDatabase.distributionLists.getStoryType(recipient.requireDistributionListId())
isStory -> StoryType.STORY_WITH_REPLIES isStory -> StoryType.STORY_WITH_REPLIES

Wyświetl plik

@ -25,6 +25,8 @@ import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.database.model.StoryType; import org.thoughtcrime.securesms.database.model.StoryType;
import org.thoughtcrime.securesms.database.model.databaseprotos.StoryTextPost; import org.thoughtcrime.securesms.database.model.databaseprotos.StoryTextPost;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.keyvalue.StorySend;
import org.thoughtcrime.securesms.linkpreview.LinkPreview; import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.mediasend.Media; import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.mediasend.v2.text.TextStoryBackgroundColors; import org.thoughtcrime.securesms.mediasend.v2.text.TextStoryBackgroundColors;
@ -222,10 +224,14 @@ public final class MultiShareSender {
storyType = StoryType.STORY_WITH_REPLIES; storyType = StoryType.STORY_WITH_REPLIES;
} }
if (recipient.isActiveGroup()) { if (recipient.isActiveGroup() && recipient.isGroup()) {
SignalDatabase.groups().markDisplayAsStory(recipient.requireGroupId()); SignalDatabase.groups().markDisplayAsStory(recipient.requireGroupId());
} }
if (!recipient.isMyStory()) {
SignalStore.storyValues().setLatestStorySend(StorySend.newSend(recipient));
}
if (multiShareArgs.isTextStory()) { if (multiShareArgs.isTextStory()) {
OutgoingMediaMessage outgoingMediaMessage = new OutgoingMediaMessage(recipient, OutgoingMediaMessage outgoingMediaMessage = new OutgoingMediaMessage(recipient,
new SlideDeck(), new SlideDeck(),

Wyświetl plik

@ -235,3 +235,7 @@ message GiftBadge {
bytes redemptionToken = 1; bytes redemptionToken = 1;
RedemptionState redemptionState = 2; RedemptionState redemptionState = 2;
} }
message SignalStoreList {
repeated string contents = 1;
}

Wyświetl plik

@ -5,12 +5,11 @@ import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.runners.JUnit4 import org.junit.runners.JUnit4
import org.mockito.ArgumentMatchers.any import org.mockito.kotlin.any
import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.kotlin.anyOrNull
import org.mockito.ArgumentMatchers.anyInt import org.mockito.kotlin.isNull
import org.mockito.ArgumentMatchers.isNull import org.mockito.kotlin.mock
import org.mockito.Mockito.mock import org.mockito.kotlin.whenever
import org.mockito.Mockito.`when`
import org.thoughtcrime.securesms.MockCursor import org.thoughtcrime.securesms.MockCursor
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.recipients.RecipientId
@ -18,20 +17,21 @@ import org.thoughtcrime.securesms.recipients.RecipientId
@RunWith(JUnit4::class) @RunWith(JUnit4::class)
class ContactSearchPagedDataSourceTest { class ContactSearchPagedDataSourceTest {
private val repository = mock(ContactSearchPagedDataSourceRepository::class.java) private val repository: ContactSearchPagedDataSourceRepository = mock()
private val cursor = mock(MockCursor::class.java) private val cursor: MockCursor = mock()
private val groupStoryData = ContactSearchData.Story(Recipient.UNKNOWN, 0) private val groupStoryData = ContactSearchData.Story(Recipient.UNKNOWN, 0)
@Before @Before
fun setUp() { fun setUp() {
`when`(repository.getRecipientFromGroupCursor(cursor)).thenReturn(Recipient.UNKNOWN) whenever(repository.getRecipientFromGroupCursor(cursor)).thenReturn(Recipient.UNKNOWN)
`when`(repository.getRecipientFromRecipientCursor(cursor)).thenReturn(Recipient.UNKNOWN) whenever(repository.getRecipientFromRecipientCursor(cursor)).thenReturn(Recipient.UNKNOWN)
`when`(repository.getRecipientFromThreadCursor(cursor)).thenReturn(Recipient.UNKNOWN) whenever(repository.getRecipientFromThreadCursor(cursor)).thenReturn(Recipient.UNKNOWN)
`when`(repository.getRecipientFromDistributionListCursor(cursor)).thenReturn(Recipient.UNKNOWN) whenever(repository.getRecipientFromDistributionListCursor(cursor)).thenReturn(Recipient.UNKNOWN)
`when`(repository.getGroupStories()).thenReturn(emptySet()) whenever(repository.getGroupStories()).thenReturn(emptySet())
`when`(cursor.moveToPosition(anyInt())).thenCallRealMethod() whenever(repository.getLatestStorySends(any())).thenReturn(emptyList())
`when`(cursor.moveToNext()).thenCallRealMethod() whenever(cursor.moveToPosition(any())).thenCallRealMethod()
`when`(cursor.position).thenCallRealMethod() whenever(cursor.moveToNext()).thenCallRealMethod()
whenever(cursor.position).thenCallRealMethod()
} }
@Test @Test
@ -126,9 +126,9 @@ class ContactSearchPagedDataSourceTest {
) )
} }
`when`(repository.getStories(any())).thenReturn(cursor) whenever(repository.getStories(anyOrNull())).thenReturn(cursor)
`when`(repository.recipientNameContainsQuery(Recipient.UNKNOWN, null)).thenReturn(true) whenever(repository.recipientNameContainsQuery(Recipient.UNKNOWN, null)).thenReturn(true)
`when`(cursor.count).thenReturn(10) whenever(cursor.count).thenReturn(10)
return ContactSearchPagedDataSource(configuration, repository) return ContactSearchPagedDataSource(configuration, repository)
} }
@ -151,9 +151,9 @@ class ContactSearchPagedDataSourceTest {
) )
} }
`when`(repository.getRecents(recents)).thenReturn(cursor) whenever(repository.getRecents(recents)).thenReturn(cursor)
`when`(repository.queryNonGroupContacts(isNull(), anyBoolean())).thenReturn(cursor) whenever(repository.queryNonGroupContacts(isNull(), any())).thenReturn(cursor)
`when`(cursor.count).thenReturn(10) whenever(cursor.count).thenReturn(10)
return ContactSearchPagedDataSource(configuration, repository) return ContactSearchPagedDataSource(configuration, repository)
} }