Refactor smart components
Co-authored-by: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com>
This commit is contained in:
parent
05c09ef769
commit
27b55e472d
109 changed files with 3583 additions and 2629 deletions
|
@ -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)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -151,7 +151,6 @@ const useProps = (overrideProps: OverridePropsType = {}): PropsType => {
|
|||
i18n,
|
||||
isMacOS: false,
|
||||
preferredWidthFromStorage: 320,
|
||||
regionCode: 'US',
|
||||
challengeStatus: 'idle',
|
||||
crashReportCount: 0,
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -63,7 +63,6 @@ const createProps = (
|
|||
selectedConversationIds
|
||||
)}
|
||||
regionCode="US"
|
||||
getPreferredBadge={() => undefined}
|
||||
ourE164={undefined}
|
||||
ourUsername={undefined}
|
||||
theme={ThemeType.light}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -119,7 +119,6 @@ const createProps = (
|
|||
candidateContacts={allCandidateContacts}
|
||||
selectedContacts={[]}
|
||||
regionCode="US"
|
||||
getPreferredBadge={() => undefined}
|
||||
theme={ThemeType.light}
|
||||
i18n={i18n}
|
||||
lookupConversationWithoutServiceId={makeFakeLookupConversationWithoutServiceId()}
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue