mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2025-12-05 01:10:49 +00:00
Add ability for poll author to terminate a poll
This commit is contained in:
@@ -1462,6 +1462,10 @@
|
|||||||
"messageformat": "Info",
|
"messageformat": "Info",
|
||||||
"description": "Shown on the drop-down menu for an individual message, takes you to message detail screen"
|
"description": "Shown on the drop-down menu for an individual message, takes you to message detail screen"
|
||||||
},
|
},
|
||||||
|
"icu:Poll__end-poll": {
|
||||||
|
"messageformat": "End poll",
|
||||||
|
"description": "Label for button/menu item to end a poll. Shown in the poll votes modal and in the message context menu"
|
||||||
|
},
|
||||||
"icu:PollMessage--SelectOne": {
|
"icu:PollMessage--SelectOne": {
|
||||||
"messageformat": "Poll · Select one",
|
"messageformat": "Poll · Select one",
|
||||||
"description": "Status text for single-choice poll where user can select one option"
|
"description": "Status text for single-choice poll where user can select one option"
|
||||||
@@ -1482,6 +1486,18 @@
|
|||||||
"messageformat": "You voted",
|
"messageformat": "You voted",
|
||||||
"description": "Accessibility label for checkmark indicating user voted for this poll option"
|
"description": "Accessibility label for checkmark indicating user voted for this poll option"
|
||||||
},
|
},
|
||||||
|
"icu:PollTerminate--you": {
|
||||||
|
"messageformat": "You ended the poll: \"{poll}\"",
|
||||||
|
"description": "Chat event shown when you end a poll"
|
||||||
|
},
|
||||||
|
"icu:PollTerminate--other": {
|
||||||
|
"messageformat": "{name} ended the poll: \"{poll}\"",
|
||||||
|
"description": "Chat event shown when someone else ends a poll"
|
||||||
|
},
|
||||||
|
"icu:PollTerminate__view-poll": {
|
||||||
|
"messageformat": "View poll",
|
||||||
|
"description": "Button in poll terminate chat event to scroll to and select the original poll message"
|
||||||
|
},
|
||||||
"icu:PollVotesModal__title": {
|
"icu:PollVotesModal__title": {
|
||||||
"messageformat": "Poll details",
|
"messageformat": "Poll details",
|
||||||
"description": "Modal title for viewing poll votes and who voted on which option"
|
"description": "Modal title for viewing poll votes and who voted on which option"
|
||||||
@@ -1498,6 +1514,10 @@
|
|||||||
"messageformat": "No votes",
|
"messageformat": "No votes",
|
||||||
"description": "Message shown when poll has no votes"
|
"description": "Message shown when poll has no votes"
|
||||||
},
|
},
|
||||||
|
"icu:Toast--PollNotFound": {
|
||||||
|
"messageformat": "Poll not found",
|
||||||
|
"description": "Toast shown when user tries to view a poll that no longer exists"
|
||||||
|
},
|
||||||
"icu:PollCreateModal__title": {
|
"icu:PollCreateModal__title": {
|
||||||
"messageformat": "New poll",
|
"messageformat": "New poll",
|
||||||
"description": "Title for the modal to create a new poll"
|
"description": "Title for the modal to create a new poll"
|
||||||
|
|||||||
1
images/icons/v3/stop/stop-circle.svg
Normal file
1
images/icons/v3/stop/stop-circle.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#stop-circle__a)" fill="#000"><path d="M7.083 8.125c0-.575.467-1.042 1.042-1.042h3.75c.575 0 1.042.467 1.042 1.042v3.75c0 .575-.467 1.042-1.042 1.042h-3.75a1.042 1.042 0 0 1-1.042-1.042v-3.75Z"/><path fill-rule="evenodd" clip-rule="evenodd" d="M10 .938a9.063 9.063 0 1 0 0 18.125A9.063 9.063 0 0 0 10 .938ZM2.396 10a7.604 7.604 0 1 1 15.208 0 7.604 7.604 0 0 1-15.208 0Z"/></g><defs><clipPath id="stop-circle__a"><path fill="#fff" d="M0 0h20v20H0z"/></clipPath></defs></svg>
|
||||||
|
After Width: | Height: | Size: 567 B |
@@ -6785,6 +6785,13 @@ button.module-calling-participants-list__contact {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__end-poll::before {
|
||||||
|
@include mixins.color-svg(
|
||||||
|
'../images/icons/v3/stop/stop-circle.svg',
|
||||||
|
light-dark(variables.$color-black, variables.$color-gray-15)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
&__more-info::before {
|
&__more-info::before {
|
||||||
@include mixins.light-theme {
|
@include mixins.light-theme {
|
||||||
@include mixins.color-svg(
|
@include mixins.color-svg(
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&::before {
|
&--has-icon::before {
|
||||||
content: '';
|
content: '';
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ const MESSAGE_DEFAULT_PROPS = {
|
|||||||
previews: [],
|
previews: [],
|
||||||
retryMessageSend: shouldNeverBeCalled,
|
retryMessageSend: shouldNeverBeCalled,
|
||||||
sendPollVote: shouldNeverBeCalled,
|
sendPollVote: shouldNeverBeCalled,
|
||||||
|
endPoll: shouldNeverBeCalled,
|
||||||
pushPanelForConversation: shouldNeverBeCalled,
|
pushPanelForConversation: shouldNeverBeCalled,
|
||||||
renderAudioAttachment: () => <div />,
|
renderAudioAttachment: () => <div />,
|
||||||
renderingContext: 'EditHistoryMessagesModal',
|
renderingContext: 'EditHistoryMessagesModal',
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ const MESSAGE_DEFAULT_PROPS = {
|
|||||||
previews: [],
|
previews: [],
|
||||||
retryMessageSend: shouldNeverBeCalled,
|
retryMessageSend: shouldNeverBeCalled,
|
||||||
sendPollVote: shouldNeverBeCalled,
|
sendPollVote: shouldNeverBeCalled,
|
||||||
|
endPoll: shouldNeverBeCalled,
|
||||||
pushPanelForConversation: shouldNeverBeCalled,
|
pushPanelForConversation: shouldNeverBeCalled,
|
||||||
renderAudioAttachment: () => <div />,
|
renderAudioAttachment: () => <div />,
|
||||||
saveAttachment: shouldNeverBeCalled,
|
saveAttachment: shouldNeverBeCalled,
|
||||||
|
|||||||
@@ -189,6 +189,8 @@ function getToast(toastType: ToastType): AnyToast {
|
|||||||
return { toastType: ToastType.OriginalMessageNotFound };
|
return { toastType: ToastType.OriginalMessageNotFound };
|
||||||
case ToastType.PinnedConversationsFull:
|
case ToastType.PinnedConversationsFull:
|
||||||
return { toastType: ToastType.PinnedConversationsFull };
|
return { toastType: ToastType.PinnedConversationsFull };
|
||||||
|
case ToastType.PollNotFound:
|
||||||
|
return { toastType: ToastType.PollNotFound };
|
||||||
case ToastType.ReactionFailed:
|
case ToastType.ReactionFailed:
|
||||||
return { toastType: ToastType.ReactionFailed };
|
return { toastType: ToastType.ReactionFailed };
|
||||||
case ToastType.ReceiptSaved:
|
case ToastType.ReceiptSaved:
|
||||||
|
|||||||
@@ -652,6 +652,10 @@ export function renderToast({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (toastType === ToastType.PollNotFound) {
|
||||||
|
return <Toast onClose={hideToast}>{i18n('icu:Toast--PollNotFound')}</Toast>;
|
||||||
|
}
|
||||||
|
|
||||||
if (toastType === ToastType._InternalMainProcessLoggingError) {
|
if (toastType === ToastType._InternalMainProcessLoggingError) {
|
||||||
return (
|
return (
|
||||||
<Toast
|
<Toast
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ export const CallingNotification: React.FC<PropsType> = React.memo(
|
|||||||
onEdit={undefined}
|
onEdit={undefined}
|
||||||
onReplyToMessage={undefined}
|
onReplyToMessage={undefined}
|
||||||
onReact={undefined}
|
onReact={undefined}
|
||||||
|
onEndPoll={undefined}
|
||||||
onRetryMessageSend={undefined}
|
onRetryMessageSend={undefined}
|
||||||
onRetryDeleteForEveryone={undefined}
|
onRetryDeleteForEveryone={undefined}
|
||||||
onCopy={undefined}
|
onCopy={undefined}
|
||||||
|
|||||||
@@ -249,6 +249,7 @@ export type PropsData = {
|
|||||||
isSelectMode: boolean;
|
isSelectMode: boolean;
|
||||||
isSMS: boolean;
|
isSMS: boolean;
|
||||||
isSpoilerExpanded?: Record<number, boolean>;
|
isSpoilerExpanded?: Record<number, boolean>;
|
||||||
|
canEndPoll?: boolean;
|
||||||
direction: DirectionType;
|
direction: DirectionType;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
receivedAtMS?: number;
|
receivedAtMS?: number;
|
||||||
@@ -363,6 +364,7 @@ export type PropsActions = {
|
|||||||
messageId: string;
|
messageId: string;
|
||||||
optionIndexes: ReadonlyArray<number>;
|
optionIndexes: ReadonlyArray<number>;
|
||||||
}) => void;
|
}) => void;
|
||||||
|
endPoll: (messageId: string) => void;
|
||||||
showContactModal: (contactId: string, conversationId?: string) => void;
|
showContactModal: (contactId: string, conversationId?: string) => void;
|
||||||
showSpoiler: (messageId: string, data: Record<number, boolean>) => void;
|
showSpoiler: (messageId: string, data: Record<number, boolean>) => void;
|
||||||
|
|
||||||
@@ -2023,7 +2025,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public renderPoll(): JSX.Element | null {
|
public renderPoll(): JSX.Element | null {
|
||||||
const { poll, direction, i18n, id } = this.props;
|
const { poll, direction, i18n, id, endPoll, canEndPoll } = this.props;
|
||||||
if (!poll || !isPollReceiveEnabled()) {
|
if (!poll || !isPollReceiveEnabled()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -2034,6 +2036,8 @@ export class Message extends React.PureComponent<Props, State> {
|
|||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
messageId={id}
|
messageId={id}
|
||||||
sendPollVote={this.props.sendPollVote}
|
sendPollVote={this.props.sendPollVote}
|
||||||
|
endPoll={endPoll}
|
||||||
|
canEndPoll={canEndPoll}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ type MessageContextProps = {
|
|||||||
onEdit: (() => void) | undefined;
|
onEdit: (() => void) | undefined;
|
||||||
onReplyToMessage: (() => void) | undefined;
|
onReplyToMessage: (() => void) | undefined;
|
||||||
onReact: (() => void) | undefined;
|
onReact: (() => void) | undefined;
|
||||||
|
onEndPoll: (() => void) | undefined;
|
||||||
onRetryMessageSend: (() => void) | undefined;
|
onRetryMessageSend: (() => void) | undefined;
|
||||||
onRetryDeleteForEveryone: (() => void) | undefined;
|
onRetryDeleteForEveryone: (() => void) | undefined;
|
||||||
onCopy: (() => void) | undefined;
|
onCopy: (() => void) | undefined;
|
||||||
@@ -39,6 +40,7 @@ export const MessageContextMenu = ({
|
|||||||
onEdit,
|
onEdit,
|
||||||
onReplyToMessage,
|
onReplyToMessage,
|
||||||
onReact,
|
onReact,
|
||||||
|
onEndPoll,
|
||||||
onMoreInfo,
|
onMoreInfo,
|
||||||
onCopy,
|
onCopy,
|
||||||
onSelect,
|
onSelect,
|
||||||
@@ -103,6 +105,22 @@ export const MessageContextMenu = ({
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{onEndPoll && (
|
||||||
|
<MenuItem
|
||||||
|
attributes={{
|
||||||
|
className:
|
||||||
|
'module-message__context--icon module-message__context__end-poll',
|
||||||
|
}}
|
||||||
|
onClick={(event: React.MouseEvent) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
onEndPoll();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{i18n('icu:Poll__end-poll')}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
{onForward && (
|
{onForward && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
attributes={{
|
attributes={{
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ export type PropsReduxActions = Pick<
|
|||||||
| 'checkForAccount'
|
| 'checkForAccount'
|
||||||
| 'clearTargetedMessage'
|
| 'clearTargetedMessage'
|
||||||
| 'doubleCheckMissingQuoteReference'
|
| 'doubleCheckMissingQuoteReference'
|
||||||
|
| 'endPoll'
|
||||||
| 'kickOffAttachmentDownload'
|
| 'kickOffAttachmentDownload'
|
||||||
| 'markAttachmentAsCorrupted'
|
| 'markAttachmentAsCorrupted'
|
||||||
| 'messageExpanded'
|
| 'messageExpanded'
|
||||||
@@ -137,6 +138,7 @@ export function MessageDetail({
|
|||||||
clearTargetedMessage,
|
clearTargetedMessage,
|
||||||
contactNameColor,
|
contactNameColor,
|
||||||
doubleCheckMissingQuoteReference,
|
doubleCheckMissingQuoteReference,
|
||||||
|
endPoll,
|
||||||
getPreferredBadge,
|
getPreferredBadge,
|
||||||
i18n,
|
i18n,
|
||||||
interactionMode,
|
interactionMode,
|
||||||
@@ -346,6 +348,7 @@ export function MessageDetail({
|
|||||||
displayLimit={Number.MAX_SAFE_INTEGER}
|
displayLimit={Number.MAX_SAFE_INTEGER}
|
||||||
showLightboxForViewOnceMedia={showLightboxForViewOnceMedia}
|
showLightboxForViewOnceMedia={showLightboxForViewOnceMedia}
|
||||||
doubleCheckMissingQuoteReference={doubleCheckMissingQuoteReference}
|
doubleCheckMissingQuoteReference={doubleCheckMissingQuoteReference}
|
||||||
|
endPoll={endPoll}
|
||||||
getPreferredBadge={getPreferredBadge}
|
getPreferredBadge={getPreferredBadge}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
interactionMode={interactionMode}
|
interactionMode={interactionMode}
|
||||||
|
|||||||
53
ts/components/conversation/PollTerminateNotification.dom.tsx
Normal file
53
ts/components/conversation/PollTerminateNotification.dom.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
// Copyright 2025 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import type { LocalizerType } from '../../types/Util.std.js';
|
||||||
|
import type { ConversationType } from '../../state/ducks/conversations.preload.js';
|
||||||
|
import { SystemMessage } from './SystemMessage.dom.js';
|
||||||
|
import { Button, ButtonVariant, ButtonSize } from '../Button.dom.js';
|
||||||
|
|
||||||
|
export type PropsType = {
|
||||||
|
sender: ConversationType;
|
||||||
|
pollQuestion: string;
|
||||||
|
pollMessageId: string;
|
||||||
|
conversationId: string;
|
||||||
|
i18n: LocalizerType;
|
||||||
|
scrollToPollMessage: (messageId: string, conversationId: string) => unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function PollTerminateNotification({
|
||||||
|
sender,
|
||||||
|
pollQuestion,
|
||||||
|
pollMessageId,
|
||||||
|
conversationId,
|
||||||
|
i18n,
|
||||||
|
scrollToPollMessage,
|
||||||
|
}: PropsType): JSX.Element {
|
||||||
|
const message = sender.isMe
|
||||||
|
? i18n('icu:PollTerminate--you', { poll: pollQuestion })
|
||||||
|
: i18n('icu:PollTerminate--other', {
|
||||||
|
name: sender.title,
|
||||||
|
poll: pollQuestion,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleViewPoll = () => {
|
||||||
|
scrollToPollMessage(pollMessageId, conversationId);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SystemMessage
|
||||||
|
symbol="poll"
|
||||||
|
contents={message}
|
||||||
|
button={
|
||||||
|
<Button
|
||||||
|
onClick={handleViewPoll}
|
||||||
|
variant={ButtonVariant.SystemMessage}
|
||||||
|
size={ButtonSize.Small}
|
||||||
|
>
|
||||||
|
{i18n('icu:PollTerminate__view-poll')}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -75,6 +75,7 @@ const defaultMessageProps: TimelineMessagesProps = {
|
|||||||
}),
|
}),
|
||||||
canCopy: true,
|
canCopy: true,
|
||||||
canEditMessage: true,
|
canEditMessage: true,
|
||||||
|
canEndPoll: false,
|
||||||
canForward: true,
|
canForward: true,
|
||||||
canReact: true,
|
canReact: true,
|
||||||
canReply: true,
|
canReply: true,
|
||||||
@@ -116,6 +117,7 @@ const defaultMessageProps: TimelineMessagesProps = {
|
|||||||
openGiftBadge: action('openGiftBadge'),
|
openGiftBadge: action('openGiftBadge'),
|
||||||
previews: [],
|
previews: [],
|
||||||
reactToMessage: action('default--reactToMessage'),
|
reactToMessage: action('default--reactToMessage'),
|
||||||
|
endPoll: action('default--endPoll'),
|
||||||
readStatus: ReadStatus.Read,
|
readStatus: ReadStatus.Read,
|
||||||
renderReactionPicker: () => <div />,
|
renderReactionPicker: () => <div />,
|
||||||
renderAudioAttachment: () => <div>*AudioAttachment*</div>,
|
renderAudioAttachment: () => <div>*AudioAttachment*</div>,
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import React, { forwardRef } from 'react';
|
import React, { forwardRef } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { AxoSymbol } from '../../axo/AxoSymbol.dom.js';
|
||||||
|
import type { AxoSymbolIconName } from '../../axo/_internal/AxoSymbolDefs.generated.std.js';
|
||||||
|
import { tw } from '../../axo/tw.dom.js';
|
||||||
|
|
||||||
export enum SystemMessageKind {
|
export enum SystemMessageKind {
|
||||||
Normal = 'Normal',
|
Normal = 'Normal',
|
||||||
@@ -11,50 +14,62 @@ export enum SystemMessageKind {
|
|||||||
Error = 'Error',
|
Error = 'Error',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PropsType = {
|
type SystemMessageBaseProps = {
|
||||||
icon:
|
|
||||||
| 'audio-incoming'
|
|
||||||
| 'audio-missed'
|
|
||||||
| 'audio-outgoing'
|
|
||||||
| 'block'
|
|
||||||
| 'group'
|
|
||||||
| 'group-access'
|
|
||||||
| 'group-add'
|
|
||||||
| 'group-approved'
|
|
||||||
| 'group-avatar'
|
|
||||||
| 'group-decline'
|
|
||||||
| 'group-edit'
|
|
||||||
| 'group-leave'
|
|
||||||
| 'group-remove'
|
|
||||||
| 'group-summary'
|
|
||||||
| 'info'
|
|
||||||
| 'phone'
|
|
||||||
| 'profile'
|
|
||||||
| 'safety-number'
|
|
||||||
| 'spam'
|
|
||||||
| 'session-refresh'
|
|
||||||
| 'thread'
|
|
||||||
| 'timer'
|
|
||||||
| 'timer-disabled'
|
|
||||||
| 'unsupported'
|
|
||||||
| 'unsupported--can-process'
|
|
||||||
| 'verified'
|
|
||||||
| 'verified-not'
|
|
||||||
| 'video'
|
|
||||||
| 'video-incoming'
|
|
||||||
| 'video-missed'
|
|
||||||
| 'video-outgoing'
|
|
||||||
| 'warning'
|
|
||||||
| 'payment-event'
|
|
||||||
| 'merge';
|
|
||||||
contents: ReactNode;
|
contents: ReactNode;
|
||||||
button?: ReactNode;
|
button?: ReactNode;
|
||||||
kind?: SystemMessageKind;
|
kind?: SystemMessageKind;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PropsType = SystemMessageBaseProps &
|
||||||
|
(
|
||||||
|
| {
|
||||||
|
/** @deprecated Use symbol instead */
|
||||||
|
icon:
|
||||||
|
| 'audio-incoming'
|
||||||
|
| 'audio-missed'
|
||||||
|
| 'audio-outgoing'
|
||||||
|
| 'block'
|
||||||
|
| 'group'
|
||||||
|
| 'group-access'
|
||||||
|
| 'group-add'
|
||||||
|
| 'group-approved'
|
||||||
|
| 'group-avatar'
|
||||||
|
| 'group-decline'
|
||||||
|
| 'group-edit'
|
||||||
|
| 'group-leave'
|
||||||
|
| 'group-remove'
|
||||||
|
| 'group-summary'
|
||||||
|
| 'info'
|
||||||
|
| 'phone'
|
||||||
|
| 'profile'
|
||||||
|
| 'safety-number'
|
||||||
|
| 'spam'
|
||||||
|
| 'session-refresh'
|
||||||
|
| 'thread'
|
||||||
|
| 'timer'
|
||||||
|
| 'timer-disabled'
|
||||||
|
| 'unsupported'
|
||||||
|
| 'unsupported--can-process'
|
||||||
|
| 'verified'
|
||||||
|
| 'verified-not'
|
||||||
|
| 'video'
|
||||||
|
| 'video-incoming'
|
||||||
|
| 'video-missed'
|
||||||
|
| 'video-outgoing'
|
||||||
|
| 'warning'
|
||||||
|
| 'payment-event'
|
||||||
|
| 'merge';
|
||||||
|
symbol?: never;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
icon?: never;
|
||||||
|
symbol: AxoSymbolIconName;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const SystemMessage = forwardRef<HTMLDivElement, PropsType>(
|
export const SystemMessage = forwardRef<HTMLDivElement, PropsType>(
|
||||||
function SystemMessageInner(
|
function SystemMessageInner(
|
||||||
{ icon, contents, button, kind = SystemMessageKind.Normal },
|
{ icon, symbol, contents, button, kind = SystemMessageKind.Normal },
|
||||||
ref
|
ref
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
@@ -69,9 +84,15 @@ export const SystemMessage = forwardRef<HTMLDivElement, PropsType>(
|
|||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'SystemMessage__contents',
|
'SystemMessage__contents',
|
||||||
`SystemMessage__contents--icon-${icon}`
|
icon && 'SystemMessage__contents--has-icon',
|
||||||
|
icon && `SystemMessage__contents--icon-${icon}`
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{symbol && (
|
||||||
|
<span className={tw('me-2 inline-block')}>
|
||||||
|
<AxoSymbol.Icon size={16} symbol={symbol} label={null} />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{contents}
|
{contents}
|
||||||
</div>
|
</div>
|
||||||
{button && (
|
{button && (
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ function mockMessageTimelineItem(
|
|||||||
canDeleteForEveryone: false,
|
canDeleteForEveryone: false,
|
||||||
canDownload: true,
|
canDownload: true,
|
||||||
canEditMessage: true,
|
canEditMessage: true,
|
||||||
|
canEndPoll: false,
|
||||||
canForward: true,
|
canForward: true,
|
||||||
canReact: true,
|
canReact: true,
|
||||||
canReply: true,
|
canReply: true,
|
||||||
@@ -286,6 +287,7 @@ const actions = () => ({
|
|||||||
clearTargetedMessage: action('clearTargetedMessage'),
|
clearTargetedMessage: action('clearTargetedMessage'),
|
||||||
updateSharedGroups: action('updateSharedGroups'),
|
updateSharedGroups: action('updateSharedGroups'),
|
||||||
|
|
||||||
|
endPoll: action('endPoll'),
|
||||||
reactToMessage: action('reactToMessage'),
|
reactToMessage: action('reactToMessage'),
|
||||||
setMessageToEdit: action('setMessageToEdit'),
|
setMessageToEdit: action('setMessageToEdit'),
|
||||||
setQuoteByMessageId: action('setQuoteByMessageId'),
|
setQuoteByMessageId: action('setQuoteByMessageId'),
|
||||||
@@ -309,6 +311,7 @@ const actions = () => ({
|
|||||||
doubleCheckMissingQuoteReference: action('doubleCheckMissingQuoteReference'),
|
doubleCheckMissingQuoteReference: action('doubleCheckMissingQuoteReference'),
|
||||||
|
|
||||||
openGiftBadge: action('openGiftBadge'),
|
openGiftBadge: action('openGiftBadge'),
|
||||||
|
scrollToPollMessage: action('scrollToPollMessage'),
|
||||||
scrollToQuotedMessage: action('scrollToQuotedMessage'),
|
scrollToQuotedMessage: action('scrollToQuotedMessage'),
|
||||||
showAttachmentDownloadStillInProgressToast: action(
|
showAttachmentDownloadStillInProgressToast: action(
|
||||||
'showAttachmentDownloadStillInProgressToast'
|
'showAttachmentDownloadStillInProgressToast'
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ const getDefaultProps = () => ({
|
|||||||
platform: 'darwin',
|
platform: 'darwin',
|
||||||
targetMessage: action('targetMessage'),
|
targetMessage: action('targetMessage'),
|
||||||
toggleSelectMessage: action('toggleSelectMessage'),
|
toggleSelectMessage: action('toggleSelectMessage'),
|
||||||
|
endPoll: action('endPoll'),
|
||||||
reactToMessage: action('reactToMessage'),
|
reactToMessage: action('reactToMessage'),
|
||||||
checkForAccount: action('checkForAccount'),
|
checkForAccount: action('checkForAccount'),
|
||||||
clearTargetedMessage: action('clearTargetedMessage'),
|
clearTargetedMessage: action('clearTargetedMessage'),
|
||||||
@@ -91,6 +92,7 @@ const getDefaultProps = () => ({
|
|||||||
),
|
),
|
||||||
showMediaNoLongerAvailableToast: action('showMediaNoLongerAvailableToast'),
|
showMediaNoLongerAvailableToast: action('showMediaNoLongerAvailableToast'),
|
||||||
showTapToViewNotAvailableModal: action('showTapToViewNotAvailableModal'),
|
showTapToViewNotAvailableModal: action('showTapToViewNotAvailableModal'),
|
||||||
|
scrollToPollMessage: action('scrollToPollMessage'),
|
||||||
scrollToQuotedMessage: action('scrollToQuotedMessage'),
|
scrollToQuotedMessage: action('scrollToQuotedMessage'),
|
||||||
showSpoiler: action('showSpoiler'),
|
showSpoiler: action('showSpoiler'),
|
||||||
startConversation: action('startConversation'),
|
startConversation: action('startConversation'),
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ import type { PropsType as ProfileChangeNotificationPropsType } from './ProfileC
|
|||||||
import { ProfileChangeNotification } from './ProfileChangeNotification.dom.js';
|
import { ProfileChangeNotification } from './ProfileChangeNotification.dom.js';
|
||||||
import type { PropsType as PaymentEventNotificationPropsType } from './PaymentEventNotification.dom.js';
|
import type { PropsType as PaymentEventNotificationPropsType } from './PaymentEventNotification.dom.js';
|
||||||
import { PaymentEventNotification } from './PaymentEventNotification.dom.js';
|
import { PaymentEventNotification } from './PaymentEventNotification.dom.js';
|
||||||
|
import type { PropsType as PollTerminateNotificationPropsType } from './PollTerminateNotification.dom.js';
|
||||||
|
import { PollTerminateNotification } from './PollTerminateNotification.dom.js';
|
||||||
import type { PropsDataType as ConversationMergeNotificationPropsType } from './ConversationMergeNotification.dom.js';
|
import type { PropsDataType as ConversationMergeNotificationPropsType } from './ConversationMergeNotification.dom.js';
|
||||||
import { ConversationMergeNotification } from './ConversationMergeNotification.dom.js';
|
import { ConversationMergeNotification } from './ConversationMergeNotification.dom.js';
|
||||||
import type { PropsDataType as PhoneNumberDiscoveryNotificationPropsType } from './PhoneNumberDiscoveryNotification.dom.js';
|
import type { PropsDataType as PhoneNumberDiscoveryNotificationPropsType } from './PhoneNumberDiscoveryNotification.dom.js';
|
||||||
@@ -152,6 +154,13 @@ type MessageRequestResponseNotificationType = {
|
|||||||
type: 'messageRequestResponse';
|
type: 'messageRequestResponse';
|
||||||
data: MessageRequestResponseNotificationData;
|
data: MessageRequestResponseNotificationData;
|
||||||
};
|
};
|
||||||
|
type PollTerminateNotificationType = {
|
||||||
|
type: 'pollTerminate';
|
||||||
|
data: Omit<
|
||||||
|
PollTerminateNotificationPropsType,
|
||||||
|
'i18n' | 'scrollToPollMessage'
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
export type TimelineItemType = (
|
export type TimelineItemType = (
|
||||||
| CallHistoryType
|
| CallHistoryType
|
||||||
@@ -165,6 +174,7 @@ export type TimelineItemType = (
|
|||||||
| JoinedSignalNotificationType
|
| JoinedSignalNotificationType
|
||||||
| MessageType
|
| MessageType
|
||||||
| PhoneNumberDiscoveryNotificationType
|
| PhoneNumberDiscoveryNotificationType
|
||||||
|
| PollTerminateNotificationType
|
||||||
| ProfileChangeNotificationType
|
| ProfileChangeNotificationType
|
||||||
| ResetSessionNotificationType
|
| ResetSessionNotificationType
|
||||||
| SafetyNumberNotificationType
|
| SafetyNumberNotificationType
|
||||||
@@ -187,6 +197,7 @@ type PropsLocalType = {
|
|||||||
isGroup: boolean;
|
isGroup: boolean;
|
||||||
isNextItemCallingNotification: boolean;
|
isNextItemCallingNotification: boolean;
|
||||||
isTargeted: boolean;
|
isTargeted: boolean;
|
||||||
|
scrollToPollMessage: (messageId: string, conversationId: string) => unknown;
|
||||||
targetMessage: (messageId: string, conversationId: string) => unknown;
|
targetMessage: (messageId: string, conversationId: string) => unknown;
|
||||||
shouldRenderDateHeader: boolean;
|
shouldRenderDateHeader: boolean;
|
||||||
onOpenEditNicknameAndNoteModal: (contactId: string) => void;
|
onOpenEditNicknameAndNoteModal: (contactId: string) => void;
|
||||||
@@ -235,6 +246,7 @@ export const TimelineItem = memo(function TimelineItem({
|
|||||||
platform,
|
platform,
|
||||||
renderUniversalTimerNotification,
|
renderUniversalTimerNotification,
|
||||||
returnToActiveCall,
|
returnToActiveCall,
|
||||||
|
scrollToPollMessage,
|
||||||
targetMessage,
|
targetMessage,
|
||||||
setMessageToEdit,
|
setMessageToEdit,
|
||||||
shouldCollapseAbove,
|
shouldCollapseAbove,
|
||||||
@@ -413,6 +425,15 @@ export const TimelineItem = memo(function TimelineItem({
|
|||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
} else if (item.type === 'pollTerminate') {
|
||||||
|
notification = (
|
||||||
|
<PollTerminateNotification
|
||||||
|
{...reducedProps}
|
||||||
|
{...item.data}
|
||||||
|
i18n={i18n}
|
||||||
|
scrollToPollMessage={scrollToPollMessage}
|
||||||
|
/>
|
||||||
|
);
|
||||||
} else if (item.type === 'messageRequestResponse') {
|
} else if (item.type === 'messageRequestResponse') {
|
||||||
notification = (
|
notification = (
|
||||||
<MessageRequestResponseNotification
|
<MessageRequestResponseNotification
|
||||||
|
|||||||
@@ -235,6 +235,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||||||
bodyRanges: overrideProps.bodyRanges,
|
bodyRanges: overrideProps.bodyRanges,
|
||||||
canCopy: true,
|
canCopy: true,
|
||||||
canEditMessage: true,
|
canEditMessage: true,
|
||||||
|
canEndPoll: overrideProps.direction === 'outgoing',
|
||||||
canReact: true,
|
canReact: true,
|
||||||
canReply: true,
|
canReply: true,
|
||||||
canDownload: true,
|
canDownload: true,
|
||||||
@@ -294,6 +295,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||||||
previews: overrideProps.previews || [],
|
previews: overrideProps.previews || [],
|
||||||
quote: overrideProps.quote || undefined,
|
quote: overrideProps.quote || undefined,
|
||||||
reactions: overrideProps.reactions,
|
reactions: overrideProps.reactions,
|
||||||
|
endPoll: action('endPoll'),
|
||||||
reactToMessage: action('reactToMessage'),
|
reactToMessage: action('reactToMessage'),
|
||||||
readStatus:
|
readStatus:
|
||||||
overrideProps.readStatus === undefined
|
overrideProps.readStatus === undefined
|
||||||
@@ -384,6 +386,7 @@ const renderBothDirections = (props: Props) => (
|
|||||||
...props,
|
...props,
|
||||||
author: { ...props.author, id: getDefaultConversation().id },
|
author: { ...props.author, id: getDefaultConversation().id },
|
||||||
direction: 'outgoing',
|
direction: 'outgoing',
|
||||||
|
canEndPoll: true,
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export type PropsData = {
|
|||||||
canDownload: boolean;
|
canDownload: boolean;
|
||||||
canCopy: boolean;
|
canCopy: boolean;
|
||||||
canEditMessage: boolean;
|
canEditMessage: boolean;
|
||||||
|
canEndPoll: boolean;
|
||||||
canForward: boolean;
|
canForward: boolean;
|
||||||
canRetry: boolean;
|
canRetry: boolean;
|
||||||
canRetryDeleteForEveryone: boolean;
|
canRetryDeleteForEveryone: boolean;
|
||||||
@@ -70,6 +71,7 @@ export type PropsActions = {
|
|||||||
pushPanelForConversation: PushPanelForConversationActionType;
|
pushPanelForConversation: PushPanelForConversationActionType;
|
||||||
toggleDeleteMessagesModal: (props: DeleteMessagesPropsType) => void;
|
toggleDeleteMessagesModal: (props: DeleteMessagesPropsType) => void;
|
||||||
toggleForwardMessagesModal: (payload: ForwardMessagesPayload) => void;
|
toggleForwardMessagesModal: (payload: ForwardMessagesPayload) => void;
|
||||||
|
endPoll: (id: string) => void;
|
||||||
reactToMessage: (
|
reactToMessage: (
|
||||||
id: string,
|
id: string,
|
||||||
{ emoji, remove }: { emoji: string; remove: boolean }
|
{ emoji, remove }: { emoji: string; remove: boolean }
|
||||||
@@ -109,6 +111,7 @@ export function TimelineMessage(props: Props): JSX.Element {
|
|||||||
canDownload,
|
canDownload,
|
||||||
canCopy,
|
canCopy,
|
||||||
canEditMessage,
|
canEditMessage,
|
||||||
|
canEndPoll,
|
||||||
canForward,
|
canForward,
|
||||||
canReact,
|
canReact,
|
||||||
canReply,
|
canReply,
|
||||||
@@ -123,6 +126,7 @@ export function TimelineMessage(props: Props): JSX.Element {
|
|||||||
isTargeted,
|
isTargeted,
|
||||||
kickOffAttachmentDownload,
|
kickOffAttachmentDownload,
|
||||||
copyMessageText,
|
copyMessageText,
|
||||||
|
endPoll,
|
||||||
pushPanelForConversation,
|
pushPanelForConversation,
|
||||||
reactToMessage,
|
reactToMessage,
|
||||||
renderReactionPicker,
|
renderReactionPicker,
|
||||||
@@ -400,6 +404,7 @@ export function TimelineMessage(props: Props): JSX.Element {
|
|||||||
}
|
}
|
||||||
onReplyToMessage={handleReplyToMessage}
|
onReplyToMessage={handleReplyToMessage}
|
||||||
onReact={handleReact}
|
onReact={handleReact}
|
||||||
|
onEndPoll={canEndPoll ? () => endPoll(id) : undefined}
|
||||||
onRetryMessageSend={canRetry ? () => retryMessageSend(id) : undefined}
|
onRetryMessageSend={canRetry ? () => retryMessageSend(id) : undefined}
|
||||||
onRetryDeleteForEveryone={
|
onRetryDeleteForEveryone={
|
||||||
canRetryDeleteForEveryone
|
canRetryDeleteForEveryone
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ export type PollMessageContentsProps = {
|
|||||||
messageId: string;
|
messageId: string;
|
||||||
optionIndexes: ReadonlyArray<number>;
|
optionIndexes: ReadonlyArray<number>;
|
||||||
}) => void;
|
}) => void;
|
||||||
|
endPoll: (messageId: string) => void;
|
||||||
|
canEndPoll?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function PollMessageContents({
|
export function PollMessageContents({
|
||||||
@@ -96,6 +98,8 @@ export function PollMessageContents({
|
|||||||
i18n,
|
i18n,
|
||||||
messageId,
|
messageId,
|
||||||
sendPollVote,
|
sendPollVote,
|
||||||
|
endPoll,
|
||||||
|
canEndPoll,
|
||||||
}: PollMessageContentsProps): JSX.Element {
|
}: PollMessageContentsProps): JSX.Element {
|
||||||
const [showVotesModal, setShowVotesModal] = useState(false);
|
const [showVotesModal, setShowVotesModal] = useState(false);
|
||||||
const isIncoming = direction === 'incoming';
|
const isIncoming = direction === 'incoming';
|
||||||
@@ -268,6 +272,9 @@ export function PollMessageContents({
|
|||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
poll={poll}
|
poll={poll}
|
||||||
onClose={() => setShowVotesModal(false)}
|
onClose={() => setShowVotesModal(false)}
|
||||||
|
endPoll={endPoll}
|
||||||
|
canEndPoll={canEndPoll}
|
||||||
|
messageId={messageId}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { tw } from '../../../axo/tw.dom.js';
|
import { tw } from '../../../axo/tw.dom.js';
|
||||||
|
import { AxoButton } from '../../../axo/AxoButton.dom.js';
|
||||||
import { Modal } from '../../Modal.dom.js';
|
import { Modal } from '../../Modal.dom.js';
|
||||||
import { Avatar, AvatarSize } from '../../Avatar.dom.js';
|
import { Avatar, AvatarSize } from '../../Avatar.dom.js';
|
||||||
import { ContactName } from '../ContactName.dom.js';
|
import { ContactName } from '../ContactName.dom.js';
|
||||||
@@ -16,12 +17,18 @@ type PollVotesModalProps = {
|
|||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
poll: PollWithResolvedVotersType;
|
poll: PollWithResolvedVotersType;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
endPoll: (messageId: string) => void;
|
||||||
|
canEndPoll?: boolean;
|
||||||
|
messageId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function PollVotesModal({
|
export function PollVotesModal({
|
||||||
i18n,
|
i18n,
|
||||||
poll,
|
poll,
|
||||||
onClose,
|
onClose,
|
||||||
|
endPoll,
|
||||||
|
canEndPoll,
|
||||||
|
messageId,
|
||||||
}: PollVotesModalProps): JSX.Element {
|
}: PollVotesModalProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -41,56 +48,64 @@ export function PollVotesModal({
|
|||||||
<div className={tw('type-body-large')}>{poll.question}</div>
|
<div className={tw('type-body-large')}>{poll.question}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{poll.options.map((option, index) => {
|
{poll.options.map((option, index, array) => {
|
||||||
const voters = poll.votesByOption.get(index) || [];
|
const voters = poll.votesByOption.get(index) || [];
|
||||||
const optionKey = `option-${index}`;
|
const optionKey = `option-${index}`;
|
||||||
|
const isLastOption = index === array.length - 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={optionKey} className={tw('flex flex-col')}>
|
<React.Fragment key={optionKey}>
|
||||||
{/* Option Header */}
|
<div className={tw('flex flex-col')}>
|
||||||
<div
|
{/* Option Header */}
|
||||||
className={tw('mb-3 flex items-start gap-3 text-label-primary')}
|
|
||||||
>
|
|
||||||
<div className={tw('type-title-small')}>{option}</div>
|
|
||||||
<div
|
<div
|
||||||
className={tw('ms-auto mt-[2px] shrink-0 type-body-medium')}
|
className={tw(
|
||||||
|
'mb-3 flex items-start gap-3 text-label-primary'
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{i18n('icu:PollVotesModal__voteCount', {
|
<div className={tw('type-title-small')}>{option}</div>
|
||||||
count: voters.length,
|
<div
|
||||||
})}
|
className={tw('ms-auto mt-[2px] shrink-0 type-body-medium')}
|
||||||
|
>
|
||||||
|
{i18n('icu:PollVotesModal__voteCount', {
|
||||||
|
count: voters.length,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Voters List */}
|
||||||
|
<div className={tw('flex flex-col gap-4')}>
|
||||||
|
{voters.map((vote: PollVoteWithUserType) => (
|
||||||
|
<div
|
||||||
|
key={vote.from.id}
|
||||||
|
className={tw('flex items-center gap-3')}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
avatarUrl={vote.from.avatarUrl}
|
||||||
|
badge={undefined}
|
||||||
|
color={vote.from.color}
|
||||||
|
conversationType="direct"
|
||||||
|
i18n={i18n}
|
||||||
|
noteToSelf={false}
|
||||||
|
phoneNumber={vote.from.phoneNumber}
|
||||||
|
profileName={vote.from.profileName}
|
||||||
|
sharedGroupNames={vote.from.sharedGroupNames}
|
||||||
|
size={AvatarSize.THIRTY_SIX}
|
||||||
|
title={vote.from.title}
|
||||||
|
/>
|
||||||
|
<div className={tw('min-w-0 flex-1')}>
|
||||||
|
<ContactName
|
||||||
|
title={vote.from.title}
|
||||||
|
module={tw('type-body-large text-label-primary')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{!isLastOption && (
|
||||||
{/* Voters List */}
|
<hr className={tw('border-t-[0.5px] border-label-secondary')} />
|
||||||
<div className={tw('flex flex-col gap-4')}>
|
)}
|
||||||
{voters.map((vote: PollVoteWithUserType) => (
|
</React.Fragment>
|
||||||
<div
|
|
||||||
key={vote.from.id}
|
|
||||||
className={tw('flex items-center gap-3')}
|
|
||||||
>
|
|
||||||
<Avatar
|
|
||||||
avatarUrl={vote.from.avatarUrl}
|
|
||||||
badge={undefined}
|
|
||||||
color={vote.from.color}
|
|
||||||
conversationType="direct"
|
|
||||||
i18n={i18n}
|
|
||||||
noteToSelf={false}
|
|
||||||
phoneNumber={vote.from.phoneNumber}
|
|
||||||
profileName={vote.from.profileName}
|
|
||||||
sharedGroupNames={vote.from.sharedGroupNames}
|
|
||||||
size={AvatarSize.THIRTY_SIX}
|
|
||||||
title={vote.from.title}
|
|
||||||
/>
|
|
||||||
<div className={tw('min-w-0 flex-1')}>
|
|
||||||
<ContactName
|
|
||||||
title={vote.from.title}
|
|
||||||
module={tw('type-body-large text-label-primary')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
@@ -104,6 +119,24 @@ export function PollVotesModal({
|
|||||||
{i18n('icu:PollVotesModal__noVotes')}
|
{i18n('icu:PollVotesModal__noVotes')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{!poll.terminatedAt && canEndPoll && (
|
||||||
|
<>
|
||||||
|
<hr className={tw('border-t-[0.5px] border-label-secondary')} />
|
||||||
|
<div className={tw('flex justify-center')}>
|
||||||
|
<AxoButton.Root
|
||||||
|
size="lg"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
endPoll(messageId);
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{i18n('icu:Poll__end-poll')}
|
||||||
|
</AxoButton.Root>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { sendDeleteForEveryone } from './helpers/sendDeleteForEveryone.preload.j
|
|||||||
import { sendDeleteStoryForEveryone } from './helpers/sendDeleteStoryForEveryone.preload.js';
|
import { sendDeleteStoryForEveryone } from './helpers/sendDeleteStoryForEveryone.preload.js';
|
||||||
import { sendProfileKey } from './helpers/sendProfileKey.preload.js';
|
import { sendProfileKey } from './helpers/sendProfileKey.preload.js';
|
||||||
import { sendReaction } from './helpers/sendReaction.preload.js';
|
import { sendReaction } from './helpers/sendReaction.preload.js';
|
||||||
|
import { sendPollTerminate } from './helpers/sendPollTerminate.preload.js';
|
||||||
import { sendPollVote } from './helpers/sendPollVote.preload.js';
|
import { sendPollVote } from './helpers/sendPollVote.preload.js';
|
||||||
import { sendStory } from './helpers/sendStory.preload.js';
|
import { sendStory } from './helpers/sendStory.preload.js';
|
||||||
import { sendReceipts } from './helpers/sendReceipts.preload.js';
|
import { sendReceipts } from './helpers/sendReceipts.preload.js';
|
||||||
@@ -70,10 +71,11 @@ export const conversationQueueJobEnum = z.enum([
|
|||||||
'GroupUpdate',
|
'GroupUpdate',
|
||||||
'NormalMessage',
|
'NormalMessage',
|
||||||
'NullMessage',
|
'NullMessage',
|
||||||
|
'PollTerminate',
|
||||||
|
'PollVote',
|
||||||
'ProfileKey',
|
'ProfileKey',
|
||||||
'ProfileKeyForCall',
|
'ProfileKeyForCall',
|
||||||
'Reaction',
|
'Reaction',
|
||||||
'PollVote',
|
|
||||||
'ResendRequest',
|
'ResendRequest',
|
||||||
'SavedProto',
|
'SavedProto',
|
||||||
'SenderKeyDistribution',
|
'SenderKeyDistribution',
|
||||||
@@ -206,6 +208,15 @@ const pollVoteJobDataSchema = z.object({
|
|||||||
});
|
});
|
||||||
export type PollVoteJobData = z.infer<typeof pollVoteJobDataSchema>;
|
export type PollVoteJobData = z.infer<typeof pollVoteJobDataSchema>;
|
||||||
|
|
||||||
|
const pollTerminateJobDataSchema = z.object({
|
||||||
|
type: z.literal(conversationQueueJobEnum.enum.PollTerminate),
|
||||||
|
conversationId: z.string(),
|
||||||
|
pollMessageId: z.string(),
|
||||||
|
targetTimestamp: z.number(),
|
||||||
|
revision: z.number().optional(),
|
||||||
|
});
|
||||||
|
export type PollTerminateJobData = z.infer<typeof pollTerminateJobDataSchema>;
|
||||||
|
|
||||||
const resendRequestJobDataSchema = z.object({
|
const resendRequestJobDataSchema = z.object({
|
||||||
type: z.literal(conversationQueueJobEnum.enum.ResendRequest),
|
type: z.literal(conversationQueueJobEnum.enum.ResendRequest),
|
||||||
conversationId: z.string(),
|
conversationId: z.string(),
|
||||||
@@ -268,9 +279,10 @@ export const conversationQueueJobDataSchema = z.union([
|
|||||||
groupUpdateJobDataSchema,
|
groupUpdateJobDataSchema,
|
||||||
normalMessageSendJobDataSchema,
|
normalMessageSendJobDataSchema,
|
||||||
nullMessageJobDataSchema,
|
nullMessageJobDataSchema,
|
||||||
|
pollTerminateJobDataSchema,
|
||||||
|
pollVoteJobDataSchema,
|
||||||
profileKeyJobDataSchema,
|
profileKeyJobDataSchema,
|
||||||
reactionJobDataSchema,
|
reactionJobDataSchema,
|
||||||
pollVoteJobDataSchema,
|
|
||||||
resendRequestJobDataSchema,
|
resendRequestJobDataSchema,
|
||||||
savedProtoJobDataSchema,
|
savedProtoJobDataSchema,
|
||||||
senderKeyDistributionJobDataSchema,
|
senderKeyDistributionJobDataSchema,
|
||||||
@@ -327,8 +339,11 @@ function shouldSendShowCaptcha(type: ConversationQueueJobEnum): boolean {
|
|||||||
if (type === 'Reaction') {
|
if (type === 'Reaction') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (type === 'PollTerminate') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (type === 'PollVote') {
|
if (type === 'PollVote') {
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
if (type === 'Receipts') {
|
if (type === 'Receipts') {
|
||||||
return false;
|
return false;
|
||||||
@@ -974,6 +989,9 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
|
|||||||
case jobSet.Reaction:
|
case jobSet.Reaction:
|
||||||
await sendReaction(conversation, jobBundle, data);
|
await sendReaction(conversation, jobBundle, data);
|
||||||
break;
|
break;
|
||||||
|
case jobSet.PollTerminate:
|
||||||
|
await sendPollTerminate(conversation, jobBundle, data);
|
||||||
|
break;
|
||||||
case jobSet.PollVote:
|
case jobSet.PollVote:
|
||||||
await sendPollVote(conversation, jobBundle, data);
|
await sendPollVote(conversation, jobBundle, data);
|
||||||
break;
|
break;
|
||||||
|
|||||||
206
ts/jobs/helpers/sendPollTerminate.preload.ts
Normal file
206
ts/jobs/helpers/sendPollTerminate.preload.ts
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
// Copyright 2025 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { ContentHint } from '@signalapp/libsignal-client';
|
||||||
|
import lodash from 'lodash';
|
||||||
|
|
||||||
|
import type { ConversationModel } from '../../models/conversations.preload.js';
|
||||||
|
import type {
|
||||||
|
ConversationQueueJobBundle,
|
||||||
|
PollTerminateJobData,
|
||||||
|
} from '../conversationJobQueue.preload.js';
|
||||||
|
import { PollTerminateSendStatus } from '../../types/Polls.dom.js';
|
||||||
|
import { wrapWithSyncMessageSend } from '../../util/wrapWithSyncMessageSend.preload.js';
|
||||||
|
import { sendContentMessageToGroup } from '../../util/sendToGroup.preload.js';
|
||||||
|
import { getMessageById } from '../../messages/getMessageById.preload.js';
|
||||||
|
import { ourProfileKeyService } from '../../services/ourProfileKey.std.js';
|
||||||
|
import { getSendOptions } from '../../util/getSendOptions.preload.js';
|
||||||
|
import { isGroupV2 } from '../../util/whatTypeOfConversation.dom.js';
|
||||||
|
import {
|
||||||
|
handleMultipleSendErrors,
|
||||||
|
maybeExpandErrors,
|
||||||
|
} from './handleMultipleSendErrors.std.js';
|
||||||
|
import { getRecipients } from '../../util/getRecipients.dom.js';
|
||||||
|
import type { MessageModel } from '../../models/messages.preload.js';
|
||||||
|
import type { LoggerType } from '../../types/Logging.std.js';
|
||||||
|
import { strictAssert } from '../../util/assert.std.js';
|
||||||
|
import { DataWriter } from '../../sql/Client.preload.js';
|
||||||
|
import { cleanupMessages } from '../../util/cleanup.preload.js';
|
||||||
|
|
||||||
|
const { isNumber } = lodash;
|
||||||
|
|
||||||
|
export async function sendPollTerminate(
|
||||||
|
conversation: ConversationModel,
|
||||||
|
{
|
||||||
|
isFinalAttempt,
|
||||||
|
messaging,
|
||||||
|
shouldContinue,
|
||||||
|
timeRemaining,
|
||||||
|
log: jobLog,
|
||||||
|
}: ConversationQueueJobBundle,
|
||||||
|
data: PollTerminateJobData
|
||||||
|
): Promise<void> {
|
||||||
|
const { pollMessageId, targetTimestamp, revision } = data;
|
||||||
|
|
||||||
|
const logId = `sendPollTerminate(${conversation.idForLogging()}, ${pollMessageId})`;
|
||||||
|
|
||||||
|
const pollMessage = await getMessageById(pollMessageId);
|
||||||
|
if (!pollMessage) {
|
||||||
|
jobLog.error(`${logId}: Failed to fetch poll message. Failing job.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const poll = pollMessage.get('poll');
|
||||||
|
if (!poll) {
|
||||||
|
jobLog.error(`${logId}: Message has no poll object. Failing job.`);
|
||||||
|
await markTerminateFailed(pollMessage, jobLog);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isGroupV2(conversation.attributes)) {
|
||||||
|
jobLog.error(`${logId}: Non-GroupV2 conversation. Failing job.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shouldContinue) {
|
||||||
|
jobLog.info(`${logId}: Ran out of time. Giving up on sending`);
|
||||||
|
await markTerminateFailed(pollMessage, jobLog);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const recipients = getRecipients(conversation.attributes);
|
||||||
|
|
||||||
|
await conversation.queueJob(
|
||||||
|
'conversationQueue/sendPollTerminate',
|
||||||
|
async abortSignal => {
|
||||||
|
jobLog.info(
|
||||||
|
`${logId}: Sending poll terminate for poll timestamp ${targetTimestamp}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const profileKey = conversation.get('profileSharing')
|
||||||
|
? await ourProfileKeyService.get()
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const sendOptions = await getSendOptions(conversation.attributes);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isGroupV2(conversation.attributes) && !isNumber(revision)) {
|
||||||
|
jobLog.error('No revision provided, but conversation is GroupV2');
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupV2Info = conversation.getGroupV2Info({
|
||||||
|
members: recipients,
|
||||||
|
});
|
||||||
|
if (groupV2Info && isNumber(revision)) {
|
||||||
|
groupV2Info.revision = revision;
|
||||||
|
}
|
||||||
|
|
||||||
|
strictAssert(groupV2Info, 'could not get group info from conversation');
|
||||||
|
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const expireTimer = conversation.get('expireTimer');
|
||||||
|
|
||||||
|
const contentMessage = await messaging.getPollTerminateContentMessage({
|
||||||
|
groupV2: groupV2Info,
|
||||||
|
timestamp,
|
||||||
|
profileKey,
|
||||||
|
expireTimer,
|
||||||
|
expireTimerVersion: conversation.getExpireTimerVersion(),
|
||||||
|
pollTerminate: {
|
||||||
|
targetTimestamp,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (abortSignal?.aborted) {
|
||||||
|
throw new Error('sendPollTerminate was aborted');
|
||||||
|
}
|
||||||
|
|
||||||
|
await wrapWithSyncMessageSend({
|
||||||
|
conversation,
|
||||||
|
logId,
|
||||||
|
messageIds: [pollMessageId],
|
||||||
|
send: async () =>
|
||||||
|
sendContentMessageToGroup({
|
||||||
|
contentHint: ContentHint.Resendable,
|
||||||
|
contentMessage,
|
||||||
|
messageId: pollMessageId,
|
||||||
|
recipients,
|
||||||
|
sendOptions,
|
||||||
|
sendTarget: conversation.toSenderKeyTarget(),
|
||||||
|
sendType: 'pollTerminate',
|
||||||
|
timestamp,
|
||||||
|
urgent: true,
|
||||||
|
}),
|
||||||
|
sendType: 'pollTerminate',
|
||||||
|
timestamp,
|
||||||
|
});
|
||||||
|
|
||||||
|
await markTerminateSuccess(pollMessage, jobLog);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const errors = maybeExpandErrors(error);
|
||||||
|
await handleMultipleSendErrors({
|
||||||
|
errors,
|
||||||
|
isFinalAttempt,
|
||||||
|
log: jobLog,
|
||||||
|
markFailed: () => markTerminateFailed(pollMessage, jobLog),
|
||||||
|
timeRemaining,
|
||||||
|
toThrow: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function markTerminateSuccess(
|
||||||
|
message: MessageModel,
|
||||||
|
log: LoggerType
|
||||||
|
): Promise<void> {
|
||||||
|
log.info('markTerminateSuccess: Poll terminate sent successfully');
|
||||||
|
const poll = message.get('poll');
|
||||||
|
if (poll) {
|
||||||
|
message.set({
|
||||||
|
poll: {
|
||||||
|
...poll,
|
||||||
|
terminateSendStatus: PollTerminateSendStatus.Complete,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await window.MessageCache.saveMessage(message.attributes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function markTerminateFailed(
|
||||||
|
message: MessageModel,
|
||||||
|
log: LoggerType
|
||||||
|
): Promise<void> {
|
||||||
|
log.error('markTerminateFailed: Poll terminate send failed');
|
||||||
|
const poll = message.get('poll');
|
||||||
|
if (!poll) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.set({
|
||||||
|
poll: {
|
||||||
|
...poll,
|
||||||
|
terminatedAt: undefined,
|
||||||
|
terminateSendStatus: PollTerminateSendStatus.Failed,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await window.MessageCache.saveMessage(message.attributes);
|
||||||
|
|
||||||
|
// Delete the poll-terminate chat event from timeline
|
||||||
|
if (poll.terminatedAt) {
|
||||||
|
const notificationMessage = await window.MessageCache.findBySentAt(
|
||||||
|
poll.terminatedAt,
|
||||||
|
m =>
|
||||||
|
m.get('type') === 'poll-terminate' &&
|
||||||
|
m.get('pollTerminateNotification')?.pollMessageId === message.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (notificationMessage) {
|
||||||
|
log.info('markTerminateFailed: Deleting poll-terminate notification');
|
||||||
|
await DataWriter.removeMessage(notificationMessage.id, {
|
||||||
|
cleanupMessages,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -254,7 +254,7 @@ export async function sendPollVote(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
await send(ephemeral, {
|
const messageSendPromise = send(ephemeral, {
|
||||||
promise: handleMessageSend(promise, {
|
promise: handleMessageSend(promise, {
|
||||||
messageIds: [pollMessageId],
|
messageIds: [pollMessageId],
|
||||||
sendType: 'pollVote',
|
sendType: 'pollVote',
|
||||||
@@ -263,7 +263,9 @@ export async function sendPollVote(
|
|||||||
targetTimestamp: currentTimestamp,
|
targetTimestamp: currentTimestamp,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Await the inner promise to get SendMessageProtoError for upstream processors
|
// Because message.send swallows and processes errors, we'll await the inner promise
|
||||||
|
// to get the SendMessageProtoError, which gives us information upstream
|
||||||
|
// processors need to detect certain kinds of situations.
|
||||||
try {
|
try {
|
||||||
await promise;
|
await promise;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -278,6 +280,8 @@ export async function sendPollVote(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await messageSendPromise;
|
||||||
|
|
||||||
// Check if the send fully succeeded
|
// Check if the send fully succeeded
|
||||||
ephemeralSendStateByConversationId =
|
ephemeralSendStateByConversationId =
|
||||||
ephemeral.get('sendStateByConversationId') || {};
|
ephemeral.get('sendStateByConversationId') || {};
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type {
|
|||||||
ReadonlyMessageAttributesType,
|
ReadonlyMessageAttributesType,
|
||||||
} from '../model-types.d.ts';
|
} from '../model-types.d.ts';
|
||||||
import type { MessagePollVoteType } from '../types/Polls.dom.js';
|
import type { MessagePollVoteType } from '../types/Polls.dom.js';
|
||||||
|
import { PollTerminateSendStatus } from '../types/Polls.dom.js';
|
||||||
import { MessageModel } from '../models/messages.preload.js';
|
import { MessageModel } from '../models/messages.preload.js';
|
||||||
import { DataReader } from '../sql/Client.preload.js';
|
import { DataReader } from '../sql/Client.preload.js';
|
||||||
import * as Errors from '../types/errors.std.js';
|
import * as Errors from '../types/errors.std.js';
|
||||||
@@ -523,10 +524,16 @@ export async function handlePollTerminate(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isFromThisDevice = terminate.source === PollSource.FromThisDevice;
|
||||||
|
|
||||||
message.set({
|
message.set({
|
||||||
poll: {
|
poll: {
|
||||||
...poll,
|
...poll,
|
||||||
terminatedAt: terminate.timestamp,
|
terminatedAt: terminate.timestamp,
|
||||||
|
// Track send status (only for our own terminates)
|
||||||
|
terminateSendStatus: isFromThisDevice
|
||||||
|
? PollTerminateSendStatus.Pending
|
||||||
|
: PollTerminateSendStatus.NotInitiated,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -537,6 +544,15 @@ export async function handlePollTerminate(
|
|||||||
|
|
||||||
if (shouldPersist) {
|
if (shouldPersist) {
|
||||||
await window.MessageCache.saveMessage(message.attributes);
|
await window.MessageCache.saveMessage(message.attributes);
|
||||||
|
|
||||||
|
await conversation.addPollTerminateNotification({
|
||||||
|
pollQuestion: poll.question,
|
||||||
|
pollMessageId: message.id,
|
||||||
|
terminatorId: terminate.fromConversationId,
|
||||||
|
timestamp: terminate.timestamp,
|
||||||
|
isMeTerminating: isMe(author.attributes),
|
||||||
|
});
|
||||||
|
|
||||||
window.reduxActions.conversations.markOpenConversationRead(conversation.id);
|
window.reduxActions.conversations.markOpenConversationRead(conversation.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
5
ts/model-types.d.ts
vendored
5
ts/model-types.d.ts
vendored
@@ -166,6 +166,7 @@ type MessageType =
|
|||||||
| 'keychange'
|
| 'keychange'
|
||||||
| 'outgoing'
|
| 'outgoing'
|
||||||
| 'phone-number-discovery'
|
| 'phone-number-discovery'
|
||||||
|
| 'poll-terminate'
|
||||||
| 'profile-change'
|
| 'profile-change'
|
||||||
| 'story'
|
| 'story'
|
||||||
| 'timer-notification'
|
| 'timer-notification'
|
||||||
@@ -212,6 +213,10 @@ export type MessageAttributesType = {
|
|||||||
quote?: QuotedMessageType;
|
quote?: QuotedMessageType;
|
||||||
reactions?: ReadonlyArray<MessageReactionType>;
|
reactions?: ReadonlyArray<MessageReactionType>;
|
||||||
poll?: PollMessageAttribute;
|
poll?: PollMessageAttribute;
|
||||||
|
pollTerminateNotification?: {
|
||||||
|
question: string;
|
||||||
|
pollMessageId: string;
|
||||||
|
};
|
||||||
requiredProtocolVersion?: number;
|
requiredProtocolVersion?: number;
|
||||||
sms?: boolean;
|
sms?: boolean;
|
||||||
sourceDevice?: number;
|
sourceDevice?: number;
|
||||||
|
|||||||
@@ -3495,6 +3495,44 @@ export class ConversationModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async addPollTerminateNotification(params: {
|
||||||
|
pollQuestion: string;
|
||||||
|
pollMessageId: string;
|
||||||
|
terminatorId: string;
|
||||||
|
timestamp: number;
|
||||||
|
isMeTerminating: boolean;
|
||||||
|
}): Promise<void> {
|
||||||
|
const terminatorConversation = window.ConversationController.get(
|
||||||
|
params.terminatorId
|
||||||
|
);
|
||||||
|
const terminatorServiceId = terminatorConversation?.getServiceId();
|
||||||
|
|
||||||
|
const message = new MessageModel({
|
||||||
|
...generateMessageId(incrementMessageCounter()),
|
||||||
|
conversationId: this.id,
|
||||||
|
type: 'poll-terminate',
|
||||||
|
sent_at: params.timestamp,
|
||||||
|
timestamp: params.timestamp,
|
||||||
|
received_at_ms: params.timestamp,
|
||||||
|
sourceServiceId: terminatorServiceId,
|
||||||
|
pollTerminateNotification: {
|
||||||
|
question: params.pollQuestion,
|
||||||
|
pollMessageId: params.pollMessageId,
|
||||||
|
},
|
||||||
|
readStatus: params.isMeTerminating ? ReadStatus.Read : ReadStatus.Unread,
|
||||||
|
seenStatus: params.isMeTerminating ? SeenStatus.Seen : SeenStatus.Unseen,
|
||||||
|
schemaVersion: Message.VERSION_NEEDED_FOR_DISPLAY,
|
||||||
|
});
|
||||||
|
|
||||||
|
await window.MessageCache.saveMessage(message, { forceSave: true });
|
||||||
|
window.MessageCache.register(message);
|
||||||
|
|
||||||
|
drop(this.onNewMessage(message));
|
||||||
|
|
||||||
|
this.throttledUpdateUnread();
|
||||||
|
await maybeNotify({ message: message.attributes, conversation: this });
|
||||||
|
}
|
||||||
|
|
||||||
async addNotification(
|
async addNotification(
|
||||||
type: MessageAttributesType['type'],
|
type: MessageAttributesType['type'],
|
||||||
extra: Partial<MessageAttributesType> = {}
|
extra: Partial<MessageAttributesType> = {}
|
||||||
|
|||||||
74
ts/polls/enqueuePollTerminateForSend.preload.ts
Normal file
74
ts/polls/enqueuePollTerminateForSend.preload.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
// Copyright 2025 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { v7 as generateUuid } from 'uuid';
|
||||||
|
import {
|
||||||
|
conversationJobQueue,
|
||||||
|
conversationQueueJobEnum,
|
||||||
|
} from '../jobs/conversationJobQueue.preload.js';
|
||||||
|
import { getMessageById } from '../messages/getMessageById.preload.js';
|
||||||
|
import {
|
||||||
|
handlePollTerminate,
|
||||||
|
PollSource,
|
||||||
|
type PollTerminateAttributesType,
|
||||||
|
} from '../messageModifiers/Polls.preload.js';
|
||||||
|
import { isGroup } from '../util/whatTypeOfConversation.dom.js';
|
||||||
|
import { strictAssert } from '../util/assert.std.js';
|
||||||
|
import { createLogger } from '../logging/log.std.js';
|
||||||
|
|
||||||
|
const log = createLogger('enqueuePollTerminateForSend');
|
||||||
|
|
||||||
|
export async function enqueuePollTerminateForSend({
|
||||||
|
messageId,
|
||||||
|
}: Readonly<{
|
||||||
|
messageId: string;
|
||||||
|
}>): Promise<void> {
|
||||||
|
const message = await getMessageById(messageId);
|
||||||
|
strictAssert(message, 'enqueuePollTerminateForSend: no message found');
|
||||||
|
|
||||||
|
const conversation = window.ConversationController.get(
|
||||||
|
message.get('conversationId')
|
||||||
|
);
|
||||||
|
strictAssert(
|
||||||
|
conversation,
|
||||||
|
'enqueuePollTerminateForSend: No conversation extracted from target message'
|
||||||
|
);
|
||||||
|
strictAssert(
|
||||||
|
isGroup(conversation.attributes),
|
||||||
|
'enqueuePollTerminateForSend: conversation must be a group'
|
||||||
|
);
|
||||||
|
|
||||||
|
const ourId = window.ConversationController.getOurConversationIdOrThrow();
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const targetTimestamp = message.get('sent_at');
|
||||||
|
|
||||||
|
const terminate: PollTerminateAttributesType = {
|
||||||
|
envelopeId: generateUuid(),
|
||||||
|
removeFromMessageReceiverCache: () => undefined,
|
||||||
|
fromConversationId: ourId,
|
||||||
|
source: PollSource.FromThisDevice,
|
||||||
|
targetTimestamp,
|
||||||
|
receivedAtDate: timestamp,
|
||||||
|
timestamp,
|
||||||
|
};
|
||||||
|
|
||||||
|
await handlePollTerminate(message, terminate, { shouldPersist: true });
|
||||||
|
|
||||||
|
await conversationJobQueue.add(
|
||||||
|
{
|
||||||
|
type: conversationQueueJobEnum.enum.PollTerminate,
|
||||||
|
conversationId: conversation.id,
|
||||||
|
pollMessageId: messageId,
|
||||||
|
targetTimestamp,
|
||||||
|
revision: conversation.get('revision'),
|
||||||
|
},
|
||||||
|
async jobToInsert => {
|
||||||
|
log.info(
|
||||||
|
`Enqueueing poll terminate for poll ${messageId} with job ${jobToInsert.id}`
|
||||||
|
);
|
||||||
|
await window.MessageCache.saveMessage(message.attributes, {
|
||||||
|
jobToInsert,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -77,6 +77,7 @@ import { canReply, isNormalBubble } from '../selectors/message.preload.js';
|
|||||||
import { getAuthorId } from '../../messages/sources.preload.js';
|
import { getAuthorId } from '../../messages/sources.preload.js';
|
||||||
import { getConversationSelector } from '../selectors/conversations.dom.js';
|
import { getConversationSelector } from '../selectors/conversations.dom.js';
|
||||||
import { enqueueReactionForSend } from '../../reactions/enqueueReactionForSend.preload.js';
|
import { enqueueReactionForSend } from '../../reactions/enqueueReactionForSend.preload.js';
|
||||||
|
import { enqueuePollTerminateForSend } from '../../polls/enqueuePollTerminateForSend.preload.js';
|
||||||
import { useBoundActions } from '../../hooks/useBoundActions.std.js';
|
import { useBoundActions } from '../../hooks/useBoundActions.std.js';
|
||||||
import {
|
import {
|
||||||
CONVERSATION_UNLOADED,
|
CONVERSATION_UNLOADED,
|
||||||
@@ -242,6 +243,7 @@ export const actions = {
|
|||||||
addAttachment,
|
addAttachment,
|
||||||
addPendingAttachment,
|
addPendingAttachment,
|
||||||
cancelJoinRequest,
|
cancelJoinRequest,
|
||||||
|
endPoll,
|
||||||
incrementSendCounter,
|
incrementSendCounter,
|
||||||
onClearAttachments,
|
onClearAttachments,
|
||||||
onCloseLinkPreview,
|
onCloseLinkPreview,
|
||||||
@@ -253,6 +255,7 @@ export const actions = {
|
|||||||
replaceAttachments,
|
replaceAttachments,
|
||||||
resetComposer,
|
resetComposer,
|
||||||
saveDraftRecordingIfNeeded,
|
saveDraftRecordingIfNeeded,
|
||||||
|
scrollToPollMessage,
|
||||||
scrollToQuotedMessage,
|
scrollToQuotedMessage,
|
||||||
sendEditedMessage,
|
sendEditedMessage,
|
||||||
sendMultiMediaMessage,
|
sendMultiMediaMessage,
|
||||||
@@ -374,6 +377,40 @@ function scrollToQuotedMessage({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function scrollToPollMessage(
|
||||||
|
pollMessageId: string,
|
||||||
|
conversationId: string
|
||||||
|
): ThunkAction<
|
||||||
|
void,
|
||||||
|
RootStateType,
|
||||||
|
unknown,
|
||||||
|
ShowToastActionType | ScrollToMessageActionType
|
||||||
|
> {
|
||||||
|
return async (dispatch, getState) => {
|
||||||
|
const pollMessage = await getMessageById(pollMessageId);
|
||||||
|
|
||||||
|
if (!pollMessage) {
|
||||||
|
dispatch({
|
||||||
|
type: SHOW_TOAST,
|
||||||
|
payload: {
|
||||||
|
toastType: ToastType.PollNotFound,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getState().conversations.selectedConversationId !== conversationId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollToMessage(conversationId, pollMessageId)(
|
||||||
|
dispatch,
|
||||||
|
getState,
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function saveDraftRecordingIfNeeded(): ThunkAction<
|
export function saveDraftRecordingIfNeeded(): ThunkAction<
|
||||||
void,
|
void,
|
||||||
RootStateType,
|
RootStateType,
|
||||||
@@ -1349,6 +1386,33 @@ function reactToMessage(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function endPoll(
|
||||||
|
messageId: string
|
||||||
|
): ThunkAction<
|
||||||
|
void,
|
||||||
|
RootStateType,
|
||||||
|
unknown,
|
||||||
|
NoopActionType | ShowToastActionType
|
||||||
|
> {
|
||||||
|
return async dispatch => {
|
||||||
|
try {
|
||||||
|
await enqueuePollTerminateForSend({ messageId });
|
||||||
|
dispatch({
|
||||||
|
type: 'NOOP',
|
||||||
|
payload: null,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
log.error('endPoll: Error sending poll terminate', error, messageId);
|
||||||
|
dispatch({
|
||||||
|
type: SHOW_TOAST,
|
||||||
|
payload: {
|
||||||
|
toastType: ToastType.Error,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function resetComposer(conversationId: string): ResetComposerActionType {
|
export function resetComposer(conversationId: string): ResetComposerActionType {
|
||||||
return {
|
return {
|
||||||
type: RESET_COMPOSER,
|
type: RESET_COMPOSER,
|
||||||
|
|||||||
@@ -889,6 +889,7 @@ export const getPropsForMessage = (
|
|||||||
canEditMessage: canEditMessage(message),
|
canEditMessage: canEditMessage(message),
|
||||||
canDeleteForEveryone: canDeleteForEveryone(message, conversation.isMe),
|
canDeleteForEveryone: canDeleteForEveryone(message, conversation.isMe),
|
||||||
canDownload: canDownload(message, conversationSelector),
|
canDownload: canDownload(message, conversationSelector),
|
||||||
|
canEndPoll: canEndPoll(message),
|
||||||
canForward: canForward(message),
|
canForward: canForward(message),
|
||||||
canReact: canReact(message, ourConversationId, conversationSelector),
|
canReact: canReact(message, ourConversationId, conversationSelector),
|
||||||
canReply: canReply(message, ourConversationId, conversationSelector),
|
canReply: canReply(message, ourConversationId, conversationSelector),
|
||||||
@@ -1068,6 +1069,13 @@ export function getPropsForBubble(
|
|||||||
timestamp,
|
timestamp,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (isPollTerminate(message)) {
|
||||||
|
return {
|
||||||
|
type: 'pollTerminate',
|
||||||
|
data: getPropsForPollTerminate(message, options),
|
||||||
|
timestamp,
|
||||||
|
};
|
||||||
|
}
|
||||||
if (isUniversalTimerNotification(message)) {
|
if (isUniversalTimerNotification(message)) {
|
||||||
return {
|
return {
|
||||||
type: 'universalTimerNotification',
|
type: 'universalTimerNotification',
|
||||||
@@ -1679,6 +1687,35 @@ function getPropsForProfileChange(
|
|||||||
} as ProfileChangeNotificationPropsType;
|
} as ProfileChangeNotificationPropsType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Poll Terminate
|
||||||
|
|
||||||
|
export function isPollTerminate(message: MessageWithUIFieldsType): boolean {
|
||||||
|
return message.type === 'poll-terminate';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPropsForPollTerminate(
|
||||||
|
message: MessageWithUIFieldsType,
|
||||||
|
{ conversationSelector }: GetPropsForBubbleOptions
|
||||||
|
) {
|
||||||
|
const { pollTerminateNotification, sourceServiceId, conversationId } =
|
||||||
|
message;
|
||||||
|
|
||||||
|
if (!pollTerminateNotification) {
|
||||||
|
throw new Error(
|
||||||
|
'getPropsForPollTerminate: pollTerminateNotification is undefined'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sender = conversationSelector(sourceServiceId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
sender,
|
||||||
|
pollQuestion: pollTerminateNotification.question,
|
||||||
|
pollMessageId: pollTerminateNotification.pollMessageId,
|
||||||
|
conversationId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Message Request Response Event
|
// Message Request Response Event
|
||||||
|
|
||||||
export function isMessageRequestResponse(
|
export function isMessageRequestResponse(
|
||||||
@@ -2221,6 +2258,25 @@ export function canRetryDeleteForEveryone(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function canEndPoll(
|
||||||
|
message: Pick<MessageWithUIFieldsType, 'type' | 'poll'>
|
||||||
|
): boolean {
|
||||||
|
if (message.type !== 'outgoing') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { poll } = message;
|
||||||
|
if (!poll) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (poll.terminatedAt != null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
export function canDownload(
|
export function canDownload(
|
||||||
message: MessageWithUIFieldsType,
|
message: MessageWithUIFieldsType,
|
||||||
conversationSelector: GetConversationByIdType
|
conversationSelector: GetConversationByIdType
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { getMessageDetails } from '../selectors/message.preload.js';
|
|||||||
import { getPreferredBadgeSelector } from '../selectors/badges.preload.js';
|
import { getPreferredBadgeSelector } from '../selectors/badges.preload.js';
|
||||||
import { renderAudioAttachment } from './renderAudioAttachment.preload.js';
|
import { renderAudioAttachment } from './renderAudioAttachment.preload.js';
|
||||||
import { useAccountsActions } from '../ducks/accounts.preload.js';
|
import { useAccountsActions } from '../ducks/accounts.preload.js';
|
||||||
|
import { useComposerActions } from '../ducks/composer.preload.js';
|
||||||
import { useConversationsActions } from '../ducks/conversations.preload.js';
|
import { useConversationsActions } from '../ducks/conversations.preload.js';
|
||||||
import { useGlobalModalActions } from '../ducks/globalModals.preload.js';
|
import { useGlobalModalActions } from '../ducks/globalModals.preload.js';
|
||||||
import { useLightboxActions } from '../ducks/lightbox.preload.js';
|
import { useLightboxActions } from '../ducks/lightbox.preload.js';
|
||||||
@@ -38,6 +39,7 @@ export const SmartMessageDetail = memo(
|
|||||||
const messageDetails = useSelector(getMessageDetails);
|
const messageDetails = useSelector(getMessageDetails);
|
||||||
const theme = useSelector(getTheme);
|
const theme = useSelector(getTheme);
|
||||||
const { checkForAccount } = useAccountsActions();
|
const { checkForAccount } = useAccountsActions();
|
||||||
|
const { endPoll } = useComposerActions();
|
||||||
const {
|
const {
|
||||||
cancelAttachmentDownload,
|
cancelAttachmentDownload,
|
||||||
clearTargetedMessage: clearSelectedMessage,
|
clearTargetedMessage: clearSelectedMessage,
|
||||||
@@ -93,6 +95,7 @@ export const SmartMessageDetail = memo(
|
|||||||
contactNameColor={contactNameColor}
|
contactNameColor={contactNameColor}
|
||||||
contacts={contacts}
|
contacts={contacts}
|
||||||
doubleCheckMissingQuoteReference={doubleCheckMissingQuoteReference}
|
doubleCheckMissingQuoteReference={doubleCheckMissingQuoteReference}
|
||||||
|
endPoll={endPoll}
|
||||||
errors={errors}
|
errors={errors}
|
||||||
getPreferredBadge={getPreferredBadge}
|
getPreferredBadge={getPreferredBadge}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
|
|||||||
@@ -147,8 +147,13 @@ export const SmartTimelineItem = memo(function SmartTimelineItem(
|
|||||||
toggleSelectMessage,
|
toggleSelectMessage,
|
||||||
} = useConversationsActions();
|
} = useConversationsActions();
|
||||||
|
|
||||||
const { reactToMessage, scrollToQuotedMessage, setQuoteByMessageId } =
|
const {
|
||||||
useComposerActions();
|
endPoll,
|
||||||
|
reactToMessage,
|
||||||
|
scrollToPollMessage,
|
||||||
|
scrollToQuotedMessage,
|
||||||
|
setQuoteByMessageId,
|
||||||
|
} = useComposerActions();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
showContactModal,
|
showContactModal,
|
||||||
@@ -218,6 +223,7 @@ export const SmartTimelineItem = memo(function SmartTimelineItem(
|
|||||||
messageExpanded={messageExpanded}
|
messageExpanded={messageExpanded}
|
||||||
openGiftBadge={openGiftBadge}
|
openGiftBadge={openGiftBadge}
|
||||||
pushPanelForConversation={pushPanelForConversation}
|
pushPanelForConversation={pushPanelForConversation}
|
||||||
|
endPoll={endPoll}
|
||||||
reactToMessage={reactToMessage}
|
reactToMessage={reactToMessage}
|
||||||
copyMessageText={copyMessageText}
|
copyMessageText={copyMessageText}
|
||||||
onOpenEditNicknameAndNoteModal={onOpenEditNicknameAndNoteModal}
|
onOpenEditNicknameAndNoteModal={onOpenEditNicknameAndNoteModal}
|
||||||
@@ -232,6 +238,7 @@ export const SmartTimelineItem = memo(function SmartTimelineItem(
|
|||||||
returnToActiveCall={returnToActiveCall}
|
returnToActiveCall={returnToActiveCall}
|
||||||
saveAttachment={saveAttachment}
|
saveAttachment={saveAttachment}
|
||||||
saveAttachments={saveAttachments}
|
saveAttachments={saveAttachments}
|
||||||
|
scrollToPollMessage={scrollToPollMessage}
|
||||||
scrollToQuotedMessage={scrollToQuotedMessage}
|
scrollToQuotedMessage={scrollToQuotedMessage}
|
||||||
targetMessage={targetMessage}
|
targetMessage={targetMessage}
|
||||||
setQuoteByMessageId={setQuoteByMessageId}
|
setQuoteByMessageId={setQuoteByMessageId}
|
||||||
|
|||||||
@@ -217,6 +217,9 @@ export type MessageOptionsType = {
|
|||||||
reaction?: ReactionType;
|
reaction?: ReactionType;
|
||||||
pollVote?: OutgoingPollVote;
|
pollVote?: OutgoingPollVote;
|
||||||
pollCreate?: PollCreateType;
|
pollCreate?: PollCreateType;
|
||||||
|
pollTerminate?: Readonly<{
|
||||||
|
targetTimestamp: number;
|
||||||
|
}>;
|
||||||
deletedForEveryoneTimestamp?: number;
|
deletedForEveryoneTimestamp?: number;
|
||||||
targetTimestampForEdit?: number;
|
targetTimestampForEdit?: number;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
@@ -243,6 +246,9 @@ export type GroupSendOptionsType = {
|
|||||||
timestamp: number;
|
timestamp: number;
|
||||||
pollVote?: OutgoingPollVote;
|
pollVote?: OutgoingPollVote;
|
||||||
pollCreate?: PollCreateType;
|
pollCreate?: PollCreateType;
|
||||||
|
pollTerminate?: Readonly<{
|
||||||
|
targetTimestamp: number;
|
||||||
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PollVoteBuildOptions = Required<
|
export type PollVoteBuildOptions = Required<
|
||||||
@@ -250,6 +256,11 @@ export type PollVoteBuildOptions = Required<
|
|||||||
> &
|
> &
|
||||||
Pick<MessageOptionsType, 'profileKey' | 'expireTimer' | 'expireTimerVersion'>;
|
Pick<MessageOptionsType, 'profileKey' | 'expireTimer' | 'expireTimerVersion'>;
|
||||||
|
|
||||||
|
export type PollTerminateBuildOptions = Required<
|
||||||
|
Pick<MessageOptionsType, 'groupV2' | 'timestamp' | 'pollTerminate'>
|
||||||
|
> &
|
||||||
|
Pick<MessageOptionsType, 'profileKey' | 'expireTimer' | 'expireTimerVersion'>;
|
||||||
|
|
||||||
class Message {
|
class Message {
|
||||||
attachments: ReadonlyArray<Proto.IAttachmentPointer>;
|
attachments: ReadonlyArray<Proto.IAttachmentPointer>;
|
||||||
|
|
||||||
@@ -288,6 +299,10 @@ class Message {
|
|||||||
|
|
||||||
pollCreate?: PollCreateType;
|
pollCreate?: PollCreateType;
|
||||||
|
|
||||||
|
pollTerminate?: Readonly<{
|
||||||
|
targetTimestamp: number;
|
||||||
|
}>;
|
||||||
|
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
|
|
||||||
dataMessage?: Proto.DataMessage;
|
dataMessage?: Proto.DataMessage;
|
||||||
@@ -318,6 +333,7 @@ class Message {
|
|||||||
this.sticker = options.sticker;
|
this.sticker = options.sticker;
|
||||||
this.reaction = options.reaction;
|
this.reaction = options.reaction;
|
||||||
this.pollCreate = options.pollCreate;
|
this.pollCreate = options.pollCreate;
|
||||||
|
this.pollTerminate = options.pollTerminate;
|
||||||
this.timestamp = options.timestamp;
|
this.timestamp = options.timestamp;
|
||||||
this.deletedForEveryoneTimestamp = options.deletedForEveryoneTimestamp;
|
this.deletedForEveryoneTimestamp = options.deletedForEveryoneTimestamp;
|
||||||
this.groupCallUpdate = options.groupCallUpdate;
|
this.groupCallUpdate = options.groupCallUpdate;
|
||||||
@@ -886,6 +902,62 @@ export class MessageSender {
|
|||||||
return contentMessage;
|
return contentMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createDataMessageProtoForPollTerminate({
|
||||||
|
groupV2,
|
||||||
|
timestamp,
|
||||||
|
profileKey,
|
||||||
|
expireTimer,
|
||||||
|
expireTimerVersion,
|
||||||
|
pollTerminate,
|
||||||
|
}: PollTerminateBuildOptions): Proto.DataMessage {
|
||||||
|
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 (typeof expireTimer !== 'undefined') {
|
||||||
|
dataMessage.expireTimer = expireTimer;
|
||||||
|
}
|
||||||
|
if (typeof expireTimerVersion !== 'undefined') {
|
||||||
|
dataMessage.expireTimerVersion = expireTimerVersion;
|
||||||
|
}
|
||||||
|
if (profileKey) {
|
||||||
|
dataMessage.profileKey = profileKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
const terminate = new Proto.DataMessage.PollTerminate();
|
||||||
|
terminate.targetSentTimestamp = Long.fromNumber(
|
||||||
|
pollTerminate.targetTimestamp
|
||||||
|
);
|
||||||
|
dataMessage.pollTerminate = terminate;
|
||||||
|
|
||||||
|
return dataMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPollTerminateContentMessage({
|
||||||
|
groupV2,
|
||||||
|
timestamp,
|
||||||
|
profileKey,
|
||||||
|
expireTimer,
|
||||||
|
expireTimerVersion,
|
||||||
|
pollTerminate,
|
||||||
|
}: PollTerminateBuildOptions): Promise<Proto.Content> {
|
||||||
|
const dataMessage = this.createDataMessageProtoForPollTerminate({
|
||||||
|
groupV2,
|
||||||
|
timestamp,
|
||||||
|
profileKey,
|
||||||
|
expireTimer,
|
||||||
|
expireTimerVersion,
|
||||||
|
pollTerminate,
|
||||||
|
});
|
||||||
|
const contentMessage = new Proto.Content();
|
||||||
|
contentMessage.dataMessage = dataMessage;
|
||||||
|
return contentMessage;
|
||||||
|
}
|
||||||
|
|
||||||
async getStoryMessage({
|
async getStoryMessage({
|
||||||
allowsReplies,
|
allowsReplies,
|
||||||
bodyRanges,
|
bodyRanges,
|
||||||
@@ -1056,6 +1128,7 @@ export class MessageSender {
|
|||||||
timestamp,
|
timestamp,
|
||||||
pollVote,
|
pollVote,
|
||||||
pollCreate,
|
pollCreate,
|
||||||
|
pollTerminate,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
if (!groupV2) {
|
if (!groupV2) {
|
||||||
@@ -1101,6 +1174,7 @@ export class MessageSender {
|
|||||||
timestamp,
|
timestamp,
|
||||||
pollVote,
|
pollVote,
|
||||||
pollCreate,
|
pollCreate,
|
||||||
|
pollTerminate,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -84,6 +84,13 @@ export const PollTerminateSchema = z
|
|||||||
})
|
})
|
||||||
.describe('PollTerminate');
|
.describe('PollTerminate');
|
||||||
|
|
||||||
|
export enum PollTerminateSendStatus {
|
||||||
|
NotInitiated = 'NotInitiated',
|
||||||
|
Pending = 'Pending',
|
||||||
|
Complete = 'Complete',
|
||||||
|
Failed = 'Failed',
|
||||||
|
}
|
||||||
|
|
||||||
export type MessagePollVoteType = {
|
export type MessagePollVoteType = {
|
||||||
fromConversationId: string;
|
fromConversationId: string;
|
||||||
optionIndexes: ReadonlyArray<number>;
|
optionIndexes: ReadonlyArray<number>;
|
||||||
@@ -98,6 +105,7 @@ export type PollMessageAttribute = {
|
|||||||
allowMultiple: boolean;
|
allowMultiple: boolean;
|
||||||
votes?: ReadonlyArray<MessagePollVoteType>;
|
votes?: ReadonlyArray<MessagePollVoteType>;
|
||||||
terminatedAt?: number;
|
terminatedAt?: number;
|
||||||
|
terminateSendStatus?: PollTerminateSendStatus;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PollCreateType = Pick<
|
export type PollCreateType = Pick<
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ export enum ToastType {
|
|||||||
NotificationProfileUpdate = 'NotificationProfileUpdate',
|
NotificationProfileUpdate = 'NotificationProfileUpdate',
|
||||||
OriginalMessageNotFound = 'OriginalMessageNotFound',
|
OriginalMessageNotFound = 'OriginalMessageNotFound',
|
||||||
PinnedConversationsFull = 'PinnedConversationsFull',
|
PinnedConversationsFull = 'PinnedConversationsFull',
|
||||||
|
PollNotFound = 'PollNotFound',
|
||||||
ReactionFailed = 'ReactionFailed',
|
ReactionFailed = 'ReactionFailed',
|
||||||
ReceiptSaved = 'ReceiptSaved',
|
ReceiptSaved = 'ReceiptSaved',
|
||||||
ReceiptSaveFailed = 'ReceiptSaveFailed',
|
ReceiptSaveFailed = 'ReceiptSaveFailed',
|
||||||
@@ -196,6 +197,7 @@ export type AnyToast =
|
|||||||
}
|
}
|
||||||
| { toastType: ToastType.OriginalMessageNotFound }
|
| { toastType: ToastType.OriginalMessageNotFound }
|
||||||
| { toastType: ToastType.PinnedConversationsFull }
|
| { toastType: ToastType.PinnedConversationsFull }
|
||||||
|
| { toastType: ToastType.PollNotFound }
|
||||||
| { toastType: ToastType.ReactionFailed }
|
| { toastType: ToastType.ReactionFailed }
|
||||||
| {
|
| {
|
||||||
toastType: ToastType.ReceiptSaved;
|
toastType: ToastType.ReceiptSaved;
|
||||||
|
|||||||
@@ -521,6 +521,25 @@ export function getNotificationDataForMessage(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { pollTerminateNotification } = attributes;
|
||||||
|
if (pollTerminateNotification) {
|
||||||
|
const sender = findAndFormatContact(attributes.sourceServiceId);
|
||||||
|
|
||||||
|
const text = sender.isMe
|
||||||
|
? i18n('icu:PollTerminate--you', {
|
||||||
|
poll: pollTerminateNotification.question,
|
||||||
|
})
|
||||||
|
: i18n('icu:PollTerminate--other', {
|
||||||
|
name: sender.title,
|
||||||
|
poll: pollTerminateNotification.question,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
emoji: '📊',
|
||||||
|
text,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (body) {
|
if (body) {
|
||||||
return {
|
return {
|
||||||
text: body,
|
text: body,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export const sendTypesEnum = z.enum([
|
|||||||
'expirationTimerUpdate', // non-urgent
|
'expirationTimerUpdate', // non-urgent
|
||||||
'groupChange', // non-urgent
|
'groupChange', // non-urgent
|
||||||
'reaction',
|
'reaction',
|
||||||
|
'pollTerminate',
|
||||||
'pollVote', // non-urgent
|
'pollVote', // non-urgent
|
||||||
'typing', // excluded from send log; non-urgent
|
'typing', // excluded from send log; non-urgent
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export async function wrapWithSyncMessageSend({
|
|||||||
messageIds,
|
messageIds,
|
||||||
sendType,
|
sendType,
|
||||||
});
|
});
|
||||||
|
|
||||||
didSuccessfullySendOne = true;
|
didSuccessfullySendOne = true;
|
||||||
} catch (thrown) {
|
} catch (thrown) {
|
||||||
if (thrown instanceof SendMessageProtoError) {
|
if (thrown instanceof SendMessageProtoError) {
|
||||||
|
|||||||
Reference in New Issue
Block a user