mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-05-19 22:29:04 +01:00
312 lines
9.8 KiB
TypeScript
312 lines
9.8 KiB
TypeScript
// Copyright 2024 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
import React, { type JSX, useId, useMemo, useState } from 'react';
|
|
import { Tabs } from 'radix-ui';
|
|
import type { LocalizerType } from '../types/I18N.std.ts';
|
|
import { AxoDialog } from '../axo/AxoDialog.dom.tsx';
|
|
import { tw } from '../axo/tw.dom.tsx';
|
|
import { useReducedMotion } from '../hooks/useReducedMotion.dom.ts';
|
|
import { AxoIconButton } from '../axo/AxoIconButton.dom.tsx';
|
|
import { AxoButton } from '../axo/AxoButton.dom.tsx';
|
|
|
|
export type SafetyTipsModalProps = Readonly<{
|
|
i18n: LocalizerType;
|
|
onClose(): void;
|
|
}>;
|
|
|
|
export function SafetyTipsModal({
|
|
i18n,
|
|
onClose,
|
|
}: SafetyTipsModalProps): JSX.Element {
|
|
const [page, setPage] = useState<'summary' | 'details'>('summary');
|
|
return (
|
|
<AxoDialog.Root open onOpenChange={onClose}>
|
|
<AxoDialog.Content size="sm" escape="cancel-is-noop">
|
|
{page === 'summary' ? (
|
|
<SafetyTipsSummary
|
|
i18n={i18n}
|
|
onViewMore={() => setPage('details')}
|
|
/>
|
|
) : (
|
|
<SafetyTipsDetails i18n={i18n} />
|
|
)}
|
|
</AxoDialog.Content>
|
|
</AxoDialog.Root>
|
|
);
|
|
}
|
|
|
|
function SafetyTipsSummary({
|
|
i18n,
|
|
onViewMore,
|
|
}: {
|
|
i18n: LocalizerType;
|
|
onViewMore: () => void;
|
|
}): React.JSX.Element {
|
|
const tips = useMemo(
|
|
() =>
|
|
[
|
|
{
|
|
key: 'signal',
|
|
iconUrl: 'images/safety-tips/safety-tip-icon-chat-x.svg',
|
|
titleId: i18n('icu:SafetyTipsModal__TipTitle--SignalChat'),
|
|
descriptionId: i18n(
|
|
'icu:SafetyTipsModal__TipDescription--SignalChat'
|
|
),
|
|
},
|
|
{
|
|
key: 'names-and-photos',
|
|
iconUrl: 'images/safety-tips/safety-tip-icon-person-question.svg',
|
|
titleId: i18n('icu:SafetyTipsModal__TipTitle--NamesAndPhotos'),
|
|
descriptionId: i18n(
|
|
'icu:SafetyTipsModal__TipDescription--NamesAndPhotos'
|
|
),
|
|
},
|
|
{
|
|
key: 'scams',
|
|
iconUrl: 'images/safety-tips/safety-tip-icon-raised-hand.svg',
|
|
titleId: i18n('icu:SafetyTipsModal__TipTitle--Scams'),
|
|
descriptionId: i18n('icu:SafetyTipsModal__TipDescription--Scams'),
|
|
},
|
|
] as const,
|
|
[i18n]
|
|
);
|
|
return (
|
|
<>
|
|
<AxoDialog.Header>
|
|
<div className={tw('sr-only')}>
|
|
<AxoDialog.Description>
|
|
{i18n('icu:SafetyTipsModal__Title-v2')}
|
|
</AxoDialog.Description>
|
|
</div>
|
|
<AxoDialog.Title>
|
|
{i18n('icu:SafetyTipsModal__Title-v2')}
|
|
</AxoDialog.Title>
|
|
<AxoDialog.Close aria-label={i18n('icu:close')} />
|
|
</AxoDialog.Header>
|
|
<AxoDialog.Body>
|
|
<div className={tw('py-4')}>
|
|
<ul className={tw('mb-7 space-y-9')}>
|
|
{tips.map(tip => (
|
|
<li key={tip.key} className={tw('flex items-start gap-4')}>
|
|
<img
|
|
role="presentation"
|
|
alt=""
|
|
className={tw('mt-1 size-11 shrink-0')}
|
|
src={tip.iconUrl}
|
|
/>
|
|
<div>
|
|
<h3 className={tw('type-title-small text-label-primary')}>
|
|
{tip.titleId}
|
|
</h3>
|
|
<p
|
|
className={tw('mt-1 type-body-medium text-label-secondary')}
|
|
>
|
|
{tip.descriptionId}
|
|
</p>
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
<AxoButton.Root
|
|
size="lg"
|
|
variant="secondary"
|
|
width="full"
|
|
onClick={onViewMore}
|
|
>
|
|
{i18n('icu:SafetyTipsModal__Button--ViewMore')}
|
|
</AxoButton.Root>
|
|
</div>
|
|
</AxoDialog.Body>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function SafetyTipsDetails({
|
|
i18n,
|
|
}: {
|
|
i18n: LocalizerType;
|
|
}): React.JSX.Element {
|
|
const cardWrapperId = useId();
|
|
|
|
const tips = useMemo(
|
|
() =>
|
|
[
|
|
{
|
|
key: 'dontRespond',
|
|
title: i18n('icu:SafetyTipsModal__TipTitle--DontRespond'),
|
|
description: i18n('icu:SafetyTipsModal__TipDescription--DontRespond'),
|
|
imageUrl: 'images/safety-tips/safety-tip-dont-respond.svg',
|
|
},
|
|
{
|
|
key: 'reviewNames',
|
|
title: i18n('icu:SafetyTipsModal__TipTitle--ReviewNames'),
|
|
description: i18n('icu:SafetyTipsModal__TipDescription--ReviewNames'),
|
|
imageUrl: 'images/safety-tips/safety-tip-review-names.svg',
|
|
},
|
|
{
|
|
key: 'vague',
|
|
title: i18n('icu:SafetyTipsModal__TipTitle--Vague'),
|
|
description: i18n('icu:SafetyTipsModal__TipDescription--Vague'),
|
|
imageUrl: 'images/safety-tips/safety-tip-vague.svg',
|
|
},
|
|
{
|
|
key: 'links',
|
|
title: i18n('icu:SafetyTipsModal__TipTitle--Links-v2'),
|
|
description: i18n('icu:SafetyTipsModal__TipDescription--Links'),
|
|
imageUrl: 'images/safety-tips/safety-tip-links.svg',
|
|
},
|
|
{
|
|
key: 'crypto',
|
|
title: i18n('icu:SafetyTipsModal__TipTitle--Crypto'),
|
|
description: i18n('icu:SafetyTipsModal__TipDescription--Crypto'),
|
|
imageUrl: 'images/safety-tips/safety-tip-crypto.svg',
|
|
},
|
|
{
|
|
key: 'business',
|
|
title: i18n('icu:SafetyTipsModal__TipTitle--Business'),
|
|
description: i18n('icu:SafetyTipsModal__TipDescription--Business'),
|
|
imageUrl: 'images/safety-tips/safety-tip-business.svg',
|
|
},
|
|
] as const,
|
|
[i18n]
|
|
);
|
|
|
|
function getCardIdForPage(pageIndex: number) {
|
|
// oxlint-disable-next-line typescript/no-non-null-assertion
|
|
return `${cardWrapperId}_${tips[pageIndex]!.key}`;
|
|
}
|
|
|
|
const maxPageIndex = tips.length - 1;
|
|
const [pageIndex, setPageIndex] = useState(0);
|
|
const reducedMotion = useReducedMotion();
|
|
|
|
function scrollToPageIndex(nextPageIndex: number) {
|
|
setPageIndex(nextPageIndex);
|
|
document.getElementById(getCardIdForPage(nextPageIndex))?.scrollIntoView({
|
|
inline: 'center',
|
|
behavior: reducedMotion ? 'instant' : 'smooth',
|
|
});
|
|
}
|
|
|
|
function scrollToPageKey(key: string) {
|
|
const index = tips.findIndex(tip => tip.key === key);
|
|
if (index >= 0) {
|
|
scrollToPageIndex(index);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<AxoDialog.Header>
|
|
<div className={tw('sr-only')}>
|
|
<AxoDialog.Title>
|
|
{i18n('icu:SafetyTipsModal__Title-v2')}
|
|
</AxoDialog.Title>
|
|
</div>
|
|
<AxoDialog.Close aria-label={i18n('icu:close')} />
|
|
</AxoDialog.Header>
|
|
<Tabs.Root
|
|
value={tips[pageIndex]?.key}
|
|
onValueChange={scrollToPageKey}
|
|
role="group"
|
|
aria-label={i18n('icu:SafetyTipsModal__Title-v2')}
|
|
aria-roledescription={i18n('icu:Carousel--Role')}
|
|
>
|
|
<div
|
|
className={tw('flex gap-3 overflow-hidden')}
|
|
id={cardWrapperId}
|
|
aria-live="polite"
|
|
aria-atomic="false"
|
|
>
|
|
{tips.map((page, index) => {
|
|
return (
|
|
<Tabs.Content
|
|
key={page.key}
|
|
id={getCardIdForPage(index)}
|
|
value={page.key}
|
|
forceMount
|
|
className={tw('w-full shrink-0 snap-center px-10')}
|
|
tabIndex={index === pageIndex ? 0 : -1}
|
|
aria-roledescription={i18n('icu:Carousel__Slide--Role')}
|
|
>
|
|
<div className={tw('flex justify-center')}>
|
|
<img
|
|
role="presentation"
|
|
alt=""
|
|
className={tw('w-[#204px]')}
|
|
src={page.imageUrl}
|
|
width={240}
|
|
/>
|
|
</div>
|
|
<h2
|
|
className={tw(
|
|
'mt-2 type-title-small font-semibold text-label-primary'
|
|
)}
|
|
>
|
|
{page.title}
|
|
</h2>
|
|
<p className={tw('mt-1 type-body-medium text-label-secondary')}>
|
|
{page.description}
|
|
</p>
|
|
</Tabs.Content>
|
|
);
|
|
})}
|
|
</div>
|
|
<AxoDialog.Footer>
|
|
<div className={tw('flex w-full items-center justify-between p-1')}>
|
|
<div className={pageIndex === 0 ? tw('invisible') : ''}>
|
|
<AxoIconButton.Root
|
|
variant="secondary"
|
|
iconWeight={300}
|
|
label={i18n('icu:previous')}
|
|
tooltip={false}
|
|
symbol="chevron-[start]"
|
|
disabled={pageIndex === 0}
|
|
size="sm"
|
|
aria-controls={cardWrapperId}
|
|
onClick={() => {
|
|
if (pageIndex === 0) {
|
|
return;
|
|
}
|
|
scrollToPageIndex(pageIndex - 1);
|
|
}}
|
|
/>
|
|
</div>
|
|
<Tabs.List loop={false}>
|
|
{tips.map(page => {
|
|
return (
|
|
<Tabs.Trigger
|
|
key={page.key}
|
|
value={page.key}
|
|
className="SafetyTipsModal__DotsButton"
|
|
aria-label={page.title}
|
|
/>
|
|
);
|
|
})}
|
|
</Tabs.List>
|
|
|
|
<div className={pageIndex === maxPageIndex ? tw('invisible') : ''}>
|
|
<AxoIconButton.Root
|
|
symbol="chevron-[end]"
|
|
variant="secondary"
|
|
tooltip={false}
|
|
iconWeight={300}
|
|
label={i18n('icu:next')}
|
|
disabled={pageIndex === maxPageIndex}
|
|
size="sm"
|
|
aria-controls={cardWrapperId}
|
|
onClick={() => {
|
|
if (pageIndex === maxPageIndex) {
|
|
return;
|
|
}
|
|
scrollToPageIndex(pageIndex + 1);
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</AxoDialog.Footer>
|
|
</Tabs.Root>
|
|
</>
|
|
);
|
|
}
|