Refactor smart components

Co-authored-by: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com>
This commit is contained in:
Jamie Kyle 2024-03-13 13:44:13 -07:00 committed by GitHub
parent 05c09ef769
commit 27b55e472d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
109 changed files with 3583 additions and 2629 deletions

View file

@ -1,7 +1,7 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useContext } from 'react';
import React from 'react';
import type { Meta, StoryFn } from '@storybook/react';
import { action } from '@storybook/addon-actions';
@ -13,7 +13,6 @@ import {
} from '../test-both/helpers/getDefaultConversation';
import { setupI18n } from '../util/setupI18n';
import { AddUserToAnotherGroupModal } from './AddUserToAnotherGroupModal';
import { StorybookThemeContext } from '../../.storybook/StorybookThemeContext';
const i18n = setupI18n('en', enMessages);
@ -36,7 +35,6 @@ const Template: StoryFn<Props> = args => {
toggleAddUserToAnotherGroupModal={action(
'toggleAddUserToAnotherGroupModal'
)}
theme={useContext(StorybookThemeContext)}
/>
);
};

View file

@ -6,7 +6,7 @@ import React, { useCallback } from 'react';
import type { ListRowProps } from 'react-virtualized';
import type { ConversationType } from '../state/ducks/conversations';
import type { LocalizerType, ThemeType } from '../types/Util';
import type { LocalizerType } from '../types/Util';
import { ToastType } from '../types/Toast';
import { filterAndSortConversationsByRecent } from '../util/filterAndSortConversations';
import { ConfirmationDialog } from './ConfirmationDialog';
@ -25,7 +25,6 @@ import { SizeObserver } from '../hooks/useSizeObserver';
type OwnProps = {
i18n: LocalizerType;
theme: ThemeType;
contact: Pick<ConversationType, 'id' | 'title' | 'serviceId' | 'pni'>;
candidateConversations: ReadonlyArray<ConversationType>;
regionCode: string | undefined;

View file

@ -139,9 +139,23 @@ export type PropsType = {
pauseVoiceNotePlayer: () => void;
} & Pick<ReactionPickerProps, 'renderEmojiPicker'>;
type ActiveCallManagerPropsType = PropsType & {
type ActiveCallManagerPropsType = {
activeCall: ActiveCallType;
};
} & Omit<
PropsType,
| 'acceptCall'
| 'bounceAppIconStart'
| 'bounceAppIconStop'
| 'declineCall'
| 'hasInitialLoadCompleted'
| 'incomingCall'
| 'isConversationTooBigToRin'
| 'notifyForCall'
| 'playRingtone'
| 'setIsCallActive'
| 'stopRingtone'
| 'isConversationTooBigToRing'
>;
function ActiveCallManager({
activeCall,
@ -472,28 +486,69 @@ function ActiveCallManager({
);
}
export function CallManager(props: PropsType): JSX.Element | null {
const {
acceptCall,
activeCall,
bounceAppIconStart,
bounceAppIconStop,
declineCall,
i18n,
incomingCall,
notifyForCall,
playRingtone,
stopRingtone,
setIsCallActive,
setOutgoingRing,
} = props;
export function CallManager({
acceptCall,
activeCall,
availableCameras,
bounceAppIconStart,
bounceAppIconStop,
callLink,
cancelCall,
changeCallView,
closeNeedPermissionScreen,
declineCall,
getGroupCallVideoFrameSource,
getPreferredBadge,
getPresentingSources,
hangUpActiveCall,
hasInitialLoadCompleted,
i18n,
incomingCall,
isConversationTooBigToRing,
isGroupCallRaiseHandEnabled,
isGroupCallReactionsEnabled,
keyChangeOk,
me,
notifyForCall,
openSystemPreferencesAction,
pauseVoiceNotePlayer,
playRingtone,
renderDeviceSelection,
renderEmojiPicker,
renderReactionPicker,
renderSafetyNumberViewer,
sendGroupCallRaiseHand,
sendGroupCallReaction,
setGroupCallVideoRequest,
setIsCallActive,
setLocalAudio,
setLocalPreview,
setLocalVideo,
setOutgoingRing,
setPresenting,
setRendererCanvas,
showToast,
startCall,
stopRingtone,
switchFromPresentationView,
switchToPresentationView,
theme,
toggleParticipants,
togglePip,
toggleScreenRecordingPermissionsDialog,
toggleSettings,
}: PropsType): JSX.Element | null {
const isCallActive = Boolean(activeCall);
useEffect(() => {
setIsCallActive(isCallActive);
}, [isCallActive, setIsCallActive]);
const shouldRing = getShouldRing(props);
const shouldRing = getShouldRing({
activeCall,
incomingCall,
isConversationTooBigToRing,
hasInitialLoadCompleted,
});
useEffect(() => {
if (shouldRing) {
log.info('CallManager: Playing ringtone');
@ -529,8 +584,50 @@ export function CallManager(props: PropsType): JSX.Element | null {
// `props` should logically have an `activeCall` at this point, but TypeScript can't
// figure that out, so we pass it in again.
return (
<CallingToastProvider i18n={props.i18n}>
<ActiveCallManager {...props} activeCall={activeCall} />
<CallingToastProvider i18n={i18n}>
<ActiveCallManager
activeCall={activeCall}
availableCameras={availableCameras}
callLink={callLink}
cancelCall={cancelCall}
changeCallView={changeCallView}
closeNeedPermissionScreen={closeNeedPermissionScreen}
getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
getPreferredBadge={getPreferredBadge}
getPresentingSources={getPresentingSources}
hangUpActiveCall={hangUpActiveCall}
i18n={i18n}
isGroupCallRaiseHandEnabled={isGroupCallRaiseHandEnabled}
isGroupCallReactionsEnabled={isGroupCallReactionsEnabled}
keyChangeOk={keyChangeOk}
me={me}
openSystemPreferencesAction={openSystemPreferencesAction}
pauseVoiceNotePlayer={pauseVoiceNotePlayer}
renderDeviceSelection={renderDeviceSelection}
renderEmojiPicker={renderEmojiPicker}
renderReactionPicker={renderReactionPicker}
renderSafetyNumberViewer={renderSafetyNumberViewer}
sendGroupCallRaiseHand={sendGroupCallRaiseHand}
sendGroupCallReaction={sendGroupCallReaction}
setGroupCallVideoRequest={setGroupCallVideoRequest}
setLocalAudio={setLocalAudio}
setLocalPreview={setLocalPreview}
setLocalVideo={setLocalVideo}
setOutgoingRing={setOutgoingRing}
setPresenting={setPresenting}
setRendererCanvas={setRendererCanvas}
showToast={showToast}
startCall={startCall}
switchFromPresentationView={switchFromPresentationView}
switchToPresentationView={switchToPresentationView}
theme={theme}
toggleParticipants={toggleParticipants}
togglePip={togglePip}
toggleScreenRecordingPermissionsDialog={
toggleScreenRecordingPermissionsDialog
}
toggleSettings={toggleSettings}
/>
</CallingToastProvider>
);
}

View file

@ -8,17 +8,20 @@ import { Button, ButtonVariant } from './Button';
import { Modal } from './Modal';
import { Spinner } from './Spinner';
export type PropsType = {
export type PropsType = Readonly<{
i18n: LocalizerType;
isPending: boolean;
onContinue: () => void;
onSkip: () => void;
};
export function CaptchaDialog(props: Readonly<PropsType>): JSX.Element {
const { i18n, isPending, onSkip, onContinue } = props;
}>;
export function CaptchaDialog({
i18n,
isPending,
onSkip,
onContinue,
}: PropsType): JSX.Element {
const [isClosing, setIsClosing] = useState(false);
const buttonRef = useRef<HTMLButtonElement | null>(null);

View file

@ -18,9 +18,12 @@ export type PropsType = {
isPending: boolean;
} & PropsActionsType;
export function CrashReportDialog(props: Readonly<PropsType>): JSX.Element {
const { i18n, isPending, writeCrashReportsToLog, eraseCrashReports } = props;
export function CrashReportDialog({
i18n,
isPending,
writeCrashReportsToLog,
eraseCrashReports,
}: Readonly<PropsType>): JSX.Element {
const onEraseClick = (event: React.MouseEvent) => {
event.preventDefault();

View file

@ -1,6 +1,7 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ComponentType } from 'react';
import React, {
useCallback,
useEffect,
@ -61,9 +62,7 @@ export type DataPropsType = {
caretLocation?: number
) => unknown;
regionCode: string | undefined;
RenderCompositionTextArea: (
props: SmartCompositionTextAreaProps
) => JSX.Element;
RenderCompositionTextArea: ComponentType<SmartCompositionTextAreaProps>;
showToast: ShowToastAction;
theme: ThemeType;
};
@ -413,9 +412,7 @@ type ForwardMessageEditorProps = Readonly<{
draft: MessageForwardDraft;
linkPreview: LinkPreviewType | null | void;
removeLinkPreview(): void;
RenderCompositionTextArea: (
props: SmartCompositionTextAreaProps
) => JSX.Element;
RenderCompositionTextArea: ComponentType<SmartCompositionTextAreaProps>;
onChange: (
messageText: string,
bodyRanges: HydratedBodyRangesType,

View file

@ -36,13 +36,12 @@ const contact3: ConversationType = getDefaultConversation({
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
areWeInvited: Boolean(overrideProps.areWeInvited),
conversationId: '123',
droppedMembers: overrideProps.droppedMembers || [contact3, contact1],
getPreferredBadge: () => undefined,
hasMigrated: Boolean(overrideProps.hasMigrated),
i18n,
invitedMembers: overrideProps.invitedMembers || [contact2],
migrate: action('migrate'),
onMigrate: action('onMigrate'),
onClose: action('onClose'),
theme: ThemeType.light,
});

View file

@ -10,7 +10,6 @@ import { sortByTitle } from '../util/sortByTitle';
import { missingCaseError } from '../util/missingCaseError';
export type DataPropsType = {
conversationId: string;
readonly areWeInvited: boolean;
readonly droppedMembers: Array<ConversationType>;
readonly hasMigrated: boolean;
@ -20,51 +19,25 @@ export type DataPropsType = {
readonly theme: ThemeType;
};
type ActionsPropsType =
| {
initiateMigrationToGroupV2: (conversationId: string) => unknown;
closeGV2MigrationDialog: () => unknown;
}
| {
readonly migrate: () => unknown;
readonly onClose: () => unknown;
};
type ActionsPropsType = Readonly<{
onMigrate: () => unknown;
onClose: () => unknown;
}>;
export type PropsType = DataPropsType & ActionsPropsType;
export const GroupV1MigrationDialog: React.FunctionComponent<PropsType> =
React.memo(function GroupV1MigrationDialogInner(props: PropsType) {
const {
areWeInvited,
conversationId,
droppedMembers,
getPreferredBadge,
hasMigrated,
i18n,
invitedMembers,
theme,
} = props;
let migrateHandler;
if ('migrate' in props) {
migrateHandler = props.migrate;
} else if ('initiateMigrationToGroupV2' in props) {
migrateHandler = () => props.initiateMigrationToGroupV2(conversationId);
} else {
throw new Error(
'GroupV1MigrationDialog: No conversationId or migration function'
);
}
let closeHandler;
if ('onClose' in props) {
closeHandler = props.onClose;
} else if ('closeGV2MigrationDialog' in props) {
closeHandler = props.closeGV2MigrationDialog;
} else {
throw new Error('GroupV1MigrationDialog: No close function provided');
}
React.memo(function GroupV1MigrationDialogInner({
areWeInvited,
droppedMembers,
getPreferredBadge,
hasMigrated,
i18n,
invitedMembers,
theme,
onClose,
onMigrate,
}: PropsType) {
const title = hasMigrated
? i18n('icu:GroupV1--Migration--info--title')
: i18n('icu:GroupV1--Migration--migrate--title');
@ -82,13 +55,13 @@ export const GroupV1MigrationDialog: React.FunctionComponent<PropsType> =
};
if (hasMigrated) {
primaryButtonText = i18n('icu:Confirmation--confirm');
onClickPrimaryButton = closeHandler;
onClickPrimaryButton = onClose;
} else {
primaryButtonText = i18n('icu:GroupV1--Migration--migrate');
onClickPrimaryButton = migrateHandler;
onClickPrimaryButton = onMigrate;
secondaryButtonProps = {
secondaryButtonText: i18n('icu:cancel'),
onClickSecondaryButton: closeHandler,
onClickSecondaryButton: onClose,
};
}
@ -96,7 +69,7 @@ export const GroupV1MigrationDialog: React.FunctionComponent<PropsType> =
<GroupDialog
i18n={i18n}
onClickPrimaryButton={onClickPrimaryButton}
onClose={closeHandler}
onClose={onClose}
primaryButtonText={primaryButtonText}
title={title}
{...secondaryButtonProps}

View file

@ -30,21 +30,18 @@ function focusRef(el: HTMLElement | null) {
}
}
export const GroupV2JoinDialog = React.memo(function GroupV2JoinDialogInner(
props: PropsType
) {
export const GroupV2JoinDialog = React.memo(function GroupV2JoinDialogInner({
approvalRequired,
avatar,
groupDescription,
i18n,
join,
memberCount,
onClose,
title,
}: PropsType) {
const [isWorking, setIsWorking] = React.useState(false);
const [isJoining, setIsJoining] = React.useState(false);
const {
approvalRequired,
avatar,
groupDescription,
i18n,
join,
memberCount,
onClose,
title,
} = props;
const joinString = approvalRequired
? i18n('icu:GroupV2--join--request-to-join-button')

View file

@ -151,7 +151,6 @@ const useProps = (overrideProps: OverridePropsType = {}): PropsType => {
i18n,
isMacOS: false,
preferredWidthFromStorage: 320,
regionCode: 'US',
challengeStatus: 'idle',
crashReportCount: 0,

View file

@ -104,7 +104,6 @@ export type PropsType = {
preferredWidthFromStorage: number;
selectedConversationId: undefined | string;
targetedMessageId: undefined | string;
regionCode: string | undefined;
challengeStatus: 'idle' | 'required' | 'pending';
setChallengeStatus: (status: 'idle') => void;
crashReportCount: number;

View file

@ -24,13 +24,44 @@ type PropsType = {
Omit<ProfileEditorPropsType, 'onEditStateChanged' | 'onProfileChanged'>;
export function ProfileEditorModal({
aboutEmoji,
aboutText,
color,
conversationId,
deleteAvatarFromDisk,
deleteUsername,
familyName,
firstName,
hasCompletedUsernameLinkOnboarding,
hasError,
i18n,
initialEditState,
isUsernameDeletionEnabled,
markCompletedUsernameLinkOnboarding,
myProfileChanged,
onSetSkinTone,
openUsernameReservationModal,
profileAvatarPath,
recentEmojis,
renderEditUsernameModalBody,
replaceAvatar,
resetUsernameLink,
saveAttachment,
saveAvatarToDisk,
setUsernameEditState,
setUsernameLinkColor,
showToast,
skinTone,
toggleProfileEditor,
toggleProfileEditorHasError,
...restProps
userAvatarData,
username,
usernameCorrupted,
usernameEditState,
usernameLink,
usernameLinkColor,
usernameLinkCorrupted,
usernameLinkState,
}: PropsType): JSX.Element {
const MODAL_TITLES_BY_EDIT_STATE: Record<EditState, string | undefined> = {
[EditState.BetterAvatar]: i18n('icu:ProfileEditorModal--avatar'),
@ -67,14 +98,47 @@ export function ProfileEditorModal({
title={modalTitle}
>
<ProfileEditor
{...restProps}
aboutEmoji={aboutEmoji}
aboutText={aboutText}
color={color}
conversationId={conversationId}
deleteAvatarFromDisk={deleteAvatarFromDisk}
deleteUsername={deleteUsername}
familyName={familyName}
firstName={firstName}
hasCompletedUsernameLinkOnboarding={hasCompletedUsernameLinkOnboarding}
i18n={i18n}
initialEditState={initialEditState}
isUsernameDeletionEnabled={isUsernameDeletionEnabled}
markCompletedUsernameLinkOnboarding={
markCompletedUsernameLinkOnboarding
}
onEditStateChanged={editState => {
setModalTitle(MODAL_TITLES_BY_EDIT_STATE[editState]);
}}
onProfileChanged={myProfileChanged}
onSetSkinTone={onSetSkinTone}
openUsernameReservationModal={openUsernameReservationModal}
profileAvatarPath={profileAvatarPath}
recentEmojis={recentEmojis}
renderEditUsernameModalBody={renderEditUsernameModalBody}
replaceAvatar={replaceAvatar}
resetUsernameLink={resetUsernameLink}
saveAttachment={saveAttachment}
saveAvatarToDisk={saveAvatarToDisk}
setUsernameEditState={setUsernameEditState}
setUsernameLinkColor={setUsernameLinkColor}
showToast={showToast}
skinTone={skinTone}
toggleProfileEditor={toggleProfileEditor}
userAvatarData={userAvatarData}
username={username}
usernameCorrupted={usernameCorrupted}
usernameEditState={usernameEditState}
usernameLink={usernameLink}
usernameLinkColor={usernameLinkColor}
usernameLinkCorrupted={usernameLinkCorrupted}
usernameLinkState={usernameLinkState}
/>
</Modal>
);

View file

@ -14,10 +14,10 @@ export type PropsType = {
conversationId: string;
files: ReadonlyArray<File>;
}) => void;
renderCompositionArea: () => JSX.Element;
renderConversationHeader: () => JSX.Element;
renderTimeline: () => JSX.Element;
renderPanel: () => JSX.Element | undefined;
renderCompositionArea: (conversationId: string) => JSX.Element;
renderConversationHeader: (conversationId: string) => JSX.Element;
renderTimeline: (conversationId: string) => JSX.Element;
renderPanel: (conversationId: string) => JSX.Element | undefined;
shouldHideConversationView?: boolean;
};
@ -121,20 +121,20 @@ export function ConversationView({
})}
>
<div className="ConversationView__header">
{renderConversationHeader()}
{renderConversationHeader(conversationId)}
</div>
<div className="ConversationView__pane">
<div className="ConversationView__timeline--container">
<div aria-live="polite" className="ConversationView__timeline">
{renderTimeline()}
{renderTimeline(conversationId)}
</div>
</div>
<div className="ConversationView__composition-area">
{renderCompositionArea()}
{renderCompositionArea(conversationId)}
</div>
</div>
</div>
{renderPanel()}
{renderPanel(conversationId)}
</div>
);
}

View file

@ -31,7 +31,6 @@ export type PropsType = PropsDataType & PropsHousekeepingType;
export function GroupV1Migration(props: PropsType): React.ReactElement {
const {
areWeInvited,
conversationId,
droppedMembers,
getPreferredBadge,
i18n,
@ -80,13 +79,12 @@ export function GroupV1Migration(props: PropsType): React.ReactElement {
{showingDialog ? (
<GroupV1MigrationDialog
areWeInvited={areWeInvited}
conversationId={conversationId}
droppedMembers={droppedMembers}
getPreferredBadge={getPreferredBadge}
hasMigrated
i18n={i18n}
invitedMembers={invitedMembers}
migrate={() => log.warn('GroupV1Migration: Modal called migrate()')}
onMigrate={() => log.warn('GroupV1Migration: Modal called migrate()')}
onClose={dismissDialog}
theme={theme}
/>

View file

@ -452,7 +452,9 @@ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
isIncomingMessageRequest: overrideProps.isIncomingMessageRequest ?? false,
items: overrideProps.items ?? Object.keys(items),
messageChangeCounter: 0,
scrollToIndex: overrideProps.scrollToIndex,
messageLoadingState: null,
isNearBottom: null,
scrollToIndex: overrideProps.scrollToIndex ?? null,
scrollToIndexCounter: 0,
shouldShowMiniPlayer: Boolean(overrideProps.shouldShowMiniPlayer),
totalUnseen: overrideProps.totalUnseen ?? 0,

View file

@ -70,11 +70,11 @@ export type PropsDataType = {
haveNewest: boolean;
haveOldest: boolean;
messageChangeCounter: number;
messageLoadingState?: TimelineMessageLoadingState;
isNearBottom?: boolean;
messageLoadingState: TimelineMessageLoadingState | null;
isNearBottom: boolean | null;
items: ReadonlyArray<string>;
oldestUnseenIndex?: number;
scrollToIndex?: number;
oldestUnseenIndex: number | null;
scrollToIndex: number | null;
scrollToIndexCounter: number;
totalUnseen: number;
};
@ -563,7 +563,7 @@ export class Timeline extends React.Component<
case ScrollAnchor.ScrollToBottom:
return { scrollBottom: 0 };
case ScrollAnchor.ScrollToIndex:
if (scrollToIndex === undefined) {
if (scrollToIndex == null) {
assertDev(
false,
'<Timeline> got "scroll to index" scroll anchor, but no index'

View file

@ -63,7 +63,6 @@ const createProps = (
selectedConversationIds
)}
regionCode="US"
getPreferredBadge={() => undefined}
ourE164={undefined}
ourUsername={undefined}
theme={ThemeType.light}

View file

@ -21,7 +21,6 @@ import { parseAndFormatPhoneNumber } from '../../../../util/libphonenumberInstan
import type { ParsedE164Type } from '../../../../util/libphonenumberInstance';
import { filterAndSortConversationsByRecent } from '../../../../util/filterAndSortConversations';
import type { ConversationType } from '../../../../state/ducks/conversations';
import type { PreferredBadgeSelectorType } from '../../../../state/selectors/badges';
import type {
UUIDFetchStateKeyType,
UUIDFetchStateType,
@ -50,7 +49,6 @@ export type StatePropsType = {
regionCode: string | undefined;
candidateContacts: ReadonlyArray<ConversationType>;
conversationIdsAlreadyInGroup: Set<string>;
getPreferredBadge: PreferredBadgeSelectorType;
i18n: LocalizerType;
theme: ThemeType;
maxGroupSize: number;

View file

@ -119,7 +119,6 @@ const createProps = (
candidateContacts={allCandidateContacts}
selectedContacts={[]}
regionCode="US"
getPreferredBadge={() => undefined}
theme={ThemeType.light}
i18n={i18n}
lookupConversationWithoutServiceId={makeFakeLookupConversationWithoutServiceId()}

View file

@ -29,7 +29,7 @@ export type OwnProps = {
export type Props = OwnProps;
function renderBody({ pack, i18n }: Props) {
function renderBody({ pack, i18n }: Pick<Props, 'i18n' | 'pack'>) {
if (!pack) {
return null;
}
@ -73,10 +73,8 @@ function renderBody({ pack, i18n }: Props) {
);
}
export const StickerPreviewModal = React.memo(function StickerPreviewModalInner(
props: Props
) {
const {
export const StickerPreviewModal = React.memo(
function StickerPreviewModalInner({
closeStickerPackPreview,
downloadStickerPack,
i18n,
@ -84,142 +82,143 @@ export const StickerPreviewModal = React.memo(function StickerPreviewModalInner(
onClose,
pack,
uninstallStickerPack,
} = props;
const [confirmingUninstall, setConfirmingUninstall] = React.useState(false);
}: Props) {
const [confirmingUninstall, setConfirmingUninstall] = React.useState(false);
// Restore focus on teardown
const [focusRef] = useRestoreFocus();
// Restore focus on teardown
const [focusRef] = useRestoreFocus();
React.useEffect(() => {
if (pack && pack.status === 'known') {
downloadStickerPack(pack.id, pack.key);
}
if (
pack &&
pack.status === 'error' &&
(pack.attemptedStatus === 'downloaded' ||
pack.attemptedStatus === 'installed')
) {
downloadStickerPack(pack.id, pack.key, {
finalStatus: pack.attemptedStatus,
});
}
// We only want to attempt downloads on initial load
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
React.useEffect(() => {
if (pack && pack.status === 'known') {
downloadStickerPack(pack.id, pack.key);
}
if (
pack &&
pack.status === 'error' &&
(pack.attemptedStatus === 'downloaded' ||
pack.attemptedStatus === 'installed')
) {
downloadStickerPack(pack.id, pack.key, {
finalStatus: pack.attemptedStatus,
});
}
// We only want to attempt downloads on initial load
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
React.useEffect(() => {
if (pack) {
return;
}
React.useEffect(() => {
if (pack) {
return;
}
// Pack fully uninstalled, don't keep the modal open
closeStickerPackPreview();
}, [pack, closeStickerPackPreview]);
const handleClose = React.useCallback(() => {
if (pack) {
// Pack fully uninstalled, don't keep the modal open
closeStickerPackPreview();
}
onClose?.();
}, [closeStickerPackPreview, onClose, pack]);
}, [pack, closeStickerPackPreview]);
const isInstalled = Boolean(pack && pack.status === 'installed');
const handleToggleInstall = React.useCallback(() => {
if (!pack) {
return;
}
if (isInstalled) {
setConfirmingUninstall(true);
} else if (pack.status === 'ephemeral') {
downloadStickerPack(pack.id, pack.key, { finalStatus: 'installed' });
handleClose();
} else {
installStickerPack(pack.id, pack.key);
handleClose();
}
}, [
downloadStickerPack,
installStickerPack,
isInstalled,
handleClose,
pack,
setConfirmingUninstall,
]);
const handleClose = React.useCallback(() => {
if (pack) {
closeStickerPackPreview();
}
onClose?.();
}, [closeStickerPackPreview, onClose, pack]);
const handleUninstall = React.useCallback(() => {
if (!pack) {
return;
}
uninstallStickerPack(pack.id, pack.key);
setConfirmingUninstall(false);
// closeStickerPackPreview is called by <ConfirmationDialog />'s onClose
}, [uninstallStickerPack, setConfirmingUninstall, pack]);
const isInstalled = Boolean(pack && pack.status === 'installed');
const handleToggleInstall = React.useCallback(() => {
if (!pack) {
return;
}
if (isInstalled) {
setConfirmingUninstall(true);
} else if (pack.status === 'ephemeral') {
downloadStickerPack(pack.id, pack.key, { finalStatus: 'installed' });
handleClose();
} else {
installStickerPack(pack.id, pack.key);
handleClose();
}
}, [
downloadStickerPack,
installStickerPack,
isInstalled,
handleClose,
pack,
setConfirmingUninstall,
]);
const buttonLabel = isInstalled
? i18n('icu:stickers--StickerManager--Uninstall')
: i18n('icu:stickers--StickerManager--Install');
const handleUninstall = React.useCallback(() => {
if (!pack) {
return;
}
uninstallStickerPack(pack.id, pack.key);
setConfirmingUninstall(false);
// closeStickerPackPreview is called by <ConfirmationDialog />'s onClose
}, [uninstallStickerPack, setConfirmingUninstall, pack]);
const modalFooter =
pack && pack.status !== 'error' ? (
<div className="module-sticker-manager__preview-modal__footer">
<div className="module-sticker-manager__preview-modal__footer--info">
<h3 className="module-sticker-manager__preview-modal__footer--title">
<UserText text={pack.title} />
{pack.isBlessed ? (
<span className="module-sticker-manager__preview-modal__footer--blessed-icon" />
) : null}
</h3>
<h4 className="module-sticker-manager__preview-modal__footer--author">
{pack.author}
</h4>
const buttonLabel = isInstalled
? i18n('icu:stickers--StickerManager--Uninstall')
: i18n('icu:stickers--StickerManager--Install');
const modalFooter =
pack && pack.status !== 'error' ? (
<div className="module-sticker-manager__preview-modal__footer">
<div className="module-sticker-manager__preview-modal__footer--info">
<h3 className="module-sticker-manager__preview-modal__footer--title">
<UserText text={pack.title} />
{pack.isBlessed ? (
<span className="module-sticker-manager__preview-modal__footer--blessed-icon" />
) : null}
</h3>
<h4 className="module-sticker-manager__preview-modal__footer--author">
{pack.author}
</h4>
</div>
<div className="module-sticker-manager__preview-modal__footer--install">
{pack.status === 'pending' ? (
<Spinner svgSize="small" size="14px" />
) : (
<Button
aria-label={buttonLabel}
ref={focusRef}
onClick={handleToggleInstall}
variant={ButtonVariant.Primary}
>
{buttonLabel}
</Button>
)}
</div>
</div>
<div className="module-sticker-manager__preview-modal__footer--install">
{pack.status === 'pending' ? (
<Spinner svgSize="small" size="14px" />
) : (
<Button
aria-label={buttonLabel}
ref={focusRef}
onClick={handleToggleInstall}
variant={ButtonVariant.Primary}
>
{buttonLabel}
</Button>
)}
</div>
</div>
) : undefined;
) : undefined;
return (
<>
{confirmingUninstall && (
<ConfirmationDialog
dialogName="StickerPreviewModal.confirmUninstall"
actions={[
{
style: 'negative',
text: i18n('icu:stickers--StickerManager--Uninstall'),
action: handleUninstall,
},
]}
return (
<>
{confirmingUninstall && (
<ConfirmationDialog
dialogName="StickerPreviewModal.confirmUninstall"
actions={[
{
style: 'negative',
text: i18n('icu:stickers--StickerManager--Uninstall'),
action: handleUninstall,
},
]}
i18n={i18n}
onClose={() => setConfirmingUninstall(false)}
>
{i18n('icu:stickers--StickerManager--UninstallWarning')}
</ConfirmationDialog>
)}
<Modal
hasXButton
i18n={i18n}
onClose={() => setConfirmingUninstall(false)}
modalFooter={modalFooter}
modalName="StickerPreviewModal"
moduleClassName="module-sticker-manager__preview-modal__modal"
onClose={handleClose}
title={i18n('icu:stickers--StickerPreview--Title')}
>
{i18n('icu:stickers--StickerManager--UninstallWarning')}
</ConfirmationDialog>
)}
<Modal
hasXButton
i18n={i18n}
modalFooter={modalFooter}
modalName="StickerPreviewModal"
moduleClassName="module-sticker-manager__preview-modal__modal"
onClose={handleClose}
title={i18n('icu:stickers--StickerPreview--Title')}
>
{renderBody(props)}
</Modal>
</>
);
});
{renderBody({ pack, i18n })}
</Modal>
</>
);
}
);