Skip to content

Commit

Permalink
chore(): mention only one user per block (#2410)
Browse files Browse the repository at this point in the history
  • Loading branch information
didd committed Sep 20, 2024
1 parent bd880ea commit 48b6d27
Show file tree
Hide file tree
Showing 11 changed files with 117 additions and 111 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ export const SlateEditorBlock = (
transformSource={transformSource}
handleDisablePublish={props.blockInfo?.externalHandler}
encodingFunction={encodeSlateToBase64}
mentionsLimit={{ count: 1, label: t('Only one person can be mentioned per block.') }}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function getEditProfileMocks({ profileDID, nsfw }: IGetProfileInfoMocks)
variableMatcher: () => true,
result: {
data: {
createAkashaProfile: {
setAkashaProfile: {
document: profileData,
clientMutationId: '',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,15 @@ export const FollowButton = ({

const [createFollowMutation, { loading: createFollowLoading }] = useCreateFollowMutation({
context: { source: sdk.services.gql.contextSources.composeDB },
onQueryUpdated: observableQuery => {
/*
** When creating a new follow document a cache is created for it in memory and the object doesn't contain
** profileID(stream id of a profile) field as such named field isn't available in the mutation result.
** Therefore, when the query associated with this mutation is executed since it uses a variable field named profileID to fetch data, it won't find it in the cache and returns null.
** Hence, the data this component receives is stale which requires a refetch to include the missing profileID in the cache.
**/
return observableQuery.refetch();
},
onCompleted: async ({ setAkashaFollow }) => {
const document = setAkashaFollow.document;
if (iconOnly) sendSuccessNotification(document.profile?.name, isFollowing);
Expand Down
11 changes: 10 additions & 1 deletion extensions/jest.setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import * as useAnalytics from '@akashaorg/ui-awf-hooks/lib/use-analytics';
import { genAppProps, getUserInfo, getAuthenticationStore } from '@akashaorg/af-testing';
import { install } from '@twind/core';
import '@testing-library/jest-dom';
import getSdk from '@akashaorg/core-sdk';

install(twindConfig);

Expand Down Expand Up @@ -75,6 +74,16 @@ jest.mock('@akashaorg/core-sdk', () => () => {
composeDB: Symbol.for('composeDB'),
default: Symbol.for('defaultContextSource'),
},
getAPI: () => ({
GetAppsByPublisherDID: () =>
Promise.resolve({
node: {
akashaAppList: {
edges: [{ node: { id: 'id', releases: { edges: [{ node: { id: 'id' } }] } } }],
},
},
}),
}),
},
common: {
misc: {
Expand Down
47 changes: 46 additions & 1 deletion libs/design-system-components/src/components/Editor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import Button from '@akashaorg/design-system-core/lib/components/Button';
import Stack from '@akashaorg/design-system-core/lib/components/Stack';
import Icon from '@akashaorg/design-system-core/lib/components/Icon';
import Text from '@akashaorg/design-system-core/lib/components/Text';
import MessageCard from '@akashaorg/design-system-core/lib/components/MessageCard';

import {
BoldAlt,
Expand Down Expand Up @@ -48,6 +49,8 @@ import { MarkButton, BlockButton } from './formatting-buttons';

const MAX_TEXT_LENGTH = 500;

type Node = Descendant | { children: Descendant[] };

export type EditorBoxProps = {
avatar?: Profile['avatar'];
showAvatar?: boolean;
Expand All @@ -73,6 +76,7 @@ export type EditorBoxProps = {
showPostButton?: boolean;
// this is to account for the limitations on the ceramic storage side
maxEncodedLength?: number;
mentionsLimit?: { count: number; label: string };
customStyle?: string;
onPublish?: (publishData: IPublishData) => void;
onClear?: () => void;
Expand Down Expand Up @@ -125,6 +129,7 @@ const EditorBox: React.FC<EditorBoxProps> = props => {
showPostButton = true,
transformSource,
maxEncodedLength = 6000,
mentionsLimit,
customStyle = '',
handleDisablePublish,
encodingFunction,
Expand All @@ -136,6 +141,7 @@ const EditorBox: React.FC<EditorBoxProps> = props => {
const [tagTargetRange, setTagTargetRange] = useState<Range | null>(null);
const [index, setIndex] = useState(0);
const [createTag, setCreateTag] = useState('');
const [mentionsLimitReached, setMentionsLimitReached] = useState(false);

const [letterCount, setLetterCount] = useState(0);

Expand Down Expand Up @@ -237,7 +243,7 @@ const EditorBox: React.FC<EditorBoxProps> = props => {
* wrap slateContent in object to make recursive getMetadata work
*/
const initContent: { children: Descendant[] } = { children: slateContent };
(function getMetadata(node: Descendant | { children: Descendant[] }) {
(function getMetadata(node: Node) {
if (Element.isElement(node) && node.type === 'mention') {
metadata.mentions.push(node.id);
}
Expand All @@ -255,6 +261,19 @@ const EditorBox: React.FC<EditorBoxProps> = props => {
onPublish(data);
};

const countMentions = (node: Node) => {
let count = 0;
(function getCount(node: Node) {
if (Element.isElement(node) && node.type === 'mention') {
count++;
}
if (Element.isElement(node) && node.children) {
node.children.map((n: Descendant) => getCount(n));
}
})(node);
return count;
};

/**
* computes the text length
* sets the editor state
Expand Down Expand Up @@ -340,7 +359,21 @@ const EditorBox: React.FC<EditorBoxProps> = props => {
const afterText = Editor.string(editor, afterRange);
const afterMatch = afterText.match(/^(\s|$)/);

if (!beforeMentionMatch && mentionsLimitReached) {
setMentionsLimitReached(false);
}

if (beforeMentionMatch && afterMatch && beforeRange && typeof getMentions === 'function') {
if (mentionsLimit) {
if (
countMentions({
children: editorState,
}) === mentionsLimit.count
) {
setMentionsLimitReached(true);
return;
}
}
setMentionTargetRange(beforeRange);
getMentions(beforeMentionMatch[1]);
setIndex(0);
Expand Down Expand Up @@ -522,6 +555,18 @@ const EditorBox: React.FC<EditorBoxProps> = props => {
)}
{/* w-0 min-w-full is used to prevent parent width expansion without setting a fixed width */}
<Stack ref={editorContainerRef} customStyle="w-0 min-w-full">
{mentionsLimitReached && (
<MessageCard
message={mentionsLimit.label}
icon={
<Icon
icon={<ExclamationTriangleIcon />}
color={{ light: 'warningLight', dark: 'warningLight' }}
/>
}
background={{ light: 'warningDark/30', dark: 'warningDark/30' }}
/>
)}
<Slate editor={editor} value={editorState || editorDefaultValue} onChange={handleChange}>
<Editable
placeholder={placeholderLabel}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { tw } from '@twind/core';
const MentionElement = (props: any) => {
const { handleMentionClick, attributes, element, children } = props;
const mention = element.name || element.did;
const displayedMention = mention && mention.startsWith('@') ? mention : `@${mention}`;
const displayedMention = `${mention && mention.startsWith('@') ? mention : `@${mention}`} `;
return (
<button
className={tw(`text-secondaryLight dark:text-secondaryDark text-${element.align}`)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,43 @@
import React from 'react';
import Icon from '@akashaorg/design-system-core/lib/components/Icon';
import type { Meta, StoryObj } from '@storybook/react';
import MessageCard, {
MessageCardProps,
} from '@akashaorg/design-system-core/lib/components/MessageCard';
import { ExclamationTriangleIcon } from '@akashaorg/design-system-core/lib/components/Icon/hero-icons-outline';

MessageCard.displayName = 'MessageCard';

const meta: Meta<MessageCardProps> = {
title: 'DSCore/Cards/MessageCard',
component: MessageCard,
argTypes: {
title: { control: 'text' },
message: { control: 'text' },
titleIcon: { control: 'object' },
titleVariant: { control: 'text' },
elevation: { control: 'text' },
background: { control: 'text' },
borderColor: { control: 'text' },
icon: { control: 'object' },
background: { control: 'object' },
customStyle: { control: 'text' },
onClose: { action: 'card closed' },
},
};

type Story = StoryObj<MessageCardProps>;

export const Default: Story = {
args: { title: 'Title', elevation: '1', message: 'A sample message...' },
args: {
message: 'A sample message...',
},
};

export const MessageCardWithIcon: Story = {
args: {
icon: (
<Icon
icon={<ExclamationTriangleIcon />}
color={{ light: 'warningLight', dark: 'warningLight' }}
/>
),
message: 'A sample message...',
background: { light: 'warningDark/30', dark: 'warningDark/30' },
},
};

export default meta;
Original file line number Diff line number Diff line change
@@ -1,52 +1,13 @@
import * as React from 'react';
import { act, fireEvent } from '@testing-library/react';
import MessageCard from '../';
import { screen } from '@testing-library/react';
import { customRender } from '../../../test-utils';

describe('<MessageCard /> Component', () => {
let componentWrapper = customRender(<></>, {});

const title = 'Title';
const message = 'Message';

const handleClose = jest.fn();

beforeEach(() => {
act(() => {
componentWrapper = customRender(
<MessageCard title={title} message={message} elevation="1" onClose={handleClose} />,
{},
);
});
});

afterEach(() => {
jest.clearAllMocks();
});

it('renders correctly', () => {
expect(componentWrapper).toBeDefined();
});

it('has correct title and message', () => {
const { getByText } = componentWrapper;

const titleLabel = getByText(title);
const messageLabel = getByText(message);

expect(titleLabel).toBeDefined();
expect(messageLabel).toBeDefined();
});

it('calls handler when clicked', () => {
const { getByRole } = componentWrapper;

const closeButton = getByRole('button', { name: 'close' });
expect(closeButton).toBeDefined();
expect(handleClose).toBeCalledTimes(0);

fireEvent.click(closeButton);

expect(handleClose).toBeCalledTimes(1);
customRender(<MessageCard message={message} />, {});
expect(screen.getByText(message)).toBeInTheDocument();
});
});
77 changes: 20 additions & 57 deletions libs/design-system-core/src/components/MessageCard/index.tsx
Original file line number Diff line number Diff line change
@@ -1,87 +1,50 @@
import React from 'react';

import Button from '../Button';
import Card from '../Card';
import Icon from '../Icon';
import { XMarkIcon } from '../Icon/hero-icons-outline';
import Stack from '../Stack';
import Text, { TextProps } from '../Text';

import { Color, Elevation } from '../types/common.types';
import { getColorClasses } from '../../utils';
import Text from '../Text';
import { Color } from '../types/common.types';

export type MessageCardProps = {
title: string;
message: string;
titleVariant?: TextProps['variant'];
titleIcon?: React.ReactElement;
icon?: React.ReactElement;
background?: Color;
elevation?: Elevation;
borderColor?: Color;
dataTestId?: string;
customStyle?: string;
onClose: () => void;
};

/**
* A MessageCard component is useful for displaying simple notifications in your app. The user
* can opt to close it by clicking on the close icon.
* @param title - string
* A MessageCard component is useful for displaying simple notifications in an app.
* @param message - string
* @param titleVariant - (optional) customize the text variant for the title
* @param titleIcon - (optional) include an icon for the title if you want
* @param icon - (optional) include an icon
* @param background - (optional) customize the background color
* @param elevation - (optional) customize the elevation property of the card
* @param borderColor - (optional) customize the border color
* @param dataTestId - (optional) useful when writing test
* @param customStyle - (optional) apply your custom styling (Make sure to use standard Tailwind classes)
* @param onClose - (optional) handler when clicking the close icon
* @example
* ```tsx
* <MessageCard title='Title' elevation='1' message='A sample message...' />
* <MessageCard message='A sample message...' />
* ```
**/
const MessageCard: React.FC<MessageCardProps> = ({
title,
titleVariant = 'button-sm',
titleIcon,
background = 'white',
elevation = 'none',
borderColor,
message,
icon,
background,
dataTestId,
customStyle = '',
message,
onClose,
}) => {
const borderStyle = borderColor ? `border ${getColorClasses(borderColor, 'border')}` : '';

return (
<Card
elevation={elevation}
<Stack
direction="row"
spacing="gap-1"
align="center"
background={background}
radius={20}
padding={'p-4'}
padding="p-2"
dataTestId={dataTestId}
customStyle={`${borderStyle} ${customStyle}`}
customStyle={`rounded ${customStyle}`}
>
<Stack direction="column" spacing="gap-y-2">
<Stack direction="row" justify="between">
<Stack align="center" justify="center" spacing="gap-x-1" fullWidth>
{titleIcon && <Icon icon={titleIcon} size="sm" />}
<Text variant={titleVariant} align="center">
{title}
</Text>
</Stack>

<Button onClick={onClose} plain aria-label="close">
<Icon icon={<XMarkIcon />} size="sm" />
</Button>
</Stack>
<Text variant="footnotes2" weight="normal">
{message}
</Text>
</Stack>
</Card>
{icon}
<Text variant="footnotes2" weight="normal">
{message}
</Text>
</Stack>
);
};

Expand Down
Loading

0 comments on commit 48b6d27

Please sign in to comment.