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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -29,7 +29,7 @@ export type OwnProps = {
export type Props = OwnProps; export type Props = OwnProps;
function renderBody({ pack, i18n }: Props) { function renderBody({ pack, i18n }: Pick<Props, 'i18n' | 'pack'>) {
if (!pack) { if (!pack) {
return null; return null;
} }
@ -73,10 +73,8 @@ function renderBody({ pack, i18n }: Props) {
); );
} }
export const StickerPreviewModal = React.memo(function StickerPreviewModalInner( export const StickerPreviewModal = React.memo(
props: Props function StickerPreviewModalInner({
) {
const {
closeStickerPackPreview, closeStickerPackPreview,
downloadStickerPack, downloadStickerPack,
i18n, i18n,
@ -84,7 +82,7 @@ export const StickerPreviewModal = React.memo(function StickerPreviewModalInner(
onClose, onClose,
pack, pack,
uninstallStickerPack, uninstallStickerPack,
} = props; }: Props) {
const [confirmingUninstall, setConfirmingUninstall] = React.useState(false); const [confirmingUninstall, setConfirmingUninstall] = React.useState(false);
// Restore focus on teardown // Restore focus on teardown
@ -218,8 +216,9 @@ export const StickerPreviewModal = React.memo(function StickerPreviewModalInner(
onClose={handleClose} onClose={handleClose}
title={i18n('icu:stickers--StickerPreview--Title')} title={i18n('icu:stickers--StickerPreview--Title')}
> >
{renderBody(props)} {renderBody({ pack, i18n })}
</Modal> </Modal>
</> </>
); );
}); }
);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -9,12 +9,12 @@ import { Provider } from 'react-redux';
import type { Store } from 'redux'; import type { Store } from 'redux';
import { ModalHost } from '../../components/ModalHost'; import { ModalHost } from '../../components/ModalHost';
import type { PropsType } from '../smart/GroupV2JoinDialog'; import type { SmartGroupV2JoinDialogProps } from '../smart/GroupV2JoinDialog';
import { SmartGroupV2JoinDialog } from '../smart/GroupV2JoinDialog'; import { SmartGroupV2JoinDialog } from '../smart/GroupV2JoinDialog';
export const createGroupV2JoinModal = ( export const createGroupV2JoinModal = (
store: Store, store: Store,
props: PropsType props: SmartGroupV2JoinDialogProps
): React.ReactElement => { ): React.ReactElement => {
const { onClose } = props; 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; 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( export const getActiveCallState = createSelector(
getCalling, getCalling,
(state: CallingStateType) => state.activeCallState (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 { function isTrusted(conversation: ConversationType): boolean {
if (conversation.type === 'group') { if (conversation.type === 'group') {
return true; return true;
@ -986,10 +993,10 @@ export function _conversationMessagesSelector(
conversation: ConversationMessageType conversation: ConversationMessageType
): TimelinePropsType { ): TimelinePropsType {
const { const {
isNearBottom, isNearBottom = null,
messageChangeCounter, messageChangeCounter,
messageIds, messageIds,
messageLoadingState, messageLoadingState = null,
metrics, metrics,
scrollToMessageCounter, scrollToMessageCounter,
scrollToMessageId, scrollToMessageId,
@ -1009,10 +1016,10 @@ export function _conversationMessagesSelector(
const oldestUnseenIndex = oldestUnseen const oldestUnseenIndex = oldestUnseen
? messageIds.findIndex(id => id === oldestUnseen.id) ? messageIds.findIndex(id => id === oldestUnseen.id)
: undefined; : null;
const scrollToIndex = scrollToMessageId const scrollToIndex = scrollToMessageId
? messageIds.findIndex(id => id === scrollToMessageId) ? messageIds.findIndex(id => id === scrollToMessageId)
: undefined; : null;
const { totalUnseen } = metrics; const { totalUnseen } = metrics;
return { return {
@ -1025,9 +1032,9 @@ export function _conversationMessagesSelector(
oldestUnseenIndex: oldestUnseenIndex:
isNumber(oldestUnseenIndex) && oldestUnseenIndex >= 0 isNumber(oldestUnseenIndex) && oldestUnseenIndex >= 0
? oldestUnseenIndex ? oldestUnseenIndex
: undefined, : null,
scrollToIndex: scrollToIndex:
isNumber(scrollToIndex) && scrollToIndex >= 0 ? scrollToIndex : undefined, isNumber(scrollToIndex) && scrollToIndex >= 0 ? scrollToIndex : null,
scrollToIndexCounter: scrollToMessageCounter, scrollToIndexCounter: scrollToMessageCounter,
totalUnseen, totalUnseen,
}; };
@ -1065,6 +1072,9 @@ export const getConversationMessagesSelector = createSelector(
scrollToIndexCounter: 0, scrollToIndexCounter: 0,
totalUnseen: 0, totalUnseen: 0,
items: [], 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); 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; 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( export const hasNetworkDialog = createSelector(
getNetwork, getNetwork,
isDone, isDone,
@ -31,6 +46,11 @@ export const hasNetworkDialog = createSelector(
socketStatus === SocketStatus.CLOSING) socketStatus === SocketStatus.CLOSING)
); );
export const getChallengeStatus = createSelector(
getNetwork,
({ challengeStatus }) => challengeStatus
);
export const isChallengePending = createSelector( export const isChallengePending = createSelector(
getNetwork, getNetwork,
({ challengeStatus }) => challengeStatus === 'pending' ({ 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 => export const getUpdatesState = (state: Readonly<StateType>): UpdatesStateType =>
state.updates; 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( export const isUpdateDialogVisible = createSelector(
getUpdatesState, getUpdatesState,
({ dialogType, didSnooze }) => { ({ dialogType, didSnooze }) => {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,53 +1,91 @@
// Copyright 2020 Signal Messenger, LLC // Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React, { memo, useCallback } from 'react';
import { connect } from 'react-redux'; import { useSelector } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import type { PropsDataType } from '../../components/ChatColorPicker';
import { ChatColorPicker } from '../../components/ChatColorPicker'; import { ChatColorPicker } from '../../components/ChatColorPicker';
import type { StateType } from '../reducer';
import { import {
getConversationSelector, getConversationSelector,
getConversationsWithCustomColorSelector, getConversationsWithCustomColorSelector,
} from '../selectors/conversations'; } from '../selectors/conversations';
import { getIntl } from '../selectors/user'; import { getIntl } from '../selectors/user';
import { getDefaultConversationColor } from '../selectors/items'; import {
getCustomColors,
getDefaultConversationColor,
} from '../selectors/items';
import { getConversationColorAttributes } from '../../util/getConversationColorAttributes'; 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; conversationId?: string;
}; }>;
const mapStateToProps = ( export const SmartChatColorPicker = memo(function SmartChatColorPicker({
state: StateType, conversationId,
props: SmartChatColorPickerProps }: SmartChatColorPickerProps) {
): PropsDataType => { const i18n = useSelector(getIntl);
const conversation = props.conversationId const customColors = useSelector(getCustomColors) ?? {};
? getConversationSelector(state)(props.conversationId) 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( const colorValues = getConversationColorAttributes(
conversation, conversation,
defaultConversationColor defaultConversationColor
); );
const { customColors } = state.items; const selectedColor = colorValues.conversationColor;
const selectedCustomColor = {
return {
...props,
customColors: customColors ? customColors.colors : {},
getConversationsWithCustomColor: (colorId: string) =>
Promise.resolve(getConversationsWithCustomColorSelector(state)(colorId)),
i18n: getIntl(state),
selectedColor: colorValues.conversationColor,
selectedCustomColor: {
id: colorValues.customColorId, id: colorValues.customColorId,
value: colorValues.customColor, value: colorValues.customColor,
};
const getConversationsWithCustomColor = useCallback(
async (colorId: string): Promise<Array<ConversationType>> => {
return conversationWithCustomColorSelector(colorId);
}, },
}; [conversationWithCustomColorSelector]
}; );
const smart = connect(mapStateToProps, mapDispatchToProps); return (
<ChatColorPicker
export const SmartChatColorPicker = smart(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 // Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React, { memo, useEffect, useRef } from 'react';
import React, { useEffect, useRef } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { ChatsTab } from '../../components/ChatsTab'; import { ChatsTab } from '../../components/ChatsTab';
import { SmartConversationView } from './ConversationView'; import { SmartConversationView } from './ConversationView';
@ -12,7 +11,6 @@ import { useGlobalModalActions } from '../ducks/globalModals';
import { getIntl } from '../selectors/user'; import { getIntl } from '../selectors/user';
import { usePrevious } from '../../hooks/usePrevious'; import { usePrevious } from '../../hooks/usePrevious';
import { TargetedMessageSource } from '../ducks/conversationsEnums'; import { TargetedMessageSource } from '../ducks/conversationsEnums';
import type { ConversationsStateType } from '../ducks/conversations';
import { useConversationsActions } from '../ducks/conversations'; import { useConversationsActions } from '../ducks/conversations';
import { useToastActions } from '../ducks/toast'; import { useToastActions } from '../ducks/toast';
import type { StateType } from '../reducer'; import type { StateType } from '../reducer';
@ -36,7 +34,7 @@ function renderMiniPlayer(options: { shouldFlow: boolean }) {
return <SmartMiniPlayer {...options} />; return <SmartMiniPlayer {...options} />;
} }
export function SmartChatsTab(): JSX.Element { export const SmartChatsTab = memo(function SmartChatsTab() {
const i18n = useSelector(getIntl); const i18n = useSelector(getIntl);
const navTabsCollapsed = useSelector(getNavTabsCollapsed); const navTabsCollapsed = useSelector(getNavTabsCollapsed);
const hasFailedStorySends = useSelector(getHasAnyFailedStorySends); const hasFailedStorySends = useSelector(getHasAnyFailedStorySends);
@ -44,9 +42,7 @@ export function SmartChatsTab(): JSX.Element {
const otherTabsUnreadStats = useSelector(getOtherTabsUnreadStats); const otherTabsUnreadStats = useSelector(getOtherTabsUnreadStats);
const { selectedConversationId, targetedMessage, targetedMessageSource } = const { selectedConversationId, targetedMessage, targetedMessageSource } =
useSelector<StateType, ConversationsStateType>( useSelector((state: StateType) => state.conversations);
state => state.conversations
);
const { const {
onConversationClosed, onConversationClosed,
@ -73,13 +69,7 @@ export function SmartChatsTab(): JSX.Element {
) { ) {
scrollToMessage(selectedConversationId, targetedMessage); scrollToMessage(selectedConversationId, targetedMessage);
} }
}, [ }, [onConversationOpened, selectedConversationId, scrollToMessage, targetedMessage, targetedMessageSource]);
onConversationOpened,
selectedConversationId,
scrollToMessage,
targetedMessage,
targetedMessageSource,
]);
const prevConversationId = usePrevious( const prevConversationId = usePrevious(
selectedConversationId, selectedConversationId,
@ -157,4 +147,4 @@ export function SmartChatsTab(): JSX.Element {
showWhatsNewModal={showWhatsNewModal} showWhatsNewModal={showWhatsNewModal}
/> />
); );
} });

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
// Copyright 2022 Signal Messenger, LLC // Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React, { useCallback } from 'react'; import React, { memo, useCallback } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { CompositionRecording } from '../../components/CompositionRecording'; import { CompositionRecording } from '../../components/CompositionRecording';
import { mapDispatchToProps } from '../actions'; import { mapDispatchToProps } from '../actions';
@ -15,9 +15,10 @@ export type SmartCompositionRecordingProps = {
onBeforeSend: () => void; onBeforeSend: () => void;
}; };
export function SmartCompositionRecording({ export const SmartCompositionRecording = memo(
function SmartCompositionRecording({
onBeforeSend, onBeforeSend,
}: SmartCompositionRecordingProps): JSX.Element | null { }: SmartCompositionRecordingProps) {
const i18n = useSelector(getIntl); const i18n = useSelector(getIntl);
const selectedConversationId = useSelector(getSelectedConversationId); const selectedConversationId = useSelector(getSelectedConversationId);
const { cancelRecording, completeRecording } = useAudioRecorderActions(); const { cancelRecording, completeRecording } = useAudioRecorderActions();
@ -33,7 +34,9 @@ export function SmartCompositionRecording({
if (selectedConversationId) { if (selectedConversationId) {
completeRecording(selectedConversationId, voiceNoteAttachment => { completeRecording(selectedConversationId, voiceNoteAttachment => {
onBeforeSend(); onBeforeSend();
sendMultiMediaMessage(selectedConversationId, { voiceNoteAttachment }); sendMultiMediaMessage(selectedConversationId, {
voiceNoteAttachment,
});
}); });
} }
}, [ }, [
@ -61,3 +64,4 @@ export function SmartCompositionRecording({
/> />
); );
} }
);

View file

@ -1,7 +1,7 @@
// Copyright 2023 Signal Messenger, LLC // Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React, { useCallback } from 'react'; import React, { memo, useCallback } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { CompositionRecordingDraft } from '../../components/CompositionRecordingDraft'; import { CompositionRecordingDraft } from '../../components/CompositionRecordingDraft';
import type { AttachmentDraftType } from '../../types/Attachment'; import type { AttachmentDraftType } from '../../types/Attachment';
@ -21,9 +21,10 @@ export type SmartCompositionRecordingDraftProps = {
voiceNoteAttachment: AttachmentDraftType; voiceNoteAttachment: AttachmentDraftType;
}; };
export function SmartCompositionRecordingDraft({ export const SmartCompositionRecordingDraft = memo(
function SmartCompositionRecordingDraft({
voiceNoteAttachment, voiceNoteAttachment,
}: SmartCompositionRecordingDraftProps): JSX.Element { }: SmartCompositionRecordingDraftProps) {
const i18n = useSelector(getIntl); const i18n = useSelector(getIntl);
const active = useSelector(selectAudioPlayerActive); const active = useSelector(selectAudioPlayerActive);
const selectedConversationId = useSelector(getSelectedConversationId); const selectedConversationId = useSelector(getSelectedConversationId);
@ -154,3 +155,4 @@ export function SmartCompositionRecordingDraft({
/> />
); );
} }
);

View file

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

View file

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

View file

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

View file

@ -1,15 +1,12 @@
// Copyright 2022 Signal Messenger, LLC // Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React, { useCallback } from 'react'; import React, { memo, useCallback } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { mapValues } from 'lodash'; import { mapValues } from 'lodash';
import type { StateType } from '../reducer'; import type { StateType } from '../reducer';
import { ContactSpoofingReviewDialog } from '../../components/conversation/ContactSpoofingReviewDialog'; import { ContactSpoofingReviewDialog } from '../../components/conversation/ContactSpoofingReviewDialog';
import { useConversationsActions } from '../ducks/conversations'; import { useConversationsActions } from '../ducks/conversations';
import type { GetConversationByIdType } from '../selectors/conversations';
import { import {
getConversationSelector, getConversationSelector,
getConversationByServiceIdSelector, getConversationByServiceIdSelector,
@ -33,14 +30,11 @@ export type PropsType = Readonly<{
onClose: () => void; onClose: () => void;
}>; }>;
export function SmartContactSpoofingReviewDialog( export const SmartContactSpoofingReviewDialog = memo(
props: PropsType function SmartContactSpoofingReviewDialog(props: PropsType) {
): JSX.Element | null {
const { conversationId } = props; const { conversationId } = props;
const getConversation = useSelector<StateType, GetConversationByIdType>( const getConversation = useSelector(getConversationSelector);
getConversationSelector
);
const { const {
acceptConversation, acceptConversation,
@ -146,3 +140,4 @@ export function SmartContactSpoofingReviewDialog(
/> />
); );
} }
);

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
// Copyright 2021 Signal Messenger, LLC // Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React from 'react'; import React, { memo, useCallback } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import type { StateType } from '../reducer'; import type { StateType } from '../reducer';
import { ConversationPanel } from './ConversationPanel'; import { ConversationPanel } from './ConversationPanel';
@ -18,7 +18,24 @@ import {
import { useComposerActions } from '../ducks/composer'; import { useComposerActions } from '../ducks/composer';
import { useConversationsActions } from '../ducks/conversations'; import { useConversationsActions } from '../ducks/conversations';
export function SmartConversationView(): JSX.Element { 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); const conversationId = useSelector(getSelectedConversationId);
if (!conversationId) { if (!conversationId) {
@ -45,24 +62,23 @@ export function SmartConversationView(): JSX.Element {
return activePanel && !isAnimating; return activePanel && !isAnimating;
}); });
const onExitSelectMode = useCallback(() => {
toggleSelectMode(false);
}, [toggleSelectMode]);
return ( return (
<ConversationView <ConversationView
conversationId={conversationId} conversationId={conversationId}
hasOpenModal={hasOpenModal} hasOpenModal={hasOpenModal}
isSelectMode={isSelectMode} isSelectMode={isSelectMode}
onExitSelectMode={() => { onExitSelectMode={onExitSelectMode}
toggleSelectMode(false);
}}
processAttachments={processAttachments} processAttachments={processAttachments}
renderCompositionArea={() => <SmartCompositionArea id={conversationId} />} renderCompositionArea={renderCompositionArea}
renderConversationHeader={() => ( renderConversationHeader={renderConversationHeader}
<SmartConversationHeader id={conversationId} /> renderTimeline={renderTimeline}
)} renderPanel={renderPanel}
renderTimeline={() => (
<SmartTimeline key={conversationId} id={conversationId} />
)}
renderPanel={() => <ConversationPanel conversationId={conversationId} />}
shouldHideConversationView={shouldHideConversationView} shouldHideConversationView={shouldHideConversationView}
/> />
); );
} }
);

View file

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

View file

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

View file

@ -1,9 +1,8 @@
// Copyright 2023 Signal Messenger, LLC // Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React from 'react'; import React, { memo } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import type { DeleteMessagesPropsType } from '../ducks/globalModals';
import type { StateType } from '../reducer'; import type { StateType } from '../reducer';
import { getIntl } from '../selectors/user'; import { getIntl } from '../selectors/user';
import { useGlobalModalActions } from '../ducks/globalModals'; import { useGlobalModalActions } from '../ducks/globalModals';
@ -12,13 +11,15 @@ import { strictAssert } from '../../util/assert';
import { canDeleteMessagesForEveryone } from '../selectors/message'; import { canDeleteMessagesForEveryone } from '../selectors/message';
import { useConversationsActions } from '../ducks/conversations'; import { useConversationsActions } from '../ducks/conversations';
import { useToastActions } from '../ducks/toast'; 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 { export const SmartDeleteMessagesModal = memo(
const deleteMessagesProps = useSelector< function SmartDeleteMessagesModal() {
StateType, const deleteMessagesProps = useSelector(getDeleteMessagesProps);
DeleteMessagesPropsType | undefined
>(state => state.globalModals.deleteMessagesProps);
strictAssert( strictAssert(
deleteMessagesProps != null, deleteMessagesProps != null,
'Cannot render delete messages modal without messages' 'Cannot render delete messages modal without messages'
@ -31,9 +32,7 @@ export function SmartDeleteMessagesModal(): JSX.Element | null {
const canDeleteForEveryone = useSelector((state: StateType) => { const canDeleteForEveryone = useSelector((state: StateType) => {
return canDeleteMessagesForEveryone(state, { messageIds, isMe }); return canDeleteMessagesForEveryone(state, { messageIds, isMe });
}); });
const lastSelectedMessage = useSelector((state: StateType) => { const lastSelectedMessage = useSelector(getLastSelectedMessage);
return state.conversations.lastSelectedMessage;
});
const i18n = useSelector(getIntl); const i18n = useSelector(getIntl);
const { toggleDeleteMessagesModal } = useGlobalModalActions(); const { toggleDeleteMessagesModal } = useGlobalModalActions();
const { deleteMessages, deleteMessagesForEveryone } = const { deleteMessages, deleteMessagesForEveryone } =
@ -65,3 +64,4 @@ export function SmartDeleteMessagesModal(): JSX.Element | null {
/> />
); );
} }
);

View file

@ -1,11 +1,9 @@
// Copyright 2023 Signal Messenger, LLC // Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React, { useMemo } from 'react'; import React, { memo, useMemo } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import type { GlobalModalsStateType } from '../ducks/globalModals';
import type { MessageAttributesType } from '../../model-types.d'; import type { MessageAttributesType } from '../../model-types.d';
import type { StateType } from '../reducer';
import { EditHistoryMessagesModal } from '../../components/EditHistoryMessagesModal'; import { EditHistoryMessagesModal } from '../../components/EditHistoryMessagesModal';
import { getIntl, getPlatform } from '../selectors/user'; import { getIntl, getPlatform } from '../selectors/user';
import { getMessagePropsSelector } from '../selectors/message'; import { getMessagePropsSelector } from '../selectors/message';
@ -14,23 +12,19 @@ import { useConversationsActions } from '../ducks/conversations';
import { useGlobalModalActions } from '../ducks/globalModals'; import { useGlobalModalActions } from '../ducks/globalModals';
import { useLightboxActions } from '../ducks/lightbox'; import { useLightboxActions } from '../ducks/lightbox';
import { strictAssert } from '../../util/assert'; import { strictAssert } from '../../util/assert';
import { getEditHistoryMessages } from '../selectors/globalModals';
export function SmartEditHistoryMessagesModal(): JSX.Element { export const SmartEditHistoryMessagesModal = memo(
function SmartEditHistoryMessagesModal(): JSX.Element {
const i18n = useSelector(getIntl); const i18n = useSelector(getIntl);
const platform = useSelector(getPlatform); const platform = useSelector(getPlatform);
const { closeEditHistoryModal } = useGlobalModalActions(); const { closeEditHistoryModal } = useGlobalModalActions();
const { kickOffAttachmentDownload } = useConversationsActions(); const { kickOffAttachmentDownload } = useConversationsActions();
const { showLightbox } = useLightboxActions(); const { showLightbox } = useLightboxActions();
const getPreferredBadge = useSelector(getPreferredBadgeSelector); const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const messagesAttributes = useSelector(getEditHistoryMessages);
const { editHistoryMessages: messagesAttributes } = useSelector<
StateType,
GlobalModalsStateType
>(state => state.globalModals);
const messagePropsSelector = useSelector(getMessagePropsSelector); const messagePropsSelector = useSelector(getMessagePropsSelector);
strictAssert(messagesAttributes, 'messages not provided'); strictAssert(messagesAttributes, 'messages not provided');
@ -60,3 +54,4 @@ export function SmartEditHistoryMessagesModal(): JSX.Element {
/> />
); );
} }
);

View file

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

View file

@ -1,19 +1,17 @@
// Copyright 2020 Signal Messenger, LLC // Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react'; import React, { forwardRef, memo } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import type { StateType } from '../reducer';
import { useRecentEmojis } from '../selectors/emojis'; import { useRecentEmojis } from '../selectors/emojis';
import { useEmojisActions as useEmojiActions } from '../ducks/emojis'; import { useEmojisActions as useEmojiActions } from '../ducks/emojis';
import type { Props as EmojiPickerProps } from '../../components/emoji/EmojiPicker'; import type { Props as EmojiPickerProps } from '../../components/emoji/EmojiPicker';
import { EmojiPicker } from '../../components/emoji/EmojiPicker'; import { EmojiPicker } from '../../components/emoji/EmojiPicker';
import { getIntl } from '../selectors/user'; import { getIntl } from '../selectors/user';
import { getEmojiSkinTone } from '../selectors/items'; import { getEmojiSkinTone } from '../selectors/items';
import type { LocalizerType } from '../../types/Util';
export const SmartEmojiPicker = React.forwardRef< export const SmartEmojiPicker = memo(
forwardRef<
HTMLDivElement, HTMLDivElement,
Pick< Pick<
EmojiPickerProps, EmojiPickerProps,
@ -23,10 +21,8 @@ export const SmartEmojiPicker = React.forwardRef<
{ onClickSettings, onPickEmoji, onSetSkinTone, onClose, style }, { onClickSettings, onPickEmoji, onSetSkinTone, onClose, style },
ref ref
) { ) {
const i18n = useSelector<StateType, LocalizerType>(getIntl); const i18n = useSelector(getIntl);
const skinTone = useSelector<StateType, number>(state => const skinTone = useSelector(getEmojiSkinTone);
getEmojiSkinTone(state)
);
const recentEmojis = useRecentEmojis(); const recentEmojis = useRecentEmojis();
@ -53,4 +49,5 @@ export const SmartEmojiPicker = React.forwardRef<
style={style} style={style}
/> />
); );
}); })
);

View file

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

View file

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

View file

@ -1,11 +1,8 @@
// Copyright 2021 Signal Messenger, LLC // Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React, { useCallback } from 'react'; import React, { memo, useCallback } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import type { GlobalModalsStateType } from '../ducks/globalModals';
import type { StateType } from '../reducer';
import type { ButtonVariant } from '../../components/Button'; import type { ButtonVariant } from '../../components/Button';
import { ErrorModal } from '../../components/ErrorModal'; import { ErrorModal } from '../../components/ErrorModal';
import { GlobalModalContainer } from '../../components/GlobalModalContainer'; import { GlobalModalContainer } from '../../components/GlobalModalContainer';
@ -26,6 +23,7 @@ import { getIntl, getTheme } from '../selectors/user';
import { useGlobalModalActions } from '../ducks/globalModals'; import { useGlobalModalActions } from '../ducks/globalModals';
import { SmartDeleteMessagesModal } from './DeleteMessagesModal'; import { SmartDeleteMessagesModal } from './DeleteMessagesModal';
import { SmartMessageRequestActionsConfirmation } from './MessageRequestActionsConfirmation'; import { SmartMessageRequestActionsConfirmation } from './MessageRequestActionsConfirmation';
import { getGlobalModalsState } from '../selectors/globalModals';
function renderEditHistoryMessagesModal(): JSX.Element { function renderEditHistoryMessagesModal(): JSX.Element {
return <SmartEditHistoryMessagesModal />; return <SmartEditHistoryMessagesModal />;
@ -71,7 +69,8 @@ function renderAboutContactModal(): JSX.Element {
return <SmartAboutContactModal />; return <SmartAboutContactModal />;
} }
export function SmartGlobalModalContainer(): JSX.Element { export const SmartGlobalModalContainer = memo(
function SmartGlobalModalContainer() {
const conversationsStoppingSend = useSelector(getConversationsStoppingSend); const conversationsStoppingSend = useSelector(getConversationsStoppingSend);
const i18n = useSelector(getIntl); const i18n = useSelector(getIntl);
const theme = useSelector(getTheme); const theme = useSelector(getTheme);
@ -101,9 +100,7 @@ export function SmartGlobalModalContainer(): JSX.Element {
sendEditWarningData, sendEditWarningData,
stickerPackPreviewId, stickerPackPreviewId,
userNotFoundModalState, userNotFoundModalState,
} = useSelector<StateType, GlobalModalsStateType>( } = useSelector(getGlobalModalsState);
state => state.globalModals
);
const { const {
cancelAuthorizeArtCreator, cancelAuthorizeArtCreator,
@ -126,7 +123,9 @@ export function SmartGlobalModalContainer(): JSX.Element {
const renderSafetyNumber = useCallback( const renderSafetyNumber = useCallback(
() => ( () => (
<SmartSafetyNumberModal contactID={String(safetyNumberModalContactId)} /> <SmartSafetyNumberModal
contactID={String(safetyNumberModalContactId)}
/>
), ),
[safetyNumberModalContactId] [safetyNumberModalContactId]
); );
@ -162,7 +161,9 @@ export function SmartGlobalModalContainer(): JSX.Element {
return ( return (
<GlobalModalContainer <GlobalModalContainer
addUserToAnotherGroupModalContactId={addUserToAnotherGroupModalContactId} addUserToAnotherGroupModalContactId={
addUserToAnotherGroupModalContactId
}
contactModalState={contactModalState} contactModalState={contactModalState}
editHistoryMessages={editHistoryMessages} editHistoryMessages={editHistoryMessages}
errorModalProps={errorModalProps} errorModalProps={errorModalProps}
@ -216,3 +217,4 @@ export function SmartGlobalModalContainer(): JSX.Element {
/> />
); );
} }
);

View file

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

View file

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

View file

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

View file

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

View file

@ -1,41 +1,77 @@
// Copyright 2020 Signal Messenger, LLC // Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React, { memo } from 'react';
import { connect } from 'react-redux'; import { useSelector } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import { ConversationHero } from '../../components/conversation/ConversationHero'; import { ConversationHero } from '../../components/conversation/ConversationHero';
import type { StateType } from '../reducer';
import { getPreferredBadgeSelector } from '../selectors/badges'; import { getPreferredBadgeSelector } from '../selectors/badges';
import { getIntl, getTheme } from '../selectors/user'; import { getIntl, getTheme } from '../selectors/user';
import { getHasStoriesSelector } from '../selectors/stories2'; import { getHasStoriesSelector } from '../selectors/stories2';
import { isSignalConversation } from '../../util/isSignalConversation'; 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; id: string;
}; }>;
const mapStateToProps = (state: StateType, props: ExternalProps) => { export const SmartHeroRow = memo(function SmartHeroRow({
const { id } = props; id,
}: SmartHeroRowProps) {
const conversation = state.conversations.conversationLookup[id]; const i18n = useSelector(getIntl);
const theme = useSelector(getTheme);
if (!conversation) { 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!`); throw new Error(`Did not find conversation ${id} in state!`);
} }
const badge = getPreferredBadge(conversation.badges);
return { const hasStories = hasStoriesSelector(id);
i18n: getIntl(state), const isSignalConversationValue = isSignalConversation(conversation);
...conversation, const { unblurAvatar, updateSharedGroups } = useConversationsActions();
conversationType: conversation.type, const { toggleAboutContactModal } = useGlobalModalActions();
hasStories: getHasStoriesSelector(state)(id), const { viewUserStories } = useStoriesActions();
badge: getPreferredBadgeSelector(state)(conversation.badges), const {
isSignalConversation: isSignalConversation(conversation), about,
theme: getTheme(state), acceptedMessageRequest,
}; avatarPath,
}; groupDescription,
isMe,
const smart = connect(mapStateToProps, mapDispatchToProps); membersCount,
phoneNumber,
export const SmartHeroRow = smart(ConversationHero); 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 // Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React from 'react'; import React, { memo } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import type { AppStateType } from '../ducks/app';
import type { StateType } from '../reducer'; import type { StateType } from '../reducer';
import { Inbox } from '../../components/Inbox'; import { Inbox } from '../../components/Inbox';
import { getIntl } from '../selectors/user'; import { getIntl } from '../selectors/user';
@ -37,19 +36,19 @@ function renderStoriesTab() {
return <SmartStoriesTab />; return <SmartStoriesTab />;
} }
export function SmartInbox(): JSX.Element { export const SmartInbox = memo(function SmartInbox(): JSX.Element {
const i18n = useSelector(getIntl); const i18n = useSelector(getIntl);
const isCustomizingPreferredReactions = useSelector( const isCustomizingPreferredReactions = useSelector(
getIsCustomizingPreferredReactions getIsCustomizingPreferredReactions
); );
const envelopeTimestamp = useSelector<StateType, number | undefined>( const envelopeTimestamp = useSelector(
state => state.inbox.envelopeTimestamp (state: StateType) => state.inbox.envelopeTimestamp
); );
const firstEnvelopeTimestamp = useSelector<StateType, number | undefined>( const firstEnvelopeTimestamp = useSelector(
state => state.inbox.firstEnvelopeTimestamp (state: StateType) => state.inbox.firstEnvelopeTimestamp
); );
const { hasInitialLoadCompleted } = useSelector<StateType, AppStateType>( const { hasInitialLoadCompleted } = useSelector(
state => state.app (state: StateType) => state.app
); );
const navTabsCollapsed = useSelector(getNavTabsCollapsed); const navTabsCollapsed = useSelector(getNavTabsCollapsed);
@ -73,4 +72,4 @@ export function SmartInbox(): JSX.Element {
renderStoriesTab={renderStoriesTab} renderStoriesTab={renderStoriesTab}
/> />
); );
} });

View file

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

View file

@ -1,23 +1,71 @@
// Copyright 2019 Signal Messenger, LLC // Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { connect } from 'react-redux';
import { get } from 'lodash'; 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 type { PropsType as LeftPanePropsType } from '../../components/LeftPane';
import { LeftPane } from '../../components/LeftPane'; import { LeftPane } from '../../components/LeftPane';
import { DialogExpiredBuild } from '../../components/DialogExpiredBuild'; import type { NavTabPanelProps } from '../../components/NavTabs';
import type { PropsType as DialogExpiredBuildPropsType } from '../../components/DialogExpiredBuild'; import type { WidthBreakpoint } from '../../components/_util';
import type { StateType } from '../reducer'; import {
import { missingCaseError } from '../../util/missingCaseError'; getGroupSizeHardLimit,
import { lookupConversationWithoutServiceId } from '../../util/lookupConversationWithoutServiceId'; getGroupSizeRecommendedLimit,
import { isDone as isRegistrationDone } from '../../util/registration'; } from '../../groups/limits';
import { getCountryDataForLocale } from '../../util/getCountryData';
import { getUsernameFromSearch } from '../../util/Username';
import { LeftPaneMode } from '../../types/leftPane'; 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 { 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 { import {
getIsSearching, getIsSearching,
getQuery, getQuery,
@ -26,65 +74,26 @@ import {
getStartSearchCounter, getStartSearchCounter,
isSearching, isSearching,
} from '../selectors/search'; } from '../selectors/search';
import {
isUpdateDownloaded as getIsUpdateDownloaded,
isOSUnsupported,
isUpdateDialogVisible,
} from '../selectors/updates';
import { import {
getIntl, getIntl,
getIsMacOS,
getRegionCode, getRegionCode,
getTheme, getTheme,
getIsMacOS,
} from '../selectors/user'; } from '../selectors/user';
import { hasExpired } from '../selectors/expiration'; import { SmartCaptchaDialog } from './CaptchaDialog';
import { import { SmartCrashReportDialog } from './CrashReportDialog';
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 { SmartMessageSearchResult } from './MessageSearchResult'; import { SmartMessageSearchResult } from './MessageSearchResult';
import { SmartNetworkStatus } from './NetworkStatus'; import { SmartNetworkStatus } from './NetworkStatus';
import { SmartRelinkDialog } from './RelinkDialog'; import { SmartRelinkDialog } from './RelinkDialog';
import { SmartUnsupportedOSDialog } from './UnsupportedOSDialog';
import { SmartToastManager } from './ToastManager'; import { SmartToastManager } from './ToastManager';
import type { PropsType as SmartUnsupportedOSDialogPropsType } from './UnsupportedOSDialog'; import type { PropsType as SmartUnsupportedOSDialogPropsType } from './UnsupportedOSDialog';
import { SmartUnsupportedOSDialog } from './UnsupportedOSDialog';
import { SmartUpdateDialog } from './UpdateDialog'; import { SmartUpdateDialog } from './UpdateDialog';
import { SmartCaptchaDialog } from './CaptchaDialog';
import { SmartCrashReportDialog } from './CrashReportDialog';
function renderMessageSearchResult(id: string): JSX.Element { function renderMessageSearchResult(id: string): JSX.Element {
return <SmartMessageSearchResult id={id} />; return <SmartMessageSearchResult id={id} />;
@ -120,7 +129,7 @@ function renderUnsupportedOSDialog(
): JSX.Element { ): JSX.Element {
return <SmartUnsupportedOSDialog {...props} />; return <SmartUnsupportedOSDialog {...props} />;
} }
function renderToastManager(props: { function renderToastManagerWithMegaphone(props: {
containerWidthBreakpoint: WidthBreakpoint; containerWidthBreakpoint: WidthBreakpoint;
}): JSX.Element { }): JSX.Element {
return <SmartToastManager {...props} />; return <SmartToastManager {...props} />;
@ -243,15 +252,81 @@ const getModeSpecificProps = (
} }
}; };
const mapStateToProps = (state: StateType) => { export const SmartLeftPane = memo(function SmartLeftPane({
const hasUpdateDialog = isUpdateDialogVisible(state); hasFailedStorySends,
const hasUnsupportedOS = isOSUnsupported(state); hasPendingUpdate,
const usernameCorrupted = getUsernameCorrupted(state); otherTabsUnreadStats,
const usernameLinkCorrupted = getUsernameLinkCorrupted(state); }: 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 hasExpiredDialog = false;
let unsupportedOSDialogType: 'error' | 'warning' | undefined; let unsupportedOSDialogType: 'error' | 'warning' | undefined;
if (hasExpired(state)) { if (hasAppExpired) {
if (hasUnsupportedOS) { if (hasUnsupportedOS) {
unsupportedOSDialogType = 'error'; unsupportedOSDialogType = 'error';
} else { } else {
@ -261,49 +336,87 @@ const mapStateToProps = (state: StateType) => {
unsupportedOSDialogType = 'warning'; unsupportedOSDialogType = 'warning';
} }
const composerStep = getComposerStep(state); const hasRelinkDialog = !isRegistrationDone();
const showArchived = getShowArchived(state);
const hasSearchQuery = isSearching(state);
return { const renderToastManager =
hasNetworkDialog: hasNetworkDialog(state),
hasExpiredDialog,
hasRelinkDialog: !isRegistrationDone(),
hasUpdateDialog,
isUpdateDownloaded: isUpdateDownloaded(state),
unsupportedOSDialogType,
usernameCorrupted,
usernameLinkCorrupted,
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 composerStep == null && !showArchived && !hasSearchQuery
? renderToastManager ? renderToastManagerWithMegaphone
: renderToastManagerWithoutMegaphone, : renderToastManagerWithoutMegaphone;
lookupConversationWithoutServiceId,
theme: getTheme(state),
};
};
const smart = connect(mapStateToProps, mapDispatchToProps); const targetedMessageId = targetedMessage?.id;
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 // Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React, { useCallback } from 'react'; import React, { memo, useCallback } from 'react';
import { useSelector } from 'react-redux'; 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 { Lightbox } from '../../components/Lightbox';
import { getConversationSelector } from '../selectors/conversations'; import { getConversationSelector } from '../selectors/conversations';
import { getIntl } from '../selectors/user'; import { getIntl } from '../selectors/user';
@ -26,8 +20,8 @@ import {
shouldShowLightbox, shouldShowLightbox,
} from '../selectors/lightbox'; } from '../selectors/lightbox';
export function SmartLightbox(): JSX.Element | null { export const SmartLightbox = memo(function SmartLightbox() {
const i18n = useSelector<StateType, LocalizerType>(getIntl); const i18n = useSelector(getIntl);
const { saveAttachment } = useConversationsActions(); const { saveAttachment } = useConversationsActions();
const { const {
closeLightbox, closeLightbox,
@ -38,20 +32,15 @@ export function SmartLightbox(): JSX.Element | null {
const { toggleForwardMessagesModal } = useGlobalModalActions(); const { toggleForwardMessagesModal } = useGlobalModalActions();
const { pauseVoiceNotePlayer } = useAudioPlayerActions(); const { pauseVoiceNotePlayer } = useAudioPlayerActions();
const conversationSelector = useSelector<StateType, GetConversationByIdType>( const conversationSelector = useSelector(getConversationSelector);
getConversationSelector
);
const isShowingLightbox = useSelector<StateType, boolean>(shouldShowLightbox); const isShowingLightbox = useSelector(shouldShowLightbox);
const isViewOnce = useSelector<StateType, boolean>(getIsViewOnce); const isViewOnce = useSelector(getIsViewOnce);
const media = useSelector< const media = useSelector(getMedia);
StateType, const hasPrevMessage = useSelector(getHasPrevMessage);
ReadonlyArray<ReadonlyDeep<MediaItemType>> const hasNextMessage = useSelector(getHasNextMessage);
>(getMedia); const selectedIndex = useSelector(getSelectedIndex);
const hasPrevMessage = useSelector<StateType, boolean>(getHasPrevMessage); const playbackDisabled = useSelector(getPlaybackDisabled);
const hasNextMessage = useSelector<StateType, boolean>(getHasNextMessage);
const selectedIndex = useSelector<StateType, number>(getSelectedIndex);
const playbackDisabled = useSelector<StateType, boolean>(getPlaybackDisabled);
const onPrevAttachment = useCallback(() => { const onPrevAttachment = useCallback(() => {
if (selectedIndex <= 0) { if (selectedIndex <= 0) {
@ -107,4 +96,4 @@ export function SmartLightbox(): JSX.Element | null {
hasPrevMessage={hasPrevMessage} hasPrevMessage={hasPrevMessage}
/> />
); );
} });

View file

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

View file

@ -1,7 +1,7 @@
// Copyright 2021 Signal Messenger, LLC // Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React, { useEffect } from 'react'; import React, { memo, useEffect } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import type { Props as MessageDetailProps } from '../../components/conversation/MessageDetail'; import type { Props as MessageDetailProps } from '../../components/conversation/MessageDetail';
@ -28,7 +28,8 @@ export type OwnProps = Pick<
'contacts' | 'errors' | 'message' | 'receivedAt' 'contacts' | 'errors' | 'message' | 'receivedAt'
>; >;
export function SmartMessageDetail(): JSX.Element | null { export const SmartMessageDetail = memo(
function SmartMessageDetail(): JSX.Element | null {
const getContactNameColor = useSelector(getContactNameColorSelector); const getContactNameColor = useSelector(getContactNameColorSelector);
const getPreferredBadge = useSelector(getPreferredBadgeSelector); const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const i18n = useSelector(getIntl); const i18n = useSelector(getIntl);
@ -114,3 +115,4 @@ export function SmartMessageDetail(): JSX.Element | null {
/> />
); );
} }
);

View file

@ -1,9 +1,8 @@
// Copyright 2024 Signal Messenger, LLC // Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // 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 { useSelector } from 'react-redux';
import { getIntl } from '../selectors/user'; import { getIntl } from '../selectors/user';
import { getGlobalModalsState } from '../selectors/globalModals'; import { getGlobalModalsState } from '../selectors/globalModals';
import { getConversationSelector } from '../selectors/conversations'; import { getConversationSelector } from '../selectors/conversations';
@ -17,7 +16,8 @@ import { getAddedByForOurPendingInvitation } from '../../util/getAddedByForOurPe
import { strictAssert } from '../../util/assert'; import { strictAssert } from '../../util/assert';
import { useGlobalModalActions } from '../ducks/globalModals'; import { useGlobalModalActions } from '../ducks/globalModals';
export function SmartMessageRequestActionsConfirmation(): JSX.Element | null { export const SmartMessageRequestActionsConfirmation = memo(
function SmartMessageRequestActionsConfirmation() {
const i18n = useSelector(getIntl); const i18n = useSelector(getIntl);
const globalModals = useSelector(getGlobalModalsState); const globalModals = useSelector(getGlobalModalsState);
const { messageRequestActionsConfirmationProps } = globalModals; const { messageRequestActionsConfirmationProps } = globalModals;
@ -82,3 +82,4 @@ export function SmartMessageRequestActionsConfirmation(): JSX.Element | null {
/> />
); );
} }
);

View file

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

View file

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

View file

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

View file

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

View file

@ -1,12 +1,8 @@
// Copyright 2021 Signal Messenger, LLC // Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React, { memo } from 'react';
import { connect } from 'react-redux'; import { useSelector } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import type { PropsDataType } from '../../components/conversation/conversation-details/PendingInvites';
import { PendingInvites } from '../../components/conversation/conversation-details/PendingInvites'; import { PendingInvites } from '../../components/conversation/conversation-details/PendingInvites';
import type { StateType } from '../reducer';
import { getIntl, getTheme } from '../selectors/user'; import { getIntl, getTheme } from '../selectors/user';
import { getPreferredBadgeSelector } from '../selectors/badges'; import { getPreferredBadgeSelector } from '../selectors/badges';
import { import {
@ -16,36 +12,48 @@ import {
import { getGroupMemberships } from '../../util/getGroupMemberships'; import { getGroupMemberships } from '../../util/getGroupMemberships';
import { assertDev } from '../../util/assert'; import { assertDev } from '../../util/assert';
import type { AciString } from '../../types/ServiceId'; import type { AciString } from '../../types/ServiceId';
import { useConversationsActions } from '../ducks/conversations';
export type SmartPendingInvitesProps = { export type SmartPendingInvitesProps = {
conversationId: string; conversationId: string;
ourAci: AciString; ourAci: AciString;
}; };
const mapStateToProps = ( export const SmartPendingInvites = memo(function SmartPendingInvites({
state: StateType, conversationId,
props: SmartPendingInvitesProps ourAci,
): PropsDataType => { }: SmartPendingInvitesProps) {
const conversationSelector = getConversationByIdSelector(state); const i18n = useSelector(getIntl);
const conversationByServiceIdSelector = const theme = useSelector(getTheme);
getConversationByServiceIdSelector(state); const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const conversationSelector = useSelector(getConversationByIdSelector);
const conversation = conversationSelector(props.conversationId); const conversationByServiceIdSelector = useSelector(
getConversationByServiceIdSelector
);
const conversation = conversationSelector(conversationId);
assertDev( assertDev(
conversation, conversation,
'<SmartPendingInvites> expected a conversation to be found' '<SmartPendingInvites> expected a conversation to be found'
); );
const groupMemberships = getGroupMemberships(
return {
...props,
...getGroupMemberships(conversation, conversationByServiceIdSelector),
conversation, conversation,
getPreferredBadge: getPreferredBadgeSelector(state), conversationByServiceIdSelector
i18n: getIntl(state), );
theme: getTheme(state), const {
}; approvePendingMembershipFromGroupV2,
}; revokePendingMembershipsFromGroupV2,
} = useConversationsActions();
const smart = connect(mapStateToProps, mapDispatchToProps); return (
<PendingInvites
export const SmartPendingInvites = smart(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 // Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React, { memo } from 'react';
import React from 'react'; import { useSelector } from 'react-redux';
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import type { PropsDataType as ProfileEditorModalPropsType } from '../../components/ProfileEditorModal';
import { ProfileEditorModal } from '../../components/ProfileEditorModal'; import { ProfileEditorModal } from '../../components/ProfileEditorModal';
import type { PropsDataType } from '../../components/ProfileEditor'; import { useConversationsActions } from '../ducks/conversations';
import { SmartEditUsernameModalBody } from './EditUsernameModalBody'; import { useGlobalModalActions } from '../ducks/globalModals';
import type { StateType } from '../reducer'; import { useItemsActions } from '../ducks/items';
import { getIntl } from '../selectors/user'; 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 { import {
getEmojiSkinTone, getEmojiSkinTone,
getHasCompletedUsernameLinkOnboarding, getHasCompletedUsernameLinkOnboarding,
getUsernameCorrupted, getUsernameCorrupted,
getUsernameLinkColor,
getUsernameLink, getUsernameLink,
getUsernameLinkColor,
getUsernameLinkCorrupted, getUsernameLinkCorrupted,
isInternalUser, isInternalUser,
} from '../selectors/items'; } from '../selectors/items';
import { getMe } from '../selectors/conversations'; import { getIntl } from '../selectors/user';
import { selectRecentEmojis } from '../selectors/emojis';
import { import {
getUsernameEditState, getUsernameEditState,
getUsernameLinkState, getUsernameLinkState,
} from '../selectors/username'; } from '../selectors/username';
import type { SmartEditUsernameModalBodyProps } from './EditUsernameModalBody';
import { SmartEditUsernameModalBody } from './EditUsernameModalBody';
function renderEditUsernameModalBody(props: { function renderEditUsernameModalBody(
isRootModal: boolean; props: SmartEditUsernameModalBodyProps
onClose: () => void; ): JSX.Element {
}): JSX.Element {
return <SmartEditUsernameModalBody {...props} />; return <SmartEditUsernameModalBody {...props} />;
} }
function mapStateToProps( export const SmartProfileEditorModal = memo(function SmartProfileEditorModal() {
state: StateType const i18n = useSelector(getIntl);
): Omit<PropsDataType, 'onEditStateChange' | 'onProfileChanged'> &
ProfileEditorModalPropsType {
const { const {
profileAvatarPath, aboutEmoji,
aboutText,
avatars: userAvatarData = [], avatars: userAvatarData = [],
aboutText,
aboutEmoji,
color, color,
familyName,
firstName, firstName,
familyName,
id: conversationId, 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, profileAvatarPath,
color,
conversationId,
familyName,
firstName: String(firstName),
hasCompletedUsernameLinkOnboarding,
hasError: state.globalModals.profileEditorHasError,
initialEditState: state.globalModals.profileEditorInitialEditState,
i18n: getIntl(state),
recentEmojis,
skinTone,
userAvatarData,
username, username,
usernameCorrupted, } = useSelector(getMe);
usernameEditState, const hasCompletedUsernameLinkOnboarding = useSelector(
usernameLinkState, getHasCompletedUsernameLinkOnboarding
usernameLinkColor, );
usernameLinkCorrupted, const hasError = useSelector(getProfileEditorHasError);
usernameLink, const initialEditState = useSelector(getProfileEditorInitialEditState);
isUsernameDeletionEnabled: isInternalUser(state), 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); return (
<ProfileEditorModal
export const SmartProfileEditorModal = smart(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 // Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // 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 { useSelector } from 'react-redux';
import type { StateType } from '../reducer';
import { usePreferredReactionsActions } from '../ducks/preferredReactions'; import { usePreferredReactionsActions } from '../ducks/preferredReactions';
import { useItemsActions } from '../ducks/items'; import { useItemsActions } from '../ducks/items';
import { getIntl } from '../selectors/user'; import { getIntl } from '../selectors/user';
import { getPreferredReactionEmoji } from '../selectors/items'; import { getPreferredReactionEmoji } from '../selectors/items';
import type { LocalizerType } from '../../types/Util';
import type { Props as InternalProps } from '../../components/conversation/ReactionPicker'; import type { Props as InternalProps } from '../../components/conversation/ReactionPicker';
import { ReactionPicker } from '../../components/conversation/ReactionPicker'; import { ReactionPicker } from '../../components/conversation/ReactionPicker';
@ -24,20 +21,18 @@ type ExternalProps = Omit<
| 'skinTone' | 'skinTone'
>; >;
export const SmartReactionPicker = React.forwardRef< export const SmartReactionPicker = memo(
HTMLDivElement, forwardRef(function SmartReactionPickerInner(
ExternalProps props: ExternalProps,
>(function SmartReactionPickerInner(props, ref) { ref: Ref<HTMLDivElement>
) {
const { openCustomizePreferredReactionsModal } = const { openCustomizePreferredReactionsModal } =
usePreferredReactionsActions(); 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 ( return (
<ReactionPicker <ReactionPicker
@ -51,4 +46,5 @@ export const SmartReactionPicker = React.forwardRef<
{...props} {...props}
/> />
); );
}); })
);

View file

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

View file

@ -1,27 +1,39 @@
// Copyright 2020 Signal Messenger, LLC // Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React, { memo } from 'react';
import { connect } from 'react-redux'; import { useSelector } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import { SafetyNumberModal } from '../../components/SafetyNumberModal'; import { SafetyNumberModal } from '../../components/SafetyNumberModal';
import type { StateType } from '../reducer'; import type { StateType } from '../reducer';
import { getContactSafetyNumber } from '../selectors/safetyNumber'; import { getContactSafetyNumber } from '../selectors/safetyNumber';
import { getConversationSelector } from '../selectors/conversations'; import { getConversationSelector } from '../selectors/conversations';
import { getIntl } from '../selectors/user'; import { getIntl } from '../selectors/user';
import { useSafetyNumberActions } from '../ducks/safetyNumber';
import { useGlobalModalActions } from '../ducks/globalModals';
export type Props = { export type SmartSafetyNumberModalProps = {
contactID: string; contactID: string;
}; };
const mapStateToProps = (state: StateType, props: Props) => { export const SmartSafetyNumberModal = memo(function SmartSafetyNumberModal({
return { contactID,
...props, }: SmartSafetyNumberModalProps) {
...getContactSafetyNumber(state, props), const i18n = useSelector(getIntl);
contact: getConversationSelector(state)(props.contactID), const conversationSelector = useSelector(getConversationSelector);
i18n: getIntl(state), const contact = conversationSelector(contactID);
}; const contactSafetyNumber = useSelector((state: StateType) => {
}; return getContactSafetyNumber(state, { contactID });
});
const smart = connect(mapStateToProps, mapDispatchToProps); const { generateSafetyNumber, toggleVerified } = useSafetyNumberActions();
const { toggleSafetyNumberModal } = useGlobalModalActions();
export const SmartSafetyNumberModal = smart(SafetyNumberModal); 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 // Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux'; import React, { memo } from 'react';
import { mapDispatchToProps } from '../actions'; import { useSelector } from 'react-redux';
import { SafetyNumberViewer } from '../../components/SafetyNumberViewer'; import { SafetyNumberViewer } from '../../components/SafetyNumberViewer';
import type { StateType } from '../reducer'; import type { StateType } from '../reducer';
import type { SafetyNumberProps } from '../../components/SafetyNumberChangeDialog'; import type { SafetyNumberProps } from '../../components/SafetyNumberChangeDialog';
import { getContactSafetyNumber } from '../selectors/safetyNumber'; import { getContactSafetyNumber } from '../selectors/safetyNumber';
import { getConversationSelector } from '../selectors/conversations'; import { getConversationSelector } from '../selectors/conversations';
import { getIntl } from '../selectors/user'; import { getIntl } from '../selectors/user';
import { useSafetyNumberActions } from '../ducks/safetyNumber';
const mapStateToProps = (state: StateType, props: SafetyNumberProps) => { export const SmartSafetyNumberViewer = memo(function SmartSafetyNumberViewer({
return { contactID,
...props, onClose,
...getContactSafetyNumber(state, props), }: SafetyNumberProps) {
contact: getConversationSelector(state)(props.contactID), const i18n = useSelector(getIntl);
i18n: getIntl(state), 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 // Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React from 'react'; import React, { memo } from 'react';
import { useSelector } from 'react-redux'; 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 * as SingleServePromise from '../../services/singleServePromise';
import { import {
SafetyNumberChangeDialog, SafetyNumberChangeDialog,
@ -18,23 +15,26 @@ import { getPreferredBadgeSelector } from '../selectors/badges';
import { useConversationsActions } from '../ducks/conversations'; import { useConversationsActions } from '../ducks/conversations';
import { useGlobalModalActions } from '../ducks/globalModals'; import { useGlobalModalActions } from '../ducks/globalModals';
import { useStoryDistributionListsActions } from '../ducks/storyDistributionLists'; import { useStoryDistributionListsActions } from '../ducks/storyDistributionLists';
import { getSafetyNumberChangedBlockingData } from '../selectors/globalModals';
export function SmartSendAnywayDialog(): JSX.Element { export const SmartSendAnywayDialog = memo(
function SmartSendAnywayDialog(): JSX.Element {
const { hideBlockingSafetyNumberChangeDialog } = useGlobalModalActions(); const { hideBlockingSafetyNumberChangeDialog } = useGlobalModalActions();
const { removeMembersFromDistributionList } = const { removeMembersFromDistributionList } =
useStoryDistributionListsActions(); useStoryDistributionListsActions();
const { cancelConversationVerification, verifyConversationsStoppingSend } = const { cancelConversationVerification, verifyConversationsStoppingSend } =
useConversationsActions(); useConversationsActions();
const getPreferredBadge = useSelector(getPreferredBadgeSelector); const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const i18n = useSelector<StateType, LocalizerType>(getIntl); const i18n = useSelector(getIntl);
const theme = useSelector(getTheme); const theme = useSelector(getTheme);
const contacts = useSelector(getByDistributionListConversationsStoppingSend); const contacts = useSelector(
getByDistributionListConversationsStoppingSend
);
const safetyNumberChangedBlockingData = useSelector< const safetyNumberChangedBlockingData = useSelector(
StateType, getSafetyNumberChangedBlockingData
SafetyNumberChangedBlockingDataType | undefined );
>(state => state.globalModals.safetyNumberChangedBlockingData);
const explodedPromise = safetyNumberChangedBlockingData const explodedPromise = safetyNumberChangedBlockingData
? SingleServePromise.get<boolean>( ? SingleServePromise.get<boolean>(
@ -83,3 +83,4 @@ export function SmartSendAnywayDialog(): JSX.Element {
/> />
); );
} }
);

View file

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

View file

@ -1,11 +1,9 @@
// Copyright 2019 Signal Messenger, LLC // Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux'; import React, { memo } from 'react';
import { mapDispatchToProps } from '../actions'; import { useSelector } from 'react-redux';
import { StickerManager } from '../../components/stickers/StickerManager'; import { StickerManager } from '../../components/stickers/StickerManager';
import type { StateType } from '../reducer';
import { getIntl } from '../selectors/user'; import { getIntl } from '../selectors/user';
import { import {
getBlessedStickerPacks, getBlessedStickerPacks,
@ -13,22 +11,31 @@ import {
getKnownStickerPacks, getKnownStickerPacks,
getReceivedStickerPacks, getReceivedStickerPacks,
} from '../selectors/stickers'; } from '../selectors/stickers';
import { useStickersActions } from '../ducks/stickers';
import { useGlobalModalActions } from '../ducks/globalModals';
const mapStateToProps = (state: StateType) => { export const SmartStickerManager = memo(function SmartStickerManager() {
const blessedPacks = getBlessedStickerPacks(state); const i18n = useSelector(getIntl);
const receivedPacks = getReceivedStickerPacks(state); const blessedPacks = useSelector(getBlessedStickerPacks);
const installedPacks = getInstalledStickerPacks(state); const receivedPacks = useSelector(getReceivedStickerPacks);
const knownPacks = getKnownStickerPacks(state); const installedPacks = useSelector(getInstalledStickerPacks);
const knownPacks = useSelector(getKnownStickerPacks);
return { const { downloadStickerPack, installStickerPack, uninstallStickerPack } =
blessedPacks, useStickersActions();
receivedPacks, const { closeStickerPackPreview } = useGlobalModalActions();
installedPacks,
knownPacks,
i18n: getIntl(state),
};
};
const smart = connect(mapStateToProps, mapDispatchToProps); return (
<StickerManager
export const SmartStickerManager = smart(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 // Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux'; import React, { memo } from 'react';
import { mapDispatchToProps } from '../actions'; import { useSelector } from 'react-redux';
import { StickerPreviewModal } from '../../components/stickers/StickerPreviewModal'; import { StickerPreviewModal } from '../../components/stickers/StickerPreviewModal';
import type { StateType } from '../reducer';
import { getIntl, getStickersPath, getTempPath } from '../selectors/user'; import { getIntl, getStickersPath, getTempPath } from '../selectors/user';
import { import {
getBlessedPacks, getBlessedPacks,
getPacks, getPacks,
translatePackFromDB, translatePackFromDB,
} from '../selectors/stickers'; } from '../selectors/stickers';
import { useStickersActions } from '../ducks/stickers';
import { useGlobalModalActions } from '../ducks/globalModals';
export type ExternalProps = { export type ExternalProps = {
packId: string; packId: string;
}; };
const mapStateToProps = (state: StateType, props: ExternalProps) => { export const SmartStickerPreviewModal = memo(function SmartStickerPreviewModal({
const { packId } = props; packId,
const stickersPath = getStickersPath(state); }: ExternalProps) {
const tempPath = getTempPath(state); 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 { downloadStickerPack, installStickerPack, uninstallStickerPack } =
const blessedPacks = getBlessedPacks(state); useStickersActions();
const pack = packs[packId]; const { closeStickerPackPreview } = useGlobalModalActions();
return { const packDb = packs[packId];
...props, const pack = packDb
pack: pack ? translatePackFromDB(packDb, packs, blessedPacks, stickersPath, tempPath)
? translatePackFromDB(pack, packs, blessedPacks, stickersPath, tempPath) : undefined;
: undefined,
i18n: getIntl(state),
};
};
const smart = connect(mapStateToProps, mapDispatchToProps); return (
<StickerPreviewModal
export const SmartStickerPreviewModal = smart(StickerPreviewModal); closeStickerPackPreview={closeStickerPackPreview}
downloadStickerPack={downloadStickerPack}
i18n={i18n}
installStickerPack={installStickerPack}
pack={pack}
uninstallStickerPack={uninstallStickerPack}
/>
);
});

View file

@ -1,11 +1,8 @@
// Copyright 2022 Signal Messenger, LLC // Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React from 'react'; import React, { memo } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import type { LocalizerType } from '../../types/Util';
import type { StateType } from '../reducer';
import { StoriesSettingsModal } from '../../components/StoriesSettingsModal'; import { StoriesSettingsModal } from '../../components/StoriesSettingsModal';
import { import {
getAllSignalConnections, getAllSignalConnections,
@ -23,7 +20,8 @@ import { useStoryDistributionListsActions } from '../ducks/storyDistributionList
import { useStoriesActions } from '../ducks/stories'; import { useStoriesActions } from '../ducks/stories';
import { useConversationsActions } from '../ducks/conversations'; import { useConversationsActions } from '../ducks/conversations';
export function SmartStoriesSettingsModal(): JSX.Element | null { export const SmartStoriesSettingsModal = memo(
function SmartStoriesSettingsModal() {
const { setStoriesDisabled } = useStoriesActions(); const { setStoriesDisabled } = useStoriesActions();
const { hideStoriesSettings, toggleSignalConnectionsModal } = const { hideStoriesSettings, toggleSignalConnectionsModal } =
useGlobalModalActions(); useGlobalModalActions();
@ -37,13 +35,12 @@ export function SmartStoriesSettingsModal(): JSX.Element | null {
updateStoryViewers, updateStoryViewers,
} = useStoryDistributionListsActions(); } = useStoryDistributionListsActions();
const { toggleGroupsForStorySend } = useConversationsActions(); const { toggleGroupsForStorySend } = useConversationsActions();
const signalConnections = useSelector(getAllSignalConnections);
const signalConnections = useSelector(getAllSignalConnections);
const getPreferredBadge = useSelector(getPreferredBadgeSelector); const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const storyViewReceiptsEnabled = useSelector(getHasStoryViewReceiptSetting); const storyViewReceiptsEnabled = useSelector(getHasStoryViewReceiptSetting);
const i18n = useSelector<StateType, LocalizerType>(getIntl); const i18n = useSelector(getIntl);
const me = useSelector(getMe); const me = useSelector(getMe);
const candidateConversations = useSelector(getCandidateContactsForNewGroup); const candidateConversations = useSelector(getCandidateContactsForNewGroup);
const distributionLists = useSelector(getDistributionListsWithMembers); const distributionLists = useSelector(getDistributionListsWithMembers);
const groupStories = useSelector(getGroupStories); const groupStories = useSelector(getGroupStories);
@ -79,3 +76,4 @@ export function SmartStoriesSettingsModal(): JSX.Element | null {
/> />
); );
} }
);

View file

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

View file

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

View file

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

View file

@ -1,46 +1,47 @@
// Copyright 2019 Signal Messenger, LLC // Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { isEmpty, pick } from 'lodash'; import { isEmpty } from 'lodash';
import React from 'react'; import React, { memo, useCallback } from 'react';
import { connect } from 'react-redux'; import { useSelector } from 'react-redux';
import type { ReadonlyDeep } from 'type-fest'; import type { ReadonlyDeep } from 'type-fest';
import { mapDispatchToProps } from '../actions';
import type { WarningType as TimelineWarningType } from '../../components/conversation/Timeline'; import type { WarningType as TimelineWarningType } from '../../components/conversation/Timeline';
import { Timeline } from '../../components/conversation/Timeline'; import { Timeline } from '../../components/conversation/Timeline';
import type { StateType } from '../reducer'; import { ContactSpoofingType } from '../../util/contactSpoofing';
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 { getGroupMemberships } from '../../util/getGroupMemberships'; import { getGroupMemberships } from '../../util/getGroupMemberships';
import { import {
dehydrateCollisionsWithConversations, dehydrateCollisionsWithConversations,
getCollisionsFromMemberships, getCollisionsFromMemberships,
} from '../../util/groupMemberNameCollisions'; } 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 { 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 { SmartMiniPlayer } from './MiniPlayer';
import { SmartTimelineItem, type SmartTimelineItemProps } from './TimelineItem';
import { SmartTypingBubble } from './TypingBubble';
type ExternalProps = { type ExternalProps = {
id: string; id: string;
@ -144,60 +145,145 @@ const getWarning = (
} }
}; };
const mapStateToProps = (state: StateType, props: ExternalProps) => { export const SmartTimeline = memo(function SmartTimeline({
const { id } = props;
const conversation = getConversationSelector(state)(id);
const conversationMessages = getConversationMessagesSelector(state)(id);
const targetedMessage = getTargetedMessage(state);
const getTimestampForMessage = (messageId: string): undefined | number =>
getMessages(state)[messageId]?.timestamp;
const shouldShowMiniPlayer = Boolean(selectAudioPlayerActive(state));
return {
id, id,
...pick(conversation, [ }: ExternalProps) {
'unreadCount', const activeAudioPlayer = useSelector(selectAudioPlayerActive);
'unreadMentionsCount', const conversationMessagesSelector = useSelector(
'isGroupV1AndDisabled', getConversationMessagesSelector
'typingContactIdTimestamps', );
]), const conversationSelector = useSelector(getConversationSelector);
isBlocked: conversation.isBlocked ?? false, const getPreferredBadge = useSelector(getPreferredBadgeSelector);
isConversationSelected: state.conversations.selectedConversationId === id, const hasContactSpoofingReview = useSelector(getHasContactSpoofingReview);
isIncomingMessageRequest: Boolean( const i18n = useSelector(getIntl);
!conversation.acceptedMessageRequest && const invitedContactsForNewlyCreatedGroup = useSelector(
conversation.removalStage !== 'justNotification' getInvitedContactsForNewlyCreatedGroup
), );
isSomeoneTyping: Boolean( const messages = useSelector(getMessages);
Object.keys(conversation.typingContactIdTimestamps ?? {}).length > 0 const selectedConversationId = useSelector(getSelectedConversationId);
), const targetedMessage = useSelector(getTargetedMessage);
...conversationMessages, const theme = useSelector(getTheme);
invitedContactsForNewlyCreatedGroup: const conversation = conversationSelector(id);
getInvitedContactsForNewlyCreatedGroup(state), const conversationMessages = conversationMessagesSelector(id);
targetedMessageId: targetedMessage ? targetedMessage.id : undefined,
shouldShowMiniPlayer,
warning: getWarning(conversation, state), const warning = useSelector(
hasContactSpoofingReview: state.conversations.hasContactSpoofingReview, useCallback(
(state: StateType) => {
return getWarning(conversation, state);
},
[conversation]
)
);
getTimestampForMessage, const {
getPreferredBadge: getPreferredBadgeSelector(state), acknowledgeGroupMemberNameCollisions,
i18n: getIntl(state), clearInvitedServiceIdsForNewlyCreatedGroup,
theme: getTheme(state), clearTargetedMessage,
closeContactSpoofingReview,
discardMessages,
loadNewerMessages,
loadNewestMessages,
loadOlderMessages,
markMessageRead,
reviewConversationNameCollision,
scrollToOldestUnreadMention,
setIsNearBottom,
targetMessage,
} = useConversationsActions();
const { peekGroupCallForTheFirstTime, peekGroupCallIfItHasMembers } =
useCallingActions();
renderCollidingAvatars, const getTimestampForMessage = useCallback(
renderContactSpoofingReviewDialog, (messageId: string): undefined | number => {
renderHeroRow, return messages[messageId]?.timestamp;
renderItem, },
renderMiniPlayer, [messages]
renderTypingBubble, );
};
};
const smart = connect(mapStateToProps, mapDispatchToProps); 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;
export const SmartTimeline = smart(Timeline); const isConversationSelected = selectedConversationId === id;
const isIncomingMessageRequest =
!acceptedMessageRequest && removalStage !== 'justNotification';
const isSomeoneTyping = Object.keys(typingContactIdTimestamps).length > 0;
const targetedMessageId = targetedMessage?.id;
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 // SPDX-License-Identifier: AGPL-3.0-only
import type { RefObject } from 'react'; import type { RefObject } from 'react';
import React, { useCallback } from 'react'; import React, { useCallback, memo } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { TimelineItem } from '../../components/conversation/TimelineItem'; import { TimelineItem } from '../../components/conversation/TimelineItem';
@ -56,7 +56,9 @@ function renderContact(contactId: string): JSX.Element {
function renderUniversalTimerNotification(): JSX.Element { function renderUniversalTimerNotification(): JSX.Element {
return <SmartUniversalTimerNotification />; return <SmartUniversalTimerNotification />;
} }
export function SmartTimelineItem(props: SmartTimelineItemProps): JSX.Element { export const SmartTimelineItem = memo(function SmartTimelineItem(
props: SmartTimelineItemProps
): JSX.Element {
const { const {
containerElementRef, containerElementRef,
containerWidthBreakpoint, containerWidthBreakpoint,
@ -224,4 +226,4 @@ export function SmartTimelineItem(props: SmartTimelineItemProps): JSX.Element {
toggleSelectMessage={toggleSelectMessage} toggleSelectMessage={toggleSelectMessage}
/> />
); );
} });

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