Skip to content

feat(api): add chat conversations #772

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Repository } from "typeorm";
import { UserIdService } from "src/core/authentication";
import { map } from "src/core/mapper";
import { CurrentOrganizationService } from "src/core/organizations";
import { createCurrentOrganizationMock } from "src/core/organizations/test/mocks/current-organization.service.mock";
import { createCurrentOrganizationServiceMock } from "src/core/organizations/test";
import { CurrentProfileService, Profile } from "src/core/profiles";
import { createMockRepository, createUserIdServiceMock } from "src/test/mocks";

Expand All @@ -24,7 +24,7 @@ describe(CreateJournalEntryHandler.name, () => {
journalEntries = createMockRepository<JournalEntry>();
userIdService = createUserIdServiceMock();

currentOrganizationService = createCurrentOrganizationMock();
currentOrganizationService = createCurrentOrganizationServiceMock();

currentProfileService = new CurrentProfileService(profiles, userIdService);

Expand Down
1 change: 1 addition & 0 deletions services/api/src/core/object-storage/buckets.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export const BUCKET_NAMES = {
BLOG_POST_PHOTO: "blog-post-photo",
CHAT_CONVERSATION_PHOTO: "chat-conversation-photo",
PROFILE_PHOTO: "profile-photo",
} as const;
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { createEventBusMock, createMockRepository } from "src/test/mocks";
import { CurrentOrganizationService } from "../../../domain/services/current-organization.service";
import { OrganizationService } from "../../../domain/services/organization.service";
import { Organization } from "../../../infrastructure/entities/organization.entity";
import { createCurrentOrganizationMock } from "../../../test/mocks/current-organization.service.mock";
import { createCurrentOrganizationServiceMock } from "../../../test/mocks/current-organization.service.mock";
import { AddMemberToOrganizationViaEmailCommand } from "../../contracts/commands/add-member-to-organization-via-email.command";
import { AddMemberToOrganizationViaEmailHandler } from "./add-member-to-organization-via-email.handler";

Expand All @@ -29,7 +29,7 @@ describe(AddMemberToOrganizationViaEmailHandler.name, () => {
]);
profiles = createMockRepository<Profile>([]);

currentOrganizationService = createCurrentOrganizationMock();
currentOrganizationService = createCurrentOrganizationServiceMock();

userIdService = { fetchUserIdByEmail: jest.fn() } as any;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { createEventBusMock, createMockRepository } from "src/test/mocks";
import { CurrentOrganizationService } from "../../../domain/services/current-organization.service";
import { OrganizationService } from "../../../domain/services/organization.service";
import { Organization } from "../../../infrastructure/entities/organization.entity";
import { createCurrentOrganizationMock } from "../../../test/mocks/current-organization.service.mock";
import { createCurrentOrganizationServiceMock } from "../../../test/mocks/current-organization.service.mock";
import { AddMemberToOrganizationCommand } from "../../contracts/commands/add-member-to-organization.command";
import { AddMemberToOrganizationHandler } from "./add-member-to-organization.handler";

Expand All @@ -27,7 +27,7 @@ describe(AddMemberToOrganizationHandler.name, () => {
]);
profiles = createMockRepository<Profile>([]);

currentOrganizationService = createCurrentOrganizationMock();
currentOrganizationService = createCurrentOrganizationServiceMock();

organizationService = new OrganizationService(
eventBus,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { createMockRepository } from "src/test/mocks";

import { CurrentOrganizationService } from "../../../domain/services/current-organization.service";
import { OrganizationMembership } from "../../../infrastructure/entities/organization-membership.entity";
import { createCurrentOrganizationMock } from "../../../test/mocks/current-organization.service.mock";
import { createCurrentOrganizationServiceMock } from "../../../test/mocks/current-organization.service.mock";
import { RemoveMemberFromCurrentOrganizationCommand } from "../../contracts/commands/remove-member-from-current-organization.command";
import { RemoveMemberFromCurrentOrganizationHandler } from "./remove-member-from-current-organization.handler";

Expand All @@ -17,7 +17,7 @@ describe(RemoveMemberFromCurrentOrganizationHandler.name, () => {
beforeEach(() => {
memberships = createMockRepository<OrganizationMembership>();

currentOrganizationService = createCurrentOrganizationMock();
currentOrganizationService = createCurrentOrganizationServiceMock();

commandHandler = new RemoveMemberFromCurrentOrganizationHandler(
{ authorizeOwnerOrAdmin: jest.fn() } as any,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Repository } from "typeorm";
import { OrganizationRole } from "src/core/authorization";
import { map } from "src/core/mapper";
import { CurrentOrganizationService } from "src/core/organizations/domain/services/current-organization.service";
import { createCurrentOrganizationMock } from "src/core/organizations/test/mocks/current-organization.service.mock";
import { createCurrentOrganizationServiceMock } from "src/core/organizations/test";
import { createMockRepository } from "src/test/mocks";

import { OrganizationMembership } from "../../../infrastructure/entities/organization-membership.entity";
Expand All @@ -16,7 +16,7 @@ describe(UpdateMemberRoleHandler.name, () => {
let memberships: Repository<OrganizationMembership>;

beforeEach(() => {
currentOrganizationService = createCurrentOrganizationMock();
currentOrganizationService = createCurrentOrganizationServiceMock();
memberships = createMockRepository<OrganizationMembership>([{}] as any);

commandHandler = new UpdateMemberRoleHandler(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Repository } from "typeorm";

import { map } from "src/core/mapper";
import { CurrentOrganizationService } from "src/core/organizations/domain/services/current-organization.service";
import { createCurrentOrganizationMock } from "src/core/organizations/test/mocks/current-organization.service.mock";
import { createCurrentOrganizationServiceMock } from "src/core/organizations/test";
import { createEventBusMock, createMockRepository } from "src/test/mocks";

import { OrganizationService } from "../../../domain/services/organization.service";
Expand All @@ -23,7 +23,7 @@ describe(UpdateOrganizationHandler.name, () => {

organizations = createMockRepository<Organization>();

currentOrganizationService = createCurrentOrganizationMock();
currentOrganizationService = createCurrentOrganizationServiceMock();

organizationService = new OrganizationService(
eventBus,
Expand Down
1 change: 1 addition & 0 deletions services/api/src/core/organizations/test/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { createCurrentOrganizationServiceMock } from "./mocks/current-organization.service.mock";
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ class CurrentOrganizationMock {
fetchCurrentOrganizationId = jest.fn();
}

export function createCurrentOrganizationMock(): CurrentOrganizationService {
export function createCurrentOrganizationServiceMock(): CurrentOrganizationService {
return new CurrentOrganizationMock() as any;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { EventBus } from "@nestjs/cqrs";
import { Repository } from "typeorm";

import { map } from "src/core/mapper";
import { createCurrentOrganizationMock } from "src/core/organizations/test/mocks/current-organization.service.mock";
import { createCurrentOrganizationServiceMock } from "src/core/organizations/test";
import { createEventBusMock, createMockRepository } from "src/test/mocks";

import { BlogPostService } from "../../../domain/services/blog-post.service";
Expand All @@ -20,7 +20,7 @@ describe(CreateBlogPostCommentHandler.name, () => {
blogPosts = createMockRepository<BlogPost>();
eventBus = createEventBusMock();

const currentOrganizationService = createCurrentOrganizationMock();
const currentOrganizationService = createCurrentOrganizationServiceMock();

const currentProfileService = {
fetchCurrentProfileId: jest.fn(() => "my-profile-id"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { EventBus } from "@nestjs/cqrs";
import { Repository } from "typeorm";

import { map } from "src/core/mapper";
import { createCurrentOrganizationMock } from "src/core/organizations/test/mocks/current-organization.service.mock";
import { createCurrentOrganizationServiceMock } from "src/core/organizations/test";
import { createEventBusMock, createMockRepository } from "src/test/mocks";

import { BlogPostService } from "../../../domain/services/blog-post.service";
Expand All @@ -20,7 +20,7 @@ describe(CreateBlogPostReactionHandler.name, () => {
blogPosts = createMockRepository<BlogPost>();
eventBus = createEventBusMock();

const currentOrganizationService = createCurrentOrganizationMock();
const currentOrganizationService = createCurrentOrganizationServiceMock();

const currentProfileService = {
fetchCurrentProfileId: jest.fn(() => "my-profile-id"),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
export class ConnectionDetails {
export class ChatConversationDetails {
imageUrl?: string;
lastMessage?: string;
lastMessageSent?: Date;
name!: string;
profileId!: number;
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { Type } from "class-transformer";
import { IsPositive } from "class-validator";
import { IsUUID } from "class-validator";

export class GetChatMessagesQuery {
@Type(() => Number)
@IsPositive()
public readonly profileId!: number;
@IsUUID()
public readonly conversationId!: string;
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export class GetConversationListQuery {}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ describe(PostChatMessageHandler.name, () => {

expect(chatMessageService.saveChatMessage).toHaveBeenCalledWith({
message: command.message,
receiverId: command.profileId,
senderId: mockProfile.id,
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export class PostChatMessageHandler

const chatMessage = new ChatMessage();
chatMessage.message = command.message;
chatMessage.receiverId = command.profileId;
// chatMessage.receiverId = command.profileId;
chatMessage.senderId = currentProfile.id;

await this.chatMessageService.saveChatMessage(chatMessage);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@ import { INotification } from "src/core/notifications/types";
import { Profile } from "src/core/profiles";

import { ChatMessageSentEvent } from "../../../domain/events/chat-message-sent.event";
import { ChatConversation } from "../../../infrastructure/entities/chat-conversation.entity";

@EventsHandler(ChatMessageSentEvent)
export class NotifyReceiverOnPostedChatMessageHandler
implements IEventHandler<ChatMessageSentEvent>
{
constructor(
@InjectRepository(ChatConversation)
private readonly chatConversations: Repository<ChatConversation>,
private readonly notificationService: NotificationService,
@InjectRepository(Profile)
private readonly profiles: Repository<Profile>,
Expand All @@ -36,17 +39,31 @@ export class NotifyReceiverOnPostedChatMessageHandler
throw new NotFoundException("Profile not found");
}

const conversation = await this.chatConversations.findOne({
select: {
members: {
profileId: true,
},
},
where: {
id: event.conversationId,
},
});

if (!conversation) {
throw new NotFoundException("Conversation not found");
}

const receivers = conversation.members.map((member) => member.profileId);

const notification: INotification = {
data: { receiverId: event.receiverId, senderId: event.senderId },
data: { senderId: event.senderId },
description: `${profile.name} sent you a message in Meet`,
message: `${profile.name} sent you a new message`,
resourcePath: `/chat/${event.senderId}`,
type: NotificationEventsConstants.NEW_CHAT_MESSAGE,
};

await this.notificationService.notifyProfiles(
[event.receiverId],
notification,
);
await this.notificationService.notifyProfiles(receivers, notification);
}
}
Original file line number Diff line number Diff line change
@@ -1,48 +1,51 @@
import { Repository } from "typeorm";

import { map } from "src/core/mapper";
import { createCurrentOrganizationServiceMock } from "src/core/organizations/test";
import { Profile } from "src/core/profiles";
import { createMockRepository } from "src/test/mocks";

import { ChatConversation } from "../../../infrastructure/entities/chat-conversation.entity";
import { ChatMessage } from "../../../infrastructure/entities/chat-message.entity";
import { GetChatMessagesQuery } from "../../contracts/queries/get-chat-messages.query";
import { GetChatMessagesHandler } from "./get-chat-messages.handler";

describe(GetChatMessagesHandler.name, () => {
let chatMessages: Repository<ChatMessage>;

let receivingProfile: Profile;
let sendingProfile: Profile;

let queryHandler: GetChatMessagesHandler;

beforeEach(() => {
receivingProfile = new Profile();
receivingProfile.id = 2;

sendingProfile = new Profile();
sendingProfile.id = 1;

const conversation = new ChatConversation();

const chatMessage = new ChatMessage();
chatMessage.receiverId = receivingProfile.id;
chatMessage.conversationId = conversation.id;
chatMessage.senderId = sendingProfile.id;

chatMessages = createMockRepository<ChatMessage>([chatMessage]);

const currentOrganizationService = createCurrentOrganizationServiceMock();

const currentProfileService = {
fetchCurrentProfileId: jest.fn(() => sendingProfile.id),
} as any;

queryHandler = new GetChatMessagesHandler(
chatMessages,
currentOrganizationService,
currentProfileService,
);
});

describe("execute", () => {
it("should return chat messages", async () => {
const query = map(GetChatMessagesQuery, {
profileId: receivingProfile.id,
conversationId: "1",
});

const result = await queryHandler.execute(query);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";

import { mapArray } from "src/core/mapper";
import { CurrentOrganizationService } from "src/core/organizations";
import { CurrentProfileService } from "src/core/profiles";

import { ChatMessage } from "../../..//infrastructure/entities/chat-message.entity";
Expand All @@ -16,19 +17,24 @@ export class GetChatMessagesHandler
constructor(
@InjectRepository(ChatMessage)
private readonly chatMessages: Repository<ChatMessage>,
private readonly currentOrganizationService: CurrentOrganizationService,
private readonly currentProfileService: CurrentProfileService,
) {}

async execute(query: GetChatMessagesQuery) {
const currentProfileId =
await this.currentProfileService.fetchCurrentProfileId();
const targetProfileId = query.profileId;

const currentOrganizationId =
await this.currentOrganizationService.fetchCurrentOrganizationId();

const foundChatMessages = await this.chatMessages.find({
where: [
{ receiverId: currentProfileId, senderId: targetProfileId },
{ receiverId: targetProfileId, senderId: currentProfileId },
],
where: {
conversation: {
id: query.conversationId,
organizationId: currentOrganizationId,
},
},
});

return mapArray(ChatMessageDetails, foundChatMessages, (item) => ({
Expand Down
Loading
Loading