Username Education
Co-authored-by: Jamie Kyle <jamie@signal.org>
This commit is contained in:
parent
c6a7637513
commit
7dc11c1928
100 changed files with 1443 additions and 1269 deletions
|
@ -29,6 +29,7 @@ import { ReceiptType } from './types/Receipt';
|
|||
import { SocketStatus } from './types/SocketStatus';
|
||||
import { DEFAULT_CONVERSATION_COLOR } from './types/Colors';
|
||||
import { ThemeType } from './types/Util';
|
||||
import { ToastType } from './types/Toast';
|
||||
import { ChallengeHandler } from './challenge';
|
||||
import * as durations from './util/durations';
|
||||
import { drop } from './util/drop';
|
||||
|
@ -143,9 +144,6 @@ import { normalizeAci } from './util/normalizeAci';
|
|||
import * as log from './logging/log';
|
||||
import { loadRecentEmojis } from './util/loadRecentEmojis';
|
||||
import { deleteAllLogs } from './util/deleteAllLogs';
|
||||
import { ToastCaptchaFailed } from './components/ToastCaptchaFailed';
|
||||
import { ToastCaptchaSolved } from './components/ToastCaptchaSolved';
|
||||
import { showToast } from './util/showToast';
|
||||
import { startInteractionMode } from './services/InteractionMode';
|
||||
import type { MainWindowStatsType } from './windows/context';
|
||||
import { ReactionSource } from './reactions/ReactionSource';
|
||||
|
@ -278,11 +276,15 @@ export async function startApp(): Promise<void> {
|
|||
onChallengeFailed() {
|
||||
// TODO: DESKTOP-1530
|
||||
// Display humanized `retryAfter`
|
||||
showToast(ToastCaptchaFailed);
|
||||
window.reduxActions.toast.showToast({
|
||||
toastType: ToastType.CaptchaFailed,
|
||||
});
|
||||
},
|
||||
|
||||
onChallengeSolved() {
|
||||
showToast(ToastCaptchaSolved);
|
||||
window.reduxActions.toast.showToast({
|
||||
toastType: ToastType.CaptchaSolved,
|
||||
});
|
||||
},
|
||||
|
||||
setChallengeStatus(challengeStatus) {
|
||||
|
|
|
@ -5,15 +5,12 @@ import React, { useEffect } from 'react';
|
|||
import { Globals } from '@react-spring/web';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import type { AnyToast } from '../types/Toast';
|
||||
import type { ViewStoryActionCreatorType } from '../state/ducks/stories';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import type { VerificationTransport } from '../types/VerificationTransport';
|
||||
import { ThemeType } from '../types/Util';
|
||||
import { AppViewType } from '../state/ducks/app';
|
||||
import { SmartInstallScreen } from '../state/smart/InstallScreen';
|
||||
import { StandaloneRegistration } from './StandaloneRegistration';
|
||||
import { ToastManager } from './ToastManager';
|
||||
import { usePageVisibility } from '../hooks/usePageVisibility';
|
||||
import { useReducedMotion } from '../hooks/useReducedMotion';
|
||||
|
||||
|
@ -27,7 +24,6 @@ type PropsType = {
|
|||
) => Promise<void>;
|
||||
renderCallManager: () => JSX.Element;
|
||||
renderGlobalModalContainer: () => JSX.Element;
|
||||
i18n: LocalizerType;
|
||||
hasSelectedStoryData: boolean;
|
||||
renderStoryViewer: (closeView: () => unknown) => JSX.Element;
|
||||
renderLightbox: () => JSX.Element | null;
|
||||
|
@ -39,13 +35,8 @@ type PropsType = {
|
|||
theme: ThemeType;
|
||||
isMaximized: boolean;
|
||||
isFullScreen: boolean;
|
||||
onUndoArchive: (conversationId: string) => unknown;
|
||||
openFileInFolder: (target: string) => unknown;
|
||||
OS: string;
|
||||
osClassName: string;
|
||||
|
||||
hideToast: () => unknown;
|
||||
toast?: AnyToast;
|
||||
scrollToMessage: (conversationId: string, messageId: string) => unknown;
|
||||
viewStory: ViewStoryActionCreatorType;
|
||||
renderInbox: () => JSX.Element;
|
||||
|
@ -54,14 +45,9 @@ type PropsType = {
|
|||
export function App({
|
||||
appView,
|
||||
hasSelectedStoryData,
|
||||
hideToast,
|
||||
i18n,
|
||||
isFullScreen,
|
||||
isMaximized,
|
||||
onUndoArchive,
|
||||
openFileInFolder,
|
||||
openInbox,
|
||||
OS,
|
||||
osClassName,
|
||||
registerSingleDevice,
|
||||
renderCallManager,
|
||||
|
@ -71,7 +57,6 @@ export function App({
|
|||
renderStoryViewer,
|
||||
requestVerification,
|
||||
theme,
|
||||
toast,
|
||||
viewStory,
|
||||
}: PropsType): JSX.Element {
|
||||
let contents;
|
||||
|
@ -139,14 +124,6 @@ export function App({
|
|||
})}
|
||||
>
|
||||
{contents}
|
||||
<ToastManager
|
||||
OS={OS}
|
||||
hideToast={hideToast}
|
||||
i18n={i18n}
|
||||
onUndoArchive={onUndoArchive}
|
||||
openFileInFolder={openFileInFolder}
|
||||
toast={toast}
|
||||
/>
|
||||
{renderGlobalModalContainer()}
|
||||
{renderCallManager()}
|
||||
{renderLightbox()}
|
||||
|
|
|
@ -17,6 +17,7 @@ import type { ActiveCallStateType } from '../state/ducks/calling';
|
|||
import { ContextMenu } from './ContextMenu';
|
||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||
import type { UnreadStats } from '../util/countUnreadStats';
|
||||
import type { WidthBreakpoint } from './_util';
|
||||
|
||||
enum CallsTabSidebarView {
|
||||
CallsListView,
|
||||
|
@ -50,6 +51,9 @@ type CallsTabProps = Readonly<{
|
|||
conversationId: string,
|
||||
callHistoryGroup: CallHistoryGroup | null
|
||||
) => JSX.Element;
|
||||
renderToastManager: (_: {
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
}) => JSX.Element;
|
||||
regionCode: string | undefined;
|
||||
savePreferredLeftPaneWidth: (preferredLeftPaneWidth: number) => void;
|
||||
}>;
|
||||
|
@ -73,6 +77,7 @@ export function CallsTab({
|
|||
onOutgoingVideoCallInConversation,
|
||||
preferredLeftPaneWidth,
|
||||
renderConversationDetails,
|
||||
renderToastManager,
|
||||
regionCode,
|
||||
savePreferredLeftPaneWidth,
|
||||
}: CallsTabProps): JSX.Element {
|
||||
|
@ -175,6 +180,7 @@ export function CallsTab({
|
|||
requiresFullWidth
|
||||
preferredLeftPaneWidth={preferredLeftPaneWidth}
|
||||
savePreferredLeftPaneWidth={savePreferredLeftPaneWidth}
|
||||
renderToastManager={renderToastManager}
|
||||
actions={
|
||||
<>
|
||||
{sidebarView === CallsTabSidebarView.CallsListView && (
|
||||
|
|
|
@ -563,6 +563,7 @@ export function CompositionArea({
|
|||
conversationId={conversationId}
|
||||
draftAttachments={draftAttachments}
|
||||
i18n={i18n}
|
||||
showToast={showToast}
|
||||
startRecording={startRecording}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -51,6 +51,8 @@ export function Default(): JSX.Element {
|
|||
errorRecording={_ => action('error')()}
|
||||
addAttachment={action('addAttachment')}
|
||||
completeRecording={action('completeRecording')}
|
||||
showToast={action('showToast')}
|
||||
hideToast={action('hideToast')}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -5,14 +5,16 @@ import { noop } from 'lodash';
|
|||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useEscapeHandling } from '../hooks/useEscapeHandling';
|
||||
import { usePrevious } from '../hooks/usePrevious';
|
||||
import type { HideToastAction, ShowToastAction } from '../state/ducks/toast';
|
||||
import type { InMemoryAttachmentDraftType } from '../types/Attachment';
|
||||
import { ErrorDialogAudioRecorderType } from '../types/AudioRecorder';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import type { AnyToast } from '../types/Toast';
|
||||
import { ToastType } from '../types/Toast';
|
||||
import { DurationInSeconds, SECOND } from '../util/durations';
|
||||
import { durationToPlaybackText } from '../util/durationToPlaybackText';
|
||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||
import { RecordingComposer } from './RecordingComposer';
|
||||
import { ToastVoiceNoteLimit } from './ToastVoiceNoteLimit';
|
||||
|
||||
export type Props = {
|
||||
i18n: LocalizerType;
|
||||
|
@ -29,6 +31,8 @@ export type Props = {
|
|||
conversationId: string,
|
||||
onRecordingComplete: (rec: InMemoryAttachmentDraftType) => unknown
|
||||
) => unknown;
|
||||
showToast: ShowToastAction;
|
||||
hideToast: HideToastAction;
|
||||
};
|
||||
|
||||
export function CompositionRecording({
|
||||
|
@ -40,11 +44,11 @@ export function CompositionRecording({
|
|||
errorDialogAudioRecorderType,
|
||||
addAttachment,
|
||||
completeRecording,
|
||||
showToast,
|
||||
hideToast,
|
||||
}: Props): JSX.Element {
|
||||
useEscapeHandling(onCancel);
|
||||
|
||||
const [showVoiceNoteLimitToast, setShowVoiceNoteLimitToast] = useState(true);
|
||||
|
||||
// when interrupted (blur, switching convos)
|
||||
// stop recording and save draft
|
||||
const handleRecordingInterruption = useCallback(() => {
|
||||
|
@ -69,15 +73,12 @@ export function CompositionRecording({
|
|||
}
|
||||
});
|
||||
|
||||
const handleCloseToast = useCallback(() => {
|
||||
setShowVoiceNoteLimitToast(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
handleCloseToast();
|
||||
};
|
||||
}, [handleCloseToast]);
|
||||
const toast: AnyToast = { toastType: ToastType.VoiceNoteLimit };
|
||||
showToast(toast);
|
||||
|
||||
return () => hideToast(toast);
|
||||
}, [showToast, hideToast]);
|
||||
|
||||
const startTime = useRef(Date.now());
|
||||
const [duration, setDuration] = useState(0);
|
||||
|
@ -148,9 +149,6 @@ export function CompositionRecording({
|
|||
</div>
|
||||
|
||||
{confirmationDialog}
|
||||
{showVoiceNoteLimitToast && (
|
||||
<ToastVoiceNoteLimit i18n={i18n} onClose={handleCloseToast} />
|
||||
)}
|
||||
</RecordingComposer>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,13 +6,15 @@ import React, { useEffect, useState } from 'react';
|
|||
import copyText from 'copy-text-to-clipboard';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import * as Errors from '../types/errors';
|
||||
import type { AnyToast } from '../types/Toast';
|
||||
import { ToastType } from '../types/Toast';
|
||||
import * as log from '../logging/log';
|
||||
import { Button, ButtonVariant } from './Button';
|
||||
import { Spinner } from './Spinner';
|
||||
import { ToastDebugLogError } from './ToastDebugLogError';
|
||||
import { ToastLinkCopied } from './ToastLinkCopied';
|
||||
import { ToastLoadingFullLogs } from './ToastLoadingFullLogs';
|
||||
import { ToastManager } from './ToastManager';
|
||||
import { WidthBreakpoint } from './_util';
|
||||
import { createSupportUrl } from '../util/createSupportUrl';
|
||||
import { shouldNeverBeCalled } from '../util/shouldNeverBeCalled';
|
||||
import { openLinkInWebBrowser } from '../util/openLinkInWebBrowser';
|
||||
import { useEscapeHandling } from '../hooks/useEscapeHandling';
|
||||
|
||||
|
@ -31,12 +33,6 @@ export type PropsType = {
|
|||
uploadLogs: (logs: string) => Promise<string>;
|
||||
};
|
||||
|
||||
enum ToastType {
|
||||
Copied,
|
||||
Error,
|
||||
Loading,
|
||||
}
|
||||
|
||||
export function DebugLogWindow({
|
||||
closeWindow,
|
||||
downloadLog,
|
||||
|
@ -50,7 +46,7 @@ export function DebugLogWindow({
|
|||
const [textAreaValue, setTextAreaValue] = useState<string>(
|
||||
i18n('icu:loading')
|
||||
);
|
||||
const [toastType, setToastType] = useState<ToastType | undefined>();
|
||||
const [toast, setToast] = useState<AnyToast | undefined>();
|
||||
|
||||
useEscapeHandling(closeWindow);
|
||||
|
||||
|
@ -66,7 +62,7 @@ export function DebugLogWindow({
|
|||
return;
|
||||
}
|
||||
|
||||
setToastType(ToastType.Loading);
|
||||
setToast({ toastType: ToastType.LoadingFullLogs });
|
||||
setLogText(fetchedLogText);
|
||||
setLoadState(LoadState.Loaded);
|
||||
|
||||
|
@ -76,7 +72,7 @@ export function DebugLogWindow({
|
|||
const value = fetchedLogText.split(/\n/g, linesToShow).join('\n');
|
||||
|
||||
setTextAreaValue(`${value}\n\n\n${i18n('icu:debugLogLogIsIncomplete')}`);
|
||||
setToastType(undefined);
|
||||
setToast(undefined);
|
||||
}
|
||||
|
||||
void doFetchLogs();
|
||||
|
@ -103,28 +99,19 @@ export function DebugLogWindow({
|
|||
} catch (error) {
|
||||
log.error('DebugLogWindow error:', Errors.toLogFormat(error));
|
||||
setLoadState(LoadState.Loaded);
|
||||
setToastType(ToastType.Error);
|
||||
setToast({ toastType: ToastType.DebugLogError });
|
||||
}
|
||||
};
|
||||
|
||||
function closeToast() {
|
||||
setToastType(undefined);
|
||||
}
|
||||
|
||||
let toastElement: JSX.Element | undefined;
|
||||
if (toastType === ToastType.Loading) {
|
||||
toastElement = <ToastLoadingFullLogs i18n={i18n} onClose={closeToast} />;
|
||||
} else if (toastType === ToastType.Copied) {
|
||||
toastElement = <ToastLinkCopied i18n={i18n} onClose={closeToast} />;
|
||||
} else if (toastType === ToastType.Error) {
|
||||
toastElement = <ToastDebugLogError i18n={i18n} onClose={closeToast} />;
|
||||
setToast(undefined);
|
||||
}
|
||||
|
||||
if (publicLogURL) {
|
||||
const copyLog = (ev: MouseEvent) => {
|
||||
ev.preventDefault();
|
||||
copyText(publicLogURL);
|
||||
setToastType(ToastType.Copied);
|
||||
setToast({ toastType: ToastType.LinkCopied });
|
||||
};
|
||||
|
||||
const supportURL = createSupportUrl({
|
||||
|
@ -162,7 +149,16 @@ export function DebugLogWindow({
|
|||
</Button>
|
||||
<Button onClick={copyLog}>{i18n('icu:debugLogCopy')}</Button>
|
||||
</div>
|
||||
{toastElement}
|
||||
<ToastManager
|
||||
OS="unused"
|
||||
hideToast={closeToast}
|
||||
i18n={i18n}
|
||||
onShowDebugLog={shouldNeverBeCalled}
|
||||
onUndoArchive={shouldNeverBeCalled}
|
||||
openFileInFolder={shouldNeverBeCalled}
|
||||
toast={toast}
|
||||
containerWidthBreakpoint={WidthBreakpoint.Narrow}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -209,7 +205,16 @@ export function DebugLogWindow({
|
|||
{i18n('icu:submit')}
|
||||
</Button>
|
||||
</div>
|
||||
{toastElement}
|
||||
<ToastManager
|
||||
OS="unused"
|
||||
hideToast={closeToast}
|
||||
i18n={i18n}
|
||||
onShowDebugLog={shouldNeverBeCalled}
|
||||
onUndoArchive={shouldNeverBeCalled}
|
||||
openFileInFolder={shouldNeverBeCalled}
|
||||
toast={toast}
|
||||
containerWidthBreakpoint={WidthBreakpoint.Narrow}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import type {
|
|||
UserNotFoundModalStateType,
|
||||
} from '../state/ducks/globalModals';
|
||||
import type { LocalizerType, ThemeType } from '../types/Util';
|
||||
import { UsernameOnboardingState } from '../types/globalModals';
|
||||
import type { ExplodePromiseResultType } from '../util/explodePromise';
|
||||
import { missingCaseError } from '../util/missingCaseError';
|
||||
|
||||
|
@ -90,6 +91,9 @@ export type PropsType = {
|
|||
// WhatsNewModal
|
||||
isWhatsNewVisible: boolean;
|
||||
hideWhatsNewModal: () => unknown;
|
||||
// UsernameOnboarding
|
||||
usernameOnboardingState: UsernameOnboardingState;
|
||||
renderUsernameOnboarding: () => JSX.Element;
|
||||
// AuthArtCreatorModal
|
||||
authArtCreatorData?: AuthorizeArtCreatorDataType;
|
||||
isAuthorizingArtCreator?: boolean;
|
||||
|
@ -151,6 +155,9 @@ export function GlobalModalContainer({
|
|||
// WhatsNewModal
|
||||
hideWhatsNewModal,
|
||||
isWhatsNewVisible,
|
||||
// UsernameOnboarding
|
||||
usernameOnboardingState,
|
||||
renderUsernameOnboarding,
|
||||
// AuthArtCreatorModal
|
||||
authArtCreatorData,
|
||||
isAuthorizingArtCreator,
|
||||
|
@ -253,6 +260,10 @@ export function GlobalModalContainer({
|
|||
return <WhatsNewModal hideWhatsNewModal={hideWhatsNewModal} i18n={i18n} />;
|
||||
}
|
||||
|
||||
if (usernameOnboardingState === UsernameOnboardingState.Open) {
|
||||
return renderUsernameOnboarding();
|
||||
}
|
||||
|
||||
if (safetyNumberModalContactId) {
|
||||
return renderSafetyNumber();
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import type { PropsType } from './LeftPane';
|
|||
import { LeftPane, LeftPaneMode } from './LeftPane';
|
||||
import { CaptchaDialog } from './CaptchaDialog';
|
||||
import { CrashReportDialog } from './CrashReportDialog';
|
||||
import { ToastManager } from './ToastManager';
|
||||
import type { PropsType as DialogNetworkStatusPropsType } from './DialogNetworkStatus';
|
||||
import { DialogExpiredBuild } from './DialogExpiredBuild';
|
||||
import { DialogNetworkStatus } from './DialogNetworkStatus';
|
||||
|
@ -251,6 +252,19 @@ const useProps = (overrideProps: OverridePropsType = {}): PropsType => {
|
|||
{...props}
|
||||
/>
|
||||
),
|
||||
renderToastManager: ({ containerWidthBreakpoint }) => (
|
||||
<ToastManager
|
||||
OS="unused"
|
||||
hideToast={action('hideToast')}
|
||||
i18n={i18n}
|
||||
onShowDebugLog={action('onShowDebugLog')}
|
||||
onUndoArchive={action('onUndoArchive')}
|
||||
openFileInFolder={action('openFileInFolder')}
|
||||
toast={undefined}
|
||||
megaphone={undefined}
|
||||
containerWidthBreakpoint={containerWidthBreakpoint}
|
||||
/>
|
||||
),
|
||||
selectedConversationId: undefined,
|
||||
targetedMessageId: undefined,
|
||||
savePreferredLeftPaneWidth: action('savePreferredLeftPaneWidth'),
|
||||
|
|
|
@ -158,6 +158,9 @@ export type PropsType = {
|
|||
renderCaptchaDialog: (props: { onSkip(): void }) => JSX.Element;
|
||||
renderCrashReportDialog: () => JSX.Element;
|
||||
renderExpiredBuildDialog: (_: DialogExpiredBuildPropsType) => JSX.Element;
|
||||
renderToastManager: (_: {
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
}) => JSX.Element;
|
||||
} & LookupConversationWithoutServiceIdActionsType;
|
||||
|
||||
export function LeftPane({
|
||||
|
@ -200,6 +203,7 @@ export function LeftPane({
|
|||
renderUnsupportedOSDialog,
|
||||
renderRelinkDialog,
|
||||
renderUpdateDialog,
|
||||
renderToastManager,
|
||||
savePreferredLeftPaneWidth,
|
||||
searchInConversation,
|
||||
selectedConversationId,
|
||||
|
@ -597,6 +601,7 @@ export function LeftPane({
|
|||
modeSpecificProps.isAboutToSearch
|
||||
}
|
||||
savePreferredLeftPaneWidth={savePreferredLeftPaneWidth}
|
||||
renderToastManager={renderToastManager}
|
||||
actions={
|
||||
<>
|
||||
<NavSidebarActionButton
|
||||
|
|
|
@ -36,6 +36,7 @@ export default {
|
|||
onForward: jest.fn(action('onForward')),
|
||||
onSave: jest.fn(action('onSave')),
|
||||
hasViewReceiptSetting: false,
|
||||
renderToastManager: () => <i />,
|
||||
queueStoryDownload: action('queueStoryDownload'),
|
||||
retryMessageSend: action('retryMessageSend'),
|
||||
viewStory: action('viewStory'),
|
||||
|
|
|
@ -20,6 +20,7 @@ import { Theme } from '../util/theme';
|
|||
import { resolveStorySendStatus } from '../util/resolveStorySendStatus';
|
||||
import { useRetryStorySend } from '../hooks/useRetryStorySend';
|
||||
import { NavSidebar } from './NavSidebar';
|
||||
import type { WidthBreakpoint } from './_util';
|
||||
import type { UnreadStats } from '../util/countUnreadStats';
|
||||
|
||||
export type PropsType = {
|
||||
|
@ -40,6 +41,9 @@ export type PropsType = {
|
|||
viewStory: ViewStoryActionCreatorType;
|
||||
hasViewReceiptSetting: boolean;
|
||||
preferredLeftPaneWidth: number;
|
||||
renderToastManager: (_: {
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
}) => JSX.Element;
|
||||
savePreferredLeftPaneWidth: (preferredLeftPaneWidth: number) => void;
|
||||
theme: ThemeType;
|
||||
};
|
||||
|
@ -62,6 +66,7 @@ export function MyStories({
|
|||
onMediaPlaybackStart,
|
||||
onToggleNavTabsCollapse,
|
||||
preferredLeftPaneWidth,
|
||||
renderToastManager,
|
||||
savePreferredLeftPaneWidth,
|
||||
theme,
|
||||
}: PropsType): JSX.Element {
|
||||
|
@ -98,6 +103,7 @@ export function MyStories({
|
|||
onToggleNavTabsCollapse={onToggleNavTabsCollapse}
|
||||
preferredLeftPaneWidth={preferredLeftPaneWidth}
|
||||
requiresFullWidth
|
||||
renderToastManager={renderToastManager}
|
||||
savePreferredLeftPaneWidth={savePreferredLeftPaneWidth}
|
||||
>
|
||||
<div className="Stories__pane__list">
|
||||
|
|
|
@ -55,6 +55,9 @@ export type NavSidebarProps = Readonly<{
|
|||
savePreferredLeftPaneWidth: (width: number) => void;
|
||||
title: string;
|
||||
otherTabsUnreadStats: UnreadStats;
|
||||
renderToastManager: (_: {
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
}) => JSX.Element;
|
||||
}>;
|
||||
|
||||
enum DragState {
|
||||
|
@ -78,6 +81,7 @@ export function NavSidebar({
|
|||
savePreferredLeftPaneWidth,
|
||||
title,
|
||||
otherTabsUnreadStats,
|
||||
renderToastManager,
|
||||
}: NavSidebarProps): JSX.Element {
|
||||
const isRTL = i18n.getLocaleDirection() === 'rtl';
|
||||
const [dragState, setDragState] = useState(DragState.INITIAL);
|
||||
|
@ -218,6 +222,8 @@ export function NavSidebar({
|
|||
tabIndex={0}
|
||||
{...moveProps}
|
||||
/>
|
||||
|
||||
{renderToastManager({ containerWidthBreakpoint: widthBreakpoint })}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -39,6 +39,7 @@ import type {
|
|||
import { Button, ButtonVariant } from './Button';
|
||||
import { ChatColorPicker } from './ChatColorPicker';
|
||||
import { Checkbox } from './Checkbox';
|
||||
import { WidthBreakpoint } from './_util';
|
||||
import {
|
||||
CircleCheckbox,
|
||||
Variant as CircleCheckboxVariant,
|
||||
|
@ -1581,9 +1582,11 @@ export function Preferences({
|
|||
OS="unused"
|
||||
hideToast={() => setToast(undefined)}
|
||||
i18n={i18n}
|
||||
onShowDebugLog={shouldNeverBeCalled}
|
||||
onUndoArchive={shouldNeverBeCalled}
|
||||
openFileInFolder={shouldNeverBeCalled}
|
||||
toast={toast}
|
||||
containerWidthBreakpoint={WidthBreakpoint.Narrow}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -79,7 +79,6 @@ export default {
|
|||
replaceAvatar: action('replaceAvatar'),
|
||||
resetUsernameLink: action('resetUsernameLink'),
|
||||
saveAvatarToDisk: action('saveAvatarToDisk'),
|
||||
markCompletedUsernameOnboarding: action('markCompletedUsernameOnboarding'),
|
||||
markCompletedUsernameLinkOnboarding: action(
|
||||
'markCompletedUsernameLinkOnboarding'
|
||||
),
|
||||
|
|
|
@ -39,7 +39,6 @@ import { missingCaseError } from '../util/missingCaseError';
|
|||
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||
import { ContextMenu } from './ContextMenu';
|
||||
import { UsernameLinkModalBody } from './UsernameLinkModalBody';
|
||||
import { UsernameOnboardingModalBody } from './UsernameOnboardingModalBody';
|
||||
import {
|
||||
ConversationDetailsIcon,
|
||||
IconType,
|
||||
|
@ -54,7 +53,6 @@ export enum EditState {
|
|||
ProfileName = 'ProfileName',
|
||||
Bio = 'Bio',
|
||||
Username = 'Username',
|
||||
UsernameOnboarding = 'UsernameOnboarding',
|
||||
UsernameLink = 'UsernameLink',
|
||||
}
|
||||
|
||||
|
@ -75,13 +73,13 @@ export type PropsDataType = {
|
|||
conversationId: string;
|
||||
familyName?: string;
|
||||
firstName: string;
|
||||
hasCompletedUsernameOnboarding: boolean;
|
||||
hasCompletedUsernameLinkOnboarding: boolean;
|
||||
i18n: LocalizerType;
|
||||
isUsernameFlagEnabled: boolean;
|
||||
phoneNumber?: string;
|
||||
userAvatarData: ReadonlyArray<AvatarDataType>;
|
||||
username?: string;
|
||||
initialEditState?: EditState;
|
||||
usernameCorrupted: boolean;
|
||||
usernameEditState: UsernameEditState;
|
||||
usernameLinkState: UsernameLinkState;
|
||||
|
@ -92,7 +90,6 @@ export type PropsDataType = {
|
|||
|
||||
type PropsActionType = {
|
||||
deleteAvatarFromDisk: DeleteAvatarFromDiskActionType;
|
||||
markCompletedUsernameOnboarding: () => void;
|
||||
markCompletedUsernameLinkOnboarding: () => void;
|
||||
onSetSkinTone: (tone: number) => unknown;
|
||||
replaceAvatar: ReplaceAvatarActionType;
|
||||
|
@ -147,11 +144,10 @@ export function ProfileEditor({
|
|||
deleteUsername,
|
||||
familyName,
|
||||
firstName,
|
||||
hasCompletedUsernameOnboarding,
|
||||
hasCompletedUsernameLinkOnboarding,
|
||||
i18n,
|
||||
initialEditState = EditState.None,
|
||||
isUsernameFlagEnabled,
|
||||
markCompletedUsernameOnboarding,
|
||||
markCompletedUsernameLinkOnboarding,
|
||||
onEditStateChanged,
|
||||
onProfileChanged,
|
||||
|
@ -179,7 +175,7 @@ export function ProfileEditor({
|
|||
usernameLinkCorrupted,
|
||||
}: PropsType): JSX.Element {
|
||||
const focusInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const [editState, setEditState] = useState<EditState>(EditState.None);
|
||||
const [editState, setEditState] = useState<EditState>(initialEditState);
|
||||
const [confirmDiscardAction, setConfirmDiscardAction] = useState<
|
||||
(() => unknown) | undefined
|
||||
>(undefined);
|
||||
|
@ -518,16 +514,6 @@ export function ProfileEditor({
|
|||
content = renderEditUsernameModalBody({
|
||||
onClose: () => setEditState(EditState.None),
|
||||
});
|
||||
} else if (editState === EditState.UsernameOnboarding) {
|
||||
content = (
|
||||
<UsernameOnboardingModalBody
|
||||
i18n={i18n}
|
||||
onNext={() => {
|
||||
markCompletedUsernameOnboarding();
|
||||
setEditState(EditState.Username);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else if (editState === EditState.UsernameLink) {
|
||||
content = (
|
||||
<UsernameLinkModalBody
|
||||
|
@ -686,11 +672,7 @@ export function ProfileEditor({
|
|||
}
|
||||
|
||||
openUsernameReservationModal();
|
||||
if (username || hasCompletedUsernameOnboarding) {
|
||||
setEditState(EditState.Username);
|
||||
} else {
|
||||
setEditState(EditState.UsernameOnboarding);
|
||||
}
|
||||
setEditState(EditState.Username);
|
||||
}}
|
||||
alwaysShowActions={alwaysShowActions}
|
||||
actions={actions}
|
||||
|
|
|
@ -37,7 +37,6 @@ export function ProfileEditorModal({
|
|||
[EditState.Bio]: i18n('icu:ProfileEditorModal--about'),
|
||||
[EditState.None]: i18n('icu:ProfileEditorModal--profile'),
|
||||
[EditState.ProfileName]: i18n('icu:ProfileEditorModal--name'),
|
||||
[EditState.UsernameOnboarding]: undefined,
|
||||
[EditState.Username]: i18n('icu:ProfileEditorModal--username'),
|
||||
[EditState.UsernameLink]: undefined,
|
||||
};
|
||||
|
|
|
@ -33,6 +33,7 @@ export default {
|
|||
onSaveStory: action('onSaveStory'),
|
||||
preferredWidthFromStorage: 380,
|
||||
queueStoryDownload: action('queueStoryDownload'),
|
||||
renderToastManager: () => <i />,
|
||||
renderStoryCreator: () => <>StoryCreator</>,
|
||||
retryMessageSend: action('retryMessageSend'),
|
||||
showConversation: action('showConversation'),
|
||||
|
|
|
@ -24,6 +24,7 @@ import { StoriesPane } from './StoriesPane';
|
|||
import { NavSidebar, NavSidebarActionButton } from './NavSidebar';
|
||||
import { StoriesAddStoryButton } from './StoriesAddStoryButton';
|
||||
import { ContextMenu } from './ContextMenu';
|
||||
import type { WidthBreakpoint } from './_util';
|
||||
import type { UnreadStats } from '../util/countUnreadStats';
|
||||
|
||||
export type PropsType = {
|
||||
|
@ -50,6 +51,9 @@ export type PropsType = {
|
|||
preferredWidthFromStorage: number;
|
||||
queueStoryDownload: (storyId: string) => unknown;
|
||||
renderStoryCreator: () => JSX.Element;
|
||||
renderToastManager: (_: {
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
}) => JSX.Element;
|
||||
retryMessageSend: (messageId: string) => unknown;
|
||||
savePreferredLeftPaneWidth: (preferredLeftPaneWidth: number) => void;
|
||||
setAddStoryData: (data: AddStoryData) => unknown;
|
||||
|
@ -84,6 +88,7 @@ export function StoriesTab({
|
|||
preferredLeftPaneWidth,
|
||||
queueStoryDownload,
|
||||
renderStoryCreator,
|
||||
renderToastManager,
|
||||
retryMessageSend,
|
||||
savePreferredLeftPaneWidth,
|
||||
setAddStoryData,
|
||||
|
@ -127,6 +132,7 @@ export function StoriesTab({
|
|||
preferredLeftPaneWidth={preferredLeftPaneWidth}
|
||||
queueStoryDownload={queueStoryDownload}
|
||||
retryMessageSend={retryMessageSend}
|
||||
renderToastManager={renderToastManager}
|
||||
savePreferredLeftPaneWidth={savePreferredLeftPaneWidth}
|
||||
theme={theme}
|
||||
viewStory={viewStory}
|
||||
|
@ -143,6 +149,7 @@ export function StoriesTab({
|
|||
requiresFullWidth
|
||||
savePreferredLeftPaneWidth={savePreferredLeftPaneWidth}
|
||||
otherTabsUnreadStats={otherTabsUnreadStats}
|
||||
renderToastManager={renderToastManager}
|
||||
actions={
|
||||
<>
|
||||
<StoriesAddStoryButton
|
||||
|
|
|
@ -4,10 +4,8 @@
|
|||
import type { KeyboardEvent, MouseEvent, ReactNode } from 'react';
|
||||
import React, { memo, useEffect } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useRestoreFocus } from '../hooks/useRestoreFocus';
|
||||
import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary';
|
||||
import * as log from '../logging/log';
|
||||
|
||||
export type PropsType = {
|
||||
autoDismissDisabled?: boolean;
|
||||
|
@ -33,51 +31,10 @@ export const Toast = memo(function ToastInner({
|
|||
timeout = 8000,
|
||||
toastAction,
|
||||
}: PropsType): JSX.Element | null {
|
||||
const [root, setRoot] = React.useState<HTMLElement | null>(null);
|
||||
const [focusRef] = useRestoreFocus();
|
||||
const [align, setAlign] = React.useState<'left' | 'center'>('left');
|
||||
|
||||
useEffect(() => {
|
||||
function updateAlign() {
|
||||
const leftPane = document.querySelector('.module-left-pane');
|
||||
const composer = document.querySelector(
|
||||
'.ConversationView__composition-area'
|
||||
);
|
||||
|
||||
if (
|
||||
leftPane != null &&
|
||||
composer != null &&
|
||||
leftPane.classList.contains('module-left-pane--width-narrow')
|
||||
) {
|
||||
setAlign('center');
|
||||
return;
|
||||
}
|
||||
|
||||
setAlign('left');
|
||||
}
|
||||
|
||||
updateAlign();
|
||||
|
||||
if (window.reduxStore == null) {
|
||||
log.warn('Toast: No redux store');
|
||||
return;
|
||||
}
|
||||
return window.reduxStore.subscribe(updateAlign);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const div = document.createElement('div');
|
||||
document.body.appendChild(div);
|
||||
setRoot(div);
|
||||
|
||||
return () => {
|
||||
document.body.removeChild(div);
|
||||
setRoot(null);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!root || autoDismissDisabled) {
|
||||
if (autoDismissDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -86,56 +43,53 @@ export const Toast = memo(function ToastInner({
|
|||
return () => {
|
||||
clearTimeoutIfNecessary(timeoutId);
|
||||
};
|
||||
}, [autoDismissDisabled, onClose, root, timeout]);
|
||||
}, [autoDismissDisabled, onClose, timeout]);
|
||||
|
||||
return root
|
||||
? createPortal(
|
||||
return (
|
||||
<div
|
||||
aria-live="assertive"
|
||||
className={classNames('Toast', className)}
|
||||
onClick={() => {
|
||||
if (!disableCloseOnClick) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
onKeyDown={(ev: KeyboardEvent<HTMLDivElement>) => {
|
||||
if (ev.key === 'Enter' || ev.key === ' ') {
|
||||
if (!disableCloseOnClick) {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
style={style}
|
||||
>
|
||||
<div className="Toast__content">{children}</div>
|
||||
{toastAction && (
|
||||
<div
|
||||
aria-live="assertive"
|
||||
className={classNames('Toast', `Toast--align-${align}`, className)}
|
||||
onClick={() => {
|
||||
if (!disableCloseOnClick) {
|
||||
onClose();
|
||||
}
|
||||
className="Toast__button"
|
||||
onClick={(ev: MouseEvent<HTMLDivElement>) => {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
toastAction.onClick();
|
||||
onClose();
|
||||
}}
|
||||
onKeyDown={(ev: KeyboardEvent<HTMLDivElement>) => {
|
||||
if (ev.key === 'Enter' || ev.key === ' ') {
|
||||
if (!disableCloseOnClick) {
|
||||
onClose();
|
||||
}
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
toastAction.onClick();
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
ref={focusRef}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
style={style}
|
||||
>
|
||||
<div className="Toast__content">{children}</div>
|
||||
{toastAction && (
|
||||
<div
|
||||
className="Toast__button"
|
||||
onClick={(ev: MouseEvent<HTMLDivElement>) => {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
toastAction.onClick();
|
||||
onClose();
|
||||
}}
|
||||
onKeyDown={(ev: KeyboardEvent<HTMLDivElement>) => {
|
||||
if (ev.key === 'Enter' || ev.key === ' ') {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
toastAction.onClick();
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
ref={focusRef}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
{toastAction.label}
|
||||
</div>
|
||||
)}
|
||||
</div>,
|
||||
root
|
||||
)
|
||||
: null;
|
||||
{toastAction.label}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import type { PropsType } from './ToastAlreadyRequestedToJoin';
|
||||
import { ToastAlreadyRequestedToJoin } from './ToastAlreadyRequestedToJoin';
|
||||
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const defaultProps = {
|
||||
i18n,
|
||||
onClose: action('onClose'),
|
||||
};
|
||||
|
||||
export default {
|
||||
title: 'Components/ToastAlreadyRequestedToJoin',
|
||||
} satisfies Meta<PropsType>;
|
||||
|
||||
export const _ToastAlreadyRequestedToJoin = (): JSX.Element => (
|
||||
<ToastAlreadyRequestedToJoin {...defaultProps} />
|
||||
);
|
|
@ -1,22 +0,0 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { Toast } from './Toast';
|
||||
|
||||
export type PropsType = {
|
||||
i18n: LocalizerType;
|
||||
onClose: () => unknown;
|
||||
};
|
||||
|
||||
export function ToastAlreadyRequestedToJoin({
|
||||
i18n,
|
||||
onClose,
|
||||
}: PropsType): JSX.Element {
|
||||
return (
|
||||
<Toast onClose={onClose}>
|
||||
{i18n('icu:GroupV2--join--already-awaiting-approval')}
|
||||
</Toast>
|
||||
);
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import type { PropsType } from './ToastCaptchaFailed';
|
||||
import { ToastCaptchaFailed } from './ToastCaptchaFailed';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const defaultProps = {
|
||||
i18n,
|
||||
onClose: action('onClose'),
|
||||
};
|
||||
|
||||
export default {
|
||||
title: 'Components/ToastCaptchaFailed',
|
||||
} satisfies Meta<PropsType>;
|
||||
|
||||
export const _ToastCaptchaFailed = (): JSX.Element => (
|
||||
<ToastCaptchaFailed {...defaultProps} />
|
||||
);
|
|
@ -1,15 +0,0 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { Toast } from './Toast';
|
||||
|
||||
export type PropsType = {
|
||||
i18n: LocalizerType;
|
||||
onClose: () => unknown;
|
||||
};
|
||||
|
||||
export function ToastCaptchaFailed({ i18n, onClose }: PropsType): JSX.Element {
|
||||
return <Toast onClose={onClose}>{i18n('icu:verificationFailed')}</Toast>;
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import type { PropsType } from './ToastCaptchaSolved';
|
||||
import { ToastCaptchaSolved } from './ToastCaptchaSolved';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const defaultProps = {
|
||||
i18n,
|
||||
onClose: action('onClose'),
|
||||
};
|
||||
|
||||
export default {
|
||||
title: 'Components/ToastCaptchaSolved',
|
||||
} satisfies Meta<PropsType>;
|
||||
|
||||
export const _ToastCaptchaSolved = (): JSX.Element => (
|
||||
<ToastCaptchaSolved {...defaultProps} />
|
||||
);
|
|
@ -1,15 +0,0 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { Toast } from './Toast';
|
||||
|
||||
export type PropsType = {
|
||||
i18n: LocalizerType;
|
||||
onClose: () => unknown;
|
||||
};
|
||||
|
||||
export function ToastCaptchaSolved({ i18n, onClose }: PropsType): JSX.Element {
|
||||
return <Toast onClose={onClose}>{i18n('icu:verificationComplete')}</Toast>;
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import type { PropsType } from './ToastDebugLogError';
|
||||
import { ToastDebugLogError } from './ToastDebugLogError';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const defaultProps = {
|
||||
i18n,
|
||||
onClose: action('onClose'),
|
||||
};
|
||||
|
||||
export default {
|
||||
title: 'Components/ToastDebugLogError',
|
||||
} satisfies Meta<PropsType>;
|
||||
|
||||
export const _ToastDebugLogError = (): JSX.Element => (
|
||||
<ToastDebugLogError {...defaultProps} />
|
||||
);
|
|
@ -1,15 +0,0 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { Toast } from './Toast';
|
||||
|
||||
export type PropsType = {
|
||||
i18n: LocalizerType;
|
||||
onClose: () => unknown;
|
||||
};
|
||||
|
||||
export function ToastDebugLogError({ i18n, onClose }: PropsType): JSX.Element {
|
||||
return <Toast onClose={onClose}>{i18n('icu:debugLogError')}</Toast>;
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { Toast } from './Toast';
|
||||
|
||||
type PropsType = {
|
||||
i18n: LocalizerType;
|
||||
onClose: () => unknown;
|
||||
};
|
||||
|
||||
export function ToastFailedToFetchPhoneNumber({
|
||||
i18n,
|
||||
onClose,
|
||||
}: PropsType): JSX.Element {
|
||||
return (
|
||||
<Toast onClose={onClose} style={{ maxWidth: '280px' }}>
|
||||
{i18n('icu:Toast--failed-to-fetch-phone-number')}
|
||||
</Toast>
|
||||
);
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { Toast } from './Toast';
|
||||
|
||||
type PropsType = {
|
||||
i18n: LocalizerType;
|
||||
onClose: () => unknown;
|
||||
};
|
||||
|
||||
export function ToastFailedToFetchUsername({
|
||||
i18n,
|
||||
onClose,
|
||||
}: PropsType): JSX.Element {
|
||||
return (
|
||||
<Toast onClose={onClose} style={{ maxWidth: '280px' }}>
|
||||
{i18n('icu:Toast--failed-to-fetch-username')}
|
||||
</Toast>
|
||||
);
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import type { PropsType } from './ToastFileSize';
|
||||
import { ToastFileSize } from './ToastFileSize';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const defaultProps = {
|
||||
i18n,
|
||||
onClose: action('onClose'),
|
||||
};
|
||||
|
||||
export default {
|
||||
title: 'Components/ToastFileSize',
|
||||
} satisfies Meta<PropsType>;
|
||||
|
||||
export const _ToastFileSize = (): JSX.Element => (
|
||||
<ToastFileSize {...defaultProps} limit={100} units="MB" />
|
||||
);
|
|
@ -1,29 +0,0 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { Toast } from './Toast';
|
||||
|
||||
export type ToastPropsType = {
|
||||
limit: number;
|
||||
units: string;
|
||||
};
|
||||
|
||||
export type PropsType = {
|
||||
i18n: LocalizerType;
|
||||
onClose: () => unknown;
|
||||
} & ToastPropsType;
|
||||
|
||||
export function ToastFileSize({
|
||||
i18n,
|
||||
limit,
|
||||
onClose,
|
||||
units,
|
||||
}: PropsType): JSX.Element {
|
||||
return (
|
||||
<Toast onClose={onClose}>
|
||||
{i18n('icu:fileSizeWarning', { limit, units })}
|
||||
</Toast>
|
||||
);
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import type { PropsType } from './ToastGroupLinkCopied';
|
||||
import { ToastGroupLinkCopied } from './ToastGroupLinkCopied';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const defaultProps = {
|
||||
i18n,
|
||||
onClose: action('onClose'),
|
||||
};
|
||||
|
||||
export default {
|
||||
title: 'Components/ToastGroupLinkCopied',
|
||||
} satisfies Meta<PropsType>;
|
||||
|
||||
export const _ToastGroupLinkCopied = (): JSX.Element => (
|
||||
<ToastGroupLinkCopied {...defaultProps} />
|
||||
);
|
|
@ -1,22 +0,0 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { Toast } from './Toast';
|
||||
|
||||
export type PropsType = {
|
||||
i18n: LocalizerType;
|
||||
onClose: () => unknown;
|
||||
};
|
||||
|
||||
export function ToastGroupLinkCopied({
|
||||
i18n,
|
||||
onClose,
|
||||
}: PropsType): JSX.Element {
|
||||
return (
|
||||
<Toast onClose={onClose}>
|
||||
{i18n('icu:GroupLinkManagement--clipboard')}
|
||||
</Toast>
|
||||
);
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import type { PropsType } from './ToastInternalError';
|
||||
import {
|
||||
ToastInternalError,
|
||||
ToastInternalErrorKind,
|
||||
} from './ToastInternalError';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const defaultProps = {
|
||||
i18n,
|
||||
onClose: action('onClose'),
|
||||
onShowDebugLog: action('onShowDebugLog'),
|
||||
};
|
||||
|
||||
export default {
|
||||
title: 'Components/ToastInternalError',
|
||||
} satisfies Meta<PropsType>;
|
||||
|
||||
export function ToastDecryptionError(): JSX.Element {
|
||||
return (
|
||||
<ToastInternalError
|
||||
kind={ToastInternalErrorKind.DecryptionError}
|
||||
deviceId={3}
|
||||
name="Someone Somewhere"
|
||||
{...defaultProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ToastCDSMirroringError(): JSX.Element {
|
||||
return (
|
||||
<ToastInternalError
|
||||
kind={ToastInternalErrorKind.CDSMirroringError}
|
||||
{...defaultProps}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { missingCaseError } from '../util/missingCaseError';
|
||||
import { Toast } from './Toast';
|
||||
|
||||
export enum ToastInternalErrorKind {
|
||||
DecryptionError = 'DecryptionError',
|
||||
CDSMirroringError = 'CDSMirroringError',
|
||||
}
|
||||
|
||||
export type ToastPropsType = {
|
||||
onShowDebugLog: () => unknown;
|
||||
} & (
|
||||
| {
|
||||
kind: ToastInternalErrorKind.DecryptionError;
|
||||
deviceId: number;
|
||||
name: string;
|
||||
}
|
||||
| {
|
||||
kind: ToastInternalErrorKind.CDSMirroringError;
|
||||
}
|
||||
);
|
||||
|
||||
export type PropsType = {
|
||||
i18n: LocalizerType;
|
||||
onClose: () => unknown;
|
||||
} & ToastPropsType;
|
||||
|
||||
export function ToastInternalError(props: PropsType): JSX.Element {
|
||||
const { kind, i18n, onClose, onShowDebugLog } = props;
|
||||
|
||||
let body: string;
|
||||
if (kind === ToastInternalErrorKind.DecryptionError) {
|
||||
const { deviceId, name } = props;
|
||||
|
||||
body = i18n('icu:decryptionErrorToast', {
|
||||
name,
|
||||
deviceId,
|
||||
});
|
||||
} else if (kind === ToastInternalErrorKind.CDSMirroringError) {
|
||||
body = i18n('icu:cdsMirroringErrorToast');
|
||||
} else {
|
||||
throw missingCaseError(kind);
|
||||
}
|
||||
|
||||
return (
|
||||
<Toast
|
||||
autoDismissDisabled
|
||||
className="internal-error-toast"
|
||||
onClose={onClose}
|
||||
style={{ maxWidth: '500px' }}
|
||||
toastAction={{
|
||||
label: i18n('icu:decryptionErrorToastAction'),
|
||||
onClick: onShowDebugLog,
|
||||
}}
|
||||
>
|
||||
{body}
|
||||
</Toast>
|
||||
);
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import type { PropsType } from './ToastLinkCopied';
|
||||
import { ToastLinkCopied } from './ToastLinkCopied';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const defaultProps = {
|
||||
i18n,
|
||||
onClose: action('onClose'),
|
||||
};
|
||||
|
||||
export default {
|
||||
title: 'Components/ToastLinkCopied',
|
||||
} satisfies Meta<PropsType>;
|
||||
|
||||
export const _ToastLinkCopied = (): JSX.Element => (
|
||||
<ToastLinkCopied {...defaultProps} />
|
||||
);
|
|
@ -1,15 +0,0 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { Toast } from './Toast';
|
||||
|
||||
export type PropsType = {
|
||||
i18n: LocalizerType;
|
||||
onClose: () => unknown;
|
||||
};
|
||||
|
||||
export function ToastLinkCopied({ i18n, onClose }: PropsType): JSX.Element {
|
||||
return <Toast onClose={onClose}>{i18n('icu:debugLogLinkCopied')}</Toast>;
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import type { PropsType } from './ToastLoadingFullLogs';
|
||||
import { ToastLoadingFullLogs } from './ToastLoadingFullLogs';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const defaultProps = {
|
||||
i18n,
|
||||
onClose: action('onClose'),
|
||||
};
|
||||
|
||||
export default {
|
||||
title: 'Components/ToastLoadingFullLogs',
|
||||
} satisfies Meta<PropsType>;
|
||||
|
||||
export const _ToastLoadingFullLogs = (): JSX.Element => (
|
||||
<ToastLoadingFullLogs {...defaultProps} />
|
||||
);
|
|
@ -1,18 +0,0 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { Toast } from './Toast';
|
||||
|
||||
export type PropsType = {
|
||||
i18n: LocalizerType;
|
||||
onClose: () => unknown;
|
||||
};
|
||||
|
||||
export function ToastLoadingFullLogs({
|
||||
i18n,
|
||||
onClose,
|
||||
}: PropsType): JSX.Element {
|
||||
return <Toast onClose={onClose}>{i18n('icu:loading')}</Toast>;
|
||||
}
|
|
@ -9,6 +9,8 @@ import enMessages from '../../_locales/en/messages.json';
|
|||
import { ToastManager } from './ToastManager';
|
||||
import type { AnyToast } from '../types/Toast';
|
||||
import { ToastType } from '../types/Toast';
|
||||
import type { AnyActionableMegaphone } from '../types/Megaphone';
|
||||
import { MegaphoneType } from '../types/Megaphone';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import { missingCaseError } from '../util/missingCaseError';
|
||||
import type { PropsType } from './ToastManager';
|
||||
|
@ -41,6 +43,10 @@ function getToast(toastType: ToastType): AnyToast {
|
|||
return { toastType: ToastType.CannotOpenGiftBadgeOutgoing };
|
||||
case ToastType.CannotStartGroupCall:
|
||||
return { toastType: ToastType.CannotStartGroupCall };
|
||||
case ToastType.CaptchaFailed:
|
||||
return { toastType: ToastType.CaptchaFailed };
|
||||
case ToastType.CaptchaSolved:
|
||||
return { toastType: ToastType.CaptchaSolved };
|
||||
case ToastType.ConversationArchived:
|
||||
return {
|
||||
toastType: ToastType.ConversationArchived,
|
||||
|
@ -61,6 +67,16 @@ function getToast(toastType: ToastType): AnyToast {
|
|||
return { toastType: ToastType.CopiedUsernameLink };
|
||||
case ToastType.DangerousFileType:
|
||||
return { toastType: ToastType.DangerousFileType };
|
||||
case ToastType.DebugLogError:
|
||||
return { toastType: ToastType.DebugLogError };
|
||||
case ToastType.DecryptionError:
|
||||
return {
|
||||
toastType: ToastType.DecryptionError,
|
||||
parameters: {
|
||||
deviceId: 2,
|
||||
name: 'Alice',
|
||||
},
|
||||
};
|
||||
case ToastType.DeleteForEveryoneFailed:
|
||||
return { toastType: ToastType.DeleteForEveryoneFailed };
|
||||
case ToastType.Error:
|
||||
|
@ -69,6 +85,10 @@ function getToast(toastType: ToastType): AnyToast {
|
|||
return { toastType: ToastType.Expired };
|
||||
case ToastType.FailedToDeleteUsername:
|
||||
return { toastType: ToastType.FailedToDeleteUsername };
|
||||
case ToastType.FailedToFetchPhoneNumber:
|
||||
return { toastType: ToastType.FailedToFetchPhoneNumber };
|
||||
case ToastType.FailedToFetchUsername:
|
||||
return { toastType: ToastType.FailedToFetchUsername };
|
||||
case ToastType.FileSaved:
|
||||
return {
|
||||
toastType: ToastType.FileSaved,
|
||||
|
@ -79,10 +99,16 @@ function getToast(toastType: ToastType): AnyToast {
|
|||
toastType: ToastType.FileSize,
|
||||
parameters: { limit: 100, units: 'MB' },
|
||||
};
|
||||
case ToastType.GroupLinkCopied:
|
||||
return { toastType: ToastType.GroupLinkCopied };
|
||||
case ToastType.InvalidConversation:
|
||||
return { toastType: ToastType.InvalidConversation };
|
||||
case ToastType.LeftGroup:
|
||||
return { toastType: ToastType.LeftGroup };
|
||||
case ToastType.LinkCopied:
|
||||
return { toastType: ToastType.LinkCopied };
|
||||
case ToastType.LoadingFullLogs:
|
||||
return { toastType: ToastType.LoadingFullLogs };
|
||||
case ToastType.MaxAttachments:
|
||||
return { toastType: ToastType.MaxAttachments };
|
||||
case ToastType.MessageBodyTooLong:
|
||||
|
@ -95,6 +121,8 @@ function getToast(toastType: ToastType): AnyToast {
|
|||
return { toastType: ToastType.ReactionFailed };
|
||||
case ToastType.ReportedSpamAndBlocked:
|
||||
return { toastType: ToastType.ReportedSpamAndBlocked };
|
||||
case ToastType.StickerPackInstallFailed:
|
||||
return { toastType: ToastType.StickerPackInstallFailed };
|
||||
case ToastType.StoryMuted:
|
||||
return { toastType: ToastType.StoryMuted };
|
||||
case ToastType.StoryReact:
|
||||
|
@ -130,6 +158,10 @@ function getToast(toastType: ToastType): AnyToast {
|
|||
group: 'Hike Group 🏔',
|
||||
},
|
||||
};
|
||||
case ToastType.VoiceNoteLimit:
|
||||
return { toastType: ToastType.VoiceNoteLimit };
|
||||
case ToastType.VoiceNoteMustBeTheOnlyAttachment:
|
||||
return { toastType: ToastType.VoiceNoteMustBeTheOnlyAttachment };
|
||||
case ToastType.WhoCanFindMeReadOnly:
|
||||
return { toastType: ToastType.WhoCanFindMeReadOnly };
|
||||
default:
|
||||
|
@ -137,8 +169,22 @@ function getToast(toastType: ToastType): AnyToast {
|
|||
}
|
||||
}
|
||||
|
||||
type Args = Omit<PropsType, 'toast'> & {
|
||||
function getMegaphone(megaphoneType: MegaphoneType): AnyActionableMegaphone {
|
||||
switch (megaphoneType) {
|
||||
case MegaphoneType.UsernameOnboarding:
|
||||
return {
|
||||
type: megaphoneType,
|
||||
onLearnMore: action('onLearnMore'),
|
||||
onDismiss: action('onDismiss'),
|
||||
};
|
||||
default:
|
||||
throw missingCaseError(megaphoneType);
|
||||
}
|
||||
}
|
||||
|
||||
type Args = Omit<PropsType, 'toast' | 'megaphone'> & {
|
||||
toastType: ToastType;
|
||||
megaphoneType: MegaphoneType;
|
||||
};
|
||||
|
||||
export default {
|
||||
|
@ -149,24 +195,34 @@ export default {
|
|||
options: ToastType,
|
||||
control: { type: 'select' },
|
||||
},
|
||||
megaphoneType: {
|
||||
options: MegaphoneType,
|
||||
control: { type: 'select' },
|
||||
},
|
||||
},
|
||||
args: {
|
||||
hideToast: action('hideToast'),
|
||||
openFileInFolder: action('openFileInFolder'),
|
||||
onShowDebugLog: action('onShowDebugLog'),
|
||||
onUndoArchive: action('onUndoArchive'),
|
||||
i18n,
|
||||
toastType: ToastType.AddingUserToGroup,
|
||||
megaphoneType: MegaphoneType.UsernameOnboarding,
|
||||
OS: 'macOS',
|
||||
},
|
||||
} satisfies Meta<Args>;
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
const Template: StoryFn<Args> = args => {
|
||||
const { toastType, ...rest } = args;
|
||||
const { toastType, megaphoneType, ...rest } = args;
|
||||
return (
|
||||
<>
|
||||
<p>Select a toast type in controls</p>
|
||||
<ToastManager toast={getToast(toastType)} {...rest} />
|
||||
<ToastManager
|
||||
toast={getToast(toastType)}
|
||||
megaphone={getMegaphone(megaphoneType)}
|
||||
{...rest}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,29 +1,43 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { SECOND } from '../util/durations';
|
||||
import { Toast } from './Toast';
|
||||
import { WidthBreakpoint } from './_util';
|
||||
import { UsernameMegaphone } from './UsernameMegaphone';
|
||||
import { assertDev } from '../util/assert';
|
||||
import { missingCaseError } from '../util/missingCaseError';
|
||||
import type { AnyToast } from '../types/Toast';
|
||||
import { ToastType } from '../types/Toast';
|
||||
import type { AnyActionableMegaphone } from '../types/Megaphone';
|
||||
import { MegaphoneType } from '../types/Megaphone';
|
||||
|
||||
export type PropsType = {
|
||||
hideToast: () => unknown;
|
||||
i18n: LocalizerType;
|
||||
openFileInFolder: (target: string) => unknown;
|
||||
OS: string;
|
||||
onShowDebugLog: () => unknown;
|
||||
onUndoArchive: (conversaetionId: string) => unknown;
|
||||
toast?: AnyToast;
|
||||
megaphone?: AnyActionableMegaphone;
|
||||
centerToast?: boolean;
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
isCompositionAreaVisible?: boolean;
|
||||
};
|
||||
|
||||
const SHORT_TIMEOUT = 3 * SECOND;
|
||||
|
||||
export function ToastManager({
|
||||
export function renderToast({
|
||||
hideToast,
|
||||
i18n,
|
||||
openFileInFolder,
|
||||
onShowDebugLog,
|
||||
onUndoArchive,
|
||||
OS,
|
||||
toast,
|
||||
|
@ -116,6 +130,16 @@ export function ToastManager({
|
|||
);
|
||||
}
|
||||
|
||||
if (toastType === ToastType.CaptchaFailed) {
|
||||
return <Toast onClose={hideToast}>{i18n('icu:verificationFailed')}</Toast>;
|
||||
}
|
||||
|
||||
if (toastType === ToastType.CaptchaSolved) {
|
||||
return (
|
||||
<Toast onClose={hideToast}>{i18n('icu:verificationComplete')}</Toast>
|
||||
);
|
||||
}
|
||||
|
||||
if (toastType === ToastType.CannotStartGroupCall) {
|
||||
return (
|
||||
<Toast onClose={hideToast}>
|
||||
|
@ -184,6 +208,10 @@ export function ToastManager({
|
|||
return <Toast onClose={hideToast}>{i18n('icu:dangerousFileType')}</Toast>;
|
||||
}
|
||||
|
||||
if (toastType === ToastType.DebugLogError) {
|
||||
return <Toast onClose={hideToast}>{i18n('icu:debugLogError')}</Toast>;
|
||||
}
|
||||
|
||||
if (toastType === ToastType.DeleteForEveryoneFailed) {
|
||||
return (
|
||||
<Toast onClose={hideToast}>{i18n('icu:deleteForEveryoneFailed')}</Toast>
|
||||
|
@ -217,6 +245,22 @@ export function ToastManager({
|
|||
);
|
||||
}
|
||||
|
||||
if (toastType === ToastType.FailedToFetchPhoneNumber) {
|
||||
return (
|
||||
<Toast onClose={hideToast} style={{ maxWidth: '280px' }}>
|
||||
{i18n('icu:Toast--failed-to-fetch-phone-number')}
|
||||
</Toast>
|
||||
);
|
||||
}
|
||||
|
||||
if (toastType === ToastType.FailedToFetchUsername) {
|
||||
return (
|
||||
<Toast onClose={hideToast} style={{ maxWidth: '280px' }}>
|
||||
{i18n('icu:Toast--failed-to-fetch-username')}
|
||||
</Toast>
|
||||
);
|
||||
}
|
||||
|
||||
if (toastType === ToastType.FileSaved) {
|
||||
return (
|
||||
<Toast
|
||||
|
@ -244,6 +288,41 @@ export function ToastManager({
|
|||
);
|
||||
}
|
||||
|
||||
if (toastType === ToastType.GroupLinkCopied) {
|
||||
return (
|
||||
<Toast onClose={hideToast}>
|
||||
{i18n('icu:GroupLinkManagement--clipboard')}
|
||||
</Toast>
|
||||
);
|
||||
}
|
||||
|
||||
if (toastType === ToastType.DecryptionError) {
|
||||
assertDev(
|
||||
toast.toastType === ToastType.DecryptionError,
|
||||
'Pacify typescript'
|
||||
);
|
||||
const { parameters } = toast;
|
||||
const { deviceId, name } = parameters;
|
||||
|
||||
return (
|
||||
<Toast
|
||||
autoDismissDisabled
|
||||
className="internal-error-toast"
|
||||
onClose={hideToast}
|
||||
style={{ maxWidth: '500px' }}
|
||||
toastAction={{
|
||||
label: i18n('icu:decryptionErrorToastAction'),
|
||||
onClick: onShowDebugLog,
|
||||
}}
|
||||
>
|
||||
{i18n('icu:decryptionErrorToast', {
|
||||
name,
|
||||
deviceId,
|
||||
})}
|
||||
</Toast>
|
||||
);
|
||||
}
|
||||
|
||||
if (toastType === ToastType.InvalidConversation) {
|
||||
return <Toast onClose={hideToast}>{i18n('icu:invalidConversation')}</Toast>;
|
||||
}
|
||||
|
@ -252,6 +331,14 @@ export function ToastManager({
|
|||
return <Toast onClose={hideToast}>{i18n('icu:youLeftTheGroup')}</Toast>;
|
||||
}
|
||||
|
||||
if (toastType === ToastType.LinkCopied) {
|
||||
return <Toast onClose={hideToast}>{i18n('icu:debugLogLinkCopied')}</Toast>;
|
||||
}
|
||||
|
||||
if (toastType === ToastType.LoadingFullLogs) {
|
||||
return <Toast onClose={hideToast}>{i18n('icu:loading')}</Toast>;
|
||||
}
|
||||
|
||||
if (toastType === ToastType.MaxAttachments) {
|
||||
return <Toast onClose={hideToast}>{i18n('icu:maximumAttachments')}</Toast>;
|
||||
}
|
||||
|
@ -284,6 +371,14 @@ export function ToastManager({
|
|||
);
|
||||
}
|
||||
|
||||
if (toastType === ToastType.StickerPackInstallFailed) {
|
||||
return (
|
||||
<Toast onClose={hideToast}>
|
||||
{i18n('icu:stickers--toast--InstallFailed')}
|
||||
</Toast>
|
||||
);
|
||||
}
|
||||
|
||||
if (toastType === ToastType.StoryMuted) {
|
||||
return (
|
||||
<Toast onClose={hideToast} timeout={SHORT_TIMEOUT}>
|
||||
|
@ -392,6 +487,18 @@ export function ToastManager({
|
|||
);
|
||||
}
|
||||
|
||||
if (toastType === ToastType.VoiceNoteLimit) {
|
||||
return <Toast onClose={hideToast}>{i18n('icu:voiceNoteLimit')}</Toast>;
|
||||
}
|
||||
|
||||
if (toastType === ToastType.VoiceNoteMustBeTheOnlyAttachment) {
|
||||
return (
|
||||
<Toast onClose={hideToast}>
|
||||
{i18n('icu:voiceNoteMustBeOnlyAttachment')}
|
||||
</Toast>
|
||||
);
|
||||
}
|
||||
|
||||
if (toastType === ToastType.WhoCanFindMeReadOnly) {
|
||||
return (
|
||||
<Toast onClose={hideToast}>{i18n('icu:WhoCanFindMeReadOnlyToast')}</Toast>
|
||||
|
@ -400,3 +507,43 @@ export function ToastManager({
|
|||
|
||||
throw missingCaseError(toastType);
|
||||
}
|
||||
|
||||
export function renderMegaphone({
|
||||
i18n,
|
||||
megaphone,
|
||||
}: PropsType): JSX.Element | null {
|
||||
if (!megaphone) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (megaphone.type === MegaphoneType.UsernameOnboarding) {
|
||||
return <UsernameMegaphone i18n={i18n} {...megaphone} />;
|
||||
}
|
||||
|
||||
throw missingCaseError(megaphone.type);
|
||||
}
|
||||
|
||||
export function ToastManager(props: PropsType): JSX.Element {
|
||||
const { centerToast, containerWidthBreakpoint, isCompositionAreaVisible } =
|
||||
props;
|
||||
|
||||
const toast = renderToast(props);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('ToastManager', {
|
||||
'ToastManager--narrow-sidebar':
|
||||
containerWidthBreakpoint === WidthBreakpoint.Narrow,
|
||||
'ToastManager--composition-area-visible': isCompositionAreaVisible,
|
||||
})}
|
||||
>
|
||||
{centerToast
|
||||
? createPortal(
|
||||
<div className="ToastManager__root">{toast}</div>,
|
||||
document.body
|
||||
)
|
||||
: toast}
|
||||
{renderMegaphone(props)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import type { PropsType } from './ToastStickerPackInstallFailed';
|
||||
import { ToastStickerPackInstallFailed } from './ToastStickerPackInstallFailed';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const defaultProps = {
|
||||
i18n,
|
||||
onClose: action('onClose'),
|
||||
};
|
||||
|
||||
export default {
|
||||
title: 'Components/ToastStickerPackInstallFailed',
|
||||
} satisfies Meta<PropsType>;
|
||||
|
||||
export const _ToastStickerPackInstallFailed = (): JSX.Element => (
|
||||
<ToastStickerPackInstallFailed {...defaultProps} />
|
||||
);
|
|
@ -1,22 +0,0 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { Toast } from './Toast';
|
||||
|
||||
export type PropsType = {
|
||||
i18n: LocalizerType;
|
||||
onClose: () => unknown;
|
||||
};
|
||||
|
||||
export function ToastStickerPackInstallFailed({
|
||||
i18n,
|
||||
onClose,
|
||||
}: PropsType): JSX.Element {
|
||||
return (
|
||||
<Toast onClose={onClose}>
|
||||
{i18n('icu:stickers--toast--InstallFailed')}
|
||||
</Toast>
|
||||
);
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { Toast } from './Toast';
|
||||
|
||||
type PropsType = {
|
||||
i18n: LocalizerType;
|
||||
onClose: () => unknown;
|
||||
};
|
||||
|
||||
export function ToastVoiceNoteLimit({ i18n, onClose }: PropsType): JSX.Element {
|
||||
return <Toast onClose={onClose}>{i18n('icu:voiceNoteError')}</Toast>;
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import type { PropsType } from './ToastVoiceNoteLimit';
|
||||
import { ToastVoiceNoteLimit } from './ToastVoiceNoteLimit';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const defaultProps = {
|
||||
i18n,
|
||||
onClose: action('onClose'),
|
||||
};
|
||||
|
||||
export default {
|
||||
title: 'Components/ToastVoiceNoteLimit',
|
||||
} satisfies Meta<PropsType>;
|
||||
|
||||
export const _ToastVoiceNoteLimit = (): JSX.Element => (
|
||||
<ToastVoiceNoteLimit {...defaultProps} />
|
||||
);
|
|
@ -1,15 +0,0 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { Toast } from './Toast';
|
||||
|
||||
export type PropsType = {
|
||||
i18n: LocalizerType;
|
||||
onClose: () => unknown;
|
||||
};
|
||||
|
||||
export function ToastVoiceNoteLimit({ i18n, onClose }: PropsType): JSX.Element {
|
||||
return <Toast onClose={onClose}>{i18n('icu:voiceNoteLimit')}</Toast>;
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import type { PropsType } from './ToastVoiceNoteMustBeOnlyAttachment';
|
||||
import { ToastVoiceNoteMustBeOnlyAttachment } from './ToastVoiceNoteMustBeOnlyAttachment';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const defaultProps = {
|
||||
i18n,
|
||||
onClose: action('onClose'),
|
||||
};
|
||||
|
||||
export default {
|
||||
title: 'Components/ToastVoiceNoteMustBeOnlyAttachment',
|
||||
} satisfies Meta<PropsType>;
|
||||
|
||||
export const _ToastVoiceNoteMustBeOnlyAttachment = (): JSX.Element => (
|
||||
<ToastVoiceNoteMustBeOnlyAttachment {...defaultProps} />
|
||||
);
|
|
@ -1,20 +0,0 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { Toast } from './Toast';
|
||||
|
||||
export type PropsType = {
|
||||
i18n: LocalizerType;
|
||||
onClose: () => unknown;
|
||||
};
|
||||
|
||||
export function ToastVoiceNoteMustBeOnlyAttachment({
|
||||
i18n,
|
||||
onClose,
|
||||
}: PropsType): JSX.Element {
|
||||
return (
|
||||
<Toast onClose={onClose}>{i18n('icu:voiceNoteMustBeOnlyAttachment')}</Toast>
|
||||
);
|
||||
}
|
27
ts/components/UsernameMegaphone.stories.tsx
Normal file
27
ts/components/UsernameMegaphone.stories.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import type { PropsType } from './UsernameMegaphone';
|
||||
import { UsernameMegaphone } from './UsernameMegaphone';
|
||||
import { type ComponentMeta } from '../storybook/types';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
export default {
|
||||
title: 'Components/UsernameMegaphone',
|
||||
component: UsernameMegaphone,
|
||||
argTypes: {},
|
||||
args: {
|
||||
i18n,
|
||||
onLearnMore: action('onLearnMore'),
|
||||
onDismiss: action('onDismiss'),
|
||||
},
|
||||
} satisfies ComponentMeta<PropsType>;
|
||||
|
||||
export function Defaults(args: PropsType): JSX.Element {
|
||||
return <UsernameMegaphone {...args} />;
|
||||
}
|
50
ts/components/UsernameMegaphone.tsx
Normal file
50
ts/components/UsernameMegaphone.tsx
Normal file
|
@ -0,0 +1,50 @@
|
|||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import type { UsernameOnboardingActionableMegaphoneType } from '../types/Megaphone';
|
||||
import { Button, ButtonSize, ButtonVariant } from './Button';
|
||||
|
||||
export type PropsType = {
|
||||
i18n: LocalizerType;
|
||||
} & Omit<UsernameOnboardingActionableMegaphoneType, 'type'>;
|
||||
|
||||
export function UsernameMegaphone({
|
||||
i18n,
|
||||
onLearnMore,
|
||||
onDismiss,
|
||||
}: PropsType): JSX.Element {
|
||||
return (
|
||||
<div className="UsernameMegaphone">
|
||||
<div className="UsernameMegaphone__row">
|
||||
<i className="UsernameMegaphone__row__icon" />
|
||||
|
||||
<div className="UsernameMegaphone__row__text">
|
||||
<h2>{i18n('icu:UsernameMegaphone__title')}</h2>
|
||||
<p>{i18n('icu:UsernameMegaphone__body')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="UsernameMegaphone__buttons">
|
||||
<Button
|
||||
className="UsernameMegaphone__buttons__button"
|
||||
variant={ButtonVariant.SecondaryAffirmative}
|
||||
size={ButtonSize.Small}
|
||||
onClick={onDismiss}
|
||||
>
|
||||
{i18n('icu:UsernameMegaphone__dismiss')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
className="UsernameMegaphone__buttons__button"
|
||||
variant={ButtonVariant.SecondaryAffirmative}
|
||||
size={ButtonSize.Small}
|
||||
onClick={onLearnMore}
|
||||
>
|
||||
{i18n('icu:UsernameMegaphone__learn-more')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -8,23 +8,25 @@ import { action } from '@storybook/addon-actions';
|
|||
import enMessages from '../../_locales/en/messages.json';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
|
||||
import type { PropsType } from './UsernameOnboardingModalBody';
|
||||
import { UsernameOnboardingModalBody } from './UsernameOnboardingModalBody';
|
||||
import type { PropsType } from './UsernameOnboardingModal';
|
||||
import { UsernameOnboardingModal } from './UsernameOnboardingModal';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
export default {
|
||||
component: UsernameOnboardingModalBody,
|
||||
title: 'Components/UsernameOnboardingModalBody',
|
||||
component: UsernameOnboardingModal,
|
||||
title: 'Components/UsernameOnboardingModal',
|
||||
args: {
|
||||
i18n,
|
||||
onNext: action('onNext'),
|
||||
onSkip: action('onSkip'),
|
||||
onClose: action('onClose'),
|
||||
},
|
||||
} satisfies Meta<PropsType>;
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
const Template: StoryFn<PropsType> = args => {
|
||||
return <UsernameOnboardingModalBody {...args} />;
|
||||
return <UsernameOnboardingModal {...args} />;
|
||||
};
|
||||
|
||||
export const Normal = Template.bind({});
|
80
ts/components/UsernameOnboardingModal.tsx
Normal file
80
ts/components/UsernameOnboardingModal.tsx
Normal file
|
@ -0,0 +1,80 @@
|
|||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { Button, ButtonVariant } from './Button';
|
||||
import { Modal } from './Modal';
|
||||
|
||||
export type PropsType = Readonly<{
|
||||
i18n: LocalizerType;
|
||||
onNext: () => void;
|
||||
onSkip: () => void;
|
||||
onClose: () => void;
|
||||
}>;
|
||||
|
||||
export function UsernameOnboardingModal({
|
||||
i18n,
|
||||
onNext,
|
||||
onSkip,
|
||||
onClose,
|
||||
}: PropsType): JSX.Element {
|
||||
return (
|
||||
<Modal
|
||||
modalName="UsernameOnboardingModal"
|
||||
hasXButton
|
||||
i18n={i18n}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className="UsernameOnboardingModal">
|
||||
<div className="UsernameOnboardingModal__title">
|
||||
{i18n('icu:UsernameOnboardingModalBody__title')}
|
||||
</div>
|
||||
|
||||
<div className="UsernameOnboardingModal__row">
|
||||
<div className="UsernameOnboardingModal__row__icon UsernameOnboardingModal__row__icon--number" />
|
||||
|
||||
<div className="UsernameOnboardingModal__row__body">
|
||||
<h2>
|
||||
{i18n('icu:UsernameOnboardingModalBody__row__number__title')}
|
||||
</h2>
|
||||
{i18n('icu:UsernameOnboardingModalBody__row__number__body')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="UsernameOnboardingModal__row">
|
||||
<div className="UsernameOnboardingModal__row__icon UsernameOnboardingModal__row__icon--username" />
|
||||
|
||||
<div className="UsernameOnboardingModal__row__body">
|
||||
<h2>
|
||||
{i18n('icu:UsernameOnboardingModalBody__row__username__title')}
|
||||
</h2>
|
||||
{i18n('icu:UsernameOnboardingModalBody__row__username__body')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="UsernameOnboardingModal__row">
|
||||
<div className="UsernameOnboardingModal__row__icon UsernameOnboardingModal__row__icon--qr" />
|
||||
|
||||
<div className="UsernameOnboardingModal__row__body">
|
||||
<h2>{i18n('icu:UsernameOnboardingModalBody__row__qr__title')}</h2>
|
||||
{i18n('icu:UsernameOnboardingModalBody__row__qr__body')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button className="UsernameOnboardingModal__submit" onClick={onNext}>
|
||||
{i18n('icu:UsernameOnboardingModalBody__continue')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
className="UsernameOnboardingModal__skip"
|
||||
variant={ButtonVariant.SecondaryAffirmative}
|
||||
onClick={onSkip}
|
||||
>
|
||||
{i18n('icu:UsernameOnboardingModalBody__skip')}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { Button } from './Button';
|
||||
|
||||
export type PropsType = Readonly<{
|
||||
i18n: LocalizerType;
|
||||
onNext: () => void;
|
||||
}>;
|
||||
|
||||
const CLASS = 'UsernameOnboardingModalBody';
|
||||
|
||||
const SUPPORT_URL = 'https://support.signal.org/hc/articles/5389476324250';
|
||||
|
||||
export function UsernameOnboardingModalBody({
|
||||
i18n,
|
||||
onNext,
|
||||
}: PropsType): JSX.Element {
|
||||
return (
|
||||
<div className={CLASS}>
|
||||
<div className={`${CLASS}__large-at`} />
|
||||
|
||||
<div className={`${CLASS}__title`}>
|
||||
{i18n('icu:UsernameOnboardingModalBody__title')}
|
||||
</div>
|
||||
|
||||
<div className={`${CLASS}__row`}>
|
||||
<div className={`${CLASS}__row__icon ${CLASS}__row__icon--number`} />
|
||||
|
||||
<div className={`${CLASS}__row__body`}>
|
||||
{i18n('icu:UsernameOnboardingModalBody__row__number')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`${CLASS}__row`}>
|
||||
<div className={`${CLASS}__row__icon ${CLASS}__row__icon--link`} />
|
||||
|
||||
<div className={`${CLASS}__row__body`}>
|
||||
{i18n('icu:UsernameOnboardingModalBody__row__link')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`${CLASS}__row`}>
|
||||
<div className={`${CLASS}__row__icon ${CLASS}__row__icon--lock`} />
|
||||
|
||||
<div className={`${CLASS}__row__body`}>
|
||||
{i18n('icu:UsernameOnboardingModalBody__row__lock')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`${CLASS}__row ${CLASS}__row--center`}>
|
||||
<a
|
||||
className={`${CLASS}__learn-more`}
|
||||
href={SUPPORT_URL}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{i18n('icu:UsernameOnboardingModalBody__learn-more')}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<Button className={`${CLASS}__submit`} onClick={onNext}>
|
||||
{i18n('icu:UsernameOnboardingModalBody__continue')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,11 +1,12 @@
|
|||
// Copyright 2016 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import type { ShowToastAction } from '../../state/ducks/toast';
|
||||
import type { AttachmentDraftType } from '../../types/Attachment';
|
||||
import type { LocalizerType } from '../../types/Util';
|
||||
import { ToastVoiceNoteMustBeOnlyAttachment } from '../ToastVoiceNoteMustBeOnlyAttachment';
|
||||
import { ToastType } from '../../types/Toast';
|
||||
import {
|
||||
useStartRecordingShortcut,
|
||||
useKeyboardShortcuts,
|
||||
|
@ -16,6 +17,7 @@ export type PropsType = {
|
|||
draftAttachments: ReadonlyArray<AttachmentDraftType>;
|
||||
i18n: LocalizerType;
|
||||
startRecording: (id: string) => unknown;
|
||||
showToast: ShowToastAction;
|
||||
};
|
||||
|
||||
export function AudioCapture({
|
||||
|
@ -23,9 +25,8 @@ export function AudioCapture({
|
|||
draftAttachments,
|
||||
i18n,
|
||||
startRecording,
|
||||
showToast,
|
||||
}: PropsType): JSX.Element {
|
||||
const [showOnlyAttachmentToast, setShowOnlyAttachmentToast] = useState(false);
|
||||
|
||||
const recordConversation = useCallback(
|
||||
() => startRecording(conversationId),
|
||||
[conversationId, startRecording]
|
||||
|
@ -33,40 +34,23 @@ export function AudioCapture({
|
|||
const startRecordingShortcut = useStartRecordingShortcut(recordConversation);
|
||||
useKeyboardShortcuts(startRecordingShortcut);
|
||||
|
||||
const handleCloseToast = useCallback(() => {
|
||||
setShowOnlyAttachmentToast(false);
|
||||
}, []);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (draftAttachments.length) {
|
||||
setShowOnlyAttachmentToast(true);
|
||||
showToast({ toastType: ToastType.VoiceNoteMustBeTheOnlyAttachment });
|
||||
} else {
|
||||
startRecording(conversationId);
|
||||
}
|
||||
}, [
|
||||
conversationId,
|
||||
draftAttachments,
|
||||
setShowOnlyAttachmentToast,
|
||||
startRecording,
|
||||
]);
|
||||
}, [conversationId, draftAttachments, showToast, startRecording]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="AudioCapture">
|
||||
<button
|
||||
aria-label={i18n('icu:voiceRecording--start')}
|
||||
className="AudioCapture__microphone"
|
||||
onClick={handleClick}
|
||||
title={i18n('icu:voiceRecording--start')}
|
||||
type="button"
|
||||
/>
|
||||
</div>
|
||||
{showOnlyAttachmentToast && (
|
||||
<ToastVoiceNoteMustBeOnlyAttachment
|
||||
i18n={i18n}
|
||||
onClose={handleCloseToast}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
<div className="AudioCapture">
|
||||
<button
|
||||
aria-label={i18n('icu:voiceRecording--start')}
|
||||
className="AudioCapture__microphone"
|
||||
onClick={handleClick}
|
||||
title={i18n('icu:voiceRecording--start')}
|
||||
type="button"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
import type { ThunkAction } from 'redux-thunk';
|
||||
import type { ReadonlyDeep } from 'type-fest';
|
||||
import type { StateType as RootStateType } from '../reducer';
|
||||
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
|
||||
import { useBoundActions } from '../../hooks/useBoundActions';
|
||||
import * as log from '../../logging/log';
|
||||
|
||||
// State
|
||||
|
@ -57,6 +59,9 @@ export const actions = {
|
|||
openStandalone,
|
||||
};
|
||||
|
||||
export const useAppActions = (): BoundActionCreatorsMapObject<typeof actions> =>
|
||||
useBoundActions(actions);
|
||||
|
||||
function initialLoadComplete(): InitialLoadCompleteActionType {
|
||||
return {
|
||||
type: INITIAL_LOAD_COMPLETE,
|
||||
|
|
|
@ -2,11 +2,14 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ReadonlyDeep } from 'type-fest';
|
||||
import type { ThunkAction } from 'redux-thunk';
|
||||
|
||||
import * as log from '../../logging/log';
|
||||
import { showToast } from '../../util/showToast';
|
||||
import * as Errors from '../../types/errors';
|
||||
import { ToastLinkCopied } from '../../components/ToastLinkCopied';
|
||||
import { ToastDebugLogError } from '../../components/ToastDebugLogError';
|
||||
import { ToastType } from '../../types/Toast';
|
||||
import type { StateType as RootStateType } from '../reducer';
|
||||
import { showToast } from './toast';
|
||||
import type { ShowToastActionType } from './toast';
|
||||
import type { PromiseAction } from '../util';
|
||||
|
||||
// State
|
||||
|
@ -45,12 +48,43 @@ function setCrashReportCount(count: number): SetCrashReportCountActionType {
|
|||
return { type: SET_COUNT, payload: count };
|
||||
}
|
||||
|
||||
function uploadCrashReports(): PromiseAction<typeof UPLOAD> {
|
||||
return { type: UPLOAD, payload: window.IPC.crashReports.upload() };
|
||||
function uploadCrashReports(): ThunkAction<
|
||||
void,
|
||||
RootStateType,
|
||||
unknown,
|
||||
PromiseAction<typeof UPLOAD> | ShowToastActionType
|
||||
> {
|
||||
return dispatch => {
|
||||
async function run() {
|
||||
try {
|
||||
await window.IPC.crashReports.upload();
|
||||
dispatch(showToast({ toastType: ToastType.LinkCopied }));
|
||||
} catch (error) {
|
||||
dispatch(showToast({ toastType: ToastType.DebugLogError }));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
dispatch({ type: UPLOAD, payload: run() });
|
||||
};
|
||||
}
|
||||
|
||||
function eraseCrashReports(): PromiseAction<typeof ERASE> {
|
||||
return { type: ERASE, payload: window.IPC.crashReports.erase() };
|
||||
function eraseCrashReports(): ThunkAction<
|
||||
void,
|
||||
RootStateType,
|
||||
unknown,
|
||||
PromiseAction<typeof ERASE> | ShowToastActionType
|
||||
> {
|
||||
return dispatch => {
|
||||
async function run() {
|
||||
try {
|
||||
await window.IPC.crashReports.erase();
|
||||
} catch (error) {
|
||||
dispatch(showToast({ toastType: ToastType.DebugLogError }));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
dispatch({ type: ERASE, payload: run() });
|
||||
};
|
||||
}
|
||||
|
||||
// Reducer
|
||||
|
@ -87,9 +121,6 @@ export function reducer(
|
|||
action.type === `${UPLOAD}_FULFILLED` ||
|
||||
action.type === `${ERASE}_FULFILLED`
|
||||
) {
|
||||
if (action.type === `${UPLOAD}_FULFILLED`) {
|
||||
showToast(ToastLinkCopied);
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
count: 0,
|
||||
|
@ -107,8 +138,6 @@ export function reducer(
|
|||
`Failed to upload crash report due to error ${Errors.toLogFormat(error)}`
|
||||
);
|
||||
|
||||
showToast(ToastDebugLogError);
|
||||
|
||||
return {
|
||||
...state,
|
||||
count: 0,
|
||||
|
|
|
@ -16,10 +16,12 @@ import type {
|
|||
import type { MessagePropsType } from '../selectors/message';
|
||||
import type { RecipientsByConversation } from './stories';
|
||||
import type { SafetyNumberChangeSource } from '../../components/SafetyNumberChangeDialog';
|
||||
import type { EditState as ProfileEditorEditState } from '../../components/ProfileEditor';
|
||||
import type { StateType as RootStateType } from '../reducer';
|
||||
import * as Errors from '../../types/errors';
|
||||
import * as SingleServePromise from '../../services/singleServePromise';
|
||||
import * as Stickers from '../../types/Stickers';
|
||||
import { UsernameOnboardingState } from '../../types/globalModals';
|
||||
import * as log from '../../logging/log';
|
||||
import { getMessagePropsSelector } from '../selectors/message';
|
||||
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
|
||||
|
@ -96,7 +98,9 @@ export type GlobalModalsStateType = ReadonlyDeep<{
|
|||
isSignalConnectionsVisible: boolean;
|
||||
isStoriesSettingsVisible: boolean;
|
||||
isWhatsNewVisible: boolean;
|
||||
usernameOnboardingState: UsernameOnboardingState;
|
||||
profileEditorHasError: boolean;
|
||||
profileEditorInitialEditState: ProfileEditorEditState | undefined;
|
||||
safetyNumberChangedBlockingData?: SafetyNumberChangedBlockingDataType;
|
||||
safetyNumberModalContactId?: string;
|
||||
sendEditWarningData?: SendEditWarningDataType;
|
||||
|
@ -151,6 +155,7 @@ const CONFIRM_AUTH_ART_CREATOR_FULFILLED =
|
|||
'globalModals/CONFIRM_AUTH_ART_CREATOR_FULFILLED';
|
||||
const SHOW_EDIT_HISTORY_MODAL = 'globalModals/SHOW_EDIT_HISTORY_MODAL';
|
||||
const CLOSE_EDIT_HISTORY_MODAL = 'globalModals/CLOSE_EDIT_HISTORY_MODAL';
|
||||
const TOGGLE_USERNAME_ONBOARDING = 'globalModals/TOGGLE_USERNAME_ONBOARDING';
|
||||
|
||||
export type ContactModalStateType = ReadonlyDeep<{
|
||||
contactId: string;
|
||||
|
@ -206,6 +211,9 @@ type ToggleForwardMessagesModalActionType = ReadonlyDeep<{
|
|||
|
||||
type ToggleProfileEditorActionType = ReadonlyDeep<{
|
||||
type: typeof TOGGLE_PROFILE_EDITOR;
|
||||
payload: {
|
||||
initialEditState?: ProfileEditorEditState;
|
||||
};
|
||||
}>;
|
||||
|
||||
export type ToggleProfileEditorErrorActionType = ReadonlyDeep<{
|
||||
|
@ -231,6 +239,10 @@ type ToggleConfirmationModalActionType = ReadonlyDeep<{
|
|||
payload: boolean;
|
||||
}>;
|
||||
|
||||
type ToggleUsernameOnboardingActionType = ReadonlyDeep<{
|
||||
type: typeof TOGGLE_USERNAME_ONBOARDING;
|
||||
}>;
|
||||
|
||||
type ShowStoriesSettingsActionType = ReadonlyDeep<{
|
||||
type: typeof SHOW_STORIES_SETTINGS;
|
||||
}>;
|
||||
|
@ -368,6 +380,7 @@ export type GlobalModalsActionType = ReadonlyDeep<
|
|||
| ToggleProfileEditorErrorActionType
|
||||
| ToggleSafetyNumberModalActionType
|
||||
| ToggleSignalConnectionsModalActionType
|
||||
| ToggleUsernameOnboardingActionType
|
||||
>;
|
||||
|
||||
// Action Creators
|
||||
|
@ -406,6 +419,7 @@ export const actions = {
|
|||
toggleProfileEditorHasError,
|
||||
toggleSafetyNumberModal,
|
||||
toggleSignalConnectionsModal,
|
||||
toggleUsernameOnboarding,
|
||||
};
|
||||
|
||||
export const useGlobalModalActions = (): BoundActionCreatorsMapObject<
|
||||
|
@ -585,8 +599,10 @@ function toggleForwardMessagesModal(
|
|||
};
|
||||
}
|
||||
|
||||
function toggleProfileEditor(): ToggleProfileEditorActionType {
|
||||
return { type: TOGGLE_PROFILE_EDITOR };
|
||||
function toggleProfileEditor(
|
||||
initialEditState?: ProfileEditorEditState
|
||||
): ToggleProfileEditorActionType {
|
||||
return { type: TOGGLE_PROFILE_EDITOR, payload: { initialEditState } };
|
||||
}
|
||||
|
||||
function toggleProfileEditorHasError(): ToggleProfileEditorErrorActionType {
|
||||
|
@ -626,6 +642,10 @@ function toggleConfirmationModal(
|
|||
};
|
||||
}
|
||||
|
||||
function toggleUsernameOnboarding(): ToggleUsernameOnboardingActionType {
|
||||
return { type: TOGGLE_USERNAME_ONBOARDING };
|
||||
}
|
||||
|
||||
function showBlockingSafetyNumberChangeDialog(
|
||||
untrustedByConversation: RecipientsByConversation,
|
||||
explodedPromise: ExplodePromiseResultType<boolean>,
|
||||
|
@ -861,7 +881,9 @@ export function getEmptyState(): GlobalModalsStateType {
|
|||
isSignalConnectionsVisible: false,
|
||||
isStoriesSettingsVisible: false,
|
||||
isWhatsNewVisible: false,
|
||||
usernameOnboardingState: UsernameOnboardingState.NeverShown,
|
||||
profileEditorHasError: false,
|
||||
profileEditorInitialEditState: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -873,6 +895,7 @@ export function reducer(
|
|||
return {
|
||||
...state,
|
||||
isProfileEditorVisible: !state.isProfileEditorVisible,
|
||||
profileEditorInitialEditState: action.payload.initialEditState,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -983,6 +1006,16 @@ export function reducer(
|
|||
};
|
||||
}
|
||||
|
||||
if (action.type === TOGGLE_USERNAME_ONBOARDING) {
|
||||
return {
|
||||
...state,
|
||||
usernameOnboardingState:
|
||||
state.usernameOnboardingState === UsernameOnboardingState.Open
|
||||
? UsernameOnboardingState.Closed
|
||||
: UsernameOnboardingState.Open,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === SHOW_SEND_ANYWAY_DIALOG) {
|
||||
const { promiseUuid, source } = action.payload;
|
||||
|
||||
|
|
|
@ -27,20 +27,7 @@ export type ItemsStateType = ReadonlyDeep<
|
|||
[key: string]: unknown;
|
||||
remoteConfig?: RemoteConfigType;
|
||||
serverTimeSkew?: number;
|
||||
} & Partial<
|
||||
Pick<
|
||||
StorageAccessType,
|
||||
| 'universalExpireTimer'
|
||||
| 'defaultConversationColor'
|
||||
| 'customColors'
|
||||
| 'preferredLeftPaneWidth'
|
||||
| 'navTabsCollapsed'
|
||||
| 'preferredReactionEmoji'
|
||||
| 'areWeASubscriber'
|
||||
| 'usernameLinkColor'
|
||||
| 'usernameLink'
|
||||
>
|
||||
>
|
||||
} & Partial<StorageAccessType>
|
||||
>;
|
||||
|
||||
// Actions
|
||||
|
|
|
@ -23,6 +23,7 @@ export const SHOW_TOAST = 'toast/SHOW_TOAST';
|
|||
|
||||
type HideToastActionType = ReadonlyDeep<{
|
||||
type: typeof HIDE_TOAST;
|
||||
payload: AnyToast | undefined;
|
||||
}>;
|
||||
|
||||
// eslint-disable-next-line local-rules/type-alias-readonlydeep
|
||||
|
@ -36,9 +37,12 @@ export type ToastActionType = HideToastActionType | ShowToastActionType;
|
|||
|
||||
// Action Creators
|
||||
|
||||
function hideToast(): HideToastActionType {
|
||||
export type HideToastAction = ReadonlyDeep<(toast?: AnyToast) => void>;
|
||||
|
||||
function hideToast(toast?: AnyToast): HideToastActionType {
|
||||
return {
|
||||
type: HIDE_TOAST,
|
||||
payload: toast,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -80,6 +84,10 @@ export function reducer(
|
|||
action: Readonly<ToastActionType>
|
||||
): ToastStateType {
|
||||
if (action.type === HIDE_TOAST) {
|
||||
if (action.payload != null && state.toast !== action.payload) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
toast: undefined,
|
||||
|
|
|
@ -28,6 +28,8 @@ import {
|
|||
import { showToast } from './toast';
|
||||
import { ToastType } from '../../types/Toast';
|
||||
import type { ToastActionType } from './toast';
|
||||
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
|
||||
import { useBoundActions } from '../../hooks/useBoundActions';
|
||||
|
||||
export type UsernameReservationStateType = ReadonlyDeep<{
|
||||
state: UsernameReservationState;
|
||||
|
@ -129,6 +131,10 @@ export const actions = {
|
|||
markCompletedUsernameLinkOnboarding,
|
||||
};
|
||||
|
||||
export const useUsernameActions = (): BoundActionCreatorsMapObject<
|
||||
typeof actions
|
||||
> => useBoundActions(actions);
|
||||
|
||||
export function setUsernameEditState(
|
||||
editState: UsernameEditState
|
||||
): SetUsernameEditStateActionType {
|
||||
|
|
|
@ -5,6 +5,7 @@ import { createSelector } from 'reselect';
|
|||
|
||||
import type { StateType } from '../reducer';
|
||||
import type { GlobalModalsStateType } from '../ducks/globalModals';
|
||||
import { UsernameOnboardingState } from '../../types/globalModals';
|
||||
|
||||
export const getGlobalModalsState = (state: StateType): GlobalModalsStateType =>
|
||||
state.globalModals;
|
||||
|
@ -12,5 +13,11 @@ export const getGlobalModalsState = (state: StateType): GlobalModalsStateType =>
|
|||
export const isShowingAnyModal = createSelector(
|
||||
getGlobalModalsState,
|
||||
(globalModalsState): boolean =>
|
||||
Object.values(globalModalsState).some(value => Boolean(value))
|
||||
Object.entries(globalModalsState).some(([key, value]) => {
|
||||
if (key === 'usernameOnboardingState') {
|
||||
return value === UsernameOnboardingState.Open;
|
||||
}
|
||||
|
||||
return Boolean(value);
|
||||
})
|
||||
);
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import type { VerificationTransport } from '../../types/VerificationTransport';
|
||||
import { App } from '../../components/App';
|
||||
|
@ -12,18 +12,16 @@ import { SmartCallManager } from './CallManager';
|
|||
import { SmartGlobalModalContainer } from './GlobalModalContainer';
|
||||
import { SmartLightbox } from './Lightbox';
|
||||
import { SmartStoryViewer } from './StoryViewer';
|
||||
import type { StateType } from '../reducer';
|
||||
import {
|
||||
getIntl,
|
||||
getLocaleMessages,
|
||||
getTheme,
|
||||
getIsMainWindowMaximized,
|
||||
getIsMainWindowFullScreen,
|
||||
getMenuOptions,
|
||||
} from '../selectors/user';
|
||||
import { hasSelectedStoryData } from '../selectors/stories';
|
||||
import { getHideMenuBar } from '../selectors/items';
|
||||
import { mapDispatchToProps } from '../actions';
|
||||
import type { StateType } from '../reducer';
|
||||
import { useAppActions } from '../ducks/app';
|
||||
import { useConversationsActions } from '../ducks/conversations';
|
||||
import { useStoriesActions } from '../ducks/stories';
|
||||
import { ErrorBoundary } from '../../components/ErrorBoundary';
|
||||
import { ModalContainer } from '../../components/ModalContainer';
|
||||
import { SmartInbox } from './Inbox';
|
||||
|
@ -32,58 +30,58 @@ function renderInbox(): JSX.Element {
|
|||
return <SmartInbox />;
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: StateType) => {
|
||||
const i18n = getIntl(state);
|
||||
export function SmartApp(): JSX.Element {
|
||||
const app = useSelector((state: StateType) => state.app);
|
||||
|
||||
return {
|
||||
...state.app,
|
||||
i18n,
|
||||
localeMessages: getLocaleMessages(state),
|
||||
isMaximized: getIsMainWindowMaximized(state),
|
||||
isFullScreen: getIsMainWindowFullScreen(state),
|
||||
menuOptions: getMenuOptions(state),
|
||||
OS: OS.getName(),
|
||||
osClassName: OS.getClassName(),
|
||||
hideMenuBar: getHideMenuBar(state),
|
||||
renderCallManager: () => (
|
||||
<ModalContainer className="module-calling__modal-container">
|
||||
<SmartCallManager />
|
||||
</ModalContainer>
|
||||
),
|
||||
renderGlobalModalContainer: () => <SmartGlobalModalContainer />,
|
||||
renderLightbox: () => <SmartLightbox />,
|
||||
hasSelectedStoryData: hasSelectedStoryData(state),
|
||||
renderStoryViewer: (closeView: () => unknown) => (
|
||||
<ErrorBoundary name="App/renderStoryViewer" closeView={closeView}>
|
||||
<SmartStoryViewer />
|
||||
</ErrorBoundary>
|
||||
),
|
||||
renderInbox,
|
||||
requestVerification: (
|
||||
number: string,
|
||||
captcha: string,
|
||||
transport: VerificationTransport
|
||||
): Promise<{ sessionId: string }> => {
|
||||
const { server } = window.textsecure;
|
||||
strictAssert(server !== undefined, 'WebAPI not available');
|
||||
const { openInbox } = useAppActions();
|
||||
|
||||
return server.requestVerification(number, captcha, transport);
|
||||
},
|
||||
registerSingleDevice: (
|
||||
number: string,
|
||||
code: string,
|
||||
sessionId: string
|
||||
): Promise<void> => {
|
||||
return window
|
||||
.getAccountManager()
|
||||
.registerSingleDevice(number, code, sessionId);
|
||||
},
|
||||
theme: getTheme(state),
|
||||
const { scrollToMessage } = useConversationsActions();
|
||||
|
||||
toast: state.toast.toast,
|
||||
};
|
||||
};
|
||||
const { viewStory } = useStoriesActions();
|
||||
|
||||
const smart = connect(mapStateToProps, mapDispatchToProps);
|
||||
return (
|
||||
<App
|
||||
{...app}
|
||||
isMaximized={useSelector(getIsMainWindowMaximized)}
|
||||
isFullScreen={useSelector(getIsMainWindowFullScreen)}
|
||||
osClassName={OS.getClassName()}
|
||||
renderCallManager={() => (
|
||||
<ModalContainer className="module-calling__modal-container">
|
||||
<SmartCallManager />
|
||||
</ModalContainer>
|
||||
)}
|
||||
renderGlobalModalContainer={() => <SmartGlobalModalContainer />}
|
||||
renderLightbox={() => <SmartLightbox />}
|
||||
hasSelectedStoryData={useSelector(hasSelectedStoryData)}
|
||||
renderStoryViewer={(closeView: () => unknown) => (
|
||||
<ErrorBoundary name="App/renderStoryViewer" closeView={closeView}>
|
||||
<SmartStoryViewer />
|
||||
</ErrorBoundary>
|
||||
)}
|
||||
renderInbox={renderInbox}
|
||||
requestVerification={(
|
||||
number: string,
|
||||
captcha: string,
|
||||
transport: VerificationTransport
|
||||
): Promise<{ sessionId: string }> => {
|
||||
const { server } = window.textsecure;
|
||||
strictAssert(server !== undefined, 'WebAPI not available');
|
||||
|
||||
export const SmartApp = smart(App);
|
||||
return server.requestVerification(number, captcha, transport);
|
||||
}}
|
||||
registerSingleDevice={(
|
||||
number: string,
|
||||
code: string,
|
||||
sessionId: string
|
||||
): Promise<void> => {
|
||||
return window
|
||||
.getAccountManager()
|
||||
.registerSingleDevice(number, code, sessionId);
|
||||
}}
|
||||
theme={useSelector(getTheme)}
|
||||
openInbox={openInbox}
|
||||
scrollToMessage={scrollToMessage}
|
||||
viewStory={viewStory}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
getPreferredLeftPaneWidth,
|
||||
} from '../selectors/items';
|
||||
import { getIntl, getRegionCode } from '../selectors/user';
|
||||
import type { WidthBreakpoint } from '../../components/_util';
|
||||
import { CallsTab } from '../../components/CallsTab';
|
||||
import {
|
||||
getAllConversations,
|
||||
|
@ -23,6 +24,7 @@ import type {
|
|||
} from '../../types/CallDisposition';
|
||||
import type { ConversationType } from '../ducks/conversations';
|
||||
import { SmartConversationDetails } from './ConversationDetails';
|
||||
import { SmartToastManager } from './ToastManager';
|
||||
import { useCallingActions } from '../ducks/calling';
|
||||
import { getActiveCallState } from '../selectors/calling';
|
||||
import { useCallHistoryActions } from '../ducks/callHistory';
|
||||
|
@ -80,6 +82,12 @@ function renderConversationDetails(
|
|||
);
|
||||
}
|
||||
|
||||
function renderToastManager(props: {
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
}): JSX.Element {
|
||||
return <SmartToastManager disableMegaphone {...props} />;
|
||||
}
|
||||
|
||||
export function SmartCallsTab(): JSX.Element {
|
||||
const i18n = useSelector(getIntl);
|
||||
const navTabsCollapsed = useSelector(getNavTabsCollapsed);
|
||||
|
@ -172,6 +180,7 @@ export function SmartCallsTab(): JSX.Element {
|
|||
onOutgoingVideoCallInConversation={onOutgoingVideoCallInConversation}
|
||||
preferredLeftPaneWidth={preferredLeftPaneWidth}
|
||||
renderConversationDetails={renderConversationDetails}
|
||||
renderToastManager={renderToastManager}
|
||||
regionCode={regionCode}
|
||||
savePreferredLeftPaneWidth={savePreferredLeftPaneWidth}
|
||||
/>
|
||||
|
|
|
@ -14,10 +14,10 @@ import { usePrevious } from '../../hooks/usePrevious';
|
|||
import { TargetedMessageSource } from '../ducks/conversationsEnums';
|
||||
import type { ConversationsStateType } from '../ducks/conversations';
|
||||
import { useConversationsActions } from '../ducks/conversations';
|
||||
import { useToastActions } from '../ducks/toast';
|
||||
import type { StateType } from '../reducer';
|
||||
import { strictAssert } from '../../util/assert';
|
||||
import { showToast } from '../../util/showToast';
|
||||
import { ToastStickerPackInstallFailed } from '../../components/ToastStickerPackInstallFailed';
|
||||
import { ToastType } from '../../types/Toast';
|
||||
import { getNavTabsCollapsed } from '../selectors/items';
|
||||
import { useItemsActions } from '../ducks/items';
|
||||
import { getHasAnyFailedStorySends } from '../selectors/stories';
|
||||
|
@ -56,6 +56,7 @@ export function SmartChatsTab(): JSX.Element {
|
|||
} = useConversationsActions();
|
||||
const { showWhatsNewModal } = useGlobalModalActions();
|
||||
const { toggleNavTabsCollapse } = useItemsActions();
|
||||
const { showToast } = useToastActions();
|
||||
|
||||
const lastOpenedConversationId = useRef<string | undefined>();
|
||||
|
||||
|
@ -121,7 +122,7 @@ export function SmartChatsTab(): JSX.Element {
|
|||
}
|
||||
|
||||
function packInstallFailed() {
|
||||
showToast(ToastStickerPackInstallFailed);
|
||||
showToast({ toastType: ToastType.StickerPackInstallFailed });
|
||||
}
|
||||
|
||||
window.Whisper.events.on('pack-install-failed', packInstallFailed);
|
||||
|
@ -133,7 +134,7 @@ export function SmartChatsTab(): JSX.Element {
|
|||
window.Whisper.events.off('refreshConversation', refreshConversation);
|
||||
window.Whisper.events.off('setupAsNewDevice', unload);
|
||||
};
|
||||
}, [onConversationClosed, prevConversationId, showConversation]);
|
||||
}, [onConversationClosed, prevConversationId, showConversation, showToast]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedConversationId) {
|
||||
|
|
|
@ -7,6 +7,7 @@ import { CompositionRecording } from '../../components/CompositionRecording';
|
|||
import { mapDispatchToProps } from '../actions';
|
||||
import { useAudioRecorderActions } from '../ducks/audioRecorder';
|
||||
import { useComposerActions } from '../ducks/composer';
|
||||
import { useToastActions } from '../ducks/toast';
|
||||
import { getSelectedConversationId } from '../selectors/conversations';
|
||||
import { getIntl } from '../selectors/user';
|
||||
|
||||
|
@ -22,6 +23,7 @@ export function SmartCompositionRecording({
|
|||
const { cancelRecording, completeRecording } = useAudioRecorderActions();
|
||||
|
||||
const { sendMultiMediaMessage } = useComposerActions();
|
||||
const { hideToast, showToast } = useToastActions();
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
cancelRecording();
|
||||
|
@ -54,6 +56,8 @@ export function SmartCompositionRecording({
|
|||
errorRecording={mapDispatchToProps.errorRecording}
|
||||
addAttachment={mapDispatchToProps.addAttachment}
|
||||
completeRecording={mapDispatchToProps.completeRecording}
|
||||
showToast={showToast}
|
||||
hideToast={hideToast}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import { SmartContactModal } from './ContactModal';
|
|||
import { SmartEditHistoryMessagesModal } from './EditHistoryMessagesModal';
|
||||
import { SmartForwardMessagesModal } from './ForwardMessagesModal';
|
||||
import { SmartProfileEditorModal } from './ProfileEditorModal';
|
||||
import { SmartUsernameOnboardingModal } from './UsernameOnboardingModal';
|
||||
import { SmartSafetyNumberModal } from './SafetyNumberModal';
|
||||
import { SmartSendAnywayDialog } from './SendAnywayDialog';
|
||||
import { SmartShortcutGuideModal } from './ShortcutGuideModal';
|
||||
|
@ -31,6 +32,10 @@ function renderProfileEditor(): JSX.Element {
|
|||
return <SmartProfileEditorModal />;
|
||||
}
|
||||
|
||||
function renderUsernameOnboarding(): JSX.Element {
|
||||
return <SmartUsernameOnboardingModal />;
|
||||
}
|
||||
|
||||
function renderContactModal(): JSX.Element {
|
||||
return <SmartContactModal />;
|
||||
}
|
||||
|
@ -77,6 +82,7 @@ export function SmartGlobalModalContainer(): JSX.Element {
|
|||
isSignalConnectionsVisible,
|
||||
isStoriesSettingsVisible,
|
||||
isWhatsNewVisible,
|
||||
usernameOnboardingState,
|
||||
safetyNumberChangedBlockingData,
|
||||
safetyNumberModalContactId,
|
||||
sendEditWarningData,
|
||||
|
@ -157,6 +163,7 @@ export function SmartGlobalModalContainer(): JSX.Element {
|
|||
renderDeleteMessagesModal={renderDeleteMessagesModal}
|
||||
renderForwardMessagesModal={renderForwardMessagesModal}
|
||||
renderProfileEditor={renderProfileEditor}
|
||||
renderUsernameOnboarding={renderUsernameOnboarding}
|
||||
renderSafetyNumber={renderSafetyNumber}
|
||||
renderSendAnywayDialog={renderSendAnywayDialog}
|
||||
renderShortcutGuideModal={renderShortcutGuideModal}
|
||||
|
@ -171,6 +178,7 @@ export function SmartGlobalModalContainer(): JSX.Element {
|
|||
theme={theme}
|
||||
toggleSignalConnectionsModal={toggleSignalConnectionsModal}
|
||||
userNotFoundModalState={userNotFoundModalState}
|
||||
usernameOnboardingState={usernameOnboardingState}
|
||||
isAuthorizingArtCreator={isAuthorizingArtCreator}
|
||||
authArtCreatorData={authArtCreatorData}
|
||||
cancelAuthorizeArtCreator={cancelAuthorizeArtCreator}
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
} from '../../components/InstallScreen';
|
||||
import { InstallError } from '../../components/installScreen/InstallScreenErrorStep';
|
||||
import { MAX_DEVICE_NAME_LENGTH } from '../../components/installScreen/InstallScreenChoosingDeviceNameStep';
|
||||
import { WidthBreakpoint } from '../../components/_util';
|
||||
import { HTTPError } from '../../textsecure/Errors';
|
||||
import { isRecord } from '../../util/isRecord';
|
||||
import * as Errors from '../../types/errors';
|
||||
|
@ -32,6 +33,7 @@ import OS from '../../util/os/osMain';
|
|||
import { SECOND } from '../../util/durations';
|
||||
import { BackOff } from '../../util/BackOff';
|
||||
import { drop } from '../../util/drop';
|
||||
import { SmartToastManager } from './ToastManager';
|
||||
|
||||
type PropsType = ComponentProps<typeof InstallScreen>;
|
||||
|
||||
|
@ -328,5 +330,13 @@ export function SmartInstallScreen(): ReactElement {
|
|||
throw missingCaseError(state);
|
||||
}
|
||||
|
||||
return <InstallScreen {...props} />;
|
||||
return (
|
||||
<>
|
||||
<InstallScreen {...props} />
|
||||
<SmartToastManager
|
||||
disableMegaphone
|
||||
containerWidthBreakpoint={WidthBreakpoint.Narrow}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -77,6 +77,7 @@ import { SmartMessageSearchResult } from './MessageSearchResult';
|
|||
import { SmartNetworkStatus } from './NetworkStatus';
|
||||
import { SmartRelinkDialog } from './RelinkDialog';
|
||||
import { SmartUnsupportedOSDialog } from './UnsupportedOSDialog';
|
||||
import { SmartToastManager } from './ToastManager';
|
||||
import type { PropsType as SmartUnsupportedOSDialogPropsType } from './UnsupportedOSDialog';
|
||||
import { SmartUpdateDialog } from './UpdateDialog';
|
||||
import { SmartCaptchaDialog } from './CaptchaDialog';
|
||||
|
@ -116,6 +117,17 @@ function renderUnsupportedOSDialog(
|
|||
): JSX.Element {
|
||||
return <SmartUnsupportedOSDialog {...props} />;
|
||||
}
|
||||
function renderToastManager(props: {
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
}): JSX.Element {
|
||||
return <SmartToastManager {...props} />;
|
||||
}
|
||||
|
||||
function renderToastManagerWithoutMegaphone(props: {
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
}): JSX.Element {
|
||||
return <SmartToastManager disableMegaphone {...props} />;
|
||||
}
|
||||
|
||||
const getModeSpecificProps = (
|
||||
state: StateType
|
||||
|
@ -223,6 +235,10 @@ const mapStateToProps = (state: StateType) => {
|
|||
unsupportedOSDialogType = 'warning';
|
||||
}
|
||||
|
||||
const composerStep = getComposerStep(state);
|
||||
const showArchived = getShowArchived(state);
|
||||
const hasSearchQuery = isSearching(state);
|
||||
|
||||
return {
|
||||
hasNetworkDialog: hasNetworkDialog(state),
|
||||
hasExpiredDialog,
|
||||
|
@ -238,7 +254,7 @@ const mapStateToProps = (state: StateType) => {
|
|||
preferredWidthFromStorage: getPreferredLeftPaneWidth(state),
|
||||
selectedConversationId: getSelectedConversationId(state),
|
||||
targetedMessageId: getTargetedMessage(state)?.id,
|
||||
showArchived: getShowArchived(state),
|
||||
showArchived,
|
||||
getPreferredBadge: getPreferredBadgeSelector(state),
|
||||
i18n: getIntl(state),
|
||||
isMacOS: getIsMacOS(state),
|
||||
|
@ -253,6 +269,10 @@ const mapStateToProps = (state: StateType) => {
|
|||
renderCrashReportDialog,
|
||||
renderExpiredBuildDialog,
|
||||
renderUnsupportedOSDialog,
|
||||
renderToastManager:
|
||||
composerStep == null && !showArchived && !hasSearchQuery
|
||||
? renderToastManager
|
||||
: renderToastManagerWithoutMegaphone,
|
||||
lookupConversationWithoutServiceId,
|
||||
theme: getTheme(state),
|
||||
};
|
||||
|
|
|
@ -14,7 +14,6 @@ import { getIntl } from '../selectors/user';
|
|||
import {
|
||||
getEmojiSkinTone,
|
||||
getUsernamesEnabled,
|
||||
getHasCompletedUsernameOnboarding,
|
||||
getHasCompletedUsernameLinkOnboarding,
|
||||
getUsernameCorrupted,
|
||||
getUsernameLinkColor,
|
||||
|
@ -53,8 +52,6 @@ function mapStateToProps(
|
|||
const recentEmojis = selectRecentEmojis(state);
|
||||
const skinTone = getEmojiSkinTone(state);
|
||||
const isUsernameFlagEnabled = getUsernamesEnabled(state);
|
||||
const hasCompletedUsernameOnboarding =
|
||||
getHasCompletedUsernameOnboarding(state);
|
||||
const hasCompletedUsernameLinkOnboarding =
|
||||
getHasCompletedUsernameLinkOnboarding(state);
|
||||
const usernameEditState = getUsernameEditState(state);
|
||||
|
@ -72,9 +69,9 @@ function mapStateToProps(
|
|||
conversationId,
|
||||
familyName,
|
||||
firstName: String(firstName),
|
||||
hasCompletedUsernameOnboarding,
|
||||
hasCompletedUsernameLinkOnboarding,
|
||||
hasError: state.globalModals.profileEditorHasError,
|
||||
initialEditState: state.globalModals.profileEditorInitialEditState,
|
||||
i18n: getIntl(state),
|
||||
isUsernameFlagEnabled,
|
||||
recentEmojis,
|
||||
|
|
|
@ -7,6 +7,8 @@ import { useSelector } from 'react-redux';
|
|||
import type { LocalizerType } from '../../types/Util';
|
||||
import type { StateType } from '../reducer';
|
||||
import { SmartStoryCreator } from './StoryCreator';
|
||||
import { SmartToastManager } from './ToastManager';
|
||||
import type { WidthBreakpoint } from '../../components/_util';
|
||||
import { StoriesTab } from '../../components/StoriesTab';
|
||||
import { getMaximumOutgoingAttachmentSizeInKb } from '../../types/AttachmentSize';
|
||||
import type { ConfigKeyType } from '../../RemoteConfig';
|
||||
|
@ -38,6 +40,12 @@ function renderStoryCreator(): JSX.Element {
|
|||
return <SmartStoryCreator />;
|
||||
}
|
||||
|
||||
function renderToastManager(props: {
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
}): JSX.Element {
|
||||
return <SmartToastManager disableMegaphone {...props} />;
|
||||
}
|
||||
|
||||
export function SmartStoriesTab(): JSX.Element | null {
|
||||
const storiesActions = useStoriesActions();
|
||||
const {
|
||||
|
@ -122,6 +130,7 @@ export function SmartStoriesTab(): JSX.Element | null {
|
|||
preferredLeftPaneWidth={preferredLeftPaneWidth}
|
||||
preferredWidthFromStorage={preferredWidthFromStorage}
|
||||
renderStoryCreator={renderStoryCreator}
|
||||
renderToastManager={renderToastManager}
|
||||
retryMessageSend={retryMessageSend}
|
||||
savePreferredLeftPaneWidth={savePreferredLeftPaneWidth}
|
||||
showConversation={showConversation}
|
||||
|
|
107
ts/state/smart/ToastManager.tsx
Normal file
107
ts/state/smart/ToastManager.tsx
Normal file
|
@ -0,0 +1,107 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import type { AnyActionableMegaphone } from '../../types/Megaphone';
|
||||
import { MegaphoneType } from '../../types/Megaphone';
|
||||
import { UsernameOnboardingState } from '../../types/globalModals';
|
||||
import OS from '../../util/os/osMain';
|
||||
import { drop } from '../../util/drop';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import {
|
||||
getGlobalModalsState,
|
||||
isShowingAnyModal as getIsShowingAnyModal,
|
||||
} from '../selectors/globalModals';
|
||||
import { hasSelectedStoryData } from '../selectors/stories';
|
||||
import { shouldShowLightbox } from '../selectors/lightbox';
|
||||
import { isInFullScreenCall as getIsInFullScreenCall } from '../selectors/calling';
|
||||
import { getSelectedNavTab } from '../selectors/nav';
|
||||
import type { StateType } from '../reducer';
|
||||
import { useConversationsActions } from '../ducks/conversations';
|
||||
import type { ConversationsStateType } from '../ducks/conversations';
|
||||
import { useToastActions } from '../ducks/toast';
|
||||
import { useGlobalModalActions } from '../ducks/globalModals';
|
||||
import { NavTab } from '../ducks/nav';
|
||||
import {
|
||||
getUsernamesEnabled,
|
||||
getHasCompletedUsernameOnboarding,
|
||||
} from '../selectors/items';
|
||||
import { ToastManager } from '../../components/ToastManager';
|
||||
import type { WidthBreakpoint } from '../../components/_util';
|
||||
|
||||
export type SmartPropsType = Readonly<{
|
||||
disableMegaphone?: boolean;
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
}>;
|
||||
|
||||
export function SmartToastManager({
|
||||
disableMegaphone = false,
|
||||
containerWidthBreakpoint,
|
||||
}: SmartPropsType): JSX.Element {
|
||||
const i18n = useSelector(getIntl);
|
||||
const isUsernameFlagEnabled = useSelector(getUsernamesEnabled);
|
||||
const hasCompletedUsernameOnboarding = useSelector(
|
||||
getHasCompletedUsernameOnboarding
|
||||
);
|
||||
const toast = useSelector((state: StateType) => state.toast.toast);
|
||||
const globalModals = useSelector(getGlobalModalsState);
|
||||
const isShowingAnyModal = useSelector(getIsShowingAnyModal);
|
||||
const isShowingStory = useSelector(hasSelectedStoryData);
|
||||
const isShowingLightbox = useSelector(shouldShowLightbox);
|
||||
const isInFullScreenCall = useSelector(getIsInFullScreenCall);
|
||||
|
||||
const selectedNavTab = useSelector(getSelectedNavTab);
|
||||
const { selectedConversationId } = useSelector<
|
||||
StateType,
|
||||
ConversationsStateType
|
||||
>(state => state.conversations);
|
||||
|
||||
const { onUndoArchive } = useConversationsActions();
|
||||
|
||||
const { openFileInFolder, hideToast } = useToastActions();
|
||||
|
||||
const { toggleUsernameOnboarding } = useGlobalModalActions();
|
||||
|
||||
let megaphone: AnyActionableMegaphone | undefined;
|
||||
|
||||
if (
|
||||
isUsernameFlagEnabled &&
|
||||
!hasCompletedUsernameOnboarding &&
|
||||
globalModals.usernameOnboardingState === UsernameOnboardingState.NeverShown
|
||||
) {
|
||||
megaphone = {
|
||||
type: MegaphoneType.UsernameOnboarding,
|
||||
onLearnMore: toggleUsernameOnboarding,
|
||||
onDismiss: () => {
|
||||
drop(window.storage.put('hasCompletedUsernameOnboarding', true));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const centerToast =
|
||||
isShowingAnyModal ||
|
||||
isShowingStory ||
|
||||
isShowingLightbox ||
|
||||
isInFullScreenCall;
|
||||
|
||||
const isCompositionAreaVisible =
|
||||
selectedNavTab === NavTab.Chats && Boolean(selectedConversationId);
|
||||
|
||||
return (
|
||||
<ToastManager
|
||||
i18n={i18n}
|
||||
OS={OS.getName()}
|
||||
toast={toast}
|
||||
megaphone={disableMegaphone ? undefined : megaphone}
|
||||
onShowDebugLog={() => window.IPC.showDebugLog()}
|
||||
onUndoArchive={onUndoArchive}
|
||||
openFileInFolder={openFileInFolder}
|
||||
hideToast={hideToast}
|
||||
centerToast={centerToast}
|
||||
containerWidthBreakpoint={containerWidthBreakpoint}
|
||||
isCompositionAreaVisible={isCompositionAreaVisible}
|
||||
/>
|
||||
);
|
||||
}
|
43
ts/state/smart/UsernameOnboardingModal.tsx
Normal file
43
ts/state/smart/UsernameOnboardingModal.tsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { UsernameOnboardingModal } from '../../components/UsernameOnboardingModal';
|
||||
import { EditState } from '../../components/ProfileEditor';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import { useGlobalModalActions } from '../ducks/globalModals';
|
||||
import { useUsernameActions } from '../ducks/username';
|
||||
|
||||
export function SmartUsernameOnboardingModal(): JSX.Element {
|
||||
const i18n = useSelector(getIntl);
|
||||
const { toggleProfileEditor, toggleUsernameOnboarding } =
|
||||
useGlobalModalActions();
|
||||
const { openUsernameReservationModal } = useUsernameActions();
|
||||
|
||||
const onNext = useCallback(async () => {
|
||||
await window.storage.put('hasCompletedUsernameOnboarding', true);
|
||||
openUsernameReservationModal();
|
||||
toggleProfileEditor(EditState.Username);
|
||||
toggleUsernameOnboarding();
|
||||
}, [
|
||||
toggleProfileEditor,
|
||||
toggleUsernameOnboarding,
|
||||
openUsernameReservationModal,
|
||||
]);
|
||||
|
||||
const onSkip = useCallback(async () => {
|
||||
await window.storage.put('hasCompletedUsernameOnboarding', true);
|
||||
toggleUsernameOnboarding();
|
||||
}, [toggleUsernameOnboarding]);
|
||||
|
||||
return (
|
||||
<UsernameOnboardingModal
|
||||
i18n={i18n}
|
||||
onNext={onNext}
|
||||
onSkip={onSkip}
|
||||
onClose={toggleUsernameOnboarding}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -54,7 +54,8 @@ describe('both/state/selectors/items', () => {
|
|||
0.1,
|
||||
1.2,
|
||||
NaN,
|
||||
].forEach(skinTone => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- for testing
|
||||
].forEach((skinTone: any) => {
|
||||
const state = getRootState({ skinTone });
|
||||
assert.strictEqual(getEmojiSkinTone(state), 0);
|
||||
});
|
||||
|
|
|
@ -156,9 +156,6 @@ describe('pnp/username', function (this: Mocha.Suite) {
|
|||
const profileEditor = window.locator('.ProfileEditor');
|
||||
await profileEditor.locator('.ProfileEditor__row >> "Username"').click();
|
||||
|
||||
debug('skipping onboarding');
|
||||
await profileEditor.locator('.module-Button >> "Continue"').click();
|
||||
|
||||
debug('entering new username');
|
||||
const usernameField = profileEditor.locator('.Input__input');
|
||||
await usernameField.type(NICKNAME);
|
||||
|
|
20
ts/types/Megaphone.ts
Normal file
20
ts/types/Megaphone.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
export enum MegaphoneType {
|
||||
UsernameOnboarding = 'UsernameOnboarding',
|
||||
}
|
||||
|
||||
export type UsernameOnboardingMegaphoneType = {
|
||||
type: MegaphoneType.UsernameOnboarding;
|
||||
};
|
||||
|
||||
export type UsernameOnboardingActionableMegaphoneType =
|
||||
UsernameOnboardingMegaphoneType & {
|
||||
onLearnMore: () => void;
|
||||
onDismiss: () => void;
|
||||
};
|
||||
|
||||
export type AnyMegaphone = UsernameOnboardingMegaphoneType;
|
||||
|
||||
export type AnyActionableMegaphone = UsernameOnboardingActionableMegaphoneType;
|
|
@ -8,6 +8,8 @@ export enum ToastType {
|
|||
Blocked = 'Blocked',
|
||||
BlockedGroup = 'BlockedGroup',
|
||||
CallHistoryCleared = 'CallHistoryCleared',
|
||||
CaptchaFailed = 'CaptchaFailed',
|
||||
CaptchaSolved = 'CaptchaSolved',
|
||||
CannotEditMessage = 'CannotEditMessage',
|
||||
CannotForwardEmptyMessage = 'CannotForwardEmptyMessage',
|
||||
CannotMixMultiAndNonMultiAttachments = 'CannotMixMultiAndNonMultiAttachments',
|
||||
|
@ -21,20 +23,28 @@ export enum ToastType {
|
|||
CopiedUsername = 'CopiedUsername',
|
||||
CopiedUsernameLink = 'CopiedUsernameLink',
|
||||
DangerousFileType = 'DangerousFileType',
|
||||
DecryptionError = 'DecryptionError',
|
||||
DebugLogError = 'DebugLogError',
|
||||
DeleteForEveryoneFailed = 'DeleteForEveryoneFailed',
|
||||
Error = 'Error',
|
||||
Expired = 'Expired',
|
||||
FailedToDeleteUsername = 'FailedToDeleteUsername',
|
||||
FailedToFetchPhoneNumber = 'FailedToFetchPhoneNumber',
|
||||
FailedToFetchUsername = 'FailedToFetchUsername',
|
||||
FileSaved = 'FileSaved',
|
||||
FileSize = 'FileSize',
|
||||
GroupLinkCopied = 'GroupLinkCopied',
|
||||
InvalidConversation = 'InvalidConversation',
|
||||
LeftGroup = 'LeftGroup',
|
||||
LinkCopied = 'LinkCopied',
|
||||
LoadingFullLogs = 'LoadingFullLogs',
|
||||
MaxAttachments = 'MaxAttachments',
|
||||
MessageBodyTooLong = 'MessageBodyTooLong',
|
||||
OriginalMessageNotFound = 'OriginalMessageNotFound',
|
||||
PinnedConversationsFull = 'PinnedConversationsFull',
|
||||
ReactionFailed = 'ReactionFailed',
|
||||
ReportedSpamAndBlocked = 'ReportedSpamAndBlocked',
|
||||
StickerPackInstallFailed = 'StickerPackInstallFailed',
|
||||
StoryMuted = 'StoryMuted',
|
||||
StoryReact = 'StoryReact',
|
||||
StoryReply = 'StoryReply',
|
||||
|
@ -48,6 +58,8 @@ export enum ToastType {
|
|||
UnsupportedMultiAttachment = 'UnsupportedMultiAttachment',
|
||||
UnsupportedOS = 'UnsupportedOS',
|
||||
UserAddedToGroup = 'UserAddedToGroup',
|
||||
VoiceNoteLimit = 'VoiceNoteLimit',
|
||||
VoiceNoteMustBeTheOnlyAttachment = 'VoiceNoteMustBeTheOnlyAttachment',
|
||||
WhoCanFindMeReadOnly = 'WhoCanFindMeReadOnly',
|
||||
}
|
||||
|
||||
|
@ -64,6 +76,8 @@ export type AnyToast =
|
|||
| { toastType: ToastType.CannotOpenGiftBadgeIncoming }
|
||||
| { toastType: ToastType.CannotOpenGiftBadgeOutgoing }
|
||||
| { toastType: ToastType.CannotStartGroupCall }
|
||||
| { toastType: ToastType.CaptchaFailed }
|
||||
| { toastType: ToastType.CaptchaSolved }
|
||||
| {
|
||||
toastType: ToastType.ConversationArchived;
|
||||
parameters: { conversationId: string };
|
||||
|
@ -74,23 +88,37 @@ export type AnyToast =
|
|||
| { toastType: ToastType.CopiedUsername }
|
||||
| { toastType: ToastType.CopiedUsernameLink }
|
||||
| { toastType: ToastType.DangerousFileType }
|
||||
| { toastType: ToastType.DebugLogError }
|
||||
| { toastType: ToastType.DeleteForEveryoneFailed }
|
||||
| { toastType: ToastType.Error }
|
||||
| { toastType: ToastType.Expired }
|
||||
| { toastType: ToastType.FailedToDeleteUsername }
|
||||
| { toastType: ToastType.FailedToFetchPhoneNumber }
|
||||
| { toastType: ToastType.FailedToFetchUsername }
|
||||
| { toastType: ToastType.FileSaved; parameters: { fullPath: string } }
|
||||
| {
|
||||
toastType: ToastType.FileSize;
|
||||
parameters: { limit: number; units: string };
|
||||
}
|
||||
| { toastType: ToastType.GroupLinkCopied }
|
||||
| {
|
||||
toastType: ToastType.DecryptionError;
|
||||
parameters: {
|
||||
name: string;
|
||||
deviceId: number;
|
||||
};
|
||||
}
|
||||
| { toastType: ToastType.InvalidConversation }
|
||||
| { toastType: ToastType.LeftGroup }
|
||||
| { toastType: ToastType.LinkCopied }
|
||||
| { toastType: ToastType.LoadingFullLogs }
|
||||
| { toastType: ToastType.MaxAttachments }
|
||||
| { toastType: ToastType.MessageBodyTooLong }
|
||||
| { toastType: ToastType.OriginalMessageNotFound }
|
||||
| { toastType: ToastType.PinnedConversationsFull }
|
||||
| { toastType: ToastType.ReactionFailed }
|
||||
| { toastType: ToastType.ReportedSpamAndBlocked }
|
||||
| { toastType: ToastType.StickerPackInstallFailed }
|
||||
| { toastType: ToastType.StoryMuted }
|
||||
| { toastType: ToastType.StoryReact }
|
||||
| { toastType: ToastType.StoryReply }
|
||||
|
@ -110,4 +138,6 @@ export type AnyToast =
|
|||
toastType: ToastType.UserAddedToGroup;
|
||||
parameters: { contact: string; group: string };
|
||||
}
|
||||
| { toastType: ToastType.VoiceNoteLimit }
|
||||
| { toastType: ToastType.VoiceNoteMustBeTheOnlyAttachment }
|
||||
| { toastType: ToastType.WhoCanFindMeReadOnly };
|
||||
|
|
8
ts/types/globalModals.ts
Normal file
8
ts/types/globalModals.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
export enum UsernameOnboardingState {
|
||||
NeverShown = 'NeverShown',
|
||||
Open = 'Open',
|
||||
Closed = 'Closed',
|
||||
}
|
|
@ -1,10 +1,9 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { showToast } from './showToast';
|
||||
import { ToastGroupLinkCopied } from '../components/ToastGroupLinkCopied';
|
||||
import { ToastType } from '../types/Toast';
|
||||
|
||||
export async function copyGroupLink(groupLink: string): Promise<void> {
|
||||
await window.navigator.clipboard.writeText(groupLink);
|
||||
showToast(ToastGroupLinkCopied);
|
||||
window.reduxActions.toast.showToast({ toastType: ToastType.GroupLinkCopied });
|
||||
}
|
||||
|
|
|
@ -18,11 +18,7 @@ import * as RemoteConfig from '../RemoteConfig';
|
|||
import { Address } from '../types/Address';
|
||||
import { QualifiedAddress } from '../types/QualifiedAddress';
|
||||
import type { AciString, ServiceIdString } from '../types/ServiceId';
|
||||
import {
|
||||
ToastInternalError,
|
||||
ToastInternalErrorKind,
|
||||
} from '../components/ToastInternalError';
|
||||
import { showToast } from './showToast';
|
||||
import { ToastType } from '../types/Toast';
|
||||
import * as Errors from '../types/errors';
|
||||
|
||||
import type { ConversationModel } from '../models/conversations';
|
||||
|
@ -200,11 +196,12 @@ function maybeShowDecryptionToast(
|
|||
}
|
||||
|
||||
log.info(`maybeShowDecryptionToast/${logId}: Showing decryption error toast`);
|
||||
showToast(ToastInternalError, {
|
||||
kind: ToastInternalErrorKind.DecryptionError,
|
||||
deviceId,
|
||||
name,
|
||||
onShowDebugLog: () => window.IPC.showDebugLog(),
|
||||
window.reduxActions.toast.showToast({
|
||||
toastType: ToastType.DecryptionError,
|
||||
parameters: {
|
||||
deviceId,
|
||||
name,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -3,14 +3,12 @@
|
|||
|
||||
import { usernames, LibSignalErrorBase } from '@signalapp/libsignal-client';
|
||||
|
||||
import { ToastFailedToFetchUsername } from '../components/ToastFailedToFetchUsername';
|
||||
import { ToastFailedToFetchPhoneNumber } from '../components/ToastFailedToFetchPhoneNumber';
|
||||
import type { UserNotFoundModalStateType } from '../state/ducks/globalModals';
|
||||
import * as log from '../logging/log';
|
||||
import type { AciString } from '../types/ServiceId';
|
||||
import * as Errors from '../types/errors';
|
||||
import { ToastType } from '../types/Toast';
|
||||
import { HTTPError } from '../textsecure/Errors';
|
||||
import { showToast } from './showToast';
|
||||
import { strictAssert } from './assert';
|
||||
import type { UUIDFetchStateKeyType } from './uuidFetchState';
|
||||
import { getServiceIdsForE164s } from './getServiceIdsForE164s';
|
||||
|
@ -121,9 +119,13 @@ export async function lookupConversationWithoutServiceId(
|
|||
);
|
||||
|
||||
if (options.type === 'e164') {
|
||||
showToast(ToastFailedToFetchPhoneNumber);
|
||||
window.reduxActions.toast.showToast({
|
||||
toastType: ToastType.FailedToFetchPhoneNumber,
|
||||
});
|
||||
} else {
|
||||
showToast(ToastFailedToFetchUsername);
|
||||
window.reduxActions.toast.showToast({
|
||||
toastType: ToastType.FailedToFetchUsername,
|
||||
});
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
|
|
@ -17,9 +17,8 @@ import { fileToBytes } from './fileToBytes';
|
|||
import { handleImageAttachment } from './handleImageAttachment';
|
||||
import { handleVideoAttachment } from './handleVideoAttachment';
|
||||
import { isHeic, stringToMIMEType } from '../types/MIME';
|
||||
import { ToastType } from '../types/Toast';
|
||||
import { isImageTypeSupported, isVideoTypeSupported } from './GoogleChrome';
|
||||
import { showToast } from './showToast';
|
||||
import { ToastFileSize } from '../components/ToastFileSize';
|
||||
|
||||
export async function processAttachment(
|
||||
file: File,
|
||||
|
@ -80,7 +79,10 @@ function isAttachmentSizeOkay(attachment: Readonly<AttachmentType>): boolean {
|
|||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
if ((attachment.data.byteLength / KIBIBYTE).toFixed(4) >= limitKb) {
|
||||
showToast(ToastFileSize, getRenderDetailsForLimit(limitKb));
|
||||
window.reduxActions.toast.showToast({
|
||||
toastType: ToastType.FileSize,
|
||||
parameters: getRenderDetailsForLimit(limitKb),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,58 +0,0 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
|
||||
import type { ToastCaptchaFailed } from '../components/ToastCaptchaFailed';
|
||||
import type { ToastCaptchaSolved } from '../components/ToastCaptchaSolved';
|
||||
import type {
|
||||
ToastInternalError,
|
||||
ToastPropsType as ToastInternalErrorPropsType,
|
||||
} from '../components/ToastInternalError';
|
||||
import type {
|
||||
ToastFileSize,
|
||||
ToastPropsType as ToastFileSizePropsType,
|
||||
} from '../components/ToastFileSize';
|
||||
import type { ToastGroupLinkCopied } from '../components/ToastGroupLinkCopied';
|
||||
import type { ToastLinkCopied } from '../components/ToastLinkCopied';
|
||||
import type { ToastLoadingFullLogs } from '../components/ToastLoadingFullLogs';
|
||||
|
||||
import type { ToastStickerPackInstallFailed } from '../components/ToastStickerPackInstallFailed';
|
||||
import type { ToastVoiceNoteLimit } from '../components/ToastVoiceNoteLimit';
|
||||
import type { ToastVoiceNoteMustBeOnlyAttachment } from '../components/ToastVoiceNoteMustBeOnlyAttachment';
|
||||
|
||||
export function showToast(Toast: typeof ToastCaptchaFailed): void;
|
||||
export function showToast(Toast: typeof ToastCaptchaSolved): void;
|
||||
export function showToast(
|
||||
Toast: typeof ToastInternalError,
|
||||
props: ToastInternalErrorPropsType
|
||||
): void;
|
||||
export function showToast(
|
||||
Toast: typeof ToastFileSize,
|
||||
props: ToastFileSizePropsType
|
||||
): void;
|
||||
export function showToast(Toast: typeof ToastGroupLinkCopied): void;
|
||||
export function showToast(Toast: typeof ToastLinkCopied): void;
|
||||
export function showToast(Toast: typeof ToastLoadingFullLogs): void;
|
||||
export function showToast(Toast: typeof ToastStickerPackInstallFailed): void;
|
||||
export function showToast(Toast: typeof ToastVoiceNoteLimit): void;
|
||||
export function showToast(
|
||||
Toast: typeof ToastVoiceNoteMustBeOnlyAttachment
|
||||
): void;
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
|
||||
export function showToast(Toast: any, props = {}): void {
|
||||
const node = document.getElementById('toast');
|
||||
|
||||
function onClose() {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
unmountComponentAtNode(node);
|
||||
}
|
||||
|
||||
render(<Toast i18n={window.i18n} onClose={onClose} {...props} />, node);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue