3263 modify timeline messagingservice to allow the frontend to get multiple participants in a thread (#3611)

* wip

* wip

* add pagination

* wip

* wip

* wip

* update resolver

* wip

* wip

* endpoint is working but there is still work to do

* merge main

* wip

* subject is now first subject

* number of messages is working

* improving query

* fix bug

* fix bug

* added parameter

* pagination introduced a bug

* pagination is working

* fix type

* improve typing

* improve typing

* fix bug

* add displayName

* display displayName in the frontend

* move entities

* fix

* generate metadata

* add avatarUrl

* modify after comments on PR

* updates

* remove email mocks

* remove console log

* move files

* remove mock

* use constant

* use constant

* use fragments

* remove console.log

* generate

* changes made

* update DTO

* generate
This commit is contained in:
bosiraphael
2024-01-25 17:04:51 +01:00
committed by GitHub
parent 6f98d1847f
commit 6004969096
19 changed files with 617 additions and 207 deletions

View File

@ -443,6 +443,17 @@ export type Telemetry = {
enabled: Scalars['Boolean']['output']; enabled: Scalars['Boolean']['output'];
}; };
export type TimelineThreadParticipant = {
__typename?: 'TimelineThreadParticipant';
avatarUrl: Scalars['String']['output'];
displayName: Scalars['String']['output'];
firstName: Scalars['String']['output'];
handle: Scalars['String']['output'];
lastName: Scalars['String']['output'];
personId?: Maybe<Scalars['String']['output']>;
workspaceMemberId?: Maybe<Scalars['String']['output']>;
};
export type UpdateFieldInput = { export type UpdateFieldInput = {
defaultValue?: InputMaybe<Scalars['JSON']['input']>; defaultValue?: InputMaybe<Scalars['JSON']['input']>;
description?: InputMaybe<Scalars['String']['input']>; description?: InputMaybe<Scalars['String']['input']>;

View File

@ -402,12 +402,16 @@ export type QueryFindWorkspaceFromInviteHashArgs = {
export type QueryGetTimelineThreadsFromCompanyIdArgs = { export type QueryGetTimelineThreadsFromCompanyIdArgs = {
companyId: Scalars['String']; companyId: Scalars['ID'];
page: Scalars['Int'];
pageSize: Scalars['Int'];
}; };
export type QueryGetTimelineThreadsFromPersonIdArgs = { export type QueryGetTimelineThreadsFromPersonIdArgs = {
personId: Scalars['String']; page: Scalars['Int'];
pageSize: Scalars['Int'];
personId: Scalars['ID'];
}; };
@ -491,15 +495,28 @@ export type Telemetry = {
export type TimelineThread = { export type TimelineThread = {
__typename?: 'TimelineThread'; __typename?: 'TimelineThread';
body: Scalars['String']; firstParticipant: TimelineThreadParticipant;
id: Scalars['ID'];
lastMessageBody: Scalars['String'];
lastMessageReceivedAt: Scalars['DateTime'];
lastTwoParticipants: Array<TimelineThreadParticipant>;
numberOfMessagesInThread: Scalars['Float']; numberOfMessagesInThread: Scalars['Float'];
participantCount: Scalars['Float'];
read: Scalars['Boolean']; read: Scalars['Boolean'];
receivedAt: Scalars['DateTime'];
senderName: Scalars['String'];
senderPictureUrl: Scalars['String'];
subject: Scalars['String']; subject: Scalars['String'];
}; };
export type TimelineThreadParticipant = {
__typename?: 'TimelineThreadParticipant';
avatarUrl: Scalars['String'];
displayName: Scalars['String'];
firstName: Scalars['String'];
handle: Scalars['String'];
lastName: Scalars['String'];
personId?: Maybe<Scalars['ID']>;
workspaceMemberId?: Maybe<Scalars['ID']>;
};
export type TransientToken = { export type TransientToken = {
__typename?: 'TransientToken'; __typename?: 'TransientToken';
transientToken: AuthToken; transientToken: AuthToken;
@ -694,19 +711,27 @@ export type RelationEdge = {
node: Relation; node: Relation;
}; };
export type ParticipantFragmentFragment = { __typename?: 'TimelineThreadParticipant', personId?: string | null, workspaceMemberId?: string | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string };
export type TimelineThreadFragmentFragment = { __typename?: 'TimelineThread', id: string, read: boolean, lastMessageReceivedAt: string, lastMessageBody: string, subject: string, numberOfMessagesInThread: number, participantCount: number, firstParticipant: { __typename?: 'TimelineThreadParticipant', personId?: string | null, workspaceMemberId?: string | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }, lastTwoParticipants: Array<{ __typename?: 'TimelineThreadParticipant', personId?: string | null, workspaceMemberId?: string | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }> };
export type GetTimelineThreadsFromCompanyIdQueryVariables = Exact<{ export type GetTimelineThreadsFromCompanyIdQueryVariables = Exact<{
companyId: Scalars['String']; companyId: Scalars['ID'];
page: Scalars['Int'];
pageSize: Scalars['Int'];
}>; }>;
export type GetTimelineThreadsFromCompanyIdQuery = { __typename?: 'Query', getTimelineThreadsFromCompanyId: Array<{ __typename?: 'TimelineThread', body: string, numberOfMessagesInThread: number, read: boolean, receivedAt: string, senderName: string, senderPictureUrl: string, subject: string }> }; export type GetTimelineThreadsFromCompanyIdQuery = { __typename?: 'Query', getTimelineThreadsFromCompanyId: Array<{ __typename?: 'TimelineThread', id: string, read: boolean, lastMessageReceivedAt: string, lastMessageBody: string, subject: string, numberOfMessagesInThread: number, participantCount: number, firstParticipant: { __typename?: 'TimelineThreadParticipant', personId?: string | null, workspaceMemberId?: string | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }, lastTwoParticipants: Array<{ __typename?: 'TimelineThreadParticipant', personId?: string | null, workspaceMemberId?: string | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }> }> };
export type GetTimelineThreadsFromPersonIdQueryVariables = Exact<{ export type GetTimelineThreadsFromPersonIdQueryVariables = Exact<{
personId: Scalars['String']; personId: Scalars['ID'];
page: Scalars['Int'];
pageSize: Scalars['Int'];
}>; }>;
export type GetTimelineThreadsFromPersonIdQuery = { __typename?: 'Query', getTimelineThreadsFromPersonId: Array<{ __typename?: 'TimelineThread', body: string, numberOfMessagesInThread: number, read: boolean, receivedAt: string, senderName: string, senderPictureUrl: string, subject: string }> }; export type GetTimelineThreadsFromPersonIdQuery = { __typename?: 'Query', getTimelineThreadsFromPersonId: Array<{ __typename?: 'TimelineThread', id: string, read: boolean, lastMessageReceivedAt: string, lastMessageBody: string, subject: string, numberOfMessagesInThread: number, participantCount: number, firstParticipant: { __typename?: 'TimelineThreadParticipant', personId?: string | null, workspaceMemberId?: string | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }, lastTwoParticipants: Array<{ __typename?: 'TimelineThreadParticipant', personId?: string | null, workspaceMemberId?: string | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }> }> };
export type CreateEventMutationVariables = Exact<{ export type CreateEventMutationVariables = Exact<{
type: Scalars['String']; type: Scalars['String'];
@ -859,6 +884,34 @@ export type GetWorkspaceFromInviteHashQueryVariables = Exact<{
export type GetWorkspaceFromInviteHashQuery = { __typename?: 'Query', findWorkspaceFromInviteHash: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, allowImpersonation: boolean } }; export type GetWorkspaceFromInviteHashQuery = { __typename?: 'Query', findWorkspaceFromInviteHash: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, allowImpersonation: boolean } };
export const ParticipantFragmentFragmentDoc = gql`
fragment ParticipantFragment on TimelineThreadParticipant {
personId
workspaceMemberId
firstName
lastName
displayName
avatarUrl
handle
}
`;
export const TimelineThreadFragmentFragmentDoc = gql`
fragment TimelineThreadFragment on TimelineThread {
id
read
firstParticipant {
...ParticipantFragment
}
lastTwoParticipants {
...ParticipantFragment
}
lastMessageReceivedAt
lastMessageBody
subject
numberOfMessagesInThread
participantCount
}
${ParticipantFragmentFragmentDoc}`;
export const AuthTokenFragmentFragmentDoc = gql` export const AuthTokenFragmentFragmentDoc = gql`
fragment AuthTokenFragment on AuthToken { fragment AuthTokenFragment on AuthToken {
token token
@ -911,18 +964,16 @@ export const UserQueryFragmentFragmentDoc = gql`
} }
`; `;
export const GetTimelineThreadsFromCompanyIdDocument = gql` export const GetTimelineThreadsFromCompanyIdDocument = gql`
query GetTimelineThreadsFromCompanyId($companyId: String!) { query GetTimelineThreadsFromCompanyId($companyId: ID!, $page: Int!, $pageSize: Int!) {
getTimelineThreadsFromCompanyId(companyId: $companyId) { getTimelineThreadsFromCompanyId(
body companyId: $companyId
numberOfMessagesInThread page: $page
read pageSize: $pageSize
receivedAt ) {
senderName ...TimelineThreadFragment
senderPictureUrl
subject
} }
} }
`; ${TimelineThreadFragmentFragmentDoc}`;
/** /**
* __useGetTimelineThreadsFromCompanyIdQuery__ * __useGetTimelineThreadsFromCompanyIdQuery__
@ -937,6 +988,8 @@ export const GetTimelineThreadsFromCompanyIdDocument = gql`
* const { data, loading, error } = useGetTimelineThreadsFromCompanyIdQuery({ * const { data, loading, error } = useGetTimelineThreadsFromCompanyIdQuery({
* variables: { * variables: {
* companyId: // value for 'companyId' * companyId: // value for 'companyId'
* page: // value for 'page'
* pageSize: // value for 'pageSize'
* }, * },
* }); * });
*/ */
@ -952,18 +1005,16 @@ export type GetTimelineThreadsFromCompanyIdQueryHookResult = ReturnType<typeof u
export type GetTimelineThreadsFromCompanyIdLazyQueryHookResult = ReturnType<typeof useGetTimelineThreadsFromCompanyIdLazyQuery>; export type GetTimelineThreadsFromCompanyIdLazyQueryHookResult = ReturnType<typeof useGetTimelineThreadsFromCompanyIdLazyQuery>;
export type GetTimelineThreadsFromCompanyIdQueryResult = Apollo.QueryResult<GetTimelineThreadsFromCompanyIdQuery, GetTimelineThreadsFromCompanyIdQueryVariables>; export type GetTimelineThreadsFromCompanyIdQueryResult = Apollo.QueryResult<GetTimelineThreadsFromCompanyIdQuery, GetTimelineThreadsFromCompanyIdQueryVariables>;
export const GetTimelineThreadsFromPersonIdDocument = gql` export const GetTimelineThreadsFromPersonIdDocument = gql`
query GetTimelineThreadsFromPersonId($personId: String!) { query GetTimelineThreadsFromPersonId($personId: ID!, $page: Int!, $pageSize: Int!) {
getTimelineThreadsFromPersonId(personId: $personId) { getTimelineThreadsFromPersonId(
body personId: $personId
numberOfMessagesInThread page: $page
read pageSize: $pageSize
receivedAt ) {
senderName ...TimelineThreadFragment
senderPictureUrl
subject
} }
} }
`; ${TimelineThreadFragmentFragmentDoc}`;
/** /**
* __useGetTimelineThreadsFromPersonIdQuery__ * __useGetTimelineThreadsFromPersonIdQuery__
@ -978,6 +1029,8 @@ export const GetTimelineThreadsFromPersonIdDocument = gql`
* const { data, loading, error } = useGetTimelineThreadsFromPersonIdQuery({ * const { data, loading, error } = useGetTimelineThreadsFromPersonIdQuery({
* variables: { * variables: {
* personId: // value for 'personId' * personId: // value for 'personId'
* page: // value for 'page'
* pageSize: // value for 'pageSize'
* }, * },
* }); * });
*/ */

View File

@ -92,20 +92,22 @@ export const EmailThreadPreview = ({
<StyledCardContent onClick={() => onClick()} divider={divider}> <StyledCardContent onClick={() => onClick()} divider={divider}>
<StyledHeading unread={!thread.read}> <StyledHeading unread={!thread.read}>
<StyledAvatar <StyledAvatar
avatarUrl={thread.senderPictureUrl} avatarUrl={thread.firstParticipant.avatarUrl}
placeholder={thread.senderName} placeholder={thread.firstParticipant.displayName}
type="rounded" type="rounded"
/> />
<StyledSenderName>{thread.senderName}</StyledSenderName> <StyledSenderName>
{thread.firstParticipant.displayName}
</StyledSenderName>
<StyledThreadCount>{thread.numberOfMessagesInThread}</StyledThreadCount> <StyledThreadCount>{thread.numberOfMessagesInThread}</StyledThreadCount>
</StyledHeading> </StyledHeading>
<StyledSubjectAndBody> <StyledSubjectAndBody>
<StyledSubject unread={!thread.read}>{thread.subject}</StyledSubject> <StyledSubject unread={!thread.read}>{thread.subject}</StyledSubject>
<StyledBody>{thread.body}</StyledBody> <StyledBody>{thread.lastMessageBody}</StyledBody>
</StyledSubjectAndBody> </StyledSubjectAndBody>
<StyledReceivedAt> <StyledReceivedAt>
{formatToHumanReadableDate(thread.receivedAt)} {formatToHumanReadableDate(thread.lastMessageReceivedAt)}
</StyledReceivedAt> </StyledReceivedAt>
</StyledCardContent> </StyledCardContent>
); );

View File

@ -2,11 +2,8 @@ import { useQuery } from '@apollo/client';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { EmailThreadPreview } from '@/activities/emails/components/EmailThreadPreview'; import { EmailThreadPreview } from '@/activities/emails/components/EmailThreadPreview';
import { TIMELINE_THREADS_DEFAULT_PAGE_SIZE } from '@/activities/emails/constants/messaging.constants';
import { useEmailThread } from '@/activities/emails/hooks/useEmailThread'; import { useEmailThread } from '@/activities/emails/hooks/useEmailThread';
import {
mockedEmailThreads,
MockedThread,
} from '@/activities/emails/mocks/mockedEmailThreads';
import { getTimelineThreadsFromCompanyId } from '@/activities/emails/queries/getTimelineThreadsFromCompanyId'; import { getTimelineThreadsFromCompanyId } from '@/activities/emails/queries/getTimelineThreadsFromCompanyId';
import { getTimelineThreadsFromPersonId } from '@/activities/emails/queries/getTimelineThreadsFromPersonId'; import { getTimelineThreadsFromPersonId } from '@/activities/emails/queries/getTimelineThreadsFromPersonId';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
@ -17,6 +14,7 @@ import {
} from '@/ui/display/typography/components/H1Title'; } from '@/ui/display/typography/components/H1Title';
import { Card } from '@/ui/layout/card/components/Card'; import { Card } from '@/ui/layout/card/components/Card';
import { Section } from '@/ui/layout/section/components/Section'; import { Section } from '@/ui/layout/section/components/Section';
import { TimelineThread } from '~/generated/graphql';
const StyledContainer = styled.div` const StyledContainer = styled.div`
display: flex; display: flex;
@ -46,10 +44,13 @@ export const EmailThreads = ({
? getTimelineThreadsFromPersonId ? getTimelineThreadsFromPersonId
: getTimelineThreadsFromCompanyId; : getTimelineThreadsFromCompanyId;
const threadQueryVariables = const threadQueryVariables = {
entity.targetObjectNameSingular === CoreObjectNameSingular.Person ...(entity.targetObjectNameSingular === CoreObjectNameSingular.Person
? { personId: entity.id } ? { personId: entity.id }
: { companyId: entity.id }; : { companyId: entity.id }),
page: 1,
pageSize: TIMELINE_THREADS_DEFAULT_PAGE_SIZE,
};
const threads = useQuery(threadQuery, { const threads = useQuery(threadQuery, {
variables: threadQueryVariables, variables: threadQueryVariables,
@ -59,16 +60,12 @@ export const EmailThreads = ({
return; return;
} }
// To use once the id is returned by the query const timelineThreads: TimelineThread[] =
threads.data[
// const fetchedTimelineThreads: TimelineThread[] = entity.targetObjectNameSingular === CoreObjectNameSingular.Person
// threads.data[ ? 'getTimelineThreadsFromPersonId'
// entity.targetObjectNameSingular === CoreObjectNameSingular.Person : 'getTimelineThreadsFromCompanyId'
// ? 'getTimelineThreadsFromPersonId' ];
// : 'getTimelineThreadsFromCompanyId'
// ];
const timelineThreads = mockedEmailThreads;
return ( return (
<StyledContainer> <StyledContainer>
@ -77,16 +74,14 @@ export const EmailThreads = ({
title={ title={
<> <>
Inbox{' '} Inbox{' '}
<StyledEmailCount> <StyledEmailCount>{timelineThreads?.length}</StyledEmailCount>
{timelineThreads && timelineThreads.length}
</StyledEmailCount>
</> </>
} }
fontColor={H1TitleFontColor.Primary} fontColor={H1TitleFontColor.Primary}
/> />
<Card> <Card>
{timelineThreads && {timelineThreads &&
timelineThreads.map((thread: MockedThread, index: number) => ( timelineThreads.map((thread: TimelineThread, index: number) => (
<EmailThreadPreview <EmailThreadPreview
key={index} key={index}
divider={index < timelineThreads.length - 1} divider={index < timelineThreads.length - 1}

View File

@ -0,0 +1 @@
export const TIMELINE_THREADS_DEFAULT_PAGE_SIZE = 20;

View File

@ -1,15 +1,15 @@
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import { MockedThread } from '@/activities/emails/mocks/mockedEmailThreads';
import { useOpenEmailThreadRightDrawer } from '@/activities/emails/right-drawer/hooks/useOpenEmailThreadRightDrawer'; import { useOpenEmailThreadRightDrawer } from '@/activities/emails/right-drawer/hooks/useOpenEmailThreadRightDrawer';
import { viewableEmailThreadState } from '@/activities/emails/state/viewableEmailThreadState'; import { viewableEmailThreadState } from '@/activities/emails/state/viewableEmailThreadState';
import { TimelineThread } from '~/generated/graphql';
export const useEmailThread = () => { export const useEmailThread = () => {
const [, setViewableEmailThread] = useRecoilState(viewableEmailThreadState); const [, setViewableEmailThread] = useRecoilState(viewableEmailThreadState);
const openEmailThredRightDrawer = useOpenEmailThreadRightDrawer(); const openEmailThredRightDrawer = useOpenEmailThreadRightDrawer();
const openEmailThread = (thread: MockedThread) => { const openEmailThread = (thread: TimelineThread) => {
openEmailThredRightDrawer(); openEmailThredRightDrawer();
setViewableEmailThread(thread); setViewableEmailThread(thread);

View File

@ -1,30 +0,0 @@
import { Scalars, TimelineThread } from '~/generated/graphql';
export type MockedThread = {
id: string;
} & TimelineThread;
export const mockedEmailThreads: MockedThread[] = [
{
__typename: 'TimelineThread',
id: 'ec7e12b9-4063-410f-ae9a-30e32452b9c0',
body: 'This is a test email' as Scalars['String'],
numberOfMessagesInThread: 5 as Scalars['Float'],
read: true as Scalars['Boolean'],
receivedAt: new Date().toISOString() as Scalars['DateTime'],
senderName: 'Thom Trp' as Scalars['String'],
senderPictureUrl: '' as Scalars['String'],
subject: 'Test email' as Scalars['String'],
},
{
__typename: 'TimelineThread',
id: 'ec7e12b9-4063-410f-ae9a-30e32452b9c0',
body: 'This is a second test email' as Scalars['String'],
numberOfMessagesInThread: 5 as Scalars['Float'],
read: true as Scalars['Boolean'],
receivedAt: new Date().toISOString() as Scalars['DateTime'],
senderName: 'Coco Den' as Scalars['String'],
senderPictureUrl: '' as Scalars['String'],
subject: 'Test email number 2' as Scalars['String'],
},
];

View File

@ -0,0 +1,13 @@
import { gql } from '@apollo/client';
export const participantFragment = gql`
fragment ParticipantFragment on TimelineThreadParticipant {
personId
workspaceMemberId
firstName
lastName
displayName
avatarUrl
handle
}
`;

View File

@ -0,0 +1,22 @@
import { gql } from '@apollo/client';
import { participantFragment } from '@/activities/emails/queries/fragments/participantFragment';
export const timelineThreadFragment = gql`
fragment TimelineThreadFragment on TimelineThread {
id
read
firstParticipant {
...ParticipantFragment
}
lastTwoParticipants {
...ParticipantFragment
}
lastMessageReceivedAt
lastMessageBody
subject
numberOfMessagesInThread
participantCount
}
${participantFragment}
`;

View File

@ -1,15 +1,20 @@
import { gql } from '@apollo/client'; import { gql } from '@apollo/client';
import { timelineThreadFragment } from '@/activities/emails/queries/fragments/timelineThreadFragment';
export const getTimelineThreadsFromCompanyId = gql` export const getTimelineThreadsFromCompanyId = gql`
query GetTimelineThreadsFromCompanyId($companyId: String!) { query GetTimelineThreadsFromCompanyId(
getTimelineThreadsFromCompanyId(companyId: $companyId) { $companyId: ID!
body $page: Int!
numberOfMessagesInThread $pageSize: Int!
read ) {
receivedAt getTimelineThreadsFromCompanyId(
senderName companyId: $companyId
senderPictureUrl page: $page
subject pageSize: $pageSize
) {
...TimelineThreadFragment
} }
} }
${timelineThreadFragment}
`; `;

View File

@ -1,15 +1,20 @@
import { gql } from '@apollo/client'; import { gql } from '@apollo/client';
import { timelineThreadFragment } from '@/activities/emails/queries/fragments/timelineThreadFragment';
export const getTimelineThreadsFromPersonId = gql` export const getTimelineThreadsFromPersonId = gql`
query GetTimelineThreadsFromPersonId($personId: String!) { query GetTimelineThreadsFromPersonId(
getTimelineThreadsFromPersonId(personId: $personId) { $personId: ID!
body $page: Int!
numberOfMessagesInThread $pageSize: Int!
read ) {
receivedAt getTimelineThreadsFromPersonId(
senderName personId: $personId
senderPictureUrl page: $page
subject pageSize: $pageSize
) {
...TimelineThreadFragment
} }
} }
${timelineThreadFragment}
`; `;

View File

@ -50,7 +50,7 @@ export const RightDrawerEmailThread = () => {
<StyledContainer> <StyledContainer>
<EmailThreadHeader <EmailThreadHeader
subject={viewableEmailThread.subject} subject={viewableEmailThread.subject}
lastMessageSentAt={viewableEmailThread.receivedAt} lastMessageSentAt={viewableEmailThread.lastMessageReceivedAt}
/> />
{messages.map((message) => ( {messages.map((message) => (
<EmailThreadMessage <EmailThreadMessage

View File

@ -1,8 +1,8 @@
import { atom } from 'recoil'; import { atom } from 'recoil';
import { MockedThread } from '@/activities/emails/mocks/mockedEmailThreads'; import { TimelineThread } from '~/generated/graphql';
export const viewableEmailThreadState = atom<MockedThread | null>({ export const viewableEmailThreadState = atom<TimelineThread | null>({
key: 'viewableEmailThreadState', key: 'viewableEmailThreadState',
default: null, default: null,
}); });

View File

@ -4,7 +4,6 @@ import { Comment } from '@/activities/types/Comment';
import { Company } from '@/companies/types/Company'; import { Company } from '@/companies/types/Company';
import { Person } from '@/people/types/Person'; import { Person } from '@/people/types/Person';
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember'; import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
import { TimelineThread } from '~/generated/graphql';
type MockedActivity = Pick< type MockedActivity = Pick<
Activity, Activity,
@ -211,24 +210,3 @@ export const mockedActivities: Array<MockedActivity> = [
__typename: 'Activity', __typename: 'Activity',
}, },
]; ];
export const mockedEmailThreads: TimelineThread[] = [
{
body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras dignissim nisi eu tellus dapibus, egestas placerat risus placerat. Praesent eget arcu consectetur, efficitur felis.',
numberOfMessagesInThread: 4,
read: false,
receivedAt: new Date('11/04/2023').toISOString(),
senderName: 'Steve Anahi',
senderPictureUrl: '',
subject: 'Partnerships',
},
{
body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras dignissim nisi eu tellus dapibus, egestas placerat risus placerat. Praesent eget arcu consectetur, efficitur felis.',
numberOfMessagesInThread: 3,
read: true,
receivedAt: new Date('11/04/2023').toISOString(),
senderName: 'Alexandre Prot',
senderPictureUrl: '',
subject: 'Next step',
},
];

View File

@ -0,0 +1,2 @@
export const TIMELINE_THREADS_DEFAULT_PAGE_SIZE = 20;
export const TIMELINE_THREADS_MAX_PAGE_SIZE = 50;

View File

@ -0,0 +1,25 @@
import { ObjectType, Field, ID } from '@nestjs/graphql';
@ObjectType('TimelineThreadParticipant')
export class TimelineThreadParticipant {
@Field(() => ID, { nullable: true })
personId: string;
@Field(() => ID, { nullable: true })
workspaceMemberId: string;
@Field()
firstName: string;
@Field()
lastName: string;
@Field()
displayName: string;
@Field()
avatarUrl: string;
@Field()
handle: string;
}

View File

@ -0,0 +1,35 @@
import { ObjectType, Field, ID } from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import { TimelineThreadParticipant } from 'src/core/messaging/dtos/timeline-thread-participant.dto';
@ObjectType('TimelineThread')
export class TimelineThread {
@IDField(() => ID)
id: string;
@Field()
read: boolean;
@Field()
firstParticipant: TimelineThreadParticipant;
@Field(() => [TimelineThreadParticipant])
lastTwoParticipants: TimelineThreadParticipant[];
@Field()
lastMessageReceivedAt: Date;
@Field()
lastMessageBody: string;
@Field()
subject: string;
@Field()
numberOfMessagesInThread: number;
@Field()
participantCount: number;
}

View File

@ -1,43 +1,47 @@
import { Args, Query, Field, Resolver, ObjectType } from '@nestjs/graphql'; import {
Args,
Query,
Resolver,
Int,
ArgsType,
Field,
ID,
} from '@nestjs/graphql';
import { UseGuards } from '@nestjs/common'; import { UseGuards } from '@nestjs/common';
import { Column, Entity } from 'typeorm'; import { Max } from 'class-validator';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard'; import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import { Workspace } from 'src/core/workspace/workspace.entity'; import { Workspace } from 'src/core/workspace/workspace.entity';
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator'; import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
import { TimelineMessagingService } from 'src/core/messaging/timeline-messaging.service'; import { TimelineMessagingService } from 'src/core/messaging/timeline-messaging.service';
import { TimelineThread } from 'src/core/messaging/dtos/timeline-thread.dto';
import { TIMELINE_THREADS_MAX_PAGE_SIZE } from 'src/core/messaging/constants/messaging.constants';
@Entity({ name: 'timelineThread', schema: 'core' }) @ArgsType()
@ObjectType('TimelineThread') class GetTimelineThreadsFromPersonIdArgs {
export class TimelineThread { @Field(() => ID)
@Field() personId: string;
@Column()
read: boolean;
@Field() @Field(() => Int)
@Column() page: number;
senderName: string;
@Field() @Field(() => Int)
@Column() @Max(TIMELINE_THREADS_MAX_PAGE_SIZE)
senderPictureUrl: string; pageSize: number;
}
@Field() @ArgsType()
@Column() class GetTimelineThreadsFromCompanyIdArgs {
numberOfMessagesInThread: number; @Field(() => ID)
companyId: string;
@Field() @Field(() => Int)
@Column() page: number;
subject: string;
@Field() @Field(() => Int)
@Column() @Max(TIMELINE_THREADS_MAX_PAGE_SIZE)
body: string; pageSize: number;
@Field()
@Column()
receivedAt: Date;
} }
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@ -50,12 +54,14 @@ export class TimelineMessagingResolver {
@Query(() => [TimelineThread]) @Query(() => [TimelineThread])
async getTimelineThreadsFromPersonId( async getTimelineThreadsFromPersonId(
@AuthWorkspace() { id: workspaceId }: Workspace, @AuthWorkspace() { id: workspaceId }: Workspace,
@Args('personId') personId: string, @Args() { personId, page, pageSize }: GetTimelineThreadsFromPersonIdArgs,
) { ) {
const timelineThreads = const timelineThreads =
await this.timelineMessagingService.getMessagesFromPersonIds( await this.timelineMessagingService.getMessagesFromPersonIds(
workspaceId, workspaceId,
[personId], [personId],
page,
pageSize,
); );
return timelineThreads; return timelineThreads;
@ -64,12 +70,14 @@ export class TimelineMessagingResolver {
@Query(() => [TimelineThread]) @Query(() => [TimelineThread])
async getTimelineThreadsFromCompanyId( async getTimelineThreadsFromCompanyId(
@AuthWorkspace() { id: workspaceId }: Workspace, @AuthWorkspace() { id: workspaceId }: Workspace,
@Args('companyId') companyId: string, @Args() { companyId, page, pageSize }: GetTimelineThreadsFromCompanyIdArgs,
) { ) {
const timelineThreads = const timelineThreads =
await this.timelineMessagingService.getMessagesFromCompanyId( await this.timelineMessagingService.getMessagesFromCompanyId(
workspaceId, workspaceId,
companyId, companyId,
page,
pageSize,
); );
return timelineThreads; return timelineThreads;

View File

@ -1,9 +1,20 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { TimelineThread } from 'src/core/messaging/timeline-messaging.resolver'; import { TIMELINE_THREADS_DEFAULT_PAGE_SIZE } from 'src/core/messaging/constants/messaging.constants';
import { TimelineThread } from 'src/core/messaging/dtos/timeline-thread.dto';
import { TypeORMService } from 'src/database/typeorm/typeorm.service'; import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { DataSourceService } from 'src/metadata/data-source/data-source.service'; import { DataSourceService } from 'src/metadata/data-source/data-source.service';
type TimelineThreadParticipant = {
personId: string;
workspaceMemberId: string;
firstName: string;
lastName: string;
displayName: string;
avatarUrl: string;
handle: string;
};
@Injectable() @Injectable()
export class TimelineMessagingService { export class TimelineMessagingService {
constructor( constructor(
@ -14,7 +25,11 @@ export class TimelineMessagingService {
async getMessagesFromPersonIds( async getMessagesFromPersonIds(
workspaceId: string, workspaceId: string,
personIds: string[], personIds: string[],
page: number = 1,
pageSize: number = TIMELINE_THREADS_DEFAULT_PAGE_SIZE,
): Promise<TimelineThread[]> { ): Promise<TimelineThread[]> {
const offset = (page - 1) * TIMELINE_THREADS_DEFAULT_PAGE_SIZE;
const dataSourceMetadata = const dataSourceMetadata =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail( await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
workspaceId, workspaceId,
@ -23,61 +38,327 @@ export class TimelineMessagingService {
const workspaceDataSource = const workspaceDataSource =
await this.typeORMService.connectToDataSource(dataSourceMetadata); await this.typeORMService.connectToDataSource(dataSourceMetadata);
// 10 first threads This hard limit is just for the POC, we will implement pagination later const messageThreads:
const messageThreads = await workspaceDataSource?.query( | {
id: string;
lastMessageReceivedAt: Date;
lastMessageId: string;
lastMessageBody: string;
rowNumber: number;
}[]
| undefined = await workspaceDataSource?.query(
` `
SELECT SELECT *
subquery.*, FROM
message_count, (SELECT "messageThread".id,
last_message_subject, MAX(message."receivedAt") AS "lastMessageReceivedAt",
last_message_text, message.id AS "lastMessageId",
last_message_received_at, message.text AS "lastMessageBody",
last_message_participant_handle, ROW_NUMBER() OVER (PARTITION BY "messageThread".id ORDER BY MAX(message."receivedAt") DESC) AS "rowNumber"
last_message_participant_displayName FROM
FROM ( ${dataSourceMetadata.schema}."message" message
SELECT LEFT JOIN
mt.*, ${dataSourceMetadata.schema}."messageThread" "messageThread" ON "messageThread".id = message."messageThreadId"
COUNT(m."id") OVER (PARTITION BY mt."id") AS message_count, LEFT JOIN
FIRST_VALUE(m."subject") OVER (PARTITION BY mt."id" ORDER BY m."receivedAt" DESC) AS last_message_subject, ${dataSourceMetadata.schema}."messageParticipant" "messageParticipant" ON "messageParticipant"."messageId" = message.id
FIRST_VALUE(m."text") OVER (PARTITION BY mt."id" ORDER BY m."receivedAt" DESC) AS last_message_text, LEFT JOIN
FIRST_VALUE(m."receivedAt") OVER (PARTITION BY mt."id" ORDER BY m."receivedAt" DESC) AS last_message_received_at, ${dataSourceMetadata.schema}."person" person ON person.id = "messageParticipant"."personId"
FIRST_VALUE(mr."handle") OVER (PARTITION BY mt."id" ORDER BY m."receivedAt" DESC) AS last_message_participant_handle, LEFT JOIN
FIRST_VALUE(mr."displayName") OVER (PARTITION BY mt."id" ORDER BY m."receivedAt" DESC) AS last_message_participant_displayName, ${dataSourceMetadata.schema}."workspaceMember" "workspaceMember" ON "workspaceMember".id = "messageParticipant"."workspaceMemberId"
ROW_NUMBER() OVER (PARTITION BY mt."id" ORDER BY m."receivedAt" DESC) AS rn WHERE
FROM person.id = ANY($1)
${dataSourceMetadata.schema}."messageThread" mt GROUP BY
LEFT JOIN "messageThread".id,
${dataSourceMetadata.schema}."message" m ON mt."id" = m."messageThreadId" message.id
LEFT JOIN ORDER BY
${dataSourceMetadata.schema}."messageParticipant" mr ON m."id" = mr."messageId" message."receivedAt" DESC
WHERE ) AS "messageThreads"
mr."personId" IN (SELECT unnest($1::uuid[])) WHERE
) AS subquery "rowNumber" = 1
WHERE LIMIT $2
subquery.rn = 1 OFFSET $3
ORDER BY `,
subquery.last_message_received_at DESC [personIds, pageSize, offset],
LIMIT 10;
`,
[personIds],
); );
const formattedMessageThreads = messageThreads.map((messageThread) => { if (!messageThreads) {
return [];
}
const messageThreadIds = messageThreads.map(
(messageThread) => messageThread.id,
);
const threadSubjects:
| {
id: string;
subject: string;
}[]
| undefined = await workspaceDataSource?.query(
`
SELECT *
FROM
(SELECT
"messageThread".id,
message.subject,
ROW_NUMBER() OVER (PARTITION BY "messageThread".id ORDER BY MAX(message."receivedAt") ASC) AS "rowNumber"
FROM
${dataSourceMetadata.schema}."message" message
LEFT JOIN
${dataSourceMetadata.schema}."messageThread" "messageThread" ON "messageThread".id = message."messageThreadId"
WHERE
"messageThread".id = ANY($1)
GROUP BY
"messageThread".id,
message.id
ORDER BY
message."receivedAt" DESC
) AS "messageThreads"
WHERE
"rowNumber" = 1
`,
[messageThreadIds],
);
const numberOfMessagesInThread:
| {
id: string;
numberOfMessagesInThread: number;
}[]
| undefined = await workspaceDataSource?.query(
`
SELECT
"messageThread".id,
COUNT(message.id) AS "numberOfMessagesInThread"
FROM
${dataSourceMetadata.schema}."message" message
LEFT JOIN
${dataSourceMetadata.schema}."messageThread" "messageThread" ON "messageThread".id = message."messageThreadId"
WHERE
"messageThread".id = ANY($1)
GROUP BY
"messageThread".id
`,
[messageThreadIds],
);
const messageThreadsByMessageThreadId: {
[key: string]: {
id: string;
lastMessageReceivedAt: Date;
lastMessageBody: string;
};
} = messageThreads.reduce((messageThreadAcc, messageThread) => {
messageThreadAcc[messageThread.id] = messageThread;
return messageThreadAcc;
}, {});
const subjectsByMessageThreadId:
| {
[key: string]: {
id: string;
subject: string;
};
}
| undefined = threadSubjects?.reduce(
(threadSubjectAcc, threadSubject) => {
threadSubjectAcc[threadSubject.id] = threadSubject;
return threadSubjectAcc;
},
{},
);
const numberOfMessagesByMessageThreadId:
| {
[key: string]: {
id: string;
numberOfMessagesInThread: number;
};
}
| undefined = numberOfMessagesInThread?.reduce(
(numberOfMessagesAcc, numberOfMessages) => {
numberOfMessagesAcc[numberOfMessages.id] = numberOfMessages;
return numberOfMessagesAcc;
},
{},
);
const threadMessagesFromActiveParticipants:
| {
id: string;
messageId: string;
receivedAt: Date;
body: string;
subject: string;
personId: string;
workspaceMemberId: string;
handle: string;
personFirstName: string;
personLastName: string;
personAvatarUrl: string;
workspaceMemberFirstName: string;
workspaceMemberLastName: string;
workspaceMemberAvatarUrl: string;
messageDisplayName: string;
}[]
| undefined = await workspaceDataSource?.query(
`
SELECT DISTINCT "messageThread".id,
message.id AS "messageId",
message."receivedAt",
message.text,
message."subject",
"messageParticipant"."personId",
"messageParticipant"."workspaceMemberId",
"messageParticipant".handle,
"person"."nameFirstName" as "personFirstName",
"person"."nameLastName" as "personLastName",
"person"."avatarUrl" as "personAvatarUrl",
"workspaceMember"."nameFirstName" as "workspaceMemberFirstName",
"workspaceMember"."nameLastName" as "workspaceMemberLastName",
"workspaceMember"."avatarUrl" as "workspaceMemberAvatarUrl",
"messageParticipant"."displayName" as "messageDisplayName"
FROM
${dataSourceMetadata.schema}."message" message
LEFT JOIN
${dataSourceMetadata.schema}."messageThread" "messageThread" ON "messageThread".id = message."messageThreadId"
LEFT JOIN
(SELECT * FROM ${dataSourceMetadata.schema}."messageParticipant" WHERE "messageParticipant".role = 'from') "messageParticipant" ON "messageParticipant"."messageId" = message.id
LEFT JOIN
${dataSourceMetadata.schema}."person" person ON person."id" = "messageParticipant"."personId"
LEFT JOIN
${dataSourceMetadata.schema}."workspaceMember" "workspaceMember" ON "workspaceMember".id = "messageParticipant"."workspaceMemberId"
WHERE
"messageThread".id = ANY($1)
ORDER BY
message."receivedAt" DESC
`,
[messageThreadIds],
);
const threadParticipantsByThreadId: {
[key: string]: TimelineThreadParticipant[];
} = messageThreadIds.reduce((messageThreadIdAcc, messageThreadId) => {
const threadMessages = threadMessagesFromActiveParticipants?.filter(
(threadMessage) => threadMessage.id === messageThreadId,
);
const threadParticipants = threadMessages?.reduce(
(
threadMessageAcc,
threadMessage,
): {
[key: string]: TimelineThreadParticipant;
} => {
const threadParticipant = threadMessageAcc[threadMessage.handle];
const firstName =
threadMessage.personFirstName ||
threadMessage.workspaceMemberFirstName ||
'';
const lastName =
threadMessage.personLastName ||
threadMessage.workspaceMemberLastName ||
'';
const displayName =
firstName ||
threadMessage.messageDisplayName ||
threadMessage.handle;
if (!threadParticipant) {
threadMessageAcc[threadMessage.handle] = {
personId: threadMessage.personId,
workspaceMemberId: threadMessage.workspaceMemberId,
firstName,
lastName,
displayName,
avatarUrl:
threadMessage.personAvatarUrl ??
threadMessage.workspaceMemberAvatarUrl ??
'',
handle: threadMessage.handle,
};
}
return threadMessageAcc;
},
{},
);
messageThreadIdAcc[messageThreadId] = threadParticipants
? Object.values(threadParticipants)
: [];
return messageThreadIdAcc;
}, {});
const timelineThreads = messageThreadIds.map((messageThreadId) => {
const threadParticipants = threadParticipantsByThreadId[messageThreadId];
const firstParticipant = threadParticipants[0];
const threadParticipantsWithoutFirstParticipant =
threadParticipants.filter(
(threadParticipant) =>
threadParticipant.handle !== firstParticipant.handle,
);
const lastTwoParticipants: TimelineThreadParticipant[] = [];
const lastParticipant =
threadParticipantsWithoutFirstParticipant.slice(-1)[0];
if (lastParticipant) {
lastTwoParticipants.push(lastParticipant);
const threadParticipantsWithoutFirstAndLastParticipants =
threadParticipantsWithoutFirstParticipant.filter(
(threadParticipant) =>
threadParticipant.handle !== lastParticipant.handle,
);
if (threadParticipantsWithoutFirstAndLastParticipants.length > 0)
lastTwoParticipants.push(
threadParticipantsWithoutFirstAndLastParticipants.slice(-1)[0],
);
}
const thread = messageThreadsByMessageThreadId[messageThreadId];
const threadSubject =
subjectsByMessageThreadId?.[messageThreadId].subject ?? '';
const numberOfMessages =
numberOfMessagesByMessageThreadId?.[messageThreadId]
.numberOfMessagesInThread ?? 1;
return { return {
id: messageThreadId,
read: true, read: true,
senderName: messageThread.last_message_participant_handle, firstParticipant,
senderPictureUrl: '', lastTwoParticipants,
numberOfMessagesInThread: messageThread.message_count, lastMessageReceivedAt: thread.lastMessageReceivedAt,
subject: messageThread.last_message_subject, lastMessageBody: thread.lastMessageBody,
body: messageThread.last_message_text, subject: threadSubject,
receivedAt: messageThread.last_message_received_at, numberOfMessagesInThread: numberOfMessages,
participantCount: threadParticipants.length,
}; };
}); });
return formattedMessageThreads; return timelineThreads;
} }
async getMessagesFromCompanyId(workspaceId: string, companyId: string) { async getMessagesFromCompanyId(
workspaceId: string,
companyId: string,
page: number = 1,
pageSize: number = TIMELINE_THREADS_DEFAULT_PAGE_SIZE,
) {
const dataSourceMetadata = const dataSourceMetadata =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail( await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
workspaceId, workspaceId,
@ -102,11 +383,15 @@ export class TimelineMessagingService {
return []; return [];
} }
const formattedPersonIds = personIds.map((personId) => personId.id); const formattedPersonIds = personIds.map(
(personId: { id: string }) => personId.id,
);
const messageThreads = await this.getMessagesFromPersonIds( const messageThreads = await this.getMessagesFromPersonIds(
workspaceId, workspaceId,
formattedPersonIds, formattedPersonIds,
page,
pageSize,
); );
return messageThreads; return messageThreads;