Allow 1:1 polls and raise character limit.

This commit is contained in:
Michelle Tang
2026-01-05 12:56:13 -05:00
committed by jeffrey-signal
parent fd635542c0
commit f7d87f3436
19 changed files with 138 additions and 156 deletions

View File

@@ -4,8 +4,6 @@ import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import assertk.assertThat
import assertk.assertions.isEqualTo
import io.mockk.every
import io.mockk.mockkStatic
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -23,7 +21,6 @@ import org.thoughtcrime.securesms.testing.GroupTestingUtils
import org.thoughtcrime.securesms.testing.GroupTestingUtils.asMember
import org.thoughtcrime.securesms.testing.MessageContentFuzzer
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.util.RemoteConfig
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
import org.whispersystems.signalservice.internal.push.DataMessage
@@ -42,10 +39,6 @@ class DataMessageProcessorTest_polls {
@Before
fun setUp() {
mockkStatic(RemoteConfig::class)
every { RemoteConfig.receivePolls } returns true
alice = Recipient.resolved(harness.others[0])
bob = Recipient.resolved(harness.others[1])
charlie = Recipient.resolved(harness.others[2])

View File

@@ -135,10 +135,6 @@ object ExportSkips {
return log(sentTimestamp, "Poll option was invalid.")
}
fun pollNotInGroupChat(sentTimestamp: Long): String {
return log(sentTimestamp, "Poll was not in a group chat.")
}
fun pinMessageIsInvalid(sentTimestamp: Long): String {
return log(sentTimestamp, "Pin message update was invalid.")
}

View File

@@ -122,6 +122,7 @@ private val TAG = Log.tag(ChatItemArchiveExporter::class.java)
private val MAX_INLINED_BODY_SIZE = 128.kibiBytes.bytes.toInt()
private val MAX_INLINED_BODY_SIZE_WITH_LONG_ATTACHMENT_POINTER = 2.kibiBytes.bytes.toInt()
private val MAX_INLINED_QUOTE_BODY_SIZE = 2.kibiBytes.bytes.toInt()
private const val MAX_POLL_QUESTION_CHARACTER_LENGTH = 200
private const val MAX_POLL_CHARACTER_LENGTH = 100
private const val MAX_POLL_OPTIONS = 10
@@ -398,13 +399,8 @@ class ChatItemArchiveExporter(
}
extraData.pollsById[record.id] != null -> {
if (exportState.threadIdToRecipientId[builder.chatId] !in exportState.groupRecipientIds) {
Log.w(TAG, ExportSkips.pollNotInGroupChat(record.dateSent))
continue
}
val poll = extraData.pollsById[record.id]!!
if (poll.question.isEmpty() || poll.question.length > MAX_POLL_CHARACTER_LENGTH) {
if (poll.question.isEmpty() || poll.question.length > MAX_POLL_QUESTION_CHARACTER_LENGTH) {
Log.w(TAG, ExportSkips.invalidPollQuestion(record.dateSent))
continue
}

View File

@@ -218,12 +218,11 @@ class ConversationRepository(
val threadRecipient = SignalDatabase.threads.getRecipientForThreadId(messageRecord.threadId)!!
val pollSentTimestamp = messageRecord.dateSent
if (threadRecipient.groupId.getOrNull()?.isV2 != true) {
if (threadRecipient.isPushV2Group && threadRecipient.groupId.getOrNull()?.isV2 != true) {
Log.w(TAG, "Missing group id")
emitter.tryOnError(Exception("Poll terminate failed"))
}
val groupId = threadRecipient.requireGroupId().requireV2()
val message = OutgoingMessage.pollTerminateMessage(
threadRecipient = threadRecipient,
sentTimeMillis = System.currentTimeMillis(),
@@ -233,12 +232,17 @@ class ConversationRepository(
Log.i(TAG, "Sending poll terminate to " + message.threadRecipient.id + ", thread: " + messageRecord.threadId)
val possibleTargets: List<Recipient> = SignalDatabase.groups.getGroupMembers(groupId, GroupTable.MemberSet.FULL_MEMBERS_EXCLUDING_SELF)
.map { it.resolve() }
.distinctBy { it.id }
val possibleTargets: List<Recipient> = if (threadRecipient.isPushV2Group) {
SignalDatabase.groups.getGroupMembers(threadRecipient.requireGroupId().requireV2(), GroupTable.MemberSet.FULL_MEMBERS_EXCLUDING_SELF)
.map { it.resolve() }
.distinctBy { it.id }
} else {
listOf(threadRecipient)
}
val isSelf = threadRecipient.isSelf
val eligibleTargets: List<Recipient> = RecipientUtil.getEligibleForSending(possibleTargets)
val results = sendEndPoll(threadRecipient, message, eligibleTargets, poll.messageId)
val results = sendEndPoll(threadRecipient, message, eligibleTargets, isSelf, poll.messageId)
val sendResults = GroupSendJobHelper.getCompletedSends(eligibleTargets, results)
if (sendResults.completed.isNotEmpty() || possibleTargets.isEmpty()) {
@@ -271,9 +275,9 @@ class ConversationRepository(
}
@Throws(IOException::class, GroupNotAMemberException::class, UndeliverableMessageException::class)
fun sendEndPoll(group: Recipient, message: OutgoingMessage, destinations: List<Recipient>, messageId: Long): List<SendMessageResult?> {
val groupId = group.requireGroupId().requireV2()
val groupRecord: GroupRecord? = SignalDatabase.groups.getGroup(group.requireGroupId()).getOrNull()
fun sendEndPoll(threadRecipient: Recipient, message: OutgoingMessage, destinations: List<Recipient>, isSelf: Boolean, messageId: Long): List<SendMessageResult?> {
val groupId = if (threadRecipient.isPushV2Group) threadRecipient.requireGroupId().requireV2() else null
val groupRecord: GroupRecord? = if (threadRecipient.isPushV2Group) SignalDatabase.groups.getGroup(threadRecipient.requireGroupId()).getOrNull() else null
if (groupRecord != null && groupRecord.isAnnouncementGroup && !groupRecord.isAdmin(Recipient.self())) {
throw UndeliverableMessageException("Non-admins cannot send messages in announcement groups!")
@@ -281,29 +285,35 @@ class ConversationRepository(
val builder = newBuilder()
GroupUtil.setDataMessageGroupContext(AppDependencies.application, builder, groupId)
if (groupId != null) {
GroupUtil.setDataMessageGroupContext(AppDependencies.application, builder, groupId)
}
val sentTime = System.currentTimeMillis()
val groupMessage = builder
val message = builder
.withTimestamp(sentTime)
.withExpiration((message.expiresIn / 1000).toInt())
.withProfileKey(ProfileKeyUtil.getSelfProfileKey().serialize())
.withPollTerminate(SignalServiceDataMessage.PollTerminate(message.messageExtras!!.pollTerminate!!.targetTimestamp))
.build()
return GroupSendUtil.sendResendableDataMessage(
applicationContext,
groupId,
null,
destinations,
false,
ContentHint.RESENDABLE,
MessageId(messageId),
groupMessage,
true,
false,
null
) { System.currentTimeMillis() - sentTime > POLL_TERMINATE_TIMEOUT.inWholeMilliseconds }
return if (isSelf) {
listOf(AppDependencies.signalServiceMessageSender.sendSyncMessage(message))
} else {
GroupSendUtil.sendResendableDataMessage(
applicationContext,
groupId,
null,
destinations,
false,
ContentHint.RESENDABLE,
MessageId(messageId),
message,
true,
false,
null
) { System.currentTimeMillis() - sentTime > POLL_TERMINATE_TIMEOUT.inWholeMilliseconds }
}
}
fun getPinnedMessages(threadId: Long): List<MmsMessageRecord> {

View File

@@ -67,6 +67,7 @@ import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.ComposeDialogFragment
import org.thoughtcrime.securesms.polls.Poll
import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.ViewUtil
import kotlin.time.Duration.Companion.milliseconds
@@ -78,6 +79,7 @@ class CreatePollFragment : ComposeDialogFragment() {
companion object {
private val TAG = Log.tag(CreatePollFragment::class)
val MAX_QUESTION_CHARACTER_LENGTH = if (RemoteConfig.pollsV2) 200 else 100
const val MAX_CHARACTER_LENGTH = 100
const val MAX_OPTIONS = 10
const val MIN_OPTIONS = 2
@@ -222,7 +224,7 @@ private fun CreatePollScreen(
TextFieldWithCountdown(
value = question,
label = { Text(text = stringResource(R.string.CreatePollFragment__ask_a_question)) },
onValueChange = { question = it.substring(0, minOf(it.length, CreatePollFragment.MAX_CHARACTER_LENGTH)) },
onValueChange = { question = it.substring(0, minOf(it.length, CreatePollFragment.MAX_QUESTION_CHARACTER_LENGTH)) },
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences),
colors = TextFieldDefaults.colors(
unfocusedContainerColor = MaterialTheme.colorScheme.surfaceVariant,
@@ -232,6 +234,7 @@ private fun CreatePollScreen(
.fillMaxWidth()
.onFocusChanged { focusState -> if (focusState.isFocused) focusedOption = -1 }
.focusRequester(focusRequester),
maxCharacterLength = CreatePollFragment.MAX_QUESTION_CHARACTER_LENGTH,
countdownThreshold = CreatePollFragment.CHARACTER_COUNTDOWN_THRESHOLD
)
@@ -266,6 +269,7 @@ private fun CreatePollScreen(
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
},
maxCharacterLength = CreatePollFragment.MAX_CHARACTER_LENGTH,
countdownThreshold = CreatePollFragment.CHARACTER_COUNTDOWN_THRESHOLD
)
}
@@ -316,9 +320,10 @@ private fun TextFieldWithCountdown(
colors: TextFieldColors,
modifier: Modifier,
trailingIcon: @Composable () -> Unit = {},
maxCharacterLength: Int,
countdownThreshold: Int
) {
val charactersRemaining = CreatePollFragment.MAX_CHARACTER_LENGTH - value.length
val charactersRemaining = maxCharacterLength - value.length
val displayCountdown = charactersRemaining <= countdownThreshold
Box(modifier = Modifier.padding(start = 24.dp, end = 24.dp, bottom = 16.dp)) {

View File

@@ -131,7 +131,7 @@ class AttachmentKeyboardFragment : LoggingFragment(R.layout.attachment_keyboard_
private fun updateButtonsAvailable(recipient: Recipient) {
val paymentsValues = SignalStore.payments
val isPaymentsAvailable = paymentsValues.paymentsAvailability.isSendAllowed && !recipient.isSelf && !recipient.isGroup && recipient.isRegistered
val isPollsAvailable = recipient.isPushV2Group && RemoteConfig.polls
val isPollsAvailable = recipient.isPushV2Group || RemoteConfig.pollsV2
if (!isPaymentsAvailable && !isPollsAvailable) {
attachmentKeyboardView.filterAttachmentKeyboardButtons(removePaymentFilter.and(removePollFilter))

View File

@@ -286,6 +286,8 @@ public class IndividualSendJob extends PushSendJob {
SignalServiceDataMessage.GiftBadge giftBadge = getGiftBadgeFor(message);
SignalServiceDataMessage.Payment payment = getPayment(message);
List<BodyRange> bodyRanges = getBodyRanges(message);
SignalServiceDataMessage.PollCreate pollCreate = getPollCreate(message);
SignalServiceDataMessage.PollTerminate pollTerminate = getPollTerminate(message);
SignalServiceDataMessage.PinnedMessage pinnedMessage = getPinnedMessage(message);
SignalServiceDataMessage.Builder mediaMessageBuilder = SignalServiceDataMessage.newBuilder()
.withBody(message.getBody())
@@ -303,6 +305,8 @@ public class IndividualSendJob extends PushSendJob {
.asEndSessionMessage(message.isEndSession())
.withPayment(payment)
.withBodyRanges(bodyRanges)
.withPollCreate(pollCreate)
.withPollTerminate(pollTerminate)
.withPinnedMessage(pinnedMessage);
if (message.getParentStoryId() != null) {

View File

@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.jobs
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
@@ -49,7 +50,11 @@ class PollVoteJob(
return null
}
val recipients = conversationRecipient.participantIds.filter { it != Recipient.self().id }.map { it.toLong() }
val recipients = if (conversationRecipient.isGroup) {
conversationRecipient.participantIds.filter { it != Recipient.self().id }.map { it.toLong() }
} else {
listOf(conversationRecipient.id.toLong())
}
return PollVoteJob(
messageId = messageId,
@@ -108,7 +113,7 @@ class PollVoteJob(
val targetSentTimestamp = message.dateSent
val recipients = Recipient.resolvedList(recipientIds.filter { it != Recipient.self().id.toLong() }.map { RecipientId.from(it) })
val recipients = Recipient.resolvedList(recipientIds.map { RecipientId.from(it) })
val registered = RecipientUtil.getEligibleForSending(recipients)
val unregistered = recipients - registered.toSet()
val completions: List<Recipient> = deliver(conversationRecipient, registered, targetAuthor, targetSentTimestamp, poll)
@@ -140,15 +145,18 @@ class PollVoteJob(
)
)
GroupUtil.setDataMessageGroupContext(context, dataMessageBuilder, conversationRecipient.requireGroupId().requirePush())
if (conversationRecipient.isPushV2Group) {
GroupUtil.setDataMessageGroupContext(context, dataMessageBuilder, conversationRecipient.requireGroupId().requirePush())
}
val dataMessage = dataMessageBuilder.build()
val nonSelfDestinations = destinations.filter { !it.isSelf }
val results = GroupSendUtil.sendResendableDataMessage(
context,
conversationRecipient.groupId.map { obj: GroupId -> obj.requireV2() }.orElse(null),
null,
destinations,
nonSelfDestinations,
false,
ContentHint.RESENDABLE,
MessageId(messageId),
@@ -159,6 +167,10 @@ class PollVoteJob(
null
)
if (conversationRecipient.isSelf) {
results.add(AppDependencies.signalServiceMessageSender.sendSyncMessage(dataMessage))
}
val groupResult = GroupSendJobHelper.getCompletedSends(destinations, results)
for (unregistered in groupResult.unregistered) {

View File

@@ -37,7 +37,6 @@ import org.thoughtcrime.securesms.messages.StorySendUtil;
import org.thoughtcrime.securesms.mms.MessageGroupContext;
import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.OutgoingMessage;
import org.thoughtcrime.securesms.polls.Poll;
import org.thoughtcrime.securesms.ratelimit.ProofRequiredExceptionHandler;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
@@ -285,8 +284,8 @@ public final class PushGroupSendJob extends PushSendJob {
List<SignalServicePreview> previews = getPreviewsFor(message);
List<SignalServiceDataMessage.Mention> mentions = getMentionsFor(message.getMentions());
List<BodyRange> bodyRanges = getBodyRanges(message);
Optional<SignalServiceDataMessage.PollCreate> pollCreate = getPollCreate(message);
Optional<SignalServiceDataMessage.PollTerminate> pollTerminate = getPollTerminate(message);
SignalServiceDataMessage.PollCreate pollCreate = getPollCreate(message);
SignalServiceDataMessage.PollTerminate pollTerminate = getPollTerminate(message);
SignalServiceDataMessage.PinnedMessage pinnedMessage = getPinnedMessage(message);
List<Attachment> attachments = Stream.of(message.getAttachments()).filterNot(Attachment::isSticker).toList();
List<SignalServiceAttachment> attachmentPointers = getAttachmentPointersFor(attachments);
@@ -371,8 +370,8 @@ public final class PushGroupSendJob extends PushSendJob {
.withPreviews(previews)
.withMentions(mentions)
.withBodyRanges(bodyRanges)
.withPollCreate(pollCreate.orElse(null))
.withPollTerminate(pollTerminate.orElse(null))
.withPollCreate(pollCreate)
.withPollTerminate(pollTerminate)
.withPinnedMessage(pinnedMessage);
if (message.getParentStoryId() != null) {
@@ -419,23 +418,6 @@ public final class PushGroupSendJob extends PushSendJob {
}
}
private Optional<SignalServiceDataMessage.PollCreate> getPollCreate(OutgoingMessage message) {
Poll poll = message.getPoll();
if (poll == null) {
return Optional.empty();
}
return Optional.of(new SignalServiceDataMessage.PollCreate(poll.getQuestion(), poll.getAllowMultipleVotes(), poll.getPollOptions()));
}
private Optional<SignalServiceDataMessage.PollTerminate> getPollTerminate(OutgoingMessage message) {
if (message.getMessageExtras() == null || message.getMessageExtras().pollTerminate == null) {
return Optional.empty();
}
return Optional.of(new SignalServiceDataMessage.PollTerminate(message.getMessageExtras().pollTerminate.targetTimestamp));
}
public static long getMessageId(@Nullable byte[] serializedData) {
JsonJobData data = JsonJobData.deserialize(serializedData);
return data.getLong(KEY_MESSAGE_ID);

View File

@@ -54,6 +54,7 @@ import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.mms.QuoteModel;
import org.thoughtcrime.securesms.net.NotPushRegisteredException;
import org.thoughtcrime.securesms.notifications.v2.ConversationId;
import org.thoughtcrime.securesms.polls.Poll;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
@@ -486,6 +487,23 @@ public abstract class PushSendJob extends SendJob {
return getBodyRanges(message.getBodyRanges());
}
protected @Nullable SignalServiceDataMessage.PollCreate getPollCreate(OutgoingMessage message) {
Poll poll = message.getPoll();
if (poll == null) {
return null;
}
return new SignalServiceDataMessage.PollCreate(poll.getQuestion(), poll.getAllowMultipleVotes(), poll.getPollOptions());
}
protected @Nullable SignalServiceDataMessage.PollTerminate getPollTerminate(OutgoingMessage message) {
if (message.getMessageExtras() == null || message.getMessageExtras().pollTerminate == null) {
return null;
}
return new SignalServiceDataMessage.PollTerminate(message.getMessageExtras().pollTerminate.targetTimestamp);
}
protected @Nullable List<BodyRange> getBodyRanges(@Nullable BodyRangeList bodyRanges) {
if (bodyRanges == null || bodyRanges.ranges.size() == 0) {
return null;

View File

@@ -124,6 +124,7 @@ import kotlin.time.Duration.Companion.seconds
object DataMessageProcessor {
private const val BODY_RANGE_PROCESSING_LIMIT = 250
private const val POLL_QUESTION_CHARACTER_LIMIT = 200
private const val POLL_CHARACTER_LIMIT = 100
private const val POLL_OPTIONS_LIMIT = 10
@@ -1066,28 +1067,23 @@ object DataMessageProcessor {
groupId: GroupId.V2?,
receivedTime: Long
): InsertResult? {
if (!RemoteConfig.receivePolls) {
log(envelope.timestamp!!, "Poll creation not allowed due to remote config.")
return null
}
log(envelope.timestamp!!, "Handle poll creation")
val poll: DataMessage.PollCreate = message.pollCreate!!
handlePossibleExpirationUpdate(envelope, metadata, senderRecipient, threadRecipient, groupId, message.expireTimerDuration, message.expireTimerVersion, receivedTime)
if (groupId == null) {
warn(envelope.timestamp!!, "[handlePollCreate] Polls can only be sent to groups. author: $senderRecipient")
if (groupId != null) {
val groupRecord = SignalDatabase.groups.getGroup(groupId).orNull()
if (groupRecord != null && !groupRecord.members.contains(senderRecipient.id)) {
warn(envelope.timestamp!!, "[handlePollCreate] Poll author is not in the group. author $senderRecipient")
return null
}
} else if (senderRecipient.id != threadRecipient.id && senderRecipient.id != Recipient.self().id) {
warn(envelope.timestamp!!, "[handlePollCreate] Sender is not a part of the 1:1 thread!")
return null
}
val groupRecord = SignalDatabase.groups.getGroup(groupId).orNull()
if (groupRecord == null || !groupRecord.members.contains(senderRecipient.id)) {
warn(envelope.timestamp!!, "[handlePollCreate] Poll author is not in the group. author $senderRecipient")
return null
}
if (poll.question == null || poll.question!!.isEmpty() || poll.question!!.length > POLL_CHARACTER_LIMIT) {
if (poll.question == null || poll.question!!.isEmpty() || poll.question!!.length > POLL_QUESTION_CHARACTER_LIMIT) {
warn(envelope.timestamp!!, "[handlePollCreate] Poll question is invalid.")
return null
}
@@ -1136,11 +1132,6 @@ object DataMessageProcessor {
groupId: GroupId.V2?,
receivedTime: Long
): InsertResult? {
if (!RemoteConfig.receivePolls) {
log(envelope.timestamp!!, "Poll terminate not allowed due to remote config.")
return null
}
val pollTerminate: DataMessage.PollTerminate = message.pollTerminate!!
val targetSentTimestamp = pollTerminate.targetSentTimestamp!!
@@ -1189,11 +1180,6 @@ object DataMessageProcessor {
senderRecipient: Recipient,
earlyMessageCacheEntry: EarlyMessageCacheEntry?
): MessageId? {
if (!RemoteConfig.receivePolls) {
log(envelope.timestamp!!, "Poll vote not allowed due to remote config.")
return null
}
val pollVote: DataMessage.PollVote = message.pollVote!!
val targetSentTimestamp = pollVote.targetSentTimestamp!!
@@ -1578,14 +1564,12 @@ object DataMessageProcessor {
}
val groupRecord = SignalDatabase.groups.getGroup(targetThread.recipient.id).orNull()
if (groupRecord == null) {
warn(envelope.timestamp!!, "[handlePollValidation] Target thread needs to be a group. timestamp: $targetSentTimestamp author: ${targetAuthor.id}")
return null
}
if (!groupRecord.members.contains(senderRecipient.id)) {
if (groupRecord != null && !groupRecord.members.contains(senderRecipient.id)) {
warn(envelope.timestamp!!, "[handlePollValidation] Sender is not in the group. timestamp: $targetSentTimestamp author: ${targetAuthor.id}")
return null
} else if (groupRecord == null && senderRecipient.id != targetThread.recipient.id && senderRecipient.id != Recipient.self().id) {
warn(envelope.timestamp!!, "[handlePollValidation] Sender is not a part of the 1:1 thread!")
return null
}
return MessageId(targetMessage.id)

View File

@@ -1744,18 +1744,9 @@ object SyncMessageProcessor {
sent: Sent,
senderRecipient: Recipient
): Long {
if (!RemoteConfig.receivePolls) {
log(envelope.timestamp!!, "Sync poll create not allowed due to remote config.")
}
log(envelope.timestamp!!, "Synchronize sent poll creation message.")
val recipient = getSyncMessageDestination(sent)
if (!recipient.isGroup) {
warn(envelope.timestamp!!, "Poll creation messages should only be synced in groups. Dropping.")
return -1
}
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
val expiresInMillis = message.expireTimerDuration.inWholeMilliseconds
@@ -1777,8 +1768,12 @@ object SyncMessageProcessor {
question = poll.question!!
)
val messageId = SignalDatabase.messages.insertMessageOutbox(outgoingMessage, threadId, false, GroupReceiptTable.STATUS_UNKNOWN, null).messageId
updateGroupReceiptStatus(sent, messageId, recipient.requireGroupId())
val receiptStatus = if (recipient.isGroup) GroupReceiptTable.STATUS_UNKNOWN else GroupReceiptTable.STATUS_UNDELIVERED
val messageId = SignalDatabase.messages.insertMessageOutbox(outgoingMessage, threadId, false, receiptStatus, null).messageId
if (recipient.isGroup) {
updateGroupReceiptStatus(sent, messageId, recipient.requireGroupId())
}
log(envelope.timestamp!!, "Inserted sync poll create message as messageId $messageId")
@@ -1799,18 +1794,9 @@ object SyncMessageProcessor {
senderRecipient: Recipient,
earlyMessageCacheEntry: EarlyMessageCacheEntry?
): Long {
if (!RemoteConfig.receivePolls) {
log(envelope.timestamp!!, "Sync poll end not allowed due to remote config.")
}
log(envelope.timestamp!!, "Synchronize sent poll terminate message")
val recipient = getSyncMessageDestination(sent)
if (!recipient.isGroup) {
warn(envelope.timestamp!!, "Poll termination messages should only be synced in groups. Dropping.")
return -1
}
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
val expiresInMillis = message.expireTimerDuration.inWholeMilliseconds
@@ -1847,7 +1833,8 @@ object SyncMessageProcessor {
)
)
val messageId = SignalDatabase.messages.insertMessageOutbox(outgoingMessage, threadId, false, GroupReceiptTable.STATUS_UNKNOWN, null).messageId
val receiptStatus = if (recipient.isGroup) GroupReceiptTable.STATUS_UNKNOWN else GroupReceiptTable.STATUS_UNDELIVERED
val messageId = SignalDatabase.messages.insertMessageOutbox(outgoingMessage, threadId, false, receiptStatus, null).messageId
SignalDatabase.messages.markAsSent(messageId, true)
log(envelope.timestamp!!, "Inserted sync poll end message as messageId $messageId")

View File

@@ -265,12 +265,11 @@ public class MessageSender {
long messageId = insertResult.getMessageId();
if (!recipient.isPushV2Group()) {
Log.w(TAG, "Can only send polls to groups.");
return threadId;
SignalLocalMetrics.IndividualMessageSend.onInsertedIntoDatabase(messageId, metricId);
} else {
SignalLocalMetrics.GroupMessageSend.onInsertedIntoDatabase(messageId, metricId);
}
SignalLocalMetrics.GroupMessageSend.onInsertedIntoDatabase(messageId, metricId);
sendMessageInternal(context, recipient, sendType, messageId, insertResult.getQuoteAttachmentId(), Collections.emptyList());
onMessageSent();
SignalDatabase.threads().update(allocatedThreadId, true, true);

View File

@@ -1166,14 +1166,6 @@ object RemoteConfig {
hotSwappable = true
)
@JvmStatic
@get:JvmName("polls")
val polls: Boolean by remoteBoolean(
key = "android.polls.2",
defaultValue = false,
hotSwappable = true
)
/** Whether or not to send over binary service ids (alongside string service ids). */
@JvmStatic
@get:JvmName("useBinaryId")
@@ -1183,14 +1175,6 @@ object RemoteConfig {
hotSwappable = false
)
@JvmStatic
@get:JvmName("receivePolls")
val receivePolls: Boolean by remoteBoolean(
key = "android.receivePolls",
defaultValue = false,
hotSwappable = true
)
@JvmStatic
@get:JvmName("backupsBetaMegaphone")
val backupsBetaMegaphone: Boolean by remoteBoolean(
@@ -1238,5 +1222,16 @@ object RemoteConfig {
defaultValue = "*:10000",
hotSwappable = true
)
/**
* Whether or not to allow 1:1 polls and a higher character limit for questions
*/
@JvmStatic
@get:JvmName("pollsV2")
val pollsV2: Boolean by remoteBoolean(
key = "android.pollsV2",
defaultValue = false,
hotSwappable = true
)
// endregion
}

View File

@@ -8941,7 +8941,7 @@
<!-- Dialog title when you go to end a poll -->
<string name="Poll__end_poll_title">End poll?</string>
<!-- Dialog body when you go to end a poll -->
<string name="Poll__end_poll_body">Group members will no longer be able to vote in this poll.</string>
<string name="Poll__end_poll_body">Members of this chat will no longer be able to vote in this poll.</string>
<!-- Body used in dialog when we are waiting for network -->
<string name="Poll__waiting_for_network">Waiting for network…</string>
<!-- Header when creating a new poll -->

View File

@@ -26,7 +26,7 @@ androidx-navigation = "2.8.5"
androidx-navigation3-core = "1.0.0"
androidx-window = "1.3.0"
glide = "4.15.1"
libsignal-client = "0.86.8"
libsignal-client = "0.86.9"
mp4parser = "1.9.39"
accompanist = "0.28.0"
nanohttpd = "2.3.1"

View File

@@ -15400,20 +15400,20 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="57b3cf8f247f1990211110734a7d1af413db145c8f17eb1b2cdc9b9321188c2b" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.signal" name="libsignal-android" version="0.86.8">
<artifact name="libsignal-android-0.86.8.aar">
<sha256 value="a9fc5fd6bf12fea69318f6c992f8f52804950bc7888ae3df3d7bad6b1c18e8a0" origin="Generated by Gradle"/>
<component group="org.signal" name="libsignal-android" version="0.86.9">
<artifact name="libsignal-android-0.86.9.aar">
<sha256 value="1780958d9a7ef098c49ee233c2e9ae3b8b5aa3d14ca4c293678d80611d008866" origin="Generated by Gradle"/>
</artifact>
<artifact name="libsignal-android-0.86.8.module">
<sha256 value="4ce19af1fc9442c8eb231eb1f2523557291fc9e9dce2520b45d4ab7c6e9642d7" origin="Generated by Gradle"/>
<artifact name="libsignal-android-0.86.9.module">
<sha256 value="aa75573a7ff8f67118725242674000622eed7c903256c74ed393225878c3c7c3" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.signal" name="libsignal-client" version="0.86.8">
<artifact name="libsignal-client-0.86.8.jar">
<sha256 value="bac0edb5e25a73686aa418a6e43e0e81df1744e2751957a5dcb960e03d59a1c3" origin="Generated by Gradle"/>
<component group="org.signal" name="libsignal-client" version="0.86.9">
<artifact name="libsignal-client-0.86.9.jar">
<sha256 value="64836ce8aab3eda74ed3e2627b9c1a2c69a463cf0fe918d793ac745a4d24e30a" origin="Generated by Gradle"/>
</artifact>
<artifact name="libsignal-client-0.86.8.module">
<sha256 value="b9e8afcc22d50a4818f9d56cb40beda66c6532710c432a27be6b05c4ba5301af" origin="Generated by Gradle"/>
<artifact name="libsignal-client-0.86.9.module">
<sha256 value="81a4e9765936d88dc6b7fc66517c76d1d6b879f92ba967126a7539dc1dd38fd9" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.signal" name="ringrtc-android" version="2.61.0">

View File

@@ -29,6 +29,7 @@ import org.whispersystems.signalservice.internal.util.Util
*/
object EnvelopeContentValidator {
private const val MAX_POLL_QUESTION_CHARACTER_LENGTH = 200
private const val MAX_POLL_CHARACTER_LENGTH = 100
private const val MIN_POLL_OPTIONS = 2
@@ -167,7 +168,7 @@ object EnvelopeContentValidator {
}
private fun DataMessage.PollCreate.hasInvalidPollQuestion(): Boolean {
return this.question.isNullOrBlank() || this.question.length > MAX_POLL_CHARACTER_LENGTH
return this.question.isNullOrBlank() || this.question.length > MAX_POLL_QUESTION_CHARACTER_LENGTH
}
private fun DataMessage.PollCreate.hasInvalidPollOptions(): Boolean {

View File

@@ -50,11 +50,11 @@ class EnvelopeContentValidatorTest {
}
@Test
fun `validate - ensure polls with a question exceeding 100 characters are marked invalid`() {
fun `validate - ensure polls with a question exceeding 200 characters are marked invalid`() {
val content = Content(
dataMessage = DataMessage(
pollCreate = DataMessage.PollCreate(
question = "abcdefghijklmnopqrstuvwxyabcdefghijklmnopqrstuvwxyabcdefghijklmnopqrstuvwxyabcdefghijklmnopqrstuvwxyz",
question = "abcdefghijklmnopqrstuvwxyabcdefghijklmnopqrstuvwxyabcdefghijklmnopqrstuvwxyabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyabcdefghijklmnopqrstuvwxyabcdefghijklmnopqrstuvwxyabcdefghijklmnopqrstuvwxyz",
options = listOf("option1", "option2"),
allowMultiple = true
)