Skip to content
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

feat(posts-saga): add posts store saga and actions #2186

Merged
merged 6 commits into from
Aug 21, 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
8 changes: 8 additions & 0 deletions src/lib/chat/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,3 +346,11 @@ export async function createUnencryptedConversation(
) {
return chat.get().matrix.createUnencryptedConversation(users, name, image, optimisticId, groupType);
}

export async function sendPostByChannelId(channelId: string, message: string, optimisticId?: string) {
return chat.get().matrix.sendPostsByChannelId(channelId, message, optimisticId);
}

export async function getPostMessagesByChannelId(channelId: string, lastCreatedAt?: number) {
return chat.get().matrix.getPostMessagesByChannelId(channelId, lastCreatedAt);
}
137 changes: 137 additions & 0 deletions src/lib/chat/matrix-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,19 @@ describe('matrix client', () => {
});
});

describe('sendPostsByChannelId', () => {
it('sends a post message successfully', async () => {
const sendEvent = jest.fn().mockResolvedValue({ event_id: '$80dh3P6kQKgA0IIrdkw5AW0vSXXcRMT2PPIGVg9nEvU' });
const client = subject({ createClient: jest.fn(() => getSdkClient({ sendEvent })) });

await client.connect(null, 'token');

const result = await client.sendPostsByChannelId('channel-id', 'post-message');

expect(result).toMatchObject({ id: '$80dh3P6kQKgA0IIrdkw5AW0vSXXcRMT2PPIGVg9nEvU' });
});
});

