diff --git a/nodejs-assets/nodejs-project/package-lock.json b/nodejs-assets/nodejs-project/package-lock.json index 9c9f4e9..120ed41 100644 --- a/nodejs-assets/nodejs-project/package-lock.json +++ b/nodejs-assets/nodejs-project/package-lock.json @@ -159,9 +159,9 @@ } }, "@telios/nebula": { - "version": "3.4.10", - "resolved": "https://registry.npmjs.org/@telios/nebula/-/nebula-3.4.10.tgz", - "integrity": "sha512-HTb7G30eBSsShBeCbjPpe0UxyM52WL46XqRLbQgJ4XW9n66nk0tOk7MziugUvozHTFc6fiWCHoKUoD69lLejJg==", + "version": "3.4.11", + "resolved": "https://registry.npmjs.org/@telios/nebula/-/nebula-3.4.11.tgz", + "integrity": "sha512-V/brwEykt/3aLDwCxZDq1aijZ7n83OU6RIV/4LLIcmPJ42jCt/Ja+kvkHYAy2kGU8iXiwdCSu5/3z/ndtotc/Q==", "requires": { "autobase": "1.0.0-alpha.6", "blakejs": "^1.1.0", @@ -471,12 +471,13 @@ } }, "@telios/telios-client-backend": { - "version": "4.1.1", - "resolved": "github:Telios-org/telios-client-backend#f8ee5ff1f067d4ea913f56a5c3861f7ac7a801d9", + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@telios/telios-client-backend/-/telios-client-backend-4.1.6.tgz", + "integrity": "sha512-vsjlWqqJJIGOZ1ThikrO8LMpsFtLxmUEKF9r2uSENgR7aYEQpEe3MSuUoxon9UD1OH7jWwYX1Vh4BBS4x7DcqQ==", "requires": { "@hyperswarm/dht": "5.0.17", "@telios/client-sdk": "^6.2.5", - "@telios/nebula": "^3.4.10", + "@telios/nebula": "^3.4.11", "@telios/nebula-migrate": "git+https://github.com/Telios-org/nebula-migrate.git", "env-cmd": "^10.1.0", "file-type": "^16.5.3", @@ -557,9 +558,9 @@ } }, "@types/node": { - "version": "18.7.18", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.18.tgz", - "integrity": "sha512-m+6nTEOadJZuTPkKR/SYK3A2d7FZrgElol9UP1Kae90VVU4a6mxnPuLiIW1m4Cq4gZ/nWb9GrdVXJCoCazDAbg==" + "version": "18.7.23", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.23.tgz", + "integrity": "sha512-DWNcCHolDq0ZKGizjx2DZjR/PqsYwAcYUJmfMWqtVU2MBMG5Mo+xFZrhGId5r/O5HOuMPyQEcM6KUBp5lBZZBg==" }, "@types/responselike": { "version": "1.0.0", @@ -3386,9 +3387,9 @@ "integrity": "sha512-mliiCSrsE29aNBI7O9W5gGv6WmA9kBR8PtTt6Apaxns076IRdYrrtFhXHEWMj5CSum3U7cv7/pi4xmi4XsIOqg==" }, "underscore": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.4.tgz", - "integrity": "sha512-BQFnUDuAQ4Yf/cYY5LNrK9NCJFKriaRbD9uR1fTeXnBeoa97W0i41qkZfGO9pSo8I5KzjAcSY2XYtdf0oKd7KQ==" + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" }, "unordered-array-remove": { "version": "1.0.2", diff --git a/nodejs-assets/nodejs-project/package.json b/nodejs-assets/nodejs-project/package.json index b8bb756..31cdd70 100644 --- a/nodejs-assets/nodejs-project/package.json +++ b/nodejs-assets/nodejs-project/package.json @@ -7,7 +7,7 @@ "license": "MIT", "dependencies": { "@hyperswarm/dht": "^5.0.11", - "@telios/telios-client-backend": "4.1.1", + "@telios/telios-client-backend": "4.1.6", "intl": "^1.2.5", "sodium-native-nodejs-mobile": "^3.2.0-2", "utp-native-nodejs-mobile": "github:rangermauve/utp-native-nodejs-mobile#ios-support" diff --git a/src/Navigator.tsx b/src/Navigator.tsx index 9394d4b..10f5cb8 100644 --- a/src/Navigator.tsx +++ b/src/Navigator.tsx @@ -68,6 +68,7 @@ export type RootStackParams = { newAlias: { namespace: string }; newAliasRandom: undefined; aliasInfo: { aliasId: string; aliasName: string }; + emailDetail: { emailId: string; isUnread: boolean }; }; export type RegisterStackParams = { @@ -106,7 +107,6 @@ export type ProfileStackParams = { export type InboxStackParams = { inboxMain: undefined; - emailDetail: { emailId: string; folderId: number }; }; const CoreStack = createNativeStackNavigator(); @@ -146,11 +146,6 @@ const InboxRoot = () => ( ), })} /> - ); @@ -290,6 +285,19 @@ function CoreScreen() { ), })} /> + ({ + title: '', + headerLeft: () => ( + navigation.goBack()} + /> + ), + })} + /> ) : ( diff --git a/src/components/DraweContent/DrawerContent.tsx b/src/components/DraweContent/DrawerContent.tsx index 280d074..77e5fd3 100644 --- a/src/components/DraweContent/DrawerContent.tsx +++ b/src/components/DraweContent/DrawerContent.tsx @@ -3,7 +3,7 @@ import { DrawerContentScrollView, DrawerContentComponentProps, } from '@react-navigation/drawer'; -import { useAppSelector } from '../../hooks'; +import { useAppDispatch, useAppSelector } from '../../hooks'; import { Image, Text, View } from 'react-native'; import { IconButton } from '../IconButton'; import { colors } from '../../util/colors'; @@ -11,22 +11,45 @@ import Avatar from '../Avatar/Avatar'; import DrawerAliasesSection from '../DrawerAliasesSection/DrawerAliasesSection'; import { TouchableOpacity } from 'react-native-gesture-handler'; import { DrawerCell } from '../DrawerCell/DrawerCell'; -import { selectMailBoxAddress } from '../../store/selectors/email'; +import { + selectMailBoxAddress, + selectMailBoxId, +} from '../../store/selectors/email'; +import { folderSelectors } from '../../store/adapters/folders'; +import { FoldersId } from '../../store/types/enums/Folders'; +import { getMailboxFolders } from '../../store/thunks/email'; +import styles from './styles'; import { selectAccountAvatar, selectAccountDisplayName, } from '../../store/selectors/account'; -import styles from './styles'; - export const DrawerContent = (props: DrawerContentComponentProps) => { + const dispatch = useAppDispatch(); const mailboxAddress = useAppSelector(selectMailBoxAddress); const displayName = useAppSelector(selectAccountDisplayName); const avatar = useAppSelector(selectAccountAvatar); + const mailboxId = useAppSelector(selectMailBoxId); const selectedRoute = props.state.routes[props.state.index]; + const folders = useAppSelector(state => + folderSelectors.selectEntities(state.folders), + ); + + const getFolderUnreadCount = (folderId: number): string | undefined => { + if (folders) { + const folder = folders[folderId]; + if (folder && folder.count > 0) { + return folder.count.toString(); + } + } + return undefined; + }; - const onRefresh = () => { - //todo + const onRefresh = async () => { + // await dispatch(getNewMailFlow()); + if (mailboxId) { + await dispatch(getMailboxFolders({ id: mailboxId })); + } }; return ( @@ -72,6 +95,7 @@ export const DrawerContent = (props: DrawerContentComponentProps) => { label="Inbox" focused={selectedRoute.name === 'inbox'} leftIcon={{ name: 'mail-outline' }} + rightText={getFolderUnreadCount(FoldersId.inbox)} onPress={() => props.navigation.navigate('inbox')} /> diff --git a/src/components/EmailCell/EmailCell.tsx b/src/components/EmailCell/EmailCell.tsx index 1b29a85..56d3ce1 100644 --- a/src/components/EmailCell/EmailCell.tsx +++ b/src/components/EmailCell/EmailCell.tsx @@ -22,16 +22,14 @@ type EmailCellPropsWithType = EmailCellProps & { type: ComponentTypes }; const DefaultEmailCell = (props: EmailCellProps) => EmailCellRender({ ...props, type: ComponentTypes.DEFAULT }); -const SearhEmailCell = (props: EmailCellProps) => +const SearchEmailCell = (props: EmailCellProps) => EmailCellRender({ ...props, type: ComponentTypes.SEARCH }); const EmailCellRender = ({ email, onPress, type }: EmailCellPropsWithType) => { let fromName; - let fromEmail; if (email?.fromJSON) { const from = JSON.parse(email?.fromJSON); fromName = from[0].name; - fromEmail = from[0].address; } const isUnread = !!email.unread; @@ -75,5 +73,5 @@ const EmailCellRender = ({ email, onPress, type }: EmailCellPropsWithType) => { }; export const EmailCell = Object.assign(DefaultEmailCell, { - Search: memo(SearhEmailCell), + Search: memo(SearchEmailCell), }); diff --git a/src/components/MailList/index.tsx b/src/components/MailList/index.tsx index 069893a..3b44eaa 100644 --- a/src/components/MailList/index.tsx +++ b/src/components/MailList/index.tsx @@ -8,18 +8,12 @@ import { EmptyComponent } from './components/EmptyComponent'; import { colors } from '../../util/colors'; import useInfiniteScroll from '../../hooks/useInfiniteScroll'; -export type MailListItem = { - id: string; - onSelect?: () => void; - mail?: Email; -}; - export type MailListProps = { items: Email[]; getMoreData: (offset: number, perPage: number) => Promise; resetData?: () => void; headerComponent?: React.ComponentType | React.ReactElement; - onItemPress?: (itemId: string) => void; + onItemPress?: (itemId: string, isUnread: boolean) => void; headerAnimatedValue?: any; }; @@ -34,11 +28,14 @@ export const MailList = ({ const { isLoading, flatListProps } = useInfiniteScroll({ getData: getMoreData, resetData, - perPage: 4, + perPage: 10, }); const renderItem = ({ item }: { item: Email }) => ( - onItemPress?.(item.emailId)} /> + onItemPress?.(item.emailId, item.unread)} + /> ); return ( diff --git a/src/components/MailListHeaderTitle.tsx b/src/components/MailListHeaderTitle.tsx index 3eb9e8d..58efa50 100644 --- a/src/components/MailListHeaderTitle.tsx +++ b/src/components/MailListHeaderTitle.tsx @@ -8,6 +8,7 @@ const styles = StyleSheet.create({ width: 10, aspectRatio: 1, borderRadius: 10, + marginLeft: 11, }, titleContainer: { flexDirection: 'row', @@ -31,7 +32,7 @@ export default ({ return ( {title} diff --git a/src/components/MailWithFiltersContainer/index.tsx b/src/components/MailWithFiltersContainer/index.tsx index 1600a9a..4c94084 100644 --- a/src/components/MailWithFiltersContainer/index.tsx +++ b/src/components/MailWithFiltersContainer/index.tsx @@ -1,7 +1,10 @@ import { useAppDispatch, useAppSelector } from '../../hooks'; import { FoldersId } from '../../store/types/enums/Folders'; import React, { useLayoutEffect, useState } from 'react'; -import MailFilters, { FilterOption, FilterType } from '../MailList/components/MailFilters'; +import MailFilters, { + FilterOption, + FilterType, +} from '../MailList/components/MailFilters'; import { resetMailsByFolder } from '../../store/emails'; import { View } from 'react-native'; import styles from './styles'; @@ -59,11 +62,8 @@ export default ({ }); }, []); - const onSelectEmail = (emailId: string) => { - navigation.navigate('inbox', { - screen: 'emailDetail', - params: { emailId: emailId }, - }); + const onSelectEmail = (emailId: string, isUnread: boolean) => { + navigation.navigate('emailDetail', { emailId: emailId, isUnread }); }; const resetData = (filter: FilterType) => () => diff --git a/src/screens/AliasInfo/index.tsx b/src/screens/AliasInfo/index.tsx index ceeac59..cfc181b 100644 --- a/src/screens/AliasInfo/index.tsx +++ b/src/screens/AliasInfo/index.tsx @@ -44,7 +44,7 @@ export const AliasInfoScreen = ({ try { await dispatch( removeAliasFlow({ - aliasId: alias._id, + aliasId: alias.aliasId, address: alias.name, domain: emailPostfix, namespaceName: alias.namespaceKey, @@ -89,7 +89,7 @@ export const AliasInfoScreen = ({ {!!alias.namespaceKey && ( @@ -103,7 +103,7 @@ export const AliasInfoScreen = ({ @@ -111,7 +111,7 @@ export const AliasInfoScreen = ({ diff --git a/src/screens/EmailDetailScreen.tsx b/src/screens/EmailDetailScreen.tsx index 598efa6..c5cddda 100644 --- a/src/screens/EmailDetailScreen.tsx +++ b/src/screens/EmailDetailScreen.tsx @@ -1,32 +1,35 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useLayoutEffect, useState } from 'react'; import { NativeStackScreenProps } from '@react-navigation/native-stack'; import { format } from 'date-fns'; import { ActivityIndicator, Alert, ScrollView, Text, View } from 'react-native'; -import { InboxStackParams } from '../Navigator'; +import { RootStackParams } from '../Navigator'; import { spacing } from '../util/spacing'; import { colors } from '../util/colors'; import { useAppDispatch, useAppSelector } from '../hooks'; import { fonts } from '../util/fonts'; import { NavIconButton } from '../components/NavIconButton'; import { selectMailByFolder } from '../store/selectors/email'; -import { deleteMail, getMessageById } from '../store/thunks/email'; +import { + deleteMail, + getMessageById, + markAsUnreadFlow, +} from '../store/thunks/email'; import { Email, ToFrom } from '../store/types'; import { FoldersId } from '../store/types/enums/Folders'; +import { updateFolderCountFlow } from '../store/thunks/folders'; +import { updateAliasCountFlow } from '../store/thunks/aliases'; export type EmailDetailScreenProps = NativeStackScreenProps< - InboxStackParams, + RootStackParams, 'emailDetail' >; export const EmailDetailScreen = (props: EmailDetailScreenProps) => { - const { emailId } = props.route.params; + const { emailId, isUnread } = props.route.params; // we need stored email to check if it was unread, getMessageById automatically marks it as read on backend; const [email, setEmail] = useState(); const [loading, setLoading] = useState(false); - // const email = useAppSelector(state => - // selectMailByFolder(state, folderId, emailId), - // ); const isTrash = useAppSelector(state => - selectMailByFolder(state, FoldersId.trash, emailId), + selectMailByFolder(state, FoldersId.trash, 'all', emailId), ); const dispatch = useAppDispatch(); @@ -61,10 +64,27 @@ export const EmailDetailScreen = (props: EmailDetailScreenProps) => { }; const onToggleUnread = () => { - Alert.alert('Not implemented'); + if (email) { + dispatch(markAsUnreadFlow({ email: email })); + props.navigation.goBack(); + } }; - React.useLayoutEffect(() => { + useEffect(() => { + const fetchMail = async () => { + setLoading(true); + try { + const resp = await dispatch(getMessageById({ id: emailId })).unwrap(); + setEmail(resp); + } catch (e) { + // ignore + } + setLoading(false); + }; + fetchMail(); + }, []); + + useLayoutEffect(() => { props.navigation.setOptions({ headerRight: () => ( @@ -85,21 +105,26 @@ export const EmailDetailScreen = (props: EmailDetailScreenProps) => { ), }); - }, [props.navigation]); + }, [props.navigation, email?.emailId]); useEffect(() => { - const fetchMail = async () => { - setLoading(true); - try { - const resp = await dispatch(getMessageById({ id: emailId })).unwrap(); - setEmail(resp); - } catch (e) { - // ignore + if (isUnread) { + if (email?.folderId === FoldersId.aliases) { + if (email.aliasId) { + dispatch(updateAliasCountFlow({ id: email.aliasId, amount: -1 })); + } + } else { + if (email?.folderId) { + dispatch( + updateFolderCountFlow({ + id: email.folderId.toString(), + amount: -1, + }), + ); + } } - setLoading(false); - }; - fetchMail(); - }, []); + } + }, [email?.unread]); if (!email) { if (loading) { @@ -116,8 +141,9 @@ export const EmailDetailScreen = (props: EmailDetailScreenProps) => { ); } - const fromArray = - email?.fromJSON && (JSON.parse(email.fromJSON) as Array); + const fromArray = email?.fromJSON + ? (JSON.parse(email.fromJSON) as Array) + : undefined; const from = fromArray?.[0]; const dayFormatted = format(new Date(email.date), 'dd MMM yyyy'); diff --git a/src/screens/NewAliasNamespace/NewAliasNamespaceScreen.tsx b/src/screens/NewAliasNamespace/NewAliasNamespaceScreen.tsx index a37c216..996bf07 100644 --- a/src/screens/NewAliasNamespace/NewAliasNamespaceScreen.tsx +++ b/src/screens/NewAliasNamespace/NewAliasNamespaceScreen.tsx @@ -13,6 +13,7 @@ import { randomLetters, randomWords } from '../../util/randomNames'; import { useAppDispatch, useAppSelector } from '../../hooks'; import styles from './styles'; import { registerNamespace } from '../../store/thunks/namespaces'; +import { selectMailBoxId } from '../../store/selectors/email'; export type NewAliasNamespaceScreenProps = NativeStackScreenProps< RootStackParams, @@ -23,7 +24,7 @@ export const NewAliasNamespaceScreen = ( props: NewAliasNamespaceScreenProps, ) => { const dispatch = useAppDispatch(); - const mailboxId = useAppSelector(state => state.mail.mailbox?._id); + const mailboxId = useAppSelector(selectMailBoxId); const [namespace, setNamespace] = React.useState(randomLetters()); const [loadingCreate, setLoadingCreate] = React.useState(false); diff --git a/src/screens/Search/SearchScreen.tsx b/src/screens/Search/SearchScreen.tsx index 36154be..42e8ee3 100644 --- a/src/screens/Search/SearchScreen.tsx +++ b/src/screens/Search/SearchScreen.tsx @@ -46,10 +46,10 @@ export const SearchScreen = ({ navigation }: SearchProps) => { }; }, [searchText]); - const onSelectEmail = (emailId: string, folderId: string) => { - navigation.navigate('inbox', { - screen: 'emailDetail', - params: { emailId: emailId, folderId: parseInt(folderId, 10) }, + const onSelectEmail = (emailId: string, isUnread: boolean) => { + navigation.navigate('emailDetail', { + emailId: emailId, + isUnread, }); }; @@ -74,10 +74,11 @@ export const SearchScreen = ({ navigation }: SearchProps) => { const handleClose = () => Keyboard.dismiss(); + // @ts-ignore const renderItem = ({ item, section }) => ( onSelectEmail(item.emailId, section.id.folderId)} + onPress={() => onSelectEmail(item.emailId, section.id.unread)} /> ); @@ -114,6 +115,7 @@ export const SearchScreen = ({ navigation }: SearchProps) => { ); + // @ts-ignore const sectionHeader = ({ section: { title, count, icon, id } }) => ( { - navigation.navigate('inbox', { - screen: 'emailDetail', - params: { emailId: emailId, folderId: parseInt(folderId, 10) }, + const onSelectEmail = (emailId: string, isUnread: boolean) => { + navigation.navigate('emailDetail', { + emailId: emailId, + isUnread, }); }; @@ -53,7 +53,10 @@ export const SearchSectionScreen = ({ ); const renderItem = ({ item }: { item: Email }) => ( - onSelectEmail?.(item.emailId)} /> + onSelectEmail?.(item.emailId, item.unread)} + /> ); return ( diff --git a/src/store/aliases.ts b/src/store/aliases.ts index 2d02ee8..8f8ff44 100644 --- a/src/store/aliases.ts +++ b/src/store/aliases.ts @@ -1,7 +1,13 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { getAliases, registerAlias, removeAliasFlow } from './thunks/aliases'; +import { + getAliases, + registerAlias, + removeAliasFlow, + updateAliasCountFlow, +} from './thunks/aliases'; import { aliasAdapter } from './adapters/aliases'; import { accountLogout } from './thunks/accountLogout'; +import _ from 'lodash'; export const aliasesSlice = createSlice({ name: 'aliases', @@ -20,6 +26,18 @@ export const aliasesSlice = createSlice({ } }, ); + builder.addCase(updateAliasCountFlow.fulfilled, (state, action) => { + const aliasId = _.findKey(state.entities, ['aliasId', action.payload.id]); + if (aliasId) { + aliasAdapter.updateOne(state, { + id: aliasId, + changes: { + count: + (state.entities[aliasId]?.count || 0) + action.payload.amount, + }, + }); + } + }); // clear state on logout builder.addCase(accountLogout.fulfilled, () => { return aliasAdapter.getInitialState(); diff --git a/src/store/emails.ts b/src/store/emails.ts index 840a05c..3fd3bc5 100644 --- a/src/store/emails.ts +++ b/src/store/emails.ts @@ -6,6 +6,7 @@ import { getMailByFolderRead, getMailByFolderUnread, getMessageById, + markAsUnreadFlow, getMessagesByAliasId, getReadMessagesByAliasId, getUnreadMessagesByAliasId, @@ -30,19 +31,23 @@ interface EmailState { mailbox: Mailbox | undefined; } -const updateMailInState = (state: EmailState, mail: Email) => { - if (mail.unread) { - emailAdapter.setOne(state.byFolderId[mail.folderId].unread, mail); - // emailAdapter.removeOne(state.byFolderId[mail.folderId].read, mail.emailId); - } - if (!mail.unread) { - emailAdapter.setOne(state.byFolderId[mail.folderId].read, mail); - // emailAdapter.removeOne( - // state.byFolderId[mail.folderId].unread, - // mail.emailId, - // ); - } +const updateReadMailInState = (state: EmailState, mail: Email) => { + // getMessageById marks mails as read on backend emailAdapter.setOne(state.byFolderId[mail.folderId].all, mail); + emailAdapter.setOne(state.byFolderId[mail.folderId].read, mail); + emailAdapter.removeOne(state.byFolderId[mail.folderId].unread, mail.emailId); +}; + +const updateUnreadMailInState = (state: EmailState, mail: Email) => { + emailAdapter.setOne(state.byFolderId[mail.folderId].all, { + ...mail, + unread: true, + }); + emailAdapter.setOne(state.byFolderId[mail.folderId].unread, { + ...mail, + unread: true, + }); + emailAdapter.removeOne(state.byFolderId[mail.folderId].read, mail.emailId); }; const emailInitialState = { @@ -71,11 +76,14 @@ export const emailSlice = createSlice({ }, extraReducers: builder => { builder.addCase(saveMailToDB.fulfilled, (state, action) => { - action.payload.msgArr.map(msg => updateMailInState(state, msg)); + action.payload.msgArr.map(msg => { + emailAdapter.setOne(state.byFolderId[msg.folderId].unread, msg); + emailAdapter.setOne(state.byFolderId[msg.folderId].all, msg); + }); + }); + builder.addCase(markAsUnreadFlow.fulfilled, (state, action) => { + updateUnreadMailInState(state, action.payload.email); }); - // builder.addCase(markAsUnread.fulfilled, (state, action) => - // emailAdapter.setMany(state, action.payload), - // ); builder.addCase(getMailboxFolders.fulfilled, (state, action) => { const storedFolders = Object.keys(state.byFolderId); action.payload.map(folder => { @@ -85,9 +93,9 @@ export const emailSlice = createSlice({ }); }); - builder.addCase(getMessageById.fulfilled, (state, action) => - updateMailInState(state, action.payload), - ); + builder.addCase(getMessageById.fulfilled, (state, action) => { + updateReadMailInState(state, action.payload); + }); builder.addCase(getAllMailByFolder.fulfilled, (state, action) => { const folderIdToUpdate = action.payload[0]?.folderId; if (folderIdToUpdate) { diff --git a/src/store/folders.ts b/src/store/folders.ts index 6d4cef7..c178a6f 100644 --- a/src/store/folders.ts +++ b/src/store/folders.ts @@ -1,6 +1,7 @@ import { folderAdapter } from './adapters/folders'; import { createSlice } from '@reduxjs/toolkit'; import { getMailboxFolders } from './thunks/email'; +import { updateFolderCountFlow } from './thunks/folders'; const folderSlice = createSlice({ name: 'folder', @@ -8,6 +9,16 @@ const folderSlice = createSlice({ reducers: {}, extraReducers: builder => { builder.addCase(getMailboxFolders.fulfilled, folderAdapter.setAll); + builder.addCase(updateFolderCountFlow.fulfilled, (state, action) => { + folderAdapter.updateOne(state, { + id: action.payload.id, + changes: { + count: + (state.entities[action.payload.id]?.count || 0) + + action.payload.amount, + }, + }); + }); }, }); diff --git a/src/store/selectors/email.ts b/src/store/selectors/email.ts index 0083d7b..a3d6d52 100644 --- a/src/store/selectors/email.ts +++ b/src/store/selectors/email.ts @@ -3,6 +3,7 @@ import { RootState } from '../../store'; import { emailSelectors } from '../adapters/emails'; import { createDeepEqualSelector } from './utils'; import { FoldersId } from '../types/enums/Folders'; +import { FilterType } from '../../components/MailList/components/MailFilters'; const mailSelectorByFolderId = createDeepEqualSelector( (state: RootState) => state.mail.byFolderId, @@ -34,15 +35,19 @@ export const selectReadMailsByFolder = createSelector( export const selectMailByFolder = createSelector( (state: RootState, folderId: number) => mailSelectorByFolderId(state, folderId), - (state: RootState, folderId: number, id: string) => id, - (mailState, id) => { - return emailSelectors.selectById(mailState.all, id); + (state: RootState, folderId: number, filter: FilterType) => filter, + (state: RootState, folderId: number, filter: FilterType, id: string) => id, + (mailState, filter, id) => { + return emailSelectors.selectById(mailState[filter], id); }, ); export const selectMailBoxAddress = (state: RootState) => state.mail.mailbox?.address; +export const selectMailBoxId = (state: RootState) => + state.mail.mailbox?.mailboxId; + export const selectMailsByAliasId = createSelector( (state: RootState) => mailSelectorByFolderId(state, FoldersId.aliases), (state: RootState, aliasId: string) => aliasId, diff --git a/src/store/thunks/aliases.ts b/src/store/thunks/aliases.ts index fbb16ea..3e664d8 100644 --- a/src/store/thunks/aliases.ts +++ b/src/store/thunks/aliases.ts @@ -4,7 +4,7 @@ import { AppDispatch, RootState } from '../../store'; import { Alias } from '../types'; import { aliasSelectors } from '../adapters/aliases'; -type GetAliasesRequest = { namespaceKeys: string[] }; +type GetAliasesRequest = { namespaceKeys: string[] | null }; type GetAliasesResponse = Array; export const getAliases = createNodeCalloutAsyncThunk< GetAliasesRequest, @@ -84,6 +84,24 @@ export const updateAliasFlow = createAsyncThunk< } }); +type UpdateAliasCountRequest = { + id: string; + amount: number; +}; + +export const updateAliasCount = createNodeCalloutAsyncThunk< + UpdateAliasCountRequest, + void +>('alias:updateAliasCount'); + +export const updateAliasCountFlow = createAsyncThunk< + UpdateAliasCountRequest, + UpdateAliasCountRequest +>('flow/updateAliasCount', async (arg, thunkAPI) => { + await thunkAPI.dispatch(updateAliasCount(arg)); + return arg; +}); + type RemoveAliasRequest = { namespaceName?: string; address: string; diff --git a/src/store/thunks/email.ts b/src/store/thunks/email.ts index 32062f4..24c1acc 100644 --- a/src/store/thunks/email.ts +++ b/src/store/thunks/email.ts @@ -1,9 +1,12 @@ import { createNodeCalloutAsyncThunk } from '../../util/nodeActions'; import { createAsyncThunk } from '@reduxjs/toolkit'; -import { RootState } from '../../store'; +import { AppDispatch, RootState } from '../../store'; import nodejs from 'nodejs-mobile-react-native'; import { registerOneTimeListener } from '../../eventListenerMiddleware'; import { Alias, Email, EmailContent, Folder, Mailbox } from '../types'; +import { updateFolderCountFlow } from './folders'; +import { FoldersId } from '../types/enums/Folders'; +import { updateAliasCountFlow } from './aliases'; export const getNewMailFlow = createAsyncThunk( 'flow/getNewMail', @@ -91,6 +94,9 @@ export const saveDraft = createAsyncThunk< saveMailToDB({ type: 'Draft', messages: [data] }), ); if (response.type === saveMailToDB.fulfilled.type) { + thunkAPI.dispatch( + updateFolderCountFlow({ id: FoldersId.drafts.toString(), amount: 1 }), + ); return response.payload as SaveMailToDBResponse; } else { throw new Error('Unable to save draft'); @@ -167,17 +173,40 @@ export const saveMailToDB = createAsyncThunk< return action.data.msgArr?.[0]?.emailId === firstEmailId; }, }, - event => { + async event => { if (event.error) { reject(event.error); } else { // TODO: break this part out into a separate flow // TODO: batch these up, rather than call for every single item inserted to DB. - const emailIds: string[] = event.data.msgArr.map( - (item: Email) => item.emailId, - ); - thunkAPI.dispatch(updateMailAsSynced({ msgArray: emailIds })); - resolve(event.data); + + try { + const emailIds: string[] = event.data.msgArr.map( + (item: Email) => item.emailId, + ); + await thunkAPI.dispatch( + updateMailAsSynced({ msgArray: emailIds }), + ); + event.data.msgArr.forEach((email: Email) => { + thunkAPI.dispatch( + updateFolderCountFlow({ + id: email.folderId.toString(), + amount: 1, + }), + ); + if (email.aliasId) { + thunkAPI.dispatch( + updateAliasCountFlow({ + id: email.aliasId, + amount: 1, + }), + ); + } + }); + resolve(event.data); + } catch (e) { + reject(e); + } } }, ); @@ -268,17 +297,36 @@ export const getMailByFolderUnread = createAsyncThunk< .unwrap(); }); export type MarkAsUnreadRequest = { id: string | number }; -export type MarkAsUnreadResponse = Array; +export type MarkAsUnreadResponse = void; export const markAsUnread = createNodeCalloutAsyncThunk< MarkAsUnreadRequest, MarkAsUnreadResponse >('email:markAsUnread'); + +export const markAsUnreadFlow = createAsyncThunk< + { email: Email; amount: number }, + { email: Email }, + { state: RootState; dispatch: AppDispatch } +>('flow/markAsUnread', async (arg, thunkAPI) => { + await thunkAPI.dispatch(markAsUnread({ id: arg.email.emailId })); + await thunkAPI.dispatch( + updateFolderCountFlow({ id: arg.email.folderId.toString(), amount: 1 }), + ); + if (arg.email.folderId === FoldersId.aliases && arg.email.aliasId) { + thunkAPI.dispatch( + await updateAliasCountFlow({ id: arg.email.aliasId, amount: 1 }), + ); + } + return { ...arg, amount: 1 }; +}); + export type GetMessageByIdRequest = { id: string | number }; export type GetMessageByIdResponse = any; //Array; export const getMessageById = createNodeCalloutAsyncThunk< GetMessageByIdRequest, GetMessageByIdResponse >('email:getMessageById'); + export type SendEmailRequest = { email: EmailContent }; export type SendEmailResponse = {}; // TODO; export const sendEmail = createNodeCalloutAsyncThunk< diff --git a/src/store/thunks/folders.ts b/src/store/thunks/folders.ts new file mode 100644 index 0000000..c2dd2dd --- /dev/null +++ b/src/store/thunks/folders.ts @@ -0,0 +1,24 @@ +import { createNodeCalloutAsyncThunk } from '../../util/nodeActions'; +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { AppDispatch, RootState } from '../../store'; + +type UpdateFolderRequest = { + id: string; + amount: number; +}; + +type UpdateFolderResponse = void; + +export const updateFolderCount = createNodeCalloutAsyncThunk< + UpdateFolderRequest, + UpdateFolderResponse +>('folder:updateFolderCount'); + +export const updateFolderCountFlow = createAsyncThunk< + UpdateFolderRequest, + UpdateFolderRequest, + { state: RootState; dispatch: AppDispatch } +>('flow/updateFolderCount', async (arg, thunkApi) => { + await thunkApi.dispatch(updateFolderCount(arg)); + return arg; +}); diff --git a/src/store/thunks/namespaces.ts b/src/store/thunks/namespaces.ts index d3d2301..5644031 100644 --- a/src/store/thunks/namespaces.ts +++ b/src/store/thunks/namespaces.ts @@ -36,17 +36,7 @@ export const getFoldersNamespacesAliasesFlow = createAsyncThunk( // thunkAPI.dispatch(getMailByFolder({ id: inbox.folderId })); } - const namespacesResponse = await thunkAPI.dispatch( - getNamespacesForMailbox({ id: mailboxId }), - ); - if (namespacesResponse.type === getNamespacesForMailbox.fulfilled.type) { - const namespaces = - namespacesResponse.payload as GetNamespacesForMailboxResponse; - thunkAPI.dispatch( - getAliases({ - namespaceKeys: namespaces?.map(namespace => namespace.name), - }), - ); - } + await thunkAPI.dispatch(getNamespacesForMailbox({ id: mailboxId })); + await thunkAPI.dispatch(getAliases({ namespaceKeys: null })); }, );