Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions ai-docs/v10-breaking-changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ Retyped props:
- `MessageContent`: `isMessageAIGenerated` removed. — Q.3
- `Giphy`, `GiphyImage`, `URLPreview`, `URLPreviewCompact`: `attachment` is `stream-chat`'s `Attachment` (custom
metadata under `attachment.custom`). — §17.1 · §R.4
- `MessageActionList`: `dismissOverlay` removed; it was never read (each `MessageActionListItem` closes the overlay
itself). A custom list that needs it reads `useMessageContext().dismissOverlay`.

### `<MessageList>` / `<MessageFlashList>`

Expand All @@ -205,6 +207,7 @@ Retyped props:
- Older-end loading component (`FooterComponent` on `MessageList`, `HeaderComponent` on `MessageFlashList`) now
receives `{ loadingMore?: boolean }`. The newer-end one gets no props and has no SDK source for `loadingMoreRecent`.
— §12.1
- `MessageFlashList`: `inverted` removed; it was never read (the FlashList is never inverted).
- New: `pendingUploadsEnabled`. — §O.3
- `ScrollToBottomButton`: `unreadCount` removed (self-derived from `channel.state`). — §7
- `TypingIndicatorContainer`: `thread` → `threadId`. — §2.1
Expand Down Expand Up @@ -239,6 +242,7 @@ Retyped props:
### Indicators, polls, misc

- `LoadingErrorIndicator`: `error` `boolean | Error` → `Error`. — §R.4 · #3785
- `ProgressControl`: `width` removed; it was never read (the control measures its own width).
- Poll components: `PollOption` → `PollOptionResponseData`, `PollAnswer` / `PollVote` → `PollVoteResponseData`
(`ShowAllVotesButton`, `PollResultsItem`, `PollOptionFullResults(Content)`, `PollAnswerListItem`,
`PollAnswersList(Content)`); `CreatePollOption.error` is a `PollComposerValidationError`. — §17.5 · §19
Expand Down
2 changes: 1 addition & 1 deletion ai-docs/v10-docs-impact.md
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,7 @@ Format: severity · what must change · scanner-detected breaking items with lin

#### `04-customize-message-actions.md` — Snippet / prop-table update

- **Change:** Custom label keys `t("Custom Mute User")` (L272) need `asDynamicKey` (or plain strings); `client.userID` (L258) is deprecated -> `client.userId`. `client.muteUser(id)` / `unmuteUser(id)` calls are still valid. Change (stacked PRs): the page already configures everything on `<Channel>`, which is now the only route (`handleX`, `messageActions` are no longer `<Message>` props). Add: (1) the `messageActions` function (L29-55, L207, L248) runs when a message's menu opens, not on every render, so it may do per-message work; (2) pass the same `messageActions` / `handleX` to `<Thread>`, or thread messages get the defaults; (3) a custom `MessageActionList` (L296-345) receives the built list as its `messageActions` prop (plus `dismissOverlay`), and code that wants the default actions or handlers calls `useMessageActions()` / `useMessageActionHandlers()` with no arguments inside the message (`MessageActionsHookProps` is gone). The intercept list (L430-440) names `handleReaction`, `handleReply`, `handleThreadReply`; `handleReply` does not exist (it is `handleQuotedReply`) and `handleMarkUnread`, `handlePinMessage`, `handleBlockUser` are missing.
- **Change:** Custom label keys `t("Custom Mute User")` (L272) need `asDynamicKey` (or plain strings); `client.userID` (L258) is deprecated -> `client.userId`. `client.muteUser(id)` / `unmuteUser(id)` calls are still valid. Change (stacked PRs): the page already configures everything on `<Channel>`, which is now the only route (`handleX`, `messageActions` are no longer `<Message>` props). Add: (1) the `messageActions` function (L29-55, L207, L248) runs when a message's menu opens, not on every render, so it may do per-message work; (2) pass the same `messageActions` / `handleX` to `<Thread>`, or thread messages get the defaults; (3) a custom `MessageActionList` (L296-345) receives the built list as its `messageActions` prop (its unused `dismissOverlay` prop is gone; read `useMessageContext().dismissOverlay` if needed), and code that wants the default actions or handlers calls `useMessageActions()` / `useMessageActionHandlers()` with no arguments inside the message (`MessageActionsHookProps` is gone). The intercept list (L430-440) names `handleReaction`, `handleReply`, `handleThreadReply`; `handleReply` does not exist (it is `handleQuotedReply`) and `handleMarkUnread`, `handlePinMessage`, `handleBlockUser` are missing.
- **Detected:** `userID` (L258); `msgOps` (L265, L267); `tEnglishKey` (L272)

