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
Original file line number Diff line number Diff line change
Expand Up @@ -147,16 +147,14 @@ export const useIncomingMessageAnnouncements = ({
return;
}

if (announcedMessageIdsRef.current.has(message.id || '')) {
if (announcedMessageIdsRef.current.has(message.id)) {
return;
}

if (message.id) {
if (announcedMessageIdsRef.current.size > 500) {
announcedMessageIdsRef.current.clear();
}
announcedMessageIdsRef.current.add(message.id);
if (announcedMessageIdsRef.current.size > 500) {
announcedMessageIdsRef.current.clear();
}
announcedMessageIdsRef.current.add(message.id);

pendingAnnouncementBatchRef.current.count += 1;
if (!pendingAnnouncementBatchRef.current.firstSender) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ const INTERACTION_PRIORITIES: Partial<
};

/**
* Per-interaction dedupe window (ms), see {@link AriaLiveAnnounceOptions.dedupeMs}. List an
* Per-interaction dedupe window (ms), see `AriaLiveAnnounceOptions.dedupeMs`. List an
* interaction only when one event can fire it more than once in quick succession, e.g. an effect
* double-invoked by React StrictMode.
*/
Expand Down Expand Up @@ -251,7 +251,7 @@ const INTERACTION_DEBOUNCE_MS: Partial<Record<InteractionAnnouncementType, numbe

/**
* Default per-interaction delay (ms) for the immediate path: a single deferred emit scheduled in
* the announcer provider (see {@link AriaLiveAnnounceOptions.delayMs}), so it survives the caller
* the announcer provider (see `AriaLiveAnnounceOptions.delayMs`), so it survives the caller
* unmounting, unlike a debounce.
*
* Opening a channel or thread focuses the composer, and a live-region message emitted alongside the
Expand Down
9 changes: 4 additions & 5 deletions src/components/Attachment/AttachmentContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ import { VisibilityDisclaimer } from './VisibilityDisclaimer';
import { VideoAttachment } from './VideoAttachment';
import type { AttachmentProps } from './Attachment';
import { useComponentContext } from '../../context/ComponentContext';
import {
type ImageAttachmentConfiguration,
useAttachmentContext,
} from '../../context/AttachmentContext';
import { AttachmentUploadProgressIndicator as DefaultAttachmentUploadProgressIndicator } from './components';

/**
Expand All @@ -61,11 +65,6 @@ const hasRenderableSource = (attachment: Attachment | LocalAttachment) =>
isPendingUpload(attachment) ||
!!getAttachmentPreviewUrl(attachment, attachment.asset_url);

import {
type ImageAttachmentConfiguration,
useAttachmentContext,
} from '../../context/AttachmentContext';

export type AttachmentContainerProps = {
attachment: Attachment | GalleryAttachment | SharedLocationResponseData;
componentType: AttachmentComponentType;
Expand Down
110 changes: 0 additions & 110 deletions src/components/Attachment/LinkPreview/CardAudio.tsx

This file was deleted.

28 changes: 28 additions & 0 deletions src/components/Attachment/__tests__/DownloadButton.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from 'react';
import { render, screen } from '@testing-library/react';

import { DownloadButton } from '../components/DownloadButton';

describe('DownloadButton', () => {
it('links to a safe asset URL', () => {
render(<DownloadButton assetUrl='https://example.com/file.mp3' />);

expect(screen.getByRole('link')).toHaveAttribute(
'href',
'https://example.com/file.mp3',
);
});

it.each([undefined, ''])('renders nothing for the asset URL %j', (assetUrl) => {
const { container } = render(<DownloadButton assetUrl={assetUrl} />);

expect(container).toBeEmptyDOMElement();
});

it('renders nothing for an unsafe asset URL', () => {
// eslint-disable-next-line no-script-url
const { container } = render(<DownloadButton assetUrl='javascript:alert(1)' />);

expect(container).toBeEmptyDOMElement();
});
});
8 changes: 0 additions & 8 deletions src/components/Attachment/__tests__/Giphy.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,9 @@ import { render } from '@testing-library/react';
import { Giphy } from '../Giphy';
import { generateGiphyAttachment } from '../../../mock-builders';

const { channelStateMock } = vi.hoisted(() => ({
channelStateMock: {
giphyVersion: 'fixed_height',
imageAttachmentSizeHandler: undefined,
},
}));

vi.mock('../../../context', async (importOriginal) => {
const actual = await importOriginal<typeof import('../../../context')>();
return {
useChannelStateContext: () => channelStateMock,
useComponentContext: () => ({}),
// The real hook: with no provider it returns the SDK icons, which is what these
// assertions are written against.
Expand Down
6 changes: 3 additions & 3 deletions src/components/Attachment/components/DownloadButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ export type DownloadButtonProps = {
};

/**
* Icon button that downloads an attachment asset. Renders nothing without an `assetUrl`;
* `sanitizeUrl` turns an unsafe one into `about:blank`.
* Icon button that downloads an attachment asset. Renders nothing without an `assetUrl` or when
* the URL is unsafe (`sanitizeUrl` turns it into `about:blank`).
*/
export const DownloadButton = ({
assetUrl,
Expand All @@ -28,7 +28,7 @@ export const DownloadButton = ({
const { t } = useTranslationContext();
if (!assetUrl) return null;
const href = sanitizeUrl(assetUrl);
if (!href) return null;
if (href === 'about:blank') return null;

return (
<a
Expand Down
6 changes: 1 addition & 5 deletions src/components/Channel/__tests__/Channel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -361,11 +361,7 @@ describe('Channel', () => {

it('does not paginate (query) when the client is disconnected', async () => {
const { channel, chatClient } = await setup();
await renderComponent({
channel,
channelQueryOptions: { messages: { limit: 25 } },
chatClient,
});
await renderComponent({ channel, chatClient });

const querySpy = vi.spyOn(channel, 'query');
const prevSpy = vi.spyOn(channel.messagePaginator, 'prev');
Expand Down
22 changes: 22 additions & 0 deletions src/components/FileIcon/__tests__/FileIcon.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react';
import { render } from '@testing-library/react';

import { FileIcon, mimeTypeToIcon } from '../FileIcon';

const XLSX_MIME_TYPE =
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
const XLSB_MIME_TYPE = 'application/vnd.ms-excel.sheet.binary.macroEnabled.12';

describe('FileIcon', () => {
it('shows the spreadsheet icon for an .xlsb file', () => {
expect(mimeTypeToIcon(XLSB_MIME_TYPE)).toBe(mimeTypeToIcon(XLSX_MIME_TYPE));
expect(mimeTypeToIcon(XLSB_MIME_TYPE)).not.toBe(mimeTypeToIcon());
});

it('labels an .xlsb file by its MIME type', () => {
const { container } = render(
<FileIcon fileName='report.bin' mimeType={XLSB_MIME_TYPE} />,
);
expect(container).toHaveTextContent('xlsb');
});
});
3 changes: 2 additions & 1 deletion src/components/FileIcon/mimeTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export const excelMimeTypes = [
// .xlam
'application/vnd.ms-excel.addin.macroEnabled.12',
// .xlsb
'application/vnd.ms-excel.addin.macroEnabled.12',
'application/vnd.ms-excel.sheet.binary.macroEnabled.12',

// LibreOffice/OpenOffice Calc
// .ods
Expand Down Expand Up @@ -178,6 +178,7 @@ export const mimeTypeToExtensionMap: Record<string, string> = {
'application/vnd.mozilla.xul+xml': 'xul',
'application/vnd.ms-excel': 'xls',
'application/vnd.ms-excel.addin.macroEnabled.12': 'xlam',
'application/vnd.ms-excel.sheet.binary.macroEnabled.12': 'xlsb',
'application/vnd.ms-excel.sheet.macroEnabled.12': 'xlsm',
'application/vnd.ms-excel.template.macroEnabled.12': 'xltm',
'application/vnd.ms-fontobject': 'eot',
Expand Down
30 changes: 26 additions & 4 deletions src/components/Form/NumericInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ export const NumericInput = forwardRef<HTMLInputElement, NumericInputProps>(
const maxDef = max ?? Infinity;
const atMin = num !== null && num <= minDef;
const atMax = num !== null && num >= maxDef;
// An empty field steps to `min`, or to 0 when there is no lower bound.
const emptyValueStep = clamp(min ?? 0, minDef, maxDef);

const handleInputChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
Expand All @@ -80,15 +82,35 @@ export const NumericInput = forwardRef<HTMLInputElement, NumericInputProps>(

const handleDecrement = useCallback(() => {
if (disabled || atMin) return;
const next = num !== null ? clamp(num - step, minDef, maxDef) : minDef;
const next = num !== null ? clamp(num - step, minDef, maxDef) : emptyValueStep;
onChange(createChangeEvent(String(next)));
}, [disabled, atMin, num, step, minDef, maxDef, onChange, createChangeEvent]);
}, [
disabled,
atMin,
num,
step,
minDef,
maxDef,
emptyValueStep,
onChange,
createChangeEvent,
]);

const handleIncrement = useCallback(() => {
if (disabled || atMax) return;
const next = num !== null ? clamp(num + step, minDef, maxDef) : minDef;
const next = num !== null ? clamp(num + step, minDef, maxDef) : emptyValueStep;
onChange(createChangeEvent(String(next)));
}, [disabled, atMax, num, step, minDef, maxDef, onChange, createChangeEvent]);
}, [
disabled,
atMax,
num,
step,
minDef,
maxDef,
emptyValueStep,
onChange,
createChangeEvent,
]);

const handleKeyDown = useCallback(
(e: KeyboardEvent<HTMLInputElement>) => {
Expand Down
29 changes: 28 additions & 1 deletion src/components/Form/__tests__/NumericInput.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { fireEvent, render, screen } from '@testing-library/react';

import { NumericInput } from '../NumericInput';

Expand Down Expand Up @@ -36,4 +36,31 @@ describe('NumericInput', () => {
);
expect(screen.getByRole('spinbutton')).toHaveAttribute('size', '8');
});

it.each([
['increment', 'Increase value'],
['decrement', 'Decrease value'],
])('steps an empty value without a lower bound to 0 on %s', (_, buttonName) => {
const onChange = vi.fn();
render(<NumericInput aria-label='Delay' onChange={onChange} value='' />);

fireEvent.click(screen.getByRole('button', { name: buttonName }));

expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({ target: { value: '0' } }),
);
});

it('steps an empty value to the lower bound', () => {
const onChange = vi.fn();
render(
<NumericInput aria-label='Votes' max={10} min={2} onChange={onChange} value='' />,
);

fireEvent.click(screen.getByRole('button', { name: 'Increase value' }));

expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({ target: { value: '2' } }),
);
});
});
4 changes: 2 additions & 2 deletions src/components/InfiniteScrollPaginator/InfiniteScroll.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,9 @@ export const InfiniteScroll = (props: PropsWithChildren<InfiniteScrollProps>) =>
scrollElement.addEventListener('wheel', mousewheelListener, { passive: false });

return () => {
scrollElement.removeEventListener('wheel', mousewheelListener, useCapture);
scrollElement.removeEventListener('wheel', mousewheelListener);
};
}, [scrollComponent, useCapture]);
}, [scrollComponent]);

return (
<Component {...elementProps} ref={setScrollComponent}>
Expand Down
Loading
Loading