Skip to content

Commit 7780290

Browse files
MartinCupelaarnautov-anton
authored andcommitted
feat: add MessageComposer
1 parent 6bb0e4c commit 7780290

38 files changed

+2278
-451
lines changed

src/components/AutoCompleteTextarea/Item.jsx

Lines changed: 0 additions & 37 deletions
This file was deleted.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import React, { MouseEventHandler, Ref, useCallback } from 'react';
2+
import clsx from 'clsx';
3+
import { SuggestionItemProps } from '../ChatAutoComplete';
4+
import type { DefaultStreamChatGenerics } from '../../types';
5+
import type { UnknownType } from '../../types/types';
6+
7+
export const Item = React.forwardRef<HTMLAnchorElement, SuggestionItemProps>(
8+
function Item<
9+
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
10+
T extends UnknownType = UnknownType,
11+
>(props: SuggestionItemProps<StreamChatGenerics, T>, innerRef: Ref<HTMLAnchorElement>) {
12+
const {
13+
className,
14+
component: Component,
15+
item,
16+
onClickHandler,
17+
onSelectHandler,
18+
selected,
19+
style,
20+
} = props;
21+
22+
const handleSelect = useCallback(
23+
() => onSelectHandler(item),
24+
[item, onSelectHandler],
25+
);
26+
const handleClick: MouseEventHandler = useCallback(
27+
(event) => onClickHandler(event, item),
28+
[item, onClickHandler],
29+
);
30+
31+
return (
32+
<li
33+
className={clsx(className, { 'str-chat__suggestion-item--selected': selected })}
34+
style={style}
35+
>
36+
<a
37+
href=''
38+
onClick={handleClick}
39+
onFocus={handleSelect}
40+
onMouseEnter={handleSelect}
41+
ref={innerRef}
42+
>
43+
<Component entity={item} selected={selected} />
44+
</a>
45+
</li>
46+
);
47+
},
48+
);

src/components/AutoCompleteTextarea/List.jsx renamed to src/components/AutoCompleteTextarea/List.tsx

Lines changed: 56 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,31 @@
1-
import React, { useCallback, useEffect, useMemo, useState } from 'react';
1+
import React, {
2+
CSSProperties,
3+
MouseEventHandler,
4+
useCallback,
5+
useEffect,
6+
useMemo,
7+
useState,
8+
} from 'react';
29
import clsx from 'clsx';
310

411
import { useComponentContext } from '../../context/ComponentContext';
512

