kopia lustrzana https://github.com/ryukoposting/Signal-Android
Apply new story list ordering rules.
Co-authored-by: Cody Henthorne <cody@signal.org>fork-5.53.8
rodzic
3b07f4a8ca
commit
88a66b49ff
|
@ -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
|
|
@ -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 }
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -235,3 +235,7 @@ message GiftBadge {
|
||||||
bytes redemptionToken = 1;
|
bytes redemptionToken = 1;
|
||||||
RedemptionState redemptionState = 2;
|
RedemptionState redemptionState = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message SignalStoreList {
|
||||||
|
repeated string contents = 1;
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
Ładowanie…
Reference in New Issue