Skip to content

Commit

Permalink
feat: Add link action to composer toolbar (#31679)
Browse files Browse the repository at this point in the history
Co-authored-by: Douglas Fabris <[email protected]>
  • Loading branch information
2 people authored and ricardogarim committed Mar 6, 2024
1 parent 6e3fe3a commit 3efefa3
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 23 deletions.
6 changes: 6 additions & 0 deletions .changeset/khaki-oranges-wink.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@rocket.chat/meteor": minor
"@rocket.chat/i18n": minor
---

Added a new formatter shortcut to add hyperlinks to a message
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Field, FieldGroup, TextInput, FieldLabel, FieldRow, Box } from '@rocket.chat/fuselage';
import { useUniqueId } from '@rocket.chat/fuselage-hooks';
import { useTranslation } from '@rocket.chat/ui-contexts';
import React, { useEffect } from 'react';
import { useForm, Controller } from 'react-hook-form';

import GenericModal from '../../../../client/components/GenericModal';

type AddLinkComposerActionModalProps = {
selectedText?: string;
onConfirm: (url: string, text: string) => void;
onClose: () => void;
};

const AddLinkComposerActionModal = ({ selectedText, onClose, onConfirm }: AddLinkComposerActionModalProps) => {
const t = useTranslation();
const textField = useUniqueId();
const urlField = useUniqueId();

const { handleSubmit, setFocus, control } = useForm({
mode: 'onBlur',
defaultValues: {
text: selectedText || '',
url: '',
},
});

useEffect(() => {
setFocus(selectedText ? 'url' : 'text');
}, [selectedText, setFocus]);

const onClickConfirm = ({ url, text }: { url: string; text: string }) => {
onConfirm(url, text);
};

const submit = handleSubmit(onClickConfirm);

return (
<GenericModal
variant='warning'
icon={null}
confirmText={t('Add')}
onCancel={onClose}
wrapperFunction={(props) => <Box is='form' onSubmit={(e) => void submit(e)} {...props} />}
title={t('Add_link')}
>
<FieldGroup>
<Field>
<FieldLabel htmlFor={textField}>{t('Text')}</FieldLabel>
<FieldRow>
<Controller control={control} name='text' render={({ field }) => <TextInput autoComplete='off' id={textField} {...field} />} />
</FieldRow>
</Field>
<Field>
<FieldLabel htmlFor={urlField}>{t('URL')}</FieldLabel>
<FieldRow>
<Controller control={control} name='url' render={({ field }) => <TextInput autoComplete='off' id={urlField} {...field} />} />
</FieldRow>
</Field>
</FieldGroup>
</GenericModal>
);
};

export default AddLinkComposerActionModal;
Original file line number Diff line number Diff line change
@@ -1,24 +1,34 @@
import type { Keys as IconName } from '@rocket.chat/icons';
import type { TranslationKey } from '@rocket.chat/ui-contexts';

import type { ComposerAPI } from '../../../../client/lib/chats/ChatAPI';
import { imperativeModal } from '../../../../client/lib/imperativeModal';
import { settings } from '../../../settings/client';
import AddLinkComposerActionModal from './AddLinkComposerActionModal';

export type FormattingButton =
| {
label: TranslationKey;
icon: IconName;
pattern: string;
// text?: () => string | undefined;
command?: string;
link?: string;
condition?: () => boolean;
}
| {
label: TranslationKey;
text: () => string | undefined;
link: string;
condition?: () => boolean;
};
type FormattingButtonDefault = { label: TranslationKey; condition?: () => boolean };

type TextButton = {
text: () => string | undefined;
link: string;
} & FormattingButtonDefault;

type PatternButton = {
icon: IconName;
pattern: string;
// text?: () => string | undefined;
command?: string;
link?: string;
} & FormattingButtonDefault;

type PromptButton = {
prompt: (composer: ComposerAPI) => void;
icon: IconName;
} & FormattingButtonDefault;

export type FormattingButton = PatternButton | PromptButton | TextButton;

export const isPromptButton = (button: FormattingButton): button is PromptButton => 'prompt' in button;

