diff --git a/ts/components/StoryViewsNRepliesModal.dom.tsx b/ts/components/StoryViewsNRepliesModal.dom.tsx index e9f77744db..be59e2dd20 100644 --- a/ts/components/StoryViewsNRepliesModal.dom.tsx +++ b/ts/components/StoryViewsNRepliesModal.dom.tsx @@ -55,6 +55,7 @@ const { noop, orderBy } = lodash; const MESSAGE_DEFAULT_PROPS = { canDeleteForEveryone: false, canRetryDeleteForEveryone: false, + canSendPollVote: false, retryDeleteForEveryone: shouldNeverBeCalled, checkForAccount: shouldNeverBeCalled, clearTargetedMessage: shouldNeverBeCalled, diff --git a/ts/components/conversation/Message.dom.tsx b/ts/components/conversation/Message.dom.tsx index 81f0228727..a88fa8d5a4 100644 --- a/ts/components/conversation/Message.dom.tsx +++ b/ts/components/conversation/Message.dom.tsx @@ -270,6 +270,7 @@ export type PropsData = { isSpoilerExpanded?: Record; isVoiceMessagePlayed: boolean; canEndPoll?: boolean; + canSendPollVote: boolean; direction: DirectionType; timestamp: number; receivedAtMS?: number; @@ -2082,7 +2083,8 @@ export class Message extends React.PureComponent { } 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 { sendPollVote={this.props.sendPollVote} endPoll={endPoll} canEndPoll={canEndPoll} + canSendPollVote={canSendPollVote} /> ); } diff --git a/ts/components/conversation/MessageDetail.dom.stories.tsx b/ts/components/conversation/MessageDetail.dom.stories.tsx index 3cb90be721..5ed0d08479 100644 --- a/ts/components/conversation/MessageDetail.dom.stories.tsx +++ b/ts/components/conversation/MessageDetail.dom.stories.tsx @@ -23,6 +23,7 @@ const defaultMessage: MessageDataPropsType = { }), canDeleteForEveryone: true, canRetryDeleteForEveryone: false, + canSendPollVote: true, conversationColor: 'crimson', conversationId: 'my-convo', conversationTitle: 'Conversation Title', diff --git a/ts/components/conversation/Quote.dom.stories.tsx b/ts/components/conversation/Quote.dom.stories.tsx index 9af11b978f..811c988c8e 100644 --- a/ts/components/conversation/Quote.dom.stories.tsx +++ b/ts/components/conversation/Quote.dom.stories.tsx @@ -82,6 +82,7 @@ const defaultMessageProps: TimelineMessagesProps = { canReply: true, canRetry: true, canRetryDeleteForEveryone: true, + canSendPollVote: true, canDeleteForEveryone: true, canDownload: true, checkForAccount: action('checkForAccount'), diff --git a/ts/components/conversation/Timeline.dom.stories.tsx b/ts/components/conversation/Timeline.dom.stories.tsx index 787bda2dbe..c206e157d2 100644 --- a/ts/components/conversation/Timeline.dom.stories.tsx +++ b/ts/components/conversation/Timeline.dom.stories.tsx @@ -48,6 +48,7 @@ function mockMessageTimelineItem( canReact: true, canReply: true, canRetry: true, + canSendPollVote: true, conversationId: 'conversation-id', conversationTitle: 'Conversation Title', conversationType: 'group', diff --git a/ts/components/conversation/TimelineMessage.dom.stories.tsx b/ts/components/conversation/TimelineMessage.dom.stories.tsx index 133290961f..e2b238139f 100644 --- a/ts/components/conversation/TimelineMessage.dom.stories.tsx +++ b/ts/components/conversation/TimelineMessage.dom.stories.tsx @@ -242,6 +242,7 @@ const createProps = (overrideProps: Partial = {}): Props => ({ canPinMessage: overrideProps.canPinMessage ?? true, canReact: true, canReply: true, + canSendPollVote: true, canDownload: true, canDeleteForEveryone: overrideProps.canDeleteForEveryone || false, canForward: true, diff --git a/ts/components/conversation/conversation-details/GroupMemberLabelEditor.dom.tsx b/ts/components/conversation/conversation-details/GroupMemberLabelEditor.dom.tsx index 9fc9047e51..afa1951d4b 100644 --- a/ts/components/conversation/conversation-details/GroupMemberLabelEditor.dom.tsx +++ b/ts/components/conversation/conversation-details/GroupMemberLabelEditor.dom.tsx @@ -250,6 +250,7 @@ export function GroupMemberLabelEditor({ isPinned={false} canDeleteForEveryone={false} canRetryDeleteForEveryone={false} + canSendPollVote={false} retryDeleteForEveryone={noop} isBlocked={false} isMessageRequestAccepted={false} diff --git a/ts/components/conversation/poll-message/PollMessageContents.dom.tsx b/ts/components/conversation/poll-message/PollMessageContents.dom.tsx index 7a85d1fdee..57f7b9932f 100644 --- a/ts/components/conversation/poll-message/PollMessageContents.dom.tsx +++ b/ts/components/conversation/poll-message/PollMessageContents.dom.tsx @@ -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) => { )} ; }) => 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
- {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.
diff --git a/ts/jobs/helpers/sendPollTerminate.preload.ts b/ts/jobs/helpers/sendPollTerminate.preload.ts index 205e334aa4..42a3be8933 100644 --- a/ts/jobs/helpers/sendPollTerminate.preload.ts +++ b/ts/jobs/helpers/sendPollTerminate.preload.ts @@ -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 => { diff --git a/ts/jobs/helpers/sendPollVote.preload.ts b/ts/jobs/helpers/sendPollVote.preload.ts index 8db0ad92f5..ce89574c81 100644 --- a/ts/jobs/helpers/sendPollVote.preload.ts +++ b/ts/jobs/helpers/sendPollVote.preload.ts @@ -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, + }, + }); +} diff --git a/ts/jobs/helpers/shouldSendToConversation.preload.ts b/ts/jobs/helpers/shouldSendToConversation.preload.ts index cffef3b091..25f8bb031a 100644 --- a/ts/jobs/helpers/shouldSendToConversation.preload.ts +++ b/ts/jobs/helpers/shouldSendToConversation.preload.ts @@ -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` diff --git a/ts/state/selectors/message.preload.ts b/ts/state/selectors/message.preload.ts index 936eb7a8ed..d8ec387985 100644 --- a/ts/state/selectors/message.preload.ts +++ b/ts/state/selectors/message.preload.ts @@ -973,6 +973,7 @@ export const getPropsForMessage = ( conversation, ourAci, }), + canSendPollVote: !isGroupTerminated, contact: getPropsForEmbeddedContact(message, regionCode, accountSelector), contactLabel, contactNameColor,