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
8 changes: 6 additions & 2 deletions src/components/Attachment/Geolocation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ import { useEffect } from 'react';
import { useRef, useState } from 'react';
import React from 'react';
import type { Coords, SharedLocationResponse } from 'stream-chat';
import { useChatContext, useTranslationContext } from '../../context';
import {
useChatContext,
useComponentContextIcons,
useTranslationContext,
} from '../../context';
import { ExternalLinkIcon } from './icons';
import { IconLocation } from '../Icons';
import { Button } from '../Button';

export type GeolocationMapProps = Coords;
Expand Down Expand Up @@ -102,6 +105,7 @@ const DefaultGeolocationAttachmentMapPlaceholder = ({
location,
}: GeolocationAttachmentMapPlaceholderProps) => {
const { t } = useTranslationContext();
const { IconLocation } = useComponentContextIcons();

return (
<div
Expand Down
17 changes: 10 additions & 7 deletions src/components/Attachment/Giphy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import clsx from 'clsx';
import {
useChannelStateContext,
useComponentContext,
useComponentContextIcons,
useTranslationContext,
} from '../../context';
import { IconGiphy } from '../Icons';
import { type CSSProperties, useLayoutEffect, useMemo, useRef, useState } from 'react';
import type { ImageAttachmentConfiguration } from '../../types/types';

Expand Down Expand Up @@ -78,9 +78,12 @@ export const Giphy = ({ attachment }: GiphyAttachmentProps) => {
);
};

const GiphyBadge = () => (
<div className='str-chat__giphy-badge'>
<IconGiphy />
Giphy
</div>
);
const GiphyBadge = () => {
const { IconGiphy } = useComponentContextIcons();
return (
<div className='str-chat__giphy-badge'>
<IconGiphy />
Giphy
</div>
);
};
3 changes: 2 additions & 1 deletion src/components/Attachment/LinkPreview/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import React from 'react';
import { BaseImage } from '../../BaseImage';
import { SafeAnchor } from '../../SafeAnchor';
import { useChannelStateContext } from '../../../context/ChannelStateContext';
import { useComponentContextIcons } from '../../../context';

import type { Attachment } from 'stream-chat';
import type { RenderAttachmentProps } from '../utils';
import type { Dimensions } from '../../../types/types';
import { IconLink } from '../../Icons';
import { UnableToRenderCard } from './UnableToRenderCard';
import clsx from 'clsx';

Expand Down Expand Up @@ -64,6 +64,7 @@ type CardContentProps = RenderAttachmentProps['attachment'];
const CardContent = (props: CardContentProps) => {
const { og_scrape_url, text, title, title_link } = props;
const url = title_link || og_scrape_url;
const { IconLink } = useComponentContextIcons();

return (
<div className='str-chat__message-attachment-card--content'>
Expand Down
36 changes: 19 additions & 17 deletions src/components/Attachment/LinkPreview/CardAudio.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { type AudioPlayerState, ProgressBar, useAudioPlayer } from '../../AudioPlayback';
import { useMessageContext } from '../../../context';
import { useComponentContextIcons, useMessageContext } from '../../../context';
import { useStateStore } from '../../../store';
import { PlayButton } from '../../Button';
import type { AudioProps } from '../Audio';
import React from 'react';
import { IconLink } from '../../Icons';
import { SafeAnchor } from '../../SafeAnchor';
import type { CardProps } from './Card';

Expand All @@ -21,22 +20,25 @@ const SourceLink = ({
author_name,
showUrl,
url,
}: Pick<CardProps, 'author_name'> & { url: string; showUrl?: boolean }) => (
<div
className='str-chat__message-attachment-card--source-link'
data-testid='card-source-link'
>
<IconLink />
<SafeAnchor
className='str-chat__message-attachment-card--url'
href={url}
rel='noopener noreferrer'
target='_blank'
}: Pick<CardProps, 'author_name'> & { url: string; showUrl?: boolean }) => {
const { IconLink } = useComponentContextIcons();
return (
<div
className='str-chat__message-attachment-card--source-link'
data-testid='card-source-link'
>
{showUrl ? url : author_name || getHostFromURL(url)}
</SafeAnchor>
</div>
);
<IconLink />
<SafeAnchor
className='str-chat__message-attachment-card--url'
href={url}
rel='noopener noreferrer'
target='_blank'
>
{showUrl ? url : author_name || getHostFromURL(url)}
</SafeAnchor>
</div>
);
};

const audioPlayerStateSelector = (state: AudioPlayerState) => ({
durationSeconds: state.durationSeconds,
Expand Down
8 changes: 6 additions & 2 deletions src/components/Attachment/ModalGallery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ import { BaseImage as DefaultBaseImage } from '../BaseImage';
import { Gallery as DefaultGallery, GalleryUI } from '../Gallery';
import { LoadingIndicator } from '../Loading';
import { GlobalModal, type ModalCloseSource } from '../Modal';
import { useComponentContext, useTranslationContext } from '../../context';
import { IconRetry } from '../Icons';
import {
useComponentContext,
useComponentContextIcons,
useTranslationContext,
} from '../../context';
import { VideoThumbnail } from '../VideoPlayer/VideoThumbnail';

const MAX_VISIBLE_THUMBNAILS = 4;
Expand Down Expand Up @@ -144,6 +147,7 @@ const ThumbnailButton = ({
showOverlay,
}: ThumbnailButtonProps) => {
const { t } = useTranslationContext();
const { IconRetry } = useComponentContextIcons();
const imageUrl = item.imageUrl;
const [isLoadFailed, setIsLoadFailed] = useState(false);
const [isImageLoading, setIsImageLoading] = useState(Boolean(imageUrl));
Expand Down
5 changes: 3 additions & 2 deletions src/components/Attachment/UnsupportedAttachment.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import React from 'react';
import type { Attachment } from 'stream-chat';
import { useTranslationContext } from '../../context';
import { IconUnsupportedAttachment } from '../Icons';
import { useComponentContextIcons, useTranslationContext } from '../../context';

export type UnsupportedAttachmentProps = {
attachment: Attachment;
};

export const UnsupportedAttachment = () => {
const { IconUnsupportedAttachment } = useComponentContextIcons();

const { t } = useTranslationContext('UnsupportedAttachment');
return (
<div
Expand Down
5 changes: 3 additions & 2 deletions src/components/Attachment/VisibilityDisclaimer.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React from 'react';
import { IconEyeFill } from '../Icons';
import { useTranslationContext } from '../../context';
import { useComponentContextIcons, useTranslationContext } from '../../context';

export const VisibilityDisclaimer = () => {
const { IconEyeFill } = useComponentContextIcons();

const { t } = useTranslationContext();
return (
<div className='str-chat__visibility-disclaimer'>
Expand Down
34 changes: 19 additions & 15 deletions src/components/Attachment/__tests__/Giphy.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,25 @@ const { channelStateMock } = vi.hoisted(() => ({
},
}));

vi.mock('../../../context', () => ({
useChannelStateContext: () => channelStateMock,
useComponentContext: () => ({}),
useTranslationContext: () => ({
t: (key, params) =>
Object.keys(params ?? {}).reduce(
(acc, paramKey) =>
acc.replace(
new RegExp(`\\{\\{\\s${paramKey}\\s\\}\\}`, 'g'),
String(params?.[paramKey]),
),
key.replace(/^aria\//, ''),
),
}),
}));
vi.mock('../../../context', async (importOriginal) => {
const actual = await importOriginal<typeof import('../../../context')>();
return {
useChannelStateContext: () => channelStateMock,
useComponentContext: () => ({}),
useComponentContextIcons: actual.useComponentContextIcons,
useTranslationContext: () => ({
t: (key, params) =>
Object.keys(params ?? {}).reduce(
(acc, paramKey) =>
acc.replace(
new RegExp(`\\{\\{\\s${paramKey}\\s\\}\\}`, 'g'),
String(params?.[paramKey]),
),
key.replace(/^aria\//, ''),
),
}),
};
});

describe('Giphy accessible name', () => {
it('uses the giphy title as the image accessible name', () => {
Expand Down
5 changes: 3 additions & 2 deletions src/components/Attachment/components/DownloadButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import React from 'react';
import clsx from 'clsx';
import { sanitizeUrl } from '@braintree/sanitize-url';

import { useTranslationContext } from '../../../context';
import { IconDownload } from '../../Icons';
import { useComponentContextIcons, useTranslationContext } from '../../../context';

export type DownloadButtonProps = {
/** Attachment asset URL (e.g. `asset_url`). */
Expand All @@ -25,6 +24,8 @@ export const DownloadButton = ({
suggestedFileName,
tooltipTitle,
}: DownloadButtonProps) => {
const { IconDownload } = useComponentContextIcons();

const { t } = useTranslationContext();
if (!assetUrl) return null;
const href = sanitizeUrl(assetUrl);
Expand Down
14 changes: 10 additions & 4 deletions src/components/Avatar/Avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@ import React, {
useMemo,
useState,
} from 'react';
import { IconUser } from '../Icons';
import { useComponentContextIcons } from '../../context';

export type AvatarProps = {
/** Custom icon rendered when there is no image and no initials */
/**
* Custom icon rendered when there is no image and no initials.
* @deprecated Use the `icons.IconUser` slot on `ComponentContext` (via `<WithComponents overrides={{ icons: { IconUser: ... } }}>`) instead.
* Passing this prop still wins over the context slot for backwards compatibility.
*/
FallbackIcon?: ComponentType<ComponentPropsWithoutRef<'svg'>>;
/** URL of the avatar image */
imageUrl?: string;
Expand Down Expand Up @@ -51,14 +55,16 @@ const getInitials = (name?: string) => {
*/
export const Avatar = ({
className,
FallbackIcon = IconUser,
FallbackIcon,
imageUrl,
initials: customInitials,
isOnline,
size,
userName,
...rest
}: AvatarProps) => {
const { IconUser } = useComponentContextIcons();
const ResolvedFallbackIcon = FallbackIcon ?? IconUser;
const [error, setError] = useState(false);

useEffect(() => () => setError(false), [imageUrl]);
Expand Down Expand Up @@ -113,7 +119,7 @@ export const Avatar = ({
{sizeAwareInitials}
</div>
)}
{!sizeAwareInitials.length && <FallbackIcon />}
{!sizeAwareInitials.length && <ResolvedFallbackIcon />}
</>
)}
</div>
Expand Down
15 changes: 9 additions & 6 deletions src/components/Badge/Badge.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import clsx from 'clsx';
import React, { type ComponentProps } from 'react';
import { IconExclamationMarkFill } from '../Icons';
import { useComponentContextIcons } from '../../context';

export type BadgeVariant =
| 'default'
Expand Down Expand Up @@ -47,8 +47,11 @@ export const ErrorBadge = ({
className,
size = 'sm',
...rest
}: Omit<BadgeProps, 'variant'>) => (
<Badge {...rest} className={className} size={size} variant='error'>
<IconExclamationMarkFill />
</Badge>
);
}: Omit<BadgeProps, 'variant'>) => {
const { IconExclamationMarkFill } = useComponentContextIcons();
return (
<Badge {...rest} className={className} size={size} variant='error'>
<IconExclamationMarkFill />
</Badge>
);
};
Comment on lines +50 to +57

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add coverage for the contextual icon override.

Extend src/components/Badge/__tests__/Badge.test.tsx to verify both the default icon and an icons.IconExclamationMarkFill override.

As per coding guidelines, “Add and extend tests in the matching module's __tests__/ folder.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Badge/Badge.tsx` around lines 51 - 59, Add tests in
Badge.test.tsx covering the error Badge’s default IconExclamationMarkFill
rendering and the contextual icons.IconExclamationMarkFill override. Use the
existing component-context test patterns and assert that each configuration
renders the expected icon.

Source: Coding guidelines

16 changes: 9 additions & 7 deletions src/components/Badge/MediaBadge.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IconMicrophoneSolid, IconVideoFill } from '../Icons';
import { useComponentContextIcons } from '../../context';
import React, { type ComponentType } from 'react';
import type { LocalAttachment } from 'stream-chat';
import clsx from 'clsx';
Expand All @@ -10,13 +10,15 @@ export type MediaBadgeProps = {
variant: 'video' | 'voice-recording' | string;
};

const MediaBadgeVariantToIcon: Record<MediaBadgeVariant, ComponentType> = {
video: IconVideoFill,
voiceRecording: IconMicrophoneSolid,
};

export const MediaBadge = ({ attachment, variant }: MediaBadgeProps) => {
const Icon = MediaBadgeVariantToIcon[variant];
const { IconMicrophoneSolid, IconVideoFill } = useComponentContextIcons();

const mediaBadgeVariantToIcon: Record<MediaBadgeVariant, ComponentType> = {
video: IconVideoFill,
voiceRecording: IconMicrophoneSolid,
};

const Icon = mediaBadgeVariantToIcon[variant];
Comment on lines 13 to +21

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Fix variant mapping key to match the exact string type.

The MediaBadgeVariant type defines the voice variant as 'voice-recording', but the mediaBadgeVariantToIcon dictionary uses the camelCased key voiceRecording. Consequently, when variant='voice-recording' is passed, the icon will fail to resolve and render.

🐛 Proposed fix for the dictionary key
   const mediaBadgeVariantToIcon: Record<MediaBadgeVariant, ComponentType> = {
     video: IconVideoFill,
-    voiceRecording: IconMicrophoneSolid,
+    'voice-recording': IconMicrophoneSolid,
   };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const MediaBadge = ({ attachment, variant }: MediaBadgeProps) => {
const Icon = MediaBadgeVariantToIcon[variant];
const {
icons: {
IconMicrophoneSolid = DefaultIconMicrophoneSolid,
IconVideoFill = DefaultIconVideoFill,
} = {},
} = useComponentContext();
const mediaBadgeVariantToIcon: Record<MediaBadgeVariant, ComponentType> = {
video: IconVideoFill,
voiceRecording: IconMicrophoneSolid,
};
const Icon = mediaBadgeVariantToIcon[variant];
export const MediaBadge = ({ attachment, variant }: MediaBadgeProps) => {
const {
icons: {
IconMicrophoneSolid = DefaultIconMicrophoneSolid,
IconVideoFill = DefaultIconVideoFill,
} = {},
} = useComponentContext();
const mediaBadgeVariantToIcon: Record<MediaBadgeVariant, ComponentType> = {
video: IconVideoFill,
'voice-recording': IconMicrophoneSolid,
};
const Icon = mediaBadgeVariantToIcon[variant];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Badge/MediaBadge.tsx` around lines 17 - 30, Update the voice
entry in MediaBadge’s mediaBadgeVariantToIcon mapping to use the exact
MediaBadgeVariant value "voice-recording" instead of "voiceRecording", while
preserving the existing microphone icon mapping.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MartinCupela - would you know whether this is a type issue or is the mapping is wrong?

return (
<div
className={clsx('str-chat__media-badge', {
Expand Down
4 changes: 3 additions & 1 deletion src/components/BaseImage/ImagePlaceholder.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import React from 'react';
import clsx from 'clsx';
import { useTranslationContext } from '../../context/TranslationContext';
import { IconImage } from '../Icons';
import { useComponentContextIcons } from '../../context';

export type ImagePlaceholderProps = {
className?: string;
};

export const ImagePlaceholder = ({ className }: ImagePlaceholderProps) => {
const { IconImage } = useComponentContextIcons();

const { t } = useTranslationContext();
return (
<div
Expand Down
5 changes: 3 additions & 2 deletions src/components/Button/PlayButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import React from 'react';
import { Button } from './Button';
import type { ComponentProps } from 'react';
import clsx from 'clsx';
import { IconPauseFill, IconPlayFill } from '../Icons';
import { useTranslationContext } from '../../context';
import { useComponentContextIcons, useTranslationContext } from '../../context';

export type PlayButtonProps = ComponentProps<'button'> & {
isPlaying: boolean;
};

export const PlayButton = ({ className, isPlaying, ...props }: PlayButtonProps) => {
const { IconPauseFill, IconPlayFill } = useComponentContextIcons();

const { t } = useTranslationContext();
return (
<Button
Expand Down
Loading