export const formattingButtons: ReadonlyArray<FormattingButton> = [
{
Expand Down Expand Up @@ -48,6 +58,28 @@ export const formattingButtons: ReadonlyArray<FormattingButton> = [
icon: 'multiline',
pattern: '```\n{{text}}\n``` ',
},
{
label: 'Link',
icon: 'link',
prompt: (composerApi: ComposerAPI) => {
const { selection } = composerApi;

const selectedText = composerApi.substring(selection.start, selection.end);

const onClose = () => {
imperativeModal.close();
composerApi.focus();
};

const onConfirm = (url: string, text: string) => {
onClose();
composerApi.replaceText(`[${text}](${url})`, selection);
composerApi.setCursorToEnd();
};

imperativeModal.open({ component: AddLinkComposerActionModal, props: { onConfirm, selectedText, onClose } });
},
},
{
label: 'KaTeX' as TranslationKey,
icon: 'katex',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useTranslation } from '@rocket.chat/ui-contexts';
import React from 'react';

import type { FormattingButton } from '../../../../../../app/ui-message/client/messageBox/messageBoxFormatting';
import { isPromptButton, type FormattingButton } from '../../../../../../app/ui-message/client/messageBox/messageBoxFormatting';
import GenericMenu from '../../../../../components/GenericMenu/GenericMenu';
import type { GenericMenuItemProps } from '../../../../../components/GenericMenu/GenericMenuItem';
import type { ComposerAPI } from '../../../../../lib/chats/ChatAPI';
Expand All @@ -21,6 +21,9 @@ const FormattingToolbarDropdown = ({ composer, items, disabled }: FormattingTool
window.open(formatter.link, '_blank', 'rel=noreferrer noopener');
return;
}
if (isPromptButton(formatter)) {
return formatter.prompt(composer);
}
composer.wrapSelection(formatter.pattern);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useTranslation } from '@rocket.chat/ui-contexts';
import React, { memo } from 'react';

import type { FormattingButton } from '../../../../../../app/ui-message/client/messageBox/messageBoxFormatting';
import { isPromptButton } from '../../../../../../app/ui-message/client/messageBox/messageBoxFormatting';
import type { ComposerAPI } from '../../../../../lib/chats/ChatAPI';
import FormattingToolbarDropdown from './FormattingToolbarDropdown';

Expand All @@ -24,7 +25,9 @@ const MessageBoxFormattingToolbar = ({ items, variant = 'large', composer, disab
<>
{'icon' in featuredFormatter && (
<MessageComposerAction
onClick={() => composer.wrapSelection(featuredFormatter.pattern)}
onClick={() =>
isPromptButton(featuredFormatter) ? featuredFormatter.prompt(composer) : composer.wrapSelection(featuredFormatter.pattern)
}
icon={featuredFormatter.icon}
disabled={disabled}
/>
Expand All @@ -45,6 +48,10 @@ const MessageBoxFormattingToolbar = ({ items, variant = 'large', composer, disab
data-id={formatter.label}
title={t(formatter.label)}
onClick={(): void => {
if (isPromptButton(formatter)) {
formatter.prompt(composer);
return;
}
if ('link' in formatter) {
window.open(formatter.link, '_blank', 'rel=noreferrer noopener');
return;
Expand Down
22 changes: 17 additions & 5 deletions apps/meteor/tests/e2e/message-composer.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { faker } from '@faker-js/faker';

import { Users } from './fixtures/userStates';
import { HomeChannel } from './page-objects';
import { createTargetChannel } from './utils';
Expand All @@ -23,21 +25,18 @@ test.describe.serial('message-composer', () => {
await poHomeChannel.sidenav.openChat(targetChannel);
await poHomeChannel.content.sendMessage('hello composer');

await expect(poHomeChannel.composerToolbarActions).toHaveCount(11);
await expect(poHomeChannel.composerToolbarActions).toHaveCount(12);
});

test('should have only the main formatter and the main action', async ({ page }) => {
await page.setViewportSize({ width: 768, height: 600 });

await poHomeChannel.sidenav.openChat(targetChannel);
await poHomeChannel.content.sendMessage('hello composer');

await expect(poHomeChannel.composerToolbarActions).toHaveCount(5);
});

test('should navigate on toolbar using arrow keys', async ({ page }) => {
await poHomeChannel.sidenav.openChat(targetChannel);
await poHomeChannel.content.sendMessage('hello composer');

await page.keyboard.press('Tab');
await page.keyboard.press('ArrowRight');
Expand All @@ -50,11 +49,24 @@ test.describe.serial('message-composer', () => {

test('should move the focus away from toolbar using tab key', async ({ page }) => {
await poHomeChannel.sidenav.openChat(targetChannel);
await poHomeChannel.content.sendMessage('hello composer');

await page.keyboard.press('Tab');
await page.keyboard.press('Tab');

await expect(poHomeChannel.composerToolbar.getByRole('button', { name: 'Emoji' })).not.toBeFocused();
});

test('should add a link to the selected text', async ({ page }) => {
const url = faker.internet.url();
await poHomeChannel.sidenav.openChat(targetChannel);

await page.keyboard.type('hello composer');
await page.keyboard.press('Control+A'); // on Windows and Linux
await page.keyboard.press('Meta+A'); // on macOS
await poHomeChannel.composerToolbar.getByRole('button', { name: 'Link' }).click()
await page.keyboard.type(url);
await page.keyboard.press('Enter');

await expect(poHomeChannel.composer).toHaveValue(`[hello composer](${url})`);
});
});
2 changes: 2 additions & 0 deletions packages/i18n/src/locales/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@
"Add_files_from": "Add files from",
"Add_manager": "Add manager",
"Add_monitor": "Add monitor",
"Add_link": "Add link",
"Add_Reaction": "Add reaction",
"Add_Role": "Add Role",
"Add_Sender_To_ReplyTo": "Add Sender to Reply-To",
Expand Down Expand Up @@ -5108,6 +5109,7 @@
"test-push-notifications": "Test push notifications",
"test-push-notifications_description": "Permission to test push notifications",
"Texts": "Texts",
"Text": "Text",
"Thank_you_for_your_feedback": "Thank you for your feedback",
"The_application_name_is_required": "The application name is required",
"The_application_will_be_able_to": "<1>{{appName}}</1> will be able to:",
Expand Down

0 comments on commit 3efefa3

Please sign in to comment.