diff --git a/app/src/main/java/org/thoughtcrime/securesms/sharing/DistributionListMultiShareTimestampProvider.kt b/app/src/main/java/org/thoughtcrime/securesms/sharing/DistributionListMultiShareTimestampProvider.kt new file mode 100644 index 000000000..ff595a41d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/sharing/DistributionListMultiShareTimestampProvider.kt @@ -0,0 +1,38 @@ +package org.thoughtcrime.securesms.sharing + +import androidx.annotation.Discouraged +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds + +/** + * Timestamp provider for distribution lists, which will reuse previous + * timestamps for identical indices. + */ +class DistributionListMultiShareTimestampProvider( + getCurrentTimeMillis: () -> Duration = { System.currentTimeMillis().milliseconds }, + sleepTimeout: Duration = 5.milliseconds +) : MultiShareTimestampProvider(getCurrentTimeMillis, sleepTimeout) { + + private val timestamps = mutableListOf(getCurrentTimeMillis()) + + override fun getMillis(index: Int): Long { + fillToIndex(index) + return timestamps[index].inWholeMilliseconds + } + + private fun fillToIndex(index: Int) { + if (index in timestamps.indices) { + return + } + + (timestamps.size..index).forEach { + timestamps.add(it, waitForTime()) + } + } + + companion object { + @JvmStatic + @Discouraged(message = "This only exists because of Java.") + fun create(): DistributionListMultiShareTimestampProvider = DistributionListMultiShareTimestampProvider() + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/sharing/MultiShareSender.java b/app/src/main/java/org/thoughtcrime/securesms/sharing/MultiShareSender.java index d014e9f0c..18273a1bd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sharing/MultiShareSender.java +++ b/app/src/main/java/org/thoughtcrime/securesms/sharing/MultiShareSender.java @@ -101,7 +101,7 @@ public final class MultiShareSender { return new MultiShareSendResultCollection(results); } - long distributionListSentTimestamp = System.currentTimeMillis(); + DistributionListMultiShareTimestampProvider distributionListSentTimestamps = DistributionListMultiShareTimestampProvider.create(); for (ContactSearchKey.RecipientSearchKey recipientSearchKey : multiShareArgs.getRecipientSearchKeys()) { Recipient recipient = Recipient.resolved(recipientSearchKey.getRecipientId()); @@ -123,8 +123,9 @@ public final class MultiShareSender { multiShareArgs.getLinkPreview() != null || !mentions.isEmpty() || needsSplit; - long sentTimestamp = recipient.isDistributionList() ? distributionListSentTimestamp : System.currentTimeMillis(); - boolean canSendAsTextStory = recipientSearchKey.isStory() && multiShareArgs.isValidForTextStoryGeneration(); + + MultiShareTimestampProvider sentTimestamp = recipient.isDistributionList() ? distributionListSentTimestamps : MultiShareTimestampProvider.create(); + boolean canSendAsTextStory = recipientSearchKey.isStory() && multiShareArgs.isValidForTextStoryGeneration(); if ((recipient.isMmsGroup() || recipient.getEmail().isPresent()) && !isMmsEnabled) { results.add(new MultiShareSendResult(recipientSearchKey, MultiShareSendResult.Type.MMS_NOT_ENABLED)); @@ -209,7 +210,7 @@ public final class MultiShareSender { int subscriptionId, @NonNull List validatedMentions, boolean isStory, - long sentTimestamp, + @NonNull MultiShareTimestampProvider sentTimestamps, boolean canSendAsTextStory, @NonNull List storiesToBatchSend, @NonNull ChatColors generatedTextStoryBackgroundColor) @@ -242,7 +243,7 @@ public final class MultiShareSender { OutgoingMessage outgoingMessage = new OutgoingMessage(recipient, new SlideDeck(), body, - sentTimestamp, + sentTimestamps.getMillis(0), subscriptionId, 0L, false, @@ -253,7 +254,7 @@ public final class MultiShareSender { outgoingMessages.add(outgoingMessage); } else if (canSendAsTextStory) { - outgoingMessages.add(generateTextStory(context, recipient, multiShareArgs, sentTimestamp, storyType, generatedTextStoryBackgroundColor)); + outgoingMessages.add(generateTextStory(context, recipient, multiShareArgs, sentTimestamps.getMillis(0), storyType, generatedTextStoryBackgroundColor)); } else { List storySupportedSlides = slideDeck.getSlides() .stream() @@ -269,14 +270,16 @@ public final class MultiShareSender { .filter(it -> MediaUtil.isStorySupportedType(it.getContentType())) .collect(Collectors.toList()); - for (final Slide slide : storySupportedSlides) { + for (int i = 0; i < storySupportedSlides.size(); i++) { + Slide slide = storySupportedSlides.get(i); SlideDeck singletonDeck = new SlideDeck(); + singletonDeck.addSlide(slide); OutgoingMessage outgoingMessage = new OutgoingMessage(recipient, singletonDeck, body, - sentTimestamp, + sentTimestamps.getMillis(i), subscriptionId, 0L, false, @@ -292,7 +295,7 @@ public final class MultiShareSender { OutgoingMessage outgoingMessage = new OutgoingMessage(recipient, slideDeck, body, - sentTimestamp, + sentTimestamps.getMillis(0), subscriptionId, expiresIn, isViewOnce, diff --git a/app/src/main/java/org/thoughtcrime/securesms/sharing/MultiShareTimestampProvider.kt b/app/src/main/java/org/thoughtcrime/securesms/sharing/MultiShareTimestampProvider.kt new file mode 100644 index 000000000..ec5462bab --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/sharing/MultiShareTimestampProvider.kt @@ -0,0 +1,30 @@ +package org.thoughtcrime.securesms.sharing + +import androidx.annotation.Discouraged +import org.signal.core.util.ThreadUtil +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds + +/** + * Default multi-share timestamp provider, which will return a different timestamp on each invocation. + */ +open class MultiShareTimestampProvider( + private val getCurrentTime: () -> Duration = { System.currentTimeMillis().milliseconds }, + private val sleepTimeout: Duration = 5.milliseconds +) { + + open fun getMillis(index: Int): Long { + return waitForTime().inWholeMilliseconds + } + + protected fun waitForTime(): Duration { + ThreadUtil.sleep(sleepTimeout.inWholeMilliseconds) + return getCurrentTime() + } + + companion object { + @JvmStatic + @Discouraged(message = "This only exists because of Java.") + fun create(): MultiShareTimestampProvider = MultiShareTimestampProvider() + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/sharing/DistributionListMultiShareTimestampProviderTest.kt b/app/src/test/java/org/thoughtcrime/securesms/sharing/DistributionListMultiShareTimestampProviderTest.kt new file mode 100644 index 000000000..797fad9bc --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/sharing/DistributionListMultiShareTimestampProviderTest.kt @@ -0,0 +1,18 @@ +package org.thoughtcrime.securesms.sharing + +import org.junit.Assert.assertEquals +import org.junit.Test +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds + +class DistributionListMultiShareTimestampProviderTest { + + @Test + fun `When I ask for index 4, then I expect to fill 4 new items`() { + val generator = mutableListOf(1L, 2, 3, 4, 5).map { it.seconds }.toMutableList() + val testSubject = DistributionListMultiShareTimestampProvider(getCurrentTimeMillis = { generator.removeAt(0) }, sleepTimeout = 0.seconds) + + val actual = testSubject.getMillis(4).milliseconds + assertEquals(5.seconds, actual) + } +}