Polls UI Enhancements

This commit is contained in:
yash-signal
2025-11-18 14:34:22 -06:00
committed by GitHub
parent ad503717fa
commit 3592bbf9f2
4 changed files with 98 additions and 43 deletions

View File

@@ -1518,6 +1518,10 @@
"messageformat": "No votes",
"description": "Message shown when poll has no votes"
},
"icu:PollVotesModal__winningOption": {
"messageformat": "Winning",
"description": "Accessibility label for the star icon indicating the winning option in a poll"
},
"icu:Toast--PinnedMessageNotFound": {
"messageformat": "Pinned message not found",
"description": "Toast shown when user tries to view a pinned message that no longer exists"

View File

@@ -6,6 +6,8 @@ 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';
import { UserText } from '../UserText.dom.js';
import { I18n } from '../I18n.dom.js';
export type PropsType = {
sender: ConversationType;
@@ -24,17 +26,29 @@ export function PollTerminateNotification({
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);
};
const message = sender.isMe ? (
<I18n
i18n={i18n}
id="icu:PollTerminate--you"
components={{
poll: <UserText text={pollQuestion} />,
}}
/>
) : (
<I18n
i18n={i18n}
id="icu:PollTerminate--other"
components={{
name: <UserText text={sender.title} />,
poll: <UserText text={pollQuestion} />,
}}
/>
);
return (
<SystemMessage
symbol="poll"

View File

@@ -12,6 +12,7 @@ import type { LocalizerType } from '../../../types/Util.std.js';
import { PollVotesModal } from './PollVotesModal.dom.js';
import { SpinnerV2 } from '../../SpinnerV2.dom.js';
import { usePrevious } from '../../../hooks/usePrevious.std.js';
import { UserText } from '../../UserText.dom.js';
function VotedCheckmark({
isIncoming,
@@ -224,11 +225,13 @@ export function PollMessageContents({
'text-start break-words whitespace-pre-wrap',
'type-body-large',
isIncoming ? 'text-label-primary' : 'text-label-primary-on-color',
'min-w-[275px]',
'w-[275px] max-w-full',
'mt-1'
)}
>
<div className={tw('mb-1 font-semibold')}>{poll.question}</div>
<div className={tw('mb-1 font-semibold')}>
<UserText text={poll.question} />
</div>
<div
className={tw(
@@ -273,9 +276,11 @@ export function PollMessageContents({
</div>
)}
<div className={tw('flex flex-1 flex-col gap-1')}>
<div className={tw('flex min-w-0 flex-1 flex-col gap-1')}>
<div className={tw('flex items-start justify-between gap-3')}>
<span className={tw('type-body-large')}>{option}</span>
<span className={tw('min-w-0 type-body-large break-words')}>
<UserText text={option} />
</span>
{totalVotes > 0 && (
<div className={tw('flex shrink-0 items-center gap-1')}>
{poll.terminatedAt != null && weVotedForThis && (

View File

@@ -1,12 +1,14 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import React, { useMemo } from 'react';
import { tw } from '../../../axo/tw.dom.js';
import { AxoButton } from '../../../axo/AxoButton.dom.js';
import { AxoSymbol } from '../../../axo/AxoSymbol.dom.js';
import { Modal } from '../../Modal.dom.js';
import { Avatar, AvatarSize } from '../../Avatar.dom.js';
import { ContactName } from '../ContactName.dom.js';
import { UserText } from '../../UserText.dom.js';
import type { LocalizerType } from '../../../types/Util.std.js';
import type {
PollVoteWithUserType,
@@ -30,6 +32,12 @@ export function PollVotesModal({
canEndPoll,
messageId,
}: PollVotesModalProps): JSX.Element {
const maxVoteCount = useMemo(() => {
return poll.votesByOption.values().reduce((max, voters) => {
return Math.max(max, voters.length);
}, 0);
}, [poll.votesByOption]);
return (
<Modal
modalName="PollVotesModal"
@@ -45,13 +53,20 @@ export function PollVotesModal({
{i18n('icu:PollVotesModal__questionLabel')}
</div>
<div className={tw('type-body-large')}>{poll.question}</div>
<div
className={tw(
'rounded-md bg-fill-secondary px-3 py-1 type-body-large'
)}
>
<UserText text={poll.question} />
</div>
</div>
{poll.options.map((option, index, array) => {
const voters = poll.votesByOption.get(index) || [];
const optionKey = `option-${index}`;
const isLastOption = index === array.length - 1;
const isWinning = voters.length > 0 && voters.length === maxVoteCount;
return (
<React.Fragment key={optionKey}>
@@ -62,17 +77,33 @@ export function PollVotesModal({
'mb-3 flex items-start gap-3 text-label-primary'
)}
>
<div className={tw('type-title-small')}>{option}</div>
<div className={tw('type-title-small')}>
<UserText text={option} />
</div>
<div
className={tw('ms-auto mt-[2px] shrink-0 type-body-medium')}
className={tw(
'ms-auto mt-[2px] flex shrink-0 items-center gap-1 type-body-medium'
)}
>
{i18n('icu:PollVotesModal__voteCount', {
{isWinning && (
<AxoSymbol.InlineGlyph
symbol="star-fill"
label={i18n('icu:PollVotesModal__winningOption')}
/>
)}
{voters.length > 0 &&
i18n('icu:PollVotesModal__voteCount', {
count: voters.length,
})}
</div>
</div>
{/* Voters List */}
{voters.length === 0 ? (
<div className={tw('type-body-medium text-label-secondary')}>
{i18n('icu:PollVotesModal__noVotes')}
</div>
) : (
<div className={tw('flex flex-col gap-4')}>
{voters.map((vote: PollVoteWithUserType) => (
<div
@@ -101,6 +132,7 @@ export function PollVotesModal({
</div>
))}
</div>
)}
</div>
{!isLastOption && (
<hr className={tw('border-t-[0.5px] border-label-secondary')} />