Use twentyORM in Timeline messaging (#6595)
- Remove raw queries and replace them by using `twentyORM` - Refactor into services and utils --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -9,7 +9,7 @@ export const useIsMatchingLocation = () => {
|
|||||||
return useCallback(
|
return useCallback(
|
||||||
(path: string, basePath?: AppBasePath) => {
|
(path: string, basePath?: AppBasePath) => {
|
||||||
const constructedPath = basePath
|
const constructedPath = basePath
|
||||||
? (new URL(basePath + path, document.location.origin).pathname ?? '')
|
? new URL(basePath + path, document.location.origin).pathname ?? ''
|
||||||
: path;
|
: path;
|
||||||
|
|
||||||
return !!matchPath(constructedPath, location.pathname);
|
return !!matchPath(constructedPath, location.pathname);
|
||||||
|
|||||||
@ -45,7 +45,7 @@ export const MessageThreadSubscribersChip = ({
|
|||||||
? `+${numberOfMessageThreadSubscribers - MAX_NUMBER_OF_AVATARS}`
|
? `+${numberOfMessageThreadSubscribers - MAX_NUMBER_OF_AVATARS}`
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const label = isPrivateThread ? privateLabel : (moreAvatarsLabel ?? '');
|
const label = isPrivateThread ? privateLabel : moreAvatarsLabel ?? '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Chip
|
<Chip
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
|||||||
export const generateDefaultRecordChipData = (record: ObjectRecord) => {
|
export const generateDefaultRecordChipData = (record: ObjectRecord) => {
|
||||||
const name = isFieldFullNameValue(record.name)
|
const name = isFieldFullNameValue(record.name)
|
||||||
? record.name.firstName + ' ' + record.name.lastName
|
? record.name.firstName + ' ' + record.name.lastName
|
||||||
: (record.name ?? '');
|
: record.name ?? '';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
|
|||||||
@ -42,8 +42,8 @@ export const MultiSelectFieldInput = ({
|
|||||||
const [searchFilter, setSearchFilter] = useState('');
|
const [searchFilter, setSearchFilter] = useState('');
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const selectedOptions = fieldDefinition.metadata.options.filter((option) =>
|
const selectedOptions = fieldDefinition.metadata.options.filter(
|
||||||
fieldValues?.includes(option.value),
|
(option) => fieldValues?.includes(option.value),
|
||||||
);
|
);
|
||||||
|
|
||||||
const optionsInDropDown = fieldDefinition.metadata.options;
|
const optionsInDropDown = fieldDefinition.metadata.options;
|
||||||
|
|||||||
@ -69,7 +69,7 @@ export const RecordDetailRelationSection = ({
|
|||||||
const relationRecords: ObjectRecord[] =
|
const relationRecords: ObjectRecord[] =
|
||||||
fieldValue && isToOneObject
|
fieldValue && isToOneObject
|
||||||
? [fieldValue as ObjectRecord]
|
? [fieldValue as ObjectRecord]
|
||||||
: ((fieldValue as ObjectRecord[]) ?? []);
|
: (fieldValue as ObjectRecord[]) ?? [];
|
||||||
|
|
||||||
const relationRecordIds = relationRecords.map(({ id }) => id);
|
const relationRecordIds = relationRecords.map(({ id }) => id);
|
||||||
|
|
||||||
|
|||||||
@ -6,10 +6,11 @@ export const findUnmatchedRequiredFields = <T extends string>(
|
|||||||
columns: Columns<T>,
|
columns: Columns<T>,
|
||||||
) =>
|
) =>
|
||||||
fields
|
fields
|
||||||
.filter((field) =>
|
.filter(
|
||||||
field.fieldValidationDefinitions?.some(
|
(field) =>
|
||||||
(validation) => validation.rule === 'required',
|
field.fieldValidationDefinitions?.some(
|
||||||
),
|
(validation) => validation.rule === 'required',
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.filter(
|
.filter(
|
||||||
(field) =>
|
(field) =>
|
||||||
|
|||||||
@ -16,7 +16,7 @@ type ContainerProps = {
|
|||||||
const StyledContainer = styled.div<ContainerProps>`
|
const StyledContainer = styled.div<ContainerProps>`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: ${({ theme, isOn, color }) =>
|
background-color: ${({ theme, isOn, color }) =>
|
||||||
isOn ? (color ?? theme.color.blue) : theme.background.quaternary};
|
isOn ? color ?? theme.color.blue : theme.background.quaternary};
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
import { ObjectType, Field } from '@nestjs/graphql';
|
import { Field, ObjectType } from '@nestjs/graphql';
|
||||||
|
|
||||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||||
|
|
||||||
@ObjectType('TimelineThreadParticipant')
|
@ObjectType('TimelineThreadParticipant')
|
||||||
export class TimelineThreadParticipant {
|
export class TimelineThreadParticipant {
|
||||||
@Field(() => UUIDScalarType, { nullable: true })
|
@Field(() => UUIDScalarType, { nullable: true })
|
||||||
personId: string;
|
personId: string | null;
|
||||||
|
|
||||||
@Field(() => UUIDScalarType, { nullable: true })
|
@Field(() => UUIDScalarType, { nullable: true })
|
||||||
workspaceMemberId: string;
|
workspaceMemberId: string | null;
|
||||||
|
|
||||||
@Field()
|
@Field()
|
||||||
firstName: string;
|
firstName: string;
|
||||||
|
|||||||
@ -0,0 +1,101 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { TIMELINE_THREADS_DEFAULT_PAGE_SIZE } from 'src/engine/core-modules/messaging/constants/messaging.constants';
|
||||||
|
import { TimelineThreadsWithTotal } from 'src/engine/core-modules/messaging/dtos/timeline-threads-with-total.dto';
|
||||||
|
import { TimelineMessagingService } from 'src/engine/core-modules/messaging/services/timeline-messaging.service';
|
||||||
|
import { formatThreads } from 'src/engine/core-modules/messaging/utils/format-threads.util';
|
||||||
|
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||||
|
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GetMessagesService {
|
||||||
|
constructor(
|
||||||
|
private readonly twentyORMManager: TwentyORMManager,
|
||||||
|
private readonly timelineMessagingService: TimelineMessagingService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getMessagesFromPersonIds(
|
||||||
|
workspaceMemberId: string,
|
||||||
|
personIds: string[],
|
||||||
|
page = 1,
|
||||||
|
pageSize: number = TIMELINE_THREADS_DEFAULT_PAGE_SIZE,
|
||||||
|
): Promise<TimelineThreadsWithTotal> {
|
||||||
|
const offset = (page - 1) * pageSize;
|
||||||
|
|
||||||
|
const { messageThreads, totalNumberOfThreads } =
|
||||||
|
await this.timelineMessagingService.getAndCountMessageThreads(
|
||||||
|
personIds,
|
||||||
|
offset,
|
||||||
|
pageSize,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!messageThreads) {
|
||||||
|
return {
|
||||||
|
totalNumberOfThreads: 0,
|
||||||
|
timelineThreads: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageThreadIds = messageThreads.map(
|
||||||
|
(messageThread) => messageThread.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
const threadParticipantsByThreadId =
|
||||||
|
await this.timelineMessagingService.getThreadParticipantsByThreadId(
|
||||||
|
messageThreadIds,
|
||||||
|
);
|
||||||
|
|
||||||
|
const threadVisibilityByThreadId =
|
||||||
|
await this.timelineMessagingService.getThreadVisibilityByThreadId(
|
||||||
|
messageThreadIds,
|
||||||
|
workspaceMemberId,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalNumberOfThreads,
|
||||||
|
timelineThreads: formatThreads(
|
||||||
|
messageThreads,
|
||||||
|
threadParticipantsByThreadId,
|
||||||
|
threadVisibilityByThreadId,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMessagesFromCompanyId(
|
||||||
|
workspaceMemberId: string,
|
||||||
|
companyId: string,
|
||||||
|
page = 1,
|
||||||
|
pageSize: number = TIMELINE_THREADS_DEFAULT_PAGE_SIZE,
|
||||||
|
): Promise<TimelineThreadsWithTotal> {
|
||||||
|
const personRepository =
|
||||||
|
await this.twentyORMManager.getRepository<PersonWorkspaceEntity>(
|
||||||
|
'person',
|
||||||
|
);
|
||||||
|
const personIds = (
|
||||||
|
await personRepository.find({
|
||||||
|
where: {
|
||||||
|
companyId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).map((person) => person.id);
|
||||||
|
|
||||||
|
if (personIds.length === 0) {
|
||||||
|
return {
|
||||||
|
totalNumberOfThreads: 0,
|
||||||
|
timelineThreads: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageThreads = await this.getMessagesFromPersonIds(
|
||||||
|
workspaceMemberId,
|
||||||
|
personIds,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
);
|
||||||
|
|
||||||
|
return messageThreads;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,259 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { Any, Not } from 'typeorm';
|
||||||
|
|
||||||
|
import { TimelineThread } from 'src/engine/core-modules/messaging/dtos/timeline-thread.dto';
|
||||||
|
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||||
|
import { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||||
|
import { MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity';
|
||||||
|
import { MessageThreadWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-thread.workspace-entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TimelineMessagingService {
|
||||||
|
constructor(private readonly twentyORMManager: TwentyORMManager) {}
|
||||||
|
|
||||||
|
public async getAndCountMessageThreads(
|
||||||
|
personIds: string[],
|
||||||
|
offset: number,
|
||||||
|
pageSize: number,
|
||||||
|
): Promise<{
|
||||||
|
messageThreads: Omit<
|
||||||
|
TimelineThread,
|
||||||
|
| 'firstParticipant'
|
||||||
|
| 'lastTwoParticipants'
|
||||||
|
| 'participantCount'
|
||||||
|
| 'read'
|
||||||
|
| 'visibility'
|
||||||
|
>[];
|
||||||
|
totalNumberOfThreads: number;
|
||||||
|
}> {
|
||||||
|
const messageThreadRepository =
|
||||||
|
await this.twentyORMManager.getRepository<MessageThreadWorkspaceEntity>(
|
||||||
|
'messageThread',
|
||||||
|
);
|
||||||
|
|
||||||
|
const [messageThreadIds, totalNumberOfThreads] =
|
||||||
|
await messageThreadRepository.findAndCount({
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
messages: {
|
||||||
|
messageParticipants: {
|
||||||
|
personId: Any(personIds),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
skip: offset,
|
||||||
|
take: pageSize,
|
||||||
|
});
|
||||||
|
|
||||||
|
const messageThreads = await messageThreadRepository.find({
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
id: Any(messageThreadIds.map((thread) => thread.id)),
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
messages: {
|
||||||
|
receivedAt: 'DESC',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
relations: ['messages'],
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
messageThreads: messageThreads.map((messageThread) => {
|
||||||
|
const lastMessage = messageThread.messages[0];
|
||||||
|
const firstMessage =
|
||||||
|
messageThread.messages[messageThread.messages.length - 1];
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: messageThread.id,
|
||||||
|
subject: firstMessage.subject,
|
||||||
|
lastMessageBody: lastMessage.text,
|
||||||
|
lastMessageReceivedAt: lastMessage.receivedAt ?? new Date(),
|
||||||
|
numberOfMessagesInThread: messageThread.messages.length,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
totalNumberOfThreads,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getThreadParticipantsByThreadId(
|
||||||
|
messageThreadIds: string[],
|
||||||
|
): Promise<{
|
||||||
|
[key: string]: MessageParticipantWorkspaceEntity[];
|
||||||
|
}> {
|
||||||
|
const messageParticipantRepository =
|
||||||
|
await this.twentyORMManager.getRepository<MessageParticipantWorkspaceEntity>(
|
||||||
|
'messageParticipant',
|
||||||
|
);
|
||||||
|
const threadParticipants = await messageParticipantRepository
|
||||||
|
.createQueryBuilder()
|
||||||
|
.select('messageParticipant')
|
||||||
|
.addSelect('message.messageThreadId')
|
||||||
|
.addSelect('message.receivedAt')
|
||||||
|
.leftJoinAndSelect('messageParticipant.person', 'person')
|
||||||
|
.leftJoinAndSelect(
|
||||||
|
'messageParticipant.workspaceMember',
|
||||||
|
'workspaceMember',
|
||||||
|
)
|
||||||
|
.leftJoin('messageParticipant.message', 'message')
|
||||||
|
.where('message.messageThreadId = ANY(:messageThreadIds)', {
|
||||||
|
messageThreadIds,
|
||||||
|
})
|
||||||
|
.andWhere('messageParticipant.role = :role', { role: 'from' })
|
||||||
|
.orderBy('message.messageThreadId')
|
||||||
|
.distinctOn(['message.messageThreadId', 'messageParticipant.handle'])
|
||||||
|
.getMany();
|
||||||
|
|
||||||
|
// This is because subqueries are not handled by twentyORM
|
||||||
|
const orderedThreadParticipants = threadParticipants.sort(
|
||||||
|
(a, b) =>
|
||||||
|
(a.message.receivedAt ?? new Date()).getTime() -
|
||||||
|
(b.message.receivedAt ?? new Date()).getTime(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// This is because composite fields are not handled correctly by the ORM
|
||||||
|
const threadParticipantsWithCompositeFields = orderedThreadParticipants.map(
|
||||||
|
(threadParticipant) => ({
|
||||||
|
...threadParticipant,
|
||||||
|
person: {
|
||||||
|
id: threadParticipant.person?.id,
|
||||||
|
name: {
|
||||||
|
//eslint-disable-next-line
|
||||||
|
//@ts-ignore
|
||||||
|
firstName: threadParticipant.person?.nameFirstName,
|
||||||
|
//eslint-disable-next-line
|
||||||
|
//@ts-ignore
|
||||||
|
lastName: threadParticipant.person?.nameLastName,
|
||||||
|
},
|
||||||
|
avatarUrl: threadParticipant.person?.avatarUrl,
|
||||||
|
},
|
||||||
|
workspaceMember: {
|
||||||
|
id: threadParticipant.workspaceMember?.id,
|
||||||
|
name: {
|
||||||
|
//eslint-disable-next-line
|
||||||
|
//@ts-ignore
|
||||||
|
firstName: threadParticipant.workspaceMember?.nameFirstName,
|
||||||
|
//eslint-disable-next-line
|
||||||
|
//@ts-ignore
|
||||||
|
lastName: threadParticipant.workspaceMember?.nameLastName,
|
||||||
|
},
|
||||||
|
avatarUrl: threadParticipant.workspaceMember?.avatarUrl,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return threadParticipantsWithCompositeFields.reduce(
|
||||||
|
(threadParticipantsAcc, threadParticipant) => {
|
||||||
|
if (!threadParticipant.message.messageThreadId)
|
||||||
|
return threadParticipantsAcc;
|
||||||
|
|
||||||
|
if (!threadParticipantsAcc[threadParticipant.message.messageThreadId])
|
||||||
|
threadParticipantsAcc[threadParticipant.message.messageThreadId] = [];
|
||||||
|
|
||||||
|
threadParticipantsAcc[threadParticipant.message.messageThreadId].push(
|
||||||
|
threadParticipant,
|
||||||
|
);
|
||||||
|
|
||||||
|
return threadParticipantsAcc;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getThreadVisibilityByThreadId(
|
||||||
|
messageThreadIds: string[],
|
||||||
|
workspaceMemberId: string,
|
||||||
|
): Promise<{
|
||||||
|
[key: string]: MessageChannelVisibility;
|
||||||
|
}> {
|
||||||
|
const messageThreadRepository =
|
||||||
|
await this.twentyORMManager.getRepository<MessageThreadWorkspaceEntity>(
|
||||||
|
'messageThread',
|
||||||
|
);
|
||||||
|
|
||||||
|
const threadsWithoutWorkspaceMember = await messageThreadRepository.find({
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
id: Any(messageThreadIds),
|
||||||
|
messages: {
|
||||||
|
messageChannelMessageAssociations: {
|
||||||
|
messageChannel: {
|
||||||
|
connectedAccount: {
|
||||||
|
accountOwnerId: Not(workspaceMemberId),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const threadIdsWithoutWorkspaceMember = threadsWithoutWorkspaceMember.map(
|
||||||
|
(thread) => thread.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
const threadVisibility = await messageThreadRepository
|
||||||
|
.createQueryBuilder()
|
||||||
|
.select('messageThread.id', 'id')
|
||||||
|
.addSelect('messageChannel.visibility', 'visibility')
|
||||||
|
.leftJoin('messageThread.messages', 'message')
|
||||||
|
.leftJoin(
|
||||||
|
'message.messageChannelMessageAssociations',
|
||||||
|
'messageChannelMessageAssociation',
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
'messageChannelMessageAssociation.messageChannel',
|
||||||
|
'messageChannel',
|
||||||
|
)
|
||||||
|
.where('messageThread.id = ANY(:messageThreadIds)', {
|
||||||
|
messageThreadIds: threadIdsWithoutWorkspaceMember,
|
||||||
|
})
|
||||||
|
.getRawMany();
|
||||||
|
|
||||||
|
const visibilityValues = Object.values(MessageChannelVisibility);
|
||||||
|
|
||||||
|
const threadVisibilityByThreadIdForWhichWorkspaceMemberIsNotInParticipants:
|
||||||
|
| {
|
||||||
|
[key: string]: MessageChannelVisibility;
|
||||||
|
}
|
||||||
|
| undefined = threadVisibility?.reduce(
|
||||||
|
(threadVisibilityAcc, threadVisibility) => {
|
||||||
|
threadVisibilityAcc[threadVisibility.id] =
|
||||||
|
visibilityValues[
|
||||||
|
Math.max(
|
||||||
|
visibilityValues.indexOf(threadVisibility.visibility),
|
||||||
|
visibilityValues.indexOf(
|
||||||
|
threadVisibilityAcc[threadVisibility.id] ??
|
||||||
|
MessageChannelVisibility.METADATA,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
return threadVisibilityAcc;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
const threadVisibilityByThreadId: {
|
||||||
|
[key: string]: MessageChannelVisibility;
|
||||||
|
} = messageThreadIds.reduce((threadVisibilityAcc, messageThreadId) => {
|
||||||
|
// If the workspace member is not in the participants of the thread, use the visibility value from the query
|
||||||
|
threadVisibilityAcc[messageThreadId] =
|
||||||
|
threadIdsWithoutWorkspaceMember.includes(messageThreadId)
|
||||||
|
? (threadVisibilityByThreadIdForWhichWorkspaceMemberIsNotInParticipants?.[
|
||||||
|
messageThreadId
|
||||||
|
] ?? MessageChannelVisibility.METADATA)
|
||||||
|
: MessageChannelVisibility.SHARE_EVERYTHING;
|
||||||
|
|
||||||
|
return threadVisibilityAcc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return threadVisibilityByThreadId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,12 +1,17 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { GetMessagesService } from 'src/engine/core-modules/messaging/services/get-messages.service';
|
||||||
|
import { TimelineMessagingService } from 'src/engine/core-modules/messaging/services/timeline-messaging.service';
|
||||||
import { TimelineMessagingResolver } from 'src/engine/core-modules/messaging/timeline-messaging.resolver';
|
import { TimelineMessagingResolver } from 'src/engine/core-modules/messaging/timeline-messaging.resolver';
|
||||||
import { TimelineMessagingService } from 'src/engine/core-modules/messaging/timeline-messaging.service';
|
|
||||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
|
||||||
import { UserModule } from 'src/engine/core-modules/user/user.module';
|
import { UserModule } from 'src/engine/core-modules/user/user.module';
|
||||||
|
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||||
@Module({
|
@Module({
|
||||||
imports: [WorkspaceDataSourceModule, UserModule],
|
imports: [WorkspaceDataSourceModule, UserModule],
|
||||||
exports: [],
|
exports: [],
|
||||||
providers: [TimelineMessagingResolver, TimelineMessagingService],
|
providers: [
|
||||||
|
TimelineMessagingResolver,
|
||||||
|
TimelineMessagingService,
|
||||||
|
GetMessagesService,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class TimelineMessagingModule {}
|
export class TimelineMessagingModule {}
|
||||||
|
|||||||
@ -1,18 +1,16 @@
|
|||||||
import { Args, Query, Resolver, Int, ArgsType, Field } from '@nestjs/graphql';
|
|
||||||
import { UseGuards } from '@nestjs/common';
|
import { UseGuards } from '@nestjs/common';
|
||||||
|
import { Args, ArgsType, Field, Int, Query, Resolver } from '@nestjs/graphql';
|
||||||
|
|
||||||
import { Max } from 'class-validator';
|
import { Max } from 'class-validator';
|
||||||
|
|
||||||
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
|
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
|
||||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
|
||||||
import { TimelineMessagingService } from 'src/engine/core-modules/messaging/timeline-messaging.service';
|
|
||||||
import { TIMELINE_THREADS_MAX_PAGE_SIZE } from 'src/engine/core-modules/messaging/constants/messaging.constants';
|
import { TIMELINE_THREADS_MAX_PAGE_SIZE } from 'src/engine/core-modules/messaging/constants/messaging.constants';
|
||||||
import { TimelineThreadsWithTotal } from 'src/engine/core-modules/messaging/dtos/timeline-threads-with-total.dto';
|
import { TimelineThreadsWithTotal } from 'src/engine/core-modules/messaging/dtos/timeline-threads-with-total.dto';
|
||||||
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
|
import { GetMessagesService } from 'src/engine/core-modules/messaging/services/get-messages.service';
|
||||||
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
|
||||||
|
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
|
||||||
|
|
||||||
@ArgsType()
|
@ArgsType()
|
||||||
class GetTimelineThreadsFromPersonIdArgs {
|
class GetTimelineThreadsFromPersonIdArgs {
|
||||||
@ -44,13 +42,12 @@ class GetTimelineThreadsFromCompanyIdArgs {
|
|||||||
@Resolver(() => TimelineThreadsWithTotal)
|
@Resolver(() => TimelineThreadsWithTotal)
|
||||||
export class TimelineMessagingResolver {
|
export class TimelineMessagingResolver {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly timelineMessagingService: TimelineMessagingService,
|
private readonly getMessagesFromPersonIdsService: GetMessagesService,
|
||||||
private readonly userService: UserService,
|
private readonly userService: UserService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Query(() => TimelineThreadsWithTotal)
|
@Query(() => TimelineThreadsWithTotal)
|
||||||
async getTimelineThreadsFromPersonId(
|
async getTimelineThreadsFromPersonId(
|
||||||
@AuthWorkspace() { id: workspaceId }: Workspace,
|
|
||||||
@AuthUser() user: User,
|
@AuthUser() user: User,
|
||||||
@Args() { personId, page, pageSize }: GetTimelineThreadsFromPersonIdArgs,
|
@Args() { personId, page, pageSize }: GetTimelineThreadsFromPersonIdArgs,
|
||||||
) {
|
) {
|
||||||
@ -61,9 +58,8 @@ export class TimelineMessagingResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const timelineThreads =
|
const timelineThreads =
|
||||||
await this.timelineMessagingService.getMessagesFromPersonIds(
|
await this.getMessagesFromPersonIdsService.getMessagesFromPersonIds(
|
||||||
workspaceMember.id,
|
workspaceMember.id,
|
||||||
workspaceId,
|
|
||||||
[personId],
|
[personId],
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
@ -74,7 +70,6 @@ export class TimelineMessagingResolver {
|
|||||||
|
|
||||||
@Query(() => TimelineThreadsWithTotal)
|
@Query(() => TimelineThreadsWithTotal)
|
||||||
async getTimelineThreadsFromCompanyId(
|
async getTimelineThreadsFromCompanyId(
|
||||||
@AuthWorkspace() { id: workspaceId }: Workspace,
|
|
||||||
@AuthUser() user: User,
|
@AuthUser() user: User,
|
||||||
@Args() { companyId, page, pageSize }: GetTimelineThreadsFromCompanyIdArgs,
|
@Args() { companyId, page, pageSize }: GetTimelineThreadsFromCompanyIdArgs,
|
||||||
) {
|
) {
|
||||||
@ -85,9 +80,8 @@ export class TimelineMessagingResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const timelineThreads =
|
const timelineThreads =
|
||||||
await this.timelineMessagingService.getMessagesFromCompanyId(
|
await this.getMessagesFromPersonIdsService.getMessagesFromCompanyId(
|
||||||
workspaceMember.id,
|
workspaceMember.id,
|
||||||
workspaceId,
|
|
||||||
companyId,
|
companyId,
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
|
|||||||
@ -1,525 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
import { TIMELINE_THREADS_DEFAULT_PAGE_SIZE } from 'src/engine/core-modules/messaging/constants/messaging.constants';
|
|
||||||
import { TimelineThreadsWithTotal } from 'src/engine/core-modules/messaging/dtos/timeline-threads-with-total.dto';
|
|
||||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
|
||||||
import { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
|
||||||
|
|
||||||
type TimelineThreadParticipant = {
|
|
||||||
personId: string;
|
|
||||||
workspaceMemberId: string;
|
|
||||||
firstName: string;
|
|
||||||
lastName: string;
|
|
||||||
displayName: string;
|
|
||||||
avatarUrl: string;
|
|
||||||
handle: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class TimelineMessagingService {
|
|
||||||
constructor(
|
|
||||||
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async getMessagesFromPersonIds(
|
|
||||||
workspaceMemberId: string,
|
|
||||||
workspaceId: string,
|
|
||||||
personIds: string[],
|
|
||||||
page = 1,
|
|
||||||
pageSize: number = TIMELINE_THREADS_DEFAULT_PAGE_SIZE,
|
|
||||||
): Promise<TimelineThreadsWithTotal> {
|
|
||||||
const offset = (page - 1) * pageSize;
|
|
||||||
|
|
||||||
const dataSourceSchema =
|
|
||||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
|
||||||
|
|
||||||
const messageThreads:
|
|
||||||
| {
|
|
||||||
id: string;
|
|
||||||
lastMessageReceivedAt: Date;
|
|
||||||
lastMessageId: string;
|
|
||||||
lastMessageBody: string;
|
|
||||||
rowNumber: number;
|
|
||||||
}[]
|
|
||||||
| undefined = await this.workspaceDataSourceService.executeRawQuery(
|
|
||||||
`
|
|
||||||
SELECT id,
|
|
||||||
"lastMessageReceivedAt",
|
|
||||||
"lastMessageId",
|
|
||||||
"lastMessageBody"
|
|
||||||
FROM
|
|
||||||
(SELECT message."messageThreadId" AS id,
|
|
||||||
MAX(message."receivedAt") AS "lastMessageReceivedAt",
|
|
||||||
message.id AS "lastMessageId",
|
|
||||||
message.text AS "lastMessageBody",
|
|
||||||
ROW_NUMBER() OVER (PARTITION BY message."messageThreadId" ORDER BY MAX(message."receivedAt") DESC) AS "rowNumber"
|
|
||||||
FROM
|
|
||||||
${dataSourceSchema}."message" message
|
|
||||||
LEFT JOIN
|
|
||||||
${dataSourceSchema}."messageParticipant" "messageParticipant" ON "messageParticipant"."messageId" = message.id
|
|
||||||
WHERE
|
|
||||||
"messageParticipant"."personId" = ANY($1)
|
|
||||||
AND EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM ${dataSourceSchema}."messageChannelMessageAssociation" mcma
|
|
||||||
WHERE mcma."messageId" = message.id
|
|
||||||
)
|
|
||||||
GROUP BY
|
|
||||||
message."messageThreadId",
|
|
||||||
message.id
|
|
||||||
ORDER BY
|
|
||||||
message."receivedAt" DESC
|
|
||||||
) AS "messageThreads"
|
|
||||||
WHERE
|
|
||||||
"rowNumber" = 1
|
|
||||||
LIMIT $2
|
|
||||||
OFFSET $3
|
|
||||||
`,
|
|
||||||
[personIds, pageSize, offset],
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!messageThreads) {
|
|
||||||
return {
|
|
||||||
totalNumberOfThreads: 0,
|
|
||||||
timelineThreads: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const messageThreadIds = messageThreads.map(
|
|
||||||
(messageThread) => messageThread.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
const threadSubjects:
|
|
||||||
| {
|
|
||||||
id: string;
|
|
||||||
subject: string;
|
|
||||||
}[]
|
|
||||||
| undefined = await this.workspaceDataSourceService.executeRawQuery(
|
|
||||||
`
|
|
||||||
SELECT *
|
|
||||||
FROM
|
|
||||||
(SELECT
|
|
||||||
message."messageThreadId" AS id,
|
|
||||||
message.subject,
|
|
||||||
ROW_NUMBER() OVER (PARTITION BY message."messageThreadId" ORDER BY MAX(message."receivedAt") ASC) AS "rowNumber"
|
|
||||||
FROM
|
|
||||||
${dataSourceSchema}."message" message
|
|
||||||
WHERE
|
|
||||||
message."messageThreadId" = ANY($1)
|
|
||||||
GROUP BY
|
|
||||||
message."messageThreadId",
|
|
||||||
message.id
|
|
||||||
ORDER BY
|
|
||||||
message."receivedAt" DESC
|
|
||||||
) AS "messageThreads"
|
|
||||||
WHERE
|
|
||||||
"rowNumber" = 1
|
|
||||||
`,
|
|
||||||
[messageThreadIds],
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
const numberOfMessagesInThread:
|
|
||||||
| {
|
|
||||||
id: string;
|
|
||||||
numberOfMessagesInThread: number;
|
|
||||||
}[]
|
|
||||||
| undefined = await this.workspaceDataSourceService.executeRawQuery(
|
|
||||||
`
|
|
||||||
SELECT
|
|
||||||
message."messageThreadId" AS id,
|
|
||||||
COUNT(message.id) AS "numberOfMessagesInThread"
|
|
||||||
FROM
|
|
||||||
${dataSourceSchema}."message" message
|
|
||||||
WHERE
|
|
||||||
message."messageThreadId" = ANY($1)
|
|
||||||
AND EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM ${dataSourceSchema}."messageChannelMessageAssociation" mcma
|
|
||||||
WHERE mcma."messageId" = message.id
|
|
||||||
)
|
|
||||||
GROUP BY
|
|
||||||
message."messageThreadId"
|
|
||||||
`,
|
|
||||||
[messageThreadIds],
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
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 threadMessagesParticipants:
|
|
||||||
| {
|
|
||||||
id: string;
|
|
||||||
messageId: string;
|
|
||||||
receivedAt: Date;
|
|
||||||
body: string;
|
|
||||||
subject: string;
|
|
||||||
role: string;
|
|
||||||
personId: string;
|
|
||||||
workspaceMemberId: string;
|
|
||||||
handle: string;
|
|
||||||
personFirstName: string;
|
|
||||||
personLastName: string;
|
|
||||||
personAvatarUrl: string;
|
|
||||||
workspaceMemberFirstName: string;
|
|
||||||
workspaceMemberLastName: string;
|
|
||||||
workspaceMemberAvatarUrl: string;
|
|
||||||
messageDisplayName: string;
|
|
||||||
}[]
|
|
||||||
| undefined = await this.workspaceDataSourceService.executeRawQuery(
|
|
||||||
`
|
|
||||||
SELECT DISTINCT message."messageThreadId" AS id,
|
|
||||||
message.id AS "messageId",
|
|
||||||
message."receivedAt",
|
|
||||||
message.text,
|
|
||||||
message."subject",
|
|
||||||
"messageParticipant"."role",
|
|
||||||
"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
|
|
||||||
${dataSourceSchema}."message" message
|
|
||||||
LEFT JOIN
|
|
||||||
${dataSourceSchema}."messageParticipant" "messageParticipant" ON "messageParticipant"."messageId" = message.id
|
|
||||||
LEFT JOIN
|
|
||||||
${dataSourceSchema}."person" person ON person."id" = "messageParticipant"."personId"
|
|
||||||
LEFT JOIN
|
|
||||||
${dataSourceSchema}."workspaceMember" "workspaceMember" ON "workspaceMember".id = "messageParticipant"."workspaceMemberId"
|
|
||||||
WHERE
|
|
||||||
message."messageThreadId" = ANY($1)
|
|
||||||
ORDER BY
|
|
||||||
message."receivedAt" DESC
|
|
||||||
`,
|
|
||||||
[messageThreadIds],
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
const threadMessagesFromActiveParticipants =
|
|
||||||
threadMessagesParticipants?.filter(
|
|
||||||
(threadMessage) => threadMessage.role === 'from',
|
|
||||||
);
|
|
||||||
|
|
||||||
const totalNumberOfThreads =
|
|
||||||
await this.workspaceDataSourceService.executeRawQuery(
|
|
||||||
`
|
|
||||||
SELECT COUNT(DISTINCT message."messageThreadId")
|
|
||||||
FROM
|
|
||||||
${dataSourceSchema}."message" message
|
|
||||||
LEFT JOIN
|
|
||||||
${dataSourceSchema}."messageParticipant" "messageParticipant" ON "messageParticipant"."messageId" = message.id
|
|
||||||
WHERE
|
|
||||||
"messageParticipant"."personId" = ANY($1)
|
|
||||||
AND EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM ${dataSourceSchema}."messageChannelMessageAssociation" mcma
|
|
||||||
WHERE mcma."messageId" = message.id
|
|
||||||
)
|
|
||||||
`,
|
|
||||||
[personIds],
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
const threadActiveParticipantsByThreadId: {
|
|
||||||
[key: string]: TimelineThreadParticipant[];
|
|
||||||
} = messageThreadIds.reduce((messageThreadIdAcc, messageThreadId) => {
|
|
||||||
const threadMessages = threadMessagesFromActiveParticipants?.filter(
|
|
||||||
(threadMessage) => threadMessage.id === messageThreadId,
|
|
||||||
);
|
|
||||||
|
|
||||||
const threadActiveParticipants = 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] = threadActiveParticipants
|
|
||||||
? Object.values(threadActiveParticipants)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
return messageThreadIdAcc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
const messageThreadIdsForWhichWorkspaceMemberIsNotInParticipants =
|
|
||||||
messageThreadIds.reduce(
|
|
||||||
(
|
|
||||||
messageThreadIdsForWhichWorkspaceMemberIsInNotParticipantsAcc: string[],
|
|
||||||
messageThreadId,
|
|
||||||
) => {
|
|
||||||
const threadMessagesWithWorkspaceMemberInParticipants =
|
|
||||||
threadMessagesParticipants?.filter(
|
|
||||||
(threadMessage) =>
|
|
||||||
threadMessage.id === messageThreadId &&
|
|
||||||
threadMessage.workspaceMemberId === workspaceMemberId,
|
|
||||||
) ?? [];
|
|
||||||
|
|
||||||
if (threadMessagesWithWorkspaceMemberInParticipants.length === 0)
|
|
||||||
messageThreadIdsForWhichWorkspaceMemberIsInNotParticipantsAcc.push(
|
|
||||||
messageThreadId,
|
|
||||||
);
|
|
||||||
|
|
||||||
return messageThreadIdsForWhichWorkspaceMemberIsInNotParticipantsAcc;
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const threadVisibility:
|
|
||||||
| {
|
|
||||||
id: string;
|
|
||||||
visibility: MessageChannelVisibility;
|
|
||||||
}[]
|
|
||||||
| undefined = await this.workspaceDataSourceService.executeRawQuery(
|
|
||||||
`
|
|
||||||
SELECT
|
|
||||||
message."messageThreadId" AS id,
|
|
||||||
"messageChannel".visibility
|
|
||||||
FROM
|
|
||||||
${dataSourceSchema}."message" message
|
|
||||||
LEFT JOIN
|
|
||||||
${dataSourceSchema}."messageChannelMessageAssociation" "messageChannelMessageAssociation" ON "messageChannelMessageAssociation"."messageId" = message.id
|
|
||||||
LEFT JOIN
|
|
||||||
${dataSourceSchema}."messageChannel" "messageChannel" ON "messageChannel".id = "messageChannelMessageAssociation"."messageChannelId"
|
|
||||||
WHERE
|
|
||||||
message."messageThreadId" = ANY($1)
|
|
||||||
`,
|
|
||||||
[messageThreadIdsForWhichWorkspaceMemberIsNotInParticipants],
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
const visibilityValues = Object.values(MessageChannelVisibility);
|
|
||||||
|
|
||||||
const threadVisibilityByThreadIdForWhichWorkspaceMemberIsNotInParticipants:
|
|
||||||
| {
|
|
||||||
[key: string]: MessageChannelVisibility;
|
|
||||||
}
|
|
||||||
| undefined = threadVisibility?.reduce(
|
|
||||||
(threadVisibilityAcc, threadVisibility) => {
|
|
||||||
threadVisibilityAcc[threadVisibility.id] =
|
|
||||||
visibilityValues[
|
|
||||||
Math.max(
|
|
||||||
visibilityValues.indexOf(threadVisibility.visibility),
|
|
||||||
visibilityValues.indexOf(
|
|
||||||
threadVisibilityAcc[threadVisibility.id] ??
|
|
||||||
MessageChannelVisibility.METADATA,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
];
|
|
||||||
|
|
||||||
return threadVisibilityAcc;
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
|
|
||||||
const threadVisibilityByThreadId: {
|
|
||||||
[key: string]: MessageChannelVisibility;
|
|
||||||
} = messageThreadIds.reduce((threadVisibilityAcc, messageThreadId) => {
|
|
||||||
// If the workspace member is not in the participants of the thread, use the visibility value from the query
|
|
||||||
threadVisibilityAcc[messageThreadId] =
|
|
||||||
messageThreadIdsForWhichWorkspaceMemberIsNotInParticipants.includes(
|
|
||||||
messageThreadId,
|
|
||||||
)
|
|
||||||
? (threadVisibilityByThreadIdForWhichWorkspaceMemberIsNotInParticipants?.[
|
|
||||||
messageThreadId
|
|
||||||
] ?? MessageChannelVisibility.METADATA)
|
|
||||||
: MessageChannelVisibility.SHARE_EVERYTHING;
|
|
||||||
|
|
||||||
return threadVisibilityAcc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
const timelineThreads = messageThreadIds.map((messageThreadId) => {
|
|
||||||
const threadActiveParticipants =
|
|
||||||
threadActiveParticipantsByThreadId[messageThreadId];
|
|
||||||
|
|
||||||
const firstParticipant = threadActiveParticipants[0];
|
|
||||||
|
|
||||||
const threadActiveParticipantsWithoutFirstParticipant =
|
|
||||||
threadActiveParticipants.filter(
|
|
||||||
(threadParticipant) =>
|
|
||||||
threadParticipant.handle !== firstParticipant.handle,
|
|
||||||
);
|
|
||||||
|
|
||||||
const lastTwoParticipants: TimelineThreadParticipant[] = [];
|
|
||||||
|
|
||||||
const lastParticipant =
|
|
||||||
threadActiveParticipantsWithoutFirstParticipant.slice(-1)[0];
|
|
||||||
|
|
||||||
if (lastParticipant) {
|
|
||||||
lastTwoParticipants.push(lastParticipant);
|
|
||||||
|
|
||||||
const threadActiveParticipantsWithoutFirstAndLastParticipants =
|
|
||||||
threadActiveParticipantsWithoutFirstParticipant.filter(
|
|
||||||
(threadParticipant) =>
|
|
||||||
threadParticipant.handle !== lastParticipant.handle,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (threadActiveParticipantsWithoutFirstAndLastParticipants.length > 0)
|
|
||||||
lastTwoParticipants.push(
|
|
||||||
threadActiveParticipantsWithoutFirstAndLastParticipants.slice(
|
|
||||||
-1,
|
|
||||||
)[0],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const thread = messageThreadsByMessageThreadId[messageThreadId];
|
|
||||||
|
|
||||||
const threadSubject =
|
|
||||||
subjectsByMessageThreadId?.[messageThreadId].subject ?? '';
|
|
||||||
|
|
||||||
const numberOfMessages =
|
|
||||||
numberOfMessagesByMessageThreadId?.[messageThreadId]
|
|
||||||
.numberOfMessagesInThread ?? 1;
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: messageThreadId,
|
|
||||||
read: true,
|
|
||||||
firstParticipant,
|
|
||||||
lastTwoParticipants,
|
|
||||||
lastMessageReceivedAt: thread.lastMessageReceivedAt,
|
|
||||||
lastMessageBody: thread.lastMessageBody,
|
|
||||||
visibility:
|
|
||||||
threadVisibilityByThreadId?.[messageThreadId] ??
|
|
||||||
MessageChannelVisibility.METADATA,
|
|
||||||
subject: threadSubject,
|
|
||||||
numberOfMessagesInThread: numberOfMessages,
|
|
||||||
participantCount: threadActiveParticipants.length,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
totalNumberOfThreads: totalNumberOfThreads[0]?.count ?? 0,
|
|
||||||
timelineThreads,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async getMessagesFromCompanyId(
|
|
||||||
workspaceMemberId: string,
|
|
||||||
workspaceId: string,
|
|
||||||
companyId: string,
|
|
||||||
page = 1,
|
|
||||||
pageSize: number = TIMELINE_THREADS_DEFAULT_PAGE_SIZE,
|
|
||||||
): Promise<TimelineThreadsWithTotal> {
|
|
||||||
const dataSourceSchema =
|
|
||||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
|
||||||
|
|
||||||
const personIds = await this.workspaceDataSourceService.executeRawQuery(
|
|
||||||
`
|
|
||||||
SELECT
|
|
||||||
p."id"
|
|
||||||
FROM
|
|
||||||
${dataSourceSchema}."person" p
|
|
||||||
WHERE
|
|
||||||
p."companyId" = $1
|
|
||||||
`,
|
|
||||||
[companyId],
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!personIds) {
|
|
||||||
return {
|
|
||||||
totalNumberOfThreads: 0,
|
|
||||||
timelineThreads: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const formattedPersonIds = personIds.map(
|
|
||||||
(personId: { id: string }) => personId.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
const messageThreads = await this.getMessagesFromPersonIds(
|
|
||||||
workspaceMemberId,
|
|
||||||
workspaceId,
|
|
||||||
formattedPersonIds,
|
|
||||||
page,
|
|
||||||
pageSize,
|
|
||||||
);
|
|
||||||
|
|
||||||
return messageThreads;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
import { TimelineThreadParticipant } from 'src/engine/core-modules/messaging/dtos/timeline-thread-participant.dto';
|
||||||
|
import { filterActiveParticipants } from 'src/engine/core-modules/messaging/utils/filter-active-participants.util';
|
||||||
|
import { formatThreadParticipant } from 'src/engine/core-modules/messaging/utils/format-thread-participant.util';
|
||||||
|
import { MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity';
|
||||||
|
|
||||||
|
export const extractParticipantSummary = (
|
||||||
|
messageParticipants: MessageParticipantWorkspaceEntity[],
|
||||||
|
): {
|
||||||
|
firstParticipant: TimelineThreadParticipant;
|
||||||
|
lastTwoParticipants: TimelineThreadParticipant[];
|
||||||
|
participantCount: number;
|
||||||
|
} => {
|
||||||
|
const activeMessageParticipants =
|
||||||
|
filterActiveParticipants(messageParticipants);
|
||||||
|
|
||||||
|
const firstParticipant = formatThreadParticipant(
|
||||||
|
activeMessageParticipants[0],
|
||||||
|
);
|
||||||
|
|
||||||
|
const activeMessageParticipantsWithoutFirstParticipant =
|
||||||
|
activeMessageParticipants.filter(
|
||||||
|
(threadParticipant) =>
|
||||||
|
threadParticipant.handle !== firstParticipant.handle,
|
||||||
|
);
|
||||||
|
|
||||||
|
const lastTwoParticipants: TimelineThreadParticipant[] = [];
|
||||||
|
|
||||||
|
const lastParticipant =
|
||||||
|
activeMessageParticipantsWithoutFirstParticipant.slice(-1)[0];
|
||||||
|
|
||||||
|
if (lastParticipant) {
|
||||||
|
lastTwoParticipants.push(formatThreadParticipant(lastParticipant));
|
||||||
|
|
||||||
|
const activeMessageParticipantsWithoutFirstAndLastParticipants =
|
||||||
|
activeMessageParticipantsWithoutFirstParticipant.filter(
|
||||||
|
(threadParticipant) =>
|
||||||
|
threadParticipant.handle !== lastParticipant.handle,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (activeMessageParticipantsWithoutFirstAndLastParticipants.length > 0) {
|
||||||
|
lastTwoParticipants.push(
|
||||||
|
formatThreadParticipant(
|
||||||
|
activeMessageParticipantsWithoutFirstAndLastParticipants.slice(-1)[0],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
firstParticipant,
|
||||||
|
lastTwoParticipants,
|
||||||
|
participantCount: activeMessageParticipants.length,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
import { MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity';
|
||||||
|
|
||||||
|
export const filterActiveParticipants = (
|
||||||
|
participants: MessageParticipantWorkspaceEntity[],
|
||||||
|
): MessageParticipantWorkspaceEntity[] => {
|
||||||
|
return participants.filter((participant) => participant.role === 'from');
|
||||||
|
};
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
import { TimelineThreadParticipant } from 'src/engine/core-modules/messaging/dtos/timeline-thread-participant.dto';
|
||||||
|
import { MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity';
|
||||||
|
|
||||||
|
export const formatThreadParticipant = (
|
||||||
|
threadParticipant: MessageParticipantWorkspaceEntity,
|
||||||
|
): TimelineThreadParticipant => ({
|
||||||
|
personId: threadParticipant.personId,
|
||||||
|
workspaceMemberId: threadParticipant.workspaceMemberId,
|
||||||
|
firstName:
|
||||||
|
threadParticipant.person?.name?.firstName ||
|
||||||
|
threadParticipant.workspaceMember?.name.firstName ||
|
||||||
|
'',
|
||||||
|
lastName:
|
||||||
|
threadParticipant.person?.name?.lastName ||
|
||||||
|
threadParticipant.workspaceMember?.name.lastName ||
|
||||||
|
'',
|
||||||
|
displayName:
|
||||||
|
threadParticipant.person?.name?.firstName ||
|
||||||
|
threadParticipant.person?.name?.lastName ||
|
||||||
|
threadParticipant.workspaceMember?.name.firstName ||
|
||||||
|
threadParticipant.workspaceMember?.name.lastName ||
|
||||||
|
threadParticipant.displayName ||
|
||||||
|
threadParticipant.handle ||
|
||||||
|
'',
|
||||||
|
avatarUrl:
|
||||||
|
threadParticipant.person?.avatarUrl ||
|
||||||
|
threadParticipant.workspaceMember?.avatarUrl ||
|
||||||
|
'',
|
||||||
|
handle: threadParticipant.handle,
|
||||||
|
});
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
import { TimelineThread } from 'src/engine/core-modules/messaging/dtos/timeline-thread.dto';
|
||||||
|
import { extractParticipantSummary } from 'src/engine/core-modules/messaging/utils/extract-participant-summary.util';
|
||||||
|
import { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||||
|
import { MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity';
|
||||||
|
|
||||||
|
export const formatThreads = (
|
||||||
|
threads: Omit<
|
||||||
|
TimelineThread,
|
||||||
|
| 'firstParticipant'
|
||||||
|
| 'lastTwoParticipants'
|
||||||
|
| 'participantCount'
|
||||||
|
| 'read'
|
||||||
|
| 'visibility'
|
||||||
|
>[],
|
||||||
|
threadParticipantsByThreadId: {
|
||||||
|
[key: string]: MessageParticipantWorkspaceEntity[];
|
||||||
|
},
|
||||||
|
threadVisibilityByThreadId: {
|
||||||
|
[key: string]: MessageChannelVisibility;
|
||||||
|
},
|
||||||
|
): TimelineThread[] => {
|
||||||
|
return threads.map((thread) => ({
|
||||||
|
...thread,
|
||||||
|
...extractParticipantSummary(threadParticipantsByThreadId[thread.id]),
|
||||||
|
visibility: threadVisibilityByThreadId[thread.id],
|
||||||
|
read: true,
|
||||||
|
}));
|
||||||
|
};
|
||||||
@ -4,6 +4,7 @@ import {
|
|||||||
ActorMetadata,
|
ActorMetadata,
|
||||||
FieldActorSource,
|
FieldActorSource,
|
||||||
} from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
} from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||||
|
import { AddressMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/address.composite-type';
|
||||||
import { CurrencyMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/currency.composite-type';
|
import { CurrencyMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/currency.composite-type';
|
||||||
import { LinksMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/links.composite-type';
|
import { LinksMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/links.composite-type';
|
||||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
@ -30,7 +31,6 @@ import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/perso
|
|||||||
import { TaskTargetWorkspaceEntity } from 'src/modules/task/standard-objects/task-target.workspace-entity';
|
import { TaskTargetWorkspaceEntity } from 'src/modules/task/standard-objects/task-target.workspace-entity';
|
||||||
import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
|
import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
|
||||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||||
import { AddressMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/address.composite-type';
|
|
||||||
|
|
||||||
@WorkspaceEntity({
|
@WorkspaceEntity({
|
||||||
standardId: STANDARD_OBJECT_IDS.company,
|
standardId: STANDARD_OBJECT_IDS.company,
|
||||||
@ -132,7 +132,7 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity {
|
|||||||
})
|
})
|
||||||
@WorkspaceIsSystem()
|
@WorkspaceIsSystem()
|
||||||
@WorkspaceIsNullable()
|
@WorkspaceIsNullable()
|
||||||
position: number | null;
|
position: number;
|
||||||
|
|
||||||
@WorkspaceField({
|
@WorkspaceField({
|
||||||
standardId: COMPANY_STANDARD_FIELD_IDS.createdBy,
|
standardId: COMPANY_STANDARD_FIELD_IDS.createdBy,
|
||||||
|
|||||||
@ -127,7 +127,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity {
|
|||||||
})
|
})
|
||||||
@WorkspaceIsSystem()
|
@WorkspaceIsSystem()
|
||||||
@WorkspaceIsNullable()
|
@WorkspaceIsNullable()
|
||||||
position: number | null;
|
position: number;
|
||||||
|
|
||||||
@WorkspaceField({
|
@WorkspaceField({
|
||||||
standardId: PERSON_STANDARD_FIELD_IDS.createdBy,
|
standardId: PERSON_STANDARD_FIELD_IDS.createdBy,
|
||||||
|
|||||||
Reference in New Issue
Block a user