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(
|
||||
(path: string, basePath?: AppBasePath) => {
|
||||
const constructedPath = basePath
|
||||
? (new URL(basePath + path, document.location.origin).pathname ?? '')
|
||||
? new URL(basePath + path, document.location.origin).pathname ?? ''
|
||||
: path;
|
||||
|
||||
return !!matchPath(constructedPath, location.pathname);
|
||||
|
||||
@ -45,7 +45,7 @@ export const MessageThreadSubscribersChip = ({
|
||||
? `+${numberOfMessageThreadSubscribers - MAX_NUMBER_OF_AVATARS}`
|
||||
: null;
|
||||
|
||||
const label = isPrivateThread ? privateLabel : (moreAvatarsLabel ?? '');
|
||||
const label = isPrivateThread ? privateLabel : moreAvatarsLabel ?? '';
|
||||
|
||||
return (
|
||||
<Chip
|
||||
|
||||
@ -4,7 +4,7 @@ import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
export const generateDefaultRecordChipData = (record: ObjectRecord) => {
|
||||
const name = isFieldFullNameValue(record.name)
|
||||
? record.name.firstName + ' ' + record.name.lastName
|
||||
: (record.name ?? '');
|
||||
: record.name ?? '';
|
||||
|
||||
return {
|
||||
name,
|
||||
|
||||
@ -42,8 +42,8 @@ export const MultiSelectFieldInput = ({
|
||||
const [searchFilter, setSearchFilter] = useState('');
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const selectedOptions = fieldDefinition.metadata.options.filter((option) =>
|
||||
fieldValues?.includes(option.value),
|
||||
const selectedOptions = fieldDefinition.metadata.options.filter(
|
||||
(option) => fieldValues?.includes(option.value),
|
||||
);
|
||||
|
||||
const optionsInDropDown = fieldDefinition.metadata.options;
|
||||
|
||||
@ -69,7 +69,7 @@ export const RecordDetailRelationSection = ({
|
||||
const relationRecords: ObjectRecord[] =
|
||||
fieldValue && isToOneObject
|
||||
? [fieldValue as ObjectRecord]
|
||||
: ((fieldValue as ObjectRecord[]) ?? []);
|
||||
: (fieldValue as ObjectRecord[]) ?? [];
|
||||
|
||||
const relationRecordIds = relationRecords.map(({ id }) => id);
|
||||
|
||||
|
||||
@ -6,10 +6,11 @@ export const findUnmatchedRequiredFields = <T extends string>(
|
||||
columns: Columns<T>,
|
||||
) =>
|
||||
fields
|
||||
.filter((field) =>
|
||||
field.fieldValidationDefinitions?.some(
|
||||
(validation) => validation.rule === 'required',
|
||||
),
|
||||
.filter(
|
||||
(field) =>
|
||||
field.fieldValidationDefinitions?.some(
|
||||
(validation) => validation.rule === 'required',
|
||||
),
|
||||
)
|
||||
.filter(
|
||||
(field) =>
|
||||
|
||||
@ -16,7 +16,7 @@ type ContainerProps = {
|
||||
const StyledContainer = styled.div<ContainerProps>`
|
||||
align-items: center;
|
||||
background-color: ${({ theme, isOn, color }) =>
|
||||
isOn ? (color ?? theme.color.blue) : theme.background.quaternary};
|
||||
isOn ? color ?? theme.color.blue : theme.background.quaternary};
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
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';
|
||||
|
||||
@ObjectType('TimelineThreadParticipant')
|
||||
export class TimelineThreadParticipant {
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
personId: string;
|
||||
personId: string | null;
|
||||
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
workspaceMemberId: string;
|
||||
workspaceMemberId: string | null;
|
||||
|
||||
@Field()
|
||||
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 { 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 { 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 { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
@Module({
|
||||
imports: [WorkspaceDataSourceModule, UserModule],
|
||||
exports: [],
|
||||
providers: [TimelineMessagingResolver, TimelineMessagingService],
|
||||
providers: [
|
||||
TimelineMessagingResolver,
|
||||
TimelineMessagingService,
|
||||
GetMessagesService,
|
||||
],
|
||||
})
|
||||
export class TimelineMessagingModule {}
|
||||
|
||||
@ -1,18 +1,16 @@
|
||||
import { Args, Query, Resolver, Int, ArgsType, Field } from '@nestjs/graphql';
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { Args, ArgsType, Field, Int, Query, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { Max } from 'class-validator';
|
||||
|
||||
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
|
||||
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 { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
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 { 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 { 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()
|
||||
class GetTimelineThreadsFromPersonIdArgs {
|
||||
@ -44,13 +42,12 @@ class GetTimelineThreadsFromCompanyIdArgs {
|
||||
@Resolver(() => TimelineThreadsWithTotal)
|
||||
export class TimelineMessagingResolver {
|
||||
constructor(
|
||||
private readonly timelineMessagingService: TimelineMessagingService,
|
||||
private readonly getMessagesFromPersonIdsService: GetMessagesService,
|
||||
private readonly userService: UserService,
|
||||
) {}
|
||||
|
||||
@Query(() => TimelineThreadsWithTotal)
|
||||
async getTimelineThreadsFromPersonId(
|
||||
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||
@AuthUser() user: User,
|
||||
@Args() { personId, page, pageSize }: GetTimelineThreadsFromPersonIdArgs,
|
||||
) {
|
||||
@ -61,9 +58,8 @@ export class TimelineMessagingResolver {
|
||||
}
|
||||
|
||||
const timelineThreads =
|
||||
await this.timelineMessagingService.getMessagesFromPersonIds(
|
||||
await this.getMessagesFromPersonIdsService.getMessagesFromPersonIds(
|
||||
workspaceMember.id,
|
||||
workspaceId,
|
||||
[personId],
|
||||
page,
|
||||
pageSize,
|
||||
@ -74,7 +70,6 @@ export class TimelineMessagingResolver {
|
||||
|
||||
@Query(() => TimelineThreadsWithTotal)
|
||||
async getTimelineThreadsFromCompanyId(
|
||||
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||
@AuthUser() user: User,
|
||||
@Args() { companyId, page, pageSize }: GetTimelineThreadsFromCompanyIdArgs,
|
||||
) {
|
||||
@ -85,9 +80,8 @@ export class TimelineMessagingResolver {
|
||||
}
|
||||
|
||||
const timelineThreads =
|
||||
await this.timelineMessagingService.getMessagesFromCompanyId(
|
||||
await this.getMessagesFromPersonIdsService.getMessagesFromCompanyId(
|
||||
workspaceMember.id,
|
||||
workspaceId,
|
||||
companyId,
|
||||
page,
|
||||
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,
|
||||
FieldActorSource,
|
||||
} 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 { 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';
|
||||
@ -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 { 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 { AddressMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/address.composite-type';
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.company,
|
||||
@ -132,7 +132,7 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
})
|
||||
@WorkspaceIsSystem()
|
||||
@WorkspaceIsNullable()
|
||||
position: number | null;
|
||||
position: number;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: COMPANY_STANDARD_FIELD_IDS.createdBy,
|
||||
|
||||
@ -127,7 +127,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
})
|
||||
@WorkspaceIsSystem()
|
||||
@WorkspaceIsNullable()
|
||||
position: number | null;
|
||||
position: number;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: PERSON_STANDARD_FIELD_IDS.createdBy,
|
||||
|
||||
Reference in New Issue
Block a user