diff --git a/lib/actions/user-actions.js b/lib/actions/user-actions.js --- a/lib/actions/user-actions.js +++ b/lib/actions/user-actions.js @@ -1470,10 +1470,11 @@ const client = React.useContext(IdentityClientContext); const identityClient = client?.identityClient; invariant(identityClient, 'Identity client should be set'); - return async () => { - const supported = await identityClient.versionSupported(); + const { versionSupported } = identityClient; + return React.useCallback(async () => { + const supported = await versionSupported(); return { supported }; - }; + }, [versionSupported]); } const restoreUserActionTypes = Object.freeze({ diff --git a/lib/hooks/ens-cache.js b/lib/hooks/ens-cache.js --- a/lib/hooks/ens-cache.js +++ b/lib/hooks/ens-cache.js @@ -139,9 +139,10 @@ function useStringForUser( user: ?{ +username?: ?string, +isViewer?: ?boolean, ... }, ): ?string { - const toFetch = user?.isViewer ? null : user; // stringForUser ignores username is isViewer, so we skip the ENS fetch - const [result] = useENSNames([toFetch]); + const toFetch = user?.isViewer ? null : user; + const usersObjArr = React.useMemo(() => [toFetch], [toFetch]); + const [result] = useENSNames(usersObjArr); if (user?.isViewer) { return stringForUser(user); } else if (result) { diff --git a/lib/shared/user-utils.js b/lib/shared/user-utils.js --- a/lib/shared/user-utils.js +++ b/lib/shared/user-utils.js @@ -1,5 +1,7 @@ // @flow +import * as React from 'react'; + import { roleIsAdminRole } from './thread-utils.js'; import { useENSNames } from '../hooks/ens-cache.js'; import type { @@ -41,7 +43,8 @@ member.role ? roleIsAdminRole(roles[member.role]) : false, ); const adminUserInfo = admin ? userInfos[admin.id] : undefined; - const [adminUserInfoWithENSName] = useENSNames([adminUserInfo]); + const adminUserInfos = React.useMemo(() => [adminUserInfo], [adminUserInfo]); + const [adminUserInfoWithENSName] = useENSNames(adminUserInfos); return adminUserInfoWithENSName; } diff --git a/lib/utils/role-utils.js b/lib/utils/role-utils.js --- a/lib/utils/role-utils.js +++ b/lib/utils/role-utils.js @@ -56,24 +56,30 @@ community ? threadInfoSelector(state)[community] : null, ); const topMostThreadInfo = communityThreadInfo || threadInfo; - const roleMap = new Map(); - if (topMostThreadInfo.type === threadTypes.GENESIS) { - memberInfos.forEach(memberInfo => + return React.useMemo(() => { + const roleMap = new Map(); + + if (topMostThreadInfo.type === threadTypes.GENESIS) { + memberInfos.forEach(memberInfo => + roleMap.set( + memberInfo.id, + memberInfo.role ? threadInfo.roles[memberInfo.role] : null, + ), + ); + return roleMap; + } + + const { members: memberInfosFromTopMostThreadInfo, roles } = + topMostThreadInfo; + memberInfosFromTopMostThreadInfo.forEach(memberInfo => { roleMap.set( memberInfo.id, - memberInfo.role ? threadInfo.roles[memberInfo.role] : null, - ), - ); + memberInfo.role ? roles[memberInfo.role] : null, + ); + }); return roleMap; - } - - const { members: memberInfosFromTopMostThreadInfo, roles } = - topMostThreadInfo; - memberInfosFromTopMostThreadInfo.forEach(memberInfo => { - roleMap.set(memberInfo.id, memberInfo.role ? roles[memberInfo.role] : null); - }); - return roleMap; + }, [topMostThreadInfo, threadInfo, memberInfos]); } function useMembersGroupedByRole( diff --git a/native/account/registration/username-selection.react.js b/native/account/registration/username-selection.react.js --- a/native/account/registration/username-selection.react.js +++ b/native/account/registration/username-selection.react.js @@ -118,26 +118,33 @@ } const styles = useStyles(unboundStyles); + const errorNumberStyle = React.useMemo( + () => [styles.errorText, styles.listItemNumber], + [styles.errorText, styles.listItemNumber], + ); + const errorTextStyle = React.useMemo( + () => [styles.errorText, styles.listItemContent], + [styles.errorText, styles.listItemContent], + ); + let errorText; if (usernameError === 'username_invalid') { errorText = ( <> Usernames must: - {'1. '} - - Be at least one character long. - + {'1. '} + Be at least one character long. - {'2. '} - + {'2. '} + Start with either a letter or a number. - {'3. '} - + {'3. '} + Contain only letters, numbers, or the characters “-” and “_”. diff --git a/native/account/terms-and-privacy-modal.react.js b/native/account/terms-and-privacy-modal.react.js --- a/native/account/terms-and-privacy-modal.react.js +++ b/native/account/terms-and-privacy-modal.react.js @@ -41,6 +41,8 @@ policyAcknowledgmentActionTypes, ); +const safeAreaEdges = ['top', 'bottom']; + function TermsAndPrivacyModal(props: Props): React.Node { const loadingStatus = useSelector(loadingStatusSelector); const [acknowledgmentError, setAcknowledgmentError] = React.useState(''); @@ -89,7 +91,6 @@ }; }, [onBackPress]); - const safeAreaEdges = ['top', 'bottom']; return ( { + const errorColorStyle = props.canUseRed ? { color: 'red' } : { color: props.color }; - return ; + return [styles.errorIcon, errorColorStyle]; + }, [props.canUseRed, props.color]); + if (props.loadingStatus === 'error') { + return ; } else if (props.loadingStatus === 'loading') { return ; } else { diff --git a/native/chat/chat-input-bar.react.js b/native/chat/chat-input-bar.react.js --- a/native/chat/chat-input-bar.react.js +++ b/native/chat/chat-input-bar.react.js @@ -987,6 +987,15 @@ prevEditedMessage.current = editedMessage; }, [blockNavigation, messageEditingContext?.editState.editedMessage]); + const currentlyEditMode = isEditMode(); + const expandoButtonsViewStyle: Array = React.useMemo(() => { + const combined = [styles.innerExpandoButtons]; + if (currentlyEditMode) { + combined.push({ display: 'none' }); + } + return combined; + }, [styles.innerExpandoButtons, currentlyEditMode]); + const renderInput = () => { const expandoButton = ( ); const threadColor = `#${threadInfo.color}`; - const expandoButtonsViewStyle: Array = [ - styles.innerExpandoButtons, - ]; - if (isEditMode()) { - expandoButtonsViewStyle.push({ display: 'none' }); - } return ( @@ -1079,6 +1082,10 @@ const isMember = viewerIsMember(threadInfo); let joinButton = null; const threadColor = `#${threadInfo.color}`; + const joinButtonStyle = React.useMemo( + () => [styles.joinButton, { backgroundColor: threadColor }], + [styles.joinButton, threadColor], + ); if (!isMember && currentUserCanJoin && !threadCreationInProgress) { let buttonContent; @@ -1106,7 +1113,7 @@ @@ -1163,6 +1170,11 @@ ); + const editingLabelStyle = React.useMemo( + () => [{ color: threadColor }, styles.editingLabel], + [threadColor, styles.editingLabel], + ); + let editedMessage; if (isEditMode() && editedMessagePreview) { const { message } = editedMessagePreview; @@ -1174,9 +1186,7 @@ > - - Editing message - + Editing message {message.text} diff --git a/native/chat/inner-robotext-message.react.js b/native/chat/inner-robotext-message.react.js --- a/native/chat/inner-robotext-message.react.js +++ b/native/chat/inner-robotext-message.react.js @@ -144,7 +144,10 @@ } function ColorEntity(props: { +color: string }) { - const colorStyle = { color: props.color }; + const colorStyle = React.useMemo( + () => ({ color: props.color }), + [props.color], + ); return {props.color}; } diff --git a/native/chat/robotext-message.react.js b/native/chat/robotext-message.react.js --- a/native/chat/robotext-message.react.js +++ b/native/chat/robotext-message.react.js @@ -220,12 +220,14 @@ return { opacity: contentAndHeaderOpacity.value }; }); - const viewStyle: { height?: number } = {}; - if (!__DEV__) { - // We don't force view height in dev mode because we - // want to measure it in Message to see if it's correct - viewStyle.height = item.contentHeight; - } + const viewStyle: { height?: number } = React.useMemo(() => { + if (__DEV__) { + // We don't force view height in dev mode because we + // want to measure it in Message to see if it's correct + return {}; + } + return { height: item.contentHeight }; + }, [item.contentHeight]); return ( diff --git a/native/chat/settings/thread-settings-edit-relationship.react.js b/native/chat/settings/thread-settings-edit-relationship.react.js --- a/native/chat/settings/thread-settings-edit-relationship.react.js +++ b/native/chat/settings/thread-settings-edit-relationship.react.js @@ -44,7 +44,11 @@ }); invariant(otherUserInfoFromRedux, 'Other user info should be specified'); - const [otherUserInfo] = useENSNames([otherUserInfoFromRedux]); + const ensNames = React.useMemo( + () => [otherUserInfoFromRedux], + [otherUserInfoFromRedux], + ); + const [otherUserInfo] = useENSNames(ensNames); const updateRelationships = useUpdateRelationships(); const updateRelationship = React.useCallback( diff --git a/native/profile/user-relationship-tooltip-modal.react.js b/native/profile/user-relationship-tooltip-modal.react.js --- a/native/profile/user-relationship-tooltip-modal.react.js +++ b/native/profile/user-relationship-tooltip-modal.react.js @@ -35,17 +35,18 @@ ...UserRelationshipTooltipModalParams, +action: Action, }; -function useRelationshipAction(input: OnRemoveUserProps) { +function useRelationshipAction({ + relativeUserInfo, + action, +}: OnRemoveUserProps) { const updateRelationships = useUpdateRelationships(); const dispatchActionPromise = useDispatchActionPromise(); - const userText = stringForUser(input.relativeUserInfo); + const userText = stringForUser(relativeUserInfo); return React.useCallback(() => { const callRemoveRelationships = async () => { try { - return await updateRelationships(input.action, [ - input.relativeUserInfo.id, - ]); + return await updateRelationships(action, [relativeUserInfo.id]); } catch (e) { Alert.alert( unknownErrorAlertDetails.title, @@ -59,25 +60,25 @@ } }; const onConfirmRemoveUser = () => { - const customKeyName = `${updateRelationshipsActionTypes.started}:${input.relativeUserInfo.id}`; + const customKeyName = `${updateRelationshipsActionTypes.started}:${relativeUserInfo.id}`; void dispatchActionPromise( updateRelationshipsActionTypes, callRemoveRelationships(), { customKeyName }, ); }; - const action = { + const actionStr = { unfriend: 'removal', block: 'block', unblock: 'unblock', - }[input.action]; + }[action]; const message = { unfriend: `remove ${userText} from friends?`, block: `block ${userText}`, unblock: `unblock ${userText}?`, - }[input.action]; + }[action]; Alert.alert( - `Confirm ${action}`, + `Confirm ${actionStr}`, `Are you sure you want to ${message}`, [ { text: 'Cancel', style: 'cancel' }, @@ -85,7 +86,13 @@ ], { cancelable: true }, ); - }, [updateRelationships, dispatchActionPromise, userText, input]); + }, [ + updateRelationships, + dispatchActionPromise, + userText, + relativeUserInfo, + action, + ]); } function TooltipMenu( diff --git a/web/chat/chat-input-text-area.react.js b/web/chat/chat-input-text-area.react.js --- a/web/chat/chat-input-text-area.react.js +++ b/web/chat/chat-input-text-area.react.js @@ -75,28 +75,26 @@ // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentText]); - const onKeyDown = (event: SyntheticKeyboardEvent) => { - if (event.key === 'Escape') { - event.preventDefault(); - if (!escape) { - return; + const onKeyDown = React.useCallback( + (event: SyntheticKeyboardEvent) => { + if (event.key === 'Escape') { + event.preventDefault(); + escape?.(); + } else if (event.key === 'Enter' && !event.shiftKey) { + event.preventDefault(); + send?.(); } - escape(); - } else if (event.key === 'Enter' && !event.shiftKey) { - event.preventDefault(); - if (!send) { - return; - } - send(); - } - }; + }, + [escape, send], + ); - const onChangeMessageText = ( - event: SyntheticEvent, - ) => { - setCurrentText(event.currentTarget.value); - updateHeight(); - }; + const onChangeMessageText = React.useCallback( + (event: SyntheticEvent) => { + setCurrentText(event.currentTarget.value); + updateHeight(); + }, + [setCurrentText, updateHeight], + ); return (
diff --git a/web/chat/edit-text-message.react.js b/web/chat/edit-text-message.react.js --- a/web/chat/edit-text-message.react.js +++ b/web/chat/edit-text-message.react.js @@ -2,7 +2,6 @@ import classNames from 'classnames'; import * as React from 'react'; -import { useCallback } from 'react'; import { XCircle as XCircleIcon } from 'react-feather'; import type { ComposableChatMessageInfoItem } from 'lib/selectors/chat-selectors.js'; @@ -29,6 +28,7 @@ }; const bottomRowStyle = { height: editBoxBottomRowHeight }; +const buttonClassNames = [css.saveButton, css.smallButton]; function EditTextMessage(props: Props): React.Node { const { background, threadInfo, item } = props; @@ -47,10 +47,7 @@ [threadColor], ); - const isMessageEmpty = React.useMemo( - () => trimMessage(editedMessageDraft) === '', - [editedMessageDraft], - ); + const isMessageEmpty = trimMessage(editedMessageDraft) === ''; const isMessageEdited = React.useMemo(() => { const { messageInfo } = item; @@ -64,7 +61,7 @@ return trimmedDraft !== messageInfo.text; }, [editState, editedMessageDraft, item]); - const checkAndEdit = async () => { + const checkAndEdit = React.useCallback(async () => { const { id: messageInfoID } = item.messageInfo; if (isMessageEmpty) { return; @@ -73,18 +70,26 @@ clearEditModal(); return; } - if (!messageInfoID || !editState?.editedMessageDraft) { + if (!messageInfoID || !editedMessageDraft) { return; } try { - await editMessage(messageInfoID, editState.editedMessageDraft); + await editMessage(messageInfoID, editedMessageDraft); clearEditModal(); } catch (e) { setError(true); } - }; - - const updateDimensions = useCallback(() => { + }, [ + item.messageInfo, + isMessageEmpty, + isMessageEdited, + editedMessageDraft, + editMessage, + clearEditModal, + setError, + ]); + + const updateDimensions = React.useCallback(() => { if (!myRef.current || !background) { return; } @@ -156,7 +161,7 @@ {editFailed}