Apply new story list ordering rules.

Co-authored-by: Cody Henthorne <cody@signal.org>
This commit is contained in:
Alex Hart 2022-06-14 12:50:53 -03:00 committed by Greyson Parrelli
parent 3b07f4a8ca
commit 88a66b49ff
16 changed files with 319 additions and 77 deletions

View file

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

View file

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

View file

@ -3,6 +3,8 @@ package org.thoughtcrime.securesms.contacts.paged
import android.database.Cursor
import org.signal.paging.PagedDataSource
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyvalue.StorySend
import java.util.concurrent.TimeUnit
import kotlin.math.min
/**
@ -13,6 +15,14 @@ class ContactSearchPagedDataSource(
private val contactSearchPagedDataSourceRepository: ContactSearchPagedDataSourceRepository = ContactSearchPagedDataSourceRepository(ApplicationDependencies.getApplication())
) : 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 {
return contactConfiguration.sections.sumOf {
getSectionSize(it, contactConfiguration.query)
@ -67,26 +77,25 @@ class ContactSearchPagedDataSource(
}
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.Groups -> contactSearchPagedDataSourceRepository.getGroupContacts(section, query)
is ContactSearchConfiguration.Section.Recents -> getRecentsCursor(section, query)
is ContactSearchConfiguration.Section.Stories -> getStoriesCursor(query)
}!!
}!!.use { cursor ->
val extras: List<ContactSearchData> = when (section) {
is ContactSearchConfiguration.Section.Stories -> getFilteredGroupStories(section, query)
else -> emptyList()
}
val extras: List<ContactSearchData> = when (section) {
is ContactSearchConfiguration.Section.Stories -> getFilteredGroupStories(section, query)
else -> emptyList()
val collection = createResultsCollection(
section = section,
cursor = cursor,
extraData = extras,
cursorMapper = { error("Unsupported") }
)
return collection.getSize()
}
val collection = ResultsCollection(
section = section,
cursor = cursor,
extraData = extras,
cursorMapper = { error("Unsupported") }
)
return collection.getSize()
}
private fun getFilteredGroupStories(section: ContactSearchConfiguration.Section.Stories, query: String?): List<ContactSearchData> {
@ -133,7 +142,7 @@ class ContactSearchPagedDataSource(
): List<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))
return results
@ -201,14 +210,27 @@ class ContactSearchPagedDataSource(
} ?: 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]
*/
private data class ResultsCollection(
private open class ResultsCollection(
val section: ContactSearchConfiguration.Section,
val cursor: Cursor,
val extraData: List<ContactSearchData>,
val cursorMapper: (Cursor) -> ContactSearchData
val cursorMapper: (Cursor) -> ContactSearchData,
val activeContactCount: Int
) {
private val contentSize = cursor.count + extraData.count()
@ -216,7 +238,7 @@ class ContactSearchPagedDataSource(
fun getSize(): Int {
val contentsAndExpand = min(
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,
contentSize
)
@ -239,22 +261,74 @@ class ContactSearchPagedDataSource(
index == getSize() - 1 && shouldDisplayExpandRow() -> ContactSearchData.Expand(section.sectionKey)
else -> {
val correctedIndex = if (section.includeHeader) index - 1 else index
if (correctedIndex < cursor.count) {
cursor.moveToPosition(correctedIndex)
cursorMapper.invoke(cursor)
} else {
val extraIndex = correctedIndex - cursor.count
extraData[extraIndex]
}
return getItemAtCorrectedIndex(correctedIndex)
}
}
}
protected open fun getItemAtCorrectedIndex(correctedIndex: Int): ContactSearchData {
return if (correctedIndex < cursor.count) {
cursor.moveToPosition(correctedIndex)
cursorMapper.invoke(cursor)
} else {
val extraIndex = correctedIndex - cursor.count
extraData[extraIndex]
}
}
private fun shouldDisplayExpandRow(): Boolean {
val expandConfig = section.expandConfig
return when {
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
}
}
}

View file

@ -9,6 +9,8 @@ import org.thoughtcrime.securesms.database.DistributionListDatabase
import org.thoughtcrime.securesms.database.GroupDatabase
import org.thoughtcrime.securesms.database.SignalDatabase
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.RecipientId
@ -22,6 +24,11 @@ open class ContactSearchPagedDataSourceRepository(
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? {
return contactRepository.querySignalContacts(query ?: "", includeSelf)
}

View file

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

View file

@ -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) {
this.id = id;
}

View file

@ -85,11 +85,11 @@ public final class PushGroupSendJob extends PushSendJob {
public PushGroupSendJob(long messageId, @NonNull RecipientId destination, @NonNull Set<RecipientId> filterRecipients, boolean hasMedia) {
this(new Job.Parameters.Builder()
.setQueue(destination.toQueueKey(hasMedia))
.addConstraint(NetworkConstraint.KEY)
.setLifespan(TimeUnit.DAYS.toMillis(1))
.setMaxAttempts(Parameters.UNLIMITED)
.build(),
.setQueue(destination.toQueueKey(hasMedia))
.addConstraint(NetworkConstraint.KEY)
.setLifespan(TimeUnit.DAYS.toMillis(1))
.setMaxAttempts(Parameters.UNLIMITED)
.build(),
messageId, filterRecipients);
}
@ -171,7 +171,7 @@ public final class PushGroupSendJob extends PushSendJob {
ApplicationDependencies.getJobManager().cancelAllInQueue(TypingSendJob.getQueue(threadId));
if (database.isSent(messageId)) {
log(TAG, String.valueOf(message.getSentTimeMillis()), "Message " + messageId + " was already sent. Ignoring.");
log(TAG, String.valueOf(message.getSentTimeMillis()), "Message " + messageId + " was already sent. Ignoring.");
return;
}
@ -246,21 +246,22 @@ public final class PushGroupSendJob extends PushSendJob {
List<SignalServiceDataMessage.Mention> mentions = getMentionsFor(message.getMentions());
List<Attachment> attachments = Stream.of(message.getAttachments()).filterNot(Attachment::isSticker).toList();
List<SignalServiceAttachment> attachmentPointers = getAttachmentPointersFor(attachments);
boolean isRecipientUpdate = Stream.of(SignalDatabase.groupReceipts().getGroupReceiptInfo(messageId))
.anyMatch(info -> info.getStatus() > GroupReceiptDatabase.STATUS_UNDELIVERED);
boolean isRecipientUpdate = Stream.of(SignalDatabase.groupReceipts().getGroupReceiptInfo(messageId))
.anyMatch(info -> info.getStatus() > GroupReceiptDatabase.STATUS_UNDELIVERED);
if (message.getStoryType().isStory()) {
Optional<GroupDatabase.GroupRecord> groupRecord = SignalDatabase.groups().getGroup(groupId);
if (groupRecord.isPresent()) {
GroupDatabase.V2GroupProperties v2GroupProperties = groupRecord.get().requireV2GroupProperties();
SignalServiceGroupV2 groupContext = SignalServiceGroupV2.newBuilder(v2GroupProperties.getGroupMasterKey())
.withRevision(v2GroupProperties.getGroupRevision())
.build();
SignalServiceGroupV2 groupContext = SignalServiceGroupV2.newBuilder(v2GroupProperties.getGroupMasterKey())
.withRevision(v2GroupProperties.getGroupRevision())
.build();
final SignalServiceStoryMessage storyMessage;
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()) {
storyMessage = SignalServiceStoryMessage.forFileAttachment(Recipient.self().getProfileKey(), groupContext, attachmentPointers.get(0), message.getStoryType().isStoryWithReplies());
} else {
@ -277,15 +278,15 @@ public final class PushGroupSendJob extends PushSendJob {
if (groupMessage.isV2Group()) {
MessageGroupContext.GroupV2Properties properties = groupMessage.requireGroupV2Properties();
GroupContextV2 groupContext = properties.getGroupContext();
SignalServiceGroupV2.Builder builder = SignalServiceGroupV2.newBuilder(properties.getGroupMasterKey())
.withRevision(groupContext.getRevision());
SignalServiceGroupV2.Builder builder = SignalServiceGroupV2.newBuilder(properties.getGroupMasterKey())
.withRevision(groupContext.getRevision());
ByteString groupChange = groupContext.getGroupChange();
if (groupChange != null) {
builder.withSignedGroupChange(groupChange.toByteArray());
}
SignalServiceGroupV2 group = builder.build();
SignalServiceGroupV2 group = builder.build();
SignalServiceDataMessage groupDataMessage = SignalServiceDataMessage.newBuilder()
.withTimestamp(message.getSentTimeMillis())
.withExpiration(groupRecipient.getExpiresInSeconds())
@ -309,7 +310,7 @@ public final class PushGroupSendJob extends PushSendJob {
SignalServiceDataMessage.Builder groupMessageBuilder = builder.withAttachments(attachmentPointers)
.withBody(message.getBody())
.withExpiration((int)(message.getExpiresIn() / 1000))
.withExpiration((int) (message.getExpiresIn() / 1000))
.withViewOnce(message.isViewOnce())
.asExpirationUpdate(message.isExpirationUpdate())
.withProfileKey(profileKey.orElse(null))
@ -377,7 +378,8 @@ public final class PushGroupSendJob extends PushSendJob {
RecipientAccessList accessList = new RecipientAccessList(target);
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);
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();
@ -390,8 +392,8 @@ public final class PushGroupSendJob extends PushSendJob {
skippedRecipients.addAll(skipped);
if (networkFailures.size() > 0 || identityMismatches.size() > 0 || proofRequired != null || unregisteredRecipients.size() > 0) {
Log.w(TAG, String.format(Locale.US, "Failed to send to some recipients. Network: %d, Identity: %d, ProofRequired: %s, Unregistered: %d",
networkFailures.size(), identityMismatches.size(), proofRequired != null, unregisteredRecipients.size()));
Log.w(TAG, String.format(Locale.US, "Failed to send to some recipients. Network: %d, Identity: %d, ProofRequired: %s, Unregistered: %d",
networkFailures.size(), identityMismatches.size(), proofRequired != null, unregisteredRecipients.size()));
}
RecipientDatabase recipientDatabase = SignalDatabase.recipients();
@ -459,7 +461,7 @@ public final class PushGroupSendJob extends PushSendJob {
private static @NonNull GroupRecipientResult getGroupMessageRecipients(@NonNull GroupId groupId, long messageId) {
List<GroupReceiptInfo> destinations = SignalDatabase.groupReceipts().getGroupReceiptInfo(messageId);
List<Recipient> possible;
List<Recipient> possible;
if (!destinations.isEmpty()) {
possible = Stream.of(destinations)
@ -471,9 +473,9 @@ public final class PushGroupSendJob extends PushSendJob {
Log.w(TAG, "No destinations found for group message " + groupId + " using current group membership");
possible = Stream.of(SignalDatabase.groups()
.getGroupMembers(groupId, GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF))
.map(Recipient::resolve)
.distinctBy(Recipient::getId)
.toList();
.map(Recipient::resolve)
.distinctBy(Recipient::getId)
.toList();
}
List<Recipient> eligible = RecipientUtil.getEligibleForSending(possible);

View file

@ -13,6 +13,7 @@ import com.annimon.stream.Stream;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.signal.core.util.Hex;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.metadata.certificate.InvalidCertificateException;
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.BitmapUtil;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.signal.core.util.Hex;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;

View file

@ -2,7 +2,13 @@ package org.thoughtcrime.securesms.keyvalue;
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.stream.Collectors;
abstract class SignalStoreValues {
@ -44,6 +50,25 @@ abstract class SignalStoreValues {
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) {
store.beginWrite().putBlob(key, value).apply();
}
@ -68,7 +93,21 @@ abstract class SignalStoreValues {
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) {
store.beginWrite().remove(key).apply();
}
interface Serializer<T> {
@NonNull String serialize(@NonNull T data);
T deserialize(@NonNull String data);
}
}

View file

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

View file

@ -1,5 +1,9 @@
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) {
companion object {
@ -14,6 +18,11 @@ internal class StoryValues(store: KeyValueStore) : SignalStoreValues(store) {
* Used to check whether we should display certain dialogs.
*/
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
@ -25,4 +34,44 @@ internal class StoryValues(store: KeyValueStore) : SignalStoreValues(store) {
var lastFontVersionCheck: Long by longValue(LAST_FONT_VERSION_CHECK, 0)
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)
}
}
}

View file

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

View file

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

View file

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

View file

@ -234,4 +234,8 @@ message GiftBadge {
bytes redemptionToken = 1;
RedemptionState redemptionState = 2;
}
message SignalStoreList {
repeated string contents = 1;
}

View file

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