,
queueStoryDownload: action('queueStoryDownload'),
retryMessageSend: action('retryMessageSend'),
viewStory: action('viewStory'),
diff --git a/ts/components/MyStories.tsx b/ts/components/MyStories.tsx
index 51c94e64d3..4535a4905d 100644
--- a/ts/components/MyStories.tsx
+++ b/ts/components/MyStories.tsx
@@ -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}
>
diff --git a/ts/components/NavSidebar.tsx b/ts/components/NavSidebar.tsx
index 8f69609da6..1e83516a44 100644
--- a/ts/components/NavSidebar.tsx
+++ b/ts/components/NavSidebar.tsx
@@ -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 })}
);
}
diff --git a/ts/components/Preferences.tsx b/ts/components/Preferences.tsx
index d62a4e561f..3505f5a0bd 100644
--- a/ts/components/Preferences.tsx
+++ b/ts/components/Preferences.tsx
@@ -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}
/>
>
);
diff --git a/ts/components/ProfileEditor.stories.tsx b/ts/components/ProfileEditor.stories.tsx
index 64da826078..cf7ce5c643 100644
--- a/ts/components/ProfileEditor.stories.tsx
+++ b/ts/components/ProfileEditor.stories.tsx
@@ -79,7 +79,6 @@ export default {
replaceAvatar: action('replaceAvatar'),
resetUsernameLink: action('resetUsernameLink'),
saveAvatarToDisk: action('saveAvatarToDisk'),
- markCompletedUsernameOnboarding: action('markCompletedUsernameOnboarding'),
markCompletedUsernameLinkOnboarding: action(
'markCompletedUsernameLinkOnboarding'
),
diff --git a/ts/components/ProfileEditor.tsx b/ts/components/ProfileEditor.tsx
index 95a10e9b9d..0e258b1c7e 100644
--- a/ts/components/ProfileEditor.tsx
+++ b/ts/components/ProfileEditor.tsx
@@ -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;
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(null);
- const [editState, setEditState] = useState(EditState.None);
+ const [editState, setEditState] = useState(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 = (
- {
- markCompletedUsernameOnboarding();
- setEditState(EditState.Username);
- }}
- />
- );
} else if (editState === EditState.UsernameLink) {
content = (
,
renderStoryCreator: () => <>StoryCreator>,
retryMessageSend: action('retryMessageSend'),
showConversation: action('showConversation'),
diff --git a/ts/components/StoriesTab.tsx b/ts/components/StoriesTab.tsx
index b914bb7be4..684b5c9b66 100644
--- a/ts/components/StoriesTab.tsx
+++ b/ts/components/StoriesTab.tsx
@@ -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={
<>
(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 (
+ {
+ if (!disableCloseOnClick) {
+ onClose();
+ }
+ }}
+ onKeyDown={(ev: KeyboardEvent
) => {
+ if (ev.key === 'Enter' || ev.key === ' ') {
+ if (!disableCloseOnClick) {
+ onClose();
+ }
+ }
+ }}
+ role="button"
+ tabIndex={0}
+ style={style}
+ >
+ {children}
+ {toastAction && (
{
- if (!disableCloseOnClick) {
- onClose();
- }
+ className="Toast__button"
+ onClick={(ev: MouseEvent
) => {
+ ev.stopPropagation();
+ ev.preventDefault();
+ toastAction.onClick();
+ onClose();
}}
onKeyDown={(ev: KeyboardEvent) => {
if (ev.key === 'Enter' || ev.key === ' ') {
- if (!disableCloseOnClick) {
- onClose();
- }
+ ev.stopPropagation();
+ ev.preventDefault();
+ toastAction.onClick();
+ onClose();
}
}}
+ ref={focusRef}
role="button"
tabIndex={0}
- style={style}
>
- {children}
- {toastAction && (
- ) => {
- ev.stopPropagation();
- ev.preventDefault();
- toastAction.onClick();
- onClose();
- }}
- onKeyDown={(ev: KeyboardEvent) => {
- if (ev.key === 'Enter' || ev.key === ' ') {
- ev.stopPropagation();
- ev.preventDefault();
- toastAction.onClick();
- onClose();
- }
- }}
- ref={focusRef}
- role="button"
- tabIndex={0}
- >
- {toastAction.label}
-
- )}
- ,
- root
- )
- : null;
+ {toastAction.label}
+
+ )}
+
+ );
});
diff --git a/ts/components/ToastAlreadyRequestedToJoin.stories.tsx b/ts/components/ToastAlreadyRequestedToJoin.stories.tsx
deleted file mode 100644
index 01f397d33f..0000000000
--- a/ts/components/ToastAlreadyRequestedToJoin.stories.tsx
+++ /dev/null
@@ -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;
-
-export const _ToastAlreadyRequestedToJoin = (): JSX.Element => (
-
-);
diff --git a/ts/components/ToastAlreadyRequestedToJoin.tsx b/ts/components/ToastAlreadyRequestedToJoin.tsx
deleted file mode 100644
index a35af96724..0000000000
--- a/ts/components/ToastAlreadyRequestedToJoin.tsx
+++ /dev/null
@@ -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 (
-
- {i18n('icu:GroupV2--join--already-awaiting-approval')}
-
- );
-}
diff --git a/ts/components/ToastCaptchaFailed.stories.tsx b/ts/components/ToastCaptchaFailed.stories.tsx
deleted file mode 100644
index 7592b59c0a..0000000000
--- a/ts/components/ToastCaptchaFailed.stories.tsx
+++ /dev/null
@@ -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;
-
-export const _ToastCaptchaFailed = (): JSX.Element => (
-
-);
diff --git a/ts/components/ToastCaptchaFailed.tsx b/ts/components/ToastCaptchaFailed.tsx
deleted file mode 100644
index 1fd0dd64e1..0000000000
--- a/ts/components/ToastCaptchaFailed.tsx
+++ /dev/null
@@ -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 {i18n('icu:verificationFailed')};
-}
diff --git a/ts/components/ToastCaptchaSolved.stories.tsx b/ts/components/ToastCaptchaSolved.stories.tsx
deleted file mode 100644
index 838b997899..0000000000
--- a/ts/components/ToastCaptchaSolved.stories.tsx
+++ /dev/null
@@ -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;
-
-export const _ToastCaptchaSolved = (): JSX.Element => (
-
-);
diff --git a/ts/components/ToastCaptchaSolved.tsx b/ts/components/ToastCaptchaSolved.tsx
deleted file mode 100644
index 1b410fb1cc..0000000000
--- a/ts/components/ToastCaptchaSolved.tsx
+++ /dev/null
@@ -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 {i18n('icu:verificationComplete')};
-}
diff --git a/ts/components/ToastDebugLogError.stories.tsx b/ts/components/ToastDebugLogError.stories.tsx
deleted file mode 100644
index 0197d49d86..0000000000
--- a/ts/components/ToastDebugLogError.stories.tsx
+++ /dev/null
@@ -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;
-
-export const _ToastDebugLogError = (): JSX.Element => (
-
-);
diff --git a/ts/components/ToastDebugLogError.tsx b/ts/components/ToastDebugLogError.tsx
deleted file mode 100644
index c0c7f95a34..0000000000
--- a/ts/components/ToastDebugLogError.tsx
+++ /dev/null
@@ -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 {i18n('icu:debugLogError')};
-}
diff --git a/ts/components/ToastFailedToFetchPhoneNumber.tsx b/ts/components/ToastFailedToFetchPhoneNumber.tsx
deleted file mode 100644
index 2a34b77c90..0000000000
--- a/ts/components/ToastFailedToFetchPhoneNumber.tsx
+++ /dev/null
@@ -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 (
-
- {i18n('icu:Toast--failed-to-fetch-phone-number')}
-
- );
-}
diff --git a/ts/components/ToastFailedToFetchUsername.tsx b/ts/components/ToastFailedToFetchUsername.tsx
deleted file mode 100644
index e62cf33af9..0000000000
--- a/ts/components/ToastFailedToFetchUsername.tsx
+++ /dev/null
@@ -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 (
-
- {i18n('icu:Toast--failed-to-fetch-username')}
-
- );
-}
diff --git a/ts/components/ToastFileSize.stories.tsx b/ts/components/ToastFileSize.stories.tsx
deleted file mode 100644
index eeca4dbb4d..0000000000
--- a/ts/components/ToastFileSize.stories.tsx
+++ /dev/null
@@ -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;
-
-export const _ToastFileSize = (): JSX.Element => (
-
-);
diff --git a/ts/components/ToastFileSize.tsx b/ts/components/ToastFileSize.tsx
deleted file mode 100644
index 76675693f3..0000000000
--- a/ts/components/ToastFileSize.tsx
+++ /dev/null
@@ -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 (
-
- {i18n('icu:fileSizeWarning', { limit, units })}
-
- );
-}
diff --git a/ts/components/ToastGroupLinkCopied.stories.tsx b/ts/components/ToastGroupLinkCopied.stories.tsx
deleted file mode 100644
index 5598c690dc..0000000000
--- a/ts/components/ToastGroupLinkCopied.stories.tsx
+++ /dev/null
@@ -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;
-
-export const _ToastGroupLinkCopied = (): JSX.Element => (
-
-);
diff --git a/ts/components/ToastGroupLinkCopied.tsx b/ts/components/ToastGroupLinkCopied.tsx
deleted file mode 100644
index e24adfdaf4..0000000000
--- a/ts/components/ToastGroupLinkCopied.tsx
+++ /dev/null
@@ -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 (
-
- {i18n('icu:GroupLinkManagement--clipboard')}
-
- );
-}
diff --git a/ts/components/ToastInternalError.stories.tsx b/ts/components/ToastInternalError.stories.tsx
deleted file mode 100644
index 39b27e4d2f..0000000000
--- a/ts/components/ToastInternalError.stories.tsx
+++ /dev/null
@@ -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;
-
-export function ToastDecryptionError(): JSX.Element {
- return (
-
- );
-}
-
-export function ToastCDSMirroringError(): JSX.Element {
- return (
-
- );
-}
diff --git a/ts/components/ToastInternalError.tsx b/ts/components/ToastInternalError.tsx
deleted file mode 100644
index d65c3557dc..0000000000
--- a/ts/components/ToastInternalError.tsx
+++ /dev/null
@@ -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 (
-
- {body}
-
- );
-}
diff --git a/ts/components/ToastLinkCopied.stories.tsx b/ts/components/ToastLinkCopied.stories.tsx
deleted file mode 100644
index 48f83e16ca..0000000000
--- a/ts/components/ToastLinkCopied.stories.tsx
+++ /dev/null
@@ -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;
-
-export const _ToastLinkCopied = (): JSX.Element => (
-
-);
diff --git a/ts/components/ToastLinkCopied.tsx b/ts/components/ToastLinkCopied.tsx
deleted file mode 100644
index d4c30b6516..0000000000
--- a/ts/components/ToastLinkCopied.tsx
+++ /dev/null
@@ -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 {i18n('icu:debugLogLinkCopied')};
-}
diff --git a/ts/components/ToastLoadingFullLogs.stories.tsx b/ts/components/ToastLoadingFullLogs.stories.tsx
deleted file mode 100644
index b242468ad9..0000000000
--- a/ts/components/ToastLoadingFullLogs.stories.tsx
+++ /dev/null
@@ -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;
-
-export const _ToastLoadingFullLogs = (): JSX.Element => (
-
-);
diff --git a/ts/components/ToastLoadingFullLogs.tsx b/ts/components/ToastLoadingFullLogs.tsx
deleted file mode 100644
index bfd6cd081f..0000000000
--- a/ts/components/ToastLoadingFullLogs.tsx
+++ /dev/null
@@ -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 {i18n('icu:loading')};
-}
diff --git a/ts/components/ToastManager.stories.tsx b/ts/components/ToastManager.stories.tsx
index 107abe6a22..705ac327b5 100644
--- a/ts/components/ToastManager.stories.tsx
+++ b/ts/components/ToastManager.stories.tsx
@@ -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 & {
+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 & {
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;
// eslint-disable-next-line react/function-component-definition
const Template: StoryFn = args => {
- const { toastType, ...rest } = args;
+ const { toastType, megaphoneType, ...rest } = args;
return (
<>
Select a toast type in controls
-
+
>
);
};
diff --git a/ts/components/ToastManager.tsx b/ts/components/ToastManager.tsx
index 02cf24ba92..4abacb84d6 100644
--- a/ts/components/ToastManager.tsx
+++ b/ts/components/ToastManager.tsx
@@ -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 {i18n('icu:verificationFailed')};
+ }
+
+ if (toastType === ToastType.CaptchaSolved) {
+ return (
+ {i18n('icu:verificationComplete')}
+ );
+ }
+
if (toastType === ToastType.CannotStartGroupCall) {
return (
@@ -184,6 +208,10 @@ export function ToastManager({
return {i18n('icu:dangerousFileType')};
}
+ if (toastType === ToastType.DebugLogError) {
+ return {i18n('icu:debugLogError')};
+ }
+
if (toastType === ToastType.DeleteForEveryoneFailed) {
return (
{i18n('icu:deleteForEveryoneFailed')}
@@ -217,6 +245,22 @@ export function ToastManager({
);
}
+ if (toastType === ToastType.FailedToFetchPhoneNumber) {
+ return (
+
+ {i18n('icu:Toast--failed-to-fetch-phone-number')}
+
+ );
+ }
+
+ if (toastType === ToastType.FailedToFetchUsername) {
+ return (
+
+ {i18n('icu:Toast--failed-to-fetch-username')}
+
+ );
+ }
+
if (toastType === ToastType.FileSaved) {
return (
+ {i18n('icu:GroupLinkManagement--clipboard')}
+
+ );
+ }
+
+ if (toastType === ToastType.DecryptionError) {
+ assertDev(
+ toast.toastType === ToastType.DecryptionError,
+ 'Pacify typescript'
+ );
+ const { parameters } = toast;
+ const { deviceId, name } = parameters;
+
+ return (
+
+ {i18n('icu:decryptionErrorToast', {
+ name,
+ deviceId,
+ })}
+
+ );
+ }
+
if (toastType === ToastType.InvalidConversation) {
return {i18n('icu:invalidConversation')};
}
@@ -252,6 +331,14 @@ export function ToastManager({
return {i18n('icu:youLeftTheGroup')};
}
+ if (toastType === ToastType.LinkCopied) {
+ return {i18n('icu:debugLogLinkCopied')};
+ }
+
+ if (toastType === ToastType.LoadingFullLogs) {
+ return {i18n('icu:loading')};
+ }
+
if (toastType === ToastType.MaxAttachments) {
return {i18n('icu:maximumAttachments')};
}
@@ -284,6 +371,14 @@ export function ToastManager({
);
}
+ if (toastType === ToastType.StickerPackInstallFailed) {
+ return (
+
+ {i18n('icu:stickers--toast--InstallFailed')}
+
+ );
+ }
+
if (toastType === ToastType.StoryMuted) {
return (
@@ -392,6 +487,18 @@ export function ToastManager({
);
}
+ if (toastType === ToastType.VoiceNoteLimit) {
+ return {i18n('icu:voiceNoteLimit')};
+ }
+
+ if (toastType === ToastType.VoiceNoteMustBeTheOnlyAttachment) {
+ return (
+
+ {i18n('icu:voiceNoteMustBeOnlyAttachment')}
+
+ );
+ }
+
if (toastType === ToastType.WhoCanFindMeReadOnly) {
return (
{i18n('icu:WhoCanFindMeReadOnlyToast')}
@@ -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 ;
+ }
+
+ throw missingCaseError(megaphone.type);
+}
+
+export function ToastManager(props: PropsType): JSX.Element {
+ const { centerToast, containerWidthBreakpoint, isCompositionAreaVisible } =
+ props;
+
+ const toast = renderToast(props);
+
+ return (
+
+ {centerToast
+ ? createPortal(
+
{toast}
,
+ document.body
+ )
+ : toast}
+ {renderMegaphone(props)}
+
+ );
+}
diff --git a/ts/components/ToastStickerPackInstallFailed.stories.tsx b/ts/components/ToastStickerPackInstallFailed.stories.tsx
deleted file mode 100644
index 09ec721d3c..0000000000
--- a/ts/components/ToastStickerPackInstallFailed.stories.tsx
+++ /dev/null
@@ -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;
-
-export const _ToastStickerPackInstallFailed = (): JSX.Element => (
-
-);
diff --git a/ts/components/ToastStickerPackInstallFailed.tsx b/ts/components/ToastStickerPackInstallFailed.tsx
deleted file mode 100644
index e396a684b1..0000000000
--- a/ts/components/ToastStickerPackInstallFailed.tsx
+++ /dev/null
@@ -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 (
-
- {i18n('icu:stickers--toast--InstallFailed')}
-
- );
-}
diff --git a/ts/components/ToastVoiceNoteError.tsx b/ts/components/ToastVoiceNoteError.tsx
deleted file mode 100644
index 645c8c2139..0000000000
--- a/ts/components/ToastVoiceNoteError.tsx
+++ /dev/null
@@ -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 {i18n('icu:voiceNoteError')};
-}
diff --git a/ts/components/ToastVoiceNoteLimit.stories.tsx b/ts/components/ToastVoiceNoteLimit.stories.tsx
deleted file mode 100644
index c3a29f5863..0000000000
--- a/ts/components/ToastVoiceNoteLimit.stories.tsx
+++ /dev/null
@@ -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;
-
-export const _ToastVoiceNoteLimit = (): JSX.Element => (
-
-);
diff --git a/ts/components/ToastVoiceNoteLimit.tsx b/ts/components/ToastVoiceNoteLimit.tsx
deleted file mode 100644
index a9d1e89dd4..0000000000
--- a/ts/components/ToastVoiceNoteLimit.tsx
+++ /dev/null
@@ -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 {i18n('icu:voiceNoteLimit')};
-}
diff --git a/ts/components/ToastVoiceNoteMustBeOnlyAttachment.stories.tsx b/ts/components/ToastVoiceNoteMustBeOnlyAttachment.stories.tsx
deleted file mode 100644
index 4b71ce8796..0000000000
--- a/ts/components/ToastVoiceNoteMustBeOnlyAttachment.stories.tsx
+++ /dev/null
@@ -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;
-
-export const _ToastVoiceNoteMustBeOnlyAttachment = (): JSX.Element => (
-
-);
diff --git a/ts/components/ToastVoiceNoteMustBeOnlyAttachment.tsx b/ts/components/ToastVoiceNoteMustBeOnlyAttachment.tsx
deleted file mode 100644
index f07f4cf367..0000000000
--- a/ts/components/ToastVoiceNoteMustBeOnlyAttachment.tsx
+++ /dev/null
@@ -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 (
- {i18n('icu:voiceNoteMustBeOnlyAttachment')}
- );
-}
diff --git a/ts/components/UsernameMegaphone.stories.tsx b/ts/components/UsernameMegaphone.stories.tsx
new file mode 100644
index 0000000000..9c8f2986ba
--- /dev/null
+++ b/ts/components/UsernameMegaphone.stories.tsx
@@ -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;
+
+export function Defaults(args: PropsType): JSX.Element {
+ return ;
+}
diff --git a/ts/components/UsernameMegaphone.tsx b/ts/components/UsernameMegaphone.tsx
new file mode 100644
index 0000000000..b7f14738f1
--- /dev/null
+++ b/ts/components/UsernameMegaphone.tsx
@@ -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;
+
+export function UsernameMegaphone({
+ i18n,
+ onLearnMore,
+ onDismiss,
+}: PropsType): JSX.Element {
+ return (
+
+
+
+
+
+
{i18n('icu:UsernameMegaphone__title')}
+
{i18n('icu:UsernameMegaphone__body')}
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/ts/components/UsernameOnboardingModalBody.stories.tsx b/ts/components/UsernameOnboardingModal.stories.tsx
similarity index 65%
rename from ts/components/UsernameOnboardingModalBody.stories.tsx
rename to ts/components/UsernameOnboardingModal.stories.tsx
index ea59722103..5974cc286f 100644
--- a/ts/components/UsernameOnboardingModalBody.stories.tsx
+++ b/ts/components/UsernameOnboardingModal.stories.tsx
@@ -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;
// eslint-disable-next-line react/function-component-definition
const Template: StoryFn = args => {
- return ;
+ return ;
};
export const Normal = Template.bind({});
diff --git a/ts/components/UsernameOnboardingModal.tsx b/ts/components/UsernameOnboardingModal.tsx
new file mode 100644
index 0000000000..ece1db1aad
--- /dev/null
+++ b/ts/components/UsernameOnboardingModal.tsx
@@ -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 (
+
+
+
+ {i18n('icu:UsernameOnboardingModalBody__title')}
+
+
+
+
+
+
+
+ {i18n('icu:UsernameOnboardingModalBody__row__number__title')}
+
+ {i18n('icu:UsernameOnboardingModalBody__row__number__body')}
+
+
+
+
+
+
+
+
+ {i18n('icu:UsernameOnboardingModalBody__row__username__title')}
+
+ {i18n('icu:UsernameOnboardingModalBody__row__username__body')}
+
+
+
+
+
+
+
+
{i18n('icu:UsernameOnboardingModalBody__row__qr__title')}
+ {i18n('icu:UsernameOnboardingModalBody__row__qr__body')}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/ts/components/UsernameOnboardingModalBody.tsx b/ts/components/UsernameOnboardingModalBody.tsx
deleted file mode 100644
index 93bd597efc..0000000000
--- a/ts/components/UsernameOnboardingModalBody.tsx
+++ /dev/null
@@ -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 (
-
-
-
-
- {i18n('icu:UsernameOnboardingModalBody__title')}
-
-
-
-
-
-
- {i18n('icu:UsernameOnboardingModalBody__row__number')}
-
-
-
-
-
-
-
- {i18n('icu:UsernameOnboardingModalBody__row__link')}
-
-
-
-
-
-
-
- {i18n('icu:UsernameOnboardingModalBody__row__lock')}
-
-
-
-
-
-
-
- );
-}
diff --git a/ts/components/conversation/AudioCapture.tsx b/ts/components/conversation/AudioCapture.tsx
index a4a5a94840..372fa5399d 100644
--- a/ts/components/conversation/AudioCapture.tsx
+++ b/ts/components/conversation/AudioCapture.tsx
@@ -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;
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 (
- <>
-
-
-
- {showOnlyAttachmentToast && (
-
- )}
- >
+
+
+
);
}
diff --git a/ts/state/ducks/app.ts b/ts/state/ducks/app.ts
index 4824259ccf..fe42c36b49 100644
--- a/ts/state/ducks/app.ts
+++ b/ts/state/ducks/app.ts
@@ -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 =>
+ useBoundActions(actions);
+
function initialLoadComplete(): InitialLoadCompleteActionType {
return {
type: INITIAL_LOAD_COMPLETE,
diff --git a/ts/state/ducks/crashReports.ts b/ts/state/ducks/crashReports.ts
index 14dffc949f..9b9a24a228 100644
--- a/ts/state/ducks/crashReports.ts
+++ b/ts/state/ducks/crashReports.ts
@@ -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 {
- return { type: UPLOAD, payload: window.IPC.crashReports.upload() };
+function uploadCrashReports(): ThunkAction<
+ void,
+ RootStateType,
+ unknown,
+ PromiseAction | 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 {
- return { type: ERASE, payload: window.IPC.crashReports.erase() };
+function eraseCrashReports(): ThunkAction<
+ void,
+ RootStateType,
+ unknown,
+ PromiseAction | 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,
diff --git a/ts/state/ducks/globalModals.ts b/ts/state/ducks/globalModals.ts
index 1a81ec0fac..aa1b18e28b 100644
--- a/ts/state/ducks/globalModals.ts
+++ b/ts/state/ducks/globalModals.ts
@@ -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,
@@ -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;
diff --git a/ts/state/ducks/items.ts b/ts/state/ducks/items.ts
index b4bbe81a55..d0c01a5c67 100644
--- a/ts/state/ducks/items.ts
+++ b/ts/state/ducks/items.ts
@@ -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
>;
// Actions
diff --git a/ts/state/ducks/toast.ts b/ts/state/ducks/toast.ts
index b4d6879f30..b92ab3f5e2 100644
--- a/ts/state/ducks/toast.ts
+++ b/ts/state/ducks/toast.ts
@@ -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
): ToastStateType {
if (action.type === HIDE_TOAST) {
+ if (action.payload != null && state.toast !== action.payload) {
+ return state;
+ }
+
return {
...state,
toast: undefined,
diff --git a/ts/state/ducks/username.ts b/ts/state/ducks/username.ts
index 23b6927751..f7e5a994a4 100644
--- a/ts/state/ducks/username.ts
+++ b/ts/state/ducks/username.ts
@@ -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 {
diff --git a/ts/state/selectors/globalModals.ts b/ts/state/selectors/globalModals.ts
index f5a5648a1a..03dbe438f7 100644
--- a/ts/state/selectors/globalModals.ts
+++ b/ts/state/selectors/globalModals.ts
@@ -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);
+ })
);
diff --git a/ts/state/smart/App.tsx b/ts/state/smart/App.tsx
index f1e10a1922..3bd0f38e28 100644
--- a/ts/state/smart/App.tsx
+++ b/ts/state/smart/App.tsx
@@ -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 ;
}
-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: () => (
-
-
-
- ),
- renderGlobalModalContainer: () => ,
- renderLightbox: () => ,
- hasSelectedStoryData: hasSelectedStoryData(state),
- renderStoryViewer: (closeView: () => unknown) => (
-
-
-
- ),
- 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 => {
- 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 (
+ (
+
+
+
+ )}
+ renderGlobalModalContainer={() => }
+ renderLightbox={() => }
+ hasSelectedStoryData={useSelector(hasSelectedStoryData)}
+ renderStoryViewer={(closeView: () => unknown) => (
+
+
+
+ )}
+ 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 => {
+ return window
+ .getAccountManager()
+ .registerSingleDevice(number, code, sessionId);
+ }}
+ theme={useSelector(getTheme)}
+ openInbox={openInbox}
+ scrollToMessage={scrollToMessage}
+ viewStory={viewStory}
+ />
+ );
+}
diff --git a/ts/state/smart/CallsTab.tsx b/ts/state/smart/CallsTab.tsx
index 7fcc5313d9..40d441ef86 100644
--- a/ts/state/smart/CallsTab.tsx
+++ b/ts/state/smart/CallsTab.tsx
@@ -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 ;
+}
+
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}
/>
diff --git a/ts/state/smart/ChatsTab.tsx b/ts/state/smart/ChatsTab.tsx
index f33797e8cf..1af1d0aa0b 100644
--- a/ts/state/smart/ChatsTab.tsx
+++ b/ts/state/smart/ChatsTab.tsx
@@ -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();
@@ -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) {
diff --git a/ts/state/smart/CompositionRecording.tsx b/ts/state/smart/CompositionRecording.tsx
index 30b37e0e47..b859064db2 100644
--- a/ts/state/smart/CompositionRecording.tsx
+++ b/ts/state/smart/CompositionRecording.tsx
@@ -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}
/>
);
}
diff --git a/ts/state/smart/GlobalModalContainer.tsx b/ts/state/smart/GlobalModalContainer.tsx
index 0215bf37eb..21ac55c122 100644
--- a/ts/state/smart/GlobalModalContainer.tsx
+++ b/ts/state/smart/GlobalModalContainer.tsx
@@ -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 ;
}
+function renderUsernameOnboarding(): JSX.Element {
+ return ;
+}
+
function renderContactModal(): JSX.Element {
return ;
}
@@ -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}
diff --git a/ts/state/smart/InstallScreen.tsx b/ts/state/smart/InstallScreen.tsx
index 6e7535d3d8..9c3a272647 100644
--- a/ts/state/smart/InstallScreen.tsx
+++ b/ts/state/smart/InstallScreen.tsx
@@ -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;
@@ -328,5 +330,13 @@ export function SmartInstallScreen(): ReactElement {
throw missingCaseError(state);
}
- return ;
+ return (
+ <>
+
+
+ >
+ );
}
diff --git a/ts/state/smart/LeftPane.tsx b/ts/state/smart/LeftPane.tsx
index e312955bc7..bb41385e4b 100644
--- a/ts/state/smart/LeftPane.tsx
+++ b/ts/state/smart/LeftPane.tsx
@@ -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 ;
}
+function renderToastManager(props: {
+ containerWidthBreakpoint: WidthBreakpoint;
+}): JSX.Element {
+ return ;
+}
+
+function renderToastManagerWithoutMegaphone(props: {
+ containerWidthBreakpoint: WidthBreakpoint;
+}): JSX.Element {
+ return ;
+}
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),
};
diff --git a/ts/state/smart/ProfileEditorModal.tsx b/ts/state/smart/ProfileEditorModal.tsx
index 824ad5f9f0..39c339ef38 100644
--- a/ts/state/smart/ProfileEditorModal.tsx
+++ b/ts/state/smart/ProfileEditorModal.tsx
@@ -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,
diff --git a/ts/state/smart/StoriesTab.tsx b/ts/state/smart/StoriesTab.tsx
index d63584875e..323a3125ac 100644
--- a/ts/state/smart/StoriesTab.tsx
+++ b/ts/state/smart/StoriesTab.tsx
@@ -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 ;
}
+function renderToastManager(props: {
+ containerWidthBreakpoint: WidthBreakpoint;
+}): JSX.Element {
+ return ;
+}
+
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}
diff --git a/ts/state/smart/ToastManager.tsx b/ts/state/smart/ToastManager.tsx
new file mode 100644
index 0000000000..776eefa533
--- /dev/null
+++ b/ts/state/smart/ToastManager.tsx
@@ -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 (
+ window.IPC.showDebugLog()}
+ onUndoArchive={onUndoArchive}
+ openFileInFolder={openFileInFolder}
+ hideToast={hideToast}
+ centerToast={centerToast}
+ containerWidthBreakpoint={containerWidthBreakpoint}
+ isCompositionAreaVisible={isCompositionAreaVisible}
+ />
+ );
+}
diff --git a/ts/state/smart/UsernameOnboardingModal.tsx b/ts/state/smart/UsernameOnboardingModal.tsx
new file mode 100644
index 0000000000..d9a2c59bf8
--- /dev/null
+++ b/ts/state/smart/UsernameOnboardingModal.tsx
@@ -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 (
+
+ );
+}
diff --git a/ts/test-both/state/selectors/items_test.ts b/ts/test-both/state/selectors/items_test.ts
index 60da1e837e..1b403eb5ff 100644
--- a/ts/test-both/state/selectors/items_test.ts
+++ b/ts/test-both/state/selectors/items_test.ts
@@ -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);
});
diff --git a/ts/test-mock/pnp/username_test.ts b/ts/test-mock/pnp/username_test.ts
index 4c0eebc07d..3d20a83f7c 100644
--- a/ts/test-mock/pnp/username_test.ts
+++ b/ts/test-mock/pnp/username_test.ts
@@ -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);
diff --git a/ts/types/Megaphone.ts b/ts/types/Megaphone.ts
new file mode 100644
index 0000000000..304b5e160b
--- /dev/null
+++ b/ts/types/Megaphone.ts
@@ -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;
diff --git a/ts/types/Toast.tsx b/ts/types/Toast.tsx
index 5b507acb12..60fb454cbb 100644
--- a/ts/types/Toast.tsx
+++ b/ts/types/Toast.tsx
@@ -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 };
diff --git a/ts/types/globalModals.ts b/ts/types/globalModals.ts
new file mode 100644
index 0000000000..03376b4960
--- /dev/null
+++ b/ts/types/globalModals.ts
@@ -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',
+}
diff --git a/ts/util/copyGroupLink.ts b/ts/util/copyGroupLink.ts
index 9fcc998b01..e990d70526 100644
--- a/ts/util/copyGroupLink.ts
+++ b/ts/util/copyGroupLink.ts
@@ -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 {
await window.navigator.clipboard.writeText(groupLink);
- showToast(ToastGroupLinkCopied);
+ window.reduxActions.toast.showToast({ toastType: ToastType.GroupLinkCopied });
}
diff --git a/ts/util/handleRetry.ts b/ts/util/handleRetry.ts
index d77825a0b0..2d9c8f0af7 100644
--- a/ts/util/handleRetry.ts
+++ b/ts/util/handleRetry.ts
@@ -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,
+ },
});
}
diff --git a/ts/util/lookupConversationWithoutServiceId.ts b/ts/util/lookupConversationWithoutServiceId.ts
index 160fb98720..130ec07f00 100644
--- a/ts/util/lookupConversationWithoutServiceId.ts
+++ b/ts/util/lookupConversationWithoutServiceId.ts
@@ -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;
diff --git a/ts/util/processAttachment.ts b/ts/util/processAttachment.ts
index 016d68a205..bf66874f32 100644
--- a/ts/util/processAttachment.ts
+++ b/ts/util/processAttachment.ts
@@ -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): 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;
}
diff --git a/ts/util/showToast.tsx b/ts/util/showToast.tsx
deleted file mode 100644
index 001f7f2922..0000000000
--- a/ts/util/showToast.tsx
+++ /dev/null
@@ -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(, node);
-}