Build message threads (#3593)

* Adding message thread component

* Add state and mocks

* Rename components and use local state for messages

---------

Co-authored-by: Thomas Trompette <thomast@twenty.com>
This commit is contained in:
Thomas Trompette
2024-01-24 14:32:57 +01:00
committed by GitHub
parent afc36c7329
commit e85f65a195
20 changed files with 404 additions and 71 deletions

View File

@ -3,10 +3,11 @@ import styled from '@emotion/styled';
import { IconMail } from '@/ui/display/icon';
import { Tag } from '@/ui/display/tag/components/Tag';
import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';
interface ThreadHeaderProps {
type EmailThreadHeaderProps = {
subject: string;
lastMessageSentAt: Date;
}
lastMessageSentAt: string;
};
const StyledContainer = styled.div`
align-items: flex-start;
@ -36,10 +37,10 @@ const StyledContent = styled.span`
width: 100%;
`;
export const ThreadHeader = ({
export const EmailThreadHeader = ({
subject,
lastMessageSentAt,
}: ThreadHeaderProps) => {
}: EmailThreadHeaderProps) => {
return (
<StyledContainer>
<Tag Icon={IconMail} color="gray" text="Email" onClick={() => {}} />

View File

@ -0,0 +1,56 @@
import React, { useState } from 'react';
import styled from '@emotion/styled';
import { EmailThreadMessageBody } from '@/activities/emails/components/EmailThreadMessageBody';
import { EmailThreadMessageBodyPreview } from '@/activities/emails/components/EmailThreadMessageBodyPreview';
import { EmailThreadMessageSender } from '@/activities/emails/components/EmailThreadMessageSender';
import { MockedEmailUser } from '@/activities/emails/mocks/mockedEmailThreads';
const StyledThreadMessage = styled.div`
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
cursor: pointer;
display: flex;
flex-direction: column;
padding: ${({ theme }) => theme.spacing(4, 6)};
`;
const StyledThreadMessageHeader = styled.div`
display: flex;
flex-direction: column;
justify-content: space-between;
margin-bottom: ${({ theme }) => theme.spacing(2)};
`;
type EmailThreadMessageProps = {
id: string;
body: string;
sentAt: string;
from: MockedEmailUser;
to: MockedEmailUser[];
};
export const EmailThreadMessage = ({
body,
sentAt,
from,
}: EmailThreadMessageProps) => {
const { displayName, avatarUrl } = from;
const [isOpen, setIsOpen] = useState(false);
return (
<StyledThreadMessage onClick={() => setIsOpen(!isOpen)}>
<StyledThreadMessageHeader>
<EmailThreadMessageSender
displayName={displayName}
avatarUrl={avatarUrl}
sentAt={sentAt}
/>
</StyledThreadMessageHeader>
{isOpen ? (
<EmailThreadMessageBody body={body} />
) : (
<EmailThreadMessageBodyPreview body={body} />
)}
</StyledThreadMessage>
);
};

View File

@ -0,0 +1,20 @@
import React from 'react';
import styled from '@emotion/styled';
const StyledThreadMessageBody = styled.div`
color: ${({ theme }) => theme.font.color.primary};
display: flex;
flex-direction: column;
margin-top: ${({ theme }) => theme.spacing(4)};
white-space: pre-line;
`;
type EmailThreadMessageBodyProps = {
body: string;
};
export const EmailThreadMessageBody = ({
body,
}: EmailThreadMessageBodyProps) => {
return <StyledThreadMessageBody>{body}</StyledThreadMessageBody>;
};

View File

@ -0,0 +1,23 @@
import React from 'react';
import styled from '@emotion/styled';
const StyledThreadMessageBodyPreview = styled.span`
color: ${({ theme }) => theme.font.color.tertiary};
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: ${({ theme }) => theme.font.size.sm};
`;
type EmailThreadMessageBodyPreviewProps = {
body: string;
};
export const EmailThreadMessageBodyPreview = ({
body,
}: EmailThreadMessageBodyPreviewProps) => {
return (
<StyledThreadMessageBodyPreview>{body}</StyledThreadMessageBodyPreview>
);
};

View File

@ -0,0 +1,61 @@
import React from 'react';
import styled from '@emotion/styled';
import { Avatar } from '@/users/components/Avatar';
import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';
const StyledEmailThreadMessageSender = styled.div`
display: flex;
justify-content: space-between;
`;
const StyledEmailThreadMessageSenderUser = styled.div`
align-items: flex-start;
display: flex;
`;
const StyledAvatar = styled(Avatar)`
margin: ${({ theme }) => theme.spacing(0, 1)};
`;
const StyledSenderName = styled.span`
font-size: ${({ theme }) => theme.font.size.sm};
overflow: hidden;
text-overflow: ellipsis;
`;
const StyledThreadMessageSentAt = styled.div`
align-items: flex-end;
display: flex;
color: ${({ theme }) => theme.font.color.tertiary};
font-size: ${({ theme }) => theme.font.size.sm};
`;
type EmailThreadMessageSenderProps = {
displayName: string;
avatarUrl: string;
sentAt: string;
};
export const EmailThreadMessageSender = ({
displayName,
avatarUrl,
sentAt,
}: EmailThreadMessageSenderProps) => {
return (
<StyledEmailThreadMessageSender>
<StyledEmailThreadMessageSenderUser>
<StyledAvatar
avatarUrl={avatarUrl}
type="rounded"
placeholder={displayName}
size="sm"
/>
<StyledSenderName>{displayName}</StyledSenderName>
</StyledEmailThreadMessageSenderUser>
<StyledThreadMessageSentAt>
{beautifyPastDateRelativeToNow(sentAt)}
</StyledThreadMessageSentAt>
</StyledEmailThreadMessageSender>
);
};

View File

@ -1,6 +1,5 @@
import styled from '@emotion/styled';
import { useOpenThreadRightDrawer } from '@/activities/emails/right-drawer/hooks/useOpenThreadRightDrawer';
import { CardContent } from '@/ui/layout/card/components/CardContent';
import { Avatar } from '@/users/components/Avatar';
import { TimelineThread } from '~/generated/graphql';
@ -78,19 +77,19 @@ const StyledReceivedAt = styled.div`
padding: ${({ theme }) => theme.spacing(0, 1)};
`;
type ThreadPreviewProps = {
type EmailThreadPreviewProps = {
divider?: boolean;
thread: TimelineThread;
onClick: () => void;
};
export const ThreadPreview = ({ divider, thread }: ThreadPreviewProps) => {
const openMessageThreadRightDrawer = useOpenThreadRightDrawer();
export const EmailThreadPreview = ({
divider,
thread,
onClick,
}: EmailThreadPreviewProps) => {
return (
<StyledCardContent
onClick={() => openMessageThreadRightDrawer()}
divider={divider}
>
<StyledCardContent onClick={() => onClick()} divider={divider}>
<StyledHeading unread={!thread.read}>
<StyledAvatar
avatarUrl={thread.senderPictureUrl}

View File

@ -1,7 +1,12 @@
import { useQuery } from '@apollo/client';
import styled from '@emotion/styled';
import { ThreadPreview } from '@/activities/emails/components/ThreadPreview';
import { EmailThreadPreview } from '@/activities/emails/components/EmailThreadPreview';
import { useEmailThread } from '@/activities/emails/hooks/useEmailThread';
import {
mockedEmailThreads,
MockedThread,
} from '@/activities/emails/mocks/mockedEmailThreads';
import { getTimelineThreadsFromCompanyId } from '@/activities/emails/queries/getTimelineThreadsFromCompanyId';
import { getTimelineThreadsFromPersonId } from '@/activities/emails/queries/getTimelineThreadsFromPersonId';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
@ -12,7 +17,6 @@ import {
} from '@/ui/display/typography/components/H1Title';
import { Card } from '@/ui/layout/card/components/Card';
import { Section } from '@/ui/layout/section/components/Section';
import { TimelineThread } from '~/generated/graphql';
const StyledContainer = styled.div`
display: flex;
@ -30,7 +34,13 @@ const StyledEmailCount = styled.span`
color: ${({ theme }) => theme.font.color.light};
`;
export const Threads = ({ entity }: { entity: ActivityTargetableObject }) => {
export const EmailThreads = ({
entity,
}: {
entity: ActivityTargetableObject;
}) => {
const { openEmailThread } = useEmailThread();
const threadQuery =
entity.targetObjectNameSingular === CoreObjectNameSingular.Person
? getTimelineThreadsFromPersonId
@ -49,12 +59,16 @@ export const Threads = ({ entity }: { entity: ActivityTargetableObject }) => {
return;
}
const timelineThreads: TimelineThread[] =
threads.data[
entity.targetObjectNameSingular === CoreObjectNameSingular.Person
? 'getTimelineThreadsFromPersonId'
: 'getTimelineThreadsFromCompanyId'
];
// To use once the id is returned by the query
// const fetchedTimelineThreads: TimelineThread[] =
// threads.data[
// entity.targetObjectNameSingular === CoreObjectNameSingular.Person
// ? 'getTimelineThreadsFromPersonId'
// : 'getTimelineThreadsFromCompanyId'
// ];
const timelineThreads = mockedEmailThreads;
return (
<StyledContainer>
@ -72,11 +86,12 @@ export const Threads = ({ entity }: { entity: ActivityTargetableObject }) => {
/>
<Card>
{timelineThreads &&
timelineThreads.map((thread: TimelineThread, index: number) => (
<ThreadPreview
timelineThreads.map((thread: MockedThread, index: number) => (
<EmailThreadPreview
key={index}
divider={index < timelineThreads.length - 1}
thread={thread}
onClick={() => openEmailThread(thread)}
/>
))}
</Card>