613
import { Item } from './Item';
714
import { escapeRegExp } from '../Message/renderText';
8-
9-
export const List = ({
15+
import type { DefaultStreamChatGenerics } from '../../types';
16+
import type { CustomTrigger, UnknownType } from '../../types/types';
17+
import {
18+
SuggestionEmoji,
19+
SuggestionItem,
20+
SuggestionListProps,
21+
SuggestionUser,
22+
} from '../ChatAutoComplete';
23+
24+
export const List = <
25+
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
26+
V extends CustomTrigger = CustomTrigger,
27+
EmojiData extends UnknownType = UnknownType,
28+
>({
1029
className,
1130
component,
1231
currentTrigger,
@@ -21,67 +40,76 @@ export const List = ({
2140
SuggestionItem: PropSuggestionItem,
2241
value: propValue,
2342
values,
24-
}) => {
25-
const { AutocompleteSuggestionItem } = useComponentContext('SuggestionList');
43+
}: SuggestionListProps<StreamChatGenerics, V>) => {
44+
const { AutocompleteSuggestionItem } =
45+
useComponentContext<StreamChatGenerics>('SuggestionList');
2646
const SuggestionItem = PropSuggestionItem || AutocompleteSuggestionItem || Item;
2747

28-
const [selectedItemIndex, setSelectedItemIndex] = useState(undefined);
48+
const [selectedItemIndex, setSelectedItemIndex] = useState<number | undefined>(
49+
undefined,
50+
);
2951

30-
const itemsRef = [];
52+
const itemsRef: HTMLElement[] = [];
3153

32-
const isSelected = (item) =>
54+
const isSelected = (item: SuggestionItem<StreamChatGenerics, EmojiData>) =>
3355
selectedItemIndex === values.findIndex((value) => getId(value) === getId(item));
3456

35-
const getId = (item) => {
57+
const getId = (item: SuggestionItem<StreamChatGenerics, EmojiData>) => {
3658
const textToReplace = getTextToReplace(item);
3759
if (textToReplace.key) {
3860
return textToReplace.key;
3961
}
4062

41-
if (typeof item === 'string' || !item.key) {
63+
if (typeof item === 'string' || !(item as SuggestionEmoji<EmojiData>).key) {
4264
return textToReplace.text;
4365
}
4466

45-
return item.key;
67+
return (item as SuggestionEmoji<V>).key;
4668
};
4769

4870
const findItemIndex = useCallback(
49-
(item) =>
71+
(item: SuggestionItem<StreamChatGenerics, V>) =>
5072
values.findIndex((value) =>
51-
value.id ? value.id === item.id : value.name === item.name,
73+
value.id
74+
? value.id === (item as SuggestionUser<StreamChatGenerics>).id
75+
: value.name === item.name,
5276
),
5377
[values],
5478
);
5579

56-
const modifyText = (value) => {
80+
const modifyText = (
81+
value: SuggestionListProps<StreamChatGenerics, V>['values'][number],
82+
) => {
5783
if (!value) return;
5884

5985
onSelect(getTextToReplace(value));
6086
if (getSelectedItem) getSelectedItem(value);
6187
};
6288

6389
const handleClick = useCallback(
64-
(e, item) => {
90+
(
91+
e: React.MouseEvent<Element, MouseEvent>,
92+
item: SuggestionItem<StreamChatGenerics, V>,
93+
) => {
6594
e?.preventDefault();
6695

6796
const index = findItemIndex(item);
6897

6998
modifyText(values[index]);
7099
},
71-
// eslint-disable-next-line react-hooks/exhaustive-deps
72-
[modifyText, findItemIndex],
100+
[modifyText, findItemIndex, values],
73101
);
74102

75103
const selectItem = useCallback(
76-
(item) => {
104+
(item: SuggestionItem<StreamChatGenerics, V>) => {
77105
const index = findItemIndex(item);
78106
setSelectedItemIndex(index);
79107
},
80108
[findItemIndex],
81109
);
82110

83111
const handleKeyDown = useCallback(
84-
(event) => {
112+
(event: KeyboardEvent) => {
85113
if (event.key === 'ArrowUp') {
86114
setSelectedItemIndex((prevSelected) => {
87115
if (prevSelected === undefined) return 0;
@@ -104,7 +132,7 @@ export const List = ({
104132
(event.key === 'Enter' || event.key === 'Tab') &&
105133
selectedItemIndex !== undefined
106134
) {
107-
handleClick(event, values[selectedItemIndex]);
135+
modifyText(values[selectedItemIndex]);
108136
}
109137

110138
return null;
@@ -120,13 +148,13 @@ export const List = ({
120148

121149
useEffect(() => {
122150
if (values?.length) selectItem(values[0]);
123-
}, [values]); // eslint-disable-line
151+
}, [selectItem, values]);
124152

125153
const restructureItem = useCallback(
126-
(item) => {
127-
const matched = item.name || item.id;
154+
(item: SuggestionItem<StreamChatGenerics, V>) => {
155+
const matched = item.name || (item as SuggestionUser<StreamChatGenerics>).id;
128156

129-
const textBeforeCursor = propValue.slice(0, selectionEnd);
157+
const textBeforeCursor = (propValue || '').slice(0, selectionEnd);
130158
const triggerIndex = textBeforeCursor.lastIndexOf(currentTrigger);
131159
const editedPropValue = escapeRegExp(textBeforeCursor.slice(triggerIndex + 1));
132160

@@ -149,16 +177,17 @@ export const List = ({
149177
{restructuredValues.map((item, i) => (
150178
<SuggestionItem
151179
className={itemClassName}
180+
// @ts-ignore
152181
component={component}
153182
item={item}
154-
key={getId(item)}
183+
key={getId(item).toString()}
155184
onClickHandler={handleClick}
156185
onSelectHandler={selectItem}
157-
ref={(ref) => {
186+
ref={(ref: HTMLAnchorElement) => {
158187
itemsRef[i] = ref;
159188
}}
160189
selected={isSelected(item)}
161-
style={itemStyle}
190+
style={itemStyle as CSSProperties}
162191
value={propValue}
163192
/>
164193
))}

0 commit comments

Comments
 (0)