Skip to content
Snippets Groups Projects
Commit c0cfcf8a authored by Marek Veselý's avatar Marek Veselý
Browse files

feat: highlight emails tabs with unread emails

parent cc29a71c
No related branches found
No related tags found
No related merge requests found
import { EmailSelection, compareDates } from '@/analyst/utilities'
import { EmailSelection } from '@/analyst/utilities'
import { Card, CardList } from '@blueprintjs/core'
import type { Email } from '@inject/graphql/fragments/Email.generated'
import type { EmailThread } from '@inject/graphql/fragments/EmailThread.generated'
import { useMemo, type FC } from 'react'
import { type FC } from 'react'
import ThreadLogCard from './ThreadLogCard'
import type { ExtendedEmaiLThread } from './typing'
interface ThreadLogCardsProps {
teamId: string
emailThreads: EmailThread[]
emailThreads: ExtendedEmaiLThread[]
selectedTab: EmailSelection.RECEIVED | EmailSelection.SENT
selectedEmailThreadId?: string
onClick: (emailThread: EmailThread) => void
......@@ -21,70 +21,28 @@ const ThreadLogCards: FC<ThreadLogCardsProps> = ({
selectedEmailThreadId,
onClick,
inAnalyst = false,
}) => {
const emailThreadsWithLastEmails = useMemo(
() =>
emailThreads
.reduce<
{
emailThread: EmailThread
lastEmail: Email
}[]
>((accumulator, emailThread) => {
const lastEmail = emailThread.emails
.filter(email =>
selectedTab === EmailSelection.RECEIVED
? email.sender.team?.id !== teamId
: email.sender.team?.id === teamId
)
.sort(
(a, b) =>
-compareDates(new Date(a.timestamp), new Date(b.timestamp))
)
.at(0)
if (lastEmail) {
accumulator.push({
emailThread,
lastEmail,
})
}
return accumulator
}, [])
.sort(
(a, b) =>
-compareDates(
new Date(a.lastEmail.timestamp),
new Date(b.lastEmail.timestamp)
)
),
[emailThreads, selectedTab, teamId]
)
return (
<CardList bordered={false}>
{emailThreadsWithLastEmails.length > 0 ? (
emailThreadsWithLastEmails.map(element => (
<ThreadLogCard
key={element.emailThread.id}
emailThread={element.emailThread}
lastEmail={element.lastEmail}
teamId={teamId}
isSelected={element.emailThread.id === selectedEmailThreadId}
onClick={() => onClick(element.emailThread)}
inAnalyst={inAnalyst}
/>
))
) : (
<Card>{`No emails${
selectedTab === undefined
? ''
: ` ${selectedTab === EmailSelection.SENT ? 'sent' : 'received'}`
}`}</Card>
)}
</CardList>
)
}
}) => (
<CardList bordered={false}>
{emailThreads.length > 0 ? (
emailThreads.map(emailThread => (
<ThreadLogCard
key={emailThread.id}
emailThread={emailThread}
lastEmail={emailThread.lastEmail}
teamId={teamId}
isSelected={emailThread.id === selectedEmailThreadId}
onClick={() => onClick(emailThread)}
inAnalyst={inAnalyst}
/>
))
) : (
<Card>{`No emails${
selectedTab === undefined
? ''
: ` ${selectedTab === EmailSelection.SENT ? 'sent' : 'received'}`
}`}</Card>
)}
</CardList>
)
export default ThreadLogCards
......@@ -6,12 +6,28 @@ import {
OPEN_REPLY_EVENT_TYPE,
} from '@/email/EmailFormOverlay'
import { useNavigate } from '@/router'
import { Button, ButtonGroup, Divider } from '@blueprintjs/core'
import {
Button,
ButtonGroup,
Classes,
Colors,
Divider,
} from '@blueprintjs/core'
import { Inbox, SendMessage } from '@blueprintjs/icons'
import { css, cx } from '@emotion/css'
import type { EmailThread } from '@inject/graphql/fragments/EmailThread.generated'
import { type FC } from 'react'
import { useCallback, type FC } from 'react'
import DraftLogCards from './DraftLogCards'
import ThreadLogCards from './ThreadLogCards'
import useFilteredThreads from './useFilteredThreads'
const unreadIcon = css`
/* override the muted color */
color: ${Colors.BLACK} !important;
.${Classes.DARK} & {
color: ${Colors.WHITE} !important;
}
`
const threadLog = css`
display: flex;
......@@ -74,10 +90,26 @@ const ThreadLog: FC<ThreadLogProps> = ({
}) => {
const nav = useNavigate()
const { received, sent } = useFilteredThreads({ emailThreads, teamId })
const getChildren = useCallback(
(tab: EmailSelection, unread: number) => {
const text = `${tab === EmailSelection.RECEIVED ? 'Received' : 'Sent'}\
${inInstructor ? ' by team' : ''}${unread ? ` (${unread})` : ''}`
if (unread) {
return <b>{text}</b>
}
return text
},
[inInstructor]
)
return (
<div className={threadLog}>
<div className={header}>
<ButtonGroup
alignText='left'
minimal
className={cx({
[buttons(inInstructor)]: true,
......@@ -88,8 +120,10 @@ const ThreadLog: FC<ThreadLogProps> = ({
<LinkButton
link={receivedLink}
button={{
text: `Received${inInstructor ? ' by team' : ''}`,
icon: 'inbox',
children: getChildren(EmailSelection.RECEIVED, received.unread),
icon: (
<Inbox className={cx({ [unreadIcon]: received.unread > 0 })} />
),
active: selectedTab === EmailSelection.RECEIVED,
}}
/>
......@@ -97,8 +131,12 @@ const ThreadLog: FC<ThreadLogProps> = ({
<LinkButton
link={sentLink}
button={{
text: `Sent${inInstructor ? ' by team' : ''}`,
icon: 'send-message',
children: getChildren(EmailSelection.SENT, sent.unread),
icon: (
<SendMessage
className={cx({ [unreadIcon]: sent.unread > 0 })}
/>
),
active: selectedTab === EmailSelection.SENT,
}}
/>
......@@ -157,7 +195,11 @@ const ThreadLog: FC<ThreadLogProps> = ({
) : (
<ThreadLogCards
teamId={teamId}
emailThreads={emailThreads}
emailThreads={
selectedTab === EmailSelection.RECEIVED
? received.emailThreads
: sent.emailThreads
}
selectedTab={selectedTab}
onClick={onClick}
inAnalyst={inAnalyst}
......
import type { Email } from '@inject/graphql/fragments/Email.generated'
import type { EmailThread } from '@inject/graphql/fragments/EmailThread.generated'
export type ExtendedEmaiLThread = EmailThread & {
lastEmail: Email
}
import { compareDates } from '@/analyst/utilities'
import type { EmailThread } from '@inject/graphql/fragments/EmailThread.generated'
import { useMemo } from 'react'
import type { ExtendedEmaiLThread } from './typing'
interface FilteredThreads {
received: ExtendedEmaiLThread[]
sent: ExtendedEmaiLThread[]
}
interface FilteredThreadsWithUnread {
sent: { emailThreads: ExtendedEmaiLThread[]; unread: number }
received: { emailThreads: ExtendedEmaiLThread[]; unread: number }
}
const getLastEmail = (
emailThread: EmailThread,
teamId: string,
type: 'sent' | 'received'
) =>
emailThread.emails
.filter(email =>
type === 'sent'
? email.sender.team?.id === teamId
: email.sender.team?.id !== teamId
)
.sort((a, b) => -compareDates(new Date(a.timestamp), new Date(b.timestamp)))
.at(0)
const sortFn = (a: ExtendedEmaiLThread, b: ExtendedEmaiLThread) =>
-compareDates(
new Date(a.lastEmail.timestamp),
new Date(b.lastEmail.timestamp)
)
const useFilteredThreads = ({
emailThreads,
teamId,
}: {
emailThreads: EmailThread[]
teamId: string
}): FilteredThreadsWithUnread => {
const { received, sent } = useMemo(
() =>
emailThreads.reduce(
(accumulator: FilteredThreads, emailThread: EmailThread) => {
const lastEmailReceived = getLastEmail(
emailThread,
teamId,
'received'
)
const lastEmailSent = getLastEmail(emailThread, teamId, 'sent')
return {
received: [
...accumulator.received,
...(lastEmailReceived
? [{ ...emailThread, lastEmail: lastEmailReceived }]
: []),
],
sent: [
...accumulator.sent,
...(lastEmailSent
? [{ ...emailThread, lastEmail: lastEmailSent }]
: []),
],
}
},
{ sent: [], received: [] }
),
[emailThreads, teamId]
)
const result = useMemo(
() => ({
received: {
emailThreads: received.sort(sortFn),
unread: received.filter(thread =>
thread.readReceipt.some(
readReceipt => readReceipt.teamId === teamId && readReceipt.isUnread
)
).length,
},
sent: {
emailThreads: sent.sort(sortFn),
unread: sent.filter(thread =>
thread.readReceipt.some(
readReceipt => readReceipt.teamId === teamId && readReceipt.isUnread
)
).length,
},
}),
[received, sent, teamId]
)
return result
}
export default useFilteredThreads
......@@ -3,7 +3,7 @@ import { Button, NonIdealState } from '@blueprintjs/core'
import { css } from '@emotion/css'
import type { Placement } from '@floating-ui/react'
import type { FC } from 'react'
import { useContext, useEffect, useState } from 'react'
import { useContext, useEffect, useMemo, useState } from 'react'
import usePopoverElement from '../popover/usePopoverElement'
import Notification from './components/Notification'
import NotificationListContext from './contexts/NotificationListContext'
......@@ -78,6 +78,16 @@ const NotificationDropdown: FC<NotificationDropdownProps> = ({
}
}, [count])
const buttonChildren = useMemo(() => {
if (hideLabel) {
return undefined
}
if (count) {
return <b>{`Notifications (${count})`}</b>
}
return 'Notifications'
}, [count, hideLabel])
return (
<>
<Button
......@@ -88,11 +98,12 @@ const NotificationDropdown: FC<NotificationDropdownProps> = ({
alignText='left'
fill={fill}
minimal
text={hideLabel ? undefined : 'Notifications'}
title='Notifications'
onClick={() => setOpen(prev => !prev)}
{...getReferenceProps}
/>
>
{buttonChildren}
</Button>
{children}
</>
)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment