Username Education
Co-authored-by: Jamie Kyle <jamie@signal.org>
This commit is contained in:
parent
c6a7637513
commit
7dc11c1928
100 changed files with 1443 additions and 1269 deletions
|
@ -4,6 +4,8 @@
|
|||
import type { ThunkAction } from 'redux-thunk';
|
||||
import type { ReadonlyDeep } from 'type-fest';
|
||||
import type { StateType as RootStateType } from '../reducer';
|
||||
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
|
||||
import { useBoundActions } from '../../hooks/useBoundActions';
|
||||
import * as log from '../../logging/log';
|
||||
|
||||
// State
|
||||
|
@ -57,6 +59,9 @@ export const actions = {
|
|||
openStandalone,
|
||||
};
|
||||
|
||||
export const useAppActions = (): BoundActionCreatorsMapObject<typeof actions> =>
|
||||
useBoundActions(actions);
|
||||
|
||||
function initialLoadComplete(): InitialLoadCompleteActionType {
|
||||
return {
|
||||
type: INITIAL_LOAD_COMPLETE,
|
||||
|
|
|
@ -2,11 +2,14 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ReadonlyDeep } from 'type-fest';
|
||||
import type { ThunkAction } from 'redux-thunk';
|
||||
|
||||
import * as log from '../../logging/log';
|
||||
import { showToast } from '../../util/showToast';
|
||||
import * as Errors from '../../types/errors';
|
||||
import { ToastLinkCopied } from '../../components/ToastLinkCopied';
|
||||
import { ToastDebugLogError } from '../../components/ToastDebugLogError';
|
||||
import { ToastType } from '../../types/Toast';
|
||||
import type { StateType as RootStateType } from '../reducer';
|
||||
import { showToast } from './toast';
|
||||
import type { ShowToastActionType } from './toast';
|
||||
import type { PromiseAction } from '../util';
|
||||
|
||||
// State
|
||||
|
@ -45,12 +48,43 @@ function setCrashReportCount(count: number): SetCrashReportCountActionType {
|
|||
return { type: SET_COUNT, payload: count };
|
||||
}
|
||||
|
||||
function uploadCrashReports(): PromiseAction<typeof UPLOAD> {
|
||||
return { type: UPLOAD, payload: window.IPC.crashReports.upload() };
|
||||
function uploadCrashReports(): ThunkAction<
|
||||
void,
|
||||
RootStateType,
|
||||
unknown,
|
||||
PromiseAction<typeof UPLOAD> | ShowToastActionType
|
||||
> {
|
||||
return dispatch => {
|
||||
async function run() {
|
||||
try {
|
||||
await window.IPC.crashReports.upload();
|
||||
dispatch(showToast({ toastType: ToastType.LinkCopied }));
|
||||
} catch (error) {
|
||||
dispatch(showToast({ toastType: ToastType.DebugLogError }));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
dispatch({ type: UPLOAD, payload: run() });
|
||||
};
|
||||
}
|
||||
|
||||
function eraseCrashReports(): PromiseAction<typeof ERASE> {
|
||||
return { type: ERASE, payload: window.IPC.crashReports.erase() };
|
||||
function eraseCrashReports(): ThunkAction<
|
||||
void,
|
||||
RootStateType,
|
||||
unknown,
|
||||
PromiseAction<typeof ERASE> | ShowToastActionType
|
||||
> {
|
||||
return dispatch => {
|
||||
async function run() {
|
||||
try {
|
||||
await window.IPC.crashReports.erase();
|
||||
} catch (error) {
|
||||
dispatch(showToast({ toastType: ToastType.DebugLogError }));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
dispatch({ type: ERASE, payload: run() });
|
||||
};
|
||||
}
|
||||
|
||||
// Reducer
|
||||
|
@ -87,9 +121,6 @@ export function reducer(
|
|||
action.type === `${UPLOAD}_FULFILLED` ||
|
||||
action.type === `${ERASE}_FULFILLED`
|
||||
) {
|
||||
if (action.type === `${UPLOAD}_FULFILLED`) {
|
||||
showToast(ToastLinkCopied);
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
count: 0,
|
||||
|
@ -107,8 +138,6 @@ export function reducer(
|
|||
`Failed to upload crash report due to error ${Errors.toLogFormat(error)}`
|
||||
);
|
||||
|
||||
showToast(ToastDebugLogError);
|
||||
|
||||
return {
|
||||
...state,
|
||||
count: 0,
|
||||
|
|
|
@ -16,10 +16,12 @@ import type {
|
|||
import type { MessagePropsType } from '../selectors/message';
|
||||
import type { RecipientsByConversation } from './stories';
|
||||
import type { SafetyNumberChangeSource } from '../../components/SafetyNumberChangeDialog';
|
||||
import type { EditState as ProfileEditorEditState } from '../../components/ProfileEditor';
|
||||
import type { StateType as RootStateType } from '../reducer';
|
||||
import * as Errors from '../../types/errors';
|
||||
import * as SingleServePromise from '../../services/singleServePromise';
|
||||
import * as Stickers from '../../types/Stickers';
|
||||
import { UsernameOnboardingState } from '../../types/globalModals';
|
||||
import * as log from '../../logging/log';
|
||||
import { getMessagePropsSelector } from '../selectors/message';
|
||||
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
|
||||
|
@ -96,7 +98,9 @@ export type GlobalModalsStateType = ReadonlyDeep<{
|
|||
isSignalConnectionsVisible: boolean;
|
||||
isStoriesSettingsVisible: boolean;
|
||||
isWhatsNewVisible: boolean;
|
||||
usernameOnboardingState: UsernameOnboardingState;
|
||||
profileEditorHasError: boolean;
|
||||
profileEditorInitialEditState: ProfileEditorEditState | undefined;
|
||||
safetyNumberChangedBlockingData?: SafetyNumberChangedBlockingDataType;
|
||||
safetyNumberModalContactId?: string;
|
||||
sendEditWarningData?: SendEditWarningDataType;
|
||||
|
@ -151,6 +155,7 @@ const CONFIRM_AUTH_ART_CREATOR_FULFILLED =
|
|||
'globalModals/CONFIRM_AUTH_ART_CREATOR_FULFILLED';
|
||||
const SHOW_EDIT_HISTORY_MODAL = 'globalModals/SHOW_EDIT_HISTORY_MODAL';
|
||||
const CLOSE_EDIT_HISTORY_MODAL = 'globalModals/CLOSE_EDIT_HISTORY_MODAL';
|
||||
const TOGGLE_USERNAME_ONBOARDING = 'globalModals/TOGGLE_USERNAME_ONBOARDING';
|
||||
|
||||
export type ContactModalStateType = ReadonlyDeep<{
|
||||
contactId: string;
|
||||
|
@ -206,6 +211,9 @@ type ToggleForwardMessagesModalActionType = ReadonlyDeep<{
|
|||
|
||||
type ToggleProfileEditorActionType = ReadonlyDeep<{
|
||||
type: typeof TOGGLE_PROFILE_EDITOR;
|
||||
payload: {
|
||||
initialEditState?: ProfileEditorEditState;
|
||||
};
|
||||
}>;
|
||||
|
||||
export type ToggleProfileEditorErrorActionType = ReadonlyDeep<{
|
||||
|
@ -231,6 +239,10 @@ type ToggleConfirmationModalActionType = ReadonlyDeep<{
|
|||
payload: boolean;
|
||||
}>;
|
||||
|
||||
type ToggleUsernameOnboardingActionType = ReadonlyDeep<{
|
||||
type: typeof TOGGLE_USERNAME_ONBOARDING;
|
||||
}>;
|
||||
|
||||
type ShowStoriesSettingsActionType = ReadonlyDeep<{
|
||||
type: typeof SHOW_STORIES_SETTINGS;
|
||||
}>;
|
||||
|
@ -368,6 +380,7 @@ export type GlobalModalsActionType = ReadonlyDeep<
|
|||
| ToggleProfileEditorErrorActionType
|
||||
| ToggleSafetyNumberModalActionType
|
||||
| ToggleSignalConnectionsModalActionType
|
||||
| ToggleUsernameOnboardingActionType
|
||||
>;
|
||||
|
||||
// Action Creators
|
||||
|
@ -406,6 +419,7 @@ export const actions = {
|
|||
toggleProfileEditorHasError,
|
||||
toggleSafetyNumberModal,
|
||||
toggleSignalConnectionsModal,
|
||||
toggleUsernameOnboarding,
|
||||
};
|
||||
|
||||
export const useGlobalModalActions = (): BoundActionCreatorsMapObject<
|
||||
|
@ -585,8 +599,10 @@ function toggleForwardMessagesModal(
|
|||
};
|
||||
}
|
||||
|
||||
function toggleProfileEditor(): ToggleProfileEditorActionType {
|
||||
return { type: TOGGLE_PROFILE_EDITOR };
|
||||
function toggleProfileEditor(
|
||||
initialEditState?: ProfileEditorEditState
|
||||
): ToggleProfileEditorActionType {
|
||||
return { type: TOGGLE_PROFILE_EDITOR, payload: { initialEditState } };
|
||||
}
|
||||
|
||||
function toggleProfileEditorHasError(): ToggleProfileEditorErrorActionType {
|
||||
|
@ -626,6 +642,10 @@ function toggleConfirmationModal(
|
|||
};
|
||||
}
|
||||
|
||||
function toggleUsernameOnboarding(): ToggleUsernameOnboardingActionType {
|
||||
return { type: TOGGLE_USERNAME_ONBOARDING };
|
||||
}
|
||||
|
||||
function showBlockingSafetyNumberChangeDialog(
|
||||
untrustedByConversation: RecipientsByConversation,
|
||||
explodedPromise: ExplodePromiseResultType<boolean>,
|
||||
|
@ -861,7 +881,9 @@ export function getEmptyState(): GlobalModalsStateType {
|
|||
isSignalConnectionsVisible: false,
|
||||
isStoriesSettingsVisible: false,
|
||||
isWhatsNewVisible: false,
|
||||
usernameOnboardingState: UsernameOnboardingState.NeverShown,
|
||||
profileEditorHasError: false,
|
||||
profileEditorInitialEditState: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -873,6 +895,7 @@ export function reducer(
|
|||
return {
|
||||
...state,
|
||||
isProfileEditorVisible: !state.isProfileEditorVisible,
|
||||
profileEditorInitialEditState: action.payload.initialEditState,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -983,6 +1006,16 @@ export function reducer(
|
|||
};
|
||||
}
|
||||
|
||||
if (action.type === TOGGLE_USERNAME_ONBOARDING) {
|
||||
return {
|
||||
...state,
|
||||
usernameOnboardingState:
|
||||
state.usernameOnboardingState === UsernameOnboardingState.Open
|
||||
? UsernameOnboardingState.Closed
|
||||
: UsernameOnboardingState.Open,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === SHOW_SEND_ANYWAY_DIALOG) {
|
||||
const { promiseUuid, source } = action.payload;
|
||||
|
||||
|
|
|
@ -27,20 +27,7 @@ export type ItemsStateType = ReadonlyDeep<
|
|||
[key: string]: unknown;
|
||||
remoteConfig?: RemoteConfigType;
|
||||
serverTimeSkew?: number;
|
||||
} & Partial<
|
||||
Pick<
|
||||
StorageAccessType,
|
||||
| 'universalExpireTimer'
|
||||
| 'defaultConversationColor'
|
||||
| 'customColors'
|
||||
| 'preferredLeftPaneWidth'
|
||||
| 'navTabsCollapsed'
|
||||
| 'preferredReactionEmoji'
|
||||
| 'areWeASubscriber'
|
||||
| 'usernameLinkColor'
|
||||
| 'usernameLink'
|
||||
>
|
||||
>
|
||||
} & Partial<StorageAccessType>
|
||||
>;
|
||||
|
||||
// Actions
|
||||
|
|
|
@ -23,6 +23,7 @@ export const SHOW_TOAST = 'toast/SHOW_TOAST';
|
|||
|
||||
type HideToastActionType = ReadonlyDeep<{
|
||||
type: typeof HIDE_TOAST;
|
||||
payload: AnyToast | undefined;
|
||||
}>;
|
||||
|
||||
// eslint-disable-next-line local-rules/type-alias-readonlydeep
|
||||
|
@ -36,9 +37,12 @@ export type ToastActionType = HideToastActionType | ShowToastActionType;
|
|||
|
||||
// Action Creators
|
||||
|
||||
function hideToast(): HideToastActionType {
|
||||
export type HideToastAction = ReadonlyDeep<(toast?: AnyToast) => void>;
|
||||
|
||||
function hideToast(toast?: AnyToast): HideToastActionType {
|
||||
return {
|
||||
type: HIDE_TOAST,
|
||||
payload: toast,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -80,6 +84,10 @@ export function reducer(
|
|||
action: Readonly<ToastActionType>
|
||||
): ToastStateType {
|
||||
if (action.type === HIDE_TOAST) {
|
||||
if (action.payload != null && state.toast !== action.payload) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
toast: undefined,
|
||||
|
|
|
@ -28,6 +28,8 @@ import {
|
|||
import { showToast } from './toast';
|
||||
import { ToastType } from '../../types/Toast';
|
||||
import type { ToastActionType } from './toast';
|
||||
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
|
||||
import { useBoundActions } from '../../hooks/useBoundActions';
|
||||
|
||||
export type UsernameReservationStateType = ReadonlyDeep<{
|
||||
state: UsernameReservationState;
|
||||
|
@ -129,6 +131,10 @@ export const actions = {
|
|||
markCompletedUsernameLinkOnboarding,
|
||||
};
|
||||
|
||||
export const useUsernameActions = (): BoundActionCreatorsMapObject<
|
||||
typeof actions
|
||||
> => useBoundActions(actions);
|
||||
|
||||
export function setUsernameEditState(
|
||||
editState: UsernameEditState
|
||||
): SetUsernameEditStateActionType {
|
||||
|
|
|
@ -5,6 +5,7 @@ import { createSelector } from 'reselect';
|
|||
|
||||
import type { StateType } from '../reducer';
|
||||
import type { GlobalModalsStateType } from '../ducks/globalModals';
|
||||
import { UsernameOnboardingState } from '../../types/globalModals';
|
||||
|
||||
export const getGlobalModalsState = (state: StateType): GlobalModalsStateType =>
|
||||
state.globalModals;
|
||||
|
@ -12,5 +13,11 @@ export const getGlobalModalsState = (state: StateType): GlobalModalsStateType =>
|
|||
export const isShowingAnyModal = createSelector(
|
||||
getGlobalModalsState,
|
||||
(globalModalsState): boolean =>
|
||||
Object.values(globalModalsState).some(value => Boolean(value))
|
||||
Object.entries(globalModalsState).some(([key, value]) => {
|
||||
if (key === 'usernameOnboardingState') {
|
||||
return value === UsernameOnboardingState.Open;
|
||||
}
|
||||
|
||||
return Boolean(value);
|
||||
})
|
||||
);
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import type { VerificationTransport } from '../../types/VerificationTransport';
|
||||
import { App } from '../../components/App';
|
||||
|
@ -12,18 +12,16 @@ import { SmartCallManager } from './CallManager';
|
|||
import { SmartGlobalModalContainer } from './GlobalModalContainer';
|
||||
import { SmartLightbox } from './Lightbox';
|
||||
import { SmartStoryViewer } from './StoryViewer';
|
||||
import type { StateType } from '../reducer';
|
||||
import {
|
||||
getIntl,
|
||||
getLocaleMessages,
|
||||
getTheme,
|
||||
getIsMainWindowMaximized,
|
||||
getIsMainWindowFullScreen,
|
||||
getMenuOptions,
|
||||
} from '../selectors/user';
|
||||
import { hasSelectedStoryData } from '../selectors/stories';
|
||||
import { getHideMenuBar } from '../selectors/items';
|
||||
import { mapDispatchToProps } from '../actions';
|
||||
import type { StateType } from '../reducer';
|
||||
import { useAppActions } from '../ducks/app';
|
||||
import { useConversationsActions } from '../ducks/conversations';
|
||||
import { useStoriesActions } from '../ducks/stories';
|
||||
import { ErrorBoundary } from '../../components/ErrorBoundary';
|
||||
import { ModalContainer } from '../../components/ModalContainer';
|
||||
import { SmartInbox } from './Inbox';
|
||||
|
@ -32,58 +30,58 @@ function renderInbox(): JSX.Element {
|
|||
return <SmartInbox />;
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: StateType) => {
|
||||
const i18n = getIntl(state);
|
||||
export function SmartApp(): JSX.Element {
|
||||
const app = useSelector((state: StateType) => state.app);
|
||||
|
||||
return {
|
||||
...state.app,
|
||||
i18n,
|
||||
localeMessages: getLocaleMessages(state),
|
||||
isMaximized: getIsMainWindowMaximized(state),
|
||||
isFullScreen: getIsMainWindowFullScreen(state),
|
||||
menuOptions: getMenuOptions(state),
|
||||
OS: OS.getName(),
|
||||
osClassName: OS.getClassName(),
|
||||
hideMenuBar: getHideMenuBar(state),
|
||||
renderCallManager: () => (
|
||||
<ModalContainer className="module-calling__modal-container">
|
||||
<SmartCallManager />
|
||||
</ModalContainer>
|
||||
),
|
||||
renderGlobalModalContainer: () => <SmartGlobalModalContainer />,
|
||||
renderLightbox: () => <SmartLightbox />,
|
||||
hasSelectedStoryData: hasSelectedStoryData(state),
|
||||
renderStoryViewer: (closeView: () => unknown) => (
|
||||
<ErrorBoundary name="App/renderStoryViewer" closeView={closeView}>
|
||||
<SmartStoryViewer />
|
||||
</ErrorBoundary>
|
||||
),
|
||||
renderInbox,
|
||||
requestVerification: (
|
||||
number: string,
|
||||
captcha: string,
|
||||
transport: VerificationTransport
|
||||
): Promise<{ sessionId: string }> => {
|
||||
const { server } = window.textsecure;
|
||||
strictAssert(server !== undefined, 'WebAPI not available');
|
||||
const { openInbox } = useAppActions();
|
||||
|
||||
return server.requestVerification(number, captcha, transport);
|
||||
},
|
||||
registerSingleDevice: (
|
||||
number: string,
|
||||
code: string,
|
||||
sessionId: string
|
||||
): Promise<void> => {
|
||||
return window
|
||||
.getAccountManager()
|
||||
.registerSingleDevice(number, code, sessionId);
|
||||
},
|
||||
theme: getTheme(state),
|
||||
const { scrollToMessage } = useConversationsActions();
|
||||
|
||||
toast: state.toast.toast,
|
||||
};
|
||||
};
|
||||
const { viewStory } = useStoriesActions();
|
||||
|
||||
const smart = connect(mapStateToProps, mapDispatchToProps);
|
||||
return (
|
||||
<App
|
||||
{...app}
|
||||
isMaximized={useSelector(getIsMainWindowMaximized)}
|
||||
isFullScreen={useSelector(getIsMainWindowFullScreen)}
|
||||
osClassName={OS.getClassName()}
|
||||
renderCallManager={() => (
|
||||
<ModalContainer className="module-calling__modal-container">
|
||||
<SmartCallManager />
|
||||
</ModalContainer>
|
||||
)}
|
||||
renderGlobalModalContainer={() => <SmartGlobalModalContainer />}
|
||||
renderLightbox={() => <SmartLightbox />}
|
||||
hasSelectedStoryData={useSelector(hasSelectedStoryData)}
|
||||
renderStoryViewer={(closeView: () => unknown) => (
|
||||
<ErrorBoundary name="App/renderStoryViewer" closeView={closeView}>
|
||||
<SmartStoryViewer />
|
||||
</ErrorBoundary>
|
||||
)}
|
||||
renderInbox={renderInbox}
|
||||
requestVerification={(
|
||||
number: string,
|
||||
captcha: string,
|
||||
transport: VerificationTransport
|
||||
): Promise<{ sessionId: string }> => {
|
||||
const { server } = window.textsecure;
|
||||
strictAssert(server !== undefined, 'WebAPI not available');
|
||||
|
||||
export const SmartApp = smart(App);
|
||||
return server.requestVerification(number, captcha, transport);
|
||||
}}
|
||||
registerSingleDevice={(
|
||||
number: string,
|
||||
code: string,
|
||||
sessionId: string
|
||||
): Promise<void> => {
|
||||
return window
|
||||
.getAccountManager()
|
||||
.registerSingleDevice(number, code, sessionId);
|
||||
}}
|
||||
theme={useSelector(getTheme)}
|
||||
openInbox={openInbox}
|
||||
scrollToMessage={scrollToMessage}
|
||||
viewStory={viewStory}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
getPreferredLeftPaneWidth,
|
||||
} from '../selectors/items';
|
||||
import { getIntl, getRegionCode } from '../selectors/user';
|
||||
import type { WidthBreakpoint } from '../../components/_util';
|
||||
import { CallsTab } from '../../components/CallsTab';
|
||||
import {
|
||||
getAllConversations,
|
||||
|
@ -23,6 +24,7 @@ import type {
|
|||
} from '../../types/CallDisposition';
|
||||
import type { ConversationType } from '../ducks/conversations';
|
||||
import { SmartConversationDetails } from './ConversationDetails';
|
||||
import { SmartToastManager } from './ToastManager';
|
||||
import { useCallingActions } from '../ducks/calling';
|
||||
import { getActiveCallState } from '../selectors/calling';
|
||||
import { useCallHistoryActions } from '../ducks/callHistory';
|
||||
|
@ -80,6 +82,12 @@ function renderConversationDetails(
|
|||
);
|
||||
}
|
||||
|
||||
function renderToastManager(props: {
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
}): JSX.Element {
|
||||
return <SmartToastManager disableMegaphone {...props} />;
|
||||
}
|
||||
|
||||
export function SmartCallsTab(): JSX.Element {
|
||||
const i18n = useSelector(getIntl);
|
||||
const navTabsCollapsed = useSelector(getNavTabsCollapsed);
|
||||
|
@ -172,6 +180,7 @@ export function SmartCallsTab(): JSX.Element {
|
|||
onOutgoingVideoCallInConversation={onOutgoingVideoCallInConversation}
|
||||
preferredLeftPaneWidth={preferredLeftPaneWidth}
|
||||
renderConversationDetails={renderConversationDetails}
|
||||
renderToastManager={renderToastManager}
|
||||
regionCode={regionCode}
|
||||
savePreferredLeftPaneWidth={savePreferredLeftPaneWidth}
|
||||
/>
|
||||
|
|
|
@ -14,10 +14,10 @@ import { usePrevious } from '../../hooks/usePrevious';
|
|||
import { TargetedMessageSource } from '../ducks/conversationsEnums';
|
||||
import type { ConversationsStateType } from '../ducks/conversations';
|
||||
import { useConversationsActions } from '../ducks/conversations';
|
||||
import { useToastActions } from '../ducks/toast';
|
||||
import type { StateType } from '../reducer';
|
||||
import { strictAssert } from '../../util/assert';
|
||||
import { showToast } from '../../util/showToast';
|
||||
import { ToastStickerPackInstallFailed } from '../../components/ToastStickerPackInstallFailed';
|
||||
import { ToastType } from '../../types/Toast';
|
||||
import { getNavTabsCollapsed } from '../selectors/items';
|
||||
import { useItemsActions } from '../ducks/items';
|
||||
import { getHasAnyFailedStorySends } from '../selectors/stories';
|
||||
|
@ -56,6 +56,7 @@ export function SmartChatsTab(): JSX.Element {
|
|||
} = useConversationsActions();
|
||||
const { showWhatsNewModal } = useGlobalModalActions();
|
||||
const { toggleNavTabsCollapse } = useItemsActions();
|
||||
const { showToast } = useToastActions();
|
||||
|
||||
const lastOpenedConversationId = useRef<string | undefined>();
|
||||
|
||||
|
@ -121,7 +122,7 @@ export function SmartChatsTab(): JSX.Element {
|
|||
}
|
||||
|
||||
function packInstallFailed() {
|
||||
showToast(ToastStickerPackInstallFailed);
|
||||
showToast({ toastType: ToastType.StickerPackInstallFailed });
|
||||
}
|
||||
|
||||
window.Whisper.events.on('pack-install-failed', packInstallFailed);
|
||||
|
@ -133,7 +134,7 @@ export function SmartChatsTab(): JSX.Element {
|
|||
window.Whisper.events.off('refreshConversation', refreshConversation);
|
||||
window.Whisper.events.off('setupAsNewDevice', unload);
|
||||
};
|
||||
}, [onConversationClosed, prevConversationId, showConversation]);
|
||||
}, [onConversationClosed, prevConversationId, showConversation, showToast]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedConversationId) {
|
||||
|
|
|
@ -7,6 +7,7 @@ import { CompositionRecording } from '../../components/CompositionRecording';
|
|||
import { mapDispatchToProps } from '../actions';
|
||||
import { useAudioRecorderActions } from '../ducks/audioRecorder';
|
||||
import { useComposerActions } from '../ducks/composer';
|
||||
import { useToastActions } from '../ducks/toast';
|
||||
import { getSelectedConversationId } from '../selectors/conversations';
|
||||
import { getIntl } from '../selectors/user';
|
||||
|
||||
|
@ -22,6 +23,7 @@ export function SmartCompositionRecording({
|
|||
const { cancelRecording, completeRecording } = useAudioRecorderActions();
|
||||
|
||||
const { sendMultiMediaMessage } = useComposerActions();
|
||||
const { hideToast, showToast } = useToastActions();
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
cancelRecording();
|
||||
|
@ -54,6 +56,8 @@ export function SmartCompositionRecording({
|
|||
errorRecording={mapDispatchToProps.errorRecording}
|
||||
addAttachment={mapDispatchToProps.addAttachment}
|
||||
completeRecording={mapDispatchToProps.completeRecording}
|
||||
showToast={showToast}
|
||||
hideToast={hideToast}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import { SmartContactModal } from './ContactModal';
|
|||
import { SmartEditHistoryMessagesModal } from './EditHistoryMessagesModal';
|
||||
import { SmartForwardMessagesModal } from './ForwardMessagesModal';
|
||||
import { SmartProfileEditorModal } from './ProfileEditorModal';
|
||||
import { SmartUsernameOnboardingModal } from './UsernameOnboardingModal';
|
||||
import { SmartSafetyNumberModal } from './SafetyNumberModal';
|
||||
import { SmartSendAnywayDialog } from './SendAnywayDialog';
|
||||
import { SmartShortcutGuideModal } from './ShortcutGuideModal';
|
||||
|
@ -31,6 +32,10 @@ function renderProfileEditor(): JSX.Element {
|
|||
return <SmartProfileEditorModal />;
|
||||
}
|
||||
|
||||
function renderUsernameOnboarding(): JSX.Element {
|
||||
return <SmartUsernameOnboardingModal />;
|
||||
}
|
||||
|
||||
function renderContactModal(): JSX.Element {
|
||||
return <SmartContactModal />;
|
||||
}
|
||||
|
@ -77,6 +82,7 @@ export function SmartGlobalModalContainer(): JSX.Element {
|
|||
isSignalConnectionsVisible,
|
||||
isStoriesSettingsVisible,
|
||||
isWhatsNewVisible,
|
||||
usernameOnboardingState,
|
||||
safetyNumberChangedBlockingData,
|
||||
safetyNumberModalContactId,
|
||||
sendEditWarningData,
|
||||
|
@ -157,6 +163,7 @@ export function SmartGlobalModalContainer(): JSX.Element {
|
|||
renderDeleteMessagesModal={renderDeleteMessagesModal}
|
||||
renderForwardMessagesModal={renderForwardMessagesModal}
|
||||
renderProfileEditor={renderProfileEditor}
|
||||
renderUsernameOnboarding={renderUsernameOnboarding}
|
||||
renderSafetyNumber={renderSafetyNumber}
|
||||
renderSendAnywayDialog={renderSendAnywayDialog}
|
||||
renderShortcutGuideModal={renderShortcutGuideModal}
|
||||
|
@ -171,6 +178,7 @@ export function SmartGlobalModalContainer(): JSX.Element {
|
|||
theme={theme}
|
||||
toggleSignalConnectionsModal={toggleSignalConnectionsModal}
|
||||
userNotFoundModalState={userNotFoundModalState}
|
||||
usernameOnboardingState={usernameOnboardingState}
|
||||
isAuthorizingArtCreator={isAuthorizingArtCreator}
|
||||
authArtCreatorData={authArtCreatorData}
|
||||
cancelAuthorizeArtCreator={cancelAuthorizeArtCreator}
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
} from '../../components/InstallScreen';
|
||||
import { InstallError } from '../../components/installScreen/InstallScreenErrorStep';
|
||||
import { MAX_DEVICE_NAME_LENGTH } from '../../components/installScreen/InstallScreenChoosingDeviceNameStep';
|
||||
import { WidthBreakpoint } from '../../components/_util';
|
||||
import { HTTPError } from '../../textsecure/Errors';
|
||||
import { isRecord } from '../../util/isRecord';
|
||||
import * as Errors from '../../types/errors';
|
||||
|
@ -32,6 +33,7 @@ import OS from '../../util/os/osMain';
|
|||
import { SECOND } from '../../util/durations';
|
||||
import { BackOff } from '../../util/BackOff';
|
||||
import { drop } from '../../util/drop';
|
||||
import { SmartToastManager } from './ToastManager';
|
||||
|
||||
type PropsType = ComponentProps<typeof InstallScreen>;
|
||||
|
||||
|
@ -328,5 +330,13 @@ export function SmartInstallScreen(): ReactElement {
|
|||
throw missingCaseError(state);
|
||||
}
|
||||
|
||||
return <InstallScreen {...props} />;
|
||||
return (
|
||||
<>
|
||||
<InstallScreen {...props} />
|
||||
<SmartToastManager
|
||||
disableMegaphone
|
||||
containerWidthBreakpoint={WidthBreakpoint.Narrow}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -77,6 +77,7 @@ import { SmartMessageSearchResult } from './MessageSearchResult';
|
|||
import { SmartNetworkStatus } from './NetworkStatus';
|
||||
import { SmartRelinkDialog } from './RelinkDialog';
|
||||
import { SmartUnsupportedOSDialog } from './UnsupportedOSDialog';
|
||||
import { SmartToastManager } from './ToastManager';
|
||||
import type { PropsType as SmartUnsupportedOSDialogPropsType } from './UnsupportedOSDialog';
|
||||
import { SmartUpdateDialog } from './UpdateDialog';
|
||||
import { SmartCaptchaDialog } from './CaptchaDialog';
|
||||
|
@ -116,6 +117,17 @@ function renderUnsupportedOSDialog(
|
|||
): JSX.Element {
|
||||
return <SmartUnsupportedOSDialog {...props} />;
|
||||
}
|
||||
function renderToastManager(props: {
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
}): JSX.Element {
|
||||
return <SmartToastManager {...props} />;
|
||||
}
|
||||
|
||||
function renderToastManagerWithoutMegaphone(props: {
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
}): JSX.Element {
|
||||
return <SmartToastManager disableMegaphone {...props} />;
|
||||
}
|
||||
|
||||
const getModeSpecificProps = (
|
||||
state: StateType
|
||||
|
@ -223,6 +235,10 @@ const mapStateToProps = (state: StateType) => {
|
|||
unsupportedOSDialogType = 'warning';
|
||||
}
|
||||
|
||||
const composerStep = getComposerStep(state);
|
||||
const showArchived = getShowArchived(state);
|
||||
const hasSearchQuery = isSearching(state);
|
||||
|
||||
return {
|
||||
hasNetworkDialog: hasNetworkDialog(state),
|
||||
hasExpiredDialog,
|
||||
|
@ -238,7 +254,7 @@ const mapStateToProps = (state: StateType) => {
|
|||
preferredWidthFromStorage: getPreferredLeftPaneWidth(state),
|
||||
selectedConversationId: getSelectedConversationId(state),
|
||||
targetedMessageId: getTargetedMessage(state)?.id,
|
||||
showArchived: getShowArchived(state),
|
||||
showArchived,
|
||||
getPreferredBadge: getPreferredBadgeSelector(state),
|
||||
i18n: getIntl(state),
|
||||
isMacOS: getIsMacOS(state),
|
||||
|
@ -253,6 +269,10 @@ const mapStateToProps = (state: StateType) => {
|
|||
renderCrashReportDialog,
|
||||
renderExpiredBuildDialog,
|
||||
renderUnsupportedOSDialog,
|
||||
renderToastManager:
|
||||
composerStep == null && !showArchived && !hasSearchQuery
|
||||
? renderToastManager
|
||||
: renderToastManagerWithoutMegaphone,
|
||||
lookupConversationWithoutServiceId,
|
||||
theme: getTheme(state),
|
||||
};
|
||||
|
|
|
@ -14,7 +14,6 @@ import { getIntl } from '../selectors/user';
|
|||
import {
|
||||
getEmojiSkinTone,
|
||||
getUsernamesEnabled,
|
||||
getHasCompletedUsernameOnboarding,
|
||||
getHasCompletedUsernameLinkOnboarding,
|
||||
getUsernameCorrupted,
|
||||
getUsernameLinkColor,
|
||||
|
@ -53,8 +52,6 @@ function mapStateToProps(
|
|||
const recentEmojis = selectRecentEmojis(state);
|
||||
const skinTone = getEmojiSkinTone(state);
|
||||
const isUsernameFlagEnabled = getUsernamesEnabled(state);
|
||||
const hasCompletedUsernameOnboarding =
|
||||
getHasCompletedUsernameOnboarding(state);
|
||||
const hasCompletedUsernameLinkOnboarding =
|
||||
getHasCompletedUsernameLinkOnboarding(state);
|
||||
const usernameEditState = getUsernameEditState(state);
|
||||
|
@ -72,9 +69,9 @@ function mapStateToProps(
|
|||
conversationId,
|
||||
familyName,
|
||||
firstName: String(firstName),
|
||||
hasCompletedUsernameOnboarding,
|
||||
hasCompletedUsernameLinkOnboarding,
|
||||
hasError: state.globalModals.profileEditorHasError,
|
||||
initialEditState: state.globalModals.profileEditorInitialEditState,
|
||||
i18n: getIntl(state),
|
||||
isUsernameFlagEnabled,
|
||||
recentEmojis,
|
||||
|
|
|
@ -7,6 +7,8 @@ import { useSelector } from 'react-redux';
|
|||
import type { LocalizerType } from '../../types/Util';
|
||||
import type { StateType } from '../reducer';
|
||||
import { SmartStoryCreator } from './StoryCreator';
|
||||
import { SmartToastManager } from './ToastManager';
|
||||
import type { WidthBreakpoint } from '../../components/_util';
|
||||
import { StoriesTab } from '../../components/StoriesTab';
|
||||
import { getMaximumOutgoingAttachmentSizeInKb } from '../../types/AttachmentSize';
|
||||
import type { ConfigKeyType } from '../../RemoteConfig';
|
||||
|
@ -38,6 +40,12 @@ function renderStoryCreator(): JSX.Element {
|
|||
return <SmartStoryCreator />;
|
||||
}
|
||||
|
||||
function renderToastManager(props: {
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
}): JSX.Element {
|
||||
return <SmartToastManager disableMegaphone {...props} />;
|
||||
}
|
||||
|
||||
export function SmartStoriesTab(): JSX.Element | null {
|
||||
const storiesActions = useStoriesActions();
|
||||
const {
|
||||
|
@ -122,6 +130,7 @@ export function SmartStoriesTab(): JSX.Element | null {
|
|||
preferredLeftPaneWidth={preferredLeftPaneWidth}
|
||||
preferredWidthFromStorage={preferredWidthFromStorage}
|
||||
renderStoryCreator={renderStoryCreator}
|
||||
renderToastManager={renderToastManager}
|
||||
retryMessageSend={retryMessageSend}
|
||||
savePreferredLeftPaneWidth={savePreferredLeftPaneWidth}
|
||||
showConversation={showConversation}
|
||||
|
|
107
ts/state/smart/ToastManager.tsx
Normal file
107
ts/state/smart/ToastManager.tsx
Normal file
|
@ -0,0 +1,107 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import type { AnyActionableMegaphone } from '../../types/Megaphone';
|
||||
import { MegaphoneType } from '../../types/Megaphone';
|
||||
import { UsernameOnboardingState } from '../../types/globalModals';
|
||||
import OS from '../../util/os/osMain';
|
||||
import { drop } from '../../util/drop';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import {
|
||||
getGlobalModalsState,
|
||||
isShowingAnyModal as getIsShowingAnyModal,
|
||||
} from '../selectors/globalModals';
|
||||
import { hasSelectedStoryData } from '../selectors/stories';
|
||||
import { shouldShowLightbox } from '../selectors/lightbox';
|
||||
import { isInFullScreenCall as getIsInFullScreenCall } from '../selectors/calling';
|
||||
import { getSelectedNavTab } from '../selectors/nav';
|
||||
import type { StateType } from '../reducer';
|
||||
import { useConversationsActions } from '../ducks/conversations';
|
||||
import type { ConversationsStateType } from '../ducks/conversations';
|
||||
import { useToastActions } from '../ducks/toast';
|
||||
import { useGlobalModalActions } from '../ducks/globalModals';
|
||||
import { NavTab } from '../ducks/nav';
|
||||
import {
|
||||
getUsernamesEnabled,
|
||||
getHasCompletedUsernameOnboarding,
|
||||
} from '../selectors/items';
|
||||
import { ToastManager } from '../../components/ToastManager';
|
||||
import type { WidthBreakpoint } from '../../components/_util';
|
||||
|
||||
export type SmartPropsType = Readonly<{
|
||||
disableMegaphone?: boolean;
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
}>;
|
||||
|
||||
export function SmartToastManager({
|
||||
disableMegaphone = false,
|
||||
containerWidthBreakpoint,
|
||||
}: SmartPropsType): JSX.Element {
|
||||
const i18n = useSelector(getIntl);
|
||||
const isUsernameFlagEnabled = useSelector(getUsernamesEnabled);
|
||||
const hasCompletedUsernameOnboarding = useSelector(
|
||||
getHasCompletedUsernameOnboarding
|
||||
);
|
||||
const toast = useSelector((state: StateType) => state.toast.toast);
|
||||
const globalModals = useSelector(getGlobalModalsState);
|
||||
const isShowingAnyModal = useSelector(getIsShowingAnyModal);
|
||||
const isShowingStory = useSelector(hasSelectedStoryData);
|
||||
const isShowingLightbox = useSelector(shouldShowLightbox);
|
||||
const isInFullScreenCall = useSelector(getIsInFullScreenCall);
|
||||
|
||||
const selectedNavTab = useSelector(getSelectedNavTab);
|
||||
const { selectedConversationId } = useSelector<
|
||||
StateType,
|
||||
ConversationsStateType
|
||||
>(state => state.conversations);
|
||||
|
||||
const { onUndoArchive } = useConversationsActions();
|
||||
|
||||
const { openFileInFolder, hideToast } = useToastActions();
|
||||
|
||||
const { toggleUsernameOnboarding } = useGlobalModalActions();
|
||||
|
||||
let megaphone: AnyActionableMegaphone | undefined;
|
||||
|
||||
if (
|
||||
isUsernameFlagEnabled &&
|
||||
!hasCompletedUsernameOnboarding &&
|
||||
globalModals.usernameOnboardingState === UsernameOnboardingState.NeverShown
|
||||
) {
|
||||
megaphone = {
|
||||
type: MegaphoneType.UsernameOnboarding,
|
||||
onLearnMore: toggleUsernameOnboarding,
|
||||
onDismiss: () => {
|
||||
drop(window.storage.put('hasCompletedUsernameOnboarding', true));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const centerToast =
|
||||
isShowingAnyModal ||
|
||||
isShowingStory ||
|
||||
isShowingLightbox ||
|
||||
isInFullScreenCall;
|
||||
|
||||
const isCompositionAreaVisible =
|
||||
selectedNavTab === NavTab.Chats && Boolean(selectedConversationId);
|
||||
|
||||
return (
|
||||
<ToastManager
|
||||
i18n={i18n}
|
||||
OS={OS.getName()}
|
||||
toast={toast}
|
||||
megaphone={disableMegaphone ? undefined : megaphone}
|
||||
onShowDebugLog={() => window.IPC.showDebugLog()}
|
||||
onUndoArchive={onUndoArchive}
|
||||
openFileInFolder={openFileInFolder}
|
||||
hideToast={hideToast}
|
||||
centerToast={centerToast}
|
||||
containerWidthBreakpoint={containerWidthBreakpoint}
|
||||
isCompositionAreaVisible={isCompositionAreaVisible}
|
||||
/>
|
||||
);
|
||||
}
|
43
ts/state/smart/UsernameOnboardingModal.tsx
Normal file
43
ts/state/smart/UsernameOnboardingModal.tsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { UsernameOnboardingModal } from '../../components/UsernameOnboardingModal';
|
||||
import { EditState } from '../../components/ProfileEditor';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import { useGlobalModalActions } from '../ducks/globalModals';
|
||||
import { useUsernameActions } from '../ducks/username';
|
||||
|
||||
export function SmartUsernameOnboardingModal(): JSX.Element {
|
||||
const i18n = useSelector(getIntl);
|
||||
const { toggleProfileEditor, toggleUsernameOnboarding } =
|
||||
useGlobalModalActions();
|
||||
const { openUsernameReservationModal } = useUsernameActions();
|
||||
|
||||
const onNext = useCallback(async () => {
|
||||
await window.storage.put('hasCompletedUsernameOnboarding', true);
|
||||
openUsernameReservationModal();
|
||||
toggleProfileEditor(EditState.Username);
|
||||
toggleUsernameOnboarding();
|
||||
}, [
|
||||
toggleProfileEditor,
|
||||
toggleUsernameOnboarding,
|
||||
openUsernameReservationModal,
|
||||
]);
|
||||
|
||||
const onSkip = useCallback(async () => {
|
||||
await window.storage.put('hasCompletedUsernameOnboarding', true);
|
||||
toggleUsernameOnboarding();
|
||||
}, [toggleUsernameOnboarding]);
|
||||
|
||||
return (
|
||||
<UsernameOnboardingModal
|
||||
i18n={i18n}
|
||||
onNext={onNext}
|
||||
onSkip={onSkip}
|
||||
onClose={toggleUsernameOnboarding}
|
||||
/>
|
||||
);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue