From 6c7384ab7c3d0080150f1eae246e16711001a5cc Mon Sep 17 00:00:00 2001 From: Bryce McMath Date: Tue, 26 Sep 2023 15:34:46 -0700 Subject: [PATCH 1/3] feat: option to rename contacts Signed-off-by: Bryce McMath --- .../components/listItems/ContactListItem.tsx | 12 +- .../listItems/NotificationListItem.tsx | 15 +- .../core/App/contexts/reducers/store.ts | 23 + packages/legacy/core/App/contexts/store.tsx | 1 + .../legacy/core/App/localization/en/index.ts | 9 + .../legacy/core/App/localization/fr/index.ts | 7 +- .../core/App/localization/pt-br/index.ts | 5 + .../core/App/navigators/ContactStack.tsx | 6 + packages/legacy/core/App/screens/Chat.tsx | 24 +- .../core/App/screens/ContactDetails.tsx | 72 +- .../legacy/core/App/screens/ProofDetails.tsx | 10 +- .../legacy/core/App/screens/ProofRequest.tsx | 9 +- .../App/screens/ProofRequestUsageHistory.tsx | 12 +- .../legacy/core/App/screens/RenameContact.tsx | 161 ++ packages/legacy/core/App/types/navigators.ts | 2 + packages/legacy/core/App/types/state.ts | 1 + packages/legacy/core/App/utils/helpers.ts | 10 - .../core/__tests__/reducers/store.test.ts | 8 + .../__tests__/screens/ContactDetails.test.tsx | 107 + .../__tests__/screens/RenameContact.test.tsx | 47 + .../ContactDetails.test.tsx.snap | 2261 +++++++++++++++++ 21 files changed, 2759 insertions(+), 43 deletions(-) create mode 100644 packages/legacy/core/App/screens/RenameContact.tsx create mode 100644 packages/legacy/core/__tests__/screens/ContactDetails.test.tsx create mode 100644 packages/legacy/core/__tests__/screens/RenameContact.test.tsx create mode 100644 packages/legacy/core/__tests__/screens/__snapshots__/ContactDetails.test.tsx.snap diff --git a/packages/legacy/core/App/components/listItems/ContactListItem.tsx b/packages/legacy/core/App/components/listItems/ContactListItem.tsx index 4e6216d452..b66ed1cdb7 100644 --- a/packages/legacy/core/App/components/listItems/ContactListItem.tsx +++ b/packages/legacy/core/App/components/listItems/ContactListItem.tsx @@ -11,6 +11,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { View, StyleSheet, TouchableOpacity, Image, Text } from 'react-native' +import { useStore } from '../../contexts/store' import { useTheme } from '../../contexts/theme' import { useCredentialsByConnectionId } from '../../hooks/credentials' import { useProofsByConnectionId } from '../../hooks/proofs' @@ -41,6 +42,7 @@ const ContactListItem: React.FC = ({ contact, navigation }) => { const credentials = useCredentialsByConnectionId(contact.id) const proofs = useProofsByConnectionId(contact.id) const [message, setMessage] = useState({ text: '', createdAt: contact.createdAt }) + const [store] = useStore() const styles = StyleSheet.create({ container: { @@ -132,8 +134,14 @@ const ContactListItem: React.FC = ({ contact, navigation }) => { ?.navigate(Stacks.ContactStack, { screen: Screens.Chat, params: { connectionId: contact.id } }) }, [contact]) - const contactLabel = useMemo(() => contact.alias || contact.theirLabel, [contact]) - const contactLabelAbbr = useMemo(() => contactLabel?.charAt(0).toUpperCase(), [contact]) + const contactLabel = useMemo( + () => store.preferences.alternateContactNames[contact.id] || contact.alias || contact.theirLabel, + [contact, store.preferences.alternateContactNames] + ) + const contactLabelAbbr = useMemo( + () => contactLabel?.charAt(0).toUpperCase(), + [contact, store.preferences.alternateContactNames] + ) return ( { const NotificationListItem: React.FC = ({ notificationType, notification }) => { const navigation = useNavigation>() const { customNotification } = useConfiguration() - const [, dispatch] = useStore() + const [store, dispatch] = useStore() const { t } = useTranslation() const { ColorPallet, TextTheme } = useTheme() const { agent } = useAgent() @@ -224,14 +224,21 @@ const NotificationListItem: React.FC = ({ notificatio const detailsForNotificationType = async (notificationType: NotificationType): Promise => { return new Promise((resolve) => { + const { connectionId } = notification + let altName = '' + if (connectionId && store.preferences.alternateContactNames[connectionId]) { + altName = store.preferences.alternateContactNames[connectionId] + } + switch (notificationType) { case NotificationType.BasicMessage: resolve({ type: InfoBoxType.Info, title: t('Home.NewMessage'), - body: connection?.theirLabel - ? `${connection.theirLabel} ${t('Home.SentMessage')}` - : t('Home.ReceivedMessage'), + body: + altName || connection?.theirLabel + ? `${altName || connection?.theirLabel} ${t('Home.SentMessage')}` + : t('Home.ReceivedMessage'), buttonTitle: t('Home.ViewMessage'), }) break diff --git a/packages/legacy/core/App/contexts/reducers/store.ts b/packages/legacy/core/App/contexts/reducers/store.ts index 7169232b9b..a36f6c2466 100644 --- a/packages/legacy/core/App/contexts/reducers/store.ts +++ b/packages/legacy/core/App/contexts/reducers/store.ts @@ -46,6 +46,7 @@ enum PreferencesDispatchAction { ACCEPT_DEV_CREDENTIALS = 'preferences/acceptDevCredentials', USE_DATA_RETENTION = 'preferences/useDataRetention', PREVENT_AUTO_LOCK = 'preferences/preventAutoLock', + UPDATE_ALTERNATE_CONTACT_NAMES = 'preferences/updateAlternateContactNames', } enum ToursDispatchAction { @@ -212,6 +213,10 @@ export const reducer = (state: S, action: ReducerAction(state: S, action: ReducerAction { title: t('Screens.ContactDetails'), }} /> + = ({ route }) => { const basicMessages = useBasicMessagesByConnectionId(connectionId) const credentials = useCredentialsByConnectionId(connectionId) const proofs = useProofsByConnectionId(connectionId) - const theirLabel = useMemo(() => connection?.theirLabel || connection?.id || '', [connection]) + const isFocused = useIsFocused() const { assertConnectedNetwork, silentAssertConnectedNetwork } = useNetwork() const [messages, setMessages] = useState>([]) const [showActionSlider, setShowActionSlider] = useState(false) const { ChatTheme: theme, Assets } = useTheme() const { ColorPallet } = useTheme() + const [theirLabel, setTheirLabel] = useState( + (connection?.id && store.preferences.alternateContactNames[connection.id]) || + connection?.theirLabel || + connection?.id || + '' + ) + + // This useEffect is for properly rendering changes to the alt contact name, useMemo did not pick them up + useEffect(() => { + setTheirLabel( + (connection?.id && store.preferences.alternateContactNames[connection.id]) || + connection?.theirLabel || + connection?.id || + '' + ) + }, [isFocused, connection, store.preferences.alternateContactNames]) useMemo(() => { assertConnectedNetwork() @@ -70,7 +86,7 @@ const Chat: React.FC = ({ route }) => { title: theirLabel, headerRight: () => , }) - }, [connection]) + }, [connection, theirLabel]) // when chat is open, mark messages as seen useEffect(() => { @@ -259,7 +275,7 @@ const Chat: React.FC = ({ route }) => { ? [...transformedMessages.sort((a: any, b: any) => b.createdAt - a.createdAt), connectedMessage] : transformedMessages.sort((a: any, b: any) => b.createdAt - a.createdAt) ) - }, [basicMessages, credentials, proofs]) + }, [basicMessages, credentials, proofs, theirLabel]) const onSend = useCallback( async (messages: IMessage[]) => { diff --git a/packages/legacy/core/App/screens/ContactDetails.tsx b/packages/legacy/core/App/screens/ContactDetails.tsx index b40fd869d4..c95467cd87 100644 --- a/packages/legacy/core/App/screens/ContactDetails.tsx +++ b/packages/legacy/core/App/screens/ContactDetails.tsx @@ -1,23 +1,23 @@ import { CredentialState } from '@aries-framework/core' import { useAgent, useConnectionById, useCredentialByState } from '@aries-framework/react-hooks' -import { Attribute } from '@hyperledger/aries-oca/build/legacy' import { useNavigation } from '@react-navigation/core' import { StackNavigationProp, StackScreenProps } from '@react-navigation/stack' -import React, { useCallback, useState } from 'react' +import React, { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import { DeviceEventEmitter } from 'react-native' +import { DeviceEventEmitter, View, Text, TouchableOpacity, StyleSheet } from 'react-native' import { SafeAreaView } from 'react-native-safe-area-context' import Toast from 'react-native-toast-message' import CommonRemoveModal from '../components/modals/CommonRemoveModal' -import RecordRemove from '../components/record/RecordRemove' import { ToastType } from '../components/toast/BaseToast' import { EventTypes } from '../constants' -import { useConfiguration } from '../contexts/configuration' +import { useStore } from '../contexts/store' +import { useTheme } from '../contexts/theme' import { BifoldError } from '../types/error' import { ContactStackParams, Screens, TabStacks } from '../types/navigators' import { ModalUsage } from '../types/remove' import { formatTime } from '../utils/helpers' +import { testIdWithKey } from '../utils/testable' type ContactDetailsProps = StackScreenProps @@ -34,7 +34,15 @@ const ContactDetails: React.FC = ({ route }) => { ...useCredentialByState(CredentialState.CredentialReceived), ...useCredentialByState(CredentialState.Done), ].filter((credential) => credential.connectionId === connection?.id) - const { record } = useConfiguration() + const { ColorPallet, TextTheme } = useTheme() + const [store] = useStore() + + const styles = StyleSheet.create({ + contentContainer: { + padding: 20, + backgroundColor: ColorPallet.brand.secondaryBackground, + }, + }) const handleOnRemove = () => { if (connectionCredentials?.length) { @@ -79,25 +87,55 @@ const ContactDetails: React.FC = ({ route }) => { setIsCredentialsRemoveModalDisplayed(false) } + const handleGoToRename = () => { + navigation.navigate(Screens.RenameContact, { connectionId }) + } + + const callGoToRename = useCallback(() => handleGoToRename(), []) const callOnRemove = useCallback(() => handleOnRemove(), []) const callSubmitRemove = useCallback(() => handleSubmitRemove(), []) const callCancelRemove = useCallback(() => handleCancelRemove(), []) const callGoToCredentials = useCallback(() => handleGoToCredentials(), []) const callCancelUnableToRemove = useCallback(() => handleCancelUnableRemove(), []) + const contactLabel = useMemo( + () => + (connection?.id && store.preferences.alternateContactNames[connection.id]) || + connection?.alias || + connection?.theirLabel, + [connection, store.preferences.alternateContactNames] + ) + return ( - {record({ - fields: [ - { - name: connection?.alias || connection?.theirLabel, - value: t('ContactDetails.DateOfConnection', { - date: connection?.createdAt ? formatTime(connection.createdAt, { includeHour: true }) : '', - }), - }, - ] as Attribute[], - footer: () => , - })} + + {contactLabel} + + {t('ContactDetails.DateOfConnection', { + date: connection?.createdAt ? formatTime(connection.createdAt, { includeHour: true }) : '', + })} + + + + {t('Screens.RenameContact')} + + + + {t('ContactDetails.RemoveContact')} + + = ({ }: VerifiedProofProps) => { const { t } = useTranslation() const { ColorPallet, TextTheme } = useTheme() + const [store] = useStore() const styles = StyleSheet.create({ container: { @@ -98,8 +99,13 @@ const VerifiedProof: React.FC = ({ const connection = useConnectionById(record.connectionId || '') const connectionLabel = useMemo( - () => (connection ? connection?.alias || connection?.theirLabel : t('Verifier.ConnectionLessLabel')), - [connection] + () => + connection + ? (connection?.id && store.preferences.alternateContactNames[connection.id]) || + connection?.alias || + connection?.theirLabel + : t('Verifier.ConnectionLessLabel'), + [connection, store.preferences.alternateContactNames] ) const [sharedProofDataItems, setSharedProofDataItems] = useState([]) diff --git a/packages/legacy/core/App/screens/ProofRequest.tsx b/packages/legacy/core/App/screens/ProofRequest.tsx index 3d37a8625c..b3e808bd5f 100644 --- a/packages/legacy/core/App/screens/ProofRequest.tsx +++ b/packages/legacy/core/App/screens/ProofRequest.tsx @@ -63,7 +63,6 @@ const ProofRequest: React.FC = ({ navigation, route }) => { const fullCredentials = useCredentials().records const proof = useProofById(proofId) const connection = proof?.connectionId ? useConnectionById(proof.connectionId) : undefined - const proofConnectionLabel = connection?.theirLabel ?? proof?.connectionId ?? '' const [pendingModalVisible, setPendingModalVisible] = useState(false) const [revocationOffense, setRevocationOffense] = useState(false) const [retrievedCredentials, setRetrievedCredentials] = useState() @@ -76,6 +75,14 @@ const ProofRequest: React.FC = ({ navigation, route }) => { const { enableTours: enableToursConfig, OCABundleResolver } = useConfiguration() const [containsPI, setContainsPI] = useState(false) const [store, dispatch] = useStore() + const proofConnectionLabel = useMemo( + () => + (proof?.connectionId && store.preferences.alternateContactNames[proof.connectionId]) ?? + connection?.theirLabel ?? + proof?.connectionId ?? + '', + [connection, store.preferences.alternateContactNames] + ) const { start } = useTour() const screenIsFocused = useIsFocused() diff --git a/packages/legacy/core/App/screens/ProofRequestUsageHistory.tsx b/packages/legacy/core/App/screens/ProofRequestUsageHistory.tsx index e55bc55313..52698cd640 100644 --- a/packages/legacy/core/App/screens/ProofRequestUsageHistory.tsx +++ b/packages/legacy/core/App/screens/ProofRequestUsageHistory.tsx @@ -9,6 +9,7 @@ import Icon from 'react-native-vector-icons/MaterialIcons' import { useProofsByTemplateId, isPresentationReceived } from '../../verifier' import EmptyList from '../components/misc/EmptyList' +import { useStore } from '../contexts/store' import { useTheme } from '../contexts/theme' import { ProofRequestsStackParams, Screens } from '../types/navigators' import { formatTime } from '../utils/helpers' @@ -40,8 +41,15 @@ const getPresentationStateLabel = (record: ProofExchangeRecord) => { const ProofRequestUsageHistoryRecord: React.FC = ({ record, navigation }) => { const { t } = useTranslation() const { ListItems, ColorPallet } = useTheme() - + const [store] = useStore() const connection = record.connectionId ? useConnectionById(record.connectionId) : undefined + const theirLabel = useMemo( + () => + (record.connectionId && store.preferences.alternateContactNames[record.connectionId]) || + connection?.theirLabel || + connection?.alias, + [record.connectionId, store.preferences.alternateContactNames] + ) const style = StyleSheet.create({ card: { @@ -101,7 +109,7 @@ const ProofRequestUsageHistoryRecord: React.FC {t('Verifier.PresentationFrom')}: - {connection?.theirLabel || t('Verifier.ConnectionlessPresentation')} + {theirLabel || t('Verifier.ConnectionlessPresentation')} {t('Verifier.PresentationState')}: diff --git a/packages/legacy/core/App/screens/RenameContact.tsx b/packages/legacy/core/App/screens/RenameContact.tsx new file mode 100644 index 0000000000..14c2e1aedb --- /dev/null +++ b/packages/legacy/core/App/screens/RenameContact.tsx @@ -0,0 +1,161 @@ +import { useConnectionById } from '@aries-framework/react-hooks' +import { useNavigation } from '@react-navigation/core' +import { StackScreenProps } from '@react-navigation/stack' +import React, { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { StyleSheet, Text, View } from 'react-native' + +import ButtonLoading from '../components/animated/ButtonLoading' +import Button, { ButtonType } from '../components/buttons/Button' +import LimitedTextInput from '../components/inputs/LimitedTextInput' +import { InfoBoxType } from '../components/misc/InfoBox' +import PopupModal from '../components/modals/PopupModal' +import KeyboardView from '../components/views/KeyboardView' +import { DispatchAction } from '../contexts/reducers/store' +import { useStore } from '../contexts/store' +import { useTheme } from '../contexts/theme' +import { ContactStackParams, Screens } from '../types/navigators' +import { testIdWithKey } from '../utils/testable' + +type ErrorState = { + visible: boolean + title: string + description: string +} + +type RenameContactProps = StackScreenProps + +const RenameContact: React.FC = ({ route }) => { + if (!route?.params) { + throw new Error('RenameContact route params were not set properly') + } + + const { connectionId } = route.params + const connection = useConnectionById(connectionId) + const { t } = useTranslation() + const { ColorPallet, TextTheme } = useTheme() + const navigation = useNavigation() + const [store, dispatch] = useStore() + const [contactName, setContactName] = useState( + store.preferences.alternateContactNames[connectionId] || connection?.theirLabel || '' + ) + const [loading, setLoading] = useState(false) + const [errorState, setErrorState] = useState({ + visible: false, + title: '', + description: '', + }) + + const styles = StyleSheet.create({ + screenContainer: { + height: '100%', + backgroundColor: ColorPallet.brand.primaryBackground, + padding: 20, + justifyContent: 'space-between', + }, + + contentContainer: { + justifyContent: 'center', + alignItems: 'center', + width: '100%', + }, + // below used as helpful label for view, no properties needed atp + controlsContainer: {}, + + buttonContainer: { + width: '100%', + }, + }) + + const handleChangeText = (text: string) => { + setContactName(text) + } + + const handleCancelPressed = () => { + navigation.goBack() + } + + const handleContinuePressed = () => { + if (contactName.length < 1) { + setErrorState({ + title: t('RenameContact.EmptyNameTitle'), + description: t('RenameContact.EmptyNameDescription'), + visible: true, + }) + } else if (contactName.length > 50) { + setErrorState({ + title: t('RenameContact.CharCountTitle'), + description: t('RenameContact.CharCountDescription'), + visible: true, + }) + } else { + setLoading(true) + dispatch({ + type: DispatchAction.UPDATE_ALTERNATE_CONTACT_NAMES, + payload: [{ [connectionId]: contactName }], + }) + setLoading(false) + navigation.goBack() + } + } + + const handleDismissError = () => { + setErrorState((prev) => ({ ...prev, visible: false })) + } + + return ( + + + + + {t('RenameContact.ThisContactName')} + + + + + + + + + +