Skip to content

Commit 5fc86be

Browse files
committed
Merge branch 'master' into feat/channel-delivery-state
2 parents 4116d6f + 51ea875 commit 5fc86be

File tree

24 files changed

+492
-28
lines changed

24 files changed

+492
-28
lines changed

AGENTS.md

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
Guidance for AI coding agents (Copilot, Cursor, Aider, Claude, etc.) working in this repository. Human readers are welcome, but this file is written for tools.
2+
3+
### Repository purpose
4+
5+
This repo hosts Stream’s React Chat SDK. It provides UI component.
6+
7+
Agents should prioritize backwards compatibility, API stability, and high test coverage when changing code.
8+
9+
### Tech & toolchain
10+
11+
- Language: React (Typescript)
12+
- Primary runtime: Node (use the version in .nvmrc via nvm use)
13+
- Testing: Unit/integration: Jest (+ React Testing Library).
14+
- CI: GitHub Actions (assume PR validation on build + tests + lint)
15+
- Lint/format: ESLint + Prettier (configs in repo root)
16+
- Styles: Import Stream styles and override via CSS layers as described in README (don’t edit compiled CSS)
17+
- Release discipline: Conventional Commits + automated release tooling (see commitlint/semantic-release configs).
18+
19+
### Project layout (high level)
20+
21+
- src/ — Components, hooks, contexts, styles, and utilities (library source).
22+
- scripts/ - Scripts run during the build process
23+
- e2e/ — Playwright specs.
24+
- examples/ — Example apps/snippets.
25+
- developers/ — Dev notes & scripts.
26+
27+
Use the closest folder’s patterns and conventions when editing.
28+
29+
### Configurations
30+
31+
Root configs:
32+
33+
- .babelrc.js
34+
- .gitignore
35+
- .lintstagedrc.fix.json
36+
- .lintstagedrc.json
37+
- .nvmrc
38+
- .prettierignore
39+
- .prettierrc
40+
- .releaserc.json
41+
- babel.config.js
42+
- codecov.yml
43+
- commitlint.config.mjs
44+
- eslint.config.mjs,
45+
- i18next-parser.config.js
46+
- jest.config.js
47+
- jest.config.js
48+
- playwright.config.ts
49+
- tsconfig.json
50+
51+
Respect any repo-specific rules. Do not suppress rules broadly; justify and scope exceptions.
52+
53+
### Runbook (commands)
54+
55+
1. Install dependencies: yarn install
56+
2. Build: yarn build
57+
3. Typecheck: yarn types
58+
4. Lint: yarn lint
59+
5. Fix lint issues: yarn lint-fix
60+
6. Unit tests: yarn test
61+
62+
### General rules
63+
64+
#### Linting & formatting
65+
66+
- Make sure the eslint and prettier configurations are followed. Run before committing:
67+
68+
```
69+
yarn lint-fix
70+
```
71+
72+
#### Commit / PR conventions
73+
74+
- Keep PRs small and focused; include tests.
75+
- Follow the project’s “zero warnings” policy—fix new warnings and avoid introducing any.
76+
- For UI changes, attach comparison screenshots (before/after) where feasible.
77+
- Ensure public API changes include docs.
78+
79+
#### Testing policy
80+
81+
Add/extend tests in the matching module’s `__tests__`/ folder.
82+
83+
Cover:
84+
85+
- React components
86+
- React hooks
87+
- Utility functions
88+
- Use fakes/mocks from the test helpers provided by the repo when possible.
89+
90+
#### Docs & samples
91+
92+
- When altering public API, update inline docs and any affected guide pages in the docs site where this repo is the source of truth.
93+
- Keep sample/snippet code compilable.
94+
95+
#### Security & credentials
96+
97+
- Never commit API keys or customer data.
98+
- Example code must use obvious placeholders (e.g., YOUR_STREAM_KEY).
99+
- If you add scripts, ensure they fail closed on missing env vars.
100+
101+
#### When in doubt
102+
103+
- Mirror existing patterns in the nearest module.
104+
- Prefer additive changes; avoid breaking public APIs.
105+
- Ask maintainers (CODEOWNERS) through PR mentions for modules you touch.
106+
107+
---
108+
109+
Quick agent checklist (per commit)
110+
111+
- Build the src
112+
- Run all tests and ensure green
113+
- Run lint commands
114+
- Update docs if public API changed
115+
- Add/adjust tests
116+
- No new warnings
117+
118+
End of machine guidance. Edit this file to refine agent behavior over time; keep human-facing details in README.md and docs.

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
## [13.7.0](https://github.com/GetStream/stream-chat-react/compare/v13.6.6...v13.7.0) (2025-09-18)
2+
3+
### Features
4+
5+
* add imageToLink Remark plugin for converting image MD links to anchor tags ([#2832](https://github.com/GetStream/stream-chat-react/issues/2832)) ([32fce17](https://github.com/GetStream/stream-chat-react/commit/32fce1760b167f94a1c2e55f8816a998a79ee8cd))
6+
* support inserted text element in message markdown rendering ([#2831](https://github.com/GetStream/stream-chat-react/issues/2831)) ([9135112](https://github.com/GetStream/stream-chat-react/commit/9135112a73bde470988f5cbb038209f84e1f3966))
7+
8+
## [13.6.6](https://github.com/GetStream/stream-chat-react/compare/v13.6.5...v13.6.6) (2025-09-17)
9+
10+
### Bug Fixes
11+
12+
* enabled headings in message markdown ([#2829](https://github.com/GetStream/stream-chat-react/issues/2829)) ([1bdcb8d](https://github.com/GetStream/stream-chat-react/commit/1bdcb8d3cf44e334c999c333d419c0f51d6cf1bc))
13+
* render html as text in ChannelPreview ([#2830](https://github.com/GetStream/stream-chat-react/issues/2830)) ([509e45f](https://github.com/GetStream/stream-chat-react/commit/509e45f34d0dde8af0a7302225306e7fbbcedf27))
14+
115
## [13.6.5](https://github.com/GetStream/stream-chat-react/compare/v13.6.4...v13.6.5) (2025-09-15)
216

317
### Bug Fixes

CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* @oliverlaz @arnautov-anton @MartinCupela

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
"types": "dist/index.d.ts",
1313
"main": "dist/index.node.cjs",
1414
"module": "dist/index.js",
15-
"jsdelivr": "./dist/browser.full-bundle.min.js",
1615
"exports": {
1716
".": {
1817
"types": "./dist/index.d.ts",

src/@types/stream-chat-custom-data.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
DefaultCommandData,
77
DefaultEventData,
88
DefaultMemberData,
9+
DefaultMessageComposerData,
910
DefaultMessageData,
1011
DefaultPollData,
1112
DefaultPollOptionData,
@@ -36,4 +37,6 @@ declare module 'stream-chat' {
3637
interface CustomUserData extends DefaultUserData {}
3738

3839
interface CustomThreadData extends DefaultThreadData {}
40+
41+
interface CustomMessageComposerData extends DefaultMessageComposerData {}
3942
}

src/components/ChannelPreview/ChannelPreview.tsx

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ export const ChannelPreview = (props: ChannelPreviewProps) => {
8888
const [lastMessage, setLastMessage] = useState<LocalMessage>(
8989
channel.state.messages[channel.state.messages.length - 1],
9090
);
91+
const [latestMessagePreview, setLatestMessagePreview] = useState<ReactNode>(() =>
92+
getLatestMessagePreview(channel, t, userLanguage, isMessageAIGenerated),
93+
);
94+
9195
const [unread, setUnread] = useState(0);
9296
const { messageDeliveryStatus } = useMessageDeliveryStatus({
9397
channel,
@@ -134,6 +138,9 @@ export const ChannelPreview = (props: ChannelPreviewProps) => {
134138

135139
useEffect(() => {
136140
refreshUnreadCount();
141+
setLatestMessagePreview(
142+
getLatestMessagePreview(channel, t, userLanguage, isMessageAIGenerated),
143+
);
137144

138145
const handleEvent = (event: Event) => {
139146
const deletedMessagesInAnotherChannel =
@@ -144,6 +151,9 @@ export const ChannelPreview = (props: ChannelPreviewProps) => {
144151
setLastMessage(
145152
channel.state.latestMessages[channel.state.latestMessages.length - 1],
146153
);
154+
setLatestMessagePreview(
155+
getLatestMessagePreview(channel, t, userLanguage, isMessageAIGenerated),
156+
);
147157
refreshUnreadCount();
148158
};
149159

@@ -162,16 +172,18 @@ export const ChannelPreview = (props: ChannelPreviewProps) => {
162172
channel.off('message.undeleted', handleEvent);
163173
channel.off('channel.truncated', handleEvent);
164174
};
165-
}, [channel, client, refreshUnreadCount, channelUpdateCount]);
166-
167-
if (!Preview) return null;
168-
169-
const latestMessagePreview = getLatestMessagePreview(
175+
}, [
170176
channel,
177+
client,
178+
refreshUnreadCount,
179+
channelUpdateCount,
180+
getLatestMessagePreview,
171181
t,
172182
userLanguage,
173183
isMessageAIGenerated,
174-
);
184+
]);
185+
186+
if (!Preview) return null;
175187

176188
return (
177189
<Preview

src/components/ChannelPreview/__tests__/utils.test.js

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import React from 'react';
2+
import ReactMarkdown from 'react-markdown';
13
import '@testing-library/jest-dom';
24
import { nanoid } from 'nanoid';
35

@@ -14,6 +16,7 @@ import {
1416

1517
import { getDisplayImage, getDisplayTitle, getLatestMessagePreview } from '../utils';
1618
import { generateStaticLocationResponse } from '../../../mock-builders';
19+
import { render } from '@testing-library/react';
1720

1821
describe('ChannelPreview utils', () => {
1922
const clientUser = generateUser();
@@ -55,16 +58,45 @@ describe('ChannelPreview utils', () => {
5558
}),
5659
],
5760
});
61+
const channelWithHTMLInMessage = generateChannel({
62+
messages: [
63+
generateMessage({
64+
attachments: [generateImageAttachment()],
65+
text:
66+
'<h1>Hello, world!</h1> \n' +
67+
'<p>This is my first web page.</p> \n' +
68+
'<p>It contains a <strong>main heading</strong> and <em> paragraph </em>.</p>',
69+
}),
70+
],
71+
});
72+
73+
const expectedTextWithHTMLRendering =
74+
'<h1>Hello, world!</h1> <p>This is my first web page.</p> <p>It contains a <strong>main heading</strong> and <em> paragraph </em>.</p>';
75+
76+
function isReactMarkdownElement(x) {
77+
return React.isValidElement(x) && x.type === ReactMarkdown;
78+
}
5879

5980
it.each([
6081
['Nothing yet...', 'channelWithEmptyMessage', channelWithEmptyMessage],
6182
['Message deleted', 'channelWithDeletedMessage', channelWithDeletedMessage],
6283
['🏙 Attachment...', 'channelWithAttachmentMessage', channelWithAttachmentMessage],
6384
['📍Shared location', 'channelWithLocationMessage', channelWithLocationMessage],
85+
[
86+
expectedTextWithHTMLRendering,
87+
'channelWithHTMLInMessage',
88+
channelWithHTMLInMessage,
89+
],
6490
])('should return %s for %s', async (expectedValue, testCaseName, c) => {
6591
const t = (text) => text;
6692
const channel = await getQueriedChannelInstance(c);
67-
expect(getLatestMessagePreview(channel, t)).toBe(expectedValue);
93+
const preview = getLatestMessagePreview(channel, t);
94+
if (isReactMarkdownElement(preview)) {
95+
const { container } = render(preview);
96+
expect(container).toHaveTextContent(expectedValue);
97+
} else {
98+
expect(getLatestMessagePreview(channel, t)).toBe(expectedValue);
99+
}
68100
});
69101
});
70102

src/components/ChannelPreview/utils.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,21 @@ import type { Channel, PollVote, TranslationLanguages, UserResponse } from 'stre
55

66
import type { TranslationContextValue } from '../../context/TranslationContext';
77
import type { ChatContextValue } from '../../context';
8+
import type { PluggableList } from 'unified';
9+
import { htmlToTextPlugin, imageToLink, plusPlusToEmphasis } from '../Message';
10+
import remarkGfm from 'remark-gfm';
11+
12+
const remarkPlugins: PluggableList = [
13+
htmlToTextPlugin,
14+
[remarkGfm, { singleTilde: false }],
15+
plusPlusToEmphasis,
16+
imageToLink,
17+
];
818

919
export const renderPreviewText = (text: string) => (
10-
<ReactMarkdown skipHtml>{text}</ReactMarkdown>
20+
<ReactMarkdown remarkPlugins={remarkPlugins} skipHtml>
21+
{text}
22+
</ReactMarkdown>
1123
);
1224

1325
const getLatestPollVote = (latestVotesByOption: Record<string, PollVote[]>) => {

src/components/Message/QuotedMessage.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { useTranslationContext } from '../../context/TranslationContext';
1212
import { useChannelActionContext } from '../../context/ChannelActionContext';
1313
import { renderText as defaultRenderText } from './renderText';
1414
import type { MessageContextValue } from '../../context/MessageContext';
15+
import { useActionHandler } from './';
1516

1617
export type QuotedMessageProps = Pick<MessageContextValue, 'renderText'>;
1718

@@ -26,6 +27,7 @@ export const QuotedMessage = ({ renderText: propsRenderText }: QuotedMessageProp
2627
} = useMessageContext('QuotedMessage');
2728
const { t, userLanguage } = useTranslationContext('QuotedMessage');
2829
const { jumpToMessage } = useChannelActionContext('QuotedMessage');
30+
const actionHandler = useActionHandler(message);
2931

3032
const renderText = propsRenderText ?? contextRenderText ?? defaultRenderText;
3133

@@ -96,7 +98,7 @@ export const QuotedMessage = ({ renderText: propsRenderText }: QuotedMessageProp
9698
</div>
9799
</div>
98100
{message.attachments?.length ? (
99-
<Attachment attachments={message.attachments} />
101+
<Attachment actionHandler={actionHandler} attachments={message.attachments} />
100102
) : null}
101103
</>
102104
);

src/components/Message/hooks/useActionHandler.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useChannelStateContext } from '../../../context/ChannelStateContext';
33

44
import type React from 'react';
55
import type { LocalMessage } from 'stream-chat';
6+
import { useStableCallback } from '../../../utils/useStableCallback';
67

78
export type FormData = Record<string, string>;
89

@@ -19,15 +20,15 @@ export function useActionHandler(message?: LocalMessage): ActionHandlerReturnTyp
1920
const { removeMessage, updateMessage } = useChannelActionContext('useActionHandler');
2021
const { channel } = useChannelStateContext('useActionHandler');
2122

22-
return async (dataOrName, value, event) => {
23+
return useStableCallback(async (dataOrName, value, event) => {
2324
if (event) event.preventDefault();
2425

2526
if (!message || !updateMessage || !removeMessage || !channel) {
2627
console.warn(handleActionWarning);
2728
return;
2829
}
2930

30-
const messageID = message.id;
31+
const messageId = message.id;
3132
let formData: FormData = {};
3233

3334
// deprecated: value&name should be removed in favor of data obj
@@ -37,14 +38,14 @@ export function useActionHandler(message?: LocalMessage): ActionHandlerReturnTyp
3738
formData = { ...dataOrName };
3839
}
3940

40-
if (messageID) {
41-
const data = await channel.sendAction(messageID, formData);
41+
if (messageId) {
42+
const data = await channel.sendAction(messageId, formData);
4243

4344
if (data?.message) {
4445
updateMessage(data.message);
4546
} else {
4647
removeMessage(message);
4748
}
4849
}
49-
};
50+
});
5051
}

0 commit comments

Comments
 (0)