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>
</>
);
}
);

View file

@ -4,8 +4,6 @@
import { useCallback, useEffect } from 'react';
import { get } from 'lodash';
import { useSelector } from 'react-redux';
import type { StateType } from '../state/reducer';
import * as KeyboardLayout from '../services/keyboardLayout';
import { getHasPanelOpen } from '../state/selectors/conversations';
import { isInFullScreenCall } from '../state/selectors/calling';
@ -33,11 +31,11 @@ function useHasPanels(): boolean {
}
function useHasGlobalModal(): boolean {
return useSelector<StateType, boolean>(isShowingAnyModal);
return useSelector(isShowingAnyModal);
}
function useHasCalling(): boolean {
return useSelector<StateType, boolean>(isInFullScreenCall);
return useSelector(isInFullScreenCall);
}
function useHasAnyOverlay(): boolean {

View file

@ -11,6 +11,8 @@ import type { StateType as RootStateType } from '../reducer';
import { showToast } from './toast';
import type { ShowToastActionType } from './toast';
import type { PromiseAction } from '../util';
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
import { useBoundActions } from '../../hooks/useBoundActions';
// State
@ -44,6 +46,10 @@ export const actions = {
eraseCrashReports,
};
export const useCrashReportsActions = (): BoundActionCreatorsMapObject<
typeof actions
> => useBoundActions(actions);
function setCrashReportCount(count: number): SetCrashReportCountActionType {
return { type: SET_COUNT, payload: count };
}

View file

@ -5,6 +5,8 @@ import type { ReadonlyDeep } from 'type-fest';
import { SocketStatus } from '../../types/SocketStatus';
import { trigger } from '../../shims/events';
import { assignWithNoUnnecessaryAllocation } from '../../util/assignWithNoUnnecessaryAllocation';
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
import { useBoundActions } from '../../hooks/useBoundActions';
// State
@ -113,6 +115,10 @@ export const actions = {
setOutage,
};
export const useNetworkActions = (): BoundActionCreatorsMapObject<
typeof actions
> => useBoundActions(actions);
// Reducer
export function getEmptyState(): NetworkStateType {

View file

@ -15,6 +15,8 @@ import {
import * as log from '../../logging/log';
import * as Errors from '../../types/errors';
import type { StateType as RootStateType } from '../reducer';
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
import { useBoundActions } from '../../hooks/useBoundActions';
export type SafetyNumberContactType = ReadonlyDeep<{
safetyNumber: SafetyNumberType;
@ -174,6 +176,10 @@ export const actions = {
toggleVerified,
};
export const useSafetyNumberActions = (): BoundActionCreatorsMapObject<
typeof actions
> => useBoundActions(actions);
export function getEmptyState(): SafetyNumberStateType {
return {
contacts: {},

View file

@ -3,7 +3,6 @@
import type { ReadonlyDeep } from 'type-fest';
import { trigger } from '../../shims/events';
import type { LocaleMessagesType } from '../../types/I18N';
import type { LocalizerType } from '../../types/Util';
import type { MenuOptionsType } from '../../types/menu';
@ -11,6 +10,8 @@ import type { NoopActionType } from './noop';
import type { AciString, PniString } from '../../types/ServiceId';
import OS from '../../util/os/osMain';
import { ThemeType } from '../../types/Util';
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
import { useBoundActions } from '../../hooks/useBoundActions';
// State
@ -73,6 +74,10 @@ export const actions = {
manualReconnect,
};
export const useUserActions = (): BoundActionCreatorsMapObject<
typeof actions
> => useBoundActions(actions);
function eraseStorageServiceState(): EraseStorageServiceStateAction {
return {
type: ERASE_STORAGE_SERVICE,

View file

@ -9,12 +9,12 @@ import { Provider } from 'react-redux';
import type { Store } from 'redux';
import { ModalHost } from '../../components/ModalHost';
import type { PropsType } from '../smart/GroupV2JoinDialog';
import type { SmartGroupV2JoinDialogProps } from '../smart/GroupV2JoinDialog';
import { SmartGroupV2JoinDialog } from '../smart/GroupV2JoinDialog';
export const createGroupV2JoinModal = (
store: Store,
props: PropsType
props: SmartGroupV2JoinDialogProps
): React.ReactElement => {
const { onClose } = props;

12
ts/state/selectors/app.ts Normal file
View file

@ -0,0 +1,12 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { createSelector } from 'reselect';
import type { StateType } from '../reducer';
import type { AppStateType } from '../ducks/app';
export const getApp = (state: StateType): AppStateType => state.app;
export const getHasInitialLoadCompleted = createSelector(
getApp,
({ hasInitialLoadCompleted }) => hasInitialLoadCompleted
);

View file

@ -23,6 +23,36 @@ export type CallStateType = DirectCallStateType | GroupCallStateType;
const getCalling = (state: StateType): CallingStateType => state.calling;
export const getAvailableMicrophones = createSelector(
getCalling,
({ availableMicrophones }) => availableMicrophones
);
export const getSelectedMicrophone = createSelector(
getCalling,
({ selectedMicrophone }) => selectedMicrophone
);
export const getAvailableSpeakers = createSelector(
getCalling,
({ availableSpeakers }) => availableSpeakers
);
export const getSelectedSpeaker = createSelector(
getCalling,
({ selectedSpeaker }) => selectedSpeaker
);
export const getAvailableCameras = createSelector(
getCalling,
({ availableCameras }) => availableCameras
);
export const getSelectedCamera = createSelector(
getCalling,
({ selectedCamera }) => selectedCamera
);
export const getActiveCallState = createSelector(
getCalling,
(state: CallingStateType) => state.activeCallState

View file

@ -513,6 +513,13 @@ export const getComposerUUIDFetchState = createSelector(
}
);
export const getHasContactSpoofingReview = createSelector(
getConversations,
(state: ConversationsStateType): boolean => {
return state.hasContactSpoofingReview;
}
);
function isTrusted(conversation: ConversationType): boolean {
if (conversation.type === 'group') {
return true;
@ -986,10 +993,10 @@ export function _conversationMessagesSelector(
conversation: ConversationMessageType
): TimelinePropsType {
const {
isNearBottom,
isNearBottom = null,
messageChangeCounter,
messageIds,
messageLoadingState,
messageLoadingState = null,
metrics,
scrollToMessageCounter,
scrollToMessageId,
@ -1009,10 +1016,10 @@ export function _conversationMessagesSelector(
const oldestUnseenIndex = oldestUnseen
? messageIds.findIndex(id => id === oldestUnseen.id)
: undefined;
: null;
const scrollToIndex = scrollToMessageId
? messageIds.findIndex(id => id === scrollToMessageId)
: undefined;
: null;
const { totalUnseen } = metrics;
return {
@ -1025,9 +1032,9 @@ export function _conversationMessagesSelector(
oldestUnseenIndex:
isNumber(oldestUnseenIndex) && oldestUnseenIndex >= 0
? oldestUnseenIndex
: undefined,
: null,
scrollToIndex:
isNumber(scrollToIndex) && scrollToIndex >= 0 ? scrollToIndex : undefined,
isNumber(scrollToIndex) && scrollToIndex >= 0 ? scrollToIndex : null,
scrollToIndexCounter: scrollToMessageCounter,
totalUnseen,
};
@ -1065,6 +1072,9 @@ export const getConversationMessagesSelector = createSelector(
scrollToIndexCounter: 0,
totalUnseen: 0,
items: [],
isNearBottom: null,
oldestUnseenIndex: null,
scrollToIndex: null,
};
}

View file

@ -0,0 +1,18 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { createSelector } from 'reselect';
import type { StateType } from '../reducer';
import type { CrashReportsStateType } from '../ducks/crashReports';
const getCrashReports = (state: StateType): CrashReportsStateType =>
state.crashReports;
export const getCrashReportsIsPending = createSelector(
getCrashReports,
({ isPending }) => isPending
);
export const getCrashReportCount = createSelector(
getCrashReports,
({ count }) => count
);

View file

@ -21,3 +21,43 @@ export const isShowingAnyModal = createSelector(
return Boolean(value);
})
);
export const getContactModalState = createSelector(
getGlobalModalsState,
({ contactModalState }) => contactModalState
);
export const getIsStoriesSettingsVisible = createSelector(
getGlobalModalsState,
({ isStoriesSettingsVisible }) => isStoriesSettingsVisible
);
export const getSafetyNumberChangedBlockingData = createSelector(
getGlobalModalsState,
({ safetyNumberChangedBlockingData }) => safetyNumberChangedBlockingData
);
export const getDeleteMessagesProps = createSelector(
getGlobalModalsState,
({ deleteMessagesProps }) => deleteMessagesProps
);
export const getEditHistoryMessages = createSelector(
getGlobalModalsState,
({ editHistoryMessages }) => editHistoryMessages
);
export const getForwardMessagesProps = createSelector(
getGlobalModalsState,
({ forwardMessagesProps }) => forwardMessagesProps
);
export const getProfileEditorHasError = createSelector(
getGlobalModalsState,
({ profileEditorHasError }) => profileEditorHasError
);
export const getProfileEditorInitialEditState = createSelector(
getGlobalModalsState,
({ profileEditorInitialEditState }) => profileEditorInitialEditState
);

View file

@ -10,6 +10,21 @@ import { SocketStatus } from '../../types/SocketStatus';
const getNetwork = (state: StateType): NetworkStateType => state.network;
export const getNetworkIsOnline = createSelector(
getNetwork,
({ isOnline }) => isOnline
);
export const getNetworkIsOutage = createSelector(
getNetwork,
({ isOutage }) => isOutage
);
export const getNetworkSocketStatus = createSelector(
getNetwork,
({ socketStatus }) => socketStatus
);
export const hasNetworkDialog = createSelector(
getNetwork,
isDone,
@ -31,6 +46,11 @@ export const hasNetworkDialog = createSelector(
socketStatus === SocketStatus.CLOSING)
);
export const getChallengeStatus = createSelector(
getNetwork,
({ challengeStatus }) => challengeStatus
);
export const isChallengePending = createSelector(
getNetwork,
({ challengeStatus }) => challengeStatus === 'pending'

View file

@ -0,0 +1,11 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { createSelector } from 'reselect';
import type { StateType } from '../reducer';
import type { ToastStateType } from '../ducks/toast';
export function getToastState(state: StateType): ToastStateType {
return state.toast;
}
export const getToast = createSelector(getToastState, ({ toast }) => toast);

View file

@ -11,6 +11,26 @@ import type { UpdatesStateType } from '../ducks/updates';
export const getUpdatesState = (state: Readonly<StateType>): UpdatesStateType =>
state.updates;
export const getUpdateDialogType = createSelector(
getUpdatesState,
({ dialogType }) => dialogType
);
export const getUpdateVersion = createSelector(
getUpdatesState,
({ version }) => version
);
export const getUpdateDownloadSize = createSelector(
getUpdatesState,
({ downloadSize }) => downloadSize
);
export const getUpdateDownloadedSize = createSelector(
getUpdatesState,
({ downloadedSize }) => downloadedSize
);
export const isUpdateDialogVisible = createSelector(
getUpdatesState,
({ dialogType, didSnooze }) => {

View file

@ -1,9 +1,7 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import { AboutContactModal } from '../../components/conversation/AboutContactModal';
import { isSignalConnection } from '../../util/getSignalConnections';
import { getIntl } from '../selectors/user';
@ -12,7 +10,7 @@ import { getConversationSelector } from '../selectors/conversations';
import { useConversationsActions } from '../ducks/conversations';
import { useGlobalModalActions } from '../ducks/globalModals';
export function SmartAboutContactModal(): JSX.Element | null {
export const SmartAboutContactModal = memo(function SmartAboutContactModal() {
const i18n = useSelector(getIntl);
const globalModals = useSelector(getGlobalModalsState);
const { aboutContactModalContactId: contactId } = globalModals;
@ -44,4 +42,4 @@ export function SmartAboutContactModal(): JSX.Element | null {
onClose={toggleAboutContactModal}
/>
);
}
});

View file

@ -1,37 +1,47 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import { useSelector } from 'react-redux';
import React, { memo } from 'react';
import { AddUserToAnotherGroupModal } from '../../components/AddUserToAnotherGroupModal';
import type { StateType } from '../reducer';
import {
getAllGroupsWithInviteAccess,
getContactSelector,
} from '../selectors/conversations';
import { getIntl, getRegionCode, getTheme } from '../selectors/user';
import { getIntl, getRegionCode } from '../selectors/user';
import { useToastActions } from '../ducks/toast';
import { useGlobalModalActions } from '../ducks/globalModals';
import { useConversationsActions } from '../ducks/conversations';
export type Props = {
export type SmartAddUserToAnotherGroupModalProps = Readonly<{
contactID: string;
};
}>;
const mapStateToProps = (state: StateType, props: Props) => {
const candidateConversations = getAllGroupsWithInviteAccess(state);
const getContact = getContactSelector(state);
export const SmartAddUserToAnotherGroupModal = memo(
function SmartAddUserToAnotherGroupModal({
contactID,
}: SmartAddUserToAnotherGroupModalProps) {
const i18n = useSelector(getIntl);
const candidateConversations = useSelector(getAllGroupsWithInviteAccess);
const getContact = useSelector(getContactSelector);
const regionCode = useSelector(getRegionCode);
const regionCode = getRegionCode(state);
const { toggleAddUserToAnotherGroupModal } = useGlobalModalActions();
const { addMembersToGroup } = useConversationsActions();
const { showToast } = useToastActions();
return {
contact: getContact(props.contactID),
i18n: getIntl(state),
theme: getTheme(state),
candidateConversations,
regionCode,
};
};
const contact = getContact(contactID);
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartAddUserToAnotherGroupModal = smart(
AddUserToAnotherGroupModal
return (
<AddUserToAnotherGroupModal
contact={contact}
i18n={i18n}
candidateConversations={candidateConversations}
regionCode={regionCode}
toggleAddUserToAnotherGroupModal={toggleAddUserToAnotherGroupModal}
addMembersToGroup={addMembersToGroup}
showToast={showToast}
/>
);
}
);

View file

@ -1,21 +1,20 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import { MediaGallery } from '../../components/conversation/media-gallery/MediaGallery';
import { getMediaGalleryState } from '../selectors/mediaGallery';
import { useConversationsActions } from '../ducks/conversations';
import { useLightboxActions } from '../ducks/lightbox';
import { useMediaGalleryActions } from '../ducks/mediaGallery';
export type PropsType = {
conversationId: string;
};
export function SmartAllMedia({ conversationId }: PropsType): JSX.Element {
export const SmartAllMedia = memo(function SmartAllMedia({
conversationId,
}: PropsType) {
const { media, documents } = useSelector(getMediaGalleryState);
const { loadMediaItems } = useMediaGalleryActions();
const { saveAttachment } = useConversationsActions();
@ -32,4 +31,4 @@ export function SmartAllMedia({ conversationId }: PropsType): JSX.Element {
saveAttachment={saveAttachment}
/>
);
}
});

View file

@ -1,9 +1,7 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import type { VerificationTransport } from '../../types/VerificationTransport';
import { App } from '../../components/App';
import OS from '../../util/os/osMain';
@ -30,13 +28,11 @@ function renderInbox(): JSX.Element {
return <SmartInbox />;
}
export function SmartApp(): JSX.Element {
export const SmartApp = memo(function SmartApp() {
const app = useSelector((state: StateType) => state.app);
const { openInbox } = useAppActions();
const { scrollToMessage } = useConversationsActions();
const { viewStory } = useStoriesActions();
return (
@ -84,4 +80,4 @@ export function SmartApp(): JSX.Element {
viewStory={viewStory}
/>
);
}
});

View file

@ -1,23 +1,29 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { connect } from 'react-redux';
import { memoize } from 'lodash';
import { mapDispatchToProps } from '../actions';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import type {
DirectIncomingCall,
GroupIncomingCall,
} from '../../components/CallManager';
import { CallManager } from '../../components/CallManager';
import type { SafetyNumberProps } from '../../components/SafetyNumberChangeDialog';
import { isConversationTooBigToRing as getIsConversationTooBigToRing } from '../../conversations/isConversationTooBigToRing';
import * as log from '../../logging/log';
import { calling as callingService } from '../../services/calling';
import { getIntl, getTheme } from '../selectors/user';
import { getMe, getConversationSelector } from '../selectors/conversations';
import { getActiveCall } from '../ducks/calling';
import type { ConversationType } from '../ducks/conversations';
import { getCallLinkSelector, getIncomingCall } from '../selectors/calling';
import { isGroupCallRaiseHandEnabled } from '../../util/isGroupCallRaiseHandEnabled';
import { isGroupCallReactionsEnabled } from '../../util/isGroupCallReactionsEnabled';
import {
FALLBACK_NOTIFICATION_TITLE,
NotificationSetting,
NotificationType,
notificationService,
} from '../../services/notifications';
import {
bounceAppIconStart,
bounceAppIconStop,
} from '../../shims/bounceAppIcon';
import type { CallLinkType } from '../../types/CallLink';
import type {
ActiveCallBaseType,
ActiveCallType,
@ -27,32 +33,32 @@ import type {
ConversationsByDemuxIdType,
GroupCallRemoteParticipantType,
} from '../../types/Calling';
import type { AciString } from '../../types/ServiceId';
import { CallMode, CallState } from '../../types/Calling';
import type { CallLinkType } from '../../types/CallLink';
import type { StateType } from '../reducer';
import { missingCaseError } from '../../util/missingCaseError';
import { SmartCallingDeviceSelection } from './CallingDeviceSelection';
import type { SafetyNumberProps } from '../../components/SafetyNumberChangeDialog';
import { SmartSafetyNumberViewer } from './SafetyNumberViewer';
import { callingTones } from '../../util/callingTones';
import {
bounceAppIconStart,
bounceAppIconStop,
} from '../../shims/bounceAppIcon';
import {
FALLBACK_NOTIFICATION_TITLE,
NotificationSetting,
NotificationType,
notificationService,
} from '../../services/notifications';
import * as log from '../../logging/log';
import { getPreferredBadgeSelector } from '../selectors/badges';
import { isConversationTooBigToRing } from '../../conversations/isConversationTooBigToRing';
import type { AciString } from '../../types/ServiceId';
import { strictAssert } from '../../util/assert';
import { callLinkToConversation } from '../../util/callLinks';
import { callingTones } from '../../util/callingTones';
import { isGroupCallRaiseHandEnabled } from '../../util/isGroupCallRaiseHandEnabled';
import { isGroupCallReactionsEnabled } from '../../util/isGroupCallReactionsEnabled';
import { missingCaseError } from '../../util/missingCaseError';
import { useAudioPlayerActions } from '../ducks/audioPlayer';
import { getActiveCall, useCallingActions } from '../ducks/calling';
import type { ConversationType } from '../ducks/conversations';
import { useToastActions } from '../ducks/toast';
import type { StateType } from '../reducer';
import { getHasInitialLoadCompleted } from '../selectors/app';
import { getPreferredBadgeSelector } from '../selectors/badges';
import {
getAvailableCameras,
getCallLinkSelector,
getIncomingCall,
} from '../selectors/calling';
import { getConversationSelector, getMe } from '../selectors/conversations';
import { getIntl, getTheme } from '../selectors/user';
import { SmartCallingDeviceSelection } from './CallingDeviceSelection';
import { SmartSafetyNumberViewer } from './SafetyNumberViewer';
import { renderEmojiPicker } from './renderEmojiPicker';
import { renderReactionPicker } from './renderReactionPicker';
import { callLinkToConversation } from '../../util/callLinks';
function renderDeviceSelection(): JSX.Element {
return <SmartCallingDeviceSelection />;
@ -321,7 +327,7 @@ const mapStateToActiveCallProp = (
conversationsByDemuxId,
deviceCount: peekInfo.deviceCount,
groupMembers,
isConversationTooBigToRing: isConversationTooBigToRing(conversation),
isConversationTooBigToRing: getIsConversationTooBigToRing(conversation),
joinState: call.joinState,
localDemuxId,
maxDevices: peekInfo.maxDevices,
@ -414,37 +420,105 @@ const mapStateToIncomingCallProp = (
}
};
const mapStateToProps = (state: StateType) => {
const incomingCall = mapStateToIncomingCallProp(state);
export const SmartCallManager = memo(function SmartCallManager() {
const i18n = useSelector(getIntl);
const theme = useSelector(getTheme);
const activeCall = useSelector(mapStateToActiveCallProp);
const callLink = useSelector(mapStateToCallLinkProp);
const incomingCall = useSelector(mapStateToIncomingCallProp);
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const availableCameras = useSelector(getAvailableCameras);
const hasInitialLoadCompleted = useSelector(getHasInitialLoadCompleted);
const me = useSelector(getMe);
const isConversationTooBigToRing = incomingCall
? getIsConversationTooBigToRing(incomingCall.conversation)
: false;
return {
activeCall: mapStateToActiveCallProp(state),
callLink: mapStateToCallLinkProp(state),
bounceAppIconStart,
bounceAppIconStop,
availableCameras: state.calling.availableCameras,
getGroupCallVideoFrameSource,
getPreferredBadge: getPreferredBadgeSelector(state),
hasInitialLoadCompleted: state.app.hasInitialLoadCompleted,
i18n: getIntl(state),
isGroupCallRaiseHandEnabled: isGroupCallRaiseHandEnabled(),
isGroupCallReactionsEnabled: isGroupCallReactionsEnabled(),
incomingCall,
me: getMe(state),
notifyForCall,
playRingtone,
stopRingtone,
renderEmojiPicker,
renderReactionPicker,
renderDeviceSelection,
renderSafetyNumberViewer,
theme: getTheme(state),
isConversationTooBigToRing: incomingCall
? isConversationTooBigToRing(incomingCall.conversation)
: false,
};
};
const {
changeCallView,
closeNeedPermissionScreen,
getPresentingSources,
cancelCall,
keyChangeOk,
startCall,
toggleParticipants,
acceptCall,
declineCall,
openSystemPreferencesAction,
sendGroupCallRaiseHand,
sendGroupCallReaction,
setGroupCallVideoRequest,
setIsCallActive,
setLocalAudio,
setLocalVideo,
setLocalPreview,
setOutgoingRing,
setPresenting,
setRendererCanvas,
switchToPresentationView,
switchFromPresentationView,
hangUpActiveCall,
togglePip,
toggleScreenRecordingPermissionsDialog,
toggleSettings,
} = useCallingActions();
const { showToast } = useToastActions();
const { pauseVoiceNotePlayer } = useAudioPlayerActions();
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartCallManager = smart(CallManager);
return (
<CallManager
acceptCall={acceptCall}
activeCall={activeCall}
availableCameras={availableCameras}
bounceAppIconStart={bounceAppIconStart}
bounceAppIconStop={bounceAppIconStop}
callLink={callLink}
cancelCall={cancelCall}
changeCallView={changeCallView}
closeNeedPermissionScreen={closeNeedPermissionScreen}
declineCall={declineCall}
getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
getPreferredBadge={getPreferredBadge}
getPresentingSources={getPresentingSources}
hangUpActiveCall={hangUpActiveCall}
hasInitialLoadCompleted={hasInitialLoadCompleted}
i18n={i18n}
incomingCall={incomingCall}
isConversationTooBigToRing={isConversationTooBigToRing}
isGroupCallRaiseHandEnabled={isGroupCallRaiseHandEnabled()}
isGroupCallReactionsEnabled={isGroupCallReactionsEnabled()}
keyChangeOk={keyChangeOk}
me={me}
notifyForCall={notifyForCall}
openSystemPreferencesAction={openSystemPreferencesAction}
pauseVoiceNotePlayer={pauseVoiceNotePlayer}
playRingtone={playRingtone}
renderDeviceSelection={renderDeviceSelection}
renderEmojiPicker={renderEmojiPicker}
renderReactionPicker={renderReactionPicker}
renderSafetyNumberViewer={renderSafetyNumberViewer}
sendGroupCallRaiseHand={sendGroupCallRaiseHand}
sendGroupCallReaction={sendGroupCallReaction}
setGroupCallVideoRequest={setGroupCallVideoRequest}
setIsCallActive={setIsCallActive}
setLocalAudio={setLocalAudio}
setLocalPreview={setLocalPreview}
setLocalVideo={setLocalVideo}
setOutgoingRing={setOutgoingRing}
setPresenting={setPresenting}
setRendererCanvas={setRendererCanvas}
showToast={showToast}
startCall={startCall}
stopRingtone={stopRingtone}
switchFromPresentationView={switchFromPresentationView}
switchToPresentationView={switchToPresentationView}
theme={theme}
toggleParticipants={toggleParticipants}
togglePip={togglePip}
toggleScreenRecordingPermissionsDialog={
toggleScreenRecordingPermissionsDialog
}
toggleSettings={toggleSettings}
/>
);
});

View file

@ -1,34 +1,42 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import { CallingDeviceSelection } from '../../components/CallingDeviceSelection';
import type { StateType } from '../reducer';
import { getIntl } from '../selectors/user';
import {
getAvailableCameras,
getAvailableMicrophones,
getAvailableSpeakers,
getSelectedCamera,
getSelectedMicrophone,
getSelectedSpeaker,
} from '../selectors/calling';
import { useCallingActions } from '../ducks/calling';
const mapStateToProps = (state: StateType) => {
const {
availableMicrophones,
availableSpeakers,
selectedMicrophone,
selectedSpeaker,
availableCameras,
selectedCamera,
} = state.calling;
return {
availableCameras,
availableMicrophones,
availableSpeakers,
i18n: getIntl(state),
selectedCamera,
selectedMicrophone,
selectedSpeaker,
};
};
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartCallingDeviceSelection = smart(CallingDeviceSelection);
export const SmartCallingDeviceSelection = memo(
function SmartCallingDeviceSelection() {
const i18n = useSelector(getIntl);
const availableMicrophones = useSelector(getAvailableMicrophones);
const selectedMicrophone = useSelector(getSelectedMicrophone);
const availableSpeakers = useSelector(getAvailableSpeakers);
const selectedSpeaker = useSelector(getSelectedSpeaker);
const availableCameras = useSelector(getAvailableCameras);
const selectedCamera = useSelector(getSelectedCamera);
const { changeIODevice, toggleSettings } = useCallingActions();
return (
<CallingDeviceSelection
availableCameras={availableCameras}
availableMicrophones={availableMicrophones}
availableSpeakers={availableSpeakers}
changeIODevice={changeIODevice}
i18n={i18n}
selectedCamera={selectedCamera}
selectedMicrophone={selectedMicrophone}
selectedSpeaker={selectedSpeaker}
toggleSettings={toggleSettings}
/>
);
}
);

View file

@ -1,7 +1,6 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useCallback, useEffect } from 'react';
import React, { memo, useCallback, useEffect } from 'react';
import { useSelector } from 'react-redux';
import { useItemsActions } from '../ducks/items';
import {
@ -88,7 +87,7 @@ function renderToastManager(props: {
return <SmartToastManager disableMegaphone {...props} />;
}
export function SmartCallsTab(): JSX.Element {
export const SmartCallsTab = memo(function SmartCallsTab() {
const i18n = useSelector(getIntl);
const navTabsCollapsed = useSelector(getNavTabsCollapsed);
const preferredLeftPaneWidth = useSelector(getPreferredLeftPaneWidth);
@ -185,4 +184,4 @@ export function SmartCallsTab(): JSX.Element {
savePreferredLeftPaneWidth={savePreferredLeftPaneWidth}
/>
);
}
});

View file

@ -1,29 +1,33 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import React, { memo, useCallback } from 'react';
import { useSelector } from 'react-redux';
import { CaptchaDialog } from '../../components/CaptchaDialog';
import type { StateType } from '../reducer';
import { getIntl } from '../selectors/user';
import { isChallengePending } from '../selectors/network';
import { getChallengeURL } from '../../challenge';
import * as log from '../../logging/log';
const mapStateToProps = (state: StateType) => {
return {
...state.updates,
isPending: isChallengePending(state),
i18n: getIntl(state),
export type SmartCaptchaDialogProps = Readonly<{
onSkip: () => void;
}>;
onContinue() {
const url = getChallengeURL('chat');
log.info(`CaptchaDialog: navigating to ${url}`);
document.location.href = url;
},
};
};
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartCaptchaDialog = smart(CaptchaDialog);
export const SmartCaptchaDialog = memo(function SmartCaptchaDialog({
onSkip,
}: SmartCaptchaDialogProps) {
const i18n = useSelector(getIntl);
const isPending = useSelector(isChallengePending);
const handleContinue = useCallback(() => {
const url = getChallengeURL('chat');
log.info(`CaptchaDialog: navigating to ${url}`);
document.location.href = url;
}, []);
return (
<CaptchaDialog
i18n={i18n}
isPending={isPending}
onSkip={onSkip}
onContinue={handleContinue}
/>
);
});

View file

@ -1,53 +1,91 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import type { PropsDataType } from '../../components/ChatColorPicker';
import React, { memo, useCallback } from 'react';
import { useSelector } from 'react-redux';
import { ChatColorPicker } from '../../components/ChatColorPicker';
import type { StateType } from '../reducer';
import {
getConversationSelector,
getConversationsWithCustomColorSelector,
} from '../selectors/conversations';
import { getIntl } from '../selectors/user';
import { getDefaultConversationColor } from '../selectors/items';
import {
getCustomColors,
getDefaultConversationColor,
} from '../selectors/items';
import { getConversationColorAttributes } from '../../util/getConversationColorAttributes';
import {
useConversationsActions,
type ConversationType,
} from '../ducks/conversations';
import { useItemsActions } from '../ducks/items';
export type SmartChatColorPickerProps = {
export type SmartChatColorPickerProps = Readonly<{
conversationId?: string;
};
}>;
const mapStateToProps = (
state: StateType,
props: SmartChatColorPickerProps
): PropsDataType => {
const conversation = props.conversationId
? getConversationSelector(state)(props.conversationId)
export const SmartChatColorPicker = memo(function SmartChatColorPicker({
conversationId,
}: SmartChatColorPickerProps) {
const i18n = useSelector(getIntl);
const customColors = useSelector(getCustomColors) ?? {};
const defaultConversationColor = useSelector(getDefaultConversationColor);
const conversationSelector = useSelector(getConversationSelector);
const conversationWithCustomColorSelector = useSelector(
getConversationsWithCustomColorSelector
);
const {
addCustomColor,
removeCustomColor,
setGlobalDefaultConversationColor,
resetDefaultChatColor,
editCustomColor,
} = useItemsActions();
const {
colorSelected,
resetAllChatColors,
removeCustomColorOnConversations,
} = useConversationsActions();
const conversation = conversationId
? conversationSelector(conversationId)
: {};
const defaultConversationColor = getDefaultConversationColor(state);
const colorValues = getConversationColorAttributes(
conversation,
defaultConversationColor
);
const { customColors } = state.items;
return {
...props,
customColors: customColors ? customColors.colors : {},
getConversationsWithCustomColor: (colorId: string) =>
Promise.resolve(getConversationsWithCustomColorSelector(state)(colorId)),
i18n: getIntl(state),
selectedColor: colorValues.conversationColor,
selectedCustomColor: {
id: colorValues.customColorId,
value: colorValues.customColor,
},
const selectedColor = colorValues.conversationColor;
const selectedCustomColor = {
id: colorValues.customColorId,
value: colorValues.customColor,
};
};
const smart = connect(mapStateToProps, mapDispatchToProps);
const getConversationsWithCustomColor = useCallback(
async (colorId: string): Promise<Array<ConversationType>> => {
return conversationWithCustomColorSelector(colorId);
},
[conversationWithCustomColorSelector]
);
export const SmartChatColorPicker = smart(ChatColorPicker);
return (
<ChatColorPicker
addCustomColor={addCustomColor}
colorSelected={colorSelected}
conversationId={conversationId}
customColors={customColors}
editCustomColor={editCustomColor}
getConversationsWithCustomColor={getConversationsWithCustomColor}
i18n={i18n}
isGlobal={false}
removeCustomColor={removeCustomColor}
removeCustomColorOnConversations={removeCustomColorOnConversations}
resetAllChatColors={resetAllChatColors}
resetDefaultChatColor={resetDefaultChatColor}
selectedColor={selectedColor}
selectedCustomColor={selectedCustomColor}
setGlobalDefaultConversationColor={setGlobalDefaultConversationColor}
/>
);
});

View file

@ -1,7 +1,6 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useEffect, useRef } from 'react';
import React, { memo, useEffect, useRef } from 'react';
import { useSelector } from 'react-redux';
import { ChatsTab } from '../../components/ChatsTab';
import { SmartConversationView } from './ConversationView';
@ -12,7 +11,6 @@ import { useGlobalModalActions } from '../ducks/globalModals';
import { getIntl } from '../selectors/user';
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';
@ -36,7 +34,7 @@ function renderMiniPlayer(options: { shouldFlow: boolean }) {
return <SmartMiniPlayer {...options} />;
}
export function SmartChatsTab(): JSX.Element {
export const SmartChatsTab = memo(function SmartChatsTab() {
const i18n = useSelector(getIntl);
const navTabsCollapsed = useSelector(getNavTabsCollapsed);
const hasFailedStorySends = useSelector(getHasAnyFailedStorySends);
@ -44,9 +42,7 @@ export function SmartChatsTab(): JSX.Element {
const otherTabsUnreadStats = useSelector(getOtherTabsUnreadStats);
const { selectedConversationId, targetedMessage, targetedMessageSource } =
useSelector<StateType, ConversationsStateType>(
state => state.conversations
);
useSelector((state: StateType) => state.conversations);
const {
onConversationClosed,
@ -73,13 +69,7 @@ export function SmartChatsTab(): JSX.Element {
) {
scrollToMessage(selectedConversationId, targetedMessage);
}
}, [
onConversationOpened,
selectedConversationId,
scrollToMessage,
targetedMessage,
targetedMessageSource,
]);
}, [onConversationOpened, selectedConversationId, scrollToMessage, targetedMessage, targetedMessageSource]);
const prevConversationId = usePrevious(
selectedConversationId,
@ -157,4 +147,4 @@ export function SmartChatsTab(): JSX.Element {
showWhatsNewModal={showWhatsNewModal}
/>
);
}
});

View file

@ -1,26 +1,20 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
import type { StateType } from '../reducer';
import { mapDispatchToProps } from '../actions';
import React, { memo, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { strictAssert } from '../../util/assert';
import { lookupConversationWithoutServiceId } from '../../util/lookupConversationWithoutServiceId';
import { getUsernameFromSearch } from '../../util/Username';
import type { StatePropsType } from '../../components/conversation/conversation-details/AddGroupMembersModal/ChooseGroupMembersModal';
import { ChooseGroupMembersModal } from '../../components/conversation/conversation-details/AddGroupMembersModal/ChooseGroupMembersModal';
import { getIntl, getTheme, getRegionCode } from '../selectors/user';
import {
getCandidateContactsForNewGroup,
getConversationByIdSelector,
getMe,
} from '../selectors/conversations';
import { getPreferredBadgeSelector } from '../selectors/badges';
import { useGlobalModalActions } from '../ducks/globalModals';
export type SmartChooseGroupMembersModalPropsType = {
export type SmartChooseGroupMembersModalPropsType = Readonly<{
conversationIdsAlreadyInGroup: Set<string>;
maxGroupSize: number;
confirmAdds: () => void;
@ -30,41 +24,63 @@ export type SmartChooseGroupMembersModalPropsType = {
selectedConversationIds: ReadonlyArray<string>;
setSearchTerm: (_: string) => void;
toggleSelectedContact: (conversationId: string) => void;
};
}>;
const mapStateToProps = (
state: StateType,
props: SmartChooseGroupMembersModalPropsType
): StatePropsType => {
const conversationSelector = getConversationByIdSelector(state);
export const SmartChooseGroupMembersModal = memo(
function SmartChooseGroupMembersModal({
conversationIdsAlreadyInGroup,
maxGroupSize,
confirmAdds,
onClose,
removeSelectedContact,
searchTerm,
selectedConversationIds,
setSearchTerm,
toggleSelectedContact,
}: SmartChooseGroupMembersModalPropsType) {
const i18n = useSelector(getIntl);
const theme = useSelector(getTheme);
const regionCode = useSelector(getRegionCode);
const me = useSelector(getMe);
const conversationSelector = useSelector(getConversationByIdSelector);
const candidateContacts = getCandidateContactsForNewGroup(state);
const selectedContacts = props.selectedConversationIds.map(conversationId => {
const convo = conversationSelector(conversationId);
strictAssert(
convo,
'<SmartChooseGroupMemberModal> selected conversation not found'
const candidateContacts = useSelector(getCandidateContactsForNewGroup);
const selectedContacts = selectedConversationIds.map(conversationId => {
const convo = conversationSelector(conversationId);
strictAssert(
convo,
'<SmartChooseGroupMemberModal> selected conversation not found'
);
return convo;
});
const { showUserNotFoundModal } = useGlobalModalActions();
const username = useMemo(() => {
return getUsernameFromSearch(searchTerm);
}, [searchTerm]);
return (
<ChooseGroupMembersModal
regionCode={regionCode}
candidateContacts={candidateContacts}
confirmAdds={confirmAdds}
conversationIdsAlreadyInGroup={conversationIdsAlreadyInGroup}
i18n={i18n}
maxGroupSize={maxGroupSize}
onClose={onClose}
ourE164={me.e164}
ourUsername={me.username}
removeSelectedContact={removeSelectedContact}
searchTerm={searchTerm}
selectedContacts={selectedContacts}
setSearchTerm={setSearchTerm}
theme={theme}
toggleSelectedContact={toggleSelectedContact}
lookupConversationWithoutServiceId={lookupConversationWithoutServiceId}
showUserNotFoundModal={showUserNotFoundModal}
username={username}
/>
);
return convo;
});
const { searchTerm } = props;
return {
...props,
regionCode: getRegionCode(state),
candidateContacts,
getPreferredBadge: getPreferredBadgeSelector(state),
i18n: getIntl(state),
theme: getTheme(state),
ourE164: getMe(state).e164,
ourUsername: getMe(state).username,
selectedContacts,
lookupConversationWithoutServiceId,
username: getUsernameFromSearch(searchTerm),
};
};
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartChooseGroupMembersModal = smart(ChooseGroupMembersModal);
}
);

View file

@ -1,9 +1,7 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useMemo } from 'react';
import React, { memo, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { CollidingAvatars } from '../../components/CollidingAvatars';
import { getIntl } from '../selectors/user';
import { getConversationSelector } from '../selectors/conversations';
@ -12,9 +10,9 @@ export type PropsType = Readonly<{
conversationIds: ReadonlyArray<string>;
}>;
export function SmartCollidingAvatars({
export const SmartCollidingAvatars = memo(function SmartCollidingAvatars({
conversationIds,
}: PropsType): JSX.Element {
}: PropsType) {
const i18n = useSelector(getIntl);
const getConversation = useSelector(getConversationSelector);
@ -25,4 +23,4 @@ export function SmartCollidingAvatars({
}, [conversationIds, getConversation]);
return <CollidingAvatars i18n={i18n} conversations={conversations} />;
}
});

View file

@ -1,7 +1,7 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useCallback, useMemo } from 'react';
import React, { useCallback, useMemo, memo } from 'react';
import { useSelector } from 'react-redux';
import { CompositionArea } from '../../components/CompositionArea';
import { useContactNameData } from '../../components/conversation/ContactName';
@ -78,7 +78,11 @@ function renderSmartCompositionRecordingDraft(
return <SmartCompositionRecordingDraft {...draftProps} />;
}
export function SmartCompositionArea({ id }: { id: string }): JSX.Element {
export const SmartCompositionArea = memo(function SmartCompositionArea({
id,
}: {
id: string;
}) {
const conversationSelector = useSelector(getConversationSelector);
const conversation = conversationSelector(id);
strictAssert(conversation, `Conversation id ${id} not found!`);
@ -346,4 +350,4 @@ export function SmartCompositionArea({ id }: { id: string }): JSX.Element {
showConversation={showConversation}
/>
);
}
});

View file

@ -1,7 +1,7 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useCallback } from 'react';
import React, { memo, useCallback } from 'react';
import { useSelector } from 'react-redux';
import { CompositionRecording } from '../../components/CompositionRecording';
import { mapDispatchToProps } from '../actions';
@ -15,49 +15,53 @@ export type SmartCompositionRecordingProps = {
onBeforeSend: () => void;
};
export function SmartCompositionRecording({
onBeforeSend,
}: SmartCompositionRecordingProps): JSX.Element | null {
const i18n = useSelector(getIntl);
const selectedConversationId = useSelector(getSelectedConversationId);
const { cancelRecording, completeRecording } = useAudioRecorderActions();
const { sendMultiMediaMessage } = useComposerActions();
const { hideToast, showToast } = useToastActions();
const handleCancel = useCallback(() => {
cancelRecording();
}, [cancelRecording]);
const handleSend = useCallback(() => {
if (selectedConversationId) {
completeRecording(selectedConversationId, voiceNoteAttachment => {
onBeforeSend();
sendMultiMediaMessage(selectedConversationId, { voiceNoteAttachment });
});
}
}, [
selectedConversationId,
completeRecording,
export const SmartCompositionRecording = memo(
function SmartCompositionRecording({
onBeforeSend,
sendMultiMediaMessage,
]);
}: SmartCompositionRecordingProps) {
const i18n = useSelector(getIntl);
const selectedConversationId = useSelector(getSelectedConversationId);
const { cancelRecording, completeRecording } = useAudioRecorderActions();
if (!selectedConversationId) {
return null;
const { sendMultiMediaMessage } = useComposerActions();
const { hideToast, showToast } = useToastActions();
const handleCancel = useCallback(() => {
cancelRecording();
}, [cancelRecording]);
const handleSend = useCallback(() => {
if (selectedConversationId) {
completeRecording(selectedConversationId, voiceNoteAttachment => {
onBeforeSend();
sendMultiMediaMessage(selectedConversationId, {
voiceNoteAttachment,
});
});
}
}, [
selectedConversationId,
completeRecording,
onBeforeSend,
sendMultiMediaMessage,
]);
if (!selectedConversationId) {
return null;
}
return (
<CompositionRecording
i18n={i18n}
conversationId={selectedConversationId}
onCancel={handleCancel}
onSend={handleSend}
errorRecording={mapDispatchToProps.errorRecording}
addAttachment={mapDispatchToProps.addAttachment}
completeRecording={mapDispatchToProps.completeRecording}
showToast={showToast}
hideToast={hideToast}
/>
);
}
return (
<CompositionRecording
i18n={i18n}
conversationId={selectedConversationId}
onCancel={handleCancel}
onSend={handleSend}
errorRecording={mapDispatchToProps.errorRecording}
addAttachment={mapDispatchToProps.addAttachment}
completeRecording={mapDispatchToProps.completeRecording}
showToast={showToast}
hideToast={hideToast}
/>
);
}
);

View file

@ -1,7 +1,7 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useCallback } from 'react';
import React, { memo, useCallback } from 'react';
import { useSelector } from 'react-redux';
import { CompositionRecordingDraft } from '../../components/CompositionRecordingDraft';
import type { AttachmentDraftType } from '../../types/Attachment';
@ -21,136 +21,138 @@ export type SmartCompositionRecordingDraftProps = {
voiceNoteAttachment: AttachmentDraftType;
};
export function SmartCompositionRecordingDraft({
voiceNoteAttachment,
}: SmartCompositionRecordingDraftProps): JSX.Element {
const i18n = useSelector(getIntl);
const active = useSelector(selectAudioPlayerActive);
const selectedConversationId = useSelector(getSelectedConversationId);
const getConversationById = useSelector(getConversationByIdSelector);
const {
loadVoiceNoteDraftAudio,
unloadMessageAudio,
setIsPlaying,
setPosition,
} = useAudioPlayerActions();
const { sendMultiMediaMessage, removeAttachment } = useComposerActions();
export const SmartCompositionRecordingDraft = memo(
function SmartCompositionRecordingDraft({
voiceNoteAttachment,
}: SmartCompositionRecordingDraftProps) {
const i18n = useSelector(getIntl);
const active = useSelector(selectAudioPlayerActive);
const selectedConversationId = useSelector(getSelectedConversationId);
const getConversationById = useSelector(getConversationByIdSelector);
const {
loadVoiceNoteDraftAudio,
unloadMessageAudio,
setIsPlaying,
setPosition,
} = useAudioPlayerActions();
const { sendMultiMediaMessage, removeAttachment } = useComposerActions();
if (!selectedConversationId) {
throw new Error('No selected conversation');
}
if (!selectedConversationId) {
throw new Error('No selected conversation');
}
const playbackRate =
getConversationById(selectedConversationId)?.voiceNotePlaybackRate ?? 1;
const playbackRate =
getConversationById(selectedConversationId)?.voiceNotePlaybackRate ?? 1;
const audioUrl = !voiceNoteAttachment.pending
? voiceNoteAttachment.url
: undefined;
const content = active?.content;
const draftActive =
content && AudioPlayerContent.isDraft(content) && content.url === audioUrl
? active
const audioUrl = !voiceNoteAttachment.pending
? voiceNoteAttachment.url
: undefined;
const handlePlay = useCallback(
(positionAsRatio?: number) => {
if (!draftActive && audioUrl) {
loadVoiceNoteDraftAudio({
conversationId: selectedConversationId,
url: audioUrl,
startPosition: positionAsRatio ?? 0,
playbackRate,
const content = active?.content;
const draftActive =
content && AudioPlayerContent.isDraft(content) && content.url === audioUrl
? active
: undefined;
const handlePlay = useCallback(
(positionAsRatio?: number) => {
if (!draftActive && audioUrl) {
loadVoiceNoteDraftAudio({
conversationId: selectedConversationId,
url: audioUrl,
startPosition: positionAsRatio ?? 0,
playbackRate,
});
}
if (draftActive) {
if (positionAsRatio !== undefined) {
setPosition(positionAsRatio);
}
if (!draftActive.playing) {
setIsPlaying(true);
}
}
},
[
draftActive,
audioUrl,
loadVoiceNoteDraftAudio,
selectedConversationId,
playbackRate,
setPosition,
setIsPlaying,
]
);
const handlePause = useCallback(() => {
setIsPlaying(false);
}, [setIsPlaying]);
const handleSend = useCallback(() => {
if (selectedConversationId) {
sendMultiMediaMessage(selectedConversationId, {
draftAttachments: [voiceNoteAttachment],
});
}
if (draftActive) {
if (positionAsRatio !== undefined) {
}, [selectedConversationId, sendMultiMediaMessage, voiceNoteAttachment]);
const handleCancel = useCallback(() => {
unloadMessageAudio();
if (selectedConversationId && voiceNoteAttachment.path) {
removeAttachment(selectedConversationId, voiceNoteAttachment.path);
}
}, [
removeAttachment,
selectedConversationId,
unloadMessageAudio,
voiceNoteAttachment.path,
]);
const handleScrub = useCallback(
(positionAsRatio: number) => {
// if scrubbing when audio not loaded
if (!draftActive && audioUrl) {
loadVoiceNoteDraftAudio({
conversationId: selectedConversationId,
url: audioUrl,
startPosition: positionAsRatio,
playbackRate,
});
return;
}
// if scrubbing when audio is loaded
if (draftActive) {
setPosition(positionAsRatio);
if (draftActive?.playing) {
setIsPlaying(true);
}
}
if (!draftActive.playing) {
setIsPlaying(true);
}
}
},
[
draftActive,
audioUrl,
loadVoiceNoteDraftAudio,
selectedConversationId,
playbackRate,
setPosition,
setIsPlaying,
]
);
},
[
audioUrl,
draftActive,
loadVoiceNoteDraftAudio,
playbackRate,
selectedConversationId,
setIsPlaying,
setPosition,
]
);
const handlePause = useCallback(() => {
setIsPlaying(false);
}, [setIsPlaying]);
const handleSend = useCallback(() => {
if (selectedConversationId) {
sendMultiMediaMessage(selectedConversationId, {
draftAttachments: [voiceNoteAttachment],
});
}
}, [selectedConversationId, sendMultiMediaMessage, voiceNoteAttachment]);
const handleCancel = useCallback(() => {
unloadMessageAudio();
if (selectedConversationId && voiceNoteAttachment.path) {
removeAttachment(selectedConversationId, voiceNoteAttachment.path);
}
}, [
removeAttachment,
selectedConversationId,
unloadMessageAudio,
voiceNoteAttachment.path,
]);
const handleScrub = useCallback(
(positionAsRatio: number) => {
// if scrubbing when audio not loaded
if (!draftActive && audioUrl) {
loadVoiceNoteDraftAudio({
conversationId: selectedConversationId,
url: audioUrl,
startPosition: positionAsRatio,
playbackRate,
});
return;
}
// if scrubbing when audio is loaded
if (draftActive) {
setPosition(positionAsRatio);
if (draftActive?.playing) {
setIsPlaying(true);
}
}
},
[
audioUrl,
draftActive,
loadVoiceNoteDraftAudio,
playbackRate,
selectedConversationId,
setIsPlaying,
setPosition,
]
);
return (
<CompositionRecordingDraft
i18n={i18n}
audioUrl={audioUrl}
active={draftActive}
onCancel={handleCancel}
onSend={handleSend}
onPlay={handlePlay}
onPause={handlePause}
onScrub={handleScrub}
/>
);
}
return (
<CompositionRecordingDraft
i18n={i18n}
audioUrl={audioUrl}
active={draftActive}
onCancel={handleCancel}
onSend={handleSend}
onPlay={handlePlay}
onPause={handlePause}
onScrub={handleScrub}
/>
);
}
);

View file

@ -1,7 +1,6 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import type { CompositionTextAreaProps } from '../../components/CompositionTextArea';
import { CompositionTextArea } from '../../components/CompositionTextArea';
@ -26,9 +25,9 @@ export type SmartCompositionTextAreaProps = Pick<
| 'scrollerRef'
>;
export function SmartCompositionTextArea(
export const SmartCompositionTextArea = memo(function SmartCompositionTextArea(
props: SmartCompositionTextAreaProps
): JSX.Element {
) {
const i18n = useSelector(getIntl);
const platform = useSelector(getPlatform);
@ -51,4 +50,4 @@ export function SmartCompositionTextArea(
platform={platform}
/>
);
}
});

View file

@ -1,16 +1,10 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
import type { StateType } from '../reducer';
import { mapDispatchToProps } from '../actions';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import { strictAssert } from '../../util/assert';
import type { StatePropsType } from '../../components/conversation/conversation-details/AddGroupMembersModal/ConfirmAdditionsModal';
import { ConfirmAdditionsModal } from '../../components/conversation/conversation-details/AddGroupMembersModal/ConfirmAdditionsModal';
import type { RequestState } from '../../components/conversation/conversation-details/util';
import { getIntl } from '../selectors/user';
import { getConversationByIdSelector } from '../selectors/conversations';
@ -22,28 +16,35 @@ export type SmartConfirmAdditionsModalPropsType = {
requestState: RequestState;
};
const mapStateToProps = (
state: StateType,
props: SmartConfirmAdditionsModalPropsType
): StatePropsType => {
const conversationSelector = getConversationByIdSelector(state);
export const SmartConfirmAdditionsModal = memo(
function SmartConfirmAdditionsModal({
selectedConversationIds,
groupTitle,
makeRequest,
onClose,
requestState,
}: SmartConfirmAdditionsModalPropsType) {
const i18n = useSelector(getIntl);
const conversationSelector = useSelector(getConversationByIdSelector);
const selectedContacts = props.selectedConversationIds.map(conversationId => {
const convo = conversationSelector(conversationId);
strictAssert(
convo,
'<SmartChooseGroupMemberModal> selected conversation not found'
const selectedContacts = selectedConversationIds.map(conversationId => {
const convo = conversationSelector(conversationId);
strictAssert(
convo,
'<SmartChooseGroupMemberModal> selected conversation not found'
);
return convo;
});
return (
<ConfirmAdditionsModal
i18n={i18n}
selectedContacts={selectedContacts}
groupTitle={groupTitle}
makeRequest={makeRequest}
onClose={onClose}
requestState={requestState}
/>
);
return convo;
});
return {
...props,
selectedContacts,
i18n: getIntl(state),
};
};
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartConfirmAdditionsModal = smart(ConfirmAdditionsModal);
}
);

View file

@ -1,59 +1,92 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import type { PropsDataType } from '../../components/conversation/ContactModal';
import React, { memo, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { ContactModal } from '../../components/conversation/ContactModal';
import type { StateType } from '../reducer';
import { getAreWeASubscriber } from '../selectors/items';
import { getIntl, getTheme } from '../selectors/user';
import { getBadgesSelector } from '../selectors/badges';
import { getConversationSelector } from '../selectors/conversations';
import { getHasStoriesSelector } from '../selectors/stories2';
import { getActiveCallState } from '../selectors/calling';
import { useStoriesActions } from '../ducks/stories';
import { useConversationsActions } from '../ducks/conversations';
import { useGlobalModalActions } from '../ducks/globalModals';
import { useCallingActions } from '../ducks/calling';
import { getContactModalState } from '../selectors/globalModals';
const mapStateToProps = (state: StateType): PropsDataType => {
const { contactId, conversationId } =
state.globalModals.contactModalState || {};
export const SmartContactModal = memo(function SmartContactModal() {
const i18n = useSelector(getIntl);
const theme = useSelector(getTheme);
const { conversationId, contactId } = useSelector(getContactModalState) ?? {};
const conversationSelector = useSelector(getConversationSelector);
const hasStoriesSelector = useSelector(getHasStoriesSelector);
const activeCallState = useSelector(getActiveCallState);
const badgesSelector = useSelector(getBadgesSelector);
const areWeASubscriber = useSelector(getAreWeASubscriber);
const currentConversation = getConversationSelector(state)(conversationId);
const contact = getConversationSelector(state)(contactId);
const conversation = conversationSelector(conversationId);
const contact = conversationSelector(contactId);
const hasStories = hasStoriesSelector(contactId);
const hasActiveCall = activeCallState != null;
const badges = badgesSelector(contact.badges);
const areWeAdmin =
currentConversation && currentConversation.areWeAdmin
? currentConversation.areWeAdmin
: false;
const areWeAdmin = conversation?.areWeAdmin ?? false;
let isMember = false;
let isAdmin = false;
if (contact && currentConversation && currentConversation.memberships) {
currentConversation.memberships.forEach(membership => {
if (membership.aci === contact.serviceId) {
isMember = true;
isAdmin = membership.isAdmin;
}
const ourMembership = useMemo(() => {
return conversation?.memberships?.find(membership => {
return membership.aci === contact.serviceId;
});
}
}, [conversation?.memberships, contact]);
const hasStories = getHasStoriesSelector(state)(contactId);
const isMember = ourMembership != null;
const isAdmin = ourMembership?.isAdmin ?? false;
return {
areWeASubscriber: getAreWeASubscriber(state),
areWeAdmin,
badges: getBadgesSelector(state)(contact.badges),
hasActiveCall: Boolean(getActiveCallState(state)),
contact,
conversation: currentConversation,
hasStories,
i18n: getIntl(state),
isAdmin,
isMember,
theme: getTheme(state),
};
};
const {
removeMemberFromGroup,
showConversation,
updateConversationModelSharedGroups,
toggleAdmin,
blockConversation,
} = useConversationsActions();
const { viewUserStories } = useStoriesActions();
const {
toggleAboutContactModal,
toggleAddUserToAnotherGroupModal,
toggleSafetyNumberModal,
hideContactModal,
} = useGlobalModalActions();
const {
onOutgoingVideoCallInConversation,
onOutgoingAudioCallInConversation,
} = useCallingActions();
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartContactModal = smart(ContactModal);
return (
<ContactModal
areWeAdmin={areWeAdmin}
areWeASubscriber={areWeASubscriber}
badges={badges}
blockConversation={blockConversation}
contact={contact}
conversation={conversation}
hasActiveCall={hasActiveCall}
hasStories={hasStories}
hideContactModal={hideContactModal}
i18n={i18n}
isAdmin={isAdmin}
isMember={isMember}
onOutgoingAudioCallInConversation={onOutgoingAudioCallInConversation}
onOutgoingVideoCallInConversation={onOutgoingVideoCallInConversation}
removeMemberFromGroup={removeMemberFromGroup}
showConversation={showConversation}
theme={theme}
toggleAboutContactModal={toggleAboutContactModal}
toggleAddUserToAnotherGroupModal={toggleAddUserToAnotherGroupModal}
toggleAdmin={toggleAdmin}
toggleSafetyNumberModal={toggleSafetyNumberModal}
updateConversationModelSharedGroups={updateConversationModelSharedGroups}
viewUserStories={viewUserStories}
/>
);
});

View file

@ -1,32 +1,25 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import type { StateType } from '../reducer';
import { ContactName } from '../../components/conversation/ContactName';
import { getIntl } from '../selectors/user';
import type { GetConversationByIdType } from '../selectors/conversations';
import {
getConversationSelector,
getSelectedConversationId,
} from '../selectors/conversations';
import type { LocalizerType } from '../../types/Util';
import { useGlobalModalActions } from '../ducks/globalModals';
type ExternalProps = {
contactId: string;
};
export function SmartContactName(props: ExternalProps): JSX.Element {
export const SmartContactName = memo(function SmartContactName(
props: ExternalProps
) {
const { contactId } = props;
const i18n = useSelector<StateType, LocalizerType>(getIntl);
const getConversation = useSelector<StateType, GetConversationByIdType>(
getConversationSelector
);
const i18n = useSelector(getIntl);
const getConversation = useSelector(getConversationSelector);
const contact = getConversation(contactId) || {
title: i18n('icu:unknownContact'),
@ -43,4 +36,4 @@ export function SmartContactName(props: ExternalProps): JSX.Element {
onClick={() => showContactModal(contact.id, currentConversation.id)}
/>
);
}
});

View file

@ -1,15 +1,12 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useCallback } from 'react';
import React, { memo, useCallback } from 'react';
import { useSelector } from 'react-redux';
import { mapValues } from 'lodash';
import type { StateType } from '../reducer';
import { ContactSpoofingReviewDialog } from '../../components/conversation/ContactSpoofingReviewDialog';
import { useConversationsActions } from '../ducks/conversations';
import type { GetConversationByIdType } from '../selectors/conversations';
import {
getConversationSelector,
getConversationByServiceIdSelector,
@ -33,116 +30,114 @@ export type PropsType = Readonly<{
onClose: () => void;
}>;
export function SmartContactSpoofingReviewDialog(
props: PropsType
): JSX.Element | null {
const { conversationId } = props;
export const SmartContactSpoofingReviewDialog = memo(
function SmartContactSpoofingReviewDialog(props: PropsType) {
const { conversationId } = props;
const getConversation = useSelector<StateType, GetConversationByIdType>(
getConversationSelector
);
const getConversation = useSelector(getConversationSelector);
const {
acceptConversation,
reportSpam,
blockAndReportSpam,
blockConversation,
deleteConversation,
removeMember,
updateSharedGroups,
} = useConversationsActions();
const { showContactModal, toggleSignalConnectionsModal } =
useGlobalModalActions();
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const i18n = useSelector(getIntl);
const theme = useSelector(getTheme);
const getConversationByServiceId = useSelector(
getConversationByServiceIdSelector
);
const conversation = getConversation(conversationId);
// Just binding the options argument
const safeConversationSelector = useCallback(
(state: StateType) => {
return getSafeConversationWithSameTitle(state, {
possiblyUnsafeConversation: conversation,
});
},
[conversation]
);
const safeConvo = useSelector(safeConversationSelector);
const sharedProps = {
...props,
acceptConversation,
reportSpam,
blockAndReportSpam,
blockConversation,
deleteConversation,
getPreferredBadge,
i18n,
removeMember,
updateSharedGroups,
showContactModal,
toggleSignalConnectionsModal,
theme,
};
if (conversation.type === 'group') {
const { memberships } = getGroupMemberships(
conversation,
getConversationByServiceId
const {
acceptConversation,
reportSpam,
blockAndReportSpam,
blockConversation,
deleteConversation,
removeMember,
updateSharedGroups,
} = useConversationsActions();
const { showContactModal, toggleSignalConnectionsModal } =
useGlobalModalActions();
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const i18n = useSelector(getIntl);
const theme = useSelector(getTheme);
const getConversationByServiceId = useSelector(
getConversationByServiceIdSelector
);
const groupNameCollisions = getCollisionsFromMemberships(memberships);
const conversation = getConversation(conversationId);
const previouslyAcknowledgedTitlesById = invertIdsByTitle(
conversation.acknowledgedGroupNameCollisions
// Just binding the options argument
const safeConversationSelector = useCallback(
(state: StateType) => {
return getSafeConversationWithSameTitle(state, {
possiblyUnsafeConversation: conversation,
});
},
[conversation]
);
const safeConvo = useSelector(safeConversationSelector);
const sharedProps = {
...props,
acceptConversation,
reportSpam,
blockAndReportSpam,
blockConversation,
deleteConversation,
getPreferredBadge,
i18n,
removeMember,
updateSharedGroups,
showContactModal,
toggleSignalConnectionsModal,
theme,
};
if (conversation.type === 'group') {
const { memberships } = getGroupMemberships(
conversation,
getConversationByServiceId
);
const groupNameCollisions = getCollisionsFromMemberships(memberships);
const previouslyAcknowledgedTitlesById = invertIdsByTitle(
conversation.acknowledgedGroupNameCollisions
);
const collisionInfoByTitle = mapValues(groupNameCollisions, collisions =>
collisions.map(collision => ({
conversation: collision,
isSignalConnection: isSignalConnection(collision),
oldName: getOwn(previouslyAcknowledgedTitlesById, collision.id),
}))
);
return (
<ContactSpoofingReviewDialog
{...sharedProps}
type={ContactSpoofingType.MultipleGroupMembersWithSameTitle}
group={conversation}
collisionInfoByTitle={collisionInfoByTitle}
/>
);
}
const possiblyUnsafeConvo = conversation;
assertDev(
possiblyUnsafeConvo.type === 'direct',
'DirectConversationWithSameTitle: expects possibly unsafe direct ' +
'conversation'
);
const collisionInfoByTitle = mapValues(groupNameCollisions, collisions =>
collisions.map(collision => ({
conversation: collision,
isSignalConnection: isSignalConnection(collision),
oldName: getOwn(previouslyAcknowledgedTitlesById, collision.id),
}))
);
if (!safeConvo) {
return null;
}
const possiblyUnsafe = {
conversation: possiblyUnsafeConvo,
isSignalConnection: isSignalConnection(possiblyUnsafeConvo),
};
const safe = {
conversation: safeConvo,
isSignalConnection: isSignalConnection(safeConvo),
};
return (
<ContactSpoofingReviewDialog
{...sharedProps}
type={ContactSpoofingType.MultipleGroupMembersWithSameTitle}
group={conversation}
collisionInfoByTitle={collisionInfoByTitle}
type={ContactSpoofingType.DirectConversationWithSameTitle}
possiblyUnsafe={possiblyUnsafe}
safe={safe}
/>
);
}
const possiblyUnsafeConvo = conversation;
assertDev(
possiblyUnsafeConvo.type === 'direct',
'DirectConversationWithSameTitle: expects possibly unsafe direct ' +
'conversation'
);
if (!safeConvo) {
return null;
}
const possiblyUnsafe = {
conversation: possiblyUnsafeConvo,
isSignalConnection: isSignalConnection(possiblyUnsafeConvo),
};
const safe = {
conversation: safeConvo,
isSignalConnection: isSignalConnection(safeConvo),
};
return (
<ContactSpoofingReviewDialog
{...sharedProps}
type={ContactSpoofingType.DirectConversationWithSameTitle}
possiblyUnsafe={possiblyUnsafe}
safe={safe}
/>
);
}
);

View file

@ -1,43 +1,45 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { connect } from 'react-redux';
import { sortBy } from 'lodash';
import type { StateType } from '../reducer';
import { mapDispatchToProps } from '../actions';
import type { StateProps } from '../../components/conversation/conversation-details/ConversationDetails';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import { ConversationDetails } from '../../components/conversation/conversation-details/ConversationDetails';
import {
getConversationByIdSelector,
getConversationByServiceIdSelector,
getAllComposableConversations,
} from '../selectors/conversations';
getGroupSizeHardLimit,
getGroupSizeRecommendedLimit,
} from '../../groups/limits';
import { SignalService as Proto } from '../../protobuf';
import type { CallHistoryGroup } from '../../types/CallDisposition';
import { assertDev } from '../../util/assert';
import { getConversationColorAttributes } from '../../util/getConversationColorAttributes';
import { getGroupMemberships } from '../../util/getGroupMemberships';
import { getActiveCallState } from '../selectors/calling';
import {
getAreWeASubscriber,
getDefaultConversationColor,
} from '../selectors/items';
import { getIntl, getTheme } from '../selectors/user';
import {
getBadgesSelector,
getPreferredBadgeSelector,
} from '../selectors/badges';
import { assertDev } from '../../util/assert';
import { SignalService as Proto } from '../../protobuf';
import { getConversationColorAttributes } from '../../util/getConversationColorAttributes';
import { getActiveCallState } from '../selectors/calling';
import {
getAllComposableConversations,
getConversationByIdSelector,
getConversationByServiceIdSelector,
} from '../selectors/conversations';
import {
getAreWeASubscriber,
getDefaultConversationColor,
} from '../selectors/items';
import { getSelectedNavTab } from '../selectors/nav';
import { getIntl, getTheme } from '../selectors/user';
import type { SmartChooseGroupMembersModalPropsType } from './ChooseGroupMembersModal';
import { SmartChooseGroupMembersModal } from './ChooseGroupMembersModal';
import type { SmartConfirmAdditionsModalPropsType } from './ConfirmAdditionsModal';
import { SmartConfirmAdditionsModal } from './ConfirmAdditionsModal';
import {
getGroupSizeRecommendedLimit,
getGroupSizeHardLimit,
} from '../../groups/limits';
import type { CallHistoryGroup } from '../../types/CallDisposition';
import { getSelectedNavTab } from '../selectors/nav';
import type { ConversationType } from '../ducks/conversations';
import { useConversationsActions } from '../ducks/conversations';
import { useCallingActions } from '../ducks/calling';
import { useSearchActions } from '../ducks/search';
import { useGlobalModalActions } from '../ducks/globalModals';
import { useLightboxActions } from '../ducks/lightbox';
export type SmartConversationDetailsProps = {
conversationId: string;
@ -58,79 +60,155 @@ const renderConfirmAdditionsModal = (
return <SmartConfirmAdditionsModal {...props} />;
};
const mapStateToProps = (
state: StateType,
props: SmartConversationDetailsProps
): StateProps => {
const conversationSelector = getConversationByIdSelector(state);
const conversation = conversationSelector(props.conversationId);
function getGroupsInCommonSorted(
conversation: ConversationType,
allComposableConversations: ReadonlyArray<ConversationType>
) {
if (conversation.type === 'direct') {
return [];
}
const groupsInCommonUnsorted = allComposableConversations.filter(
otherConversation => {
if (otherConversation.type !== 'group') {
return false;
}
return otherConversation.memberships?.some(member => {
return member.aci === conversation.serviceId;
});
}
);
return sortBy(groupsInCommonUnsorted, 'title');
}
export const SmartConversationDetails = memo(function SmartConversationDetails({
conversationId,
callHistoryGroup,
}: SmartConversationDetailsProps) {
const i18n = useSelector(getIntl);
const theme = useSelector(getTheme);
const activeCall = useSelector(getActiveCallState);
const allComposableConversations = useSelector(getAllComposableConversations);
const areWeASubscriber = useSelector(getAreWeASubscriber);
const badgesSelector = useSelector(getBadgesSelector);
const conversationByServiceIdSelector = useSelector(
getConversationByServiceIdSelector
);
const conversationSelector = useSelector(getConversationByIdSelector);
const defaultConversationColor = useSelector(getDefaultConversationColor);
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const selectedNavTab = useSelector(getSelectedNavTab);
const {
acceptConversation,
addMembersToGroup,
blockConversation,
deleteAvatarFromDisk,
getProfilesForConversation,
leaveGroup,
loadRecentMediaItems,
pushPanelForConversation,
replaceAvatar,
saveAvatarToDisk,
setDisappearingMessages,
setMuteExpiration,
showConversation,
updateGroupAttributes,
} = useConversationsActions();
const {
onOutgoingAudioCallInConversation,
onOutgoingVideoCallInConversation,
} = useCallingActions();
const { searchInConversation } = useSearchActions();
const {
showContactModal,
toggleAboutContactModal,
toggleAddUserToAnotherGroupModal,
toggleSafetyNumberModal,
} = useGlobalModalActions();
const { showLightboxWithMedia } = useLightboxActions();
const conversation = conversationSelector(conversationId);
assertDev(
conversation,
'<SmartConversationDetails> expected a conversation to be found'
);
const conversationWithColorAttributes = {
...conversation,
...getConversationColorAttributes(conversation, defaultConversationColor),
};
const canEditGroupInfo = Boolean(conversation.canEditGroupInfo);
const canAddNewMembers = Boolean(conversation.canAddNewMembers);
const isAdmin = Boolean(conversation.areWeAdmin);
const hasGroupLink =
Boolean(conversation.groupLink) &&
conversation.accessControlAddFromInviteLink !== ACCESS_ENUM.UNSATISFIABLE;
const conversationByServiceIdSelector =
getConversationByServiceIdSelector(state);
const groupMemberships = getGroupMemberships(
conversation,
conversationByServiceIdSelector
);
const badges = getBadgesSelector(state)(conversation.badges);
const defaultConversationColor = getDefaultConversationColor(state);
const groupsInCommon =
conversation.type === 'direct'
? getAllComposableConversations(state).filter(
c =>
c.type === 'group' &&
(c.memberships ?? []).some(
member => member.aci === conversation.serviceId
)
)
: [];
const groupsInCommonSorted = sortBy(groupsInCommon, 'title');
const { memberships, pendingApprovalMemberships, pendingMemberships } =
groupMemberships;
const badges = badgesSelector(conversation.badges);
const canAddNewMembers = conversation.canAddNewMembers ?? false;
const canEditGroupInfo = conversation.canEditGroupInfo ?? false;
const groupsInCommon = getGroupsInCommonSorted(
conversation,
allComposableConversations
);
const hasActiveCall = activeCall != null;
const hasGroupLink =
conversation.groupLink != null &&
conversation.accessControlAddFromInviteLink !== ACCESS_ENUM.UNSATISFIABLE;
const isAdmin = conversation.areWeAdmin ?? false;
const isGroup = conversation.type === 'group';
const maxGroupSize = getGroupSizeHardLimit(1001);
const maxRecommendedGroupSize = getGroupSizeRecommendedLimit(151);
return {
...props,
const userAvatarData = conversation.avatars ?? [];
areWeASubscriber: getAreWeASubscriber(state),
badges,
canEditGroupInfo,
canAddNewMembers,
conversation: {
...conversation,
...getConversationColorAttributes(conversation, defaultConversationColor),
},
getPreferredBadge: getPreferredBadgeSelector(state),
hasActiveCall: Boolean(getActiveCallState(state)),
i18n: getIntl(state),
isAdmin,
...groupMemberships,
maxGroupSize,
maxRecommendedGroupSize,
userAvatarData: conversation.avatars || [],
hasGroupLink,
groupsInCommon: groupsInCommonSorted,
isGroup: conversation.type === 'group',
selectedNavTab: getSelectedNavTab(state),
theme: getTheme(state),
renderChooseGroupMembersModal,
renderConfirmAdditionsModal,
};
};
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartConversationDetails = smart(ConversationDetails);
return (
<ConversationDetails
acceptConversation={acceptConversation}
addMembersToGroup={addMembersToGroup}
areWeASubscriber={areWeASubscriber}
badges={badges}
blockConversation={blockConversation}
callHistoryGroup={callHistoryGroup}
canAddNewMembers={canAddNewMembers}
canEditGroupInfo={canEditGroupInfo}
conversation={conversationWithColorAttributes}
deleteAvatarFromDisk={deleteAvatarFromDisk}
getPreferredBadge={getPreferredBadge}
getProfilesForConversation={getProfilesForConversation}
groupsInCommon={groupsInCommon}
hasActiveCall={hasActiveCall}
hasGroupLink={hasGroupLink}
i18n={i18n}
isAdmin={isAdmin}
isGroup={isGroup}
leaveGroup={leaveGroup}
loadRecentMediaItems={loadRecentMediaItems}
maxGroupSize={maxGroupSize}
maxRecommendedGroupSize={maxRecommendedGroupSize}
memberships={memberships}
onOutgoingAudioCallInConversation={onOutgoingAudioCallInConversation}
onOutgoingVideoCallInConversation={onOutgoingVideoCallInConversation}
pendingApprovalMemberships={pendingApprovalMemberships}
pendingMemberships={pendingMemberships}
pushPanelForConversation={pushPanelForConversation}
renderChooseGroupMembersModal={renderChooseGroupMembersModal}
renderConfirmAdditionsModal={renderConfirmAdditionsModal}
replaceAvatar={replaceAvatar}
saveAvatarToDisk={saveAvatarToDisk}
searchInConversation={searchInConversation}
selectedNavTab={selectedNavTab}
setDisappearingMessages={setDisappearingMessages}
setMuteExpiration={setMuteExpiration}
showContactModal={showContactModal}
showConversation={showConversation}
showLightboxWithMedia={showLightboxWithMedia}
theme={theme}
toggleAboutContactModal={toggleAboutContactModal}
toggleAddUserToAnotherGroupModal={toggleAddUserToAnotherGroupModal}
toggleSafetyNumberModal={toggleSafetyNumberModal}
updateGroupAttributes={updateGroupAttributes}
userAvatarData={userAvatarData}
/>
);
});

View file

@ -1,7 +1,7 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useMemo } from 'react';
import React, { memo, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { pick } from 'lodash';
import type { ConversationType } from '../ducks/conversations';
@ -78,7 +78,9 @@ const getOutgoingCallButtonStyle = (
}
};
export function SmartConversationHeader({ id }: OwnProps): JSX.Element {
export const SmartConversationHeader = memo(function SmartConversationHeader({
id,
}: OwnProps) {
const conversationSelector = useSelector(getConversationSelector);
const conversation = conversationSelector(id);
if (!conversation) {
@ -91,11 +93,10 @@ export function SmartConversationHeader({ id }: OwnProps): JSX.Element {
const badgeSelector = useSelector(getPreferredBadgeSelector);
const badge = badgeSelector(conversation.badges);
const i18n = useSelector(getIntl);
const hasPanelShowing = useSelector<StateType, boolean>(getHasPanelOpen);
const outgoingCallButtonStyle = useSelector<
StateType,
OutgoingCallButtonStyle
>(state => getOutgoingCallButtonStyle(conversation, state));
const hasPanelShowing = useSelector(getHasPanelOpen);
const outgoingCallButtonStyle = useSelector((state: StateType) => {
return getOutgoingCallButtonStyle(conversation, state);
});
const theme = useSelector(getTheme);
const {
@ -216,4 +217,4 @@ export function SmartConversationHeader({ id }: OwnProps): JSX.Element {
deleteConversation={deleteConversation}
/>
);
}
});

View file

@ -1,38 +1,43 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
import { useSelector } from 'react-redux';
import React, { memo } from 'react';
import { ConversationNotificationsSettings } from '../../components/conversation/conversation-details/ConversationNotificationsSettings';
import type { StateType } from '../reducer';
import { getIntl } from '../selectors/user';
import { getConversationByIdSelector } from '../selectors/conversations';
import { strictAssert } from '../../util/assert';
import { mapDispatchToProps } from '../actions';
import { useConversationsActions } from '../ducks/conversations';
export type OwnProps = {
export type SmartConversationNotificationsSettingsProps = {
conversationId: string;
};
const mapStateToProps = (state: StateType, props: OwnProps) => {
const { conversationId } = props;
const conversationSelector = getConversationByIdSelector(state);
const conversation = conversationSelector(conversationId);
strictAssert(conversation, 'Expected a conversation to be found');
return {
id: conversationId,
conversationType: conversation.type,
dontNotifyForMentionsIfMuted: Boolean(
conversation.dontNotifyForMentionsIfMuted
),
i18n: getIntl(state),
muteExpiresAt: conversation.muteExpiresAt,
};
};
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartConversationNotificationsSettings = smart(
ConversationNotificationsSettings
export const SmartConversationNotificationsSettings = memo(
function SmartConversationNotificationsSettings({
conversationId,
}: SmartConversationNotificationsSettingsProps) {
const i18n = useSelector(getIntl);
const conversationSelector = useSelector(getConversationByIdSelector);
const { setMuteExpiration, setDontNotifyForMentionsIfMuted } =
useConversationsActions();
const conversation = conversationSelector(conversationId);
strictAssert(conversation, 'Expected a conversation to be found');
const {
type: conversationType,
dontNotifyForMentionsIfMuted,
muteExpiresAt,
} = conversation;
return (
<ConversationNotificationsSettings
id={conversationId}
conversationType={conversationType}
dontNotifyForMentionsIfMuted={dontNotifyForMentionsIfMuted ?? false}
i18n={i18n}
muteExpiresAt={muteExpiresAt}
setMuteExpiration={setMuteExpiration}
setDontNotifyForMentionsIfMuted={setDontNotifyForMentionsIfMuted}
/>
);
}
);

View file

@ -4,6 +4,7 @@
import type { MutableRefObject } from 'react';
import React, {
forwardRef,
memo,
useCallback,
useEffect,
useRef,
@ -91,11 +92,11 @@ function doAnimate({
};
}
export function ConversationPanel({
export const ConversationPanel = memo(function ConversationPanel({
conversationId,
}: {
conversationId: string;
}): JSX.Element | null {
}) {
const panelInformation = useSelector(getPanelInformation);
const { panelAnimationDone, panelAnimationStarted } =
useConversationsActions();
@ -250,7 +251,7 @@ export function ConversationPanel({
}
return null;
}
});
type PanelPropsType = {
conversationId: string;

View file

@ -1,7 +1,7 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import React, { memo, useCallback } from 'react';
import { useSelector } from 'react-redux';
import type { StateType } from '../reducer';
import { ConversationPanel } from './ConversationPanel';
@ -18,51 +18,67 @@ import {
import { useComposerActions } from '../ducks/composer';
import { useConversationsActions } from '../ducks/conversations';
export function SmartConversationView(): JSX.Element {
const conversationId = useSelector(getSelectedConversationId);
if (!conversationId) {
throw new Error('SmartConversationView: No selected conversation');
}
const { toggleSelectMode } = useConversationsActions();
const selectedMessageIds = useSelector(getSelectedMessageIds);
const isSelectMode = selectedMessageIds != null;
const { processAttachments } = useComposerActions();
const hasOpenModal = useSelector((state: StateType) => {
return (
state.globalModals.forwardMessagesProps != null ||
state.globalModals.deleteMessagesProps != null ||
state.globalModals.hasConfirmationModal
);
});
const shouldHideConversationView = useSelector((state: StateType) => {
const activePanel = getActivePanel(state);
const isAnimating = getIsPanelAnimating(state);
return activePanel && !isAnimating;
});
return (
<ConversationView
conversationId={conversationId}
hasOpenModal={hasOpenModal}
isSelectMode={isSelectMode}
onExitSelectMode={() => {
toggleSelectMode(false);
}}
processAttachments={processAttachments}
renderCompositionArea={() => <SmartCompositionArea id={conversationId} />}
renderConversationHeader={() => (
<SmartConversationHeader id={conversationId} />
)}
renderTimeline={() => (
<SmartTimeline key={conversationId} id={conversationId} />
)}
renderPanel={() => <ConversationPanel conversationId={conversationId} />}
shouldHideConversationView={shouldHideConversationView}
/>
);
function renderCompositionArea(conversationId: string) {
return <SmartCompositionArea id={conversationId} />;
}
function renderConversationHeader(conversationId: string) {
return <SmartConversationHeader id={conversationId} />;
}
function renderTimeline(conversationId: string) {
return <SmartTimeline key={conversationId} id={conversationId} />;
}
function renderPanel(conversationId: string) {
return <ConversationPanel conversationId={conversationId} />;
}
export const SmartConversationView = memo(
function SmartConversationView(): JSX.Element {
const conversationId = useSelector(getSelectedConversationId);
if (!conversationId) {
throw new Error('SmartConversationView: No selected conversation');
}
const { toggleSelectMode } = useConversationsActions();
const selectedMessageIds = useSelector(getSelectedMessageIds);
const isSelectMode = selectedMessageIds != null;
const { processAttachments } = useComposerActions();
const hasOpenModal = useSelector((state: StateType) => {
return (
state.globalModals.forwardMessagesProps != null ||
state.globalModals.deleteMessagesProps != null ||
state.globalModals.hasConfirmationModal
);
});
const shouldHideConversationView = useSelector((state: StateType) => {
const activePanel = getActivePanel(state);
const isAnimating = getIsPanelAnimating(state);
return activePanel && !isAnimating;
});
const onExitSelectMode = useCallback(() => {
toggleSelectMode(false);
}, [toggleSelectMode]);
return (
<ConversationView
conversationId={conversationId}
hasOpenModal={hasOpenModal}
isSelectMode={isSelectMode}
onExitSelectMode={onExitSelectMode}
processAttachments={processAttachments}
renderCompositionArea={renderCompositionArea}
renderConversationHeader={renderConversationHeader}
renderTimeline={renderTimeline}
renderPanel={renderPanel}
shouldHideConversationView={shouldHideConversationView}
/>
);
}
);

View file

@ -1,19 +1,23 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import { useSelector } from 'react-redux';
import React, { memo } from 'react';
import { CrashReportDialog } from '../../components/CrashReportDialog';
import type { StateType } from '../reducer';
import { getIntl } from '../selectors/user';
import { useCrashReportsActions } from '../ducks/crashReports';
import { getCrashReportsIsPending } from '../selectors/crashReports';
const mapStateToProps = (state: StateType) => {
return {
...state.crashReports,
i18n: getIntl(state),
};
};
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartCrashReportDialog = smart(CrashReportDialog);
export const SmartCrashReportDialog = memo(function SmartCrashReportDialog() {
const i18n = useSelector(getIntl);
const isPending = useSelector(getCrashReportsIsPending);
const { writeCrashReportsToLog, eraseCrashReports } =
useCrashReportsActions();
return (
<CrashReportDialog
i18n={i18n}
isPending={isPending}
writeCrashReportsToLog={writeCrashReportsToLog}
eraseCrashReports={eraseCrashReports}
/>
);
});

View file

@ -1,51 +1,66 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import type { StateType } from '../reducer';
import type { LocalizerType } from '../../types/Util';
import { usePreferredReactionsActions } from '../ducks/preferredReactions';
import { useItemsActions } from '../ducks/items';
import { getIntl } from '../selectors/user';
import { getEmojiSkinTone } from '../selectors/items';
import { useRecentEmojis } from '../selectors/emojis';
import { getCustomizeModalState } from '../selectors/preferredReactions';
import { CustomizingPreferredReactionsModal } from '../../components/CustomizingPreferredReactionsModal';
import { strictAssert } from '../../util/assert';
export function SmartCustomizingPreferredReactionsModal(): JSX.Element {
const preferredReactionsActions = usePreferredReactionsActions();
const { onSetSkinTone } = useItemsActions();
export const SmartCustomizingPreferredReactionsModal = memo(
function SmartCustomizingPreferredReactionsModal(): JSX.Element {
const i18n = useSelector(getIntl);
const customizeModalState = useSelector(getCustomizeModalState);
const skinTone = useSelector(getEmojiSkinTone);
const recentEmojis = useRecentEmojis();
const i18n = useSelector<StateType, LocalizerType>(getIntl);
const {
cancelCustomizePreferredReactionsModal,
deselectDraftEmoji,
replaceSelectedDraftEmoji,
resetDraftEmoji,
savePreferredReactions,
selectDraftEmojiToBeReplaced,
} = usePreferredReactionsActions();
const { onSetSkinTone } = useItemsActions();
const customizeModalState = useSelector<
StateType,
ReturnType<typeof getCustomizeModalState>
>(state => getCustomizeModalState(state));
const recentEmojis = useRecentEmojis();
const skinTone = useSelector<StateType, number>(state =>
getEmojiSkinTone(state)
);
if (!customizeModalState) {
throw new Error(
strictAssert(
customizeModalState != null,
'<SmartCustomizingPreferredReactionsModal> requires a modal'
);
}
return (
<CustomizingPreferredReactionsModal
i18n={i18n}
onSetSkinTone={onSetSkinTone}
recentEmojis={recentEmojis}
skinTone={skinTone}
{...preferredReactionsActions}
{...customizeModalState}
/>
);
}
const {
hadSaveError,
isSaving,
draftPreferredReactions,
originalPreferredReactions,
selectedDraftEmojiIndex,
} = customizeModalState;
return (
<CustomizingPreferredReactionsModal
cancelCustomizePreferredReactionsModal={
cancelCustomizePreferredReactionsModal
}
deselectDraftEmoji={deselectDraftEmoji}
draftPreferredReactions={draftPreferredReactions}
hadSaveError={hadSaveError}
i18n={i18n}
isSaving={isSaving}
onSetSkinTone={onSetSkinTone}
originalPreferredReactions={originalPreferredReactions}
recentEmojis={recentEmojis}
replaceSelectedDraftEmoji={replaceSelectedDraftEmoji}
resetDraftEmoji={resetDraftEmoji}
savePreferredReactions={savePreferredReactions}
selectDraftEmojiToBeReplaced={selectDraftEmojiToBeReplaced}
selectedDraftEmojiIndex={selectedDraftEmojiIndex}
skinTone={skinTone}
/>
);
}
);

View file

@ -1,9 +1,8 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import type { DeleteMessagesPropsType } from '../ducks/globalModals';
import type { StateType } from '../reducer';
import { getIntl } from '../selectors/user';
import { useGlobalModalActions } from '../ducks/globalModals';
@ -12,56 +11,57 @@ import { strictAssert } from '../../util/assert';
import { canDeleteMessagesForEveryone } from '../selectors/message';
import { useConversationsActions } from '../ducks/conversations';
import { useToastActions } from '../ducks/toast';
import { getConversationSelector } from '../selectors/conversations';
import {
getConversationSelector,
getLastSelectedMessage,
} from '../selectors/conversations';
import { getDeleteMessagesProps } from '../selectors/globalModals';
export function SmartDeleteMessagesModal(): JSX.Element | null {
const deleteMessagesProps = useSelector<
StateType,
DeleteMessagesPropsType | undefined
>(state => state.globalModals.deleteMessagesProps);
strictAssert(
deleteMessagesProps != null,
'Cannot render delete messages modal without messages'
);
const { conversationId, messageIds, onDelete } = deleteMessagesProps;
const isMe = useSelector((state: StateType) => {
return getConversationSelector(state)(conversationId).isMe;
});
export const SmartDeleteMessagesModal = memo(
function SmartDeleteMessagesModal() {
const deleteMessagesProps = useSelector(getDeleteMessagesProps);
strictAssert(
deleteMessagesProps != null,
'Cannot render delete messages modal without messages'
);
const { conversationId, messageIds, onDelete } = deleteMessagesProps;
const isMe = useSelector((state: StateType) => {
return getConversationSelector(state)(conversationId).isMe;
});
const canDeleteForEveryone = useSelector((state: StateType) => {
return canDeleteMessagesForEveryone(state, { messageIds, isMe });
});
const lastSelectedMessage = useSelector((state: StateType) => {
return state.conversations.lastSelectedMessage;
});
const i18n = useSelector(getIntl);
const { toggleDeleteMessagesModal } = useGlobalModalActions();
const { deleteMessages, deleteMessagesForEveryone } =
useConversationsActions();
const { showToast } = useToastActions();
const canDeleteForEveryone = useSelector((state: StateType) => {
return canDeleteMessagesForEveryone(state, { messageIds, isMe });
});
const lastSelectedMessage = useSelector(getLastSelectedMessage);
const i18n = useSelector(getIntl);
const { toggleDeleteMessagesModal } = useGlobalModalActions();
const { deleteMessages, deleteMessagesForEveryone } =
useConversationsActions();
const { showToast } = useToastActions();
return (
<DeleteMessagesModal
isMe={isMe}
canDeleteForEveryone={canDeleteForEveryone}
i18n={i18n}
messageCount={deleteMessagesProps.messageIds.length}
onClose={() => {
toggleDeleteMessagesModal(undefined);
}}
onDeleteForMe={() => {
deleteMessages({
conversationId,
messageIds,
lastSelectedMessage,
});
onDelete?.();
}}
onDeleteForEveryone={() => {
deleteMessagesForEveryone(messageIds);
onDelete?.();
}}
showToast={showToast}
/>
);
}
return (
<DeleteMessagesModal
isMe={isMe}
canDeleteForEveryone={canDeleteForEveryone}
i18n={i18n}
messageCount={deleteMessagesProps.messageIds.length}
onClose={() => {
toggleDeleteMessagesModal(undefined);
}}
onDeleteForMe={() => {
deleteMessages({
conversationId,
messageIds,
lastSelectedMessage,
});
onDelete?.();
}}
onDeleteForEveryone={() => {
deleteMessagesForEveryone(messageIds);
onDelete?.();
}}
showToast={showToast}
/>
);
}
);

View file

@ -1,11 +1,9 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useMemo } from 'react';
import React, { memo, useMemo } from 'react';
import { useSelector } from 'react-redux';
import type { GlobalModalsStateType } from '../ducks/globalModals';
import type { MessageAttributesType } from '../../model-types.d';
import type { StateType } from '../reducer';
import { EditHistoryMessagesModal } from '../../components/EditHistoryMessagesModal';
import { getIntl, getPlatform } from '../selectors/user';
import { getMessagePropsSelector } from '../selectors/message';
@ -14,49 +12,46 @@ import { useConversationsActions } from '../ducks/conversations';
import { useGlobalModalActions } from '../ducks/globalModals';
import { useLightboxActions } from '../ducks/lightbox';
import { strictAssert } from '../../util/assert';
import { getEditHistoryMessages } from '../selectors/globalModals';
export function SmartEditHistoryMessagesModal(): JSX.Element {
const i18n = useSelector(getIntl);
const platform = useSelector(getPlatform);
export const SmartEditHistoryMessagesModal = memo(
function SmartEditHistoryMessagesModal(): JSX.Element {
const i18n = useSelector(getIntl);
const platform = useSelector(getPlatform);
const { closeEditHistoryModal } = useGlobalModalActions();
const { closeEditHistoryModal } = useGlobalModalActions();
const { kickOffAttachmentDownload } = useConversationsActions();
const { showLightbox } = useLightboxActions();
const { kickOffAttachmentDownload } = useConversationsActions();
const { showLightbox } = useLightboxActions();
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const messagesAttributes = useSelector(getEditHistoryMessages);
const messagePropsSelector = useSelector(getMessagePropsSelector);
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
strictAssert(messagesAttributes, 'messages not provided');
const { editHistoryMessages: messagesAttributes } = useSelector<
StateType,
GlobalModalsStateType
>(state => state.globalModals);
const editHistoryMessages = useMemo(() => {
return messagesAttributes.map(messageAttributes => ({
...messagePropsSelector(messageAttributes as MessageAttributesType),
// Make sure the messages don't get an "edited" badge
isEditedMessage: false,
// Do not show the same reactions in the message history UI
reactions: undefined,
// Make sure that the timestamp is the correct timestamp from attributes
// not the one that the selector derives.
timestamp: messageAttributes.timestamp,
}));
}, [messagesAttributes, messagePropsSelector]);
const messagePropsSelector = useSelector(getMessagePropsSelector);
strictAssert(messagesAttributes, 'messages not provided');
const editHistoryMessages = useMemo(() => {
return messagesAttributes.map(messageAttributes => ({
...messagePropsSelector(messageAttributes as MessageAttributesType),
// Make sure the messages don't get an "edited" badge
isEditedMessage: false,
// Do not show the same reactions in the message history UI
reactions: undefined,
// Make sure that the timestamp is the correct timestamp from attributes
// not the one that the selector derives.
timestamp: messageAttributes.timestamp,
}));
}, [messagesAttributes, messagePropsSelector]);
return (
<EditHistoryMessagesModal
closeEditHistoryModal={closeEditHistoryModal}
editHistoryMessages={editHistoryMessages}
getPreferredBadge={getPreferredBadge}
i18n={i18n}
platform={platform}
kickOffAttachmentDownload={kickOffAttachmentDownload}
showLightbox={showLightbox}
/>
);
}
return (
<EditHistoryMessagesModal
closeEditHistoryModal={closeEditHistoryModal}
editHistoryMessages={editHistoryMessages}
getPreferredBadge={getPreferredBadge}
i18n={i18n}
platform={platform}
kickOffAttachmentDownload={kickOffAttachmentDownload}
showLightbox={showLightbox}
/>
);
}
);

View file

@ -1,14 +1,9 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import type { PropsDataType } from '../../components/EditUsernameModalBody';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import { EditUsernameModalBody } from '../../components/EditUsernameModalBody';
import { getMinNickname, getMaxNickname } from '../../util/Username';
import type { StateType } from '../reducer';
import { getIntl } from '../selectors/user';
import {
getUsernameReservationState,
@ -18,25 +13,55 @@ import {
} from '../selectors/username';
import { getUsernameCorrupted } from '../selectors/items';
import { getMe } from '../selectors/conversations';
import { useUsernameActions } from '../ducks/username';
import { useToastActions } from '../ducks/toast';
function mapStateToProps(state: StateType): PropsDataType {
const i18n = getIntl(state);
const { username } = getMe(state);
const usernameCorrupted = getUsernameCorrupted(state);
export type SmartEditUsernameModalBodyProps = Readonly<{
isRootModal: boolean;
onClose(): void;
}>;
return {
i18n,
usernameCorrupted,
currentUsername: usernameCorrupted ? undefined : username,
minNickname: getMinNickname(),
maxNickname: getMaxNickname(),
state: getUsernameReservationState(state),
recoveredUsername: getRecoveredUsername(state),
reservation: getUsernameReservationObject(state),
error: getUsernameReservationError(state),
};
}
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartEditUsernameModalBody = smart(EditUsernameModalBody);
export const SmartEditUsernameModalBody = memo(
function SmartEditUsernameModalBody({
isRootModal,
onClose,
}: SmartEditUsernameModalBodyProps) {
const i18n = useSelector(getIntl);
const { username } = useSelector(getMe);
const usernameCorrupted = useSelector(getUsernameCorrupted);
const currentUsername = usernameCorrupted ? undefined : username;
const minNickname = getMinNickname();
const maxNickname = getMaxNickname();
const state = useSelector(getUsernameReservationState);
const recoveredUsername = useSelector(getRecoveredUsername);
const reservation = useSelector(getUsernameReservationObject);
const error = useSelector(getUsernameReservationError);
const {
setUsernameReservationError,
clearUsernameReservation,
reserveUsername,
confirmUsername,
} = useUsernameActions();
const { showToast } = useToastActions();
return (
<EditUsernameModalBody
i18n={i18n}
usernameCorrupted={usernameCorrupted}
currentUsername={currentUsername}
minNickname={minNickname}
maxNickname={maxNickname}
state={state}
recoveredUsername={recoveredUsername}
reservation={reservation}
error={error}
setUsernameReservationError={setUsernameReservationError}
clearUsernameReservation={clearUsernameReservation}
reserveUsername={reserveUsername}
confirmUsername={confirmUsername}
showToast={showToast}
isRootModal={isRootModal}
onClose={onClose}
/>
);
}
);

View file

@ -1,56 +1,53 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import React, { forwardRef, memo } from 'react';
import { useSelector } from 'react-redux';
import type { StateType } from '../reducer';
import { useRecentEmojis } from '../selectors/emojis';
import { useEmojisActions as useEmojiActions } from '../ducks/emojis';
import type { Props as EmojiPickerProps } from '../../components/emoji/EmojiPicker';
import { EmojiPicker } from '../../components/emoji/EmojiPicker';
import { getIntl } from '../selectors/user';
import { getEmojiSkinTone } from '../selectors/items';
import type { LocalizerType } from '../../types/Util';
export const SmartEmojiPicker = React.forwardRef<
HTMLDivElement,
Pick<
EmojiPickerProps,
'onClickSettings' | 'onPickEmoji' | 'onSetSkinTone' | 'onClose' | 'style'
>
>(function SmartEmojiPickerInner(
{ onClickSettings, onPickEmoji, onSetSkinTone, onClose, style },
ref
) {
const i18n = useSelector<StateType, LocalizerType>(getIntl);
const skinTone = useSelector<StateType, number>(state =>
getEmojiSkinTone(state)
);
export const SmartEmojiPicker = memo(
forwardRef<
HTMLDivElement,
Pick<
EmojiPickerProps,
'onClickSettings' | 'onPickEmoji' | 'onSetSkinTone' | 'onClose' | 'style'
>
>(function SmartEmojiPickerInner(
{ onClickSettings, onPickEmoji, onSetSkinTone, onClose, style },
ref
) {
const i18n = useSelector(getIntl);
const skinTone = useSelector(getEmojiSkinTone);
const recentEmojis = useRecentEmojis();
const recentEmojis = useRecentEmojis();
const { onUseEmoji } = useEmojiActions();
const { onUseEmoji } = useEmojiActions();
const handlePickEmoji = React.useCallback(
data => {
onUseEmoji({ shortName: data.shortName });
onPickEmoji(data);
},
[onUseEmoji, onPickEmoji]
);
const handlePickEmoji = React.useCallback(
data => {
onUseEmoji({ shortName: data.shortName });
onPickEmoji(data);
},
[onUseEmoji, onPickEmoji]
);
return (
<EmojiPicker
ref={ref}
i18n={i18n}
skinTone={skinTone}
onClickSettings={onClickSettings}
onSetSkinTone={onSetSkinTone}
onPickEmoji={handlePickEmoji}
recentEmojis={recentEmojis}
onClose={onClose}
style={style}
/>
);
});
return (
<EmojiPicker
ref={ref}
i18n={i18n}
skinTone={skinTone}
onClickSettings={onClickSettings}
onSetSkinTone={onSetSkinTone}
onPickEmoji={handlePickEmoji}
recentEmojis={recentEmojis}
onClose={onClose}
style={style}
/>
);
})
);

View file

@ -7,7 +7,6 @@ import type {
ForwardMessagePropsType,
ForwardMessagesPropsType,
} from '../ducks/globalModals';
import type { StateType } from '../reducer';
import * as log from '../../logging/log';
import { ForwardMessagesModal } from '../../components/ForwardMessagesModal';
import { LinkPreviewSourceType } from '../../types/LinkPreview';
@ -37,6 +36,7 @@ import type {
ForwardMessageData,
MessageForwardDraft,
} from '../../types/ForwardDraft';
import { getForwardMessagesProps } from '../selectors/globalModals';
function toMessageForwardDraft(
props: ForwardMessagePropsType,
@ -54,10 +54,7 @@ function toMessageForwardDraft(
}
export function SmartForwardMessagesModal(): JSX.Element | null {
const forwardMessagesProps = useSelector<
StateType,
ForwardMessagesPropsType | undefined
>(state => state.globalModals.forwardMessagesProps);
const forwardMessagesProps = useSelector(getForwardMessagesProps);
if (forwardMessagesProps == null) {
return null;

View file

@ -1,7 +1,7 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import { ConversationDetailsMembershipList } from '../../components/conversation/conversation-details/ConversationDetailsMembershipList';
@ -19,7 +19,9 @@ export type PropsType = {
conversationId: string;
};
export function SmartGV1Members({ conversationId }: PropsType): JSX.Element {
export const SmartGV1Members = memo(function SmartGV1Members({
conversationId,
}: PropsType): JSX.Element {
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const i18n = useSelector(getIntl);
const theme = useSelector(getTheme);
@ -53,4 +55,4 @@ export function SmartGV1Members({ conversationId }: PropsType): JSX.Element {
theme={theme}
/>
);
}
});

View file

@ -1,11 +1,8 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useCallback } from 'react';
import React, { memo, useCallback } from 'react';
import { useSelector } from 'react-redux';
import type { GlobalModalsStateType } from '../ducks/globalModals';
import type { StateType } from '../reducer';
import type { ButtonVariant } from '../../components/Button';
import { ErrorModal } from '../../components/ErrorModal';
import { GlobalModalContainer } from '../../components/GlobalModalContainer';
@ -26,6 +23,7 @@ import { getIntl, getTheme } from '../selectors/user';
import { useGlobalModalActions } from '../ducks/globalModals';
import { SmartDeleteMessagesModal } from './DeleteMessagesModal';
import { SmartMessageRequestActionsConfirmation } from './MessageRequestActionsConfirmation';
import { getGlobalModalsState } from '../selectors/globalModals';
function renderEditHistoryMessagesModal(): JSX.Element {
return <SmartEditHistoryMessagesModal />;
@ -71,148 +69,152 @@ function renderAboutContactModal(): JSX.Element {
return <SmartAboutContactModal />;
}
export function SmartGlobalModalContainer(): JSX.Element {
const conversationsStoppingSend = useSelector(getConversationsStoppingSend);
const i18n = useSelector(getIntl);
const theme = useSelector(getTheme);
export const SmartGlobalModalContainer = memo(
function SmartGlobalModalContainer() {
const conversationsStoppingSend = useSelector(getConversationsStoppingSend);
const i18n = useSelector(getIntl);
const theme = useSelector(getTheme);
const hasSafetyNumberChangeModal = conversationsStoppingSend.length > 0;
const hasSafetyNumberChangeModal = conversationsStoppingSend.length > 0;
const {
aboutContactModalContactId,
addUserToAnotherGroupModalContactId,
authArtCreatorData,
contactModalState,
deleteMessagesProps,
editHistoryMessages,
errorModalProps,
formattingWarningData,
forwardMessagesProps,
messageRequestActionsConfirmationProps,
isAuthorizingArtCreator,
isProfileEditorVisible,
isShortcutGuideModalVisible,
isSignalConnectionsVisible,
isStoriesSettingsVisible,
isWhatsNewVisible,
usernameOnboardingState,
safetyNumberChangedBlockingData,
safetyNumberModalContactId,
sendEditWarningData,
stickerPackPreviewId,
userNotFoundModalState,
} = useSelector<StateType, GlobalModalsStateType>(
state => state.globalModals
);
const {
aboutContactModalContactId,
addUserToAnotherGroupModalContactId,
authArtCreatorData,
contactModalState,
deleteMessagesProps,
editHistoryMessages,
errorModalProps,
formattingWarningData,
forwardMessagesProps,
messageRequestActionsConfirmationProps,
isAuthorizingArtCreator,
isProfileEditorVisible,
isShortcutGuideModalVisible,
isSignalConnectionsVisible,
isStoriesSettingsVisible,
isWhatsNewVisible,
usernameOnboardingState,
safetyNumberChangedBlockingData,
safetyNumberModalContactId,
sendEditWarningData,
stickerPackPreviewId,
userNotFoundModalState,
} = useSelector(getGlobalModalsState);
const {
cancelAuthorizeArtCreator,
closeErrorModal,
confirmAuthorizeArtCreator,
hideUserNotFoundModal,
hideWhatsNewModal,
showFormattingWarningModal,
showSendEditWarningModal,
toggleSignalConnectionsModal,
} = useGlobalModalActions();
const {
cancelAuthorizeArtCreator,
closeErrorModal,
confirmAuthorizeArtCreator,
hideUserNotFoundModal,
hideWhatsNewModal,
showFormattingWarningModal,
showSendEditWarningModal,
toggleSignalConnectionsModal,
} = useGlobalModalActions();
const renderAddUserToAnotherGroup = useCallback(() => {
return (
<SmartAddUserToAnotherGroupModal
contactID={String(addUserToAnotherGroupModalContactId)}
/>
);
}, [addUserToAnotherGroupModalContactId]);
const renderSafetyNumber = useCallback(
() => (
<SmartSafetyNumberModal
contactID={String(safetyNumberModalContactId)}
/>
),
[safetyNumberModalContactId]
);
const renderStickerPreviewModal = useCallback(
() =>
stickerPackPreviewId ? (
<SmartStickerPreviewModal packId={stickerPackPreviewId} />
) : null,
[stickerPackPreviewId]
);
const renderErrorModal = useCallback(
({
buttonVariant,
description,
title,
}: {
buttonVariant?: ButtonVariant;
description?: string;
title?: string;
}) => (
<ErrorModal
buttonVariant={buttonVariant}
description={description}
title={title}
i18n={i18n}
onClose={closeErrorModal}
/>
),
[closeErrorModal, i18n]
);
const renderAddUserToAnotherGroup = useCallback(() => {
return (
<SmartAddUserToAnotherGroupModal
contactID={String(addUserToAnotherGroupModalContactId)}
<GlobalModalContainer
addUserToAnotherGroupModalContactId={
addUserToAnotherGroupModalContactId
}
contactModalState={contactModalState}
editHistoryMessages={editHistoryMessages}
errorModalProps={errorModalProps}
deleteMessagesProps={deleteMessagesProps}
formattingWarningData={formattingWarningData}
forwardMessagesProps={forwardMessagesProps}
messageRequestActionsConfirmationProps={
messageRequestActionsConfirmationProps
}
hasSafetyNumberChangeModal={hasSafetyNumberChangeModal}
hideUserNotFoundModal={hideUserNotFoundModal}
hideWhatsNewModal={hideWhatsNewModal}
i18n={i18n}
isAboutContactModalVisible={aboutContactModalContactId != null}
isProfileEditorVisible={isProfileEditorVisible}
isShortcutGuideModalVisible={isShortcutGuideModalVisible}
isSignalConnectionsVisible={isSignalConnectionsVisible}
isStoriesSettingsVisible={isStoriesSettingsVisible}
isWhatsNewVisible={isWhatsNewVisible}
renderAboutContactModal={renderAboutContactModal}
renderAddUserToAnotherGroup={renderAddUserToAnotherGroup}
renderContactModal={renderContactModal}
renderEditHistoryMessagesModal={renderEditHistoryMessagesModal}
renderErrorModal={renderErrorModal}
renderDeleteMessagesModal={renderDeleteMessagesModal}
renderForwardMessagesModal={renderForwardMessagesModal}
renderMessageRequestActionsConfirmation={
renderMessageRequestActionsConfirmation
}
renderProfileEditor={renderProfileEditor}
renderUsernameOnboarding={renderUsernameOnboarding}
renderSafetyNumber={renderSafetyNumber}
renderSendAnywayDialog={renderSendAnywayDialog}
renderShortcutGuideModal={renderShortcutGuideModal}
renderStickerPreviewModal={renderStickerPreviewModal}
renderStoriesSettings={renderStoriesSettings}
safetyNumberChangedBlockingData={safetyNumberChangedBlockingData}
safetyNumberModalContactId={safetyNumberModalContactId}
sendEditWarningData={sendEditWarningData}
showFormattingWarningModal={showFormattingWarningModal}
showSendEditWarningModal={showSendEditWarningModal}
stickerPackPreviewId={stickerPackPreviewId}
theme={theme}
toggleSignalConnectionsModal={toggleSignalConnectionsModal}
userNotFoundModalState={userNotFoundModalState}
usernameOnboardingState={usernameOnboardingState}
isAuthorizingArtCreator={isAuthorizingArtCreator}
authArtCreatorData={authArtCreatorData}
cancelAuthorizeArtCreator={cancelAuthorizeArtCreator}
confirmAuthorizeArtCreator={confirmAuthorizeArtCreator}
/>
);
}, [addUserToAnotherGroupModalContactId]);
const renderSafetyNumber = useCallback(
() => (
<SmartSafetyNumberModal contactID={String(safetyNumberModalContactId)} />
),
[safetyNumberModalContactId]
);
const renderStickerPreviewModal = useCallback(
() =>
stickerPackPreviewId ? (
<SmartStickerPreviewModal packId={stickerPackPreviewId} />
) : null,
[stickerPackPreviewId]
);
const renderErrorModal = useCallback(
({
buttonVariant,
description,
title,
}: {
buttonVariant?: ButtonVariant;
description?: string;
title?: string;
}) => (
<ErrorModal
buttonVariant={buttonVariant}
description={description}
title={title}
i18n={i18n}
onClose={closeErrorModal}
/>
),
[closeErrorModal, i18n]
);
return (
<GlobalModalContainer
addUserToAnotherGroupModalContactId={addUserToAnotherGroupModalContactId}
contactModalState={contactModalState}
editHistoryMessages={editHistoryMessages}
errorModalProps={errorModalProps}
deleteMessagesProps={deleteMessagesProps}
formattingWarningData={formattingWarningData}
forwardMessagesProps={forwardMessagesProps}
messageRequestActionsConfirmationProps={
messageRequestActionsConfirmationProps
}
hasSafetyNumberChangeModal={hasSafetyNumberChangeModal}
hideUserNotFoundModal={hideUserNotFoundModal}
hideWhatsNewModal={hideWhatsNewModal}
i18n={i18n}
isAboutContactModalVisible={aboutContactModalContactId != null}
isProfileEditorVisible={isProfileEditorVisible}
isShortcutGuideModalVisible={isShortcutGuideModalVisible}
isSignalConnectionsVisible={isSignalConnectionsVisible}
isStoriesSettingsVisible={isStoriesSettingsVisible}
isWhatsNewVisible={isWhatsNewVisible}
renderAboutContactModal={renderAboutContactModal}
renderAddUserToAnotherGroup={renderAddUserToAnotherGroup}
renderContactModal={renderContactModal}
renderEditHistoryMessagesModal={renderEditHistoryMessagesModal}
renderErrorModal={renderErrorModal}
renderDeleteMessagesModal={renderDeleteMessagesModal}
renderForwardMessagesModal={renderForwardMessagesModal}
renderMessageRequestActionsConfirmation={
renderMessageRequestActionsConfirmation
}
renderProfileEditor={renderProfileEditor}
renderUsernameOnboarding={renderUsernameOnboarding}
renderSafetyNumber={renderSafetyNumber}
renderSendAnywayDialog={renderSendAnywayDialog}
renderShortcutGuideModal={renderShortcutGuideModal}
renderStickerPreviewModal={renderStickerPreviewModal}
renderStoriesSettings={renderStoriesSettings}
safetyNumberChangedBlockingData={safetyNumberChangedBlockingData}
safetyNumberModalContactId={safetyNumberModalContactId}
sendEditWarningData={sendEditWarningData}
showFormattingWarningModal={showFormattingWarningModal}
showSendEditWarningModal={showSendEditWarningModal}
stickerPackPreviewId={stickerPackPreviewId}
theme={theme}
toggleSignalConnectionsModal={toggleSignalConnectionsModal}
userNotFoundModalState={userNotFoundModalState}
usernameOnboardingState={usernameOnboardingState}
isAuthorizingArtCreator={isAuthorizingArtCreator}
authArtCreatorData={authArtCreatorData}
cancelAuthorizeArtCreator={cancelAuthorizeArtCreator}
confirmAuthorizeArtCreator={confirmAuthorizeArtCreator}
/>
);
}
}
);

View file

@ -1,34 +1,38 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
import type { PropsDataType } from '../../components/conversation/conversation-details/GroupLinkManagement';
import type { StateType } from '../reducer';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import { GroupLinkManagement } from '../../components/conversation/conversation-details/GroupLinkManagement';
import { getConversationSelector } from '../selectors/conversations';
import { getIntl } from '../selectors/user';
import { mapDispatchToProps } from '../actions';
import { useConversationsActions } from '../ducks/conversations';
export type SmartGroupLinkManagementProps = {
export type SmartGroupLinkManagementProps = Readonly<{
conversationId: string;
};
}>;
const mapStateToProps = (
state: StateType,
props: SmartGroupLinkManagementProps
): PropsDataType => {
const conversation = getConversationSelector(state)(props.conversationId);
const isAdmin = Boolean(conversation?.areWeAdmin);
return {
...props,
conversation,
i18n: getIntl(state),
isAdmin,
};
};
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartGroupLinkManagement = smart(GroupLinkManagement);
export const SmartGroupLinkManagement = memo(function SmartGroupLinkManagement({
conversationId,
}: SmartGroupLinkManagementProps) {
const i18n = useSelector(getIntl);
const conversationSelector = useSelector(getConversationSelector);
const conversation = conversationSelector(conversationId);
const isAdmin = conversation?.areWeAdmin ?? false;
const {
changeHasGroupLink,
generateNewGroupLink,
setAccessControlAddFromInviteLinkSetting,
} = useConversationsActions();
return (
<GroupLinkManagement
i18n={i18n}
changeHasGroupLink={changeHasGroupLink}
conversation={conversation}
generateNewGroupLink={generateNewGroupLink}
isAdmin={isAdmin}
setAccessControlAddFromInviteLinkSetting={
setAccessControlAddFromInviteLinkSetting
}
/>
);
});

View file

@ -1,19 +1,18 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import React, { memo, useCallback, useMemo } from 'react';
import { useSelector } from 'react-redux';
import type { DataPropsType as GroupV1MigrationDialogPropsType } from '../../components/GroupV1MigrationDialog';
import { GroupV1MigrationDialog } from '../../components/GroupV1MigrationDialog';
import type { ConversationType } from '../ducks/conversations';
import type { StateType } from '../reducer';
import { useConversationsActions } from '../ducks/conversations';
import { getPreferredBadgeSelector } from '../selectors/badges';
import { getConversationSelector } from '../selectors/conversations';
import { getIntl, getTheme } from '../selectors/user';
import * as log from '../../logging/log';
import { useGlobalModalActions } from '../ducks/globalModals';
export type PropsType = {
readonly conversationId: string;
readonly droppedMemberIds: Array<string>;
readonly invitedMemberIds: Array<string>;
} & Omit<
@ -21,37 +20,62 @@ export type PropsType = {
'i18n' | 'droppedMembers' | 'invitedMembers' | 'theme' | 'getPreferredBadge'
>;
const mapStateToProps = (
state: StateType,
props: PropsType
): GroupV1MigrationDialogPropsType => {
const getConversation = getConversationSelector(state);
const { droppedMemberIds, invitedMemberIds } = props;
function isNonNullable<T>(value: T | null | undefined): value is T {
return value != null;
}
const droppedMembers = droppedMemberIds
.map(getConversation)
.filter(Boolean) as Array<ConversationType>;
if (droppedMembers.length !== droppedMemberIds.length) {
log.warn('smart/GroupV1MigrationDialog: droppedMembers length changed');
export const SmartGroupV1MigrationDialog = memo(
function SmartGroupV1MigrationDialog({
conversationId,
areWeInvited,
hasMigrated,
droppedMemberIds,
invitedMemberIds,
}: PropsType) {
const i18n = useSelector(getIntl);
const theme = useSelector(getTheme);
const getConversation = useSelector(getConversationSelector);
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const { initiateMigrationToGroupV2 } = useConversationsActions();
const { closeGV2MigrationDialog } = useGlobalModalActions();
const droppedMembers = useMemo(() => {
const result = droppedMemberIds
.map(getConversation)
.filter(isNonNullable);
if (result.length !== droppedMemberIds.length) {
log.warn('smart/GroupV1MigrationDialog: droppedMembers length changed');
}
return result;
}, [droppedMemberIds, getConversation]);
const invitedMembers = useMemo(() => {
const result = invitedMemberIds
.map(getConversation)
.filter(isNonNullable);
if (result.length !== invitedMemberIds.length) {
log.warn('smart/GroupV1MigrationDialog: invitedMembers length changed');
}
return result;
}, [invitedMemberIds, getConversation]);
const handleMigrate = useCallback(() => {
initiateMigrationToGroupV2(conversationId);
}, [initiateMigrationToGroupV2, conversationId]);
return (
<GroupV1MigrationDialog
i18n={i18n}
theme={theme}
areWeInvited={areWeInvited}
hasMigrated={hasMigrated}
getPreferredBadge={getPreferredBadge}
droppedMembers={droppedMembers}
invitedMembers={invitedMembers}
onMigrate={handleMigrate}
onClose={closeGV2MigrationDialog}
/>
);
}
const invitedMembers = invitedMemberIds
.map(getConversation)
.filter(Boolean) as Array<ConversationType>;
if (invitedMembers.length !== invitedMemberIds.length) {
log.warn('smart/GroupV1MigrationDialog: invitedMembers length changed');
}
return {
...props,
droppedMembers,
getPreferredBadge: getPreferredBadgeSelector(state),
invitedMembers,
i18n: getIntl(state),
theme: getTheme(state),
};
};
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartGroupV1MigrationDialog = smart(GroupV1MigrationDialog);
);

View file

@ -1,34 +1,38 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import type { PropsType as GroupV2JoinDialogPropsType } from '../../components/GroupV2JoinDialog';
import { GroupV2JoinDialog } from '../../components/GroupV2JoinDialog';
import type { StateType } from '../reducer';
import { getIntl } from '../selectors/user';
import { getPreJoinConversation } from '../selectors/conversations';
export type PropsType = Pick<GroupV2JoinDialogPropsType, 'join' | 'onClose'>;
export type SmartGroupV2JoinDialogProps = Pick<
GroupV2JoinDialogPropsType,
'join' | 'onClose'
>;
const mapStateToProps = (
state: StateType,
props: PropsType
): GroupV2JoinDialogPropsType => {
const preJoinConversation = getPreJoinConversation(state);
if (!preJoinConversation) {
export const SmartGroupV2JoinDialog = memo(function SmartGroupV2JoinDialog({
join,
onClose,
}: SmartGroupV2JoinDialogProps) {
const i18n = useSelector(getIntl);
const preJoinConversation = useSelector(getPreJoinConversation);
if (preJoinConversation == null) {
throw new Error('smart/GroupV2JoinDialog: No pre-join conversation!');
}
return {
...props,
...preJoinConversation,
i18n: getIntl(state),
};
};
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartGroupV2JoinDialog = smart(GroupV2JoinDialog);
const { memberCount, title, groupDescription, approvalRequired, avatar } =
preJoinConversation;
return (
<GroupV2JoinDialog
approvalRequired={approvalRequired}
avatar={avatar}
groupDescription={groupDescription}
i18n={i18n}
join={join}
memberCount={memberCount}
onClose={onClose}
title={title}
/>
);
});

View file

@ -1,32 +1,35 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
import type { StateType } from '../reducer';
import type { PropsDataType } from '../../components/conversation/conversation-details/GroupV2Permissions';
import { mapDispatchToProps } from '../actions';
import { useSelector } from 'react-redux';
import React, { memo } from 'react';
import { GroupV2Permissions } from '../../components/conversation/conversation-details/GroupV2Permissions';
import { getConversationSelector } from '../selectors/conversations';
import { getIntl } from '../selectors/user';
import { useConversationsActions } from '../ducks/conversations';
export type SmartGroupV2PermissionsProps = {
conversationId: string;
};
const mapStateToProps = (
state: StateType,
props: SmartGroupV2PermissionsProps
): PropsDataType => {
const conversation = getConversationSelector(state)(props.conversationId);
return {
...props,
conversation,
i18n: getIntl(state),
};
};
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartGroupV2Permissions = smart(GroupV2Permissions);
export const SmartGroupV2Permissions = memo(function SmartGroupV2Permissions({
conversationId,
}: SmartGroupV2PermissionsProps) {
const i18n = useSelector(getIntl);
const conversationSelector = useSelector(getConversationSelector);
const conversation = conversationSelector(conversationId);
const {
setAccessControlAttributesSetting,
setAccessControlMembersSetting,
setAnnouncementsOnly,
} = useConversationsActions();
return (
<GroupV2Permissions
i18n={i18n}
conversation={conversation}
setAccessControlAttributesSetting={setAccessControlAttributesSetting}
setAccessControlMembersSetting={setAccessControlMembersSetting}
setAnnouncementsOnly={setAnnouncementsOnly}
/>
);
});

View file

@ -1,41 +1,77 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import { ConversationHero } from '../../components/conversation/ConversationHero';
import type { StateType } from '../reducer';
import { getPreferredBadgeSelector } from '../selectors/badges';
import { getIntl, getTheme } from '../selectors/user';
import { getHasStoriesSelector } from '../selectors/stories2';
import { isSignalConversation } from '../../util/isSignalConversation';
import { getConversationSelector } from '../selectors/conversations';
import { useConversationsActions } from '../ducks/conversations';
import { useGlobalModalActions } from '../ducks/globalModals';
import { useStoriesActions } from '../ducks/stories';
type ExternalProps = {
type SmartHeroRowProps = Readonly<{
id: string;
};
}>;
const mapStateToProps = (state: StateType, props: ExternalProps) => {
const { id } = props;
const conversation = state.conversations.conversationLookup[id];
if (!conversation) {
export const SmartHeroRow = memo(function SmartHeroRow({
id,
}: SmartHeroRowProps) {
const i18n = useSelector(getIntl);
const theme = useSelector(getTheme);
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const hasStoriesSelector = useSelector(getHasStoriesSelector);
const conversationSelector = useSelector(getConversationSelector);
const conversation = conversationSelector(id);
if (conversation == null) {
throw new Error(`Did not find conversation ${id} in state!`);
}
return {
i18n: getIntl(state),
...conversation,
conversationType: conversation.type,
hasStories: getHasStoriesSelector(state)(id),
badge: getPreferredBadgeSelector(state)(conversation.badges),
isSignalConversation: isSignalConversation(conversation),
theme: getTheme(state),
};
};
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartHeroRow = smart(ConversationHero);
const badge = getPreferredBadge(conversation.badges);
const hasStories = hasStoriesSelector(id);
const isSignalConversationValue = isSignalConversation(conversation);
const { unblurAvatar, updateSharedGroups } = useConversationsActions();
const { toggleAboutContactModal } = useGlobalModalActions();
const { viewUserStories } = useStoriesActions();
const {
about,
acceptedMessageRequest,
avatarPath,
groupDescription,
isMe,
membersCount,
phoneNumber,
profileName,
sharedGroupNames,
title,
type,
unblurredAvatarPath,
} = conversation;
return (
<ConversationHero
about={about}
acceptedMessageRequest={acceptedMessageRequest}
avatarPath={avatarPath}
badge={badge}
conversationType={type}
groupDescription={groupDescription}
hasStories={hasStories}
i18n={i18n}
id={id}
isMe={isMe}
isSignalConversation={isSignalConversationValue}
membersCount={membersCount}
phoneNumber={phoneNumber}
profileName={profileName}
sharedGroupNames={sharedGroupNames}
theme={theme}
title={title}
toggleAboutContactModal={toggleAboutContactModal}
unblurAvatar={unblurAvatar}
unblurredAvatarPath={unblurredAvatarPath}
updateSharedGroups={updateSharedGroups}
viewUserStories={viewUserStories}
/>
);
});

View file

@ -1,9 +1,8 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import type { AppStateType } from '../ducks/app';
import type { StateType } from '../reducer';
import { Inbox } from '../../components/Inbox';
import { getIntl } from '../selectors/user';
@ -37,19 +36,19 @@ function renderStoriesTab() {
return <SmartStoriesTab />;
}
export function SmartInbox(): JSX.Element {
export const SmartInbox = memo(function SmartInbox(): JSX.Element {
const i18n = useSelector(getIntl);
const isCustomizingPreferredReactions = useSelector(
getIsCustomizingPreferredReactions
);
const envelopeTimestamp = useSelector<StateType, number | undefined>(
state => state.inbox.envelopeTimestamp
const envelopeTimestamp = useSelector(
(state: StateType) => state.inbox.envelopeTimestamp
);
const firstEnvelopeTimestamp = useSelector<StateType, number | undefined>(
state => state.inbox.firstEnvelopeTimestamp
const firstEnvelopeTimestamp = useSelector(
(state: StateType) => state.inbox.firstEnvelopeTimestamp
);
const { hasInitialLoadCompleted } = useSelector<StateType, AppStateType>(
state => state.app
const { hasInitialLoadCompleted } = useSelector(
(state: StateType) => state.app
);
const navTabsCollapsed = useSelector(getNavTabsCollapsed);
@ -73,4 +72,4 @@ export function SmartInbox(): JSX.Element {
renderStoriesTab={renderStoriesTab}
/>
);
}
});

View file

@ -1,16 +1,14 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ComponentProps, ReactElement } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import type { ComponentProps } from 'react';
import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import pTimeout, { TimeoutError } from 'p-timeout';
import { getIntl } from '../selectors/user';
import { getUpdatesState } from '../selectors/updates';
import { useUpdatesActions } from '../ducks/updates';
import { hasExpired as hasExpiredSelector } from '../selectors/expiration';
import * as log from '../../logging/log';
import type { Loadable } from '../../util/loadable';
import { LoadingState } from '../../util/loadable';
@ -87,7 +85,7 @@ function getInstallError(err: unknown): InstallError {
return InstallError.UnknownError;
}
export function SmartInstallScreen(): ReactElement {
export const SmartInstallScreen = memo(function SmartInstallScreen() {
const i18n = useSelector(getIntl);
const updates = useSelector(getUpdatesState);
const { startUpdate } = useUpdatesActions();
@ -339,4 +337,4 @@ export function SmartInstallScreen(): ReactElement {
/>
</>
);
}
});

View file

@ -1,23 +1,71 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { connect } from 'react-redux';
import { get } from 'lodash';
import { mapDispatchToProps } from '../actions';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import type { PropsType as DialogExpiredBuildPropsType } from '../../components/DialogExpiredBuild';
import { DialogExpiredBuild } from '../../components/DialogExpiredBuild';
import type { PropsType as LeftPanePropsType } from '../../components/LeftPane';
import { LeftPane } from '../../components/LeftPane';
import { DialogExpiredBuild } from '../../components/DialogExpiredBuild';
import type { PropsType as DialogExpiredBuildPropsType } from '../../components/DialogExpiredBuild';
import type { StateType } from '../reducer';
import { missingCaseError } from '../../util/missingCaseError';
import { lookupConversationWithoutServiceId } from '../../util/lookupConversationWithoutServiceId';
import { isDone as isRegistrationDone } from '../../util/registration';
import { getCountryDataForLocale } from '../../util/getCountryData';
import { getUsernameFromSearch } from '../../util/Username';
import type { NavTabPanelProps } from '../../components/NavTabs';
import type { WidthBreakpoint } from '../../components/_util';
import {
getGroupSizeHardLimit,
getGroupSizeRecommendedLimit,
} from '../../groups/limits';
import { LeftPaneMode } from '../../types/leftPane';
import { getUsernameFromSearch } from '../../util/Username';
import { getCountryDataForLocale } from '../../util/getCountryData';
import { lookupConversationWithoutServiceId } from '../../util/lookupConversationWithoutServiceId';
import { missingCaseError } from '../../util/missingCaseError';
import { isDone as isRegistrationDone } from '../../util/registration';
import { useCallingActions } from '../ducks/calling';
import { useConversationsActions } from '../ducks/conversations';
import { ComposerStep, OneTimeModalState } from '../ducks/conversationsEnums';
import { useGlobalModalActions } from '../ducks/globalModals';
import { useItemsActions } from '../ducks/items';
import { useNetworkActions } from '../ducks/network';
import { useSearchActions } from '../ducks/search';
import { useUsernameActions } from '../ducks/username';
import type { StateType } from '../reducer';
import { getPreferredBadgeSelector } from '../selectors/badges';
import {
getComposeAvatarData,
getComposeGroupAvatar,
getComposeGroupExpireTimer,
getComposeGroupName,
getComposeSelectedContacts,
getComposerConversationSearchTerm,
getComposerSelectedRegion,
getComposerStep,
getComposerUUIDFetchState,
getFilteredCandidateContactsForNewGroup,
getFilteredComposeContacts,
getFilteredComposeGroups,
getLeftPaneLists,
getMaximumGroupSizeModalState,
getMe,
getRecommendedGroupSizeModalState,
getSelectedConversationId,
getShowArchived,
getTargetedMessage,
hasGroupCreationError,
isCreatingGroup,
isEditingAvatar,
} from '../selectors/conversations';
import { getCrashReportCount } from '../selectors/crashReports';
import { hasExpired } from '../selectors/expiration';
import {
getNavTabsCollapsed,
getPreferredLeftPaneWidth,
getUsernameCorrupted,
getUsernameLinkCorrupted,
} from '../selectors/items';
import {
getChallengeStatus,
hasNetworkDialog as getHasNetworkDialog,
} from '../selectors/network';
import {
getIsSearching,
getQuery,
@ -26,65 +74,26 @@ import {
getStartSearchCounter,
isSearching,
} from '../selectors/search';
import {
isUpdateDownloaded as getIsUpdateDownloaded,
isOSUnsupported,
isUpdateDialogVisible,
} from '../selectors/updates';
import {
getIntl,
getIsMacOS,
getRegionCode,
getTheme,
getIsMacOS,
} from '../selectors/user';
import { hasExpired } from '../selectors/expiration';
import {
isUpdateDialogVisible,
isUpdateDownloaded,
isOSUnsupported,
} from '../selectors/updates';
import { getPreferredBadgeSelector } from '../selectors/badges';
import { hasNetworkDialog } from '../selectors/network';
import {
getPreferredLeftPaneWidth,
getUsernameCorrupted,
getUsernameLinkCorrupted,
getNavTabsCollapsed,
} from '../selectors/items';
import {
getComposeAvatarData,
getComposeGroupAvatar,
getComposeGroupExpireTimer,
getComposeGroupName,
getComposerConversationSearchTerm,
getComposerSelectedRegion,
getComposerStep,
getComposerUUIDFetchState,
getComposeSelectedContacts,
getFilteredCandidateContactsForNewGroup,
getFilteredComposeContacts,
getFilteredComposeGroups,
getLeftPaneLists,
getMaximumGroupSizeModalState,
getMe,
getRecommendedGroupSizeModalState,
getSelectedConversationId,
getTargetedMessage,
getShowArchived,
hasGroupCreationError,
isCreatingGroup,
isEditingAvatar,
} from '../selectors/conversations';
import type { WidthBreakpoint } from '../../components/_util';
import {
getGroupSizeRecommendedLimit,
getGroupSizeHardLimit,
} from '../../groups/limits';
import { SmartCaptchaDialog } from './CaptchaDialog';
import { SmartCrashReportDialog } from './CrashReportDialog';
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 { SmartUnsupportedOSDialog } from './UnsupportedOSDialog';
import { SmartUpdateDialog } from './UpdateDialog';
import { SmartCaptchaDialog } from './CaptchaDialog';
import { SmartCrashReportDialog } from './CrashReportDialog';
function renderMessageSearchResult(id: string): JSX.Element {
return <SmartMessageSearchResult id={id} />;
@ -120,7 +129,7 @@ function renderUnsupportedOSDialog(
): JSX.Element {
return <SmartUnsupportedOSDialog {...props} />;
}
function renderToastManager(props: {
function renderToastManagerWithMegaphone(props: {
containerWidthBreakpoint: WidthBreakpoint;
}): JSX.Element {
return <SmartToastManager {...props} />;
@ -243,15 +252,81 @@ const getModeSpecificProps = (
}
};
const mapStateToProps = (state: StateType) => {
const hasUpdateDialog = isUpdateDialogVisible(state);
const hasUnsupportedOS = isOSUnsupported(state);
const usernameCorrupted = getUsernameCorrupted(state);
const usernameLinkCorrupted = getUsernameLinkCorrupted(state);
export const SmartLeftPane = memo(function SmartLeftPane({
hasFailedStorySends,
hasPendingUpdate,
otherTabsUnreadStats,
}: NavTabPanelProps) {
const challengeStatus = useSelector(getChallengeStatus);
const composerStep = useSelector(getComposerStep);
const crashReportCount = useSelector(getCrashReportCount);
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const hasAppExpired = useSelector(hasExpired);
const hasNetworkDialog = useSelector(getHasNetworkDialog);
const hasSearchQuery = useSelector(isSearching);
const hasUnsupportedOS = useSelector(isOSUnsupported);
const hasUpdateDialog = useSelector(isUpdateDialogVisible);
const i18n = useSelector(getIntl);
const isMacOS = useSelector(getIsMacOS);
const isUpdateDownloaded = useSelector(getIsUpdateDownloaded);
const modeSpecificProps = useSelector(getModeSpecificProps);
const navTabsCollapsed = useSelector(getNavTabsCollapsed);
const preferredWidthFromStorage = useSelector(getPreferredLeftPaneWidth);
const selectedConversationId = useSelector(getSelectedConversationId);
const showArchived = useSelector(getShowArchived);
const targetedMessage = useSelector(getTargetedMessage);
const theme = useSelector(getTheme);
const usernameCorrupted = useSelector(getUsernameCorrupted);
const usernameLinkCorrupted = useSelector(getUsernameLinkCorrupted);
const {
blockConversation,
clearGroupCreationError,
closeMaximumGroupSizeModal,
closeRecommendedGroupSizeModal,
composeDeleteAvatarFromDisk,
composeReplaceAvatar,
composeSaveAvatarToDisk,
createGroup,
removeConversation,
setComposeGroupAvatar,
setComposeGroupExpireTimer,
setComposeGroupName,
setComposeSearchTerm,
setComposeSelectedRegion,
setIsFetchingUUID,
showArchivedConversations,
showChooseGroupMembers,
showConversation,
showFindByPhoneNumber,
showFindByUsername,
showInbox,
startComposing,
startSettingGroupMetadata,
toggleComposeEditingAvatar,
toggleConversationInChooseMembers,
} = useConversationsActions();
const {
clearConversationSearch,
clearSearch,
searchInConversation,
startSearch,
updateSearchTerm,
} = useSearchActions();
const {
onOutgoingAudioCallInConversation,
onOutgoingVideoCallInConversation,
} = useCallingActions();
const { openUsernameReservationModal } = useUsernameActions();
const { savePreferredLeftPaneWidth, toggleNavTabsCollapse } =
useItemsActions();
const { setChallengeStatus } = useNetworkActions();
const { showUserNotFoundModal, toggleProfileEditor } =
useGlobalModalActions();
let hasExpiredDialog = false;
let unsupportedOSDialogType: 'error' | 'warning' | undefined;
if (hasExpired(state)) {
if (hasAppExpired) {
if (hasUnsupportedOS) {
unsupportedOSDialogType = 'error';
} else {
@ -261,49 +336,87 @@ const mapStateToProps = (state: StateType) => {
unsupportedOSDialogType = 'warning';
}
const composerStep = getComposerStep(state);
const showArchived = getShowArchived(state);
const hasSearchQuery = isSearching(state);
const hasRelinkDialog = !isRegistrationDone();
return {
hasNetworkDialog: hasNetworkDialog(state),
hasExpiredDialog,
hasRelinkDialog: !isRegistrationDone(),
hasUpdateDialog,
isUpdateDownloaded: isUpdateDownloaded(state),
unsupportedOSDialogType,
usernameCorrupted,
usernameLinkCorrupted,
const renderToastManager =
composerStep == null && !showArchived && !hasSearchQuery
? renderToastManagerWithMegaphone
: renderToastManagerWithoutMegaphone;
modeSpecificProps: getModeSpecificProps(state),
navTabsCollapsed: getNavTabsCollapsed(state),
preferredWidthFromStorage: getPreferredLeftPaneWidth(state),
selectedConversationId: getSelectedConversationId(state),
targetedMessageId: getTargetedMessage(state)?.id,
showArchived,
getPreferredBadge: getPreferredBadgeSelector(state),
i18n: getIntl(state),
isMacOS: getIsMacOS(state),
regionCode: getRegionCode(state),
challengeStatus: state.network.challengeStatus,
crashReportCount: state.crashReports.count,
renderMessageSearchResult,
renderNetworkStatus,
renderRelinkDialog,
renderUpdateDialog,
renderCaptchaDialog,
renderCrashReportDialog,
renderExpiredBuildDialog,
renderUnsupportedOSDialog,
renderToastManager:
composerStep == null && !showArchived && !hasSearchQuery
? renderToastManager
: renderToastManagerWithoutMegaphone,
lookupConversationWithoutServiceId,
theme: getTheme(state),
};
};
const targetedMessageId = targetedMessage?.id;
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartLeftPane = smart(LeftPane);
return (
<LeftPane
blockConversation={blockConversation}
challengeStatus={challengeStatus}
clearConversationSearch={clearConversationSearch}
clearGroupCreationError={clearGroupCreationError}
clearSearch={clearSearch}
closeMaximumGroupSizeModal={closeMaximumGroupSizeModal}
closeRecommendedGroupSizeModal={closeRecommendedGroupSizeModal}
composeDeleteAvatarFromDisk={composeDeleteAvatarFromDisk}
composeReplaceAvatar={composeReplaceAvatar}
composeSaveAvatarToDisk={composeSaveAvatarToDisk}
crashReportCount={crashReportCount}
createGroup={createGroup}
getPreferredBadge={getPreferredBadge}
hasExpiredDialog={hasExpiredDialog}
hasFailedStorySends={hasFailedStorySends}
hasNetworkDialog={hasNetworkDialog}
hasPendingUpdate={hasPendingUpdate}
hasRelinkDialog={hasRelinkDialog}
hasUpdateDialog={hasUpdateDialog}
i18n={i18n}
isMacOS={isMacOS}
isUpdateDownloaded={isUpdateDownloaded}
lookupConversationWithoutServiceId={lookupConversationWithoutServiceId}
modeSpecificProps={modeSpecificProps}
navTabsCollapsed={navTabsCollapsed}
onOutgoingAudioCallInConversation={onOutgoingAudioCallInConversation}
onOutgoingVideoCallInConversation={onOutgoingVideoCallInConversation}
openUsernameReservationModal={openUsernameReservationModal}
otherTabsUnreadStats={otherTabsUnreadStats}
preferredWidthFromStorage={preferredWidthFromStorage}
removeConversation={removeConversation}
renderCaptchaDialog={renderCaptchaDialog}
renderCrashReportDialog={renderCrashReportDialog}
renderExpiredBuildDialog={renderExpiredBuildDialog}
renderMessageSearchResult={renderMessageSearchResult}
renderNetworkStatus={renderNetworkStatus}
renderRelinkDialog={renderRelinkDialog}
renderToastManager={renderToastManager}
renderUnsupportedOSDialog={renderUnsupportedOSDialog}
renderUpdateDialog={renderUpdateDialog}
savePreferredLeftPaneWidth={savePreferredLeftPaneWidth}
searchInConversation={searchInConversation}
selectedConversationId={selectedConversationId}
setChallengeStatus={setChallengeStatus}
setComposeGroupAvatar={setComposeGroupAvatar}
setComposeGroupExpireTimer={setComposeGroupExpireTimer}
setComposeGroupName={setComposeGroupName}
setComposeSearchTerm={setComposeSearchTerm}
setComposeSelectedRegion={setComposeSelectedRegion}
setIsFetchingUUID={setIsFetchingUUID}
showArchivedConversations={showArchivedConversations}
showChooseGroupMembers={showChooseGroupMembers}
showConversation={showConversation}
showFindByPhoneNumber={showFindByPhoneNumber}
showFindByUsername={showFindByUsername}
showInbox={showInbox}
showUserNotFoundModal={showUserNotFoundModal}
startComposing={startComposing}
startSearch={startSearch}
startSettingGroupMetadata={startSettingGroupMetadata}
targetedMessageId={targetedMessageId}
theme={theme}
toggleComposeEditingAvatar={toggleComposeEditingAvatar}
toggleConversationInChooseMembers={toggleConversationInChooseMembers}
toggleNavTabsCollapse={toggleNavTabsCollapse}
toggleProfileEditor={toggleProfileEditor}
unsupportedOSDialogType={unsupportedOSDialogType}
updateSearchTerm={updateSearchTerm}
usernameCorrupted={usernameCorrupted}
usernameLinkCorrupted={usernameLinkCorrupted}
/>
);
});

View file

@ -1,14 +1,8 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useCallback } from 'react';
import React, { memo, useCallback } from 'react';
import { useSelector } from 'react-redux';
import type { ReadonlyDeep } from 'type-fest';
import type { GetConversationByIdType } from '../selectors/conversations';
import type { LocalizerType } from '../../types/Util';
import type { MediaItemType } from '../../types/MediaItem';
import type { StateType } from '../reducer';
import { Lightbox } from '../../components/Lightbox';
import { getConversationSelector } from '../selectors/conversations';
import { getIntl } from '../selectors/user';
@ -26,8 +20,8 @@ import {
shouldShowLightbox,
} from '../selectors/lightbox';
export function SmartLightbox(): JSX.Element | null {
const i18n = useSelector<StateType, LocalizerType>(getIntl);
export const SmartLightbox = memo(function SmartLightbox() {
const i18n = useSelector(getIntl);
const { saveAttachment } = useConversationsActions();
const {
closeLightbox,
@ -38,20 +32,15 @@ export function SmartLightbox(): JSX.Element | null {
const { toggleForwardMessagesModal } = useGlobalModalActions();
const { pauseVoiceNotePlayer } = useAudioPlayerActions();
const conversationSelector = useSelector<StateType, GetConversationByIdType>(
getConversationSelector
);
const conversationSelector = useSelector(getConversationSelector);
const isShowingLightbox = useSelector<StateType, boolean>(shouldShowLightbox);
const isViewOnce = useSelector<StateType, boolean>(getIsViewOnce);
const media = useSelector<
StateType,
ReadonlyArray<ReadonlyDeep<MediaItemType>>
>(getMedia);
const hasPrevMessage = useSelector<StateType, boolean>(getHasPrevMessage);
const hasNextMessage = useSelector<StateType, boolean>(getHasNextMessage);
const selectedIndex = useSelector<StateType, number>(getSelectedIndex);
const playbackDisabled = useSelector<StateType, boolean>(getPlaybackDisabled);
const isShowingLightbox = useSelector(shouldShowLightbox);
const isViewOnce = useSelector(getIsViewOnce);
const media = useSelector(getMedia);
const hasPrevMessage = useSelector(getHasPrevMessage);
const hasNextMessage = useSelector(getHasNextMessage);
const selectedIndex = useSelector(getSelectedIndex);
const playbackDisabled = useSelector(getPlaybackDisabled);
const onPrevAttachment = useCallback(() => {
if (selectedIndex <= 0) {
@ -107,4 +96,4 @@ export function SmartLightbox(): JSX.Element | null {
hasPrevMessage={hasPrevMessage}
/>
);
}
});

View file

@ -1,6 +1,6 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useCallback } from 'react';
import React, { memo, useCallback } from 'react';
import { useSelector } from 'react-redux';
import { MessageAudio } from '../../components/conversation/MessageAudio';
@ -26,10 +26,10 @@ export type Props = Omit<MessageAudioOwnProps, 'active' | 'onPlayMessage'> & {
renderingContext: string;
};
export function SmartMessageAudio({
export const SmartMessageAudio = memo(function SmartMessageAudio({
renderingContext,
...props
}: Props): JSX.Element | null {
}: Props) {
const active = useSelector(selectAudioPlayerActive);
const { loadVoiceNoteAudio, setIsPlaying, setPlaybackRate, setPosition } =
useAudioPlayerActions();
@ -100,4 +100,4 @@ export function SmartMessageAudio({
{...props}
/>
);
}
});

View file

@ -1,7 +1,7 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useEffect } from 'react';
import React, { memo, useEffect } from 'react';
import { useSelector } from 'react-redux';
import type { Props as MessageDetailProps } from '../../components/conversation/MessageDetail';
@ -28,89 +28,91 @@ export type OwnProps = Pick<
'contacts' | 'errors' | 'message' | 'receivedAt'
>;
export function SmartMessageDetail(): JSX.Element | null {
const getContactNameColor = useSelector(getContactNameColorSelector);
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const i18n = useSelector(getIntl);
const platform = useSelector(getPlatform);
const interactionMode = useSelector(getInteractionMode);
const messageDetails = useSelector(getMessageDetails);
const theme = useSelector(getTheme);
const { checkForAccount } = useAccountsActions();
const {
clearTargetedMessage: clearSelectedMessage,
doubleCheckMissingQuoteReference,
kickOffAttachmentDownload,
markAttachmentAsCorrupted,
messageExpanded,
openGiftBadge,
retryMessageSend,
popPanelForConversation,
pushPanelForConversation,
saveAttachment,
showConversation,
showExpiredIncomingTapToViewToast,
showExpiredOutgoingTapToViewToast,
showSpoiler,
startConversation,
} = useConversationsActions();
const { showContactModal, showEditHistoryModal, toggleSafetyNumberModal } =
useGlobalModalActions();
const { showLightbox, showLightboxForViewOnceMedia } = useLightboxActions();
const { viewStory } = useStoriesActions();
export const SmartMessageDetail = memo(
function SmartMessageDetail(): JSX.Element | null {
const getContactNameColor = useSelector(getContactNameColorSelector);
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const i18n = useSelector(getIntl);
const platform = useSelector(getPlatform);
const interactionMode = useSelector(getInteractionMode);
const messageDetails = useSelector(getMessageDetails);
const theme = useSelector(getTheme);
const { checkForAccount } = useAccountsActions();
const {
clearTargetedMessage: clearSelectedMessage,
doubleCheckMissingQuoteReference,
kickOffAttachmentDownload,
markAttachmentAsCorrupted,
messageExpanded,
openGiftBadge,
retryMessageSend,
popPanelForConversation,
pushPanelForConversation,
saveAttachment,
showConversation,
showExpiredIncomingTapToViewToast,
showExpiredOutgoingTapToViewToast,
showSpoiler,
startConversation,
} = useConversationsActions();
const { showContactModal, showEditHistoryModal, toggleSafetyNumberModal } =
useGlobalModalActions();
const { showLightbox, showLightboxForViewOnceMedia } = useLightboxActions();
const { viewStory } = useStoriesActions();
useEffect(() => {
if (!messageDetails) {
popPanelForConversation();
}
}, [messageDetails, popPanelForConversation]);
useEffect(() => {
if (!messageDetails) {
popPanelForConversation();
return null;
}
}, [messageDetails, popPanelForConversation]);
if (!messageDetails) {
return null;
const { contacts, errors, message, receivedAt } = messageDetails;
const contactNameColor =
message.conversationType === 'group'
? getContactNameColor(message.conversationId, message.author.id)
: undefined;
return (
<MessageDetail
checkForAccount={checkForAccount}
clearTargetedMessage={clearSelectedMessage}
contactNameColor={contactNameColor}
contacts={contacts}
doubleCheckMissingQuoteReference={doubleCheckMissingQuoteReference}
errors={errors}
getPreferredBadge={getPreferredBadge}
i18n={i18n}
platform={platform}
interactionMode={interactionMode}
kickOffAttachmentDownload={kickOffAttachmentDownload}
markAttachmentAsCorrupted={markAttachmentAsCorrupted}
message={message}
messageExpanded={messageExpanded}
openGiftBadge={openGiftBadge}
retryMessageSend={retryMessageSend}
pushPanelForConversation={pushPanelForConversation}
receivedAt={receivedAt}
renderAudioAttachment={renderAudioAttachment}
saveAttachment={saveAttachment}
sentAt={message.timestamp}
showContactModal={showContactModal}
showConversation={showConversation}
showEditHistoryModal={showEditHistoryModal}
showExpiredIncomingTapToViewToast={showExpiredIncomingTapToViewToast}
showExpiredOutgoingTapToViewToast={showExpiredOutgoingTapToViewToast}
showLightbox={showLightbox}
showLightboxForViewOnceMedia={showLightboxForViewOnceMedia}
showSpoiler={showSpoiler}
startConversation={startConversation}
theme={theme}
toggleSafetyNumberModal={toggleSafetyNumberModal}
viewStory={viewStory}
/>
);
}
const { contacts, errors, message, receivedAt } = messageDetails;
const contactNameColor =
message.conversationType === 'group'
? getContactNameColor(message.conversationId, message.author.id)
: undefined;
return (
<MessageDetail
checkForAccount={checkForAccount}
clearTargetedMessage={clearSelectedMessage}
contactNameColor={contactNameColor}
contacts={contacts}
doubleCheckMissingQuoteReference={doubleCheckMissingQuoteReference}
errors={errors}
getPreferredBadge={getPreferredBadge}
i18n={i18n}
platform={platform}
interactionMode={interactionMode}
kickOffAttachmentDownload={kickOffAttachmentDownload}
markAttachmentAsCorrupted={markAttachmentAsCorrupted}
message={message}
messageExpanded={messageExpanded}
openGiftBadge={openGiftBadge}
retryMessageSend={retryMessageSend}
pushPanelForConversation={pushPanelForConversation}
receivedAt={receivedAt}
renderAudioAttachment={renderAudioAttachment}
saveAttachment={saveAttachment}
sentAt={message.timestamp}
showContactModal={showContactModal}
showConversation={showConversation}
showEditHistoryModal={showEditHistoryModal}
showExpiredIncomingTapToViewToast={showExpiredIncomingTapToViewToast}
showExpiredOutgoingTapToViewToast={showExpiredOutgoingTapToViewToast}
showLightbox={showLightbox}
showLightboxForViewOnceMedia={showLightboxForViewOnceMedia}
showSpoiler={showSpoiler}
startConversation={startConversation}
theme={theme}
toggleSafetyNumberModal={toggleSafetyNumberModal}
viewStory={viewStory}
/>
);
}
);

View file

@ -1,9 +1,8 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useCallback, useMemo } from 'react';
import React, { memo, useCallback, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { getIntl } from '../selectors/user';
import { getGlobalModalsState } from '../selectors/globalModals';
import { getConversationSelector } from '../selectors/conversations';
@ -17,68 +16,70 @@ import { getAddedByForOurPendingInvitation } from '../../util/getAddedByForOurPe
import { strictAssert } from '../../util/assert';
import { useGlobalModalActions } from '../ducks/globalModals';
export function SmartMessageRequestActionsConfirmation(): JSX.Element | null {
const i18n = useSelector(getIntl);
const globalModals = useSelector(getGlobalModalsState);
const { messageRequestActionsConfirmationProps } = globalModals;
strictAssert(
messageRequestActionsConfirmationProps,
'messageRequestActionsConfirmationProps are required'
);
const { conversationId, state } = messageRequestActionsConfirmationProps;
strictAssert(state !== MessageRequestState.default, 'state is required');
const getConversation = useSelector(getConversationSelector);
const conversation = getConversation(conversationId);
const addedBy = useMemo(() => {
if (conversation.type === 'group') {
return getAddedByForOurPendingInvitation(conversation);
}
return null;
}, [conversation]);
const conversationName = useContactNameData(conversation);
strictAssert(conversationName, 'conversationName is required');
const addedByName = useContactNameData(addedBy);
const {
acceptConversation,
blockConversation,
reportSpam,
blockAndReportSpam,
deleteConversation,
} = useConversationsActions();
const { toggleMessageRequestActionsConfirmation } = useGlobalModalActions();
const handleChangeState = useCallback(
(nextState: MessageRequestState) => {
if (nextState === MessageRequestState.default) {
toggleMessageRequestActionsConfirmation(null);
} else {
toggleMessageRequestActionsConfirmation({
conversationId,
state: nextState,
});
export const SmartMessageRequestActionsConfirmation = memo(
function SmartMessageRequestActionsConfirmation() {
const i18n = useSelector(getIntl);
const globalModals = useSelector(getGlobalModalsState);
const { messageRequestActionsConfirmationProps } = globalModals;
strictAssert(
messageRequestActionsConfirmationProps,
'messageRequestActionsConfirmationProps are required'
);
const { conversationId, state } = messageRequestActionsConfirmationProps;
strictAssert(state !== MessageRequestState.default, 'state is required');
const getConversation = useSelector(getConversationSelector);
const conversation = getConversation(conversationId);
const addedBy = useMemo(() => {
if (conversation.type === 'group') {
return getAddedByForOurPendingInvitation(conversation);
}
},
[conversationId, toggleMessageRequestActionsConfirmation]
);
return null;
}, [conversation]);
return (
<MessageRequestActionsConfirmation
i18n={i18n}
conversationId={conversation.id}
conversationType={conversation.type}
conversationName={conversationName}
addedByName={addedByName}
isBlocked={conversation.isBlocked ?? false}
isReported={conversation.isReported ?? false}
acceptConversation={acceptConversation}
blockConversation={blockConversation}
reportSpam={reportSpam}
blockAndReportSpam={blockAndReportSpam}
deleteConversation={deleteConversation}
state={state}
onChangeState={handleChangeState}
/>
);
}
const conversationName = useContactNameData(conversation);
strictAssert(conversationName, 'conversationName is required');
const addedByName = useContactNameData(addedBy);
const {
acceptConversation,
blockConversation,
reportSpam,
blockAndReportSpam,
deleteConversation,
} = useConversationsActions();
const { toggleMessageRequestActionsConfirmation } = useGlobalModalActions();
const handleChangeState = useCallback(
(nextState: MessageRequestState) => {
if (nextState === MessageRequestState.default) {
toggleMessageRequestActionsConfirmation(null);
} else {
toggleMessageRequestActionsConfirmation({
conversationId,
state: nextState,
});
}
},
[conversationId, toggleMessageRequestActionsConfirmation]
);
return (
<MessageRequestActionsConfirmation
i18n={i18n}
conversationId={conversation.id}
conversationType={conversation.type}
conversationName={conversationName}
addedByName={addedByName}
isBlocked={conversation.isBlocked ?? false}
isReported={conversation.isReported ?? false}
acceptConversation={acceptConversation}
blockConversation={blockConversation}
reportSpam={reportSpam}
blockAndReportSpam={blockAndReportSpam}
deleteConversation={deleteConversation}
state={state}
onChangeState={handleChangeState}
/>
);
}
);

View file

@ -1,40 +1,51 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { CSSProperties } from 'react';
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import type { StateType } from '../reducer';
// SPDX-License-Identifier: AGPL-3.0-onlyå
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import { MessageSearchResult } from '../../components/conversationList/MessageSearchResult';
import { getPreferredBadgeSelector } from '../selectors/badges';
import { getIntl, getTheme } from '../selectors/user';
import { getMessageSearchResultSelector } from '../selectors/search';
import * as log from '../../logging/log';
import { useConversationsActions } from '../ducks/conversations';
type SmartProps = {
type SmartMessageSearchResultProps = {
id: string;
style?: CSSProperties;
};
function mapStateToProps(state: StateType, ourProps: SmartProps) {
const { id, style } = ourProps;
export const SmartMessageSearchResult = memo(function SmartMessageSearchResult({
id,
}: SmartMessageSearchResultProps) {
const i18n = useSelector(getIntl);
const theme = useSelector(getTheme);
const messageSearchResultSelector = useSelector(
getMessageSearchResultSelector
);
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const { showConversation } = useConversationsActions();
const props = getMessageSearchResultSelector(state)(id);
if (!props) {
const messageResult = messageSearchResultSelector(id);
if (messageResult == null) {
log.error('SmartMessageSearchResult: no message was found');
return null;
}
const { conversationId, snippet, body, bodyRanges, from, to, sentAt } =
messageResult;
return {
...props,
getPreferredBadge: getPreferredBadgeSelector(state),
i18n: getIntl(state),
style,
theme: getTheme(state),
};
}
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartMessageSearchResult = smart(MessageSearchResult);
return (
<MessageSearchResult
i18n={i18n}
theme={theme}
getPreferredBadge={getPreferredBadge}
id={id}
conversationId={conversationId}
snippet={snippet}
body={body}
bodyRanges={bodyRanges}
from={from}
to={to}
showConversation={showConversation}
sentAt={sentAt}
/>
);
});

View file

@ -1,7 +1,7 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useCallback } from 'react';
import React, { memo, useCallback } from 'react';
import { useSelector } from 'react-redux';
import { MiniPlayer, PlayerState } from '../../components/MiniPlayer';
import type { Props as DumbProps } from '../../components/MiniPlayer';
@ -23,7 +23,9 @@ type Props = Pick<DumbProps, 'shouldFlow'>;
* It also triggers side-effecting actions (actual playback) in response to changes in
* the state
*/
export function SmartMiniPlayer({ shouldFlow }: Props): JSX.Element | null {
export const SmartMiniPlayer = memo(function SmartMiniPlayer({
shouldFlow,
}: Props): JSX.Element | null {
const i18n = useSelector(getIntl);
const active = useSelector(selectAudioPlayerActive);
const getVoiceNoteTitle = useSelector(selectVoiceNoteTitle);
@ -66,4 +68,4 @@ export function SmartMiniPlayer({ shouldFlow }: Props): JSX.Element | null {
playbackRate={active.playbackRate}
/>
);
}
});

View file

@ -1,7 +1,7 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useCallback } from 'react';
import React, { memo, useCallback } from 'react';
import { useSelector } from 'react-redux';
import type { NavTabPanelProps } from '../../components/NavTabs';
import { NavTabs } from '../../components/NavTabs';
@ -33,7 +33,7 @@ export type SmartNavTabsProps = Readonly<{
renderStoriesTab(props: NavTabPanelProps): JSX.Element;
}>;
export function SmartNavTabs({
export const SmartNavTabs = memo(function SmartNavTabs({
navTabsCollapsed,
onToggleNavTabsCollapse,
renderCallsTab,
@ -91,4 +91,4 @@ export function SmartNavTabs({
unreadStoriesCount={unreadStoriesCount}
/>
);
}
});

View file

@ -1,23 +1,37 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import { DialogNetworkStatus } from '../../components/DialogNetworkStatus';
import type { StateType } from '../reducer';
import { getIntl } from '../selectors/user';
import type { WidthBreakpoint } from '../../components/_util';
import {
getNetworkIsOnline,
getNetworkIsOutage,
getNetworkSocketStatus,
} from '../selectors/network';
import { useUserActions } from '../ducks/user';
type PropsType = Readonly<{ containerWidthBreakpoint: WidthBreakpoint }>;
type SmartNetworkStatusProps = Readonly<{
containerWidthBreakpoint: WidthBreakpoint;
}>;
const mapStateToProps = (state: StateType, ownProps: PropsType) => {
return {
...state.network,
i18n: getIntl(state),
...ownProps,
};
};
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartNetworkStatus = smart(DialogNetworkStatus);
export const SmartNetworkStatus = memo(function SmartNetworkStatus({
containerWidthBreakpoint,
}: SmartNetworkStatusProps) {
const i18n = useSelector(getIntl);
const isOnline = useSelector(getNetworkIsOnline);
const isOutage = useSelector(getNetworkIsOutage);
const socketStatus = useSelector(getNetworkSocketStatus);
const { manualReconnect } = useUserActions();
return (
<DialogNetworkStatus
containerWidthBreakpoint={containerWidthBreakpoint}
i18n={i18n}
isOnline={isOnline}
isOutage={isOutage}
socketStatus={socketStatus}
manualReconnect={manualReconnect}
/>
);
});

View file

@ -1,12 +1,8 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import type { PropsDataType } from '../../components/conversation/conversation-details/PendingInvites';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import { PendingInvites } from '../../components/conversation/conversation-details/PendingInvites';
import type { StateType } from '../reducer';
import { getIntl, getTheme } from '../selectors/user';
import { getPreferredBadgeSelector } from '../selectors/badges';
import {
@ -16,36 +12,48 @@ import {
import { getGroupMemberships } from '../../util/getGroupMemberships';
import { assertDev } from '../../util/assert';
import type { AciString } from '../../types/ServiceId';
import { useConversationsActions } from '../ducks/conversations';
export type SmartPendingInvitesProps = {
conversationId: string;
ourAci: AciString;
};
const mapStateToProps = (
state: StateType,
props: SmartPendingInvitesProps
): PropsDataType => {
const conversationSelector = getConversationByIdSelector(state);
const conversationByServiceIdSelector =
getConversationByServiceIdSelector(state);
const conversation = conversationSelector(props.conversationId);
export const SmartPendingInvites = memo(function SmartPendingInvites({
conversationId,
ourAci,
}: SmartPendingInvitesProps) {
const i18n = useSelector(getIntl);
const theme = useSelector(getTheme);
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const conversationSelector = useSelector(getConversationByIdSelector);
const conversationByServiceIdSelector = useSelector(
getConversationByServiceIdSelector
);
const conversation = conversationSelector(conversationId);
assertDev(
conversation,
'<SmartPendingInvites> expected a conversation to be found'
);
return {
...props,
...getGroupMemberships(conversation, conversationByServiceIdSelector),
const groupMemberships = getGroupMemberships(
conversation,
getPreferredBadge: getPreferredBadgeSelector(state),
i18n: getIntl(state),
theme: getTheme(state),
};
};
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartPendingInvites = smart(PendingInvites);
conversationByServiceIdSelector
);
const {
approvePendingMembershipFromGroupV2,
revokePendingMembershipsFromGroupV2,
} = useConversationsActions();
return (
<PendingInvites
i18n={i18n}
theme={theme}
getPreferredBadge={getPreferredBadge}
conversation={conversation}
ourAci={ourAci}
pendingMemberships={groupMemberships.pendingMemberships}
pendingApprovalMemberships={groupMemberships.pendingApprovalMemberships}
approvePendingMembershipFromGroupV2={approvePendingMembershipFromGroupV2}
revokePendingMembershipsFromGroupV2={revokePendingMembershipsFromGroupV2}
/>
);
});

View file

@ -1,93 +1,130 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import type { PropsDataType as ProfileEditorModalPropsType } from '../../components/ProfileEditorModal';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import { ProfileEditorModal } from '../../components/ProfileEditorModal';
import type { PropsDataType } from '../../components/ProfileEditor';
import { SmartEditUsernameModalBody } from './EditUsernameModalBody';
import type { StateType } from '../reducer';
import { getIntl } from '../selectors/user';
import { useConversationsActions } from '../ducks/conversations';
import { useGlobalModalActions } from '../ducks/globalModals';
import { useItemsActions } from '../ducks/items';
import { useToastActions } from '../ducks/toast';
import { useUsernameActions } from '../ducks/username';
import { getMe } from '../selectors/conversations';
import { selectRecentEmojis } from '../selectors/emojis';
import {
getProfileEditorHasError,
getProfileEditorInitialEditState,
} from '../selectors/globalModals';
import {
getEmojiSkinTone,
getHasCompletedUsernameLinkOnboarding,
getUsernameCorrupted,
getUsernameLinkColor,
getUsernameLink,
getUsernameLinkColor,
getUsernameLinkCorrupted,
isInternalUser,
} from '../selectors/items';
import { getMe } from '../selectors/conversations';
import { selectRecentEmojis } from '../selectors/emojis';
import { getIntl } from '../selectors/user';
import {
getUsernameEditState,
getUsernameLinkState,
} from '../selectors/username';
import type { SmartEditUsernameModalBodyProps } from './EditUsernameModalBody';
import { SmartEditUsernameModalBody } from './EditUsernameModalBody';
function renderEditUsernameModalBody(props: {
isRootModal: boolean;
onClose: () => void;
}): JSX.Element {
function renderEditUsernameModalBody(
props: SmartEditUsernameModalBodyProps
): JSX.Element {
return <SmartEditUsernameModalBody {...props} />;
}
function mapStateToProps(
state: StateType
): Omit<PropsDataType, 'onEditStateChange' | 'onProfileChanged'> &
ProfileEditorModalPropsType {
export const SmartProfileEditorModal = memo(function SmartProfileEditorModal() {
const i18n = useSelector(getIntl);
const {
profileAvatarPath,
aboutEmoji,
aboutText,
avatars: userAvatarData = [],
aboutText,
aboutEmoji,
color,
familyName,
firstName,
familyName,
id: conversationId,
username,
} = getMe(state);
const recentEmojis = selectRecentEmojis(state);
const skinTone = getEmojiSkinTone(state);
const hasCompletedUsernameLinkOnboarding =
getHasCompletedUsernameLinkOnboarding(state);
const usernameEditState = getUsernameEditState(state);
const usernameLinkState = getUsernameLinkState(state);
const usernameLinkColor = getUsernameLinkColor(state);
const usernameLink = getUsernameLink(state);
const usernameCorrupted = getUsernameCorrupted(state);
const usernameLinkCorrupted = getUsernameLinkCorrupted(state);
return {
aboutEmoji,
aboutText,
profileAvatarPath,
color,
conversationId,
familyName,
firstName: String(firstName),
hasCompletedUsernameLinkOnboarding,
hasError: state.globalModals.profileEditorHasError,
initialEditState: state.globalModals.profileEditorInitialEditState,
i18n: getIntl(state),
recentEmojis,
skinTone,
userAvatarData,
username,
usernameCorrupted,
usernameEditState,
usernameLinkState,
usernameLinkColor,
usernameLinkCorrupted,
usernameLink,
isUsernameDeletionEnabled: isInternalUser(state),
} = useSelector(getMe);
const hasCompletedUsernameLinkOnboarding = useSelector(
getHasCompletedUsernameLinkOnboarding
);
const hasError = useSelector(getProfileEditorHasError);
const initialEditState = useSelector(getProfileEditorInitialEditState);
const isUsernameDeletionEnabled = useSelector(isInternalUser);
const recentEmojis = useSelector(selectRecentEmojis);
const skinTone = useSelector(getEmojiSkinTone);
const usernameCorrupted = useSelector(getUsernameCorrupted);
const usernameEditState = useSelector(getUsernameEditState);
const usernameLink = useSelector(getUsernameLink);
const usernameLinkColor = useSelector(getUsernameLinkColor);
const usernameLinkCorrupted = useSelector(getUsernameLinkCorrupted);
const usernameLinkState = useSelector(getUsernameLinkState);
renderEditUsernameModalBody,
};
}
const {
replaceAvatar,
saveAvatarToDisk,
saveAttachment,
deleteAvatarFromDisk,
myProfileChanged,
} = useConversationsActions();
const {
resetUsernameLink,
setUsernameLinkColor,
setUsernameEditState,
openUsernameReservationModal,
markCompletedUsernameLinkOnboarding,
deleteUsername,
} = useUsernameActions();
const { toggleProfileEditor, toggleProfileEditorHasError } =
useGlobalModalActions();
const { showToast } = useToastActions();
const { onSetSkinTone } = useItemsActions();
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartProfileEditorModal = smart(ProfileEditorModal);
return (
<ProfileEditorModal
aboutEmoji={aboutEmoji}
aboutText={aboutText}
color={color}
conversationId={conversationId}
deleteAvatarFromDisk={deleteAvatarFromDisk}
deleteUsername={deleteUsername}
familyName={familyName}
firstName={firstName ?? ''}
hasCompletedUsernameLinkOnboarding={hasCompletedUsernameLinkOnboarding}
hasError={hasError}
i18n={i18n}
initialEditState={initialEditState}
isUsernameDeletionEnabled={isUsernameDeletionEnabled}
markCompletedUsernameLinkOnboarding={markCompletedUsernameLinkOnboarding}
myProfileChanged={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}
toggleProfileEditorHasError={toggleProfileEditorHasError}
userAvatarData={userAvatarData}
username={username}
usernameCorrupted={usernameCorrupted}
usernameEditState={usernameEditState}
usernameLink={usernameLink}
usernameLinkColor={usernameLinkColor}
usernameLinkCorrupted={usernameLinkCorrupted}
usernameLinkState={usernameLinkState}
/>
);
});

View file

@ -1,16 +1,13 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import type { Ref } from 'react';
import React, { forwardRef, memo } from 'react';
import { useSelector } from 'react-redux';
import type { StateType } from '../reducer';
import { usePreferredReactionsActions } from '../ducks/preferredReactions';
import { useItemsActions } from '../ducks/items';
import { getIntl } from '../selectors/user';
import { getPreferredReactionEmoji } from '../selectors/items';
import type { LocalizerType } from '../../types/Util';
import type { Props as InternalProps } from '../../components/conversation/ReactionPicker';
import { ReactionPicker } from '../../components/conversation/ReactionPicker';
@ -24,31 +21,30 @@ type ExternalProps = Omit<
| 'skinTone'
>;
export const SmartReactionPicker = React.forwardRef<
HTMLDivElement,
ExternalProps
>(function SmartReactionPickerInner(props, ref) {
const { openCustomizePreferredReactionsModal } =
usePreferredReactionsActions();
export const SmartReactionPicker = memo(
forwardRef(function SmartReactionPickerInner(
props: ExternalProps,
ref: Ref<HTMLDivElement>
) {
const { openCustomizePreferredReactionsModal } =
usePreferredReactionsActions();
const { onSetSkinTone } = useItemsActions();
const { onSetSkinTone } = useItemsActions();
const i18n = useSelector<StateType, LocalizerType>(getIntl);
const i18n = useSelector(getIntl);
const preferredReactionEmoji = useSelector(getPreferredReactionEmoji);
const preferredReactionEmoji = useSelector<StateType, ReadonlyArray<string>>(
getPreferredReactionEmoji
);
return (
<ReactionPicker
i18n={i18n}
onSetSkinTone={onSetSkinTone}
openCustomizePreferredReactionsModal={
openCustomizePreferredReactionsModal
}
preferredReactionEmoji={preferredReactionEmoji}
ref={ref}
{...props}
/>
);
});
return (
<ReactionPicker
i18n={i18n}
onSetSkinTone={onSetSkinTone}
openCustomizePreferredReactionsModal={
openCustomizePreferredReactionsModal
}
preferredReactionEmoji={preferredReactionEmoji}
ref={ref}
{...props}
/>
);
})
);

View file

@ -1,22 +1,26 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import { DialogRelink } from '../../components/DialogRelink';
import type { StateType } from '../reducer';
import { getIntl } from '../selectors/user';
import type { WidthBreakpoint } from '../../components/_util';
import { useNetworkActions } from '../ducks/network';
type PropsType = Readonly<{ containerWidthBreakpoint: WidthBreakpoint }>;
type SmartRelinkDialogProps = Readonly<{
containerWidthBreakpoint: WidthBreakpoint;
}>;
const mapStateToProps = (state: StateType, ownProps: PropsType) => {
return {
i18n: getIntl(state),
...ownProps,
};
};
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartRelinkDialog = smart(DialogRelink);
export const SmartRelinkDialog = memo(function SmartRelinkDialog({
containerWidthBreakpoint,
}: SmartRelinkDialogProps) {
const i18n = useSelector(getIntl);
const { relinkDevice } = useNetworkActions();
return (
<DialogRelink
i18n={i18n}
containerWidthBreakpoint={containerWidthBreakpoint}
relinkDevice={relinkDevice}
/>
);
});

View file

@ -1,27 +1,39 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import { SafetyNumberModal } from '../../components/SafetyNumberModal';
import type { StateType } from '../reducer';
import { getContactSafetyNumber } from '../selectors/safetyNumber';
import { getConversationSelector } from '../selectors/conversations';
import { getIntl } from '../selectors/user';
import { useSafetyNumberActions } from '../ducks/safetyNumber';
import { useGlobalModalActions } from '../ducks/globalModals';
export type Props = {
export type SmartSafetyNumberModalProps = {
contactID: string;
};
const mapStateToProps = (state: StateType, props: Props) => {
return {
...props,
...getContactSafetyNumber(state, props),
contact: getConversationSelector(state)(props.contactID),
i18n: getIntl(state),
};
};
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartSafetyNumberModal = smart(SafetyNumberModal);
export const SmartSafetyNumberModal = memo(function SmartSafetyNumberModal({
contactID,
}: SmartSafetyNumberModalProps) {
const i18n = useSelector(getIntl);
const conversationSelector = useSelector(getConversationSelector);
const contact = conversationSelector(contactID);
const contactSafetyNumber = useSelector((state: StateType) => {
return getContactSafetyNumber(state, { contactID });
});
const { generateSafetyNumber, toggleVerified } = useSafetyNumberActions();
const { toggleSafetyNumberModal } = useGlobalModalActions();
return (
<SafetyNumberModal
i18n={i18n}
contact={contact}
safetyNumber={contactSafetyNumber.safetyNumber}
verificationDisabled={contactSafetyNumber.verificationDisabled}
toggleSafetyNumberModal={toggleSafetyNumberModal}
generateSafetyNumber={generateSafetyNumber}
toggleVerified={toggleVerified}
/>
);
});

View file

@ -1,24 +1,38 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import { SafetyNumberViewer } from '../../components/SafetyNumberViewer';
import type { StateType } from '../reducer';
import type { SafetyNumberProps } from '../../components/SafetyNumberChangeDialog';
import { getContactSafetyNumber } from '../selectors/safetyNumber';
import { getConversationSelector } from '../selectors/conversations';
import { getIntl } from '../selectors/user';
import { useSafetyNumberActions } from '../ducks/safetyNumber';
const mapStateToProps = (state: StateType, props: SafetyNumberProps) => {
return {
...props,
...getContactSafetyNumber(state, props),
contact: getConversationSelector(state)(props.contactID),
i18n: getIntl(state),
};
};
export const SmartSafetyNumberViewer = memo(function SmartSafetyNumberViewer({
contactID,
onClose,
}: SafetyNumberProps) {
const i18n = useSelector(getIntl);
const safetyNumberContact = useSelector((state: StateType) => {
return getContactSafetyNumber(state, { contactID });
});
const conversationSelector = useSelector(getConversationSelector);
const contact = conversationSelector(contactID);
const smart = connect(mapStateToProps, mapDispatchToProps);
const { generateSafetyNumber, toggleVerified } = useSafetyNumberActions();
export const SmartSafetyNumberViewer = smart(SafetyNumberViewer);
return (
<SafetyNumberViewer
contact={contact}
generateSafetyNumber={generateSafetyNumber}
i18n={i18n}
onClose={onClose}
safetyNumber={safetyNumberContact.safetyNumber}
toggleVerified={toggleVerified}
verificationDisabled={safetyNumberContact.verificationDisabled}
/>
);
});

View file

@ -1,11 +1,8 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import type { LocalizerType } from '../../types/Util';
import type { SafetyNumberChangedBlockingDataType } from '../ducks/globalModals';
import type { StateType } from '../reducer';
import * as SingleServePromise from '../../services/singleServePromise';
import {
SafetyNumberChangeDialog,
@ -18,68 +15,72 @@ import { getPreferredBadgeSelector } from '../selectors/badges';
import { useConversationsActions } from '../ducks/conversations';
import { useGlobalModalActions } from '../ducks/globalModals';
import { useStoryDistributionListsActions } from '../ducks/storyDistributionLists';
import { getSafetyNumberChangedBlockingData } from '../selectors/globalModals';
export function SmartSendAnywayDialog(): JSX.Element {
const { hideBlockingSafetyNumberChangeDialog } = useGlobalModalActions();
const { removeMembersFromDistributionList } =
useStoryDistributionListsActions();
const { cancelConversationVerification, verifyConversationsStoppingSend } =
useConversationsActions();
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const i18n = useSelector<StateType, LocalizerType>(getIntl);
const theme = useSelector(getTheme);
export const SmartSendAnywayDialog = memo(
function SmartSendAnywayDialog(): JSX.Element {
const { hideBlockingSafetyNumberChangeDialog } = useGlobalModalActions();
const { removeMembersFromDistributionList } =
useStoryDistributionListsActions();
const { cancelConversationVerification, verifyConversationsStoppingSend } =
useConversationsActions();
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const i18n = useSelector(getIntl);
const theme = useSelector(getTheme);
const contacts = useSelector(getByDistributionListConversationsStoppingSend);
const contacts = useSelector(
getByDistributionListConversationsStoppingSend
);
const safetyNumberChangedBlockingData = useSelector<
StateType,
SafetyNumberChangedBlockingDataType | undefined
>(state => state.globalModals.safetyNumberChangedBlockingData);
const safetyNumberChangedBlockingData = useSelector(
getSafetyNumberChangedBlockingData
);
const explodedPromise = safetyNumberChangedBlockingData
? SingleServePromise.get<boolean>(
safetyNumberChangedBlockingData.promiseUuid
)
: undefined;
const explodedPromise = safetyNumberChangedBlockingData
? SingleServePromise.get<boolean>(
safetyNumberChangedBlockingData.promiseUuid
)
: undefined;
let confirmText: string | undefined = i18n(
'icu:safetyNumberChangeDialog__pending-messages'
);
if (
safetyNumberChangedBlockingData?.source ===
SafetyNumberChangeSource.InitiateCall
) {
confirmText = i18n('icu:callAnyway');
} else if (
safetyNumberChangedBlockingData?.source ===
SafetyNumberChangeSource.JoinCall
) {
confirmText = i18n('icu:joinAnyway');
} else {
confirmText = undefined;
let confirmText: string | undefined = i18n(
'icu:safetyNumberChangeDialog__pending-messages'
);
if (
safetyNumberChangedBlockingData?.source ===
SafetyNumberChangeSource.InitiateCall
) {
confirmText = i18n('icu:callAnyway');
} else if (
safetyNumberChangedBlockingData?.source ===
SafetyNumberChangeSource.JoinCall
) {
confirmText = i18n('icu:joinAnyway');
} else {
confirmText = undefined;
}
return (
<SafetyNumberChangeDialog
confirmText={confirmText}
contacts={contacts}
getPreferredBadge={getPreferredBadge}
i18n={i18n}
onCancel={() => {
cancelConversationVerification();
explodedPromise?.resolve(false);
hideBlockingSafetyNumberChangeDialog();
}}
onConfirm={() => {
verifyConversationsStoppingSend();
explodedPromise?.resolve(true);
hideBlockingSafetyNumberChangeDialog();
}}
removeFromStory={removeMembersFromDistributionList}
renderSafetyNumber={({ contactID, onClose }) => (
<SmartSafetyNumberViewer contactID={contactID} onClose={onClose} />
)}
theme={theme}
/>
);
}
return (
<SafetyNumberChangeDialog
confirmText={confirmText}
contacts={contacts}
getPreferredBadge={getPreferredBadge}
i18n={i18n}
onCancel={() => {
cancelConversationVerification();
explodedPromise?.resolve(false);
hideBlockingSafetyNumberChangeDialog();
}}
onConfirm={() => {
verifyConversationsStoppingSend();
explodedPromise?.resolve(true);
hideBlockingSafetyNumberChangeDialog();
}}
removeFromStory={removeMembersFromDistributionList}
renderSafetyNumber={({ contactID, onClose }) => (
<SmartSafetyNumberViewer contactID={contactID} onClose={onClose} />
)}
theme={theme}
/>
);
}
);

View file

@ -1,11 +1,9 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import React, { memo, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { ShortcutGuideModal } from '../../components/ShortcutGuideModal';
import type { StateType } from '../reducer';
import { countStickers } from '../../components/stickers/lib';
import { getIntl, getPlatform } from '../selectors/user';
import {
@ -14,30 +12,35 @@ import {
getKnownStickerPacks,
getReceivedStickerPacks,
} from '../selectors/stickers';
import { useGlobalModalActions } from '../ducks/globalModals';
const mapStateToProps = (state: StateType) => {
const blessedPacks = getBlessedStickerPacks(state);
const installedPacks = getInstalledStickerPacks(state);
const knownPacks = getKnownStickerPacks(state);
const receivedPacks = getReceivedStickerPacks(state);
export const SmartShortcutGuideModal = memo(function SmartShortcutGuideModal() {
const i18n = useSelector(getIntl);
const blessedPacks = useSelector(getBlessedStickerPacks);
const installedPacks = useSelector(getInstalledStickerPacks);
const knownPacks = useSelector(getKnownStickerPacks);
const receivedPacks = useSelector(getReceivedStickerPacks);
const platform = useSelector(getPlatform);
const hasInstalledStickers =
countStickers({
knownPacks,
blessedPacks,
installedPacks,
receivedPacks,
}) > 0;
const { closeShortcutGuideModal } = useGlobalModalActions();
const platform = getPlatform(state);
const hasInstalledStickers = useMemo(() => {
return (
countStickers({
knownPacks,
blessedPacks,
installedPacks,
receivedPacks,
}) > 0
);
}, [blessedPacks, installedPacks, knownPacks, receivedPacks]);
return {
hasInstalledStickers,
platform,
i18n: getIntl(state),
};
};
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartShortcutGuideModal = smart(ShortcutGuideModal);
return (
<ShortcutGuideModal
hasInstalledStickers={hasInstalledStickers}
platform={platform}
closeShortcutGuideModal={closeShortcutGuideModal}
i18n={i18n}
/>
);
});

View file

@ -1,11 +1,9 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import { StickerManager } from '../../components/stickers/StickerManager';
import type { StateType } from '../reducer';
import { getIntl } from '../selectors/user';
import {
getBlessedStickerPacks,
@ -13,22 +11,31 @@ import {
getKnownStickerPacks,
getReceivedStickerPacks,
} from '../selectors/stickers';
import { useStickersActions } from '../ducks/stickers';
import { useGlobalModalActions } from '../ducks/globalModals';
const mapStateToProps = (state: StateType) => {
const blessedPacks = getBlessedStickerPacks(state);
const receivedPacks = getReceivedStickerPacks(state);
const installedPacks = getInstalledStickerPacks(state);
const knownPacks = getKnownStickerPacks(state);
export const SmartStickerManager = memo(function SmartStickerManager() {
const i18n = useSelector(getIntl);
const blessedPacks = useSelector(getBlessedStickerPacks);
const receivedPacks = useSelector(getReceivedStickerPacks);
const installedPacks = useSelector(getInstalledStickerPacks);
const knownPacks = useSelector(getKnownStickerPacks);
return {
blessedPacks,
receivedPacks,
installedPacks,
knownPacks,
i18n: getIntl(state),
};
};
const { downloadStickerPack, installStickerPack, uninstallStickerPack } =
useStickersActions();
const { closeStickerPackPreview } = useGlobalModalActions();
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartStickerManager = smart(StickerManager);
return (
<StickerManager
blessedPacks={blessedPacks}
closeStickerPackPreview={closeStickerPackPreview}
downloadStickerPack={downloadStickerPack}
i18n={i18n}
installStickerPack={installStickerPack}
installedPacks={installedPacks}
knownPacks={knownPacks}
receivedPacks={receivedPacks}
uninstallStickerPack={uninstallStickerPack}
/>
);
});

View file

@ -1,40 +1,48 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import { StickerPreviewModal } from '../../components/stickers/StickerPreviewModal';
import type { StateType } from '../reducer';
import { getIntl, getStickersPath, getTempPath } from '../selectors/user';
import {
getBlessedPacks,
getPacks,
translatePackFromDB,
} from '../selectors/stickers';
import { useStickersActions } from '../ducks/stickers';
import { useGlobalModalActions } from '../ducks/globalModals';
export type ExternalProps = {
packId: string;
};
const mapStateToProps = (state: StateType, props: ExternalProps) => {
const { packId } = props;
const stickersPath = getStickersPath(state);
const tempPath = getTempPath(state);
export const SmartStickerPreviewModal = memo(function SmartStickerPreviewModal({
packId,
}: ExternalProps) {
const i18n = useSelector(getIntl);
const packs = useSelector(getPacks);
const blessedPacks = useSelector(getBlessedPacks);
const stickersPath = useSelector(getStickersPath);
const tempPath = useSelector(getTempPath);
const packs = getPacks(state);
const blessedPacks = getBlessedPacks(state);
const pack = packs[packId];
const { downloadStickerPack, installStickerPack, uninstallStickerPack } =
useStickersActions();
const { closeStickerPackPreview } = useGlobalModalActions();
return {
...props,
pack: pack
? translatePackFromDB(pack, packs, blessedPacks, stickersPath, tempPath)
: undefined,
i18n: getIntl(state),
};
};
const packDb = packs[packId];
const pack = packDb
? translatePackFromDB(packDb, packs, blessedPacks, stickersPath, tempPath)
: undefined;
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartStickerPreviewModal = smart(StickerPreviewModal);
return (
<StickerPreviewModal
closeStickerPackPreview={closeStickerPackPreview}
downloadStickerPack={downloadStickerPack}
i18n={i18n}
installStickerPack={installStickerPack}
pack={pack}
uninstallStickerPack={uninstallStickerPack}
/>
);
});

View file

@ -1,11 +1,8 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import type { LocalizerType } from '../../types/Util';
import type { StateType } from '../reducer';
import { StoriesSettingsModal } from '../../components/StoriesSettingsModal';
import {
getAllSignalConnections,
@ -23,59 +20,60 @@ import { useStoryDistributionListsActions } from '../ducks/storyDistributionList
import { useStoriesActions } from '../ducks/stories';
import { useConversationsActions } from '../ducks/conversations';
export function SmartStoriesSettingsModal(): JSX.Element | null {
const { setStoriesDisabled } = useStoriesActions();
const { hideStoriesSettings, toggleSignalConnectionsModal } =
useGlobalModalActions();
const {
allowsRepliesChanged,
createDistributionList,
deleteDistributionList,
hideMyStoriesFrom,
removeMembersFromDistributionList,
setMyStoriesToAllSignalConnections,
updateStoryViewers,
} = useStoryDistributionListsActions();
const { toggleGroupsForStorySend } = useConversationsActions();
const signalConnections = useSelector(getAllSignalConnections);
export const SmartStoriesSettingsModal = memo(
function SmartStoriesSettingsModal() {
const { setStoriesDisabled } = useStoriesActions();
const { hideStoriesSettings, toggleSignalConnectionsModal } =
useGlobalModalActions();
const {
allowsRepliesChanged,
createDistributionList,
deleteDistributionList,
hideMyStoriesFrom,
removeMembersFromDistributionList,
setMyStoriesToAllSignalConnections,
updateStoryViewers,
} = useStoryDistributionListsActions();
const { toggleGroupsForStorySend } = useConversationsActions();
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const storyViewReceiptsEnabled = useSelector(getHasStoryViewReceiptSetting);
const i18n = useSelector<StateType, LocalizerType>(getIntl);
const me = useSelector(getMe);
const signalConnections = useSelector(getAllSignalConnections);
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const storyViewReceiptsEnabled = useSelector(getHasStoryViewReceiptSetting);
const i18n = useSelector(getIntl);
const me = useSelector(getMe);
const candidateConversations = useSelector(getCandidateContactsForNewGroup);
const distributionLists = useSelector(getDistributionListsWithMembers);
const groupStories = useSelector(getGroupStories);
const candidateConversations = useSelector(getCandidateContactsForNewGroup);
const distributionLists = useSelector(getDistributionListsWithMembers);
const groupStories = useSelector(getGroupStories);
const getConversationByServiceId = useSelector(
getConversationByServiceIdSelector
);
const theme = useSelector(getTheme);
const getConversationByServiceId = useSelector(
getConversationByServiceIdSelector
);
const theme = useSelector(getTheme);
return (
<StoriesSettingsModal
candidateConversations={candidateConversations}
distributionLists={distributionLists}
groupStories={groupStories}
signalConnections={signalConnections}
hideStoriesSettings={hideStoriesSettings}
getPreferredBadge={getPreferredBadge}
i18n={i18n}
me={me}
getConversationByServiceId={getConversationByServiceId}
onDeleteList={deleteDistributionList}
toggleGroupsForStorySend={toggleGroupsForStorySend}
onDistributionListCreated={createDistributionList}
onHideMyStoriesFrom={hideMyStoriesFrom}
onRemoveMembers={removeMembersFromDistributionList}
onRepliesNReactionsChanged={allowsRepliesChanged}
onViewersUpdated={updateStoryViewers}
setMyStoriesToAllSignalConnections={setMyStoriesToAllSignalConnections}
storyViewReceiptsEnabled={storyViewReceiptsEnabled}
theme={theme}
toggleSignalConnectionsModal={toggleSignalConnectionsModal}
setStoriesDisabled={setStoriesDisabled}
/>
);
}
return (
<StoriesSettingsModal
candidateConversations={candidateConversations}
distributionLists={distributionLists}
groupStories={groupStories}
signalConnections={signalConnections}
hideStoriesSettings={hideStoriesSettings}
getPreferredBadge={getPreferredBadge}
i18n={i18n}
me={me}
getConversationByServiceId={getConversationByServiceId}
onDeleteList={deleteDistributionList}
toggleGroupsForStorySend={toggleGroupsForStorySend}
onDistributionListCreated={createDistributionList}
onHideMyStoriesFrom={hideMyStoriesFrom}
onRemoveMembers={removeMembersFromDistributionList}
onRepliesNReactionsChanged={allowsRepliesChanged}
onViewersUpdated={updateStoryViewers}
setMyStoriesToAllSignalConnections={setMyStoriesToAllSignalConnections}
storyViewReceiptsEnabled={storyViewReceiptsEnabled}
theme={theme}
toggleSignalConnectionsModal={toggleSignalConnectionsModal}
setStoriesDisabled={setStoriesDisabled}
/>
);
}
);

View file

@ -1,11 +1,8 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useEffect } from 'react';
import React, { memo, useEffect } from 'react';
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';
@ -35,6 +32,7 @@ import { useAudioPlayerActions } from '../ducks/audioPlayer';
import { useItemsActions } from '../ducks/items';
import { getHasPendingUpdate } from '../selectors/updates';
import { getOtherTabsUnreadStats } from '../selectors/nav';
import { getIsStoriesSettingsVisible } from '../selectors/globalModals';
function renderStoryCreator(): JSX.Element {
return <SmartStoryCreator />;
@ -46,7 +44,7 @@ function renderToastManager(props: {
return <SmartToastManager disableMegaphone {...props} />;
}
export function SmartStoriesTab(): JSX.Element | null {
export const SmartStoriesTab = memo(function SmartStoriesTab() {
const storiesActions = useStoriesActions();
const {
retryMessageSend,
@ -58,30 +56,20 @@ export function SmartStoriesTab(): JSX.Element | null {
useGlobalModalActions();
const { showToast } = useToastActions();
const i18n = useSelector<StateType, LocalizerType>(getIntl);
const preferredWidthFromStorage = useSelector<StateType, number>(
getPreferredLeftPaneWidth
);
const i18n = useSelector(getIntl);
const preferredWidthFromStorage = useSelector(getPreferredLeftPaneWidth);
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const addStoryData = useSelector(getAddStoryData);
const { hiddenStories, myStories, stories } = useSelector(getStories);
const me = useSelector(getMe);
const selectedStoryData = useSelector(getSelectedStoryData);
const isStoriesSettingsVisible = useSelector(
(state: StateType) => state.globalModals.isStoriesSettingsVisible
);
const isStoriesSettingsVisible = useSelector(getIsStoriesSettingsVisible);
const hasViewReceiptSetting = useSelector(getHasStoryViewReceiptSetting);
const hasPendingUpdate = useSelector(getHasPendingUpdate);
const hasFailedStorySends = useSelector(getHasAnyFailedStorySends);
const otherTabsUnreadStats = useSelector(getOtherTabsUnreadStats);
const remoteConfig = useSelector(getRemoteConfig);
const maxAttachmentSizeInKb = getMaximumOutgoingAttachmentSizeInKb(
(name: ConfigKeyType) => {
const value = remoteConfig[name]?.value;
@ -145,4 +133,4 @@ export function SmartStoriesTab(): JSX.Element | null {
{...storiesActions}
/>
);
}
});

View file

@ -1,11 +1,9 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import { ThemeType, type LocalizerType } from '../../types/Util';
import type { StateType } from '../reducer';
import { ThemeType } from '../../types/Util';
import { LinkPreviewSourceType } from '../../types/LinkPreview';
import { StoryCreator } from '../../components/StoryCreator';
import {
@ -48,7 +46,7 @@ export type PropsType = {
onClose: () => unknown;
};
export function SmartStoryCreator(): JSX.Element | null {
export const SmartStoryCreator = memo(function SmartStoryCreator() {
const { debouncedMaybeGrabLinkPreview } = useLinkPreviewActions();
const {
sendStoryModalOpenStateChanged,
@ -75,7 +73,7 @@ export function SmartStoryCreator(): JSX.Element | null {
const groupConversations = useSelector(getNonGroupStories);
const groupStories = useSelector(getGroupStories);
const hasSetMyStoriesPrivacy = useSelector(getHasSetMyStoriesPrivacy);
const i18n = useSelector<StateType, LocalizerType>(getIntl);
const i18n = useSelector(getIntl);
const installedPacks = useSelector(getInstalledStickerPacks);
const linkPreviewForSource = useSelector(getLinkPreview);
const me = useSelector(getMe);
@ -96,7 +94,7 @@ export function SmartStoryCreator(): JSX.Element | null {
}
const recentEmojis = useRecentEmojis();
const skinTone = useSelector<StateType, number>(getEmojiSkinTone);
const skinTone = useSelector(getEmojiSkinTone);
const { onSetSkinTone } = useItemsActions();
const { onUseEmoji } = useEmojisActions();
const { pauseVoiceNotePlayer } = useAudioPlayerActions();
@ -155,4 +153,4 @@ export function SmartStoryCreator(): JSX.Element | null {
toggleSignalConnectionsModal={toggleSignalConnectionsModal}
/>
);
}
});

View file

@ -1,13 +1,8 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import type { GetConversationByIdType } from '../selectors/conversations';
import type { LocalizerType } from '../../types/Util';
import type { StateType } from '../reducer';
import type { SelectedStoryDataType } from '../ducks/stories';
import { StoryViewer } from '../../components/StoryViewer';
import { ToastType } from '../../types/Toast';
import { useToastActions } from '../ducks/toast';
@ -41,7 +36,7 @@ import { useGlobalModalActions } from '../ducks/globalModals';
import { useStoriesActions } from '../ducks/stories';
import { useIsWindowActive } from '../../hooks/useIsWindowActive';
export function SmartStoryViewer(): JSX.Element | null {
export const SmartStoryViewer = memo(function SmartStoryViewer() {
const storiesActions = useStoriesActions();
const { onUseEmoji } = useEmojisActions();
const {
@ -56,40 +51,24 @@ export function SmartStoryViewer(): JSX.Element | null {
const isWindowActive = useIsWindowActive();
const i18n = useSelector<StateType, LocalizerType>(getIntl);
const i18n = useSelector(getIntl);
const platform = useSelector(getPlatform);
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const preferredReactionEmoji = useSelector<StateType, ReadonlyArray<string>>(
getPreferredReactionEmoji
);
const selectedStoryData = useSelector<
StateType,
SelectedStoryDataType | undefined
>(getSelectedStoryData);
const internalUser = useSelector<StateType, boolean>(isInternalUser);
const preferredReactionEmoji = useSelector(getPreferredReactionEmoji);
const selectedStoryData = useSelector(getSelectedStoryData);
const internalUser = useSelector(isInternalUser);
strictAssert(selectedStoryData, 'StoryViewer: !selectedStoryData');
const conversationSelector = useSelector<StateType, GetConversationByIdType>(
getConversationSelector
);
const conversationSelector = useSelector(getConversationSelector);
const getStoryById = useSelector(getStoryByIdSelector);
const recentEmojis = useRecentEmojis();
const skinTone = useSelector<StateType, number>(getEmojiSkinTone);
const skinTone = useSelector(getEmojiSkinTone);
const replyState = useSelector(getStoryReplies);
const hasAllStoriesUnmuted = useSelector<StateType, boolean>(
getHasAllStoriesUnmuted
);
const hasAllStoriesUnmuted = useSelector(getHasAllStoriesUnmuted);
const hasActiveCall = useSelector(isInFullScreenCall);
const hasViewReceiptSetting = useSelector<StateType, boolean>(
getHasStoryViewReceiptSetting
);
const hasViewReceiptSetting = useSelector(getHasStoryViewReceiptSetting);
const isFormattingEnabled = useSelector(getTextFormattingEnabled);
const { pauseVoiceNotePlayer } = useAudioPlayerActions();
@ -161,4 +140,4 @@ export function SmartStoryViewer(): JSX.Element | null {
{...storiesActions}
/>
);
}
});

View file

@ -1,46 +1,47 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { isEmpty, pick } from 'lodash';
import React from 'react';
import { connect } from 'react-redux';
import { isEmpty } from 'lodash';
import React, { memo, useCallback } from 'react';
import { useSelector } from 'react-redux';
import type { ReadonlyDeep } from 'type-fest';
import { mapDispatchToProps } from '../actions';
import type { WarningType as TimelineWarningType } from '../../components/conversation/Timeline';
import { Timeline } from '../../components/conversation/Timeline';
import type { StateType } from '../reducer';
import type { ConversationType } from '../ducks/conversations';
import { getIntl, getTheme } from '../selectors/user';
import {
getMessages,
getConversationByServiceIdSelector,
getConversationMessagesSelector,
getConversationSelector,
getInvitedContactsForNewlyCreatedGroup,
getSafeConversationWithSameTitle,
getTargetedMessage,
} from '../selectors/conversations';
import { selectAudioPlayerActive } from '../selectors/audioPlayer';
import { SmartTimelineItem, type SmartTimelineItemProps } from './TimelineItem';
import { SmartCollidingAvatars } from './CollidingAvatars';
import type { PropsType as SmartCollidingAvatarsPropsType } from './CollidingAvatars';
import { SmartContactSpoofingReviewDialog } from './ContactSpoofingReviewDialog';
import type { PropsType as SmartContactSpoofingReviewDialogPropsType } from './ContactSpoofingReviewDialog';
import { SmartTypingBubble } from './TypingBubble';
import { SmartHeroRow } from './HeroRow';
import { missingCaseError } from '../../util/missingCaseError';
import { ContactSpoofingType } from '../../util/contactSpoofing';
import { getGroupMemberships } from '../../util/getGroupMemberships';
import {
dehydrateCollisionsWithConversations,
getCollisionsFromMemberships,
} from '../../util/groupMemberNameCollisions';
import { ContactSpoofingType } from '../../util/contactSpoofing';
import { missingCaseError } from '../../util/missingCaseError';
import { useCallingActions } from '../ducks/calling';
import {
useConversationsActions,
type ConversationType,
} from '../ducks/conversations';
import type { StateType } from '../reducer';
import { selectAudioPlayerActive } from '../selectors/audioPlayer';
import { getPreferredBadgeSelector } from '../selectors/badges';
import {
getConversationByServiceIdSelector,
getConversationMessagesSelector,
getConversationSelector,
getHasContactSpoofingReview,
getInvitedContactsForNewlyCreatedGroup,
getMessages,
getSafeConversationWithSameTitle,
getSelectedConversationId,
getTargetedMessage,
} from '../selectors/conversations';
import { getIntl, getTheme } from '../selectors/user';
import type { PropsType as SmartCollidingAvatarsPropsType } from './CollidingAvatars';
import { SmartCollidingAvatars } from './CollidingAvatars';
import type { PropsType as SmartContactSpoofingReviewDialogPropsType } from './ContactSpoofingReviewDialog';
import { SmartContactSpoofingReviewDialog } from './ContactSpoofingReviewDialog';
import { SmartHeroRow } from './HeroRow';
import { SmartMiniPlayer } from './MiniPlayer';
import { SmartTimelineItem, type SmartTimelineItemProps } from './TimelineItem';
import { SmartTypingBubble } from './TypingBubble';
type ExternalProps = {
id: string;
@ -144,60 +145,145 @@ const getWarning = (
}
};
const mapStateToProps = (state: StateType, props: ExternalProps) => {
const { id } = props;
export const SmartTimeline = memo(function SmartTimeline({
id,
}: ExternalProps) {
const activeAudioPlayer = useSelector(selectAudioPlayerActive);
const conversationMessagesSelector = useSelector(
getConversationMessagesSelector
);
const conversationSelector = useSelector(getConversationSelector);
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const hasContactSpoofingReview = useSelector(getHasContactSpoofingReview);
const i18n = useSelector(getIntl);
const invitedContactsForNewlyCreatedGroup = useSelector(
getInvitedContactsForNewlyCreatedGroup
);
const messages = useSelector(getMessages);
const selectedConversationId = useSelector(getSelectedConversationId);
const targetedMessage = useSelector(getTargetedMessage);
const theme = useSelector(getTheme);
const conversation = getConversationSelector(state)(id);
const conversation = conversationSelector(id);
const conversationMessages = conversationMessagesSelector(id);
const conversationMessages = getConversationMessagesSelector(state)(id);
const targetedMessage = getTargetedMessage(state);
const warning = useSelector(
useCallback(
(state: StateType) => {
return getWarning(conversation, state);
},
[conversation]
)
);
const getTimestampForMessage = (messageId: string): undefined | number =>
getMessages(state)[messageId]?.timestamp;
const {
acknowledgeGroupMemberNameCollisions,
clearInvitedServiceIdsForNewlyCreatedGroup,
clearTargetedMessage,
closeContactSpoofingReview,
discardMessages,
loadNewerMessages,
loadNewestMessages,
loadOlderMessages,
markMessageRead,
reviewConversationNameCollision,
scrollToOldestUnreadMention,
setIsNearBottom,
targetMessage,
} = useConversationsActions();
const { peekGroupCallForTheFirstTime, peekGroupCallIfItHasMembers } =
useCallingActions();
const shouldShowMiniPlayer = Boolean(selectAudioPlayerActive(state));
const getTimestampForMessage = useCallback(
(messageId: string): undefined | number => {
return messages[messageId]?.timestamp;
},
[messages]
);
return {
id,
...pick(conversation, [
'unreadCount',
'unreadMentionsCount',
'isGroupV1AndDisabled',
'typingContactIdTimestamps',
]),
isBlocked: conversation.isBlocked ?? false,
isConversationSelected: state.conversations.selectedConversationId === id,
isIncomingMessageRequest: Boolean(
!conversation.acceptedMessageRequest &&
conversation.removalStage !== 'justNotification'
),
isSomeoneTyping: Boolean(
Object.keys(conversation.typingContactIdTimestamps ?? {}).length > 0
),
...conversationMessages,
const shouldShowMiniPlayer = activeAudioPlayer != null;
const {
acceptedMessageRequest,
isBlocked = false,
isGroupV1AndDisabled,
removalStage,
typingContactIdTimestamps = {},
unreadCount,
unreadMentionsCount,
} = conversation ?? {};
const {
haveNewest,
haveOldest,
isNearBottom,
items,
messageChangeCounter,
messageLoadingState,
oldestUnseenIndex,
scrollToIndex,
scrollToIndexCounter,
totalUnseen,
} = conversationMessages;
invitedContactsForNewlyCreatedGroup:
getInvitedContactsForNewlyCreatedGroup(state),
targetedMessageId: targetedMessage ? targetedMessage.id : undefined,
shouldShowMiniPlayer,
const isConversationSelected = selectedConversationId === id;
const isIncomingMessageRequest =
!acceptedMessageRequest && removalStage !== 'justNotification';
const isSomeoneTyping = Object.keys(typingContactIdTimestamps).length > 0;
const targetedMessageId = targetedMessage?.id;
warning: getWarning(conversation, state),
hasContactSpoofingReview: state.conversations.hasContactSpoofingReview,
getTimestampForMessage,
getPreferredBadge: getPreferredBadgeSelector(state),
i18n: getIntl(state),
theme: getTheme(state),
renderCollidingAvatars,
renderContactSpoofingReviewDialog,
renderHeroRow,
renderItem,
renderMiniPlayer,
renderTypingBubble,
};
};
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartTimeline = smart(Timeline);
return (
<Timeline
acknowledgeGroupMemberNameCollisions={
acknowledgeGroupMemberNameCollisions
}
clearInvitedServiceIdsForNewlyCreatedGroup={
clearInvitedServiceIdsForNewlyCreatedGroup
}
clearTargetedMessage={clearTargetedMessage}
closeContactSpoofingReview={closeContactSpoofingReview}
discardMessages={discardMessages}
getPreferredBadge={getPreferredBadge}
getTimestampForMessage={getTimestampForMessage}
hasContactSpoofingReview={hasContactSpoofingReview}
haveNewest={haveNewest}
haveOldest={haveOldest}
i18n={i18n}
id={id}
invitedContactsForNewlyCreatedGroup={invitedContactsForNewlyCreatedGroup}
isBlocked={isBlocked}
isConversationSelected={isConversationSelected}
isGroupV1AndDisabled={isGroupV1AndDisabled}
isIncomingMessageRequest={isIncomingMessageRequest}
isNearBottom={isNearBottom}
isSomeoneTyping={isSomeoneTyping}
items={items}
loadNewerMessages={loadNewerMessages}
loadNewestMessages={loadNewestMessages}
loadOlderMessages={loadOlderMessages}
markMessageRead={markMessageRead}
messageChangeCounter={messageChangeCounter}
messageLoadingState={messageLoadingState}
oldestUnseenIndex={oldestUnseenIndex}
peekGroupCallForTheFirstTime={peekGroupCallForTheFirstTime}
peekGroupCallIfItHasMembers={peekGroupCallIfItHasMembers}
renderCollidingAvatars={renderCollidingAvatars}
renderContactSpoofingReviewDialog={renderContactSpoofingReviewDialog}
renderHeroRow={renderHeroRow}
renderItem={renderItem}
renderMiniPlayer={renderMiniPlayer}
renderTypingBubble={renderTypingBubble}
reviewConversationNameCollision={reviewConversationNameCollision}
scrollToIndex={scrollToIndex}
scrollToIndexCounter={scrollToIndexCounter}
scrollToOldestUnreadMention={scrollToOldestUnreadMention}
setIsNearBottom={setIsNearBottom}
shouldShowMiniPlayer={shouldShowMiniPlayer}
targetedMessageId={targetedMessageId}
targetMessage={targetMessage}
theme={theme}
totalUnseen={totalUnseen}
unreadCount={unreadCount}
unreadMentionsCount={unreadMentionsCount}
warning={warning}
/>
);
});

View file

@ -2,7 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
import type { RefObject } from 'react';
import React, { useCallback } from 'react';
import React, { useCallback, memo } from 'react';
import { useSelector } from 'react-redux';
import { TimelineItem } from '../../components/conversation/TimelineItem';
@ -56,7 +56,9 @@ function renderContact(contactId: string): JSX.Element {
function renderUniversalTimerNotification(): JSX.Element {
return <SmartUniversalTimerNotification />;
}
export function SmartTimelineItem(props: SmartTimelineItemProps): JSX.Element {
export const SmartTimelineItem = memo(function SmartTimelineItem(
props: SmartTimelineItemProps
): JSX.Element {
const {
containerElementRef,
containerWidthBreakpoint,
@ -224,4 +226,4 @@ export function SmartTimelineItem(props: SmartTimelineItemProps): JSX.Element {
toggleSelectMessage={toggleSelectMessage}
/>
);
}
});

Some files were not shown because too many files have changed in this diff Show more