diff --git a/src/components/EmailListItem.tsx b/src/components/EmailListItem.tsx index 1485c4b2491a6214523a5f599bee1b270b66b9f8..81eb10385d784acb7b1d42ab05d5e44f906a57a4 100644 --- a/src/components/EmailListItem.tsx +++ b/src/components/EmailListItem.tsx @@ -42,6 +42,9 @@ const EmailListItem: React.FC<Props> = ({ key={id} > <Shadow style={styles.container}> + {!read && ( + <View style={styles.unread} /> + )} <View style={styles.info}> {subject && <Text style={styles.title}>{subject}</Text>} {sender?.name && <Text style={styles.sender}>{sender.name}</Text>} @@ -77,6 +80,16 @@ const styles = StyleSheet.create({ flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-start', + position: 'relative' + }, + unread: { + position: 'absolute', + width: 14, + height: 14, + backgroundColor: ColorPallet.baseColors.red, + borderRadius: 7, + top: 6, + right: 6 }, icon: { backgroundColor: ColorPallet.brand.primary, diff --git a/src/components/SearchBar.tsx b/src/components/SearchBar.tsx index 4424a6735ba17b3433eb7acdcbaa4ff7d84ef684..fd9db0f5c945591ff98cf10a1a686e1a2abcc91d 100644 --- a/src/components/SearchBar.tsx +++ b/src/components/SearchBar.tsx @@ -12,7 +12,7 @@ import { import { StackScreenProps } from '@react-navigation/stack'; import { useNavigation } from '@react-navigation/core'; import { borderRadius, ColorPallet, TextTheme } from 'src/theme/theme'; -import useNotifications from 'src/hooks/notifications'; +import {useAgentNotifications, useEmailNotifications} from 'src/hooks/notifications'; import SettingsSvg from 'src/assets/svg/settings.svg'; import SearchSvg from 'src/assets/svg/search.svg'; import BellSvg from 'src/assets/svg/bell.svg'; @@ -27,7 +27,10 @@ const SearchBar: React.FC<Props> = ({ searchPhrase, setSearchPhrase }) => { const { t } = useTranslation(); const [focused, setFocused] = useState(false); const navigation = useNavigation<StackScreenProps<SettingStackParams>>(); - const notifications = useNotifications(); + const agentNotifications = useAgentNotifications(); + const emailNotifications = useEmailNotifications(); + + const count = agentNotifications.length + emailNotifications.length; const cancel = () => { Keyboard.dismiss(); @@ -73,9 +76,9 @@ const SearchBar: React.FC<Props> = ({ searchPhrase, setSearchPhrase }) => { }} > <BellSvg style={styles.bellIcon} /> - {notifications.length > 0 && ( + {count > 0 && ( <View style={styles.notificationBadge}> - <Text style={styles.badgeText}>{notifications.length < 100 ? notifications.length : 99}</Text> + <Text style={styles.badgeText}>{count < 100 ? count : 99}</Text> </View> )} </TouchableOpacity> diff --git a/src/components/TextInput.tsx b/src/components/TextInput.tsx index 20826df364cd089e875a1f49b1d2892cdab599f7..72b3f5b1f6d1a0fcc9f8288e2beee81a04840d7a 100644 --- a/src/components/TextInput.tsx +++ b/src/components/TextInput.tsx @@ -58,6 +58,7 @@ const TextInput: React.FC<Props> = ({ onBlur={() => setFocused(false)} {...textInputProps} placeholderTextColor={ColorPallet.grayscale.lightGrey} + autoCapitalize='none' /> </View> ); diff --git a/src/components/notifications/NotificationEmailListItem.tsx b/src/components/notifications/NotificationEmailListItem.tsx new file mode 100644 index 0000000000000000000000000000000000000000..526de36bb6bbdffe8b664b3a6eca4ed2a4aa1d26 --- /dev/null +++ b/src/components/notifications/NotificationEmailListItem.tsx @@ -0,0 +1,111 @@ +import React from 'react'; +import { Pressable, StyleSheet, View } from 'react-native'; +import { format } from "date-fns"; +import { Shadow } from 'react-native-shadow-2'; +import {borderRadius, ColorPallet, TextTheme} from 'src/theme/theme'; +import Text from 'src/components/Text'; +import { DATE_TIME_FORMAT } from "src/constants/constants"; +import {parseAddress} from "src/utils/email"; +import EmailSvg from 'src/assets/svg/verifiable_e-mail2.svg'; + +interface Props { + id: string; + createdAt: Date; + from: string; + to: string; + cc: string; + bcc: string; + subject: string; + sealed: boolean; + sealUrl: string | null | undefined; + onPress?: () => void; +} + +const EmailListItem: React.FC<Props> = ({ + id, + createdAt, + from, + to, + cc, + bcc, + subject, + sealed, + sealUrl, + onPress +}) => { + const sender = parseAddress(from); + return ( + <Pressable + onPress={onPress} + key={id} + > + <View style={styles.container}> + <View style={styles.avatar}> + <EmailSvg style={styles.svg} /> + </View> + <View style={styles.details}> + {subject && <Text style={styles.title}>{subject}</Text>} + {sender?.name && <Text style={styles.sender}>{sender.name}</Text>} + {sender?.email && ( + <Text style={styles.senderEmail}>({sender.email})</Text> + )} + <Text style={styles.date}> + {format(createdAt, DATE_TIME_FORMAT)} + </Text> + </View> + </View> + </Pressable> + ); +}; + +export default EmailListItem; + +const styles = StyleSheet.create({ + container: { + backgroundColor: ColorPallet.grayscale.veryLightGrey, + borderRadius: borderRadius, + padding: 8, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'flex-start', + }, + avatar: { + width: 50, + height: 50, + justifyContent: 'center', + alignItems: 'center', + borderRadius: borderRadius, + }, + svg: { + width: 50, + height: 50, + fill: ColorPallet.brand.primary, + }, + details: { + flex: 1, + flexDirection: 'column', + marginLeft: 10, + flexShrink: 1, + justifyContent: 'flex-start', + }, + + title: { + fontWeight: 'bold', + color: ColorPallet.brand.primary, + fontSize: 20, + }, + sender: { + marginTop: 4, + fontWeight: 'bold', + color: ColorPallet.baseColors.black, + fontSize: 19, + }, + senderEmail: { + color: ColorPallet.baseColors.black, + fontSize: 14, + }, + date: { + color: ColorPallet.baseColors.black, + fontSize: 14, + }, +}); diff --git a/src/components/NotificationListItem/index.tsx b/src/components/notifications/NotificationListItem.tsx similarity index 94% rename from src/components/NotificationListItem/index.tsx rename to src/components/notifications/NotificationListItem.tsx index ade8a309f72828c0847a21788f11f019142d1adf..72e9f53d372054e0dc69f4d2d72465b9e8a9e096 100644 --- a/src/components/NotificationListItem/index.tsx +++ b/src/components/notifications/NotificationListItem.tsx @@ -3,13 +3,13 @@ import { useTranslation } from 'react-i18next'; import {StyleSheet, View, Text, Pressable} from 'react-native'; import {borderRadius, ColorPallet, TextTheme} from 'src/theme/theme'; import Button, { ButtonType } from 'src/components/Button'; -import {Notification} from "src/utils/agentUtils"; +import {AgentNotification} from "src/utils/agentUtils"; import rootStore from "src/store/rootStore"; import {hashCode, hashToRGBA, parseCredDef} from "src/utils/helpers"; import NewConnectionSvg from 'src/assets/svg/new_connection.svg'; interface NotificationListItemProps { - notification: Notification; + notification: AgentNotification; onView?: () => void; } @@ -23,6 +23,7 @@ const NotificationListItem: React.FC<NotificationListItemProps> = ({ useEffect(() => { (async () => { + console.log(notification); if (notification.proofRequest) { setTitle(t<string>('ProofRequest.ProofRequest')); setBody(notification.connection?.theirLabel ?? 'Connectionless proof request'); @@ -38,6 +39,10 @@ const NotificationListItem: React.FC<NotificationListItemProps> = ({ })(); }, []) + // console.log('title'); + // console.log(title); + // console.log(body); + return ( <Pressable testID="notification-list-item" onPress={onView}> diff --git a/src/hooks/notifications.ts b/src/hooks/notifications.ts index 76dc29f337f11a7bf9b8d90dba283d70ce0781ec..c9a7faba83057ad516170cff3b81b15500740e46 100644 --- a/src/hooks/notifications.ts +++ b/src/hooks/notifications.ts @@ -1,9 +1,11 @@ import { useEffect, useState } from 'react'; -import {getNotifications, Notification} from "src/utils/agentUtils"; +import {getNotifications, AgentNotification} from "src/utils/agentUtils"; import rootStore from "src/store/rootStore"; +import {Results, useQuery} from "@realm/react"; +import Email from "../db-models/Email"; -const useNotifications = (): Notification[] => { - const [notifications, setNotifications] = useState<Notification[]>([]); +export const useAgentNotifications = (): AgentNotification[] => { + const [notifications, setNotifications] = useState<AgentNotification[]>([]); const fetchNotifications = async () => { const data = await getNotifications(rootStore.agentStore.agent); @@ -19,4 +21,10 @@ const useNotifications = (): Notification[] => { return notifications; }; -export default useNotifications; +export const useEmailNotifications = (): Results<Email> => { + let emailQuery = useQuery(Email); + emailQuery = emailQuery.filtered(`read == $0`, false); + emailQuery = emailQuery.sorted('createdAt', true); + + return emailQuery; +}; \ No newline at end of file diff --git a/src/screens/Notifications/index.tsx b/src/screens/Notifications/index.tsx index 0676d1a59d7b58b180de551e847e7868a0f59c92..2de3357c685f9284b94f9bc446975ccd457e8ec4 100644 --- a/src/screens/Notifications/index.tsx +++ b/src/screens/Notifications/index.tsx @@ -2,54 +2,109 @@ import React from 'react'; import {FlatList, StatusBar, StyleSheet, View} from 'react-native'; import {useNavigation} from "@react-navigation/core"; import {useTranslation} from "react-i18next"; -import NotificationListItem from 'src/components/NotificationListItem'; +import NotificationListItem from 'src/components/notifications/NotificationListItem'; +import NotificationEmailListItem from 'src/components/notifications/NotificationEmailListItem'; import {observer} from "mobx-react"; -import {Screens} from "src/type/navigators"; -import useNotifications from "src/hooks/notifications"; +import {Screens, TabStacks} from "src/type/navigators"; +import {useAgentNotifications, useEmailNotifications} from "src/hooks/notifications"; import PageTitle from "src/components/PageTitle"; import NoRecordsAvailable from "src/components/NoRecordsAvailable"; -import {ColorPallet} from "src/theme/theme"; +import {borderRadius, ColorPallet} from "src/theme/theme"; +import {AgentNotification} from "../../utils/agentUtils"; +import Email from "src/db-models/Email"; + +interface CombinedItem { + key: string; + agentNotification: AgentNotification | null; + emailNotification: Email | null; +} const Notifications = observer(() => { const { t } = useTranslation(); const navigation = useNavigation(); - const notifications = useNotifications(); + const agentNotification = useAgentNotifications(); + const emailNotifications = useEmailNotifications(); + + const combinedNotifications = [ + ...agentNotification.map(p => ({ + key: p.key, + agentNotification: p, + emailNotification: null + })), + ...emailNotifications.map((p: Email) => ({ + key: p._id.toString(), + agentNotification: null, + emailNotification: p + })) + ]; return ( <View style={styles.container}> <StatusBar barStyle="light-content" /> - {!!notifications.length && ( + {!!combinedNotifications.length && ( <PageTitle> {t<string>('Notifications.Title', { - count: notifications.length + count: combinedNotifications.length })} </PageTitle> )} - {!notifications.length && ( + {!combinedNotifications.length && ( <NoRecordsAvailable text={t<string>('Notifications.NoNewUpdates')} /> )} - {!!notifications.length && ( + {!!combinedNotifications.length && ( <FlatList - data={notifications} + data={combinedNotifications} keyExtractor={item => item.key} - renderItem={({ item, index }) => ( + style={styles.list} + renderItem={({ item, index }: { item: CombinedItem, index: number }) => ( <View key={index} style={{ + marginHorizontal: 10, marginTop: 15, - marginBottom: index === notifications.length - 1 ? 15 : 0, + marginBottom: index === combinedNotifications.length - 1 ? 15 : 0, }} > - <NotificationListItem - notification={item} - onView={() => { - if (item.credentialOffer) { - navigation.navigate(Screens.CredentialOffer, { credentialId: item.credentialOffer.id }); - } else if (item.proofRequest) { - navigation.navigate(Screens.ProofRequest, { proofId: item.proofRequest.id }); - } - }} - /> + {item.agentNotification && ( + <NotificationListItem + notification={item.agentNotification} + onView={() => { + if (item.agentNotification.credentialOffer) { + navigation.navigate(Screens.CredentialOffer, { credentialId: item.agentNotification.credentialOffer.id }); + } else if (item.agentNotification.proofRequest) { + navigation.navigate(Screens.ProofRequest, { proofId: item.agentNotification.proofRequest.id }); + } + }} + /> + )} + {item.emailNotification && ( + <NotificationEmailListItem + id={item.emailNotification._id.toString()} + createdAt={item.emailNotification.createdAt} + from={item.emailNotification.from} + to={item.emailNotification.to} + cc={item.emailNotification.cc} + bcc={item.emailNotification.bcc} + subject={item.emailNotification.subject} + read={item.emailNotification.read} + sealed={item.emailNotification.type === "sealed"} + sealUrl={item.emailNotification.sealUrl} + onPress={() => { + const realmId = item.emailNotification!._id.toString(); + if (item.emailNotification!.type === "sealed") { + navigation.getParent().navigate(TabStacks.EmailStack, { + screen: Screens.SealDetailsInfo, + params: { realmId: realmId }, + }); + } else { + navigation.getParent().navigate(TabStacks.EmailStack, { + screen: Screens.EmailDetails, + params: { realmId: realmId }, + }); + } + }} + /> + )} </View> )} /> @@ -78,4 +133,10 @@ const styles = StyleSheet.create({ marginTop: offset, marginBottom: 20, }, + list: { + marginTop: 26, + backgroundColor: ColorPallet.grayscale.white, + borderRadius: borderRadius, + flex: 1, + } }); diff --git a/src/screens/ReceivedEmailList/index.tsx b/src/screens/ReceivedEmailList/index.tsx index 012421600cdf808c3ce270f6e063d757c01751c6..725e15a6a5bc383f74d1f20b64f7eb14a3890c87 100644 --- a/src/screens/ReceivedEmailList/index.tsx +++ b/src/screens/ReceivedEmailList/index.tsx @@ -80,6 +80,7 @@ export default ReceivedEmailList; const styles = StyleSheet.create({ container: { + backgroundColor: ColorPallet.grayscale.white, margin: 20, flex: 1, }, diff --git a/src/screens/SealDetailsInfo/useInformation.ts b/src/screens/SealDetailsInfo/useInformation.ts index f617745f92b3706ece8836ef8c8b104ac7661f16..a528dd30e7679b4e0825a4cefcc538d790cd9e7e 100644 --- a/src/screens/SealDetailsInfo/useInformation.ts +++ b/src/screens/SealDetailsInfo/useInformation.ts @@ -89,7 +89,7 @@ const useInformation = ( cc: qrCodeData.recipients.cc?.map(formatEmailAddress).join(', '), bcc: "", subject: qrCodeData.subject, - read: false, + read: true, svdxMime: null, svdxImapSaved: null, diff --git a/src/utils/agentUtils.ts b/src/utils/agentUtils.ts index b33df40fe03b0b4a9c441171b95d2c8b79a35337..4eb10f489407d89e7e0b7d79f0e5aaea7117ffe3 100644 --- a/src/utils/agentUtils.ts +++ b/src/utils/agentUtils.ts @@ -1,14 +1,14 @@ import {Agent, CredentialExchangeRecord, CredentialState, ProofExchangeRecord, ProofState} from "@aries-framework/core"; -export interface Notification { +export interface AgentNotification { key: string; connection?: any; credentialOffer?: CredentialExchangeRecord; proofRequest?: ProofExchangeRecord; } -export const getNotifications = async (agent: Agent): Promise<Notification[]> => { +export const getNotifications = async (agent: Agent): Promise<AgentNotification[]> => { const newOffers = await agent.credentials.findAllByQuery({ state: CredentialState.OfferReceived }); const newProofRequests = await agent.proofs.findAllByQuery({ state: ProofState.RequestReceived }); @@ -28,7 +28,7 @@ export const getNotifications = async (agent: Agent): Promise<Notification[]> => } } - const notifications: Notification[] = []; + const notifications: AgentNotification[] = []; const filteredOffers = newOffers.filter(p => !rejectedIds.some(rejId => rejId === p.id)); for await (const offer of filteredOffers) {