mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2025-12-20 02:08:57 +00:00
@@ -6,6 +6,7 @@ import lodash from 'lodash';
|
|||||||
|
|
||||||
import { action } from '@storybook/addon-actions';
|
import { action } from '@storybook/addon-actions';
|
||||||
import type { Meta, StoryFn } from '@storybook/react';
|
import type { Meta, StoryFn } from '@storybook/react';
|
||||||
|
import { tw } from '../../axo/tw.dom.js';
|
||||||
|
|
||||||
import { SignalService } from '../../protobuf/index.std.js';
|
import { SignalService } from '../../protobuf/index.std.js';
|
||||||
import { ConversationColors } from '../../types/Colors.std.js';
|
import { ConversationColors } from '../../types/Colors.std.js';
|
||||||
@@ -43,6 +44,7 @@ import { ThemeType } from '../../types/Util.std.js';
|
|||||||
import { BadgeCategory } from '../../badges/BadgeCategory.std.js';
|
import { BadgeCategory } from '../../badges/BadgeCategory.std.js';
|
||||||
import { PaymentEventKind } from '../../types/Payment.std.js';
|
import { PaymentEventKind } from '../../types/Payment.std.js';
|
||||||
import type { RenderAudioAttachmentProps } from '../../state/smart/renderAudioAttachment.preload.js';
|
import type { RenderAudioAttachmentProps } from '../../state/smart/renderAudioAttachment.preload.js';
|
||||||
|
import type { PollVoteWithUserType } from '../../state/selectors/message.preload.js';
|
||||||
|
|
||||||
const { isBoolean, noop } = lodash;
|
const { isBoolean, noop } = lodash;
|
||||||
|
|
||||||
@@ -314,7 +316,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||||||
saveAttachments: action('saveAttachments'),
|
saveAttachments: action('saveAttachments'),
|
||||||
setQuoteByMessageId: action('setQuoteByMessageId'),
|
setQuoteByMessageId: action('setQuoteByMessageId'),
|
||||||
retryMessageSend: action('retryMessageSend'),
|
retryMessageSend: action('retryMessageSend'),
|
||||||
sendPollVote: action('sendPollVote'),
|
sendPollVote: overrideProps.sendPollVote ?? action('sendPollVote'),
|
||||||
copyMessageText: action('copyMessageText'),
|
copyMessageText: action('copyMessageText'),
|
||||||
retryDeleteForEveryone: action('retryDeleteForEveryone'),
|
retryDeleteForEveryone: action('retryDeleteForEveryone'),
|
||||||
scrollToQuotedMessage: action('scrollToQuotedMessage'),
|
scrollToQuotedMessage: action('scrollToQuotedMessage'),
|
||||||
@@ -1991,7 +1993,99 @@ AudioWithPendingAttachment.args = {
|
|||||||
|
|
||||||
// Poll Messages
|
// Poll Messages
|
||||||
|
|
||||||
function createMockPollWithVotes(
|
function getStableVoter(optionIndex: number, voterIndex: number) {
|
||||||
|
const name = `Voter ${optionIndex * 100 + voterIndex + 1}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
acceptedMessageRequest: true,
|
||||||
|
avatarUrl: undefined,
|
||||||
|
badges: [],
|
||||||
|
color: 'A100' as const,
|
||||||
|
id: `stable-voter-${optionIndex}-${voterIndex}`,
|
||||||
|
isMe: false,
|
||||||
|
name,
|
||||||
|
phoneNumber: undefined,
|
||||||
|
profileName: undefined,
|
||||||
|
sharedGroupNames: [],
|
||||||
|
title: name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMockPollWithVoteCounts(
|
||||||
|
question: string,
|
||||||
|
options: Array<string>,
|
||||||
|
otherVoteCounts: Map<number, number>,
|
||||||
|
allowMultiple: boolean,
|
||||||
|
myVotes: Set<number>,
|
||||||
|
pendingVoteDiff?: Map<number, 'PENDING_VOTE' | 'PENDING_UNVOTE'>
|
||||||
|
) {
|
||||||
|
const votesByOption = new Map<number, Array<PollVoteWithUserType>>();
|
||||||
|
|
||||||
|
let totalNumVotes = 0;
|
||||||
|
const uniqueVoterIds = new Set<string>();
|
||||||
|
|
||||||
|
// Add other voters
|
||||||
|
otherVoteCounts.forEach((count, optionIndex) => {
|
||||||
|
if (count > 0) {
|
||||||
|
const voters = [];
|
||||||
|
for (let i = 0; i < count; i += 1) {
|
||||||
|
const from = getStableVoter(optionIndex, i);
|
||||||
|
uniqueVoterIds.add(from.id);
|
||||||
|
voters.push({
|
||||||
|
optionIndexes: [optionIndex],
|
||||||
|
timestamp: Date.now() - (optionIndex * 100 + i) * 1000,
|
||||||
|
isMe: false,
|
||||||
|
from,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
votesByOption.set(optionIndex, voters);
|
||||||
|
totalNumVotes += count;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add my votes if present
|
||||||
|
if (myVotes.size > 0) {
|
||||||
|
uniqueVoterIds.add('me');
|
||||||
|
const myVoteIndexes = Array.from(myVotes);
|
||||||
|
for (const voteIndex of myVoteIndexes) {
|
||||||
|
const existingVoters = votesByOption.get(voteIndex) ?? [];
|
||||||
|
votesByOption.set(voteIndex, [
|
||||||
|
...existingVoters,
|
||||||
|
{
|
||||||
|
optionIndexes: myVoteIndexes,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
isMe: true,
|
||||||
|
from: {
|
||||||
|
acceptedMessageRequest: true,
|
||||||
|
avatarUrl: undefined,
|
||||||
|
badges: [],
|
||||||
|
color: 'A100' as const,
|
||||||
|
id: 'me',
|
||||||
|
isMe: true,
|
||||||
|
name: 'You',
|
||||||
|
phoneNumber: undefined,
|
||||||
|
profileName: undefined,
|
||||||
|
sharedGroupNames: [],
|
||||||
|
title: 'You',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
totalNumVotes += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
question,
|
||||||
|
options,
|
||||||
|
allowMultiple,
|
||||||
|
votesByOption,
|
||||||
|
totalNumVotes,
|
||||||
|
uniqueVoters: uniqueVoterIds.size,
|
||||||
|
pendingVoteDiff,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMockPollWithVoters(
|
||||||
question: string,
|
question: string,
|
||||||
options: Array<string>,
|
options: Array<string>,
|
||||||
allowMultiple: boolean,
|
allowMultiple: boolean,
|
||||||
@@ -2090,7 +2184,7 @@ PollMultipleChoice.args = {
|
|||||||
export const PollWithVotes = Template.bind({});
|
export const PollWithVotes = Template.bind({});
|
||||||
PollWithVotes.args = {
|
PollWithVotes.args = {
|
||||||
conversationType: 'group',
|
conversationType: 'group',
|
||||||
poll: createMockPollWithVotes(
|
poll: createMockPollWithVoters(
|
||||||
'Best day for the team meeting?',
|
'Best day for the team meeting?',
|
||||||
['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
|
['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
|
||||||
false,
|
false,
|
||||||
@@ -2114,7 +2208,7 @@ PollWithVotes.args = {
|
|||||||
export const PollWithPendingVotes = Template.bind({});
|
export const PollWithPendingVotes = Template.bind({});
|
||||||
PollWithPendingVotes.args = {
|
PollWithPendingVotes.args = {
|
||||||
conversationType: 'group',
|
conversationType: 'group',
|
||||||
poll: createMockPollWithVotes(
|
poll: createMockPollWithVoters(
|
||||||
'Best day for the team meeting?',
|
'Best day for the team meeting?',
|
||||||
['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
|
['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
|
||||||
false,
|
false,
|
||||||
@@ -2142,7 +2236,7 @@ PollWithPendingVotes.args = {
|
|||||||
export const PollTerminated = Template.bind({});
|
export const PollTerminated = Template.bind({});
|
||||||
PollTerminated.args = {
|
PollTerminated.args = {
|
||||||
conversationType: 'group',
|
conversationType: 'group',
|
||||||
poll: createMockPollWithVotes(
|
poll: createMockPollWithVoters(
|
||||||
'Quick poll: Coffee or tea?',
|
'Quick poll: Coffee or tea?',
|
||||||
['Coffee ☕', 'Tea 🍵'],
|
['Coffee ☕', 'Tea 🍵'],
|
||||||
false,
|
false,
|
||||||
@@ -2168,7 +2262,7 @@ PollTerminated.args = {
|
|||||||
export const PollLongText = Template.bind({});
|
export const PollLongText = Template.bind({});
|
||||||
PollLongText.args = {
|
PollLongText.args = {
|
||||||
conversationType: 'group',
|
conversationType: 'group',
|
||||||
poll: createMockPollWithVotes(
|
poll: createMockPollWithVoters(
|
||||||
'Given the current situation with remote work becoming more prevalent, what would be your preferred working arrangement for the future once everything stabilizes?',
|
'Given the current situation with remote work becoming more prevalent, what would be your preferred working arrangement for the future once everything stabilizes?',
|
||||||
[
|
[
|
||||||
'Fully remote with no requirement to come to office except for special team events or emergencies', // 96 chars
|
'Fully remote with no requirement to come to office except for special team events or emergencies', // 96 chars
|
||||||
@@ -2193,7 +2287,7 @@ PollLongText.args = {
|
|||||||
export const PollMultipleChoiceWithVotes = Template.bind({});
|
export const PollMultipleChoiceWithVotes = Template.bind({});
|
||||||
PollMultipleChoiceWithVotes.args = {
|
PollMultipleChoiceWithVotes.args = {
|
||||||
conversationType: 'group',
|
conversationType: 'group',
|
||||||
poll: createMockPollWithVotes(
|
poll: createMockPollWithVoters(
|
||||||
'Which toppings do you want on the pizza?',
|
'Which toppings do you want on the pizza?',
|
||||||
[
|
[
|
||||||
'Pepperoni',
|
'Pepperoni',
|
||||||
@@ -2215,6 +2309,178 @@ PollMultipleChoiceWithVotes.args = {
|
|||||||
status: 'read',
|
status: 'read',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const POLL_ANIMATION_OPTIONS = ['Pizza', 'Sushi', 'Tacos', 'Salad'];
|
||||||
|
const BAD_NETWORK_DELAY_MS = 5000;
|
||||||
|
|
||||||
|
export function PollAnimationPlayground(): JSX.Element {
|
||||||
|
const [otherVoteCounts, setOtherVoteCounts] = React.useState<
|
||||||
|
Map<number, number>
|
||||||
|
>(() => new Map(POLL_ANIMATION_OPTIONS.map((_, i) => [i, 0])));
|
||||||
|
|
||||||
|
const [myVotes, setMyVotes] = React.useState<Set<number>>(() => new Set());
|
||||||
|
|
||||||
|
// Pending state for my vote (only used with bad network)
|
||||||
|
const [pendingVoteDiff, setPendingVoteDiff] = React.useState<
|
||||||
|
Map<number, 'PENDING_VOTE' | 'PENDING_UNVOTE'>
|
||||||
|
>(() => new Map());
|
||||||
|
|
||||||
|
const [badNetwork, setBadNetwork] = React.useState(false);
|
||||||
|
const [allowMultiple, setAllowMultiple] = React.useState(false);
|
||||||
|
|
||||||
|
const handleSendPollVote = React.useCallback(
|
||||||
|
(params: { messageId: string; optionIndexes: ReadonlyArray<number> }) => {
|
||||||
|
const newVotes = new Set(params.optionIndexes);
|
||||||
|
|
||||||
|
if (badNetwork) {
|
||||||
|
// Calculate pending diff: difference between committed state (myVotes)
|
||||||
|
// and requested state (newVotes)
|
||||||
|
const nextPendingDiff = new Map<
|
||||||
|
number,
|
||||||
|
'PENDING_VOTE' | 'PENDING_UNVOTE'
|
||||||
|
>();
|
||||||
|
// Options being added (in newVotes but not in myVotes)
|
||||||
|
for (const idx of newVotes) {
|
||||||
|
if (!myVotes.has(idx)) {
|
||||||
|
nextPendingDiff.set(idx, 'PENDING_VOTE');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Options being removed (in myVotes but not in newVotes)
|
||||||
|
for (const idx of myVotes) {
|
||||||
|
if (!newVotes.has(idx)) {
|
||||||
|
nextPendingDiff.set(idx, 'PENDING_UNVOTE');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setPendingVoteDiff(nextPendingDiff);
|
||||||
|
|
||||||
|
// After delay, clear pending and apply votes
|
||||||
|
setTimeout(() => {
|
||||||
|
setPendingVoteDiff(new Map());
|
||||||
|
setMyVotes(newVotes);
|
||||||
|
}, BAD_NETWORK_DELAY_MS);
|
||||||
|
} else {
|
||||||
|
setMyVotes(newVotes);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[badNetwork, myVotes]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Increment other voters (instant, no pending)
|
||||||
|
const incrementOther = (index: number) => {
|
||||||
|
setOtherVoteCounts(prev => {
|
||||||
|
const next = new Map(prev);
|
||||||
|
next.set(index, (prev.get(index) ?? 0) + 1);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Decrement other voters (instant, no pending)
|
||||||
|
const decrementOther = (index: number) => {
|
||||||
|
setOtherVoteCounts(prev => {
|
||||||
|
const next = new Map(prev);
|
||||||
|
const current = prev.get(index) ?? 0;
|
||||||
|
if (current > 0) {
|
||||||
|
next.set(index, current - 1);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
setOtherVoteCounts(new Map(POLL_ANIMATION_OPTIONS.map((_, i) => [i, 0])));
|
||||||
|
setMyVotes(new Set());
|
||||||
|
setPendingVoteDiff(new Map());
|
||||||
|
};
|
||||||
|
|
||||||
|
const poll = createMockPollWithVoteCounts(
|
||||||
|
'What should we have for lunch?',
|
||||||
|
POLL_ANIMATION_OPTIONS,
|
||||||
|
otherVoteCounts,
|
||||||
|
allowMultiple,
|
||||||
|
myVotes,
|
||||||
|
pendingVoteDiff.size > 0 ? pendingVoteDiff : undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
const props = createProps({
|
||||||
|
conversationType: 'group',
|
||||||
|
poll,
|
||||||
|
status: 'sent',
|
||||||
|
sendPollVote: handleSendPollVote,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<TimelineMessage {...props} />
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={tw(
|
||||||
|
'mt-6 max-w-[300px] rounded-lg border border-solid border-label-primary p-4'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<label className={tw('mb-2 flex cursor-pointer items-center gap-2')}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={allowMultiple}
|
||||||
|
onChange={e => {
|
||||||
|
setAllowMultiple(e.target.checked);
|
||||||
|
// Clear votes when changing mode to avoid invalid state
|
||||||
|
setMyVotes(new Set());
|
||||||
|
setPendingVoteDiff(new Map());
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span>Allow Multiple Votes</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className={tw('mb-4 flex cursor-pointer items-center gap-2')}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={badNetwork}
|
||||||
|
onChange={e => setBadNetwork(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<span>Bad Network (5s delay for your vote)</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div className={tw('mb-3 type-body-medium font-semibold')}>
|
||||||
|
Other Voters
|
||||||
|
</div>
|
||||||
|
{POLL_ANIMATION_OPTIONS.map((option, index) => (
|
||||||
|
<div
|
||||||
|
key={option}
|
||||||
|
className={tw('mb-2 flex items-center justify-between')}
|
||||||
|
>
|
||||||
|
<span className={tw('flex-1')}>{option}</span>
|
||||||
|
<div className={tw('flex items-center gap-2')}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => decrementOther(index)}
|
||||||
|
className={tw('size-7 cursor-pointer type-body-large')}
|
||||||
|
>
|
||||||
|
-
|
||||||
|
</button>
|
||||||
|
<span className={tw('min-w-5 text-center')}>
|
||||||
|
{otherVoteCounts.get(index) ?? 0}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => incrementOther(index)}
|
||||||
|
className={tw('size-7 cursor-pointer type-body-large')}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={reset}
|
||||||
|
className={tw('mt-2 cursor-pointer px-3 py-1.5')}
|
||||||
|
>
|
||||||
|
Reset All
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export const OtherFileType = Template.bind({});
|
export const OtherFileType = Template.bind({});
|
||||||
OtherFileType.args = {
|
OtherFileType.args = {
|
||||||
attachments: [
|
attachments: [
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import React, { memo, useState, useEffect, useRef } from 'react';
|
import React, { memo, useState, useEffect, useRef } from 'react';
|
||||||
import { Checkbox } from 'radix-ui';
|
import { Checkbox } from 'radix-ui';
|
||||||
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { type TailwindStyles, tw } from '../../../axo/tw.dom.js';
|
import { type TailwindStyles, tw } from '../../../axo/tw.dom.js';
|
||||||
import { AxoButton } from '../../../axo/AxoButton.dom.js';
|
import { AxoButton } from '../../../axo/AxoButton.dom.js';
|
||||||
import { AxoSymbol } from '../../../axo/AxoSymbol.dom.js';
|
import { AxoSymbol } from '../../../axo/AxoSymbol.dom.js';
|
||||||
@@ -83,20 +84,28 @@ const PollCheckbox = memo((props: PollCheckboxProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isPending ? (
|
<AnimatePresence>
|
||||||
<div className={tw('pointer-events-none absolute')}>
|
{isPending && (
|
||||||
<SpinnerV2
|
<motion.div
|
||||||
value="indeterminate"
|
initial={{ opacity: 0 }}
|
||||||
size={24}
|
animate={{ opacity: 1 }}
|
||||||
strokeWidth={1.5}
|
exit={{ opacity: 0 }}
|
||||||
marginRatio={1}
|
transition={{ duration: 0.25 }}
|
||||||
variant={{
|
className={tw('pointer-events-none absolute')}
|
||||||
bg: tw('stroke-none'),
|
>
|
||||||
fg: strokeColor,
|
<SpinnerV2
|
||||||
}}
|
value="indeterminate"
|
||||||
/>
|
size={24}
|
||||||
</div>
|
strokeWidth={1.5}
|
||||||
) : null}
|
marginRatio={1}
|
||||||
|
variant={{
|
||||||
|
bg: tw('stroke-none'),
|
||||||
|
fg: strokeColor,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
<Checkbox.Root
|
<Checkbox.Root
|
||||||
checked={props.checked}
|
checked={props.checked}
|
||||||
onCheckedChange={props.onCheckedChange}
|
onCheckedChange={props.onCheckedChange}
|
||||||
@@ -105,14 +114,25 @@ const PollCheckbox = memo((props: PollCheckboxProps) => {
|
|||||||
isPending ? '' : 'border-[1.5px]',
|
isPending ? '' : 'border-[1.5px]',
|
||||||
'outline-0 outline-border-focused focused:outline-[2.5px]',
|
'outline-0 outline-border-focused focused:outline-[2.5px]',
|
||||||
'overflow-hidden',
|
'overflow-hidden',
|
||||||
|
'transition-colors duration-250',
|
||||||
bgColor,
|
bgColor,
|
||||||
borderColor
|
borderColor
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Checkbox.Indicator
|
<Checkbox.Indicator forceMount asChild>
|
||||||
className={tw(checkmarkColor, 'flex items-center justify-center')}
|
<motion.div
|
||||||
>
|
initial={false}
|
||||||
<AxoSymbol.Icon symbol="check" size={16} label={null} />
|
animate={{ opacity: checked ? 1 : 0 }}
|
||||||
|
transition={{ duration: 0.25 }}
|
||||||
|
className={tw(
|
||||||
|
checkmarkColor,
|
||||||
|
'flex items-center justify-center',
|
||||||
|
// Animate color so it doesn't instantly switch to grey during fade-out
|
||||||
|
'transition-colors duration-250'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<AxoSymbol.Icon symbol="check" size={16} label={null} />
|
||||||
|
</motion.div>
|
||||||
</Checkbox.Indicator>
|
</Checkbox.Indicator>
|
||||||
</Checkbox.Root>
|
</Checkbox.Root>
|
||||||
</>
|
</>
|
||||||
@@ -281,24 +301,28 @@ export function PollMessageContents({
|
|||||||
<span className={tw('min-w-0 type-body-large break-words')}>
|
<span className={tw('min-w-0 type-body-large break-words')}>
|
||||||
<UserText text={option} />
|
<UserText text={option} />
|
||||||
</span>
|
</span>
|
||||||
{totalVotes > 0 && (
|
<div
|
||||||
<div className={tw('flex shrink-0 items-center gap-1')}>
|
className={tw(
|
||||||
{poll.terminatedAt != null && weVotedForThis && (
|
'flex shrink-0 items-center gap-1',
|
||||||
<VotedCheckmark isIncoming={isIncoming} i18n={i18n} />
|
'transition-opacity duration-250',
|
||||||
|
totalVotes > 0 ? 'opacity-100' : 'invisible opacity-0'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{poll.terminatedAt != null && weVotedForThis && (
|
||||||
|
<VotedCheckmark isIncoming={isIncoming} i18n={i18n} />
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
className={tw(
|
||||||
|
'type-body-medium',
|
||||||
|
isIncoming
|
||||||
|
? 'text-label-secondary'
|
||||||
|
: 'text-label-secondary-on-color'
|
||||||
)}
|
)}
|
||||||
<span
|
data-testid={`poll-option-${index}-votes-${optionVotes}`}
|
||||||
className={tw(
|
>
|
||||||
'type-body-medium',
|
{optionVotes}
|
||||||
isIncoming
|
</span>
|
||||||
? 'text-label-secondary'
|
</div>
|
||||||
: 'text-label-secondary-on-color'
|
|
||||||
)}
|
|
||||||
data-testid={`poll-option-${index}-votes-${optionVotes}`}
|
|
||||||
>
|
|
||||||
{optionVotes}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -314,17 +338,16 @@ export function PollMessageContents({
|
|||||||
: 'bg-message-fill-outgoing-tertiary'
|
: 'bg-message-fill-outgoing-tertiary'
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{percentage > 0 && (
|
<div
|
||||||
<div
|
className={tw(
|
||||||
className={tw(
|
'absolute inset-y-0 start-0 rounded-s-full',
|
||||||
'absolute inset-y-0 start-0 rounded-s-full',
|
'transition-[width] duration-250 motion-reduce:transition-none',
|
||||||
isIncoming
|
isIncoming
|
||||||
? 'bg-color-fill-primary'
|
? 'bg-color-fill-primary'
|
||||||
: 'bg-label-primary-on-color'
|
: 'bg-label-primary-on-color'
|
||||||
)}
|
)}
|
||||||
style={{ width: `${percentage}%` }}
|
style={{ width: `${percentage}%` }}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -332,26 +355,45 @@ export function PollMessageContents({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{totalVotes > 0 ? (
|
<div className={tw('mt-4 flex h-10 items-center justify-center')}>
|
||||||
<div className={tw('mt-4 flex justify-center scheme-light')}>
|
<AnimatePresence exitBeforeEnter initial={false}>
|
||||||
<AxoButton.Root
|
{totalVotes > 0 ? (
|
||||||
size="md"
|
<motion.div
|
||||||
variant="floating-secondary"
|
key="view-votes"
|
||||||
onClick={() => setShowVotesModal(true)}
|
initial={{ opacity: 0 }}
|
||||||
>
|
animate={{
|
||||||
{i18n('icu:PollMessage__ViewVotesButton')}
|
opacity: 1,
|
||||||
</AxoButton.Root>
|
transition: { duration: 0.25, ease: 'easeOut' },
|
||||||
</div>
|
}}
|
||||||
) : (
|
exit={{
|
||||||
<div
|
opacity: 0,
|
||||||
className={tw(
|
transition: { duration: 0.25, ease: 'easeIn' },
|
||||||
'mt-4 text-center type-body-medium',
|
}}
|
||||||
isIncoming ? 'text-label-primary' : 'text-label-primary-on-color'
|
className={tw('scheme-light')}
|
||||||
|
>
|
||||||
|
<AxoButton.Root
|
||||||
|
size="md"
|
||||||
|
variant="floating-secondary"
|
||||||
|
onClick={() => setShowVotesModal(true)}
|
||||||
|
>
|
||||||
|
{i18n('icu:PollMessage__ViewVotesButton')}
|
||||||
|
</AxoButton.Root>
|
||||||
|
</motion.div>
|
||||||
|
) : (
|
||||||
|
<motion.div
|
||||||
|
key="no-votes"
|
||||||
|
className={tw(
|
||||||
|
'type-body-medium',
|
||||||
|
isIncoming
|
||||||
|
? 'text-label-primary'
|
||||||
|
: 'text-label-primary-on-color'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{i18n('icu:PollVotesModal__noVotes')}
|
||||||
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
>
|
</AnimatePresence>
|
||||||
{i18n('icu:PollVotesModal__noVotes')}
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showVotesModal && (
|
{showVotesModal && (
|
||||||
<PollVotesModal
|
<PollVotesModal
|
||||||
|
|||||||
Reference in New Issue
Block a user