#### `05-livestream-messagelist.md` — Snippet / prop-table update
Expand Down
9 changes: 6 additions & 3 deletions examples/SampleApp/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,13 @@ export const de: TranslationDictionary = {
'channelDetails.editChannel.name.placeholder': 'Kanalname',
'channelDetails.editChannel.upload.accessibilityLabel': 'Kanalbild hochladen',
'channelDetails.editChannel.upload.label': 'Hochladen',
'channelDetails.editImageSheet.cameraPermission.text':
'Die Kamera wird verwendet, um das Gruppenbild aufzunehmen.',
'channelDetails.editImageSheet.chooseImage.label': 'Bild auswählen',
'channelDetails.editImageSheet.close.accessibilityLabel':
'Fenster zum Bearbeiten des Bildes schließen',
'channelDetails.editImageSheet.galleryPermission.text':
'Der Galeriezugriff wird verwendet, um das Gruppenbild auszuwählen.',
'channelDetails.editImageSheet.resetPicture.label': 'Bild zurücksetzen',
'channelDetails.editImageSheet.takePhoto.label': 'Foto aufnehmen',
'channelDetails.editImageSheet.title': 'Gruppenbild bearbeiten',
Expand Down Expand Up @@ -222,15 +226,14 @@ export const de: TranslationDictionary = {

// Shared labels reused across screens
'common.anonymousUser.label': 'Anonym',
'common.cameraPermission.text':
'Die Kamera des Geräts wird verwendet, um Fotos oder Videos aufzunehmen.',
'common.cameraPermission.text': 'Die Kamera wird verwendet, um Fotos und Videos aufzunehmen.',
'common.cameraPermission.title': 'Kamerazugriff in den Geräteeinstellungen erlauben',
'common.cancel.label': 'Abbrechen',
'common.close.accessibilityLabel': 'Schließen',
'common.draft.label': 'Entwurf',
'common.editMessageFailed.error': 'Anfrage zum Bearbeiten der Nachricht fehlgeschlagen',
'common.galleryPermission.text':
'Die Galerie-Berechtigungen des Geräts werden verwendet, um Fotos oder Videos aufzunehmen.',
'Der Galeriezugriff wird verwendet, um Fotos und Videos zu teilen.',
'common.galleryPermission.title': 'Zugriff auf deine Galerie erlauben',
'common.linksDisabled.text': 'Das Senden von Links ist in dieser Unterhaltung nicht erlaubt',
'common.linksDisabled.title': 'Links sind deaktiviert',
Expand Down
9 changes: 6 additions & 3 deletions examples/SampleApp/src/i18n/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,13 @@ export const it: TranslationDictionary = {
'channelDetails.editChannel.name.placeholder': 'Nome del canale',
'channelDetails.editChannel.upload.accessibilityLabel': 'Carica immagine del canale',
'channelDetails.editChannel.upload.label': 'Carica',
'channelDetails.editImageSheet.cameraPermission.text':
"La fotocamera viene utilizzata per scattare l'immagine del gruppo.",
'channelDetails.editImageSheet.chooseImage.label': 'Scegli immagine',
'channelDetails.editImageSheet.close.accessibilityLabel':
'Chiudi il pannello di modifica immagine',
'channelDetails.editImageSheet.galleryPermission.text':
"L'accesso alla galleria viene utilizzato per scegliere l'immagine del gruppo.",
'channelDetails.editImageSheet.resetPicture.label': 'Reimposta immagine',
'channelDetails.editImageSheet.takePhoto.label': 'Scatta foto',
'channelDetails.editImageSheet.title': 'Modifica immagine del gruppo',
Expand Down Expand Up @@ -216,16 +220,15 @@ export const it: TranslationDictionary = {

// Shared labels reused across screens
'common.anonymousUser.label': 'Anonimo',
'common.cameraPermission.text':
'La fotocamera del dispositivo viene utilizzata per scattare foto o video.',
'common.cameraPermission.text': 'La fotocamera viene utilizzata per scattare foto e video.',
'common.cameraPermission.title':
"Consenti l'accesso alla fotocamera nelle impostazioni del dispositivo",
'common.cancel.label': 'Annulla',
'common.close.accessibilityLabel': 'Chiudi',
'common.draft.label': 'Bozza',
'common.editMessageFailed.error': 'Richiesta di modifica del messaggio non riuscita',
'common.galleryPermission.text':
'Le autorizzazioni della galleria del dispositivo vengono utilizzate per scattare foto o video.',
"L'accesso alla galleria viene utilizzato per condividere foto e video.",
'common.galleryPermission.title': "Consenti l'accesso alla tua galleria",
'common.linksDisabled.text': "L'invio di link non è consentito in questa conversazione",
'common.linksDisabled.title': 'I link sono disabilitati',
Expand Down
20 changes: 20 additions & 0 deletions package/src/ai-components/__tests__/Markdown.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,26 @@ describe('ai-components Markdown', () => {
expect(onLink).toHaveBeenCalledWith('https://getstream.io');
});

it('logs a rejected onLink without leaving an unhandled rejection', async () => {
const unhandled = jest.fn();
process.on('unhandledRejection', unhandled);
const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
const onLink = jest.fn().mockRejectedValue(new Error('cannot open'));

try {
renderMarkdown('[docs](https://getstream.io)', { onLink });
fireEvent.press(screen.getByText('docs'));
await new Promise((resolve) => setTimeout(resolve, 0));

expect(onLink).toHaveBeenCalledWith('https://getstream.io');
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('cannot open'));
expect(unhandled).not.toHaveBeenCalled();
} finally {
process.off('unhandledRejection', unhandled);
logSpy.mockRestore();
}
});

it('renders tables, block quotes, inline code and horizontal rules', () => {
renderMarkdown('| a | b |\n|---|---|\n| 1 | 2 |\n\n> quoted\n\nuse `npm i`\n\n---\n\nafter');

Expand Down
5 changes: 4 additions & 1 deletion package/src/ai-components/__tests__/charts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,10 @@ describe('ai-components chart parsers', () => {
}),
),
).toThrow();
expect(() => parseJsonChart('{ not json')).toThrow();
});

it('rejects invalid JSON with the same error as an unsupported chart', () => {
expect(() => parseJsonChart('{ not json')).toThrow('Unknown type of JSON formatted chart.');
});
});