describe('deleteMessageByRoomId', () => {
it('deletes a message by room ID and message ID', async () => {
const messageId = '123456';
Expand Down Expand Up @@ -687,6 +700,43 @@ describe('matrix client', () => {
expect(fetchedMessages[0].message).toEqual('message 2');
});

it('filters out post messages', async () => {
const getUser = jest.fn().mockReturnValue({ displayName: 'Mock User' });
const getEvents = jest.fn().mockReturnValue([
{
getEffectiveEvent: () => ({
type: 'm.room.message',
content: { body: 'message 1', msgtype: 'm.text' },
event_id: 'message-id-1',
}),
},
{
getEffectiveEvent: () => ({
type: 'm.room.post',
content: { body: 'post message 1', msgtype: 'm.text' },
event_id: 'post-message-id-1',
}),
},
{
getEffectiveEvent: () => ({
type: 'm.room.message',
content: { body: 'message 2', msgtype: 'm.text' },
event_id: 'message-id-2',
}),
},
]);
const getRoom = jest.fn().mockReturnValue(stubRoom({ getLiveTimeline: () => stubTimeline({ getEvents }) }));

const client = subject({ createClient: jest.fn(() => getSdkClient({ getUser, getRoom })) });

await client.connect(null, 'token');
const { messages: fetchedMessages } = await client.getMessagesByChannelId('channel-id');

expect(fetchedMessages).toHaveLength(2);
expect(fetchedMessages[0].message).toEqual('message 1');
expect(fetchedMessages[1].message).toEqual('message 2');
});

it('fetches messages successfully', async () => {
const getUser = jest.fn().mockReturnValue({ displayName: 'Mock User' });
const getEvents = jest.fn().mockReturnValue([
Expand Down Expand Up @@ -737,6 +787,93 @@ describe('matrix client', () => {
});
});

describe('getPostMessagesByChannelId', () => {
it('fetches post messages successfully', async () => {
const getUser = jest.fn().mockReturnValue({ displayName: 'Mock User' });
const getEvents = jest.fn().mockReturnValue([
{
getEffectiveEvent: () => ({
type: 'm.room.post',
content: { body: 'post message 1', msgtype: 'm.text' },
event_id: 'post-message-id-1',
}),
},
{
getEffectiveEvent: () => ({
type: 'm.room.post',
content: { body: 'post message 2', msgtype: 'm.text' },
event_id: 'post-message-id-2',
}),
},
{
getEffectiveEvent: () => ({
type: 'm.room.post',
content: { body: 'post message 3', msgtype: 'm.text' },
event_id: 'post-message-id-3',
}),
},
]);
const getRoom = jest.fn().mockReturnValue(stubRoom({ getLiveTimeline: () => stubTimeline({ getEvents }) }));

const client = subject({ createClient: jest.fn(() => getSdkClient({ getUser, getRoom })) });

await client.connect(null, 'token');
const { postMessages: fetchedPostMessages } = await client.getPostMessagesByChannelId('channel-id');

expect(fetchedPostMessages).toHaveLength(3);
});

it('filters out regular chat messages', async () => {
const getUser = jest.fn().mockReturnValue({ displayName: 'Mock User' });
const getEvents = jest.fn().mockReturnValue([
{
getEffectiveEvent: () => ({
type: 'm.room.message',
content: { body: 'message 1', msgtype: 'm.text' },
event_id: 'message-id-1',
}),
},
{
getEffectiveEvent: () => ({
type: 'm.room.post',
content: { body: 'post message 1', msgtype: 'm.text' },
event_id: 'post-message-id-1',
}),
},
{
getEffectiveEvent: () => ({
type: 'm.room.message',
content: { body: 'message 2', msgtype: 'm.text' },
event_id: 'message-id-2',
}),
},
]);
const getRoom = jest.fn().mockReturnValue(stubRoom({ getLiveTimeline: () => stubTimeline({ getEvents }) }));

const client = subject({ createClient: jest.fn(() => getSdkClient({ getUser, getRoom })) });

await client.connect(null, 'token');
const { postMessages: fetchedPostMessages } = await client.getPostMessagesByChannelId('channel-id');

expect(fetchedPostMessages).toHaveLength(1);
expect(fetchedPostMessages[0].message).toEqual('post message 1');
});

it('returns an empty array if no messages are found', async () => {
const getUser = jest.fn().mockReturnValue({ displayName: 'Mock User' });
const createPostMessagesRequest = jest.fn().mockResolvedValue({ chunk: [] });

const client = subject({
createClient: jest.fn(() => getSdkClient({ createPostMessagesRequest, getUser })),
});

await client.connect(null, 'token');
const { postMessages: fetchedMessages } = await client.getPostMessagesByChannelId('channel-id');

expect(fetchedMessages).toHaveLength(0);
});
});

describe('editMessage', () => {
it('edits a message successfully', async () => {
const originalMessageId = 'orig-message-id';
Expand Down
68 changes: 64 additions & 4 deletions src/lib/chat/matrix-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ import {
ReceiptType,
} from 'matrix-js-sdk';
import { RealtimeChatEvents, IChatClient } from './';
import { mapEventToAdminMessage, mapMatrixMessage, mapToLiveRoomEvent } from './matrix/chat-message';
import {
mapEventToAdminMessage,
mapEventToPostMessage,
mapMatrixMessage,
mapToLiveRoomEvent,
} from './matrix/chat-message';
import { ConversationStatus, Channel, User as UserModel } from '../../store/channels';
import { EditMessageOptions, Message, MessagesResponse } from '../../store/messages';
import { FileUploadResult } from '../../store/messages/saga';
Expand All @@ -45,6 +50,7 @@ import { encryptFile, getImageDimensions } from './matrix/media';
import { uploadAttachment } from '../../store/messages/api';
import { featureFlags } from '../feature-flags';
import { logger } from 'matrix-js-sdk/lib/logger';
import { PostsResponse } from '../../store/posts';

export const USER_TYPING_TIMEOUT = 5000; // 5s

Expand Down Expand Up @@ -374,6 +380,10 @@ export class MatrixClient implements IChatClient {
return mapEventToAdminMessage(event);
}
return null;

case CustomEventType.ROOM_POST:
return mapEventToPostMessage(event, this.matrix);

case EventType.RoomPowerLevels:
return mapEventToAdminMessage(event);
default:
Expand Down Expand Up @@ -429,17 +439,29 @@ export class MatrixClient implements IChatClient {
};
}

// Chat Messages
async getMessagesByChannelId(roomId: string, _lastCreatedAt?: number): Promise<MessagesResponse> {
await this.waitForConnection();
const room = this.matrix.getRoom(roomId);
const liveTimeline = room.getLiveTimeline();
const hasMore = await this.matrix.paginateEventTimeline(liveTimeline, { backwards: true, limit: 50 });

// For now, just return the full list again. Could filter out anything prior to lastCreatedAt
const messages = await this.getAllMessagesFromRoom(room);
const messages = await this.getAllChatMessagesFromRoom(room);
return { messages, hasMore };
}

// Post Messages
async getPostMessagesByChannelId(roomId: string, _lastCreatedAt?: number): Promise<PostsResponse> {
await this.waitForConnection();
const room = this.matrix.getRoom(roomId);
const liveTimeline = room.getLiveTimeline();
const hasMore = await this.matrix.paginateEventTimeline(liveTimeline, { backwards: true, limit: 50 });

const postMessages = await this.getAllPostMessagesFromRoom(room);
return { postMessages: postMessages, hasMore };
}

async getMessageByRoomId(channelId: string, messageId: string) {
await this.waitForConnection();
const newMessage = await this.matrix.fetchRoomEvent(channelId, messageId);
Expand Down Expand Up @@ -593,6 +615,23 @@ export class MatrixClient implements IChatClient {
};
}

async sendPostsByChannelId(channelId: string, message: string, optimisticId?: string): Promise<any> {
await this.waitForConnection();

const content = {
body: message,
msgtype: MsgType.Text,
optimisticId: optimisticId,
};

const postResult = await this.matrix.sendEvent(channelId, CustomEventType.ROOM_POST, content);

return {
id: postResult.event_id,
optimisticId,
};
}

async uploadFileMessage(roomId: string, media: File, rootMessageId: string = '', optimisticId = '') {
const isEncrypted = this.matrix.isRoomEncrypted(roomId);

Expand Down Expand Up @@ -954,6 +993,8 @@ export class MatrixClient implements IChatClient {
} else {
this.publishMessageEvent(event);
}
} else if (event.type === CustomEventType.ROOM_POST) {
this.publishPostEvent(event);
}
}

Expand Down Expand Up @@ -1145,6 +1186,10 @@ export class MatrixClient implements IChatClient {
this.events.receiveNewMessage(event.room_id, (await mapMatrixMessage(event, this.matrix)) as any);
}

private async publishPostEvent(event) {
this.events.receiveNewMessage(event.room_id, mapEventToPostMessage(event, this.matrix) as any);
}

private publishRoomNameChange = (room: Room) => {
this.events.onRoomNameChanged(room.roomId, this.getRoomName(room));
};
Expand Down Expand Up @@ -1299,13 +1344,28 @@ export class MatrixClient implements IChatClient {
return this.getLatestEvent(room, EventType.RoomCreate)?.getTs() || 0;
}

private async getAllMessagesFromRoom(room: Room): Promise<any[]> {
private async getAllChatMessagesFromRoom(room: Room): Promise<any[]> {
const events = room
.getLiveTimeline()
.getEvents()
.map((event) => event.getEffectiveEvent());

return await this.processRawEventsToMessages(events);
const chatMessageEvents = events.filter((event) => !this.isPostEvent(event));
return await this.processRawEventsToMessages(chatMessageEvents);
}

private async getAllPostMessagesFromRoom(room: Room): Promise<any[]> {
const events = room
.getLiveTimeline()
.getEvents()
.map((event) => event.getEffectiveEvent());

const postMessageEvents = events.filter((event) => this.isPostEvent(event));
return await this.processRawEventsToMessages(postMessageEvents);
}

private isPostEvent(event): boolean {
return event.type === CustomEventType.ROOM_POST;
}

// Performance improvement: Fetches only the latest user message to avoid processing large image files and other attachments
Expand Down
30 changes: 30 additions & 0 deletions src/lib/chat/matrix/chat-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,36 @@ export function mapEventToAdminMessage(matrixMessage): Message {
hidePreview: false,
preview: null,
sendStatus: MessageSendStatus.SUCCESS,
isPost: false,
};
}

export function mapEventToPostMessage(matrixMessage, sdkMatrixClient: SDKMatrixClient) {
const { event_id, content, origin_server_ts, sender: senderId } = matrixMessage;

const senderData = sdkMatrixClient.getUser(senderId);

return {
id: event_id,
message: content.body,
createdAt: origin_server_ts,
updatedAt: 0,
optimisticId: content.optimisticId,

sender: {
userId: senderId,
firstName: senderData?.displayName,
lastName: '',
profileImage: '',
profileId: '',
},

isAdmin: false,
mentionedUsers: [],
hidePreview: false,
preview: null,
sendStatus: MessageSendStatus.SUCCESS,
isPost: true,
};
}

Expand Down
1 change: 1 addition & 0 deletions src/lib/chat/matrix/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export enum MatrixConstants {
export enum CustomEventType {
USER_JOINED_INVITER_ON_ZERO = 'user_joined_inviter_on_zero',
GROUP_TYPE = 'm.room.group_type',
ROOM_POST = 'm.room.post',
}

export enum DecryptErrorConstants {
Expand Down
2 changes: 2 additions & 0 deletions src/store/channels/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export interface Channel {
otherMembers: User[];
memberHistory: User[];
hasMore: boolean;
hasMorePosts?: boolean;
createdAt: number;
lastMessage: Message;
unreadCount?: number;
Expand All @@ -75,6 +76,7 @@ export const CHANNEL_DEFAULTS = {
otherMembers: [],
memberHistory: [],
hasMore: true,
hasMorePosts: true,
createdAt: 0,
lastMessage: null,
unreadCount: 0,
Expand Down
1 change: 1 addition & 0 deletions src/store/channels/saga.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ const CHANNEL_DEFAULTS = {
otherMembers: [],
memberHistory: [],
hasMore: true,
hasMorePosts: true,
createdAt: 0,
lastMessage: null,
unreadCount: 0,
Expand Down
1 change: 1 addition & 0 deletions src/store/messages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export interface Message {
rootMessageId?: string;
sendStatus: MessageSendStatus;
readBy?: User[];
isPost: boolean;
}

export interface EditMessageOptions {
Expand Down
1 change: 1 addition & 0 deletions src/store/messages/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export function createOptimisticMessageObject(
preview: null,
media,
sendStatus: MessageSendStatus.IN_PROGRESS,
isPost: false,
};
}

Expand Down
Loading