Skip to content

Commit

Permalink
feat: refactor chat msg fetching and order contacts by most recent msg (
Browse files Browse the repository at this point in the history
#1126)

Signed-off-by: Bryce McMath <[email protected]>
  • Loading branch information
bryce-mcmath authored Apr 30, 2024
1 parent 915c951 commit f14b18a
Show file tree
Hide file tree
Showing 12 changed files with 456 additions and 386 deletions.
90 changes: 16 additions & 74 deletions packages/legacy/core/App/components/listItems/ContactListItem.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,17 @@
import type {
BasicMessageRecord,
ConnectionRecord,
CredentialExchangeRecord,
ProofExchangeRecord,
} from '@credo-ts/core'
import type { ConnectionRecord } from '@credo-ts/core'

import { useBasicMessagesByConnectionId } from '@credo-ts/react-hooks'
import { StackNavigationProp } from '@react-navigation/stack'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import React, { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native'
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'
import { Role } from '../../types/chat'
import { useChatMessagesByConnection } from '../../hooks/chat-messages'
import { ContactStackParams, Screens, Stacks } from '../../types/navigators'
import {
formatTime,
getConnectionName,
getCredentialEventLabel,
getCredentialEventRole,
getProofEventLabel,
getProofEventRole,
} from '../../utils/helpers'
import { formatTime, getConnectionName } from '../../utils/helpers'
import { testIdWithKey } from '../../utils/testable'

interface CondensedMessage {
text: string
createdAt: Date
}
interface Props {
contact: ConnectionRecord
navigation: StackNavigationProp<ContactStackParams, Screens.Contacts>
Expand All @@ -39,10 +20,9 @@ interface Props {
const ContactListItem: React.FC<Props> = ({ contact, navigation }) => {
const { t } = useTranslation()
const { TextTheme, ColorPallet, ListItems } = useTheme()
const basicMessages = useBasicMessagesByConnectionId(contact.id)
const credentials = useCredentialsByConnectionId(contact.id)
const proofs = useProofsByConnectionId(contact.id)
const [message, setMessage] = useState<CondensedMessage>({ text: '', createdAt: contact.createdAt })
const messages = useChatMessagesByConnection(contact)
const message = messages[0]
const hasOnlyInitialMessage = messages.length < 2
const [store] = useStore()

const styles = StyleSheet.create({
Expand Down Expand Up @@ -90,48 +70,6 @@ const ContactListItem: React.FC<Props> = ({ contact, navigation }) => {
},
})

useEffect(() => {
const transformedMessages: Array<CondensedMessage> = basicMessages.map((record: BasicMessageRecord) => {
return {
text: record.content,
createdAt: record.updatedAt || record.createdAt,
}
})

transformedMessages.push(
...credentials.map((record: CredentialExchangeRecord) => {
const role = getCredentialEventRole(record)
const userLabel = role === Role.me ? `${t('Chat.UserYou')} ` : ''
const actionLabel = t(getCredentialEventLabel(record) as any)
return {
text: `${userLabel}${actionLabel}.`,
createdAt: record.updatedAt || record.createdAt,
}
})
)

transformedMessages.push(
...proofs.map((record: ProofExchangeRecord) => {
const role = getProofEventRole(record)
const userLabel = role === Role.me ? `${t('Chat.UserYou')} ` : ''
const actionLabel = t(getProofEventLabel(record) as any)

return {
text: `${userLabel}${actionLabel}.`,
createdAt: record.updatedAt || record.createdAt,
}
})
)

// don't show a message snippet for the initial connection
const connectedMessage = {
text: '',
createdAt: contact.createdAt,
}

setMessage([...transformedMessages.sort((a: any, b: any) => b.createdAt - a.createdAt), connectedMessage][0])
}, [basicMessages, credentials, proofs])

const navigateToContact = useCallback(() => {
navigation
.getParent()
Expand Down Expand Up @@ -169,13 +107,17 @@ const ContactListItem: React.FC<Props> = ({ contact, navigation }) => {
<Text style={styles.contactNameText}>{contactLabel}</Text>
</View>
<View style={styles.timeContainer}>
<Text style={styles.timeText}>{formatTime(message.createdAt, { shortMonth: true, trim: true })}</Text>
{message && (
<Text style={styles.timeText}>{formatTime(message.createdAt, { shortMonth: true, trim: true })}</Text>
)}
</View>
</View>
<View>
<Text style={TextTheme.normal} numberOfLines={1} ellipsizeMode={'tail'}>
{message.text}
</Text>
{message && !hasOnlyInitialMessage && (
<Text style={TextTheme.normal} numberOfLines={1} ellipsizeMode={'tail'}>
{message.text}
</Text>
)}
</View>
</View>
</View>
Expand Down
243 changes: 243 additions & 0 deletions packages/legacy/core/App/hooks/chat-messages.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
import {
BasicMessageRecord,
ConnectionRecord,
CredentialExchangeRecord,
CredentialState,
ProofExchangeRecord,
ProofState,
} from '@credo-ts/core'
import { useBasicMessagesByConnectionId } from '@credo-ts/react-hooks'
import { isPresentationReceived } from '@hyperledger/aries-bifold-verifier'
import { useNavigation } from '@react-navigation/core'
import { StackNavigationProp } from '@react-navigation/stack'
import React, { Fragment, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Linking, Text } from 'react-native'

import { ChatEvent } from '../components/chat/ChatEvent'
import { ExtendedChatMessage, CallbackType } from '../components/chat/ChatMessage'
import { useStore } from '../contexts/store'
import { useTheme } from '../contexts/theme'
import { useCredentialsByConnectionId } from '../hooks/credentials'
import { useProofsByConnectionId } from '../hooks/proofs'
import { Role } from '../types/chat'
import { RootStackParams, ContactStackParams, Screens, Stacks } from '../types/navigators'
import {
getConnectionName,
getCredentialEventLabel,
getCredentialEventRole,
getMessageEventRole,
getProofEventLabel,
getProofEventRole,
} from '../utils/helpers'

/**
* Determines the callback to be called when the button below a given chat message is pressed, if it exists.
*
* eg. 'View offer' -> opens the credential offer screen
*
* @param {CredentialExchangeRecord | ProofExchangeRecord} record - The record to determine the callback type for.
* @returns {CallbackType} The callback type for the given record.
*/
const callbackTypeForMessage = (record: CredentialExchangeRecord | ProofExchangeRecord) => {
if (
record instanceof CredentialExchangeRecord &&
(record.state === CredentialState.Done || record.state === CredentialState.OfferReceived)
) {
return CallbackType.CredentialOffer
}

if (
(record instanceof ProofExchangeRecord && isPresentationReceived(record) && record.isVerified !== undefined) ||
record.state === ProofState.RequestReceived ||
(record.state === ProofState.Done && record.isVerified === undefined)
) {
return CallbackType.ProofRequest
}

if (
record instanceof ProofExchangeRecord &&
(record.state === ProofState.PresentationSent || record.state === ProofState.Done)
) {
return CallbackType.PresentationSent
}
}

/**
* Custom hook for retrieving chat messages for a given connection. This hook includes some of
* the JSX for rendering the chat messages, including the logic for handling links in messages.
*
* @param {ConnectionRecord} connection - The connection to retrieve chat messages for.
* @returns {ExtendedChatMessage[]} The chat messages for the given connection.
*/
export const useChatMessagesByConnection = (connection: ConnectionRecord): ExtendedChatMessage[] => {
const [messages, setMessages] = useState<Array<ExtendedChatMessage>>([])
const [store] = useStore()
const { t } = useTranslation()
const { ChatTheme: theme, ColorPallet } = useTheme()
const navigation = useNavigation<StackNavigationProp<RootStackParams | ContactStackParams>>()
const basicMessages = useBasicMessagesByConnectionId(connection.id)
const credentials = useCredentialsByConnectionId(connection.id)
const proofs = useProofsByConnectionId(connection.id)
const [theirLabel, setTheirLabel] = useState(getConnectionName(connection, store.preferences.alternateContactNames))

// This useEffect is for properly rendering changes to the alt contact name, useMemo did not pick them up
useEffect(() => {
setTheirLabel(getConnectionName(connection, store.preferences.alternateContactNames))
}, [connection, store.preferences.alternateContactNames])

useEffect(() => {
const transformedMessages: Array<ExtendedChatMessage> = basicMessages.map((record: BasicMessageRecord) => {
const role = getMessageEventRole(record)
// eslint-disable-next-line
const linkRegex = /(?:https?\:\/\/\w+(?:\.\w+)+\S*)|(?:[\w\d\.\_\-]+@\w+(?:\.\w+)+)/gm
// eslint-disable-next-line
const mailRegex = /^[\w\d\.\_\-]+@\w+(?:\.\w+)+$/gm
const links = record.content.match(linkRegex) ?? []
const handleLinkPress = (link: string) => {
if (link.match(mailRegex)) {
link = 'mailto:' + link
}
Linking.openURL(link)
}
const msgText = (
<Text style={role === Role.me ? theme.rightText : theme.leftText}>
{record.content.split(linkRegex).map((split, i) => {
if (i < links.length) {
const link = links[i]
return (
<Fragment key={`${record.id}-${i}`}>
<Text>{split}</Text>
<Text
onPress={() => handleLinkPress(link)}
style={{ color: ColorPallet.brand.link, textDecorationLine: 'underline' }}
accessibilityRole={'link'}
>
{link}
</Text>
</Fragment>
)
}
return <Text key={`${record.id}-${i}`}>{split}</Text>
})}
</Text>
)
return {
_id: record.id,
text: record.content,
renderEvent: () => msgText,
createdAt: record.updatedAt || record.createdAt,
type: record.type,
user: { _id: role },
}
})

transformedMessages.push(
...credentials.map((record: CredentialExchangeRecord) => {
const role = getCredentialEventRole(record)
const userLabel = role === Role.me ? t('Chat.UserYou') : theirLabel
const actionLabel = t(getCredentialEventLabel(record) as any)

return {
_id: record.id,
text: actionLabel,
renderEvent: () => <ChatEvent role={role} userLabel={userLabel} actionLabel={actionLabel} />,
createdAt: record.updatedAt || record.createdAt,
type: record.type,
user: { _id: role },
messageOpensCallbackType: callbackTypeForMessage(record),
onDetails: () => {
const navMap: { [key in CredentialState]?: () => void } = {
[CredentialState.Done]: () => {
navigation.navigate(Stacks.ContactStack as any, {
screen: Screens.CredentialDetails,
params: { credential: record },
})
},
[CredentialState.OfferReceived]: () => {
navigation.navigate(Stacks.ContactStack as any, {
screen: Screens.CredentialOffer,
params: { credentialId: record.id },
})
},
}
const nav = navMap[record.state]
if (nav) {
nav()
}
},
}
})
)

transformedMessages.push(
...proofs.map((record: ProofExchangeRecord) => {
const role = getProofEventRole(record)
const userLabel = role === Role.me ? t('Chat.UserYou') : theirLabel
const actionLabel = t(getProofEventLabel(record) as any)

return {
_id: record.id,
text: actionLabel,
renderEvent: () => <ChatEvent role={role} userLabel={userLabel} actionLabel={actionLabel} />,
createdAt: record.updatedAt || record.createdAt,
type: record.type,
user: { _id: role },
messageOpensCallbackType: callbackTypeForMessage(record),
onDetails: () => {
const toProofDetails = () => {
navigation.navigate(Stacks.ContactStack as any, {
screen: Screens.ProofDetails,
params: {
recordId: record.id,
isHistory: true,
senderReview:
record.state === ProofState.PresentationSent ||
(record.state === ProofState.Done && record.isVerified === undefined),
},
})
}
const navMap: { [key in ProofState]?: () => void } = {
[ProofState.Done]: toProofDetails,
[ProofState.PresentationSent]: toProofDetails,
[ProofState.PresentationReceived]: toProofDetails,
[ProofState.RequestReceived]: () => {
navigation.navigate(Stacks.ContactStack as any, {
screen: Screens.ProofRequest,
params: { proofId: record.id },
})
},
}
const nav = navMap[record.state]
if (nav) {
nav()
}
},
}
})
)

const connectedMessage = connection
? {
_id: 'connected',
text: `${t('Chat.YouConnected')} ${theirLabel}`,
renderEvent: () => (
<Text style={theme.rightText}>
{t('Chat.YouConnected')}
<Text style={[theme.rightText, theme.rightTextHighlighted]}> {theirLabel}</Text>
</Text>
),
createdAt: connection.createdAt,
user: { _id: Role.me },
}
: undefined

setMessages(
connectedMessage
? [...transformedMessages.sort((a: any, b: any) => b.createdAt - a.createdAt), connectedMessage]
: transformedMessages.sort((a: any, b: any) => b.createdAt - a.createdAt)
)
}, [basicMessages, credentials, proofs, theirLabel])

return messages
}
2 changes: 2 additions & 0 deletions packages/legacy/core/App/localization/en/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ const translation = {
"Message1044": "There was a problem while initializing onboarding.",
"Title1045": "Unable to initialize agent.",
"Message1045": "There was a problem while initializing agent.",
"Title1046": "Unable to fetch contacts.",
"Message1046": "There was a problem while fetching contacts.",
},
"ActivityLog": {
"Your": "Your",
Expand Down
2 changes: 2 additions & 0 deletions packages/legacy/core/App/localization/fr/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ const translation = {
"Message1044": "There was a problem while initializing onboarding. (FR)",
"Title1045": "Unable to initialize agent. (FR)",
"Message1045": "There was a problem while initializing agent. (FR)",
"Title1046": "Unable to fetch contacts. (FR)",
"Message1046": "There was a problem while fetching contacts. (FR)",
},
"ActivityLog": {
"Your": "Votre ",
Expand Down
2 changes: 2 additions & 0 deletions packages/legacy/core/App/localization/pt-br/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ const translation = {
"Message1044": "Ocorreu um problema ao incializar o onboarding.",
"Title1045": "Não foi possível inicializar o agente.",
"Message1045": "Ocorreu um erro ao inicializar o agente.",
"Title1046": "Unable to fetch contacts. (PT-BR)",
"Message1046": "There was a problem while fetching contacts. (PT-BR)",
},
"StatusMessages": {
"InitAgent": "Iniciando agente .."
Expand Down
Loading

0 comments on commit f14b18a

Please sign in to comment.