diff --git a/src/components/Attachment/Geolocation.tsx b/src/components/Attachment/Geolocation.tsx index e997bb259..9ef901548 100644 --- a/src/components/Attachment/Geolocation.tsx +++ b/src/components/Attachment/Geolocation.tsx @@ -3,9 +3,13 @@ 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, + useComponentContext, + useTranslationContext, +} from '../../context'; import { ExternalLinkIcon } from './icons'; -import { IconLocation } from '../Icons'; +import { IconLocation as DefaultIconLocation } from '../Icons'; import { Button } from '../Button'; export type GeolocationMapProps = Coords; @@ -102,6 +106,7 @@ const DefaultGeolocationAttachmentMapPlaceholder = ({ location, }: GeolocationAttachmentMapPlaceholderProps) => { const { t } = useTranslationContext(); + const { icons: { IconLocation = DefaultIconLocation } = {} } = useComponentContext(); return (
{ ); }; -const GiphyBadge = () => ( -
- - Giphy -
-); +const GiphyBadge = () => { + const { icons: { IconGiphy = DefaultIconGiphy } = {} } = useComponentContext(); + return ( +
+ + Giphy +
+ ); +}; diff --git a/src/components/Attachment/LinkPreview/Card.tsx b/src/components/Attachment/LinkPreview/Card.tsx index 365a8088d..caa488a12 100644 --- a/src/components/Attachment/LinkPreview/Card.tsx +++ b/src/components/Attachment/LinkPreview/Card.tsx @@ -2,11 +2,12 @@ import React from 'react'; import { BaseImage } from '../../BaseImage'; import { SafeAnchor } from '../../SafeAnchor'; import { useChannelStateContext } from '../../../context/ChannelStateContext'; +import { useComponentContext } from '../../../context'; import type { Attachment } from 'stream-chat'; import type { RenderAttachmentProps } from '../utils'; import type { Dimensions } from '../../../types/types'; -import { IconLink } from '../../Icons'; +import { IconLink as DefaultIconLink } from '../../Icons'; import { UnableToRenderCard } from './UnableToRenderCard'; import clsx from 'clsx'; @@ -64,6 +65,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 { icons: { IconLink = DefaultIconLink } = {} } = useComponentContext(); return (
diff --git a/src/components/Attachment/LinkPreview/CardAudio.tsx b/src/components/Attachment/LinkPreview/CardAudio.tsx index 538edbe44..4072386d4 100644 --- a/src/components/Attachment/LinkPreview/CardAudio.tsx +++ b/src/components/Attachment/LinkPreview/CardAudio.tsx @@ -1,10 +1,10 @@ import { type AudioPlayerState, ProgressBar, useAudioPlayer } from '../../AudioPlayback'; -import { useMessageContext } from '../../../context'; +import { useComponentContext, 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 { IconLink as DefaultIconLink } from '../../Icons'; import { SafeAnchor } from '../../SafeAnchor'; import type { CardProps } from './Card'; @@ -21,22 +21,25 @@ const SourceLink = ({ author_name, showUrl, url, -}: Pick & { url: string; showUrl?: boolean }) => ( -
- - & { url: string; showUrl?: boolean }) => { + const { icons: { IconLink = DefaultIconLink } = {} } = useComponentContext(); + return ( +
- {showUrl ? url : author_name || getHostFromURL(url)} - -
-); + + + {showUrl ? url : author_name || getHostFromURL(url)} + +
+ ); +}; const audioPlayerStateSelector = (state: AudioPlayerState) => ({ durationSeconds: state.durationSeconds, diff --git a/src/components/Attachment/ModalGallery.tsx b/src/components/Attachment/ModalGallery.tsx index 3557c68be..4e8760cd9 100644 --- a/src/components/Attachment/ModalGallery.tsx +++ b/src/components/Attachment/ModalGallery.tsx @@ -8,7 +8,7 @@ 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 { IconRetry as DefaultIconRetry } from '../Icons'; import { VideoThumbnail } from '../VideoPlayer/VideoThumbnail'; const MAX_VISIBLE_THUMBNAILS = 4; @@ -144,6 +144,7 @@ const ThumbnailButton = ({ showOverlay, }: ThumbnailButtonProps) => { const { t } = useTranslationContext(); + const { icons: { IconRetry = DefaultIconRetry } = {} } = useComponentContext(); const imageUrl = item.imageUrl; const [isLoadFailed, setIsLoadFailed] = useState(false); const [isImageLoading, setIsImageLoading] = useState(Boolean(imageUrl)); diff --git a/src/components/Attachment/UnsupportedAttachment.tsx b/src/components/Attachment/UnsupportedAttachment.tsx index b91fcb6d1..75bbaeccc 100644 --- a/src/components/Attachment/UnsupportedAttachment.tsx +++ b/src/components/Attachment/UnsupportedAttachment.tsx @@ -1,13 +1,16 @@ import React from 'react'; import type { Attachment } from 'stream-chat'; -import { useTranslationContext } from '../../context'; -import { IconUnsupportedAttachment } from '../Icons'; +import { useComponentContext, useTranslationContext } from '../../context'; +import { IconUnsupportedAttachment as DefaultIconUnsupportedAttachment } from '../Icons'; export type UnsupportedAttachmentProps = { attachment: Attachment; }; export const UnsupportedAttachment = () => { + const { icons: { IconUnsupportedAttachment = DefaultIconUnsupportedAttachment } = {} } = + useComponentContext(); + const { t } = useTranslationContext('UnsupportedAttachment'); return (
{ + const { icons: { IconEyeFill = DefaultIconEyeFill } = {} } = useComponentContext(); + const { t } = useTranslationContext(); return (
diff --git a/src/components/Attachment/components/DownloadButton.tsx b/src/components/Attachment/components/DownloadButton.tsx index 73c150dff..7c62eb819 100644 --- a/src/components/Attachment/components/DownloadButton.tsx +++ b/src/components/Attachment/components/DownloadButton.tsx @@ -2,8 +2,8 @@ import React from 'react'; import clsx from 'clsx'; import { sanitizeUrl } from '@braintree/sanitize-url'; -import { useTranslationContext } from '../../../context'; -import { IconDownload } from '../../Icons'; +import { useComponentContext, useTranslationContext } from '../../../context'; +import { IconDownload as DefaultIconDownload } from '../../Icons'; export type DownloadButtonProps = { /** Attachment asset URL (e.g. `asset_url`). */ @@ -25,6 +25,8 @@ export const DownloadButton = ({ suggestedFileName, tooltipTitle, }: DownloadButtonProps) => { + const { icons: { IconDownload = DefaultIconDownload } = {} } = useComponentContext(); + const { t } = useTranslationContext(); if (!assetUrl) return null; const href = sanitizeUrl(assetUrl); diff --git a/src/components/Avatar/Avatar.tsx b/src/components/Avatar/Avatar.tsx index af2fa6b76..c8917aad8 100644 --- a/src/components/Avatar/Avatar.tsx +++ b/src/components/Avatar/Avatar.tsx @@ -6,10 +6,15 @@ import React, { useMemo, useState, } from 'react'; -import { IconUser } from '../Icons'; +import { useComponentContext } from '../../context'; +import { IconUser as DefaultIconUser } from '../Icons'; 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 ``) instead. + * Passing this prop still wins over the context slot for backwards compatibility. + */ FallbackIcon?: ComponentType>; /** URL of the avatar image */ imageUrl?: string; @@ -46,13 +51,15 @@ const getInitials = (name?: string) => { */ export const Avatar = ({ className, - FallbackIcon = IconUser, + FallbackIcon, imageUrl, isOnline, size, userName, ...rest }: AvatarProps) => { + const { icons: { IconUser = DefaultIconUser } = {} } = useComponentContext(); + const ResolvedFallbackIcon = FallbackIcon ?? IconUser; const [error, setError] = useState(false); useEffect(() => () => setError(false), [imageUrl]); @@ -108,7 +115,7 @@ export const Avatar = ({ {sizeAwareInitials}
)} - {!sizeAwareInitials.length && } + {!sizeAwareInitials.length && } )}
diff --git a/src/components/Badge/Badge.tsx b/src/components/Badge/Badge.tsx index 66fe564f7..1a8e0ba62 100644 --- a/src/components/Badge/Badge.tsx +++ b/src/components/Badge/Badge.tsx @@ -1,6 +1,7 @@ import clsx from 'clsx'; import React, { type ComponentProps } from 'react'; -import { IconExclamationMarkFill } from '../Icons'; +import { useComponentContext } from '../../context'; +import { IconExclamationMarkFill as DefaultIconExclamationMarkFill } from '../Icons'; export type BadgeVariant = | 'default' @@ -47,8 +48,12 @@ export const ErrorBadge = ({ className, size = 'sm', ...rest -}: Omit) => ( - - - -); +}: Omit) => { + const { icons: { IconExclamationMarkFill = DefaultIconExclamationMarkFill } = {} } = + useComponentContext(); + return ( + + + + ); +}; diff --git a/src/components/Badge/MediaBadge.tsx b/src/components/Badge/MediaBadge.tsx index 05129ccbd..7ce4a165d 100644 --- a/src/components/Badge/MediaBadge.tsx +++ b/src/components/Badge/MediaBadge.tsx @@ -1,4 +1,8 @@ -import { IconMicrophoneSolid, IconVideoFill } from '../Icons'; +import { useComponentContext } from '../../context'; +import { + IconMicrophoneSolid as DefaultIconMicrophoneSolid, + IconVideoFill as DefaultIconVideoFill, +} from '../Icons'; import React, { type ComponentType } from 'react'; import type { LocalAttachment } from 'stream-chat'; import clsx from 'clsx'; @@ -10,13 +14,20 @@ export type MediaBadgeProps = { variant: 'video' | 'voice-recording' | string; }; -const MediaBadgeVariantToIcon: Record = { - video: IconVideoFill, - voiceRecording: IconMicrophoneSolid, -}; - export const MediaBadge = ({ attachment, variant }: MediaBadgeProps) => { - const Icon = MediaBadgeVariantToIcon[variant]; + const { + icons: { + IconMicrophoneSolid = DefaultIconMicrophoneSolid, + IconVideoFill = DefaultIconVideoFill, + } = {}, + } = useComponentContext(); + + const mediaBadgeVariantToIcon: Record = { + video: IconVideoFill, + voiceRecording: IconMicrophoneSolid, + }; + + const Icon = mediaBadgeVariantToIcon[variant]; return (
{ + const { icons: { IconImage = DefaultIconImage } = {} } = useComponentContext(); + const { t } = useTranslationContext(); return (
& { isPlaying: boolean; }; export const PlayButton = ({ className, isPlaying, ...props }: PlayButtonProps) => { + const { + icons: { + IconPauseFill = DefaultIconPauseFill, + IconPlayFill = DefaultIconPlayFill, + } = {}, + } = useComponentContext(); + const { t } = useTranslationContext(); return ( -); +}: BaseContextMenuButtonProps) => { + const { icons: { IconChevronRight = DefaultIconChevronRight } = {} } = + useComponentContext(); + const ResolvedSubmenuIcon = SubmenuIcon ?? IconChevronRight; + return ( + + ); +}; export type UserContextMenuButtonProps = Pick & ComponentProps<'button'>; @@ -668,6 +680,8 @@ export function ContextMenuContent({ ...props }: ContextMenuContentProps) { const { t } = useTranslationContext(); + const { icons: { IconChevronLeft = DefaultIconChevronLeft } = {} } = + useComponentContext(); const resolvedBackLabel = backLabel ?? t('Back'); const { ['aria-describedby']: rootAriaDescribedBy, diff --git a/src/components/Dialog/components/Prompt.tsx b/src/components/Dialog/components/Prompt.tsx index 87eb2c576..528c67804 100644 --- a/src/components/Dialog/components/Prompt.tsx +++ b/src/components/Dialog/components/Prompt.tsx @@ -1,8 +1,15 @@ import React, { type ComponentProps, type PropsWithChildren } from 'react'; import clsx from 'clsx'; import { Button, type ButtonProps } from '../../Button'; -import { IconArrowLeft, IconXmark } from '../../Icons'; -import { useModalContext, useTranslationContext } from '../../../context'; +import { + IconArrowLeft as DefaultIconArrowLeft, + IconXmark as DefaultIconXmark, +} from '../../Icons'; +import { + useComponentContext, + useModalContext, + useTranslationContext, +} from '../../../context'; import { useAriaIdentifiers } from '../../../a11y/hooks/useAriaIdentifiers'; const PromptRoot = ({ children, className, ...props }: ComponentProps<'div'>) => ( @@ -36,6 +43,9 @@ const PromptHeader = ({ }: PromptHeaderProps) => { const { t } = useTranslationContext(); const { dialogId } = useModalContext(); + const { + icons: { IconArrowLeft = DefaultIconArrowLeft, IconXmark = DefaultIconXmark } = {}, + } = useComponentContext(); const { descriptionId: derivedDescriptionId, titleId: derivedTitleId } = useAriaIdentifiers(dialogId); const resolvedTitleId = titleId ?? derivedTitleId; diff --git a/src/components/Dialog/components/Viewer.tsx b/src/components/Dialog/components/Viewer.tsx index 70b5ec903..9dcf9dd61 100644 --- a/src/components/Dialog/components/Viewer.tsx +++ b/src/components/Dialog/components/Viewer.tsx @@ -1,8 +1,15 @@ import React, { type ComponentProps, type PropsWithChildren } from 'react'; import clsx from 'clsx'; import { Button, type ButtonProps } from '../../Button'; -import { IconArrowLeft, IconXmark } from '../../Icons'; -import { useModalContext, useTranslationContext } from '../../../context'; +import { + IconArrowLeft as DefaultIconArrowLeft, + IconXmark as DefaultIconXmark, +} from '../../Icons'; +import { + useComponentContext, + useModalContext, + useTranslationContext, +} from '../../../context'; import { useAriaIdentifiers } from '../../../a11y/hooks/useAriaIdentifiers'; const ViewerRoot = ({ children, className, ...props }: ComponentProps<'div'>) => ( @@ -32,6 +39,9 @@ const ViewerHeader = ({ }: ViewerHeaderProps) => { const { t } = useTranslationContext(); const { dialogId } = useModalContext(); + const { + icons: { IconArrowLeft = DefaultIconArrowLeft, IconXmark = DefaultIconXmark } = {}, + } = useComponentContext(); const { descriptionId: derivedDescriptionId, titleId: derivedTitleId } = useAriaIdentifiers(dialogId); const resolvedTitleId = titleId ?? derivedTitleId; diff --git a/src/components/EmptyStateIndicator/EmptyStateIndicator.tsx b/src/components/EmptyStateIndicator/EmptyStateIndicator.tsx index 1f18a4faa..7c7be2d73 100644 --- a/src/components/EmptyStateIndicator/EmptyStateIndicator.tsx +++ b/src/components/EmptyStateIndicator/EmptyStateIndicator.tsx @@ -1,7 +1,11 @@ import React from 'react'; +import { useComponentContext } from '../../context'; import { useTranslationContext } from '../../context/TranslationContext'; -import { IconMessageBubble, IconMessageBubbles } from '../Icons'; +import { + IconMessageBubble as DefaultIconMessageBubble, + IconMessageBubbles as DefaultIconMessageBubbles, +} from '../Icons'; export type EmptyStateIndicatorProps = { /** List Type: channel | message */ @@ -13,6 +17,12 @@ const UnMemoizedEmptyStateIndicator = (props: EmptyStateIndicatorProps) => { const { listType, messageText } = props; const { t } = useTranslationContext('EmptyStateIndicator'); + const { + icons: { + IconMessageBubble = DefaultIconMessageBubble, + IconMessageBubbles = DefaultIconMessageBubbles, + } = {}, + } = useComponentContext(); if (listType === 'thread') return null; diff --git a/src/components/Form/NumericInput.tsx b/src/components/Form/NumericInput.tsx index e58e7f8b5..730773e40 100644 --- a/src/components/Form/NumericInput.tsx +++ b/src/components/Form/NumericInput.tsx @@ -1,9 +1,12 @@ import clsx from 'clsx'; import React, { forwardRef, useCallback } from 'react'; import type { ChangeEvent, ComponentProps, KeyboardEvent } from 'react'; -import { useTranslationContext } from '../../context'; +import { useComponentContext, useTranslationContext } from '../../context'; import { useStableId } from '../UtilityComponents/useStableId'; -import { IconMinus, IconPlusSmall } from '../Icons'; +import { + IconMinus as DefaultIconMinus, + IconPlusSmall as DefaultIconPlusSmall, +} from '../Icons'; import { Button } from '../Button'; export type NumericInputProps = Omit< @@ -52,6 +55,9 @@ export const NumericInput = forwardRef( const generatedId = useStableId(); const id = idProp ?? generatedId; const { t } = useTranslationContext(); + const { + icons: { IconMinus = DefaultIconMinus, IconPlusSmall = DefaultIconPlusSmall } = {}, + } = useComponentContext(); const num = parseNumeric(value); const minDef = min ?? -Infinity; diff --git a/src/components/Form/TextInput.tsx b/src/components/Form/TextInput.tsx index cb6479f67..9f6058551 100644 --- a/src/components/Form/TextInput.tsx +++ b/src/components/Form/TextInput.tsx @@ -2,7 +2,11 @@ import clsx from 'clsx'; import React, { forwardRef } from 'react'; import type { ComponentProps, ReactNode } from 'react'; import { useStableId } from '../UtilityComponents/useStableId'; -import { IconCheckmark, IconExclamationMark } from '../Icons'; +import { useComponentContext } from '../../context'; +import { + IconCheckmark as DefaultIconCheckmark, + IconExclamationMark as DefaultIconExclamationMark, +} from '../Icons'; export type TextInputVariant = 'outline' | 'ghost'; @@ -79,6 +83,12 @@ type TextInputFieldMessageProps = }; const TextInputFieldMessage = (props: TextInputFieldMessageProps) => { + const { + icons: { + IconCheckmark = DefaultIconCheckmark, + IconExclamationMark = DefaultIconExclamationMark, + } = {}, + } = useComponentContext(); if (props.kind === 'neutral') { return (
{ + const { + icons: { + IconArrowDownCircle = DefaultIconArrowDownCircle, + IconXmark = DefaultIconXmark, + } = {}, + } = useComponentContext(); + const { t } = useTranslationContext(); const { MessageTimestamp = DefaultMessageTimestamp } = useComponentContext('GalleryUI'); const { isMyMessage, message } = useMessageContext('GalleryUI'); diff --git a/src/components/Gallery/GalleryUI.tsx b/src/components/Gallery/GalleryUI.tsx index 4112fd415..59dabbce0 100644 --- a/src/components/Gallery/GalleryUI.tsx +++ b/src/components/Gallery/GalleryUI.tsx @@ -4,8 +4,11 @@ import { BaseImage } from '../BaseImage'; import { GalleryHeader } from './GalleryHeader'; import { useGalleryContext } from './GalleryContext'; import { Button, type ButtonProps } from '../Button'; -import { IconChevronLeft, IconChevronRight } from '../Icons'; -import { ModalContext, useTranslationContext } from '../../context'; +import { + IconChevronLeft as DefaultIconChevronLeft, + IconChevronRight as DefaultIconChevronRight, +} from '../Icons'; +import { ModalContext, useComponentContext, useTranslationContext } from '../../context'; import { VideoPlayer } from '../VideoPlayer'; import { VideoThumbnail } from '../VideoPlayer/VideoThumbnail'; @@ -15,6 +18,13 @@ const SWIPE_THRESHOLD = 50; const TRANSITION_DURATION = 300; export const GalleryUI = () => { + const { + icons: { + IconChevronLeft = DefaultIconChevronLeft, + IconChevronRight = DefaultIconChevronRight, + } = {}, + } = useComponentContext(); + const { t } = useTranslationContext(); const { closeOnBackgroundClick, diff --git a/src/components/Icons/index.ts b/src/components/Icons/index.ts index 01f3a3b08..47ee77c42 100644 --- a/src/components/Icons/index.ts +++ b/src/components/Icons/index.ts @@ -1,2 +1,3 @@ export { createIcon } from './createIcon'; export * from './icons'; +export * from './slots'; diff --git a/src/components/Icons/slots.ts b/src/components/Icons/slots.ts new file mode 100644 index 000000000..f05d1f92c --- /dev/null +++ b/src/components/Icons/slots.ts @@ -0,0 +1,101 @@ +import type { ComponentPropsWithoutRef, ComponentType } from 'react'; + +export type IconComponent = ComponentType>; + +/** + * Names of icons that can be overridden via `ComponentContext.icons`. Enumerated from icons + * actually imported from `../components/Icons` anywhere in `src/`. Overrides are deep-merged + * with sibling entries by `WithComponents`, so a consumer can rebrand a single icon without + * clearing the others. + */ +export type IconSlots = Partial< + Record< + | 'IconArchive' + | 'IconArrowDown' + | 'IconArrowDownCircle' + | 'IconArrowLeft' + | 'IconArrowUp' + | 'IconArrowUpRight' + | 'IconAttachment' + | 'IconAudio' + | 'IconBell' + | 'IconBellOff' + | 'IconBolt' + | 'IconBookmark' + | 'IconBookmarkRemove' + | 'IconCamera' + | 'IconCheckmark' + | 'IconCheckmark1Small' + | 'IconChecks' + | 'IconChevronDown' + | 'IconChevronLeft' + | 'IconChevronRight' + | 'IconClock' + | 'IconCommand' + | 'IconCopy' + | 'IconDelete' + | 'IconDownload' + | 'IconEdit' + | 'IconEmoji' + | 'IconEmojiAdd' + | 'IconExclamationCircleFill' + | 'IconExclamationMark' + | 'IconExclamationMarkFill' + | 'IconExclamationTriangleFill' + | 'IconEyeFill' + | 'IconFile' + | 'IconFlag' + | 'IconFolder' + | 'IconGiphy' + | 'IconImage' + | 'IconInfo' + | 'IconLeave' + | 'IconLink' + | 'IconLoading' + | 'IconLocation' + | 'IconMegaphone' + | 'IconMenu' + | 'IconMessageBubble' + | 'IconMessageBubbleFill' + | 'IconMessageBubbles' + | 'IconMicrophoneSolid' + | 'IconMinus' + | 'IconMinusCircle' + | 'IconMore' + | 'IconMute' + | 'IconNoSign' + | 'IconNotification' + | 'IconPauseFill' + | 'IconPin' + | 'IconPlayFill' + | 'IconPlus' + | 'IconPlusSmall' + | 'IconPoll' + | 'IconQuote' + | 'IconRefresh' + | 'IconReorder' + | 'IconReply' + | 'IconRetry' + | 'IconSearch' + | 'IconSend' + | 'IconShield' + | 'IconThread' + | 'IconThreadFill' + | 'IconTranslate' + | 'IconTrophy' + | 'IconUnpin' + | 'IconUnsupportedAttachment' + | 'IconUpload' + | 'IconUser' + | 'IconUserAdd' + | 'IconUserCheck' + | 'IconUserRemove' + | 'IconUsers' + | 'IconVideo' + | 'IconVideoFill' + | 'IconVoice' + | 'IconXmark' + | 'IconXmarkSmall', + IconComponent + > +>; diff --git a/src/components/Loading/LoadingIndicator.tsx b/src/components/Loading/LoadingIndicator.tsx index 1ba631195..69a040ad3 100644 --- a/src/components/Loading/LoadingIndicator.tsx +++ b/src/components/Loading/LoadingIndicator.tsx @@ -1,8 +1,10 @@ import React, { type ComponentProps } from 'react'; -import { IconLoading } from '../Icons'; +import { useComponentContext } from '../../context'; +import { IconLoading as DefaultIconLoading } from '../Icons'; -export type LoadingIndicatorProps = ComponentProps; +export type LoadingIndicatorProps = ComponentProps; -export const LoadingIndicator = (props: LoadingIndicatorProps) => ( - -); +export const LoadingIndicator = (props: LoadingIndicatorProps) => { + const { icons: { IconLoading = DefaultIconLoading } = {} } = useComponentContext(); + return ; +}; diff --git a/src/components/Location/ShareLocationDialog.tsx b/src/components/Location/ShareLocationDialog.tsx index 5496147b9..80742f1e3 100644 --- a/src/components/Location/ShareLocationDialog.tsx +++ b/src/components/Location/ShareLocationDialog.tsx @@ -5,14 +5,14 @@ import React, { useMemo, useState, } from 'react'; -import { useTranslationContext } from '../../context'; +import { useComponentContext, useTranslationContext } from '../../context'; import { ContextMenuBody, ContextMenuButton, ContextMenuRoot, Prompt } from '../Dialog'; import { Dropdown, type DropdownTriggerProps, useDropdownContext, } from '../Form/Dropdown'; -import { IconChevronDown } from '../Icons'; +import { IconChevronDown as DefaultIconChevronDown } from '../Icons'; import { useMessageComposerController } from '../MessageComposer/hooks/useMessageComposerController'; import { SwitchField } from '../Form/SwitchField'; import { useNotificationApi } from '../Notifications'; @@ -65,6 +65,9 @@ export const ShareLocationDialog = ({ GeolocationMap = DefaultGeolocationMap, shareDurations = DEFAULT_SHARE_LOCATION_DURATIONS, }: ShareLocationDialogProps) => { + const { icons: { IconChevronDown = DefaultIconChevronDown } = {} } = + useComponentContext(); + const { addNotification } = useNotificationApi(); const { t } = useTranslationContext(); const messageComposer = useMessageComposerController(); @@ -101,7 +104,7 @@ export const ShareLocationDialog = ({ ) : null, }), - [selectedDurationLabel], + [IconChevronDown, selectedDurationLabel], ); const getPosition = useCallback( diff --git a/src/components/MediaRecorder/AudioRecorder/AudioRecorderRecordingControls.tsx b/src/components/MediaRecorder/AudioRecorder/AudioRecorderRecordingControls.tsx index a3b96cb52..664f6c952 100644 --- a/src/components/MediaRecorder/AudioRecorder/AudioRecorderRecordingControls.tsx +++ b/src/components/MediaRecorder/AudioRecorder/AudioRecorderRecordingControls.tsx @@ -1,13 +1,24 @@ import { CheckSignIcon } from '../../MessageComposer/icons'; -import { IconDelete, IconPauseFill, IconVoice } from '../../Icons'; +import { + IconDelete as DefaultIconDelete, + IconPauseFill as DefaultIconPauseFill, + IconVoice as DefaultIconVoice, +} from '../../Icons'; import React from 'react'; -import { useMessageComposerContext, useTranslationContext } from '../../../context'; +import { + useComponentContext, + useMessageComposerContext, + useTranslationContext, +} from '../../../context'; import { isRecording } from './recordingStateIdentity'; import { Button } from '../../Button'; import { useNotificationApi } from '../../Notifications'; import { UploadProgressIndicator } from '../../Loading/UploadProgressIndicator'; const ToggleRecordingButton = () => { + const { + icons: { IconPauseFill = DefaultIconPauseFill, IconVoice = DefaultIconVoice } = {}, + } = useComponentContext(); const { t } = useTranslationContext(); const { recordingController: { recorder, recordingState }, @@ -31,6 +42,8 @@ const ToggleRecordingButton = () => { }; export const AudioRecorderRecordingControls = () => { + const { icons: { IconDelete = DefaultIconDelete } = {} } = useComponentContext(); + const { addNotification } = useNotificationApi(); const { t } = useTranslationContext(); const { diff --git a/src/components/MediaRecorder/AudioRecorder/AudioRecordingButtonWithNotification.tsx b/src/components/MediaRecorder/AudioRecorder/AudioRecordingButtonWithNotification.tsx index 03abcdf0f..39a6885c6 100644 --- a/src/components/MediaRecorder/AudioRecorder/AudioRecordingButtonWithNotification.tsx +++ b/src/components/MediaRecorder/AudioRecorder/AudioRecordingButtonWithNotification.tsx @@ -9,7 +9,7 @@ import { } from '../../../context'; import { Callout, useDialogOnNearestManager } from '../../Dialog'; import { Button } from '../../Button'; -import { IconVoice } from '../../Icons'; +import { IconVoice as DefaultIconVoice } from '../../Icons'; const dialogId = 'recording-permission-denied-notification'; @@ -69,6 +69,7 @@ export const DefaultStartRecordingAudioButton = forwardRef< StartRecordingAudioButtonProps >(function StartRecordingAudioButton(props, ref) { const { t } = useTranslationContext(); + const { icons: { IconVoice = DefaultIconVoice } = {} } = useComponentContext(); return ( -); +}: ComponentProps<'button'>) => { + const { icons: { IconXmark = DefaultIconXmark } = {} } = useComponentContext(); + return ( + + ); +}; diff --git a/src/components/Notifications/Notification.tsx b/src/components/Notifications/Notification.tsx index 75270f2e6..0b7574373 100644 --- a/src/components/Notifications/Notification.tsx +++ b/src/components/Notifications/Notification.tsx @@ -3,12 +3,13 @@ import clsx from 'clsx'; import type { NotificationSeverity } from 'stream-chat'; import { type Notification as NotificationType } from 'stream-chat'; +import { useComponentContext } from '../../context'; import { - IconCheckmark, - IconExclamationMark, - IconExclamationTriangleFill, - IconRefresh, - IconXmark, + IconCheckmark as DefaultIconCheckmark, + IconExclamationMark as DefaultIconExclamationMark, + IconExclamationTriangleFill as DefaultIconExclamationTriangleFill, + IconRefresh as DefaultIconRefresh, + IconXmark as DefaultIconXmark, } from '../../components/Icons'; import { useTranslationContext } from '../../context/TranslationContext'; import { Button } from '../Button'; @@ -21,18 +22,26 @@ export type NotificationIconProps = { notification: NotificationType; }; -const IconsBySeverity: Record = { - error: IconExclamationMark, - info: null, - loading: IconRefresh, - success: IconCheckmark, - warning: IconExclamationTriangleFill, -}; - const DefaultNotificationIcon = ({ notification }: NotificationIconProps) => { + const { + icons: { + IconCheckmark = DefaultIconCheckmark, + IconExclamationMark = DefaultIconExclamationMark, + IconExclamationTriangleFill = DefaultIconExclamationTriangleFill, + IconRefresh = DefaultIconRefresh, + } = {}, + } = useComponentContext(); if (!notification.severity) return null; - const Icon = IconsBySeverity[notification.severity] ?? null; + const iconsBySeverity: Record = { + error: IconExclamationMark, + info: null, + loading: IconRefresh, + success: IconCheckmark, + warning: IconExclamationTriangleFill, + }; + + const Icon = iconsBySeverity[notification.severity] ?? null; return ( Icon && (
@@ -72,6 +81,8 @@ export const Notification = forwardRef( }: NotificationProps, ref, ) => { + const { icons: { IconXmark = DefaultIconXmark } = {} } = useComponentContext(); + const { removeNotification } = useNotificationApi(); const { t } = useTranslationContext(); diff --git a/src/components/Poll/PollActions/PollResults/PollOptionWithVotesHeader.tsx b/src/components/Poll/PollActions/PollResults/PollOptionWithVotesHeader.tsx index 6e37c8bfc..f68ccdde0 100644 --- a/src/components/Poll/PollActions/PollResults/PollOptionWithVotesHeader.tsx +++ b/src/components/Poll/PollActions/PollResults/PollOptionWithVotesHeader.tsx @@ -1,8 +1,12 @@ import React from 'react'; import { useStateStore } from '../../../../store'; -import { usePollContext, useTranslationContext } from '../../../../context'; +import { + useComponentContext, + usePollContext, + useTranslationContext, +} from '../../../../context'; import type { PollOption, PollState } from 'stream-chat'; -import { IconTrophy } from '../../../Icons'; +import { IconTrophy as DefaultIconTrophy } from '../../../Icons'; type PollStateSelectorReturnValue = { maxVotedOptionIds: string[]; @@ -20,6 +24,8 @@ export type PollResultOptionVoteCounterProps = { export const PollResultOptionVoteCounter = ({ optionId, }: PollResultOptionVoteCounterProps) => { + const { icons: { IconTrophy = DefaultIconTrophy } = {} } = useComponentContext(); + const { t } = useTranslationContext(); const { poll } = usePollContext(); const { maxVotedOptionIds, vote_counts_by_option } = useStateStore( diff --git a/src/components/Poll/PollCreationDialog/OptionFieldSet.tsx b/src/components/Poll/PollCreationDialog/OptionFieldSet.tsx index e597ce70a..f6284bb16 100644 --- a/src/components/Poll/PollCreationDialog/OptionFieldSet.tsx +++ b/src/components/Poll/PollCreationDialog/OptionFieldSet.tsx @@ -1,11 +1,11 @@ import clsx from 'clsx'; import React, { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { TextInput } from '../../Form/TextInput'; -import { useTranslationContext } from '../../../context'; +import { useComponentContext, useTranslationContext } from '../../../context'; import { useMessageComposerController } from '../../MessageComposer/hooks/useMessageComposerController'; import { useStateStore } from '../../../store'; import type { PollComposerOption, PollComposerState } from 'stream-chat'; -import { IconMinusCircle } from '../../Icons'; +import { IconMinusCircle as DefaultIconMinusCircle } from '../../Icons'; import { Button, type ButtonProps } from '../../Button'; import { TextInputFieldSet } from '../../Form/TextInputFieldSet'; import { VisuallyHidden } from '../../VisuallyHidden'; @@ -281,15 +281,19 @@ export const OptionFieldSet = () => { ); }; -const RemoveOptionButton = ({ className, ...props }: ButtonProps) => ( - -); +const RemoveOptionButton = ({ className, ...props }: ButtonProps) => { + const { icons: { IconMinusCircle = DefaultIconMinusCircle } = {} } = + useComponentContext(); + return ( + + ); +}; diff --git a/src/components/Poll/PollCreationDialog/PollCreationDialogControls.tsx b/src/components/Poll/PollCreationDialog/PollCreationDialogControls.tsx index b218f909e..d0e4b5bbc 100644 --- a/src/components/Poll/PollCreationDialog/PollCreationDialogControls.tsx +++ b/src/components/Poll/PollCreationDialog/PollCreationDialogControls.tsx @@ -2,9 +2,13 @@ import React from 'react'; import { flushSync } from 'react-dom'; import { useCanCreatePoll } from '../../MessageComposer/hooks/useCanCreatePoll'; import { useMessageComposerController } from '../../MessageComposer/hooks/useMessageComposerController'; -import { useMessageComposerContext, useTranslationContext } from '../../../context'; +import { + useComponentContext, + useMessageComposerContext, + useTranslationContext, +} from '../../../context'; import clsx from 'clsx'; -import { IconSend } from '../../Icons'; +import { IconSend as DefaultIconSend } from '../../Icons'; import { Prompt } from '../../Dialog'; import { useNotificationApi } from '../../Notifications'; @@ -15,6 +19,8 @@ export type PollCreationDialogControlsProps = { export const PollCreationDialogControls = ({ close, }: PollCreationDialogControlsProps) => { + const { icons: { IconSend = DefaultIconSend } = {} } = useComponentContext(); + const { t } = useTranslationContext('PollCreationDialogControls'); const { handleSubmit: handleSubmitMessage, textareaRef } = useMessageComposerContext(); const messageComposer = useMessageComposerController(); diff --git a/src/components/Poll/PollCreationDialog/PollOptionReorderHandle.tsx b/src/components/Poll/PollCreationDialog/PollOptionReorderHandle.tsx index edcc0a14d..652bbe6fb 100644 --- a/src/components/Poll/PollCreationDialog/PollOptionReorderHandle.tsx +++ b/src/components/Poll/PollCreationDialog/PollOptionReorderHandle.tsx @@ -2,9 +2,9 @@ import type { KeyboardEvent as ReactKeyboardEvent } from 'react'; import React, { useEffect, useRef } from 'react'; import type { PollComposerOption } from 'stream-chat'; -import { IconReorder } from '../../Icons'; +import { IconReorder as DefaultIconReorder } from '../../Icons'; import { useAriaLiveAnnouncer } from '../../Accessibility'; -import { useTranslationContext } from '../../../context'; +import { useComponentContext, useTranslationContext } from '../../../context'; type PollOptionReorderHandleProps = { index: number; @@ -30,6 +30,8 @@ export const PollOptionReorderHandle = ({ registerRef, totalOptionCount, }: PollOptionReorderHandleProps) => { + const { icons: { IconReorder = DefaultIconReorder } = {} } = useComponentContext(); + const { t } = useTranslationContext(); const announce = useAriaLiveAnnouncer(); const hasAnnouncedFocusRef = useRef(false); diff --git a/src/components/Reactions/MessageReactionsDetail.tsx b/src/components/Reactions/MessageReactionsDetail.tsx index 689a899c4..86ba3522a 100644 --- a/src/components/Reactions/MessageReactionsDetail.tsx +++ b/src/components/Reactions/MessageReactionsDetail.tsx @@ -14,7 +14,7 @@ import { import type { ReactionSort } from 'stream-chat'; import { defaultReactionOptions, getHasExtendedReactions } from './reactionOptions'; import type { useProcessReactions } from './hooks/useProcessReactions'; -import { IconEmojiAdd } from '../Icons'; +import { IconEmojiAdd as DefaultIconEmojiAdd } from '../Icons'; import { ReactionSelector, type ReactionSelectorProps } from './ReactionSelector'; export type MessageReactionsDetailProps = Partial< @@ -66,6 +66,7 @@ export const MessageReactionsDetail: MessageReactionsDetailInterface = ({ const { client } = useChatContext(); const { Avatar = DefaultAvatar, + icons: { IconEmojiAdd = DefaultIconEmojiAdd } = {}, LoadingIndicator = MessageReactionsDetailLoadingIndicator, reactionOptions = defaultReactionOptions, ReactionSelectorExtendedList = ReactionSelector.ExtendedList, diff --git a/src/components/Reactions/ReactionSelector.tsx b/src/components/Reactions/ReactionSelector.tsx index 2cda7962f..e6d3d6ef1 100644 --- a/src/components/Reactions/ReactionSelector.tsx +++ b/src/components/Reactions/ReactionSelector.tsx @@ -7,7 +7,7 @@ import { useComponentContext } from '../../context/ComponentContext'; import { useMessageContext } from '../../context/MessageContext'; import { useTranslationContext } from '../../context/TranslationContext'; import { Button } from '../Button'; -import { IconPlus } from '../Icons'; +import { IconPlus as DefaultIconPlus } from '../Icons'; import type { ReactionResponse } from 'stream-chat'; @@ -41,6 +41,7 @@ export const ReactionSelector: ReactionSelectorInterface = (props) => { const [extendedListOpen, setExtendedListOpen] = useState(false); const { + icons: { IconPlus = DefaultIconPlus } = {}, reactionOptions = defaultReactionOptions, ReactionSelectorExtendedList = ReactionSelector.ExtendedList, } = useComponentContext('ReactionSelector'); diff --git a/src/components/Reactions/ReactionSelectorWithButton.tsx b/src/components/Reactions/ReactionSelectorWithButton.tsx index d489c81d3..d4cc48310 100644 --- a/src/components/Reactions/ReactionSelectorWithButton.tsx +++ b/src/components/Reactions/ReactionSelectorWithButton.tsx @@ -10,10 +10,15 @@ import { import type { IconProps } from '../../types/types'; import { QuickMessageActionsButton } from '../MessageActions'; +import { IconEmoji as DefaultIconEmoji } from '../Icons'; type ReactionSelectorWithButtonProps = { - /* Custom component rendering the icon used in a button invoking reactions selector for a given message. */ - ReactionIcon: React.ComponentType; + /** + * Custom component rendering the icon used in a button invoking reactions selector for a given message. + * @deprecated Use the `icons.IconEmoji` slot on `ComponentContext` (via ``) instead. + * Passing this prop still wins over the context slot for backwards compatibility. + */ + ReactionIcon?: React.ComponentType; }; /** @@ -25,8 +30,11 @@ export const ReactionSelectorWithButton = ({ }: ReactionSelectorWithButtonProps) => { const { t } = useTranslationContext('ReactionSelectorWithButton'); const { isMyMessage, message, threadList } = useMessageContext('MessageOptions'); - const { ReactionSelector = DefaultReactionSelector } = - useComponentContext('MessageOptions'); + const { + icons: { IconEmoji = DefaultIconEmoji } = {}, + ReactionSelector = DefaultReactionSelector, + } = useComponentContext(); + const ResolvedReactionIcon = ReactionIcon ?? IconEmoji; const buttonRef = useRef>(null); const dialogId = DefaultReactionSelector.getDialogId({ messageId: message.id, @@ -56,7 +64,7 @@ export const ReactionSelectorWithButton = ({ onClick={() => dialog?.toggle()} ref={buttonRef} > - + ); diff --git a/src/components/SummarizedMessagePreview/SummarizedMessagePreview.tsx b/src/components/SummarizedMessagePreview/SummarizedMessagePreview.tsx index acccac0f8..5561d1128 100644 --- a/src/components/SummarizedMessagePreview/SummarizedMessagePreview.tsx +++ b/src/components/SummarizedMessagePreview/SummarizedMessagePreview.tsx @@ -6,49 +6,69 @@ import { useLatestMessagePreview, type UseLatestMessagePreviewParams, } from './hooks/useLatestMessagePreview'; +import { useComponentContext } from '../../context'; import { - IconCamera, - IconCheckmark1Small, - IconChecks, - IconClock, - IconExclamationCircleFill, - IconFile, - IconGiphy, - IconLink, - IconLocation, - IconNoSign, - IconUnsupportedAttachment, - IconVideo, - IconVoice, + IconCamera as DefaultIconCamera, + IconCheckmark1Small as DefaultIconCheckmark1Small, + IconChecks as DefaultIconChecks, + IconClock as DefaultIconClock, + IconExclamationCircleFill as DefaultIconExclamationCircleFill, + IconFile as DefaultIconFile, + IconGiphy as DefaultIconGiphy, + IconLink as DefaultIconLink, + IconLocation as DefaultIconLocation, + IconNoSign as DefaultIconNoSign, + IconUnsupportedAttachment as DefaultIconUnsupportedAttachment, + IconVideo as DefaultIconVideo, + IconVoice as DefaultIconVoice, } from '../Icons'; -const deliveryStatusIconMap: Record = { - delivered: IconChecks, - read: IconChecks, - sending: IconClock, - sent: IconCheckmark1Small, -}; - -const contentTypeIconMap: Partial< - Record -> = { - deleted: IconNoSign, - error: IconExclamationCircleFill, - file: IconFile, - giphy: IconGiphy, - image: IconCamera, - link: IconLink, - location: IconLocation, - unsupported: IconUnsupportedAttachment, - video: IconVideo, - voice: IconVoice, -}; - export const SummarizedMessagePreview = ({ latestMessage, messageDeliveryStatus, participantCount, }: UseLatestMessagePreviewParams) => { + const { + icons: { + IconCamera = DefaultIconCamera, + IconCheckmark1Small = DefaultIconCheckmark1Small, + IconChecks = DefaultIconChecks, + IconClock = DefaultIconClock, + IconExclamationCircleFill = DefaultIconExclamationCircleFill, + IconFile = DefaultIconFile, + IconGiphy = DefaultIconGiphy, + IconLink = DefaultIconLink, + IconLocation = DefaultIconLocation, + IconNoSign = DefaultIconNoSign, + IconUnsupportedAttachment = DefaultIconUnsupportedAttachment, + IconVideo = DefaultIconVideo, + IconVoice = DefaultIconVoice, + } = {}, + } = useComponentContext(); + + const deliveryStatusIconMap: Record = + { + delivered: IconChecks, + read: IconChecks, + sending: IconClock, + sent: IconCheckmark1Small, + }; + + const contentTypeIconMap: Partial< + Record + > = { + deleted: IconNoSign, + error: IconExclamationCircleFill, + file: IconFile, + giphy: IconGiphy, + image: IconCamera, + link: IconLink, + location: IconLocation, + unsupported: IconUnsupportedAttachment, + video: IconVideo, + voice: IconVoice, + }; + const { deliveryStatus, senderName, text, type } = useLatestMessagePreview({ latestMessage, messageDeliveryStatus, diff --git a/src/components/TextareaComposer/SuggestionList/MentionItem/BroadcastMentionItem.tsx b/src/components/TextareaComposer/SuggestionList/MentionItem/BroadcastMentionItem.tsx index d6f1e7105..b76968a8c 100644 --- a/src/components/TextareaComposer/SuggestionList/MentionItem/BroadcastMentionItem.tsx +++ b/src/components/TextareaComposer/SuggestionList/MentionItem/BroadcastMentionItem.tsx @@ -1,9 +1,9 @@ import clsx from 'clsx'; import React from 'react'; import type { ChannelMentionSuggestion, HereMentionSuggestion } from 'stream-chat'; -import { IconMegaphone } from '../../../Icons'; +import { IconMegaphone as DefaultIconMegaphone } from '../../../Icons'; import { ListItemLayout } from '../../../ListItemLayout'; -import { useTranslationContext } from '../../../../context'; +import { useComponentContext, useTranslationContext } from '../../../../context'; import { MentionSuggestionTitle } from './MentionSuggestionTitle'; import type { MentionItemComponentProps } from './types'; @@ -16,6 +16,8 @@ export const BroadcastMentionItem = ({ focused, ...buttonProps }: BroadcastMentionItemProps) => { + const { icons: { IconMegaphone = DefaultIconMegaphone } = {} } = useComponentContext(); + const { t } = useTranslationContext(); const description = entity.mentionType === 'channel' diff --git a/src/components/TextareaComposer/SuggestionList/MentionItem/RoleItem.tsx b/src/components/TextareaComposer/SuggestionList/MentionItem/RoleItem.tsx index 0c3de993d..dbfdc12ef 100644 --- a/src/components/TextareaComposer/SuggestionList/MentionItem/RoleItem.tsx +++ b/src/components/TextareaComposer/SuggestionList/MentionItem/RoleItem.tsx @@ -1,9 +1,9 @@ import clsx from 'clsx'; import React from 'react'; import type { RoleMentionSuggestion } from 'stream-chat'; -import { IconShield } from '../../../Icons'; +import { IconShield as DefaultIconShield } from '../../../Icons'; import { ListItemLayout } from '../../../ListItemLayout'; -import { useTranslationContext } from '../../../../context'; +import { useComponentContext, useTranslationContext } from '../../../../context'; import { MentionSuggestionTitle } from './MentionSuggestionTitle'; import type { MentionItemComponentProps } from './types'; import { TokenizedSuggestionParts } from '../TokenizedSuggestionParts'; @@ -11,6 +11,8 @@ import { TokenizedSuggestionParts } from '../TokenizedSuggestionParts'; export type RoleItemProps = MentionItemComponentProps; export const RoleItem = ({ entity, focused, ...buttonProps }: RoleItemProps) => { + const { icons: { IconShield = DefaultIconShield } = {} } = useComponentContext(); + void focused; const { t } = useTranslationContext(); const role = entity.name; diff --git a/src/components/TextareaComposer/SuggestionList/MentionItem/UserGroupItem.tsx b/src/components/TextareaComposer/SuggestionList/MentionItem/UserGroupItem.tsx index cad38aca7..70692a2ba 100644 --- a/src/components/TextareaComposer/SuggestionList/MentionItem/UserGroupItem.tsx +++ b/src/components/TextareaComposer/SuggestionList/MentionItem/UserGroupItem.tsx @@ -1,7 +1,8 @@ import clsx from 'clsx'; import React from 'react'; import type { UserGroupMentionSuggestion } from 'stream-chat'; -import { IconUsers } from '../../../Icons'; +import { useComponentContext } from '../../../../context'; +import { IconUsers as DefaultIconUsers } from '../../../Icons'; import { ListItemLayout } from '../../../ListItemLayout'; import { MentionSuggestionTitle } from './MentionSuggestionTitle'; import type { MentionItemComponentProps } from './types'; @@ -14,6 +15,8 @@ export const UserGroupItem = ({ focused, ...buttonProps }: UserGroupItemProps) => { + const { icons: { IconUsers = DefaultIconUsers } = {} } = useComponentContext(); + void focused; return ( diff --git a/src/components/TextareaComposer/__tests__/CommandItem.test.tsx b/src/components/TextareaComposer/__tests__/CommandItem.test.tsx index 5dffad064..84f33e559 100644 --- a/src/components/TextareaComposer/__tests__/CommandItem.test.tsx +++ b/src/components/TextareaComposer/__tests__/CommandItem.test.tsx @@ -22,6 +22,7 @@ vi.mock('../../MessageComposer/hooks', () => ({ })); vi.mock('../../../context', () => ({ + useComponentContext: () => ({}), useTranslationContext: () => ({ t: (key: string) => key, }), diff --git a/src/components/Thread/ThreadHeader.tsx b/src/components/Thread/ThreadHeader.tsx index f61303e62..814148795 100644 --- a/src/components/Thread/ThreadHeader.tsx +++ b/src/components/Thread/ThreadHeader.tsx @@ -13,7 +13,7 @@ import { useTypingContext } from '../../context/TypingContext'; import type { LocalMessage } from 'stream-chat'; import type { ThreadState } from 'stream-chat'; import { Button } from '../Button'; -import { IconXmark } from '../Icons'; +import { IconXmark as DefaultIconXmark } from '../Icons'; import { useChatViewContext } from '../ChatView'; const threadStateSelector = ({ replyCount }: ThreadState) => ({ replyCount }); @@ -72,6 +72,8 @@ export type ThreadHeaderProps = { }; export const ThreadHeader = (props: ThreadHeaderProps) => { + const { icons: { IconXmark = DefaultIconXmark } = {} } = useComponentContext(); + const { closeThread, overrideTitle, thread } = props; const { t } = useTranslationContext(); diff --git a/src/components/Threads/ThreadList/ThreadListEmptyPlaceholder.tsx b/src/components/Threads/ThreadList/ThreadListEmptyPlaceholder.tsx index a15dc0e52..a2b03246e 100644 --- a/src/components/Threads/ThreadList/ThreadListEmptyPlaceholder.tsx +++ b/src/components/Threads/ThreadList/ThreadListEmptyPlaceholder.tsx @@ -1,9 +1,12 @@ import React from 'react'; -import { useTranslationContext } from '../../../context'; -import { IconMessageBubbles } from '../../Icons'; +import { useComponentContext, useTranslationContext } from '../../../context'; +import { IconMessageBubbles as DefaultIconMessageBubbles } from '../../Icons'; export const ThreadListEmptyPlaceholder = () => { + const { icons: { IconMessageBubbles = DefaultIconMessageBubbles } = {} } = + useComponentContext(); + const { t } = useTranslationContext('ThreadListEmptyPlaceholder'); return ( diff --git a/src/components/Threads/ThreadList/ThreadListUnseenThreadsBanner.tsx b/src/components/Threads/ThreadList/ThreadListUnseenThreadsBanner.tsx index 66a96d7aa..988badba6 100644 --- a/src/components/Threads/ThreadList/ThreadListUnseenThreadsBanner.tsx +++ b/src/components/Threads/ThreadList/ThreadListUnseenThreadsBanner.tsx @@ -3,8 +3,12 @@ import clsx from 'clsx'; import type { ThreadManagerState } from 'stream-chat'; -import { IconRefresh } from '../../Icons'; -import { useChatContext, useTranslationContext } from '../../../context'; +import { IconRefresh as DefaultIconRefresh } from '../../Icons'; +import { + useChatContext, + useComponentContext, + useTranslationContext, +} from '../../../context'; import { useStateStore } from '../../../store'; import { LoadingIndicator } from '../../Loading'; @@ -14,6 +18,8 @@ const selector = (nextValue: ThreadManagerState) => ({ }); export const ThreadListUnseenThreadsBanner = () => { + const { icons: { IconRefresh = DefaultIconRefresh } = {} } = useComponentContext(); + const { client } = useChatContext(); const { t } = useTranslationContext(); const { isLoading, unseenThreadIds } = useStateStore(client.threads.state, selector); diff --git a/src/components/VideoPlayer/VideoThumbnail.tsx b/src/components/VideoPlayer/VideoThumbnail.tsx index 724fb2a3a..296004557 100644 --- a/src/components/VideoPlayer/VideoThumbnail.tsx +++ b/src/components/VideoPlayer/VideoThumbnail.tsx @@ -1,9 +1,9 @@ import { BaseImage, type BaseImageProps } from '../BaseImage'; import { Button } from '../Button'; import clsx from 'clsx'; -import { IconPlayFill } from '../Icons'; +import { IconPlayFill as DefaultIconPlayFill } from '../Icons'; import React from 'react'; -import { useTranslationContext } from '../../context'; +import { useComponentContext, useTranslationContext } from '../../context'; export type VideoThumbnailProps = BaseImageProps & { onPlay?: () => void; @@ -14,6 +14,8 @@ export const VideoThumbnail = ({ onPlay, ...imageProps }: VideoThumbnailProps) => { + const { icons: { IconPlayFill = DefaultIconPlayFill } = {} } = useComponentContext(); + const { t } = useTranslationContext(); return ( diff --git a/src/context/ComponentContext.tsx b/src/context/ComponentContext.tsx index ae6c5fb70..eee80e409 100644 --- a/src/context/ComponentContext.tsx +++ b/src/context/ComponentContext.tsx @@ -71,6 +71,7 @@ import type { SuggestionListProps, } from '../components/TextareaComposer'; +import type { IconSlots } from '../components/Icons'; import type { PropsWithChildrenOnly } from '../types/types'; import type { StopAIGenerationButtonProps } from '../components/MessageComposer/StopAIGenerationButton'; import type { VideoPlayerProps } from '../components/VideoPlayer'; @@ -103,6 +104,8 @@ export type ComponentContextValue = { AutocompleteSuggestionItem?: React.ComponentType; /** Optional UI component to override the default List component that displays suggestions, defaults to and accepts same props as: [List](https://github.com/GetStream/stream-chat-react/blob/master/src/components/AutoCompleteTextarea/List.js) */ AutocompleteSuggestionList?: React.ComponentType; + /** Overrides for icons rendered across the SDK. Individual keys are deep-merged with parent overrides via `WithComponents`, so a consumer can rebrand a single icon without wiping out the others. Preferred over component-level icon props (which are `@deprecated`). */ + icons?: IconSlots; /** UI component to display a user's avatar, defaults to and accepts same props as: [Avatar](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Avatar/Avatar.tsx) */ Avatar?: React.ComponentType; /** UI component to display a list of avatars stacked in a row, defaults to and accepts same props as: [AvatarStack](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Avatar/AvatarStack.tsx) */ diff --git a/src/context/WithComponents.tsx b/src/context/WithComponents.tsx index 4f67a0da7..33a89f540 100644 --- a/src/context/WithComponents.tsx +++ b/src/context/WithComponents.tsx @@ -9,7 +9,15 @@ export function WithComponents({ overrides, }: PropsWithChildren<{ overrides: Partial }>) { const parentOverrides = useContext(ComponentContext); - const actualOverrides: ComponentContextValue = { ...parentOverrides, ...overrides }; + const actualOverrides: ComponentContextValue = { + ...parentOverrides, + ...overrides, + icons: { + ...parentOverrides?.icons, + ...overrides?.icons, + }, + }; + return ( {children} diff --git a/src/plugins/ChannelDetail/ChannelDetail.tsx b/src/plugins/ChannelDetail/ChannelDetail.tsx index 05c2f3614..d8e7be6d2 100644 --- a/src/plugins/ChannelDetail/ChannelDetail.tsx +++ b/src/plugins/ChannelDetail/ChannelDetail.tsx @@ -18,33 +18,39 @@ import { ChannelMediaView } from './Views/ChannelMediaView'; import { ChannelMembersView } from './Views/ChannelMembersView'; import { PinnedMessagesView } from './Views/PinnedMessagesView'; import { Prompt } from '../../components/Dialog'; +import { useComponentContext } from '../../context'; import { - IconFolder, - IconImage, - IconInfo, - IconPin, - IconUser, + IconFolder as DefaultIconFolder, + IconImage as DefaultIconImage, + IconInfo as DefaultIconInfo, + IconPin as DefaultIconPin, + IconUser as DefaultIconUser, } from '../../components/Icons'; -const ChannelManagementNavButtonIcon = () => ( - -); +const ChannelManagementNavButtonIcon = () => { + const { icons: { IconInfo = DefaultIconInfo } = {} } = useComponentContext(); + return ; +}; -const ChannelMembersNavButtonIcon = () => ( - -); +const ChannelMembersNavButtonIcon = () => { + const { icons: { IconUser = DefaultIconUser } = {} } = useComponentContext(); + return ; +}; -const PinnedMessagesNavButtonIcon = () => ( - -); +const PinnedMessagesNavButtonIcon = () => { + const { icons: { IconPin = DefaultIconPin } = {} } = useComponentContext(); + return ; +}; -const ChannelMediaNavButtonIcon = () => ( - -); +const ChannelMediaNavButtonIcon = () => { + const { icons: { IconImage = DefaultIconImage } = {} } = useComponentContext(); + return ; +}; -const ChannelFilesNavButtonIcon = () => ( - -); +const ChannelFilesNavButtonIcon = () => { + const { icons: { IconFolder = DefaultIconFolder } = {} } = useComponentContext(); + return ; +}; export const ChannelManagementNavButton = (props: SectionNavigatorNavButtonProps) => ( ( -
- -
{children}
-
-); +export const ChannelDetailEmptyList = ({ children }: PropsWithChildrenOnly) => { + const { icons: { IconSearch = DefaultIconSearch } = {} } = useComponentContext(); + return ( +
+ +
{children}
+
+ ); +}; diff --git a/src/plugins/ChannelDetail/ChannelDetailSearchInput.tsx b/src/plugins/ChannelDetail/ChannelDetailSearchInput.tsx index ec591d60e..0dc70b400 100644 --- a/src/plugins/ChannelDetail/ChannelDetailSearchInput.tsx +++ b/src/plugins/ChannelDetail/ChannelDetailSearchInput.tsx @@ -1,8 +1,8 @@ import React, { useCallback, useEffect, useState } from 'react'; -import { useTranslationContext } from '../../context'; +import { useComponentContext, useTranslationContext } from '../../context'; import { TextInput } from '../../components/Form'; -import { IconSearch } from '../../components/Icons'; +import { IconSearch as DefaultIconSearch } from '../../components/Icons'; export type ChannelDetailSearchInputProps = { autoFocus?: boolean; @@ -12,6 +12,8 @@ export type ChannelDetailSearchInputProps = { export const ChannelDetailSearchInput = React.memo( ({ autoFocus, onSearchChange, resetKey }: ChannelDetailSearchInputProps) => { + const { icons: { IconSearch = DefaultIconSearch } = {} } = useComponentContext(); + const { t } = useTranslationContext(); const [searchInput, setSearchInput] = useState(''); diff --git a/src/plugins/ChannelDetail/SectionNavigator/SectionNavigatorHeader.tsx b/src/plugins/ChannelDetail/SectionNavigator/SectionNavigatorHeader.tsx index ef4fc40cb..57373cf20 100644 --- a/src/plugins/ChannelDetail/SectionNavigator/SectionNavigatorHeader.tsx +++ b/src/plugins/ChannelDetail/SectionNavigator/SectionNavigatorHeader.tsx @@ -1,10 +1,10 @@ import React, { useMemo } from 'react'; import { SECTION_NAVIGATOR_LAYOUT, useSectionNavigatorContext } from './SectionNavigator'; -import { useTranslationContext } from '../../../context'; +import { useComponentContext, useTranslationContext } from '../../../context'; import { Button } from '../../../components/Button'; import { Prompt, type PromptHeaderProps } from '../../../components/Dialog'; -import { IconMenu } from '../../../components/Icons'; +import { IconMenu as DefaultIconMenu } from '../../../components/Icons'; export type SectionNavigatorHeaderProps = Omit; @@ -16,6 +16,8 @@ export type SectionNavigatorHeaderProps = Omit { + const { icons: { IconMenu = DefaultIconMenu } = {} } = useComponentContext(); + const { t } = useTranslationContext('SectionNavigatorHeader'); const { layout, openNavigation } = useSectionNavigatorContext(); @@ -38,7 +40,7 @@ export const SectionNavigatorHeader = (props: SectionNavigatorHeaderProps) => { ); }; - }, [layout, openNavigation, props.goBack, t]); + }, [IconMenu, layout, openNavigation, props.goBack, t]); return ; }; diff --git a/src/plugins/ChannelDetail/Views/ChannelFilesView/ChannelFilesEmptyList.tsx b/src/plugins/ChannelDetail/Views/ChannelFilesView/ChannelFilesEmptyList.tsx index 83669b3e7..9ee412478 100644 --- a/src/plugins/ChannelDetail/Views/ChannelFilesView/ChannelFilesEmptyList.tsx +++ b/src/plugins/ChannelDetail/Views/ChannelFilesView/ChannelFilesEmptyList.tsx @@ -1,7 +1,9 @@ -import { useTranslationContext } from '../../../../context'; -import { IconFolder } from '../../../../components/Icons'; +import { useComponentContext, useTranslationContext } from '../../../../context'; +import { IconFolder as DefaultIconFolder } from '../../../../components/Icons'; export const ChannelFilesEmptyList = () => { + const { icons: { IconFolder = DefaultIconFolder } = {} } = useComponentContext(); + const { t } = useTranslationContext('ChannelFilesEmptyList'); return ( diff --git a/src/plugins/ChannelDetail/Views/ChannelFilesView/__tests__/ChannelFilesView.test.tsx b/src/plugins/ChannelDetail/Views/ChannelFilesView/__tests__/ChannelFilesView.test.tsx index e11394011..b8b0b1f7a 100644 --- a/src/plugins/ChannelDetail/Views/ChannelFilesView/__tests__/ChannelFilesView.test.tsx +++ b/src/plugins/ChannelDetail/Views/ChannelFilesView/__tests__/ChannelFilesView.test.tsx @@ -6,6 +6,7 @@ import { fromPartial } from '@total-typescript/shoehorn'; import { useChatContext, + useComponentContext, useModalContext, useTranslationContext, } from '../../../../../context'; @@ -193,6 +194,8 @@ describe('ChannelFilesView', () => { tDateTimeParser: (input?: string | number | Date) => Dayjs(input), } as unknown as ReturnType); + vi.mocked(useComponentContext).mockReturnValue({}); + vi.mocked(useChatContext).mockReturnValue({ client: { userID: 'user-1' }, } as ReturnType); diff --git a/src/plugins/ChannelDetail/Views/ChannelManagementView/ChannelManagementActions.defaults.tsx b/src/plugins/ChannelDetail/Views/ChannelManagementView/ChannelManagementActions.defaults.tsx index 8c04cbeb3..5a805ece2 100644 --- a/src/plugins/ChannelDetail/Views/ChannelManagementView/ChannelManagementActions.defaults.tsx +++ b/src/plugins/ChannelDetail/Views/ChannelManagementView/ChannelManagementActions.defaults.tsx @@ -15,11 +15,11 @@ import { Alert } from '../../../../components/Dialog'; import { Button } from '../../../../components/Button'; import { Switch } from '../../../../components/Form'; import { - IconAudio, - IconDelete, - IconLeave, - IconMute, - IconNoSign, + IconAudio as DefaultIconAudio, + IconDelete as DefaultIconDelete, + IconLeave as DefaultIconLeave, + IconMute as DefaultIconMute, + IconNoSign as DefaultIconNoSign, } from '../../../../components/Icons'; import { ListItemLayout } from '../../../../components/ListItemLayout'; import { GlobalModal } from '../../../../components/Modal'; @@ -45,21 +45,36 @@ const toError = (error: unknown) => const getDisplayName = (name?: string, fallback?: string) => name || fallback || ''; -const BlockUserActionIcon = () => ( - -); -const DeleteChatActionIcon = () => ( - -); -const MuteActionIcon = () => ( - -); -const MutedActionIcon = () => ( - -); -const LeaveChannelActionIcon = () => ( - -); +const BlockUserActionIcon = () => { + const { icons: { IconNoSign = DefaultIconNoSign } = {} } = useComponentContext(); + return ( + + ); +}; +const DeleteChatActionIcon = () => { + const { icons: { IconDelete = DefaultIconDelete } = {} } = useComponentContext(); + return ( + + ); +}; +const MuteActionIcon = () => { + const { icons: { IconMute = DefaultIconMute } = {} } = useComponentContext(); + return ( + + ); +}; +const MutedActionIcon = () => { + const { icons: { IconAudio = DefaultIconAudio } = {} } = useComponentContext(); + return ( + + ); +}; +const LeaveChannelActionIcon = () => { + const { icons: { IconLeave = DefaultIconLeave } = {} } = useComponentContext(); + return ( + + ); +}; const channelManagementViewActionClassName = 'str-chat__channel-management-view-action'; diff --git a/src/plugins/ChannelDetail/Views/ChannelManagementView/ChannelManagementView.tsx b/src/plugins/ChannelDetail/Views/ChannelManagementView/ChannelManagementView.tsx index b4979b38f..0054d4ba0 100644 --- a/src/plugins/ChannelDetail/Views/ChannelManagementView/ChannelManagementView.tsx +++ b/src/plugins/ChannelDetail/Views/ChannelManagementView/ChannelManagementView.tsx @@ -23,7 +23,11 @@ import { useChannelPreviewInfo, useIsUserMuted, } from '../../../../components/ChannelListItem'; -import { IconCheckmark, IconMute, IconPin } from '../../../../components/Icons'; +import { + IconCheckmark as DefaultIconCheckmark, + IconMute as DefaultIconMute, + IconPin as DefaultIconPin, +} from '../../../../components/Icons'; import { useChannelMembershipState } from '../../../../components/ChannelList'; import { useIsChannelMuted } from '../../../../components/ChannelListItem/hooks/useIsChannelMuted'; import { useChannelHasMembersOnline } from '../../../../components/ChannelHeader/hooks/useChannelHasMembersOnline'; @@ -55,6 +59,9 @@ export type ChannelManagementInfoBodyProps = { export const ChannelManagementInfoBody = ({ actions, }: ChannelManagementInfoBodyProps) => { + const { icons: { IconMute = DefaultIconMute, IconPin = DefaultIconPin } = {} } = + useComponentContext(); + const { client } = useChatContext(); const { channel } = useChannelDetailContext(); const { Avatar = DefaultChannelAvatar } = useComponentContext(); @@ -313,6 +320,7 @@ const useChannelManagementEditForm = ({ }; export const ChannelManagementEditBody = (props: ChannelManagementEditBodyProps) => { + const { icons: { IconCheckmark = DefaultIconCheckmark } = {} } = useComponentContext(); const { Avatar = DefaultChannelAvatar } = useComponentContext(); const { canSubmit, diff --git a/src/plugins/ChannelDetail/Views/ChannelMediaView/ChannelMediaEmptyList.tsx b/src/plugins/ChannelDetail/Views/ChannelMediaView/ChannelMediaEmptyList.tsx index a03128e78..e5d3471d0 100644 --- a/src/plugins/ChannelDetail/Views/ChannelMediaView/ChannelMediaEmptyList.tsx +++ b/src/plugins/ChannelDetail/Views/ChannelMediaView/ChannelMediaEmptyList.tsx @@ -1,7 +1,9 @@ -import { useTranslationContext } from '../../../../context'; -import { IconImage } from '../../../../components/Icons'; +import { useComponentContext, useTranslationContext } from '../../../../context'; +import { IconImage as DefaultIconImage } from '../../../../components/Icons'; export const ChannelMediaEmptyList = () => { + const { icons: { IconImage = DefaultIconImage } = {} } = useComponentContext(); + const { t } = useTranslationContext('ChannelMediaEmptyList'); return ( diff --git a/src/plugins/ChannelDetail/Views/ChannelMediaView/ChannelMediaView.tsx b/src/plugins/ChannelDetail/Views/ChannelMediaView/ChannelMediaView.tsx index 3e54e383a..9949a0842 100644 --- a/src/plugins/ChannelDetail/Views/ChannelMediaView/ChannelMediaView.tsx +++ b/src/plugins/ChannelDetail/Views/ChannelMediaView/ChannelMediaView.tsx @@ -16,10 +16,10 @@ import { import { Prompt } from '../../../../components/Dialog'; import { Gallery as DefaultGallery, GalleryUI } from '../../../../components/Gallery'; import { - IconChevronLeft, - IconChevronRight, - IconImage, - IconVideoFill, + IconChevronLeft as DefaultIconChevronLeft, + IconChevronRight as DefaultIconChevronRight, + IconImage as DefaultIconImage, + IconVideoFill as DefaultIconVideoFill, } from '../../../../components/Icons'; import { GlobalModal } from '../../../../components/Modal'; import { @@ -51,6 +51,9 @@ const ChannelMediaGridItem = ({ onClick, }: ChannelMediaGridItemProps) => { const { t } = useTranslationContext('ChannelMediaView'); + const { + icons: { IconImage = DefaultIconImage, IconVideoFill = DefaultIconVideoFill } = {}, + } = useComponentContext(); const displayName = getUserDisplayName(item.user); const mediaSrc = item.type === 'video' @@ -118,6 +121,12 @@ const ChannelMediaPagination = ({ previousDisabled, }: ChannelMediaPaginationProps) => { const { t } = useTranslationContext('ChannelMediaView'); + const { + icons: { + IconChevronLeft = DefaultIconChevronLeft, + IconChevronRight = DefaultIconChevronRight, + } = {}, + } = useComponentContext(); return (
diff --git a/src/plugins/ChannelDetail/Views/ChannelMemberDetailView/ChannelMemberActions.defaults.tsx b/src/plugins/ChannelDetail/Views/ChannelMemberDetailView/ChannelMemberActions.defaults.tsx index afcc3ff38..0f0e531e2 100644 --- a/src/plugins/ChannelDetail/Views/ChannelMemberDetailView/ChannelMemberActions.defaults.tsx +++ b/src/plugins/ChannelDetail/Views/ChannelMemberDetailView/ChannelMemberActions.defaults.tsx @@ -24,11 +24,11 @@ import { Alert } from '../../../../components/Dialog'; import { Button } from '../../../../components/Button'; import { Switch } from '../../../../components/Form'; import { - IconAudio, - IconMessageBubble, - IconMute, - IconNoSign, - IconUserRemove, + IconAudio as DefaultIconAudio, + IconMessageBubble as DefaultIconMessageBubble, + IconMute as DefaultIconMute, + IconNoSign as DefaultIconNoSign, + IconUserRemove as DefaultIconUserRemove, } from '../../../../components/Icons'; import { ListItemLayout } from '../../../../components/ListItemLayout'; import { GlobalModal } from '../../../../components/Modal'; @@ -80,25 +80,40 @@ export const useChannelMemberActionContext = () => { const toError = (error: unknown) => error instanceof Error ? error : new Error('An unknown error occurred'); -const MemberMuteActionIcon = () => ( - -); +const MemberMuteActionIcon = () => { + const { icons: { IconMute = DefaultIconMute } = {} } = useComponentContext(); + return ( + + ); +}; -const MemberUnmuteActionIcon = () => ( - -); +const MemberUnmuteActionIcon = () => { + const { icons: { IconAudio = DefaultIconAudio } = {} } = useComponentContext(); + return ( + + ); +}; -const SendDirectMessageActionIcon = () => ( - -); +const SendDirectMessageActionIcon = () => { + const { icons: { IconMessageBubble = DefaultIconMessageBubble } = {} } = + useComponentContext(); + return ; +}; -const BlockUserActionIcon = () => ( - -); +const BlockUserActionIcon = () => { + const { icons: { IconNoSign = DefaultIconNoSign } = {} } = useComponentContext(); + return ( + + ); +}; -const RemoveUserActionIcon = () => ( - -); +const RemoveUserActionIcon = () => { + const { icons: { IconUserRemove = DefaultIconUserRemove } = {} } = + useComponentContext(); + return ( + + ); +}; const channelMemberDetailActionClassName = 'str-chat__channel-member-detail-action'; diff --git a/src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersAddView.tsx b/src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersAddView.tsx index 711620f95..6cace9724 100644 --- a/src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersAddView.tsx +++ b/src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersAddView.tsx @@ -1,11 +1,15 @@ import { type SearchSourceState, type UserResponse, UserSearchSource } from 'stream-chat'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { useChatContext, useTranslationContext } from '../../../../context'; +import { + useChatContext, + useComponentContext, + useTranslationContext, +} from '../../../../context'; import { useStateStore } from '../../../../store'; import { Avatar } from '../../../../components/Avatar'; import { Checkbox } from '../../../../components/Form'; -import { IconMute } from '../../../../components/Icons'; +import { IconMute as DefaultIconMute } from '../../../../components/Icons'; import { ListItemLayout } from '../../../../components/ListItemLayout'; import { VirtualizedList } from '../../VirtualizedList'; import { Prompt } from '../../../../components/Dialog'; @@ -33,9 +37,12 @@ const EMPTY_USERS: UserResponse[] = []; const computeUserItemKey = (_: number, user: UserResponse) => user.id; -const MuteIndicator = () => ( - -); +const MuteIndicator = () => { + const { icons: { IconMute = DefaultIconMute } = {} } = useComponentContext(); + return ( + + ); +}; const readOnlyRootProps = { className: 'str-chat__channel-detail__channel-members-view__list-item', diff --git a/src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersBrowseView.tsx b/src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersBrowseView.tsx index 52dda9f8c..c3bc153ad 100644 --- a/src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersBrowseView.tsx +++ b/src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersBrowseView.tsx @@ -1,9 +1,13 @@ import type { ChannelMemberResponse } from 'stream-chat'; import React, { useCallback, useMemo } from 'react'; -import { useChatContext, useTranslationContext } from '../../../../context'; +import { + useChatContext, + useComponentContext, + useTranslationContext, +} from '../../../../context'; import { Avatar } from '../../../../components/Avatar'; -import { IconMute } from '../../../../components/Icons'; +import { IconMute as DefaultIconMute } from '../../../../components/Icons'; import { ListItemLayout } from '../../../../components/ListItemLayout'; import { VirtualizedList } from '../../VirtualizedList'; import { Prompt } from '../../../../components/Dialog'; @@ -78,6 +82,7 @@ const ChannelMembersBrowseViewItem = ({ const TrailingSlot = useMemo( () => function MemberTrailingSlot() { + const { icons: { IconMute = DefaultIconMute } = {} } = useComponentContext(); return (
{roleTranslation ? ( diff --git a/src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersHeaderActions.defaults.tsx b/src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersHeaderActions.defaults.tsx index e125f3c5d..d78c575f0 100644 --- a/src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersHeaderActions.defaults.tsx +++ b/src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersHeaderActions.defaults.tsx @@ -15,7 +15,7 @@ import type { ChannelMembersHeaderActionsProps, ChannelMembersModeController, } from './ChannelMembersView'; -import { IconUserAdd } from '../../../../components/Icons'; +import { IconUserAdd as DefaultIconUserAdd } from '../../../../components/Icons'; export type ChannelMembersHeaderActionType = 'addMembers' | (string & {}); @@ -91,6 +91,7 @@ const AddMembersMenuAction = ({ modeController, }: ChannelMembersHeaderActionComponentProps) => { const { t } = useTranslationContext(); + const { icons: { IconUserAdd = DefaultIconUserAdd } = {} } = useComponentContext(); if (modeController.mode !== 'browse') return null; diff --git a/src/plugins/ChannelDetail/Views/ChannelMembersView/__tests__/ChannelMembersAddView.test.tsx b/src/plugins/ChannelDetail/Views/ChannelMembersView/__tests__/ChannelMembersAddView.test.tsx index a16c2e7d5..4935ba6c2 100644 --- a/src/plugins/ChannelDetail/Views/ChannelMembersView/__tests__/ChannelMembersAddView.test.tsx +++ b/src/plugins/ChannelDetail/Views/ChannelMembersView/__tests__/ChannelMembersAddView.test.tsx @@ -2,7 +2,11 @@ import { fireEvent, screen, waitFor } from '@testing-library/react'; import React from 'react'; import type { UserResponse } from 'stream-chat'; -import { useChatContext, useTranslationContext } from '../../../../../context'; +import { + useChatContext, + useComponentContext, + useTranslationContext, +} from '../../../../../context'; import { useStateStore } from '../../../../../store'; import { ChannelMembersAddView } from '../ChannelMembersAddView'; import { @@ -88,6 +92,8 @@ describe('ChannelMembersAddView', () => { options?.count ? `${key}:${options.count}` : key, } as ReturnType); + vi.mocked(useComponentContext).mockReturnValue({}); + vi.mocked(useChatContext).mockReturnValue({ client: { user: { id: 'user-1' } }, mutes: [], diff --git a/src/plugins/ChannelDetail/Views/ChannelMembersView/__tests__/ChannelMembersBrowseView.test.tsx b/src/plugins/ChannelDetail/Views/ChannelMembersView/__tests__/ChannelMembersBrowseView.test.tsx index 99d8491b0..26a686cdb 100644 --- a/src/plugins/ChannelDetail/Views/ChannelMembersView/__tests__/ChannelMembersBrowseView.test.tsx +++ b/src/plugins/ChannelDetail/Views/ChannelMembersView/__tests__/ChannelMembersBrowseView.test.tsx @@ -2,7 +2,11 @@ import { fireEvent, screen } from '@testing-library/react'; import React from 'react'; import type { ChannelMemberResponse } from 'stream-chat'; -import { useChatContext, useTranslationContext } from '../../../../../context'; +import { + useChatContext, + useComponentContext, + useTranslationContext, +} from '../../../../../context'; import { useStateStore } from '../../../../../store'; import { ChannelMembersBrowseView } from '../ChannelMembersBrowseView'; import { createChannel, emitChannelEvent, renderWithChannel } from './testUtils'; @@ -117,6 +121,7 @@ describe('ChannelMembersBrowseView', () => { return key; }, } as ReturnType); + vi.mocked(useComponentContext).mockReturnValue({}); vi.mocked(useChatContext).mockReturnValue({ mutes: [], } as ReturnType); diff --git a/src/plugins/ChannelDetail/Views/PinnedMessagesView/PinnedMessagesEmptyList.tsx b/src/plugins/ChannelDetail/Views/PinnedMessagesView/PinnedMessagesEmptyList.tsx index 210c9610a..12784205b 100644 --- a/src/plugins/ChannelDetail/Views/PinnedMessagesView/PinnedMessagesEmptyList.tsx +++ b/src/plugins/ChannelDetail/Views/PinnedMessagesView/PinnedMessagesEmptyList.tsx @@ -1,7 +1,9 @@ -import { IconPin } from '../../../../components/Icons'; -import { useTranslationContext } from '../../../../context'; +import { IconPin as DefaultIconPin } from '../../../../components/Icons'; +import { useComponentContext, useTranslationContext } from '../../../../context'; export const PinnedMessagesEmptyList = () => { + const { icons: { IconPin = DefaultIconPin } = {} } = useComponentContext(); + const { t } = useTranslationContext(); return ( diff --git a/src/plugins/ChannelDetail/Views/PinnedMessagesView/__tests__/PinnedMessagesView.test.tsx b/src/plugins/ChannelDetail/Views/PinnedMessagesView/__tests__/PinnedMessagesView.test.tsx index d57a31766..1e5d211f6 100644 --- a/src/plugins/ChannelDetail/Views/PinnedMessagesView/__tests__/PinnedMessagesView.test.tsx +++ b/src/plugins/ChannelDetail/Views/PinnedMessagesView/__tests__/PinnedMessagesView.test.tsx @@ -6,6 +6,7 @@ import { fromPartial } from '@total-typescript/shoehorn'; import { useChannelActionContext, useChatContext, + useComponentContext, useModalContext, useTranslationContext, } from '../../../../../context'; @@ -210,6 +211,8 @@ describe('PinnedMessagesView', () => { tDateTimeParser: (input?: string | Date) => new Date(input ?? Date.now()), } as ReturnType); + vi.mocked(useComponentContext).mockReturnValue({}); + vi.mocked(useChatContext).mockReturnValue({ client: { userID: 'user-1' }, } as ReturnType); diff --git a/src/plugins/Emojis/EmojiPicker.tsx b/src/plugins/Emojis/EmojiPicker.tsx index 4972da647..5833ada11 100644 --- a/src/plugins/Emojis/EmojiPicker.tsx +++ b/src/plugins/Emojis/EmojiPicker.tsx @@ -1,10 +1,14 @@ import React, { useEffect, useState } from 'react'; import PickerImport from '@emoji-mart/react'; -import { useMessageComposerContext, useTranslationContext } from '../../context'; +import { + useComponentContext, + useMessageComposerContext, + useTranslationContext, +} from '../../context'; import { Button, - IconEmoji, + IconEmoji as DefaultIconEmoji, type PopperLikePlacement, useMessageComposerController, } from '../../components'; @@ -22,6 +26,10 @@ const Picker = const isShadowRoot = (node: Node): node is ShadowRoot => !!(node as ShadowRoot).host; export type EmojiPickerProps = { + /** + * @deprecated Use the `icons.IconEmoji` slot on `ComponentContext` (via ``) instead. + * Passing this prop still wins over the context slot for backwards compatibility. + */ ButtonIconComponent?: React.ComponentType; buttonClassName?: string; pickerContainerClassName?: string; @@ -72,7 +80,9 @@ export const EmojiPicker = (props: EmojiPickerProps) => { const { pickerContainerClassName, wrapperClassName } = classNames; - const { ButtonIconComponent = IconEmoji } = props; + const { icons: { IconEmoji = DefaultIconEmoji } = {} } = useComponentContext(); + const ResolvedButtonIconComponent = props.ButtonIconComponent ?? IconEmoji; + const pickerStyle = props.pickerProps?.style as React.CSSProperties | undefined; useEffect(() => { @@ -134,7 +144,7 @@ export const EmojiPicker = (props: EmojiPickerProps) => { type='button' variant='secondary' > - {ButtonIconComponent && } + {ResolvedButtonIconComponent && }
);