Skip to main content
Version: v11 (legacy)

Suggestion List

In this example, we will demonstrate how to customize the autocomplete suggestion list items that appear above the MessageInput component when one of the supported autocompleteTriggers is entered into the text input.

Commands translations

The default strings for command suggestion items are in English. To define your custom translations, please, provide these under the following keys in the JSON file corresponding to the language mutation. For example, override the German translations in your de.json file, you would target these defaults:

{
...
"ban-command-args": "[@Benutzername] [Text]",
"ban-command-description": "Einen Benutzer verbannen",
"ban-command-name": "Verbannen",
...
"giphy-command-args": "[Text]",
"giphy-command-description": "Poste ein zufälliges Gif in den Kanal",
"giphy-command-name": "Giphy",
...
"mute-command-args": "[@Benutzername]",
"mute-command-description": "Stummschalten eines Benutzers",
"mute-command-name": "Stumm schalten"
...
"unban-command-args": "[@Benutzername]",
"unban-command-description": "Einen Benutzer entbannen",
"unban-command-name": "Entbannen",
...
"unmute-command-args": "[@Benutzername]",
"unmute-command-description": "Stummschaltung eines Benutzers aufheben",
"unmute-command-name": "Stummschaltung aufheben",

Where:

  • name is the name of the command
  • description a phrase describing what the command does
  • args format notation in which the following strings will be considered as inputs for the given command
note

The SDK's default English translation sheet (en.json) does not contain the above keys, as the displayed strings are taken directly from the channel data queried from the Stream's back-end.

Custom suggestion list item components

The Channel component accepts three props that adjust the look and feel of the autocomplete suggestion list:

Below we show how to create custom header and list items, while leaving the list container unchanged.

Suggestion Header

By default, the component library handles autocomplete suggestions for user mentions @, commands /, and emojis :. The header component receives the text value of the MessageInput via props. The current trigger is the first character in the value string, so we condition the UI of our header component based on the currently entered trigger.

const SuggestionHeader: React.FC<SuggestionListHeaderProps> = (props) => {
const { value } = props;
const initialCharacter = value[0];

switch (initialCharacter) {
case '@':
return <div className='suggestion-header'>Mention someone...</div>;

case '/':
return <div className='suggestion-header'>Available commands...</div>;

case ':':
return <div className='suggestion-header'>Choose an emoji...</div>;

default:
return null;
}
};
success

To customize autocompleteTriggers, pass your own TriggerProvider component to Channel.

Suggestion List Items

Similar to our header component, we will conditionally render the list items based on the item type received by our custom component. The SuggestionItem type represents a union of type options for the various autocompleteTriggers.

Since our component can receive three potential item types, we must construct a few type guards to ensure proper typing and prevent TypeScript errors. We check if the received item is an emoji, a mention, or both. Each case will trigger slightly different UI in our component.

type SuggestionItem = BaseEmoji | SuggestionUser | SuggestionCommand;

const isEmoji = (output: SuggestionItem): output is BaseEmoji =>
(output as BaseEmoji).native != null;

const isMention = (output: SuggestionItem): output is SuggestionUser =>
(output as SuggestionUser).id != null && (output as SuggestionUser).native == null;

const isEmojiOrMention = (output: SuggestionItem): output is BaseEmoji | SuggestionUser =>
(output as BaseEmoji | SuggestionUser).id != null;

Once we've aligned our types, we pull necessary data off props and assemble a component that on click interacts with the MessageInput and properly selects data. The type guards are necessary since the received items do not conform to a common type with shared object keys.

const SuggestionItem = React.forwardRef(
(props: SuggestionItemProps, ref: React.Ref<HTMLDivElement>) => {
const { item, onClickHandler, onSelectHandler, selected } = props;

const selectItem = () => onSelectHandler(item);

const itemName = isEmojiOrMention(item) ? item.name || item.id : item.name;
const displayText = isEmoji(item) ? `${item.native} - ${itemName}` : itemName;

return (
<div
className={`suggestion-item ${selected ? 'selected' : ''}`}
onClick={onClickHandler}
onMouseEnter={selectItem}
ref={ref}
role='button'
tabIndex={0}
>
{isMention(item) && <Avatar image={item.image} size={20} />}
{displayText}
</div>
);
},
);

Implementation

Now that each individual piece has been constructed, we can assemble all of the snippets into the final code example.

The Code

.suggestion-header {
font-weight: 700;
font-size: 16px;
}

.suggestion-item {
display: flex;
align-items: center;
height: 32px;
font-size: 18px;
padding-left: 16px;
}

.suggestion-item.selected {
background: var(--white-smoke);
color: var(--primary-color);
font-weight: 700;
cursor: pointer;
}
const SuggestionHeader: React.FC<SuggestionListHeaderProps> = (props) => {
const { value } = props;
const initialCharacter = value[0];

switch (initialCharacter) {
case '@':
return <div className='suggestion-header'>Mention someone...</div>;

case '/':
return <div className='suggestion-header'>Available commands...</div>;

case ':':
return <div className='suggestion-header'>Choose an emoji...</div>;

default:
return null;
}
};

type SuggestionItem = BaseEmoji | SuggestionUser | SuggestionCommand;

const isEmoji = (output: SuggestionItem): output is BaseEmoji =>
(output as BaseEmoji).native != null;

const isMention = (output: SuggestionItem): output is SuggestionUser =>
(output as SuggestionUser).id != null && (output as SuggestionUser).native == null;

const isEmojiOrMention = (output: SuggestionItem): output is BaseEmoji | SuggestionUser =>
(output as BaseEmoji | SuggestionUser).id != null;

const SuggestionItem = React.forwardRef(
(props: SuggestionItemProps, ref: React.Ref<HTMLDivElement>) => {
const { item, onClickHandler, onSelectHandler, selected } = props;

const selectItem = () => onSelectHandler(item);

const itemName = isEmojiOrMention(item) ? item.name || item.id : item.name;
const displayText = isEmoji(item) ? `${item.native} - ${itemName}` : itemName;

return (
<div
className={`suggestion-item ${selected ? 'selected' : ''}`}
onClick={onClickHandler}
onMouseEnter={selectItem}
ref={ref}
role='button'
tabIndex={0}
>
{isMention(item) && <Avatar image={item.image} size={20} />}
{displayText}
</div>
);
},
);

const App = () => (
<Chat client={client}>
<ChannelList />
<Channel
AutocompleteSuggestionHeader={SuggestionHeader}
AutocompleteSuggestionItem={SuggestionItem}
>
<Window>
<ChannelHeader />
<MessageList />
<MessageInput />
</Window>
<Thread />
</Channel>
</Chat>
);

The Result

Mentions UI:

Suggestions 1

Commands UI:

Suggestions 2

Emojis UI:

Suggestions 2

Did you find this page helpful?