Prevent poll voting in ended groups

This commit is contained in:
ayumi-signal
2026-03-27 15:13:09 -07:00
committed by GitHub
parent caa10d02c3
commit b7ca8f278a
12 changed files with 69 additions and 35 deletions
@@ -55,6 +55,7 @@ const { noop, orderBy } = lodash;
const MESSAGE_DEFAULT_PROPS = {
canDeleteForEveryone: false,
canRetryDeleteForEveryone: false,
canSendPollVote: false,
retryDeleteForEveryone: shouldNeverBeCalled,
checkForAccount: shouldNeverBeCalled,
clearTargetedMessage: shouldNeverBeCalled,
+4 -1
View File
@@ -270,6 +270,7 @@ export type PropsData = {
isSpoilerExpanded?: Record<number, boolean>;
isVoiceMessagePlayed: boolean;
canEndPoll?: boolean;
canSendPollVote: boolean;
direction: DirectionType;
timestamp: number;
receivedAtMS?: number;
@@ -2082,7 +2083,8 @@ export class Message extends React.PureComponent<Props, State> {
}
public renderPoll(): React.JSX.Element | null {
const { poll, direction, i18n, id, endPoll, canEndPoll } = this.props;
const { poll, direction, i18n, id, endPoll, canEndPoll, canSendPollVote } =
this.props;
if (!poll) {
return null;
}
@@ -2095,6 +2097,7 @@ export class Message extends React.PureComponent<Props, State> {
sendPollVote={this.props.sendPollVote}
endPoll={endPoll}
canEndPoll={canEndPoll}
canSendPollVote={canSendPollVote}
/>
);
}
@@ -23,6 +23,7 @@ const defaultMessage: MessageDataPropsType = {
}),
canDeleteForEveryone: true,
canRetryDeleteForEveryone: false,
canSendPollVote: true,
conversationColor: 'crimson',
conversationId: 'my-convo',
conversationTitle: 'Conversation Title',
@@ -82,6 +82,7 @@ const defaultMessageProps: TimelineMessagesProps = {
canReply: true,
canRetry: true,
canRetryDeleteForEveryone: true,
canSendPollVote: true,
canDeleteForEveryone: true,
canDownload: true,
checkForAccount: action('checkForAccount'),
@@ -48,6 +48,7 @@ function mockMessageTimelineItem(
canReact: true,
canReply: true,
canRetry: true,
canSendPollVote: true,
conversationId: 'conversation-id',
conversationTitle: 'Conversation Title',
conversationType: 'group',
@@ -242,6 +242,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
canPinMessage: overrideProps.canPinMessage ?? true,
canReact: true,
canReply: true,
canSendPollVote: true,
canDownload: true,
canDeleteForEveryone: overrideProps.canDeleteForEveryone || false,
canForward: true,
@@ -250,6 +250,7 @@ export function GroupMemberLabelEditor({
isPinned={false}
canDeleteForEveryone={false}
canRetryDeleteForEveryone={false}
canSendPollVote={false}
retryDeleteForEveryone={noop}
isBlocked={false}
isMessageRequestAccepted={false}
@@ -49,7 +49,7 @@ type PollCheckboxProps = {
};
const PollCheckbox = memo((props: PollCheckboxProps) => {
const { isIncoming, isPending, checked } = props;
const { checked, isIncoming, isPending, onCheckedChange } = props;
let bgColor: TailwindStyles;
let borderColor: TailwindStyles;
@@ -107,8 +107,8 @@ const PollCheckbox = memo((props: PollCheckboxProps) => {
)}
</AnimatePresence>
<Checkbox.Root
checked={props.checked}
onCheckedChange={props.onCheckedChange}
checked={checked}
onCheckedChange={onCheckedChange}
className={tw(
'flex size-6 items-center justify-center rounded-full',
isPending ? '' : 'border-[1.5px]',
@@ -146,12 +146,13 @@ export type PollMessageContentsProps = {
direction: DirectionType;
i18n: LocalizerType;
messageId: string;
canEndPoll?: boolean;
canSendPollVote: boolean;
sendPollVote: (params: {
messageId: string;
optionIndexes: ReadonlyArray<number>;
}) => void;
endPoll: (messageId: string) => void;
canEndPoll?: boolean;
};
const DELAY_BEFORE_SHOWING_PENDING_ANIMATION = 500;
@@ -160,9 +161,10 @@ export function PollMessageContents({
direction,
i18n,
messageId,
canEndPoll,
canSendPollVote,
sendPollVote,
endPoll,
canEndPoll,
}: PollMessageContentsProps): React.JSX.Element {
const [showVotesModal, setShowVotesModal] = useState(false);
const [isPending, setIsPending] = useState(false);
@@ -281,7 +283,7 @@ export function PollMessageContents({
return (
// oxlint-disable-next-line react/no-array-index-key
<div key={`option-${index}`} className={tw('flex gap-3')}>
{poll.terminatedAt == null && (
{canSendPollVote && poll.terminatedAt == null && (
// 3px offset: type-body-large has 14px font-size and 20px line-height,
// creating 3px space above text. This aligns checkbox with text baseline.
<div className={tw('mt-[3px] self-start')}>
+9 -1
View File
@@ -32,7 +32,10 @@ import { strictAssert } from '../../util/assert.std.js';
import { DataWriter } from '../../sql/Client.preload.js';
import { cleanupMessages } from '../../util/cleanup.preload.js';
import { addPniSignatureMessageToProto } from '../../textsecure/SendMessage.preload.js';
import { shouldSendToDirectConversation } from './shouldSendToConversation.preload.js';
import {
shouldSendToConversation,
shouldSendToDirectConversation,
} from './shouldSendToConversation.preload.js';
import { handleMessageSend } from '../../util/handleMessageSend.preload.js';
const { isNumber } = lodash;
@@ -187,6 +190,11 @@ export async function sendPollTerminate(
`${logId}: expected GroupV2 conversation when not direct`
);
const shouldSend = shouldSendToConversation(conversation, jobLog);
if (!shouldSend) {
return;
}
await conversation.queueJob(
'conversationQueue/sendPollTerminate',
async abortSignal => {
+34 -27
View File
@@ -30,7 +30,10 @@ import { getSendRecipientLists } from './getSendRecipientLists.dom.js';
import { isDirectConversation } from '../../util/whatTypeOfConversation.dom.js';
import type { CallbackResultType } from '../../textsecure/Types.d.ts';
import { addPniSignatureMessageToProto } from '../../textsecure/SendMessage.preload.js';
import { shouldSendToDirectConversation } from './shouldSendToConversation.preload.js';
import {
shouldSendToConversation,
shouldSendToDirectConversation,
} from './shouldSendToConversation.preload.js';
export async function sendPollVote(
conversation: ConversationModel,
@@ -98,19 +101,7 @@ export async function sendPollVote(
if (!shouldContinue) {
jobLog.info('sendPollVote: ran out of time; giving up');
const pollField = pollMessage.get('poll');
if (pollField?.votes) {
const updatedVotes = pollVoteUtil.markOutgoingPollVoteFailed(
pollField.votes,
currentPendingVote
);
pollMessage.set({
poll: {
...pollField,
votes: updatedVotes,
},
});
}
setMessagePollVoteFailed(pollMessage, currentPendingVote);
await window.MessageCache.saveMessage(pollMessage.attributes);
return;
}
@@ -251,6 +242,13 @@ export async function sendPollVote(
urgent: true,
});
} else {
const shouldSend = shouldSendToConversation(conversation, jobLog);
if (!shouldSend) {
setMessagePollVoteFailed(pollMessage, currentPendingVote);
await window.MessageCache.saveMessage(pollMessage.attributes);
return;
}
jobLog.info('sending group poll vote message');
promise = conversation.queueJob(
'conversationQueue/sendPollVote',
@@ -363,19 +361,7 @@ export async function sendPollVote(
log: jobLog,
markFailed: () => {
jobLog.info('poll vote send failed');
const updatedPoll = pollMessage.get('poll');
if (updatedPoll?.votes && pendingVote) {
const updatedVotes = pollVoteUtil.markOutgoingPollVoteFailed(
updatedPoll.votes,
pendingVote
);
pollMessage.set({
poll: {
...updatedPoll,
votes: updatedVotes,
},
});
}
setMessagePollVoteFailed(pollMessage, pendingVote);
},
timeRemaining,
toThrow: originalError || thrownError,
@@ -384,3 +370,24 @@ export async function sendPollVote(
await window.MessageCache.saveMessage(pollMessage.attributes);
}
}
function setMessagePollVoteFailed(
message: MessageModel,
pendingVote: MessagePollVoteType | undefined
): void {
const poll = message.get('poll');
if (!poll?.votes || pendingVote == null) {
return;
}
const updatedVotes = pollVoteUtil.markOutgoingPollVoteFailed(
poll.votes,
pendingVote
);
message.set({
poll: {
...poll,
votes: updatedVotes,
},
});
}
@@ -42,6 +42,13 @@ export function shouldSendToConversation(
return false;
}
if (conversation.get('terminated')) {
log.info(
`conversation ${conversation.idForLogging()} is terminated; refusing to send`
);
return false;
}
if (isSignalConversation(conversation.attributes)) {
log.info(
`conversation ${conversation.idForLogging()} is Signal conversation; refusing to send`
+1
View File
@@ -973,6 +973,7 @@ export const getPropsForMessage = (
conversation,
ourAci,
}),
canSendPollVote: !isGroupTerminated,
contact: getPropsForEmbeddedContact(message, regionCode, accountSelector),
contactLabel,
contactNameColor,