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
2 changes: 1 addition & 1 deletion examples/tutorial/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"emoji-mart": "^5.6.0",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"stream-chat": "10.0.0-rc.2",
"stream-chat": "10.0.0-rc.4",
"stream-chat-react": "workspace:^"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion examples/vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"modern-normalize": "^3.0.1",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"stream-chat": "10.0.0-rc.2",
"stream-chat": "10.0.0-rc.4",
"stream-chat-react": "workspace:^"
},
"devDependencies": {
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@
"modern-normalize": "^3.0.1",
"react": "^19.0.0 || ^18.0.0 || ^17.0.0",
"react-dom": "^19.0.0 || ^18.0.0 || ^17.0.0",
"stream-chat": "10.0.0-rc.2"
"stream-chat": "10.0.0-rc.4"
},
"peerDependenciesMeta": {
"@breezystack/lamejs": {
Expand Down Expand Up @@ -202,7 +202,7 @@
"react-dom": "^19.2.6",
"sass": "^1.100.0",
"semantic-release": "^25.0.3",
"stream-chat": "10.0.0-rc.2",
"stream-chat": "10.0.0-rc.4",
"typescript": "^6.0.3",
"typescript-eslint": "^8.59.4",
"vite": "^8.1.3",
Expand Down
43 changes: 23 additions & 20 deletions src/components/Channel/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
type APIErrorResponse,
type APIError,
type ChannelState,
type MessageResponse,
StreamAPIError,
Expand Down Expand Up @@ -75,14 +75,27 @@ export const findInMsgSetByDate = (
}
return { index: -1 };
};
/**
* Builds the `APIError` payload for a synthetic error response, for transport failures
* that never produced one.
*/
const buildErrorResponseData = (message: string, status: number, code = 0): APIError => ({
code,
details: [],
duration: '',
message,
more_info: '',
status_code: status,
});

/**
* Compatibility adapter:
* LocalMessage.error expects StreamAPIError<APIErrorResponse>, but some transport failures
* LocalMessage.error expects StreamAPIError<APIError>, but some transport failures
* (for example Axios ERR_NETWORK while offline) do not have an HTTP response payload.
*/
export const adaptMessageSendErrorToErrorFromResponse = (
error: unknown,
): StreamAPIError<APIErrorResponse> => {
): StreamAPIError<APIError> => {
if (error instanceof StreamAPIError) {
return error;
}
Expand All @@ -97,7 +110,7 @@ export const adaptMessageSendErrorToErrorFromResponse = (
code?: unknown;
message?: unknown;
name?: unknown;
response?: StreamAPIError<APIErrorResponse>['response'];
response?: StreamAPIError<APIError>['response'];
status?: unknown;
};

Expand All @@ -108,20 +121,15 @@ export const adaptMessageSendErrorToErrorFromResponse = (
: 'Network Error';
status = maybeAxiosError.response?.status ?? 0;

return new StreamAPIError<APIErrorResponse>(message, {
return new StreamAPIError<APIError>(message, {
code: undefined,
response:
maybeAxiosError.response ??
({
// Compatibility shim: this is an intentionally incomplete AxiosResponse-like object.
data: {
duration: '',
message,
more_info: '',
StatusCode: status,
},
data: buildErrorResponseData(message, status),
status,
} as StreamAPIError<APIErrorResponse>['response']),
} as StreamAPIError<APIError>['response']),
status,
});
}
Expand All @@ -147,18 +155,13 @@ export const adaptMessageSendErrorToErrorFromResponse = (
}
}

return new StreamAPIError<APIErrorResponse>(message, {
return new StreamAPIError<APIError>(message, {
code,
response: {
// Compatibility shim: this is an intentionally incomplete AxiosResponse-like object.
data: {
duration: '',
message,
more_info: '',
StatusCode: status,
},
data: buildErrorResponseData(message, status, code),
status,
} as StreamAPIError<APIErrorResponse>['response'],
} as StreamAPIError<APIError>['response'],
status,
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ describe('MessageInput', () => {
// Mock getAppSettings so the SDK's upload config check doesn't make a real network request
vi.spyOn(client, 'getAppSettings').mockResolvedValue(fromPartial({}));
const sendFileSpy = vi
.spyOn(channel, 'sendFile')
.spyOn(channel, 'uploadFile')
.mockResolvedValue(fromPartial({ file: fileObjectURL }));
await renderComponent({
channelStateCtx: { channel },
Expand Down Expand Up @@ -366,7 +366,7 @@ describe('MessageInput', () => {
// Mock getAppSettings so the SDK's upload config check doesn't make a real network request
vi.spyOn(client, 'getAppSettings').mockResolvedValue(fromPartial({}));
const sendFileSpy = vi
.spyOn(channel, 'sendFile')
.spyOn(channel, 'uploadFile')
.mockResolvedValue(fromPartial({ file: fileObjectURL }));
const sendMessageSpy = vi
.spyOn(channel, 'sendMessage')
Expand Down Expand Up @@ -402,7 +402,7 @@ describe('MessageInput', () => {
});

vi.spyOn(client, 'getAppSettings').mockResolvedValue({} as AppSettingsAPIResponse);
vi.spyOn(channel, 'sendFile').mockResolvedValue({
vi.spyOn(channel, 'uploadFile').mockResolvedValue({
file: fileObjectURL,
} as SendFileAPIResponse);

Expand Down
14 changes: 8 additions & 6 deletions src/components/MessageComposer/__tests__/MessageInput.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -328,12 +328,12 @@ const setup = async ({ channelData }: { channelData?: GenerateChannelOptions } =
channelsData: [channelData ?? mockedChannelData],
customUser: user,
});
const sendImageSpy = vi.spyOn(customChannel, 'sendImage').mockResolvedValueOnce(
const sendImageSpy = vi.spyOn(customChannel, 'uploadImage').mockResolvedValueOnce(
fromPartial<SendFileAPIResponse>({
file: fileUploadUrl,
}),
);
const sendFileSpy = vi.spyOn(customChannel, 'sendFile').mockResolvedValueOnce(
const sendFileSpy = vi.spyOn(customChannel, 'uploadFile').mockResolvedValueOnce(
fromPartial<SendFileAPIResponse>({
file: fileUploadUrl,
}),
Expand All @@ -351,13 +351,15 @@ const setupUploadRejected = async (error: unknown) => {
channelsData: [mockedChannelData],
customUser: user,
});
const sendImageSpy = vi.spyOn(customChannel, 'sendImage').mockRejectedValueOnce(error);
const sendFileSpy = vi.spyOn(customChannel, 'sendFile').mockRejectedValueOnce(error);
const sendImageSpy = vi
.spyOn(customChannel, 'uploadImage')
.mockRejectedValueOnce(error);
const sendFileSpy = vi.spyOn(customChannel, 'uploadFile').mockRejectedValueOnce(error);
customClient.activeChannels[customChannel.cid] = customChannel;
return { customChannel, customClient, sendFileSpy, sendImageSpy };
};

/** `channel.sendImage` / `channel.sendFile` pass upload options (e.g. `onUploadProgress`) after the file. */
/** `channel.uploadImage` / `channel.uploadFile` take `({ file }, requestOptions)`. */
type UploadSpy = {
mock: {
calls: [unknown, ...unknown[]][];
Expand All @@ -367,7 +369,7 @@ type UploadSpy = {
const expectChannelUploadCall = (spy: UploadSpy, expectedFile: File) => {
expect(spy.mock.calls.length).toBeGreaterThan(0);
const callArgs = spy.mock.calls[0];
expect(callArgs[0]).toBe(expectedFile);
expect(callArgs[0]).toEqual({ file: expectedFile });
expect(callArgs[callArgs.length - 1]).toEqual(
expect.objectContaining({ onUploadProgress: expect.any(Function) }),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,10 @@ const setup = async ({ channelData }: any = {}) => {
customUser: user,
});
const sendImageSpy = vi
.spyOn(customChannel, 'sendImage')
.spyOn(customChannel, 'uploadImage')
.mockResolvedValueOnce(fromPartial({ file: fileUploadUrl }));
const sendFileSpy = vi
.spyOn(customChannel, 'sendFile')
.spyOn(customChannel, 'uploadFile')
.mockResolvedValueOnce(fromPartial({ file: fileUploadUrl }));
const getDraftSpy = vi
.spyOn(customChannel, 'getDraft')
Expand Down
3 changes: 1 addition & 2 deletions src/components/Thread/Thread.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import type { MessageProps } from '../Message/types';
import type { MessageActionsArray } from '../Message/utils';
import type {
DeleteMessageOptions,
EventAPIResponse,
LocalMessage,
MarkReadRequest,
MessageRequest,
Expand Down Expand Up @@ -64,7 +63,7 @@ export type ThreadProps = {
doMarkReadRequest?: (params: {
thread: StreamThread;
options?: MarkReadRequest;
}) => Promise<EventAPIResponse | null> | void;
}) => ReturnType<StreamChannel['markRead']> | void;
/** Custom action handler to override the default `channel.sendMessage` request function in thread flows */
doSendMessageRequest?: (
thread: StreamThread,
Expand Down
3 changes: 1 addition & 2 deletions src/components/Thread/hooks/useThreadRequestHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { useEffect } from 'react';
import { localMessageToNewMessagePayload } from 'stream-chat';
import type {
DeleteMessageOptions,
EventAPIResponse,
LocalMessage,
MarkReadRequest,
MessageRequest,
Expand All @@ -24,7 +23,7 @@ export type ThreadRequestHandlersParams = {
doMarkReadRequest?: (params: {
thread: StreamThread;
options?: MarkReadRequest;
}) => Promise<EventAPIResponse | null> | void;
}) => ReturnType<StreamChannel['markRead']> | void;
doSendMessageRequest?: (
thread: StreamThread,
message: MessageRequest,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ const useChannelManagementEditForm = ({
async (file: File) => {
const url = uploadImage
? await uploadImage(file)
: (await channel.sendImage(file)).file;
: (await channel.uploadImage({ file })).file;
if (!url) throw new Error('Image upload did not return a URL');
return url;
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ const mocks = vi.hoisted(() => ({
member_count: 2,
own_capabilities: ['update-channel'],
},
sendImage: vi.fn(),
state: {
members: {
'other-user': { user: { id: 'other-user' } },
Expand All @@ -23,6 +22,7 @@ const mocks = vi.hoisted(() => ({
membership: {},
},
updatePartial: vi.fn(),
uploadImage: vi.fn(),
},
close: vi.fn(),
displayImage: undefined as string | undefined,
Expand Down Expand Up @@ -187,9 +187,11 @@ describe('ChannelManagementView', () => {
value: vi.fn(),
});
mocks.addNotification.mockReset();
mocks.channel.sendImage.mockReset();
mocks.channel.uploadImage.mockReset();
mocks.channel.updatePartial.mockReset();
mocks.channel.sendImage.mockResolvedValue({ file: 'https://stream-upload.example' });
mocks.channel.uploadImage.mockResolvedValue({
file: 'https://stream-upload.example',
});
mocks.channel.updatePartial.mockResolvedValue({});
mocks.channel.data.custom.name = 'Test channel';
mocks.channel.data.member_count = 2;
Expand Down Expand Up @@ -306,7 +308,7 @@ describe('ChannelManagementView', () => {
fireEvent.click(screen.getByRole('button', { name: 'Save' }));

await waitFor(() => expect(uploadImage).toHaveBeenCalledTimes(1));
expect(mocks.channel.sendImage).not.toHaveBeenCalled();
expect(mocks.channel.uploadImage).not.toHaveBeenCalled();
expect(mocks.channel.updatePartial).toHaveBeenCalledWith({
set: { 'custom.image': 'https://custom-upload.example' },
});
Expand Down Expand Up @@ -340,17 +342,17 @@ describe('ChannelManagementView', () => {
set: { 'custom.name': 'Renamed channel' },
}),
);
expect(mocks.channel.sendImage).not.toHaveBeenCalled();
expect(mocks.channel.uploadImage).not.toHaveBeenCalled();
});

it('uploads via channel.sendImage when no custom upload is provided', async () => {
it('uploads via channel.uploadImage when no custom upload is provided', async () => {
const { container } = renderChannelManagementView();

enterEditMode();
uploadFile(container);
save();

await waitFor(() => expect(mocks.channel.sendImage).toHaveBeenCalledTimes(1));
await waitFor(() => expect(mocks.channel.uploadImage).toHaveBeenCalledTimes(1));
expect(mocks.channel.updatePartial).toHaveBeenCalledWith({
set: { 'custom.image': 'https://stream-upload.example' },
});
Expand Down Expand Up @@ -387,7 +389,7 @@ describe('ChannelManagementView', () => {
unset: ['custom.image'],
}),
);
expect(mocks.channel.sendImage).not.toHaveBeenCalled();
expect(mocks.channel.uploadImage).not.toHaveBeenCalled();
});

it('emits a success notification after saving', async () => {
Expand Down Expand Up @@ -427,7 +429,7 @@ describe('ChannelManagementView', () => {
});

it('does not persist when the upload returns no URL', async () => {
mocks.channel.sendImage.mockResolvedValueOnce({ file: undefined });
mocks.channel.uploadImage.mockResolvedValueOnce({ file: undefined });
const { container } = renderChannelManagementView();

enterEditMode();
Expand Down
Loading
Loading