Skip to content

Commit

Permalink
feat: add support for threads in federated rooms (#29655)
Browse files Browse the repository at this point in the history
Co-authored-by: Guilherme Gazzo <guilhermegazzo@gmail.com>
  • Loading branch information
MarcosSpessatto and ggazzo committed Aug 22, 2023
1 parent a81bad2 commit f83ea5d
Show file tree
Hide file tree
Showing 31 changed files with 2,038 additions and 342 deletions.
5 changes: 5 additions & 0 deletions .changeset/fluffy-beds-buy.md
@@ -0,0 +1,5 @@
---
"@rocket.chat/meteor": minor
---

Added support for threaded conversation in Federated rooms.
Expand Up @@ -16,7 +16,7 @@ Meteor.startup(() => {
id: 'reply-in-thread',
icon: 'thread',
label: 'Reply_in_thread',
context: ['message', 'message-mobile', 'videoconf'],
context: ['message', 'message-mobile', 'federated', 'videoconf'],
action(e, props) {
const { message = messageArgs(this).msg } = props;
e.stopPropagation();
Expand Down
11 changes: 2 additions & 9 deletions apps/meteor/client/hooks/roomActions/useThreadRoomAction.tsx
@@ -1,11 +1,10 @@
import { isRoomFederated } from '@rocket.chat/core-typings';
import type { BadgeProps } from '@rocket.chat/fuselage';
import { HeaderToolboxAction, HeaderToolboxActionBadge } from '@rocket.chat/ui-client';
import { useSetting } from '@rocket.chat/ui-contexts';
import React, { lazy, useMemo } from 'react';
import { useTranslation } from 'react-i18next';

import { useRoom, useRoomSubscription } from '../../views/room/contexts/RoomContext';
import { useRoomSubscription } from '../../views/room/contexts/RoomContext';
import type { RoomToolboxActionConfig } from '../../views/room/contexts/RoomToolboxContext';

const getVariant = (tunreadUser: number, tunreadGroup: number): BadgeProps['variant'] => {
Expand All @@ -24,8 +23,6 @@ const Threads = lazy(() => import('../../views/room/contextualBar/Threads'));

export const useThreadRoomAction = () => {
const enabled = useSetting('Threads_enabled', false);
const room = useRoom();
const federated = isRoomFederated(room);
const subscription = useRoomSubscription();

const tunread = subscription?.tunread?.length ?? 0;
Expand All @@ -47,10 +44,6 @@ export const useThreadRoomAction = () => {
title: 'Threads',
icon: 'thread',
tabComponent: Threads,
...(federated && {
tooltip: t('core.Threads_unavailable_for_federation'),
disabled: true,
}),
order: 2,
renderToolboxItem: ({ id, className, index, icon, title, toolbox: { tab }, action, disabled, tooltip }) => (
<HeaderToolboxAction
Expand All @@ -69,5 +62,5 @@ export const useThreadRoomAction = () => {
</HeaderToolboxAction>
),
};
}, [enabled, federated, t, unread, variant]);
}, [enabled, t, unread, variant]);
};
Expand Up @@ -43,12 +43,18 @@ interface IFederationChangeMembershipInputDto extends IFederationReceiverBaseRoo
}[];
}

interface IFederationThread {
rootEventId: string;
replyToEventId: string;
}

interface IFederationSendInternalMessageInputDto extends IFederationReceiverBaseRoomInputDto {
externalSenderId: string;
normalizedSenderId: string;
rawMessage: string;
externalFormattedText: string;
replyToEventId?: string;
thread?: IFederationThread;
}

interface IFederationRoomChangeJoinRulesDtoInputDto extends IFederationReceiverBaseRoomInputDto {
Expand Down Expand Up @@ -218,6 +224,7 @@ export class FederationRoomReceiveExternalMessageDto extends ExternalMessageBase
rawMessage,
externalEventId,
replyToEventId,
thread,
}: IFederationSendInternalMessageInputDto) {
super({ externalRoomId, normalizedRoomId });
this.externalSenderId = externalSenderId;
Expand All @@ -226,6 +233,7 @@ export class FederationRoomReceiveExternalMessageDto extends ExternalMessageBase
this.rawMessage = rawMessage;
this.replyToEventId = replyToEventId;
this.externalEventId = externalEventId;
this.thread = thread;
}

externalSenderId: string;
Expand All @@ -237,6 +245,11 @@ export class FederationRoomReceiveExternalMessageDto extends ExternalMessageBase
rawMessage: string;

replyToEventId?: string;

