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'];
};
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 = {
defaultValue?: InputMaybe<Scalars['JSON']['input']>;
description?: InputMaybe<Scalars['String']['input']>;

View File

@ -402,12 +402,16 @@ export type QueryFindWorkspaceFromInviteHashArgs = {
export type QueryGetTimelineThreadsFromCompanyIdArgs = {
companyId: Scalars['String'];
companyId: Scalars['ID'];
page: Scalars['Int'];
pageSize: Scalars['Int'];
};
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 = {
__typename?: 'TimelineThread';
body: Scalars['String'];
firstParticipant: TimelineThreadParticipant;
id: Scalars['ID'];
lastMessageBody: Scalars['String'];
lastMessageReceivedAt: Scalars['DateTime'];
lastTwoParticipants: Array<TimelineThreadParticipant>;
numberOfMessagesInThread: Scalars['Float'];
participantCount: Scalars['Float'];
read: Scalars['Boolean'];
receivedAt: Scalars['DateTime'];
senderName: Scalars['String'];
senderPictureUrl: 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 = {
__typename?: 'TransientToken';
transientToken: AuthToken;
@ -694,19 +711,27 @@ export type RelationEdge = {
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<{
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<{
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<{
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 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`
fragment AuthTokenFragment on AuthToken {
token
@ -911,18 +964,16 @@ export const UserQueryFragmentFragmentDoc = gql`
}
`;
export const GetTimelineThreadsFromCompanyIdDocument = gql`
query GetTimelineThreadsFromCompanyId($companyId: String!) {
getTimelineThreadsFromCompanyId(companyId: $companyId) {
body
numberOfMessagesInThread
read
receivedAt
senderName
senderPictureUrl
subject
query GetTimelineThreadsFromCompanyId($companyId: ID!, $page: Int!, $pageSize: Int!) {
getTimelineThreadsFromCompanyId(
companyId: $companyId
page: $page
pageSize: $pageSize
) {
...TimelineThreadFragment
}
}
`;
${TimelineThreadFragmentFragmentDoc}`;
/**
* __useGetTimelineThreadsFromCompanyIdQuery__
@ -937,6 +988,8 @@ export const GetTimelineThreadsFromCompanyIdDocument = gql`
* const { data, loading, error } = useGetTimelineThreadsFromCompanyIdQuery({
* variables: {
* 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 GetTimelineThreadsFromCompanyIdQueryResult = Apollo.QueryResult<GetTimelineThreadsFromCompanyIdQuery, GetTimelineThreadsFromCompanyIdQueryVariables>;
export const GetTimelineThreadsFromPersonIdDocument = gql`
query GetTimelineThreadsFromPersonId($personId: String!) {
getTimelineThreadsFromPersonId(personId: $personId) {
body
numberOfMessagesInThread
read
receivedAt
senderName
senderPictureUrl
subject
query GetTimelineThreadsFromPersonId($personId: ID!, $page: Int!, $pageSize: Int!) {
getTimelineThreadsFromPersonId(
personId: $personId
page: $page
pageSize: $pageSize
) {
...TimelineThreadFragment
}
}
`;
${TimelineThreadFragmentFragmentDoc}`;
/**
* __useGetTimelineThreadsFromPersonIdQuery__
@ -978,6 +1029,8 @@ export const GetTimelineThreadsFromPersonIdDocument = gql`
* const { data, loading, error } = useGetTimelineThreadsFromPersonIdQuery({
* variables: {
* 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}>
<StyledHeading unread={!thread.read}>
<StyledAvatar
avatarUrl={thread.senderPictureUrl}
placeholder={thread.senderName}
avatarUrl={thread.firstParticipant.avatarUrl}
placeholder={thread.firstParticipant.displayName}
type="rounded"
/>
<StyledSenderName>{thread.senderName}</StyledSenderName>
<StyledSenderName>
{thread.firstParticipant.displayName}
</StyledSenderName>
<StyledThreadCount>{thread.numberOfMessagesInThread}</StyledThreadCount>
</StyledHeading>
<StyledSubjectAndBody>
<StyledSubject unread={!thread.read}>{thread.subject}</StyledSubject>
<StyledBody>{thread.body}</StyledBody>
<StyledBody>{thread.lastMessageBody}</StyledBody>
</StyledSubjectAndBody>
<StyledReceivedAt>
{formatToHumanReadableDate(thread.receivedAt)}
{formatToHumanReadableDate(thread.lastMessageReceivedAt)}
</StyledReceivedAt>
</StyledCardContent>
);

View File

@ -2,11 +2,8 @@ import { useQuery } from '@apollo/client';
import styled from '@emotion/styled';
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 {
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';
@ -17,6 +14,7 @@ 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;
@ -46,10 +44,13 @@ export const EmailThreads = ({
? getTimelineThreadsFromPersonId
: getTimelineThreadsFromCompanyId;
const threadQueryVariables =
entity.targetObjectNameSingular === CoreObjectNameSingular.Person
const threadQueryVariables = {
...(entity.targetObjectNameSingular === CoreObjectNameSingular.Person
? { personId: entity.id }
: { companyId: entity.id };
: { companyId: entity.id }),
page: 1,
pageSize: TIMELINE_THREADS_DEFAULT_PAGE_SIZE,
};
const threads = useQuery(threadQuery, {
variables: threadQueryVariables,
@ -59,16 +60,12 @@ export const EmailThreads = ({
return;
}
// To use once the id is returned by the query
// const fetchedTimelineThreads: TimelineThread[] =
// threads.data[
// entity.targetObjectNameSingular === CoreObjectNameSingular.Person
// ? 'getTimelineThreadsFromPersonId'
// : 'getTimelineThreadsFromCompanyId'
// ];
const timelineThreads = mockedEmailThreads;
const timelineThreads: TimelineThread[] =
threads.data[
entity.targetObjectNameSingular === CoreObjectNameSingular.Person
? 'getTimelineThreadsFromPersonId'
: 'getTimelineThreadsFromCompanyId'
];
return (
<StyledContainer>
@ -77,16 +74,14 @@ export const EmailThreads = ({
title={
<>
Inbox{' '}
<StyledEmailCount>
{timelineThreads && timelineThreads.length}
</StyledEmailCount>
<StyledEmailCount>{timelineThreads?.length}</StyledEmailCount>
</>
}
fontColor={H1TitleFontColor.Primary}
/>
<Card>
{timelineThreads &&
timelineThreads.map((thread: MockedThread, index: number) => (
timelineThreads.map((thread: TimelineThread, index: number) => (
<EmailThreadPreview
key={index}
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 { MockedThread } from '@/activities/emails/mocks/mockedEmailThreads';
import { useOpenEmailThreadRightDrawer } from '@/activities/emails/right-drawer/hooks/useOpenEmailThreadRightDrawer';
import { viewableEmailThreadState } from '@/activities/emails/state/viewableEmailThreadState';
import { TimelineThread } from '~/generated/graphql';
export const useEmailThread = () => {
const [, setViewableEmailThread] = useRecoilState(viewableEmailThreadState);
const openEmailThredRightDrawer = useOpenEmailThreadRightDrawer();
const openEmailThread = (thread: MockedThread) => {
const openEmailThread = (thread: TimelineThread) => {
openEmailThredRightDrawer();
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 { timelineThreadFragment } from '@/activities/emails/queries/fragments/timelineThreadFragment';
export const getTimelineThreadsFromCompanyId = gql`
query GetTimelineThreadsFromCompanyId($companyId: String!) {
getTimelineThreadsFromCompanyId(companyId: $companyId) {
body
numberOfMessagesInThread
read
receivedAt
senderName
senderPictureUrl
subject
query GetTimelineThreadsFromCompanyId(
$companyId: ID!
$page: Int!
$pageSize: Int!
) {
getTimelineThreadsFromCompanyId(
companyId: $companyId
page: $page
pageSize: $pageSize
) {
...TimelineThreadFragment
}
}
${timelineThreadFragment}
`;

View File

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

View File

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

View File

@ -1,8 +1,8 @@
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',
default: null,
});

View File

@ -4,7 +4,6 @@ import { Comment } from '@/activities/types/Comment';
import { Company } from '@/companies/types/Company';
import { Person } from '@/people/types/Person';
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
import { TimelineThread } from '~/generated/graphql';
type MockedActivity = Pick<
Activity,
@ -211,24 +210,3 @@ export const mockedActivities: Array<MockedActivity> = [
__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 { Column, Entity } from 'typeorm';
import { Max } from 'class-validator';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import { Workspace } from 'src/core/workspace/workspace.entity';
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
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' })
@ObjectType('TimelineThread')
export class TimelineThread {
@Field()
@Column()
read: boolean;
@ArgsType()
class GetTimelineThreadsFromPersonIdArgs {
@Field(() => ID)
personId: string;
@Field()
@Column()
senderName: string;
@Field(() => Int)
page: number;
@Field()
@Column()
senderPictureUrl: string;
@Field(() => Int)
@Max(TIMELINE_THREADS_MAX_PAGE_SIZE)
pageSize: number;
}
@Field()
@Column()
numberOfMessagesInThread: number;
@ArgsType()
class GetTimelineThreadsFromCompanyIdArgs {
@Field(() => ID)
companyId: string;
@Field()
@Column()
subject: string;
@Field(() => Int)
page: number;
@Field()
@Column()
body: string;
@Field()
@Column()
receivedAt: Date;
@Field(() => Int)
@Max(TIMELINE_THREADS_MAX_PAGE_SIZE)
pageSize: number;
}
@UseGuards(JwtAuthGuard)
@ -50,12 +54,14 @@ export class TimelineMessagingResolver {
@Query(() => [TimelineThread])
async getTimelineThreadsFromPersonId(
@AuthWorkspace() { id: workspaceId }: Workspace,
@Args('personId') personId: string,
@Args() { personId, page, pageSize }: GetTimelineThreadsFromPersonIdArgs,
) {
const timelineThreads =
await this.timelineMessagingService.getMessagesFromPersonIds(
workspaceId,
[personId],
page,
pageSize,
);
return timelineThreads;
@ -64,12 +70,14 @@ export class TimelineMessagingResolver {
@Query(() => [TimelineThread])
async getTimelineThreadsFromCompanyId(
@AuthWorkspace() { id: workspaceId }: Workspace,
@Args('companyId') companyId: string,
@Args() { companyId, page, pageSize }: GetTimelineThreadsFromCompanyIdArgs,
) {
const timelineThreads =
await this.timelineMessagingService.getMessagesFromCompanyId(
workspaceId,
companyId,
page,
pageSize,
);
return timelineThreads;

View File

@ -1,9 +1,20 @@
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 { 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()
export class TimelineMessagingService {
constructor(
@ -14,7 +25,11 @@ export class TimelineMessagingService {
async getMessagesFromPersonIds(
workspaceId: string,
personIds: string[],
page: number = 1,
pageSize: number = TIMELINE_THREADS_DEFAULT_PAGE_SIZE,
): Promise<TimelineThread[]> {
const offset = (page - 1) * TIMELINE_THREADS_DEFAULT_PAGE_SIZE;
const dataSourceMetadata =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
workspaceId,
@ -23,61 +38,327 @@ export class TimelineMessagingService {
const workspaceDataSource =
await this.typeORMService.connectToDataSource(dataSourceMetadata);
// 10 first threads This hard limit is just for the POC, we will implement pagination later
const messageThreads = await workspaceDataSource?.query(
const messageThreads:
| {
id: string;
lastMessageReceivedAt: Date;
lastMessageId: string;
lastMessageBody: string;
rowNumber: number;
}[]
| undefined = await workspaceDataSource?.query(
`
SELECT
subquery.*,
message_count,
last_message_subject,
last_message_text,
last_message_received_at,
last_message_participant_handle,
last_message_participant_displayName
FROM (
SELECT
mt.*,
COUNT(m."id") OVER (PARTITION BY mt."id") AS message_count,
FIRST_VALUE(m."subject") OVER (PARTITION BY mt."id" ORDER BY m."receivedAt" DESC) AS last_message_subject,
FIRST_VALUE(m."text") OVER (PARTITION BY mt."id" ORDER BY m."receivedAt" DESC) AS last_message_text,
FIRST_VALUE(m."receivedAt") OVER (PARTITION BY mt."id" ORDER BY m."receivedAt" DESC) AS last_message_received_at,
FIRST_VALUE(mr."handle") OVER (PARTITION BY mt."id" ORDER BY m."receivedAt" DESC) AS last_message_participant_handle,
FIRST_VALUE(mr."displayName") OVER (PARTITION BY mt."id" ORDER BY m."receivedAt" DESC) AS last_message_participant_displayName,
ROW_NUMBER() OVER (PARTITION BY mt."id" ORDER BY m."receivedAt" DESC) AS rn
FROM
${dataSourceMetadata.schema}."messageThread" mt
LEFT JOIN
${dataSourceMetadata.schema}."message" m ON mt."id" = m."messageThreadId"
LEFT JOIN
${dataSourceMetadata.schema}."messageParticipant" mr ON m."id" = mr."messageId"
WHERE
mr."personId" IN (SELECT unnest($1::uuid[]))
) AS subquery
WHERE
subquery.rn = 1
ORDER BY
subquery.last_message_received_at DESC
LIMIT 10;
`,
[personIds],
SELECT *
FROM
(SELECT "messageThread".id,
MAX(message."receivedAt") AS "lastMessageReceivedAt",
message.id AS "lastMessageId",
message.text AS "lastMessageBody",
ROW_NUMBER() OVER (PARTITION BY "messageThread".id ORDER BY MAX(message."receivedAt") DESC) AS "rowNumber"
FROM
${dataSourceMetadata.schema}."message" message
LEFT JOIN
${dataSourceMetadata.schema}."messageThread" "messageThread" ON "messageThread".id = message."messageThreadId"
LEFT JOIN
${dataSourceMetadata.schema}."messageParticipant" "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
person.id = ANY($1)
GROUP BY
"messageThread".id,
message.id
ORDER BY
message."receivedAt" DESC
) AS "messageThreads"
WHERE
"rowNumber" = 1
LIMIT $2
OFFSET $3
`,
[personIds, pageSize, offset],
);
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 {
id: messageThreadId,
read: true,
senderName: messageThread.last_message_participant_handle,
senderPictureUrl: '',
numberOfMessagesInThread: messageThread.message_count,
subject: messageThread.last_message_subject,
body: messageThread.last_message_text,
receivedAt: messageThread.last_message_received_at,
firstParticipant,
lastTwoParticipants,
lastMessageReceivedAt: thread.lastMessageReceivedAt,
lastMessageBody: thread.lastMessageBody,
subject: threadSubject,
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 =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
workspaceId,
@ -102,11 +383,15 @@ export class TimelineMessagingService {
return [];
}
const formattedPersonIds = personIds.map((personId) => personId.id);
const formattedPersonIds = personIds.map(
(personId: { id: string }) => personId.id,
);
const messageThreads = await this.getMessagesFromPersonIds(
workspaceId,
formattedPersonIds,
page,
pageSize,
);
return messageThreads;