mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-02-14 23:18:54 +00:00
Polls: Longer question length and 1:1 Receive Support
Co-authored-by: jimio <jimio@jimio-m3-max.local> Co-authored-by: Yash <yash@signal.org>
This commit is contained in:
@@ -18,7 +18,8 @@ import { getEmojiVariantByKey } from './fun/data/emojis.std.js';
|
||||
import { strictAssert } from '../util/assert.std.js';
|
||||
import {
|
||||
type PollCreateType,
|
||||
POLL_QUESTION_MAX_LENGTH,
|
||||
POLL_QUESTION_MAX_LENGTH_SEND,
|
||||
POLL_OPTION_MAX_LENGTH,
|
||||
POLL_OPTIONS_MIN_COUNT,
|
||||
POLL_OPTIONS_MAX_COUNT,
|
||||
} from '../types/Polls.dom.js';
|
||||
@@ -188,7 +189,7 @@ export function PollCreateModal({
|
||||
}
|
||||
|
||||
// Don't insert if it would exceed the max grapheme length
|
||||
if (countGraphemes(newValue) > POLL_QUESTION_MAX_LENGTH) {
|
||||
if (countGraphemes(newValue) > POLL_OPTION_MAX_LENGTH) {
|
||||
return opt; // Return unchanged
|
||||
}
|
||||
|
||||
@@ -314,8 +315,8 @@ export function PollCreateModal({
|
||||
value={question}
|
||||
onChange={handleQuestionChange}
|
||||
placeholder={i18n('icu:PollCreateModal__questionPlaceholder')}
|
||||
maxLengthCount={POLL_QUESTION_MAX_LENGTH}
|
||||
whenToShowRemainingCount={POLL_QUESTION_MAX_LENGTH - 30}
|
||||
maxLengthCount={POLL_QUESTION_MAX_LENGTH_SEND}
|
||||
whenToShowRemainingCount={POLL_QUESTION_MAX_LENGTH_SEND - 30}
|
||||
aria-invalid={validationErrors.question || undefined}
|
||||
aria-errormessage={
|
||||
validationErrors.question ? 'poll-question-error' : undefined
|
||||
@@ -340,8 +341,8 @@ export function PollCreateModal({
|
||||
placeholder={i18n('icu:PollCreateModal__optionPlaceholder', {
|
||||
number: String(index + 1),
|
||||
})}
|
||||
maxLengthCount={POLL_QUESTION_MAX_LENGTH}
|
||||
whenToShowRemainingCount={POLL_QUESTION_MAX_LENGTH - 30}
|
||||
maxLengthCount={POLL_OPTION_MAX_LENGTH}
|
||||
whenToShowRemainingCount={POLL_OPTION_MAX_LENGTH - 30}
|
||||
aria-invalid={validationErrors.options || undefined}
|
||||
aria-errormessage={
|
||||
validationErrors.options ? 'poll-options-error' : undefined
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
import { ContentHint } from '@signalapp/libsignal-client';
|
||||
import * as Errors from '../../types/errors.std.js';
|
||||
import { isGroupV2 } from '../../util/whatTypeOfConversation.dom.js';
|
||||
import { getSendOptions } from '../../util/getSendOptions.preload.js';
|
||||
import { handleMessageSend } from '../../util/handleMessageSend.preload.js';
|
||||
import { sendContentMessageToGroup } from '../../util/sendToGroup.preload.js';
|
||||
@@ -28,6 +27,11 @@ import type {
|
||||
import * as pollVoteUtil from '../../polls/util.std.js';
|
||||
import { strictAssert } from '../../util/assert.std.js';
|
||||
import { getSendRecipientLists } from './getSendRecipientLists.dom.js';
|
||||
import { isDirectConversation } from '../../util/whatTypeOfConversation.dom.js';
|
||||
import { isConversationAccepted } from '../../util/isConversationAccepted.preload.js';
|
||||
import { isConversationUnregistered } from '../../util/isConversationUnregistered.dom.js';
|
||||
import type { CallbackResultType } from '../../textsecure/Types.d.ts';
|
||||
import { addPniSignatureMessageToProto } from '../../textsecure/SendMessage.preload.js';
|
||||
|
||||
export async function sendPollVote(
|
||||
conversation: ConversationModel,
|
||||
@@ -52,10 +56,6 @@ export async function sendPollVote(
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isGroupV2(conversation.attributes)) {
|
||||
jobLog.error('sendPollVote: Non-group conversation; aborting');
|
||||
return;
|
||||
}
|
||||
let sendErrors: Array<Error> = [];
|
||||
const saveErrors = (errors: Array<Error>): void => {
|
||||
sendErrors = errors;
|
||||
@@ -173,10 +173,14 @@ export async function sendPollVote(
|
||||
|
||||
if (recipientServiceIdsWithoutMe.length === 0) {
|
||||
jobLog.info('sending sync poll vote message only');
|
||||
const groupV2Info = conversation.getGroupV2Info({
|
||||
members: recipientServiceIdsWithoutMe,
|
||||
});
|
||||
if (!groupV2Info) {
|
||||
|
||||
const groupV2Info = isDirectConversation(conversation.attributes)
|
||||
? undefined
|
||||
: conversation.getGroupV2Info({
|
||||
members: recipientServiceIdsWithoutMe,
|
||||
});
|
||||
|
||||
if (!groupV2Info && !isDirectConversation(conversation.attributes)) {
|
||||
jobLog.error(
|
||||
'sendPollVote: Missing groupV2Info for group conversation'
|
||||
);
|
||||
@@ -207,52 +211,107 @@ export async function sendPollVote(
|
||||
} else {
|
||||
const sendOptions = await getSendOptions(conversation.attributes);
|
||||
|
||||
const promise = conversation.queueJob(
|
||||
'conversationQueue/sendPollVote',
|
||||
async abortSignal => {
|
||||
const groupV2Info = conversation.getGroupV2Info({
|
||||
members: recipientServiceIdsWithoutMe,
|
||||
});
|
||||
strictAssert(
|
||||
groupV2Info,
|
||||
'could not get group info from conversation'
|
||||
let promise: Promise<CallbackResultType>;
|
||||
|
||||
if (isDirectConversation(conversation.attributes)) {
|
||||
if (!isConversationAccepted(conversation.attributes)) {
|
||||
jobLog.info(
|
||||
`conversation ${conversation.idForLogging()} is not accepted; refusing to send`
|
||||
);
|
||||
|
||||
if (revision != null) {
|
||||
groupV2Info.revision = revision;
|
||||
}
|
||||
|
||||
const contentMessage = await messaging.getPollVoteContentMessage({
|
||||
groupV2: groupV2Info,
|
||||
timestamp: currentTimestamp,
|
||||
profileKey,
|
||||
expireTimer,
|
||||
expireTimerVersion: conversation.getExpireTimerVersion(),
|
||||
pollVote: {
|
||||
targetAuthorAci: data.targetAuthorAci,
|
||||
targetTimestamp: data.targetTimestamp,
|
||||
optionIndexes: currentOptionIndexes,
|
||||
voteCount: currentVoteCount,
|
||||
},
|
||||
});
|
||||
|
||||
if (abortSignal?.aborted) {
|
||||
throw new Error('sendPollVote was aborted');
|
||||
}
|
||||
|
||||
return sendContentMessageToGroup({
|
||||
contentHint: ContentHint.Resendable,
|
||||
contentMessage,
|
||||
messageId: pollMessageId,
|
||||
recipients: recipientServiceIdsWithoutMe,
|
||||
sendOptions,
|
||||
sendTarget: conversation.toSenderKeyTarget(),
|
||||
sendType: 'pollVote',
|
||||
timestamp: currentTimestamp,
|
||||
urgent: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
);
|
||||
if (isConversationUnregistered(conversation.attributes)) {
|
||||
jobLog.info(
|
||||
`conversation ${conversation.idForLogging()} is unregistered; refusing to send`
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (conversation.isBlocked()) {
|
||||
jobLog.info(
|
||||
`conversation ${conversation.idForLogging()} is blocked; refusing to send`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
jobLog.info('sending direct poll vote message');
|
||||
const contentMessage = await messaging.getPollVoteContentMessage({
|
||||
groupV2: undefined,
|
||||
timestamp: currentTimestamp,
|
||||
profileKey,
|
||||
expireTimer,
|
||||
expireTimerVersion: conversation.getExpireTimerVersion(),
|
||||
pollVote: {
|
||||
targetAuthorAci: data.targetAuthorAci,
|
||||
targetTimestamp: data.targetTimestamp,
|
||||
optionIndexes: currentOptionIndexes,
|
||||
voteCount: currentVoteCount,
|
||||
},
|
||||
});
|
||||
|
||||
addPniSignatureMessageToProto({
|
||||
conversation,
|
||||
proto: contentMessage,
|
||||
reason: `sendPollVote(${currentTimestamp})`,
|
||||
});
|
||||
|
||||
promise = messaging.sendMessageProtoAndWait({
|
||||
timestamp: currentTimestamp,
|
||||
recipients: [recipientServiceIdsWithoutMe[0]],
|
||||
proto: contentMessage,
|
||||
contentHint: ContentHint.Resendable,
|
||||
groupId: undefined,
|
||||
options: sendOptions,
|
||||
urgent: true,
|
||||
});
|
||||
} else {
|
||||
jobLog.info('sending group poll vote message');
|
||||
promise = conversation.queueJob(
|
||||
'conversationQueue/sendPollVote',
|
||||
async abortSignal => {
|
||||
const groupV2Info = conversation.getGroupV2Info({
|
||||
members: recipientServiceIdsWithoutMe,
|
||||
});
|
||||
strictAssert(
|
||||
groupV2Info,
|
||||
'could not get group info from conversation'
|
||||
);
|
||||
|
||||
if (revision != null) {
|
||||
groupV2Info.revision = revision;
|
||||
}
|
||||
|
||||
const contentMessage = await messaging.getPollVoteContentMessage({
|
||||
groupV2: groupV2Info,
|
||||
timestamp: currentTimestamp,
|
||||
profileKey,
|
||||
expireTimer,
|
||||
expireTimerVersion: conversation.getExpireTimerVersion(),
|
||||
pollVote: {
|
||||
targetAuthorAci: data.targetAuthorAci,
|
||||
targetTimestamp: data.targetTimestamp,
|
||||
optionIndexes: currentOptionIndexes,
|
||||
voteCount: currentVoteCount,
|
||||
},
|
||||
});
|
||||
|
||||
if (abortSignal?.aborted) {
|
||||
throw new Error('sendPollVote was aborted');
|
||||
}
|
||||
|
||||
return sendContentMessageToGroup({
|
||||
contentHint: ContentHint.Resendable,
|
||||
contentMessage,
|
||||
messageId: pollMessageId,
|
||||
recipients: recipientServiceIdsWithoutMe,
|
||||
sendOptions,
|
||||
sendTarget: conversation.toSenderKeyTarget(),
|
||||
sendType: 'pollVote',
|
||||
timestamp: currentTimestamp,
|
||||
urgent: true,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const messageSendPromise = send(ephemeral, {
|
||||
promise: handleMessageSend(promise, {
|
||||
|
||||
@@ -477,13 +477,6 @@ export async function handleDataMessage(
|
||||
confirm();
|
||||
return;
|
||||
}
|
||||
if (!isGroup(conversation.attributes)) {
|
||||
log.warn(
|
||||
`${idLog}: Dropping PollCreate in non-group conversation ${conversation.idForLogging()}`
|
||||
);
|
||||
confirm();
|
||||
return;
|
||||
}
|
||||
const result = safeParsePartial(
|
||||
PollCreateSchema,
|
||||
initialMessage.pollCreate
|
||||
|
||||
@@ -15,7 +15,6 @@ import type { PollVoteAttributesType } from '../messageModifiers/Polls.preload.j
|
||||
import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp.std.js';
|
||||
import { getSourceServiceId } from '../messages/sources.preload.js';
|
||||
import { isAciString } from '../util/isAciString.std.js';
|
||||
import { isGroup } from '../util/whatTypeOfConversation.dom.js';
|
||||
import { strictAssert } from '../util/assert.std.js';
|
||||
import { createLogger } from '../logging/log.std.js';
|
||||
|
||||
@@ -38,10 +37,6 @@ export async function enqueuePollVoteForSend({
|
||||
conversation,
|
||||
'enqueuePollVoteForSend: No conversation extracted from target message'
|
||||
);
|
||||
strictAssert(
|
||||
isGroup(conversation.attributes),
|
||||
'enqueuePollVoteForSend: conversation must be a group'
|
||||
);
|
||||
|
||||
const timestamp = Date.now();
|
||||
const targetAuthorAci = getSourceServiceId(message.attributes);
|
||||
|
||||
@@ -243,9 +243,12 @@ export type GroupMessageOptionsType = Readonly<
|
||||
>;
|
||||
|
||||
export type PollVoteBuildOptions = Required<
|
||||
Pick<MessageOptionsType, 'groupV2' | 'timestamp' | 'pollVote'>
|
||||
Pick<MessageOptionsType, 'timestamp' | 'pollVote'>
|
||||
> &
|
||||
Pick<MessageOptionsType, 'profileKey' | 'expireTimer' | 'expireTimerVersion'>;
|
||||
Pick<
|
||||
MessageOptionsType,
|
||||
'groupV2' | 'profileKey' | 'expireTimer' | 'expireTimerVersion'
|
||||
>;
|
||||
|
||||
export type PollTerminateBuildOptions = Required<
|
||||
Pick<MessageOptionsType, 'groupV2' | 'timestamp' | 'pollTerminate'>
|
||||
@@ -689,13 +692,13 @@ class Message {
|
||||
}
|
||||
}
|
||||
|
||||
type AddPniSignatureMessageToProtoOptionsType = Readonly<{
|
||||
export type AddPniSignatureMessageToProtoOptionsType = Readonly<{
|
||||
conversation?: ConversationModel;
|
||||
proto: Proto.Content;
|
||||
reason: string;
|
||||
}>;
|
||||
|
||||
function addPniSignatureMessageToProto({
|
||||
export function addPniSignatureMessageToProto({
|
||||
conversation,
|
||||
proto,
|
||||
reason,
|
||||
@@ -838,10 +841,12 @@ export class MessageSender {
|
||||
const dataMessage = new Proto.DataMessage();
|
||||
dataMessage.timestamp = Long.fromNumber(timestamp);
|
||||
|
||||
const groupContext = new Proto.GroupContextV2();
|
||||
groupContext.masterKey = groupV2.masterKey;
|
||||
groupContext.revision = groupV2.revision;
|
||||
dataMessage.groupV2 = groupContext;
|
||||
if (groupV2) {
|
||||
const groupContext = new Proto.GroupContextV2();
|
||||
groupContext.masterKey = groupV2.masterKey;
|
||||
groupContext.revision = groupV2.revision;
|
||||
dataMessage.groupV2 = groupContext;
|
||||
}
|
||||
|
||||
if (typeof expireTimer !== 'undefined') {
|
||||
dataMessage.expireTimer = expireTimer;
|
||||
|
||||
@@ -14,7 +14,10 @@ import type { SendStateByConversationId } from '../messages/MessageSendState.std
|
||||
import { aciSchema } from './ServiceId.std.js';
|
||||
import { MAX_MESSAGE_BODY_BYTE_LENGTH } from '../util/longAttachment.std.js';
|
||||
|
||||
export const POLL_QUESTION_MAX_LENGTH = 100;
|
||||
// temporarily limit poll questions to an outbound 100 char and an inbound 200 char
|
||||
export const POLL_QUESTION_MAX_LENGTH_RECEIVE = 200;
|
||||
export const POLL_QUESTION_MAX_LENGTH_SEND = 100;
|
||||
export const POLL_OPTION_MAX_LENGTH = 100;
|
||||
export const POLL_OPTIONS_MIN_COUNT = 2;
|
||||
export const POLL_OPTIONS_MAX_COUNT = 10;
|
||||
|
||||
@@ -27,9 +30,12 @@ export const PollCreateSchema = z
|
||||
question: z
|
||||
.string()
|
||||
.min(1)
|
||||
.refine(value => hasAtMostGraphemes(value, POLL_QUESTION_MAX_LENGTH), {
|
||||
message: `question must contain at most ${POLL_QUESTION_MAX_LENGTH} characters`,
|
||||
})
|
||||
.refine(
|
||||
value => hasAtMostGraphemes(value, POLL_QUESTION_MAX_LENGTH_RECEIVE),
|
||||
{
|
||||
message: `question must contain at most ${POLL_QUESTION_MAX_LENGTH_RECEIVE} characters`,
|
||||
}
|
||||
)
|
||||
.refine(
|
||||
value => Buffer.byteLength(value) <= MAX_MESSAGE_BODY_BYTE_LENGTH,
|
||||
{
|
||||
@@ -41,12 +47,9 @@ export const PollCreateSchema = z
|
||||
z
|
||||
.string()
|
||||
.min(1)
|
||||
.refine(
|
||||
value => hasAtMostGraphemes(value, POLL_QUESTION_MAX_LENGTH),
|
||||
{
|
||||
message: `option must contain at most ${POLL_QUESTION_MAX_LENGTH} characters`,
|
||||
}
|
||||
)
|
||||
.refine(value => hasAtMostGraphemes(value, POLL_OPTION_MAX_LENGTH), {
|
||||
message: `option must contain at most ${POLL_OPTION_MAX_LENGTH} characters`,
|
||||
})
|
||||
.refine(
|
||||
value => Buffer.byteLength(value) <= MAX_MESSAGE_BODY_BYTE_LENGTH,
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user