thread?: {
rootEventId: string;
replyToEventId: string;
};
}

export class FederationRoomEditExternalMessageDto extends ExternalMessageBaseDto {
Expand Down Expand Up @@ -280,6 +293,10 @@ interface IFederationFileMessageInputDto {
messageText: string;
url: string;
replyToEventId?: string;
thread?: {
rootEventId: string;
replyToEventId: string;
};
}

class FederationFileMessageInputDto {
Expand Down Expand Up @@ -315,12 +332,14 @@ export class FederationRoomReceiveExternalFileMessageDto extends ExternalMessage
url,
externalEventId,
replyToEventId,
thread,
}: IFederationSendInternalMessageBaseInputDto & IFederationFileMessageInputDto) {
super({ externalRoomId, normalizedRoomId, externalEventId });
this.externalSenderId = externalSenderId;
this.normalizedSenderId = normalizedSenderId;
this.replyToEventId = replyToEventId;
this.messageBody = new FederationFileMessageInputDto({ filename, mimetype, size, messageText, url });
this.thread = thread;
}

externalSenderId: string;
Expand All @@ -330,6 +349,8 @@ export class FederationRoomReceiveExternalFileMessageDto extends ExternalMessage
messageBody: FederationFileMessageInputDto;

replyToEventId?: string;

thread?: IFederationThread;
}