Expand Down
6 changes: 2 additions & 4 deletions package/src/ai-components/charts/utils/parse-json-chart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@ import type { ChartSpec } from '../types';
import { parseVegalite, VegaLiteSchema } from '../vega-lite';

export const parseJsonChart = (code: string): ChartSpec => {
const json = JSON.parse(code);

try {
const parsedVegaLite = VegaLiteSchema.parse(json);
const parsedVegaLite = VegaLiteSchema.parse(JSON.parse(code));
return parseVegalite(parsedVegaLite);
} catch (_error) {
// Not a supported chart spec: fall through to the error below.
// Invalid JSON or not a supported chart spec: fall through to the error below.
}

throw new Error('Unknown type of JSON formatted chart.');
Expand Down
3 changes: 1 addition & 2 deletions package/src/ai-components/markdown/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,14 @@ export const getLocalRules = (
const { onLink, paragraphNumberOfLines } = opts;
const openLinkHandler = (target: string) => {
if (onLink) {
// `onLink` may be async: a rejection is logged and rethrown.
// `onLink` may be async: nothing awaits this press handler, so a rejection is only logged.
Promise.resolve(onLink(target)).catch((error: unknown) => {
const msg =
error && typeof error === 'object' && 'toString' in error
? String(error)
: 'Unknown error';

console.log('There has been a problem with this action. ' + msg);
throw error;
});
}
};
Expand Down
15 changes: 2 additions & 13 deletions package/src/components/Attachment/FileIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -191,14 +191,6 @@ const docMimeTypes = [
'application/vnd.ms-word.document.macroEnabled.12',
// .dotm (no test)
'application/vnd.ms-word.template.macroEnabled.12',

// LibreOffice/OpenOffice Writer
// .odt
'application/vnd.oasis.opendocument.text',
// .ott
'application/vnd.oasis.opendocument.text-template',
// .fodt
'application/vnd.oasis.opendocument.text-flat-xml',
];

const excelMimeTypes = [
Expand All @@ -225,7 +217,7 @@ const excelMimeTypes = [
// .xlam (no test)
'application/vnd.ms-excel.addin.macroEnabled.12',
// .xlsb (no test)
'application/vnd.ms-excel.addin.macroEnabled.12',
'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
];

const powerpointMimeTypes = [
Expand Down Expand Up @@ -259,7 +251,7 @@ const powerpointMimeTypes = [
];

const zipFileTypes = [
// .gzip
// .gzip .tar.gz
'application/gzip',
// .zip
'application/zip',
Expand All @@ -268,8 +260,6 @@ const zipFileTypes = [
'application/x-archive',
// .tar
'application/x-tar',
// .tar.gz
'application/gzip',
// .tar.Z
'application/x-compress',
// .tar.bz2
Expand Down Expand Up @@ -324,7 +314,6 @@ const videoFileTypes = [
'video/wmv',
'video/yuv',
'video/quicktime',
'video/webm',
'video/x-flv',
];

Expand Down
46 changes: 46 additions & 0 deletions package/src/components/Attachment/__tests__/FileIcon.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React from 'react';
import { Text } from 'react-native';

import { render, screen } from '@testing-library/react-native';

import { WithComponents } from '../../../contexts/componentsContext/ComponentsContext';
import { ThemeProvider } from '../../../contexts/themeContext/ThemeContext';
import { defaultTheme } from '../../../contexts/themeContext/utils/theme';
import { FileIcon } from '../FileIcon';

const renderFileIcon = (mimeType: string) =>
render(
<WithComponents
overrides={{
icons: {
DOC: () => <Text>doc</Text>,
OtherFileIcon: () => <Text>other</Text>,
SpreadSheet: () => <Text>spreadsheet</Text>,
},
}}
>
<ThemeProvider theme={defaultTheme}>
<FileIcon mimeType={mimeType} />
</ThemeProvider>
</WithComponents>,
);

describe('FileIcon', () => {
it.each([
['application/vnd.ms-excel.addin.macroEnabled.12'],
['application/vnd.ms-excel.sheet.binary.macroEnabled.12'],
])('shows the spreadsheet icon for %s', (mimeType) => {
renderFileIcon(mimeType);
expect(screen.getByText('spreadsheet')).toBeTruthy();
});

it('shows the document icon for an OpenDocument text', () => {
renderFileIcon('application/vnd.oasis.opendocument.text');
expect(screen.getByText('doc')).toBeTruthy();
});

it('falls back to the generic icon for an unknown type', () => {
renderFileIcon('application/x-unknown');
expect(screen.getByText('other')).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ describe('useEditChannelImage', () => {
expect(Alert.alert).toHaveBeenCalledTimes(1);
expect(Alert.alert).toHaveBeenCalledWith(
'Allow camera access in device settings',
'Device camera is used to take photos or videos.',
'The camera is used to take the channel image.',
expect.any(Array),
);
expect(returned).toBeUndefined();
Expand Down Expand Up @@ -202,7 +202,7 @@ describe('useEditChannelImage', () => {
expect(Alert.alert).toHaveBeenCalledTimes(1);
expect(Alert.alert).toHaveBeenCalledWith(
'Allow access to your Gallery',
'Device gallery permissions is used to take photos or videos.',
'Gallery access is used to choose the channel image.',
expect.any(Array),
);
expect(returned).toBeUndefined();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ export const useEditChannelImage = (): UseEditChannelImageResult => {
if (file.askToOpenSettings) {
Alert.alert(
t('common.cameraPermission.title', 'Allow camera access in device settings'),
t('common.cameraPermission.text', 'Device camera is used to take photos or videos.'),
t(
'channelDetails.editImageSheet.cameraPermission.text',
'The camera is used to take the channel image.',
),
[
{ style: 'cancel', text: t('common.cancel.label', 'Cancel') },
{
Expand All @@ -67,8 +70,8 @@ export const useEditChannelImage = (): UseEditChannelImageResult => {
Alert.alert(
t('common.galleryPermission.title', 'Allow access to your Gallery'),
t(
'common.galleryPermission.text',
'Device gallery permissions is used to take photos or videos.',
'channelDetails.editImageSheet.galleryPermission.text',
'Gallery access is used to choose the channel image.',
),
[
{ style: 'cancel', text: t('common.cancel.label', 'Cancel') },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,32 @@ describe('ImageGallery', () => {
});
});

it('renders a labelled placeholder for a video outside the playback window', async () => {
render(
<ImageGalleryComponent
message={generateMessage({
// Index 2 is inside the pager window but outside the video window (`shouldRender < 2`).
attachments: [
generateImageAttachment(),
generateImageAttachment(),
generateVideoAttachment({ type: 'video' }),
],
})}
/>,
);

await waitFor(() => {
expect(
screen.queryAllByLabelText('Image Gallery Video Placeholder', {
includeHiddenElements: true,
}),
).toHaveLength(1);
});
expect(
screen.queryAllByLabelText('Image Gallery Video', { includeHiddenElements: true }),
).toHaveLength(0);
});

it('keeps the pager in ltr coordinates when rtl is enabled', async () => {
setRTL(true);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ const AnimatedGalleryVideoComponent = (props: AnimatedGalleryVideoType) => {
if (!shouldRender) {
return (
<View
accessibilityLabel='Empty View Image Gallery'
accessibilityLabel='Image Gallery Video Placeholder'
style={[style, { transform: [{ scale: oneEighth }] }]}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ export const ImageGalleryVideoControl = React.memo((props: ImageGalleryVideoCont

const videoDuration = getDurationLabelFromDuration(duration);

const progressValueInSeconds = progress * duration;
const progressValueInMilliseconds = progress * duration;

const progressDuration = getDurationLabelFromDuration(progressValueInSeconds);
const progressDuration = getDurationLabelFromDuration(progressValueInMilliseconds);

const { icons } = useComponentsContext();
const {
Expand Down Expand Up @@ -115,7 +115,6 @@ export const ImageGalleryVideoControl = React.memo((props: ImageGalleryVideoCont
onStartDrag={handleStartDrag}
progress={progress}
testID={'progress-control'}
width={180}
/>
</View>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,13 @@ describe('ImageGalleryVideoControl', () => {
expect(seekSpy).toHaveBeenCalledWith(0.5 * DURATION);
expect(playSpy).not.toHaveBeenCalled();
});

it('labels the progress from the duration in milliseconds', () => {
const { videoPlayer } = setup();
act(() => {
videoPlayer.progress = 0.5;
});

expect(screen.getByText('0:05')).toBeTruthy();
});
});
Loading
Loading