diff --git a/_locales/en/messages.json b/_locales/en/messages.json
index 0adeaaf804..a7cfc70140 100644
--- a/_locales/en/messages.json
+++ b/_locales/en/messages.json
@@ -1462,6 +1462,10 @@
"messageformat": "Info",
"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": {
"messageformat": "Poll ยท Select one",
"description": "Status text for single-choice poll where user can select one option"
@@ -1482,6 +1486,18 @@
"messageformat": "You voted",
"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": {
"messageformat": "Poll details",
"description": "Modal title for viewing poll votes and who voted on which option"
@@ -1498,6 +1514,10 @@
"messageformat": "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": {
"messageformat": "New poll",
"description": "Title for the modal to create a new poll"
diff --git a/images/icons/v3/stop/stop-circle.svg b/images/icons/v3/stop/stop-circle.svg
new file mode 100644
index 0000000000..2e6a8c30f1
--- /dev/null
+++ b/images/icons/v3/stop/stop-circle.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss
index c363dcf905..9ff9e02c53 100644
--- a/stylesheets/_modules.scss
+++ b/stylesheets/_modules.scss
@@ -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 {
@include mixins.light-theme {
@include mixins.color-svg(
diff --git a/stylesheets/components/SystemMessage.scss b/stylesheets/components/SystemMessage.scss
index 384dab6aaf..72f8fa7a03 100644
--- a/stylesheets/components/SystemMessage.scss
+++ b/stylesheets/components/SystemMessage.scss
@@ -53,7 +53,7 @@
}
}
- &::before {
+ &--has-icon::before {
content: '';
display: inline-block;
height: 16px;
diff --git a/ts/components/EditHistoryMessagesModal.dom.tsx b/ts/components/EditHistoryMessagesModal.dom.tsx
index 64eca22867..348d2bf398 100644
--- a/ts/components/EditHistoryMessagesModal.dom.tsx
+++ b/ts/components/EditHistoryMessagesModal.dom.tsx
@@ -52,6 +52,7 @@ const MESSAGE_DEFAULT_PROPS = {
previews: [],
retryMessageSend: shouldNeverBeCalled,
sendPollVote: shouldNeverBeCalled,
+ endPoll: shouldNeverBeCalled,
pushPanelForConversation: shouldNeverBeCalled,
renderAudioAttachment: () =>
,
renderingContext: 'EditHistoryMessagesModal',
diff --git a/ts/components/StoryViewsNRepliesModal.dom.tsx b/ts/components/StoryViewsNRepliesModal.dom.tsx
index 07336164f1..2120a53087 100644
--- a/ts/components/StoryViewsNRepliesModal.dom.tsx
+++ b/ts/components/StoryViewsNRepliesModal.dom.tsx
@@ -66,6 +66,7 @@ const MESSAGE_DEFAULT_PROPS = {
previews: [],
retryMessageSend: shouldNeverBeCalled,
sendPollVote: shouldNeverBeCalled,
+ endPoll: shouldNeverBeCalled,
pushPanelForConversation: shouldNeverBeCalled,
renderAudioAttachment: () => ,
saveAttachment: shouldNeverBeCalled,
diff --git a/ts/components/ToastManager.dom.stories.tsx b/ts/components/ToastManager.dom.stories.tsx
index 912375683b..a80d459788 100644
--- a/ts/components/ToastManager.dom.stories.tsx
+++ b/ts/components/ToastManager.dom.stories.tsx
@@ -189,6 +189,8 @@ function getToast(toastType: ToastType): AnyToast {
return { toastType: ToastType.OriginalMessageNotFound };
case ToastType.PinnedConversationsFull:
return { toastType: ToastType.PinnedConversationsFull };
+ case ToastType.PollNotFound:
+ return { toastType: ToastType.PollNotFound };
case ToastType.ReactionFailed:
return { toastType: ToastType.ReactionFailed };
case ToastType.ReceiptSaved:
diff --git a/ts/components/ToastManager.dom.tsx b/ts/components/ToastManager.dom.tsx
index bdc03ade2e..c2d7a1c4a8 100644
--- a/ts/components/ToastManager.dom.tsx
+++ b/ts/components/ToastManager.dom.tsx
@@ -652,6 +652,10 @@ export function renderToast({
);
}
+ if (toastType === ToastType.PollNotFound) {
+ return {i18n('icu:Toast--PollNotFound')};
+ }
+
if (toastType === ToastType._InternalMainProcessLoggingError) {
return (
= React.memo(
onEdit={undefined}
onReplyToMessage={undefined}
onReact={undefined}
+ onEndPoll={undefined}
onRetryMessageSend={undefined}
onRetryDeleteForEveryone={undefined}
onCopy={undefined}
diff --git a/ts/components/conversation/Message.dom.tsx b/ts/components/conversation/Message.dom.tsx
index fd444647f8..cd7f12dc7a 100644
--- a/ts/components/conversation/Message.dom.tsx
+++ b/ts/components/conversation/Message.dom.tsx
@@ -249,6 +249,7 @@ export type PropsData = {
isSelectMode: boolean;
isSMS: boolean;
isSpoilerExpanded?: Record;
+ canEndPoll?: boolean;
direction: DirectionType;
timestamp: number;
receivedAtMS?: number;
@@ -363,6 +364,7 @@ export type PropsActions = {
messageId: string;
optionIndexes: ReadonlyArray;
}) => void;
+ endPoll: (messageId: string) => void;
showContactModal: (contactId: string, conversationId?: string) => void;
showSpoiler: (messageId: string, data: Record) => void;
@@ -2023,7 +2025,7 @@ export class Message extends React.PureComponent {
}
public renderPoll(): JSX.Element | null {
- const { poll, direction, i18n, id } = this.props;
+ const { poll, direction, i18n, id, endPoll, canEndPoll } = this.props;
if (!poll || !isPollReceiveEnabled()) {
return null;
}
@@ -2034,6 +2036,8 @@ export class Message extends React.PureComponent {
i18n={i18n}
messageId={id}
sendPollVote={this.props.sendPollVote}
+ endPoll={endPoll}
+ canEndPoll={canEndPoll}
/>
);
}
diff --git a/ts/components/conversation/MessageContextMenu.dom.tsx b/ts/components/conversation/MessageContextMenu.dom.tsx
index 66b78c6858..adf1c5f459 100644
--- a/ts/components/conversation/MessageContextMenu.dom.tsx
+++ b/ts/components/conversation/MessageContextMenu.dom.tsx
@@ -22,6 +22,7 @@ type MessageContextProps = {
onEdit: (() => void) | undefined;
onReplyToMessage: (() => void) | undefined;
onReact: (() => void) | undefined;
+ onEndPoll: (() => void) | undefined;
onRetryMessageSend: (() => void) | undefined;
onRetryDeleteForEveryone: (() => void) | undefined;
onCopy: (() => void) | undefined;
@@ -39,6 +40,7 @@ export const MessageContextMenu = ({
onEdit,
onReplyToMessage,
onReact,
+ onEndPoll,
onMoreInfo,
onCopy,
onSelect,
@@ -103,6 +105,22 @@ export const MessageContextMenu = ({
)}
>
)}
+ {onEndPoll && (
+
+ )}
{onForward && (