Skip to content

Commit

Permalink
Merge branch 'master' into rc
Browse files Browse the repository at this point in the history
  • Loading branch information
arnautov-anton committed Nov 2, 2023
2 parents 3e88940 + f9a4836 commit 30d91c9
Show file tree
Hide file tree
Showing 20 changed files with 371 additions and 49 deletions.
14 changes: 13 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,19 @@
### BREAKING CHANGES

* **emoji-mart:** `reactionOptions` signature has changed, see [release guide](https://github.com/GetStream/stream-chat-react/blob/7a19e386aa3adcc5741a7f0d92bc816a1a424094/docusaurus/docs/React/release-guides/new-reactions.mdx) for more information


# [10.16.0](https://github.com/GetStream/stream-chat-react/compare/v10.15.0...v10.16.0) (2023-10-31)


### Bug Fixes

* prevent flashing EmptyStateIndicator in ChannelList before the first channels page is loaded ([#2150](https://github.com/GetStream/stream-chat-react/issues/2150)) ([a2a9645](https://github.com/GetStream/stream-chat-react/commit/a2a964513e400b62f800b118433f1d5d671b557d))


### Features

* add commands translations ([#2149](https://github.com/GetStream/stream-chat-react/issues/2149)) ([f55c86f](https://github.com/GetStream/stream-chat-react/commit/f55c86fab8dcbfd2fb3b68aaa912e31e8c5fbe67))

# [10.15.0](https://github.com/GetStream/stream-chat-react/compare/v10.14.1...v10.15.0) (2023-10-25)


Expand Down
50 changes: 45 additions & 5 deletions docusaurus/docs/React/guides/customization/suggestion-list.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,46 @@ In this example, we will demonstrate how to customize the autocomplete suggestio
appear above the `MessageInput` component when one of the supported [`autocompleteTriggers`](../../components/contexts/message-input-context.mdx#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`](../../components/core-components/channel.mdx) component accepts three props that adjust the look and feel of the autocomplete
suggestion list:

Expand All @@ -21,7 +61,7 @@ suggestion list:

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

## Suggestion Header
### 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
Expand Down Expand Up @@ -54,7 +94,7 @@ To customize `autocompleteTriggers`, pass your own [`TriggerProvider`](../../com
component to `Channel`.
:::

## Suggestion List Items
### 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
Expand Down Expand Up @@ -108,11 +148,11 @@ const SuggestionItem = React.forwardRef(
);
```

## Implementation
### Implementation

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

### The Code
#### The Code

```css
.suggestion-header {
Expand Down Expand Up @@ -210,7 +250,7 @@ const App = () => (
);
```

### The Result
#### The Result

**Mentions UI:**

Expand Down
49 changes: 39 additions & 10 deletions docusaurus/docs/React/guides/theming/translations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -330,16 +330,45 @@ The `Streami18n` class wraps [`i18next`](https://www.npmjs.com/package/i18next)
### Class Constructor Options
| Option | Description | Type | Default |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ---------- |
| language | connected user's language | string | 'en' |
| translationsForLanguage | overrides existing component text | object | {} |
| disableDateTimeTranslations | disables translation of date times | boolean | false |
| debug | enables i18n debug mode | boolean | false |
| logger | logs warnings/errors | function | () => {} |
| dayjsLocaleConfigForLanguage | internal Day.js [config object](https://github.com/iamkun/dayjs/tree/dev/src/locale) and [calendar locale config object](https://day.js.org/docs/en/plugin/calendar) | object | 'enConfig' |
| DateTimeParser | custom date time parser | function | Day.js |
| timezone | valid timezone identifier string (https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) | function | Day.js |
| Option | Description | Type | Default |
|------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-------------------------------------------------|
| DateTimeParser | custom date time parser | function | Day.js |
| dayjsLocaleConfigForLanguage | internal Day.js [config object](https://github.com/iamkun/dayjs/tree/dev/src/locale) and [calendar locale config object](https://day.js.org/docs/en/plugin/calendar) | object | 'enConfig' |
| debug | enables i18n debug mode | boolean | false |
| disableDateTimeTranslations | disables translation of date times | boolean | false |
| language | connected user's language | string | 'en' |
| logger | logs warnings/errors | function | () => {} |
| parseMissingKeyHandler | function executed, when a key is not found among the translations | function | (key: string, defaultValue?: string) => string; |
| translationsForLanguage | overrides existing component text | object | {} |
| timezone | valid timezone identifier string (https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) | function | Day.js |
#### parseMissingKeyHandler
The default implementation returns the default value provided to the translator function, for example the component below will display string `'hello'` if the key `'some-key'` is not found among the translations for a given language:
```tsx
import { useTranslationContext } from 'stream-chat-react';

const Component = () => {
const { t } = useTranslationContext('useCommandTrigger');

return (
<div>{t('some-key', {defaultValue: 'hello'})}</div>
);
}
```
The custom handler may log missing key warnings to the console in the development environment:
```ts
import { Streami18n, Streami18nOptions } from 'stream-chat-react';

const parseMissingKeyHandler: Streami18nOptions['parseMissingKeyHandler'] = (key: string, defaultValue?: string) => {
console.warn(`Streami18n: Missing translation for key: ${key}`);
return defaultValue ?? key;
};

const i18nInstance = new Streami18n({ parseMissingKeyHandler });
```
### Class Instance Methods
Expand Down
5 changes: 4 additions & 1 deletion src/components/ChannelList/ChannelList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,10 @@ const UnMemoizedChannelList = <
<List
error={channelsQueryState.error}
loadedChannels={sendChannelsToList ? loadedChannels : undefined}
loading={channelsQueryState.queryInProgress === 'reload'}
loading={
!!channelsQueryState.queryInProgress &&
['reload', 'uninitialized'].includes(channelsQueryState.queryInProgress)
}
LoadingErrorIndicator={LoadingErrorIndicator}
LoadingIndicator={LoadingIndicator}
setChannels={setChannels}
Expand Down
39 changes: 38 additions & 1 deletion src/components/ChannelList/__tests__/ChannelList.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ import {
ChannelPreviewMessenger,
} from '../../ChannelPreview';

import { ChatContext } from '../../../context/ChatContext';
import { ChatContext, useChatContext } from '../../../context/ChatContext';
import { ChannelListMessenger } from '../ChannelListMessenger';

expect.extend(toHaveNoViolations);

Expand Down Expand Up @@ -256,6 +257,42 @@ describe('ChannelList', () => {
expect(results).toHaveNoViolations();
});

it('should render loading indicator before the first channel list load and on reload', async () => {
const channelsQueryStatesHistory = [];
const channelListMessengerLoadingHistory = [];
useMockedApis(chatClient, [queryChannelsApi([testChannel1])]);

const QueryStateInterceptor = ({ children }) => {
const { channelsQueryState } = useChatContext();
channelsQueryStatesHistory.push(channelsQueryState.queryInProgress);
return children;
};

const ChannelListMessengerPropsInterceptor = (props) => {
channelListMessengerLoadingHistory.push(props.loading);
return <ChannelListMessenger {...props} />;
};

await act(() => {
render(
<Chat client={chatClient}>
<QueryStateInterceptor>
<ChannelList List={ChannelListMessengerPropsInterceptor} />
</QueryStateInterceptor>
</Chat>,
);
});

expect(channelsQueryStatesHistory).toHaveLength(3);
expect(channelListMessengerLoadingHistory).toHaveLength(3);
expect(channelsQueryStatesHistory[0]).toBe('uninitialized');
expect(channelListMessengerLoadingHistory[0]).toBe(true);
expect(channelsQueryStatesHistory[1]).toBe('reload');
expect(channelListMessengerLoadingHistory[1]).toBe(true);
expect(channelsQueryStatesHistory[2]).toBeNull();
expect(channelListMessengerLoadingHistory[2]).toBe(false);
});

it('ChannelPreview UI components should render `Avatar` when the custom prop is provided', async () => {
useMockedApis(chatClient, [queryChannelsApi([testChannel1])]);

Expand Down
12 changes: 8 additions & 4 deletions src/components/Chat/hooks/useChannelsQueryState.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import { Dispatch, SetStateAction, useState } from 'react';
import type { APIErrorResponse, ErrorFromResponse } from 'stream-chat';

type ChannelQueryType = 'reload' | 'load-more';
type ChannelQueryState =
| 'uninitialized' // the initial state before the first channels query is trigerred
| 'reload' // the initial channels query (loading the first page) is in progress
| 'load-more' // loading the next page of channels
| null; // at least one channels page has been loaded and there is no query in progress at the moment

export interface ChannelsQueryState {
error: ErrorFromResponse<APIErrorResponse> | null;
queryInProgress: ChannelQueryType | null;
queryInProgress: ChannelQueryState | null;
setError: Dispatch<SetStateAction<ErrorFromResponse<APIErrorResponse> | null>>;
setQueryInProgress: Dispatch<SetStateAction<ChannelQueryType | null>>;
setQueryInProgress: Dispatch<SetStateAction<ChannelQueryState | null>>;
}

export const useChannelsQueryState = (): ChannelsQueryState => {
const [error, setError] = useState<ErrorFromResponse<APIErrorResponse> | null>(null);
const [queryInProgress, setQueryInProgress] = useState<ChannelQueryType | null>(null);
const [queryInProgress, setQueryInProgress] = useState<ChannelQueryState>('uninitialized');

return {
error,
Expand Down
36 changes: 31 additions & 5 deletions src/components/MessageInput/hooks/useCommandTrigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,25 @@ import { CommandItem } from '../../CommandItem/CommandItem';

import { useChannelStateContext } from '../../../context/ChannelStateContext';
import { useChatContext } from '../../../context/ChatContext';
import { useTranslationContext } from '../../../context';

import type { CommandResponse } from 'stream-chat';

import type { CommandTriggerSetting } from '../DefaultTriggerProvider';

import type { DefaultStreamChatGenerics } from '../../../types/types';

type ValidCommand<
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
> = Required<Pick<CommandResponse<StreamChatGenerics>, 'name'>> &
Omit<CommandResponse<StreamChatGenerics>, 'name'>;

export const useCommandTrigger = <
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
>(): CommandTriggerSetting<StreamChatGenerics> => {
const { themeVersion } = useChatContext<StreamChatGenerics>('useCommandTrigger');
const { channelConfig } = useChannelStateContext<StreamChatGenerics>('useCommandTrigger');
const { t } = useTranslationContext('useCommandTrigger');

const commands = channelConfig?.commands;

Expand All @@ -25,7 +32,7 @@ export const useCommandTrigger = <
}
const selectedCommands = commands.filter((command) => command.name?.indexOf(query) !== -1);

// sort alphabetically unless the you're matching the first char
// sort alphabetically unless you're matching the first char
selectedCommands.sort((a, b) => {
let nameA = a.name?.toLowerCase();
let nameB = b.name?.toLowerCase();
Expand All @@ -51,10 +58,29 @@ export const useCommandTrigger = <
const result = selectedCommands.slice(0, themeVersion === '2' ? 5 : 10);
if (onReady)
onReady(
result.filter(
(result): result is CommandResponse<StreamChatGenerics> & { name: string } =>
result.name !== undefined,
),
result
.filter(
(result): result is CommandResponse<StreamChatGenerics> & { name: string } =>
result.name !== undefined,
)
.map((commandData) => {
const translatedCommandData: ValidCommand<StreamChatGenerics> = {
name: t(`${commandData.name}-command-name`, {
defaultValue: commandData.name,
}),
};

if (commandData.args)
translatedCommandData.args = t(`${commandData.name}-command-args`, {
defaultValue: commandData.args,
});
if (commandData.description)
translatedCommandData.description = t(`${commandData.name}-command-description`, {
defaultValue: commandData.description,
});

return translatedCommandData;
}),
query,
);

Expand Down
17 changes: 8 additions & 9 deletions src/i18n/Streami18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,13 +242,14 @@ type TimezoneParser = {
const supportsTz = (dateTimeParser: unknown): dateTimeParser is TimezoneParser =>
(dateTimeParser as TimezoneParser).tz !== undefined;

type Options = {
export type Streami18nOptions = {
DateTimeParser?: DateTimeParserModule;
dayjsLocaleConfigForLanguage?: Partial<ILocale> & { calendar?: CalendarLocaleConfig };
debug?: boolean;
disableDateTimeTranslations?: boolean;
language?: TranslationLanguages;
logger?: (message?: string) => void;
parseMissingKeyHandler?: (key: string, defaultValue?: string) => string;
timezone?: string;
translationsForLanguage?: Partial<typeof enTranslations>;
};
Expand Down Expand Up @@ -467,7 +468,7 @@ export class Streami18n {
keySeparator: false;
lng: string;
nsSeparator: false;
parseMissingKeyHandler: (key: string) => string;
parseMissingKeyHandler?: (key: string, defaultValue?: string) => string;
};
/**
* A valid TZ identifier string (https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)
Expand Down Expand Up @@ -499,7 +500,7 @@ export class Streami18n {
*
* @param {*} options
*/
constructor(options: Options = {}) {
constructor(options: Streami18nOptions = {}) {
const finalOptions = {
...defaultStreami18nOptions,
...options,
Expand Down Expand Up @@ -553,14 +554,12 @@ export class Streami18n {
keySeparator: false,
lng: this.currentLanguage,
nsSeparator: false,

parseMissingKeyHandler: (key) => {
this.logger(`Streami18n: Missing translation for key: ${key}`);

return key;
},
};

if (finalOptions.parseMissingKeyHandler) {
this.i18nextConfig.parseMissingKeyHandler = finalOptions.parseMissingKeyHandler;
}

this.validateCurrentLanguage();

const dayjsLocaleConfigForLanguage = finalOptions.dayjsLocaleConfigForLanguage;
Expand Down
Loading

0 comments on commit 30d91c9

Please sign in to comment.