mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2025-12-24 12:19:41 +00:00
173 lines
4.8 KiB
TypeScript
173 lines
4.8 KiB
TypeScript
// Copyright 2025 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
import { z } from 'zod';
|
|
import { hasAtMostGraphemes } from '../util/grapheme.std.js';
|
|
import {
|
|
Environment,
|
|
getEnvironment,
|
|
isMockEnvironment,
|
|
} from '../environment.std.js';
|
|
import * as RemoteConfig from '../RemoteConfig.dom.js';
|
|
import { isAlpha, isBeta, isProduction } from '../util/version.std.js';
|
|
import type { SendStateByConversationId } from '../messages/MessageSendState.std.js';
|
|
import { aciSchema } from './ServiceId.std.js';
|
|
import { MAX_MESSAGE_BODY_BYTE_LENGTH } from '../util/longAttachment.std.js';
|
|
|
|
export const POLL_QUESTION_MAX_LENGTH = 100;
|
|
export const POLL_OPTIONS_MIN_COUNT = 2;
|
|
export const POLL_OPTIONS_MAX_COUNT = 10;
|
|
|
|
// PollCreate schema (processed shape)
|
|
// - question: required, 1..100 chars
|
|
// - options: required, 2..10 items; each 1..100 chars
|
|
// - allowMultiple: optional boolean
|
|
export const PollCreateSchema = z
|
|
.object({
|
|
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 => Buffer.byteLength(value) <= MAX_MESSAGE_BODY_BYTE_LENGTH,
|
|
{
|
|
message: `question must contain at most ${MAX_MESSAGE_BODY_BYTE_LENGTH} bytes`,
|
|
}
|
|
),
|
|
options: z
|
|
.array(
|
|
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 => Buffer.byteLength(value) <= MAX_MESSAGE_BODY_BYTE_LENGTH,
|
|
{
|
|
message: `option must contain at most ${MAX_MESSAGE_BODY_BYTE_LENGTH} bytes`,
|
|
}
|
|
)
|
|
)
|
|
.min(POLL_OPTIONS_MIN_COUNT)
|
|
.max(POLL_OPTIONS_MAX_COUNT)
|
|
.readonly(),
|
|
allowMultiple: z.boolean().optional(),
|
|
})
|
|
.describe('PollCreate');
|
|
|
|
// PollVote schema (processed shape)
|
|
// - targetAuthorAci: required, non-empty ACI string
|
|
// - targetTimestamp: required, positive int
|
|
// - optionIndexes: required, 0..10 ints in [0, 9] (empty array = clearing vote)
|
|
// - voteCount: optional, int in [0, 1_000_000]
|
|
export const PollVoteSchema = z
|
|
.object({
|
|
targetAuthorAci: aciSchema,
|
|
targetTimestamp: z.number().int().positive(),
|
|
optionIndexes: z.array(z.number().int().min(0).max(9)).min(0).max(10),
|
|
voteCount: z.number().int().min(0),
|
|
})
|
|
.describe('PollVote');
|
|
export type OutgoingPollVote = Readonly<z.infer<typeof PollVoteSchema>>;
|
|
|
|
// PollTerminate schema (processed shape)
|
|
// - targetTimestamp: required, positive int
|
|
export const PollTerminateSchema = z
|
|
.object({
|
|
targetTimestamp: z.number().int().positive(),
|
|
})
|
|
.describe('PollTerminate');
|
|
|
|
export enum PollTerminateSendStatus {
|
|
NotInitiated = 'NotInitiated',
|
|
Pending = 'Pending',
|
|
Complete = 'Complete',
|
|
Failed = 'Failed',
|
|
}
|
|
|
|
export type MessagePollVoteType = {
|
|
fromConversationId: string;
|
|
optionIndexes: ReadonlyArray<number>;
|
|
voteCount: number;
|
|
timestamp: number;
|
|
sendStateByConversationId?: SendStateByConversationId;
|
|
};
|
|
|
|
export type PollMessageAttribute = {
|
|
question: string;
|
|
options: ReadonlyArray<string>;
|
|
allowMultiple: boolean;
|
|
votes?: ReadonlyArray<MessagePollVoteType>;
|
|
terminatedAt?: number;
|
|
terminateSendStatus?: PollTerminateSendStatus;
|
|
};
|
|
|
|
export type PollCreateType = Pick<
|
|
PollMessageAttribute,
|
|
'question' | 'options' | 'allowMultiple'
|
|
>;
|
|
|
|
export function isPollReceiveEnabled(): boolean {
|
|
const env = getEnvironment();
|
|
|
|
if (
|
|
env === Environment.Development ||
|
|
env === Environment.Test ||
|
|
env === Environment.Staging ||
|
|
isMockEnvironment()
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
const version = window.getVersion?.();
|
|
|
|
if (version != null) {
|
|
if (isProduction(version)) {
|
|
return RemoteConfig.isEnabled('desktop.pollReceive.prod1');
|
|
}
|
|
if (isBeta(version)) {
|
|
return RemoteConfig.isEnabled('desktop.pollReceive.beta1');
|
|
}
|
|
if (isAlpha(version)) {
|
|
return RemoteConfig.isEnabled('desktop.pollReceive.alpha');
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
export function isPollSendEnabled(): boolean {
|
|
const env = getEnvironment();
|
|
|
|
if (
|
|
env === Environment.Development ||
|
|
env === Environment.Test ||
|
|
env === Environment.Staging ||
|
|
isMockEnvironment()
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
const version = window.getVersion?.();
|
|
|
|
if (version != null) {
|
|
if (isProduction(version)) {
|
|
return RemoteConfig.isEnabled('desktop.pollSend.prod');
|
|
}
|
|
if (isBeta(version)) {
|
|
return RemoteConfig.isEnabled('desktop.pollSend.beta');
|
|
}
|
|
if (isAlpha(version)) {
|
|
return RemoteConfig.isEnabled('desktop.pollSend.alpha');
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|