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

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

return (
Expand Down
23 changes: 15 additions & 8 deletions src/components/Attachment/Giphy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ import { BaseImage as DefaultBaseImage } from '../BaseImage';
import { toGalleryItemDescriptors } from '../Gallery';
import { getGiphyDescriptiveTitle } from './giphyAccessibility';
import clsx from 'clsx';
import { useComponentContext, useTranslationContext } from '../../context';
import { IconGiphy } from '../Icons';
import {
useComponentContext,
useComponentContextIcons,
useTranslationContext,
} from '../../context';
import { type CSSProperties, useLayoutEffect, useMemo, useRef, useState } from 'react';
import {
type ImageAttachmentConfiguration,
Expand Down Expand Up @@ -79,9 +82,13 @@ 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 @@ -6,9 +6,9 @@ import { useAttachmentContext } from '../../../context/AttachmentContext';
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';
import { useComponentContextIcons } from '../../../context';

type CardRootProps = {
cardUrl: string | undefined;
Expand Down Expand Up @@ -62,6 +62,7 @@ const CardHeader = (props: CardHeaderProps) => {
type CardContentProps = RenderAttachmentProps['attachment'];

const CardContent = (props: CardContentProps) => {
const { IconLink } = useComponentContextIcons();
const { og_scrape_url, text, title, title_link } = props;
const url = title_link || og_scrape_url;

Expand Down
36 changes: 20 additions & 16 deletions src/components/Attachment/LinkPreview/CardAudio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useStateStore } from '../../../store';
import { PlayButton } from '../../Button';
import type { AudioProps } from '../Audio';
import React, { useContext } from 'react';
import { IconLink } from '../../Icons';
import { useComponentContextIcons } from '../../../context';
import { SafeAnchor } from '../../SafeAnchor';
import type { CardProps } from './Card';
import { useThreadContext } from '../../Threads';
Expand All @@ -22,22 +22,26 @@ 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
3 changes: 2 additions & 1 deletion src/components/Attachment/ModalGallery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import { GlobalModal, type ModalCloseSource } from '../Modal';
import {
MessageContext,
useComponentContext,
useComponentContextIcons,
useTranslationContext,
} from '../../context';
import { IconRetry } from '../Icons';
import { VideoThumbnail } from '../VideoPlayer/VideoThumbnail';

const MAX_VISIBLE_THUMBNAILS = 4;
Expand Down Expand Up @@ -160,6 +160,7 @@ const ThumbnailButton = ({
overflowCount,
showOverlay,
}: ThumbnailButtonProps) => {
const { IconRetry } = useComponentContextIcons();
const { t } = useTranslationContext();
const imageUrl = item.imageUrl;
const [isLoadFailed, setIsLoadFailed] = useState(false);
Expand Down
4 changes: 2 additions & 2 deletions src/components/Attachment/UnsupportedAttachment.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
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();
return (
<div
Expand Down
4 changes: 2 additions & 2 deletions src/components/Attachment/VisibilityDisclaimer.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
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
56 changes: 31 additions & 25 deletions src/components/Attachment/__tests__/Giphy.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,37 @@ const { channelStateMock } = vi.hoisted(() => ({
},
}));

vi.mock('../../../context', () => ({
useChannelStateContext: () => channelStateMock,
useComponentContext: () => ({}),
useTranslationContext: () => ({
t: (key: string, second?: unknown, third?: unknown) => {
const defaultValue = typeof second === 'string' ? second : undefined;
const options = ((typeof second === 'object' ? second : third) ?? {}) as Record<
string,
unknown
>;
let template = defaultValue;
if (template === undefined && typeof options.count === 'number') {
template = (
options.count === 1 ? options.defaultValue_one : options.defaultValue_other
) as string | undefined;
}
template ??= options.defaultValue as string | undefined;
template ??= key;
return template.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (whole, name: string) => {
const value = options[name];
return value === undefined || value === null ? whole : String(value);
});
},
}),
}));
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.
useComponentContextIcons: actual.useComponentContextIcons,
useTranslationContext: () => ({
t: (key: string, second?: unknown, third?: unknown) => {
const defaultValue = typeof second === 'string' ? second : undefined;
const options = ((typeof second === 'object' ? second : third) ?? {}) as Record<
string,
unknown
>;
let template = defaultValue;
if (template === undefined && typeof options.count === 'number') {
template = (
options.count === 1 ? options.defaultValue_one : options.defaultValue_other
) as string | undefined;
}
template ??= options.defaultValue as string | undefined;
template ??= key;
return template.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (whole, name: string) => {
const value = options[name];
return value === undefined || value === null ? whole : String(value);
});
},
}),
};
});

describe('Giphy accessible name', () => {
it('uses the giphy title as the image accessible name', () => {
Expand Down
4 changes: 2 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,7 @@ export const DownloadButton = ({
suggestedFileName,
tooltipTitle,
}: DownloadButtonProps) => {
const { IconDownload } = useComponentContextIcons();
const { t } = useTranslationContext();
if (!assetUrl) return null;
const href = sanitizeUrl(assetUrl);
Expand Down
9 changes: 6 additions & 3 deletions src/components/Avatar/Avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ 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 */
Expand Down Expand Up @@ -51,14 +51,17 @@ const getInitials = (name?: string) => {
*/
export const Avatar = ({
className,
FallbackIcon = IconUser,
FallbackIcon,
imageUrl,
initials: customInitials,
isOnline,
size,
userName,
...rest
}: AvatarProps) => {
const { IconUser } = useComponentContextIcons();
// The prop still wins over the context slot: it targets one avatar, the slot rebrands all of them.
const ResolvedFallbackIcon = FallbackIcon ?? IconUser;
const [error, setError] = useState(false);

useEffect(() => () => setError(false), [imageUrl]);
Expand Down Expand Up @@ -113,7 +116,7 @@ export const Avatar = ({
{sizeAwareInitials}
</div>
)}
{!sizeAwareInitials.length && <FallbackIcon />}
{!sizeAwareInitials.length && <ResolvedFallbackIcon />}
</>
)}
</div>
Expand Down
16 changes: 10 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,12 @@ 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>
);
};
14 changes: 8 additions & 6 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, LocalVoiceRecordingAttachment } from 'stream-chat';
import clsx from 'clsx';
Expand All @@ -10,12 +10,14 @@ export type MediaBadgeProps = {
variant: 'video' | 'voice-recording' | string;
};

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

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

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

const Icon = MediaBadgeVariantToIcon[variant];
const { duration } = (attachment as LocalVoiceRecordingAttachment).custom ?? {};
return (
Expand Down
4 changes: 3 additions & 1 deletion src/components/BaseImage/ImagePlaceholder.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
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 { t } = useTranslationContext();
const { IconImage } = useComponentContextIcons();

return (
<div
aria-label={t(
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,15 +2,16 @@ 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 { t } = useTranslationContext();
const { IconPauseFill, IconPlayFill } = useComponentContextIcons();

return (
<Button
appearance='outline'
Expand Down
Loading
Loading