export class FederationRoomChangeJoinRulesDto extends FederationBaseRoomInputDto {
Expand Down
Expand Up @@ -14,6 +14,7 @@ interface IFederationCreateDMAndInviteUserDto extends IFederationSenderBaseRoomI
interface IFederationRoomSendExternalMessageDto extends IFederationSenderBaseRoomInputDto {
message: IMessage;
internalSenderId: string;
isThreadedMessage: boolean;
}

interface IFederationAfterLeaveRoomDto extends IFederationSenderBaseRoomInputDto {
Expand Down Expand Up @@ -58,14 +59,17 @@ export class FederationCreateDMAndInviteUserDto extends FederationSenderBaseRoom
}

export class FederationRoomSendExternalMessageDto extends FederationSenderBaseRoomInputDto {
constructor({ internalRoomId, internalSenderId, message }: IFederationRoomSendExternalMessageDto) {
constructor({ internalRoomId, internalSenderId, message, isThreadedMessage }: IFederationRoomSendExternalMessageDto) {
super({ internalRoomId });
this.internalSenderId = internalSenderId;
this.message = message;
this.isThreadedMessage = isThreadedMessage;
}

internalSenderId: string;

isThreadedMessage: boolean;

message: IMessage;
}

Expand Down
Expand Up @@ -50,7 +50,7 @@ class FileExternalMessageSender implements IExternalMessageSender {

public async sendMessage(externalRoomId: string, externalSenderId: string, message: IMessage): Promise<void> {
const file = await this.internalFileHelper.getFileRecordById((message.files || [])[0]?._id);
if (!file || !file.size || !file.type) {
if (!file?.size || !file.type) {
return;
}

Expand Down Expand Up @@ -78,7 +78,7 @@ class FileExternalMessageSender implements IExternalMessageSender {
messageToReplyTo: IMessage,
): Promise<void> {
const file = await this.internalFileHelper.getFileRecordById((message.files || [])[0]?._id);
if (!file || !file.size || !file.type) {
if (!file?.size || !file.type) {
return;
}

Expand Down Expand Up @@ -106,14 +106,167 @@ class FileExternalMessageSender implements IExternalMessageSender {
}
}

export const getExternalMessageSender = (
message: IMessage,
bridge: IFederationBridge,
internalFileHelper: RocketChatFileAdapter,
internalMessageAdapter: RocketChatMessageAdapter,
internalUserAdapter: RocketChatUserAdapter,
): IExternalMessageSender => {
class ThreadTextExternalMessageSender implements IExternalMessageSender {
constructor(
private readonly bridge: IFederationBridge,
private readonly internalMessageAdapter: RocketChatMessageAdapter,
private readonly internalUserAdapter: RocketChatUserAdapter,
) {}

public async sendMessage(externalRoomId: string, externalSenderId: string, message: IMessage): Promise<void> {
if (!message.tmid) {
return;
}
const parentMessage = await this.internalMessageAdapter.getMessageById(message.tmid);
if (!parentMessage?.federation?.eventId) {
return;
}
const externalMessageId = await this.bridge.sendThreadMessage(
externalRoomId,
externalSenderId,
message,
parentMessage.federation.eventId,
);

await this.internalMessageAdapter.setExternalFederationEventOnMessage(message._id, externalMessageId);
}

public async sendQuoteMessage(
externalRoomId: string,
externalSenderId: string,
message: IMessage,
messageToReplyTo: IMessage,
): Promise<void> {
if (!message.tmid) {
return;
}
const parentMessage = await this.internalMessageAdapter.getMessageById(message.tmid);
if (!parentMessage?.federation?.eventId) {
return;
}

const originalSender = await this.internalUserAdapter.getFederatedUserByInternalId(messageToReplyTo?.u?._id);
const externalMessageId = await this.bridge.sendThreadReplyToMessage(
externalRoomId,
externalSenderId,
messageToReplyTo.federation?.eventId as string,
originalSender?.getExternalId() as string,
message.msg,
parentMessage.federation.eventId,
);
await this.internalMessageAdapter.setExternalFederationEventOnMessage(message._id, externalMessageId);
}
}

class ThreadFileExternalMessageSender implements IExternalMessageSender {
constructor(
private readonly bridge: IFederationBridge,
private readonly internalFileHelper: RocketChatFileAdapter,
private readonly internalMessageAdapter: RocketChatMessageAdapter,
) {}

public async sendMessage(externalRoomId: string, externalSenderId: string, message: IMessage): Promise<void> {
const file = await this.internalFileHelper.getFileRecordById((message.files || [])[0]?._id);
if (!file?.size || !file.type || !file.name) {
return;
}

if (!message.tmid) {
return;
}
const parentMessage = await this.internalMessageAdapter.getMessageById(message.tmid);
if (!parentMessage?.federation?.eventId) {
return;
}

const buffer = await this.internalFileHelper.getBufferFromFileRecord(file);
const metadata = await this.internalFileHelper.extractMetadataFromFile(file);

const externalMessageId = await this.bridge.sendMessageFileToThread(
externalRoomId,
externalSenderId,
buffer,
{
filename: file.name,
fileSize: file.size,
mimeType: file.type,
metadata: {
width: metadata?.width,
height: metadata?.height,
format: metadata?.format,
},
},
parentMessage.federation.eventId,
);

await this.internalMessageAdapter.setExternalFederationEventOnMessage(message._id, externalMessageId);
}

public async sendQuoteMessage(
externalRoomId: string,
externalSenderId: string,
message: IMessage,
messageToReplyTo: IMessage,
): Promise<void> {
const file = await this.internalFileHelper.getFileRecordById((message.files || [])[0]?._id);
if (!file?.size || !file.type || !file.name) {
return;
}

if (!message.tmid) {
return;
}
const parentMessage = await this.internalMessageAdapter.getMessageById(message.tmid);
if (!parentMessage?.federation?.eventId) {
return;
}

const buffer = await this.internalFileHelper.getBufferFromFileRecord(file);
const metadata = await this.internalFileHelper.extractMetadataFromFile(file);

const externalMessageId = await this.bridge.sendReplyMessageFileToThread(
externalRoomId,
externalSenderId,
buffer,
{
filename: file.name,
fileSize: file.size,
mimeType: file.type,
metadata: {
width: metadata?.width,
height: metadata?.height,
format: metadata?.format,
},
},
messageToReplyTo.federation?.eventId as string,
parentMessage.federation.eventId,
);

await this.internalMessageAdapter.setExternalFederationEventOnMessage(message._id, externalMessageId);
}
}

export const getExternalMessageSender = ({
message,
bridge,
internalFileAdapter,
internalMessageAdapter,
internalUserAdapter,
isThreadedMessage,
}: {
message: IMessage;
isThreadedMessage: boolean;
bridge: IFederationBridge;
internalFileAdapter: RocketChatFileAdapter;
internalMessageAdapter: RocketChatMessageAdapter;
internalUserAdapter: RocketChatUserAdapter;
}): IExternalMessageSender => {
if (isThreadedMessage) {
return message.files
? new ThreadFileExternalMessageSender(bridge, internalFileAdapter, internalMessageAdapter)
: new ThreadTextExternalMessageSender(bridge, internalMessageAdapter, internalUserAdapter);
}
return message.files
? new FileExternalMessageSender(bridge, internalFileHelper, internalMessageAdapter)
? new FileExternalMessageSender(bridge, internalFileAdapter, internalMessageAdapter)
: new TextExternalMessageSender(bridge, internalMessageAdapter, internalUserAdapter);
};

0 comments on commit f83ea5d

Please sign in to comment.