,
+ return useMemo(() => {
+ const defaults = getDefaults();
+ return {
+ ...defaults,
+ ...overrides,
+ // Deep-merge icons so user overrides layer over the full default map.
+ icons: { ...defaults.icons, ...overrides.icons },
+ } as ResolvedComponents;
// eslint-disable-next-line react-hooks/exhaustive-deps -- intentionally stable: overrides are set once at mount
- [],
- );
+ }, []);
};
diff --git a/package/src/contexts/componentsContext/defaultComponents.ts b/package/src/contexts/componentsContext/defaultComponents.ts
index b0aabf2bd6..2ac7a93a43 100644
--- a/package/src/contexts/componentsContext/defaultComponents.ts
+++ b/package/src/contexts/componentsContext/defaultComponents.ts
@@ -186,6 +186,90 @@ import { ChannelAvatar } from '../../components/ui/Avatar/ChannelAvatar';
import { SvgAwareImage } from '../../components/UIComponents/SvgAwareImage';
import { DefaultMessageOverlayBackground } from '../../contexts/overlayContext/MessageOverlayHostLayer';
import type { MessageActionsProps } from '../../contexts/overlayContext/MessageOverlayHostLayer';
+import { ArrowUp, Down } from '../../icons/arrow-up';
+import { ArrowUpRight } from '../../icons/arrow-up-right';
+import { Sound } from '../../icons/audio';
+import { Bell } from '../../icons/bell';
+import { Lightning } from '../../icons/bolt';
+import { Camera } from '../../icons/camera';
+import { Check, Tick } from '../../icons/checkmark';
+import { Checkmark } from '../../icons/checkmark-1';
+import { CheckAll } from '../../icons/checks';
+import { ChevronLeft } from '../../icons/chevron-left';
+import { ChevronRight } from '../../icons/chevron-right';
+import { ChevronUp } from '../../icons/chevron-up';
+import { Time } from '../../icons/clock';
+import { CommandsIcon } from '../../icons/command';
+import { Copy } from '../../icons/copy';
+import { Delete } from '../../icons/delete';
+import { Edit } from '../../icons/edit';
+import { Smile } from '../../icons/emoji';
+import { MoreEmojis } from '../../icons/emoji-add-1';
+import { ExclamationCircle } from '../../icons/exclamation-circle-fill';
+import { Exclamation } from '../../icons/exclamation-mark-fill';
+import { Warning } from '../../icons/exclamation-triangle-fill';
+import { EyeOpen } from '../../icons/EyeOpen';
+import { File, FilePickerIcon } from '../../icons/file';
+import { Audio } from '../../icons/filetype-audio-xl';
+import { Code } from '../../icons/filetype-code-xl';
+import { ZIP } from '../../icons/filetype-compression-xl';
+import { OtherFileIcon } from '../../icons/filetype-other-xl';
+import { PDF } from '../../icons/filetype-pdf-xl';
+import { Presentation } from '../../icons/filetype-presentation-xl';
+import { SpreadSheet } from '../../icons/filetype-spreadsheet-xl';
+import { DOC } from '../../icons/filetype-text-xl';
+import { Video } from '../../icons/filetype-video-xl';
+import { Flag, MessageFlag } from '../../icons/flag';
+import { Folder } from '../../icons/folder';
+import { ImageGrid } from '../../icons/gallery';
+import { Giphy as GiphyFiletypeIcon, GiphyIcon } from '../../icons/giphy';
+import { PhotoIcon, Picture } from '../../icons/image';
+import { Imgur } from '../../icons/imgur';
+import { InfoTooltip } from '../../icons/info';
+import { ArrowBoxLeft } from '../../icons/leave';
+import { Link } from '../../icons/link';
+import { Loading } from '../../icons/loading';
+import { MapPin } from '../../icons/location';
+import { Lock } from '../../icons/lock';
+import { Megaphone } from '../../icons/megaphone';
+import { MessageBubbleEmpty } from '../../icons/message-bubble';
+import { Minus } from '../../icons/minus';
+import { CircleMinus } from '../../icons/minus-circle';
+import { MenuPointHorizontal } from '../../icons/more';
+import { Mute } from '../../icons/mute';
+import { BlockUser, CircleBan } from '../../icons/no-sign';
+import { UnreadIndicator } from '../../icons/notification';
+import { Pause } from '../../icons/pause-fill';
+import { Pin } from '../../icons/pin';
+import { Play } from '../../icons/play-fill';
+import { Plus } from '../../icons/plus';
+import { PollIcon, PollThumbnail } from '../../icons/poll';
+import { Reload } from '../../icons/refresh';
+import { DotGrid } from '../../icons/reorder';
+import { ArrowShareLeft, CurveLineLeftUp } from '../../icons/reply';
+import { ReplyConnectorLeft } from '../../icons/ReplyConnectorLeft';
+import { ReplyConnectorRight } from '../../icons/ReplyConnectorRight';
+import { Resend } from '../../icons/Retry';
+import { Bookmark } from '../../icons/save';
+import { Search } from '../../icons/search';
+import { SendRight } from '../../icons/send';
+import { Share } from '../../icons/share';
+import { Shield } from '../../icons/shield';
+import { Stop } from '../../icons/stop-fill';
+import { ThreadReply } from '../../icons/thread';
+import { Unknown } from '../../icons/Unknown';
+import { Unlock } from '../../icons/unlock';
+import { Unpin } from '../../icons/unpin';
+import { UserAdd } from '../../icons/user-add';
+import { UserDelete } from '../../icons/user-remove';
+import { PeopleIcon } from '../../icons/users';
+import type { IconProps } from '../../icons/utils/base';
+import { VideoIcon } from '../../icons/video';
+import { Recorder } from '../../icons/video-fill';
+import { Mic } from '../../icons/voice';
+import { XCircle } from '../../icons/x-circle';
+import { NewClose } from '../../icons/xmark';
+import { Cross } from '../../icons/xmark-1';
/**
* Normalizes each component entry to React.ComponentType, stripping
@@ -197,7 +281,116 @@ type NormalizeComponents = {
[K in keyof T]: T[K] extends React.ComponentType ? React.ComponentType : T[K];
};
+/**
+ * All overridable icon components in the SDK, keyed by their exported name.
+ * Exposed via `WithComponents` under the nested `icons` key so integrators can
+ * swap any icon: ``.
+ *
+ * Only icons that are actually rendered by the SDK are registered here.
+ */
+export const defaultIcons = {
+ ArrowBoxLeft,
+ ArrowShareLeft,
+ ArrowUp,
+ ArrowUpRight,
+ Audio,
+ Bell,
+ BlockUser,
+ Bookmark,
+ Camera,
+ Check,
+ CheckAll,
+ Checkmark,
+ ChevronLeft,
+ ChevronRight,
+ ChevronUp,
+ CircleBan,
+ CircleMinus,
+ Code,
+ CommandsIcon,
+ Copy,
+ Cross,
+ CurveLineLeftUp,
+ Delete,
+ DOC,
+ DotGrid,
+ Down,
+ Edit,
+ Exclamation,
+ ExclamationCircle,
+ EyeOpen,
+ File,
+ FilePickerIcon,
+ Flag,
+ Folder,
+ Giphy: GiphyFiletypeIcon,
+ GiphyIcon,
+ ImageGrid,
+ Imgur,
+ InfoTooltip,
+ Lightning,
+ Link,
+ Loading,
+ Lock,
+ MapPin,
+ Megaphone,
+ MenuPointHorizontal,
+ MessageBubbleEmpty,
+ MessageFlag,
+ Mic,
+ Minus,
+ MoreEmojis,
+ Mute,
+ NewClose,
+ OtherFileIcon,
+ Pause,
+ PDF,
+ PeopleIcon,
+ PhotoIcon,
+ Picture,
+ Pin,
+ Play,
+ Plus,
+ PollIcon,
+ PollThumbnail,
+ Presentation,
+ Recorder,
+ Reload,
+ ReplyConnectorLeft,
+ ReplyConnectorRight,
+ Resend,
+ Search,
+ SendRight,
+ Share,
+ Shield,
+ Smile,
+ Sound,
+ SpreadSheet,
+ Stop,
+ ThreadReply,
+ Tick,
+ Time,
+ Unknown,
+ Unlock,
+ Unpin,
+ UnreadIndicator,
+ UserAdd,
+ UserDelete,
+ Video,
+ VideoIcon,
+ Warning,
+ XCircle,
+ ZIP,
+} satisfies Record>;
+
+/**
+ * Map of all overridable SDK icons. Used to type the `icons` override slot
+ * and the resolved value returned by `useComponentsContext().icons`.
+ */
+export type IconsMap = typeof defaultIcons;
+
const components = {
+ icons: defaultIcons,
Attachment,
AttachmentUploadIndicator,
AttachButton,
diff --git a/package/src/contexts/pollContext/pollContext.tsx b/package/src/contexts/pollContext/pollContext.tsx
index ec0ef46e8d..bfb1f07797 100644
--- a/package/src/contexts/pollContext/pollContext.tsx
+++ b/package/src/contexts/pollContext/pollContext.tsx
@@ -29,7 +29,7 @@ export const usePollContext = () => {
if (contextValue === DEFAULT_BASE_CONTEXT_VALUE && !isTestEnvironment()) {
throw new Error(
- 'The useCreatePollContext hook was called outside of the PollContext provider. Make sure you have configured the Poll component correctly - https://getstream.io/chat/docs/sdk/reactnative/basics/hello_stream_chat/#channel',
+ 'The usePollContext hook was called outside of the PollContext provider. Make sure you have configured the Poll component correctly - https://getstream.io/chat/docs/sdk/reactnative/basics/hello_stream_chat/#channel',
);
}
diff --git a/package/src/contexts/themeContext/utils/theme.ts b/package/src/contexts/themeContext/utils/theme.ts
index 66ce7e8354..4bb03625c8 100644
--- a/package/src/contexts/themeContext/utils/theme.ts
+++ b/package/src/contexts/themeContext/utils/theme.ts
@@ -5,7 +5,6 @@ import {
type TextStyle,
type ViewStyle,
} from 'react-native';
-import type { CircleProps } from 'react-native-svg';
import type { IconProps } from '../../../icons/utils/base';
import { primitives, lightSemantics, darkSemantics } from '../../../theme';
@@ -139,11 +138,9 @@ export type Theme = {
speedChangeButtonText: TextStyle;
};
avatar: {
- BASE_AVATAR_SIZE: number;
+ border: ViewStyle;
container: ViewStyle;
image: ImageStyle;
- presenceIndicator: CircleProps;
- presenceIndicatorContainer: ViewStyle;
};
avatarStack: {
userAvatarWrapper: ViewStyle;
@@ -967,6 +964,8 @@ export type Theme = {
reactionListItemWrapper: {
wrapper: ViewStyle;
container: ViewStyle;
+ containerSelected: ViewStyle;
+ containerPressed: ViewStyle;
};
reactionListTop: {
container: ViewStyle;
@@ -1256,20 +1255,9 @@ export const defaultTheme: Theme = {
speedChangeButtonText: {},
},
avatar: {
- BASE_AVATAR_SIZE,
+ border: {},
container: {},
- image: {
- borderRadius: 16,
- height: 32,
- width: 32,
- },
- presenceIndicator: {
- cx: 6,
- cy: 6,
- r: 5,
- strokeWidth: 2,
- },
- presenceIndicatorContainer: {},
+ image: {},
},
avatarStack: {
userAvatarWrapper: {},
@@ -2062,6 +2050,8 @@ export const defaultTheme: Theme = {
reactionListItemWrapper: {
wrapper: {},
container: {},
+ containerSelected: {},
+ containerPressed: {},
},
reactionListTop: {
container: {},
diff --git a/package/src/hooks/actions/__tests__/useChannelActionItems.test.tsx b/package/src/hooks/actions/__tests__/useChannelActionItems.test.tsx
index f3bb3d4c57..b77428c9f9 100644
--- a/package/src/hooks/actions/__tests__/useChannelActionItems.test.tsx
+++ b/package/src/hooks/actions/__tests__/useChannelActionItems.test.tsx
@@ -5,6 +5,7 @@ import type { Channel, Mute } from 'stream-chat';
import * as useMutedUsersModule from '../../../components/ChannelList/hooks/useMutedUsers';
import * as useIsChannelMutedModule from '../../../components/ChannelPreview/hooks/useIsChannelMuted';
+import { defaultIcons } from '../../../contexts/componentsContext/defaultComponents';
import type { TranslationContextValue } from '../../../contexts/translationContext/TranslationContext';
import * as TranslationContext from '../../../contexts/translationContext/TranslationContext';
import * as useChannelMembershipStateModule from '../../useChannelMembershipState';
@@ -219,6 +220,7 @@ describe('useChannelActionItems', () => {
isDirectChat: false,
isPinned: false,
surface: 'list',
+ icons: defaultIcons,
t: expect.any(Function),
userMuteActive: false,
},
@@ -246,6 +248,7 @@ describe('getChannelActionItems', () => {
isDirectChat: false,
isPinned: false,
surface: 'list',
+ icons: defaultIcons,
t: ((value: string) => value) as TranslationContextValue['t'],
userMuteActive: false,
});
@@ -259,6 +262,7 @@ describe('getChannelActionItems', () => {
isDirectChat: false,
isPinned: false,
surface: 'list',
+ icons: defaultIcons,
t: ((value: string) => value) as TranslationContextValue['t'],
userMuteActive: false,
},
@@ -291,6 +295,7 @@ describe('getChannelActionItems', () => {
isDirectChat: true,
isPinned: false,
surface: 'list',
+ icons: defaultIcons,
t: ((value: string) => value) as TranslationContextValue['t'],
userMuteActive: true,
});
@@ -339,6 +344,7 @@ describe('getChannelActionItems', () => {
isDirectChat: false,
isPinned: false,
surface: 'list',
+ icons: defaultIcons,
t: ((value: string) => value) as TranslationContextValue['t'],
userMuteActive: false,
});
@@ -356,6 +362,7 @@ describe('getChannelActionItems', () => {
isDirectChat: false,
isPinned: false,
surface: 'list',
+ icons: defaultIcons,
t: ((value: string) => value) as TranslationContextValue['t'],
userMuteActive: false,
});
@@ -374,6 +381,7 @@ describe('getChannelActionItems', () => {
isDirectChat: false,
isPinned: false,
surface: 'list',
+ icons: defaultIcons,
t: ((value: string) => value) as TranslationContextValue['t'],
userMuteActive: false,
});
@@ -399,6 +407,7 @@ describe('getChannelActionItems', () => {
isDirectChat: false,
isPinned: true,
surface: 'list',
+ icons: defaultIcons,
t: ((value: string) => value) as TranslationContextValue['t'],
userMuteActive: false,
});
@@ -420,6 +429,7 @@ describe('getChannelActionItems', () => {
isDirectChat: true,
isPinned: false,
surface: 'list',
+ icons: defaultIcons,
t: ((value: string) => value) as TranslationContextValue['t'],
userMuteActive: false,
});
@@ -439,6 +449,7 @@ describe('getChannelActionItems', () => {
isDirectChat: false,
isPinned: false,
surface: 'details',
+ icons: defaultIcons,
t: ((value: string) => value) as TranslationContextValue['t'],
userMuteActive: false,
});
@@ -456,6 +467,7 @@ describe('getChannelActionItems', () => {
isDirectChat: false,
isPinned: false,
surface: 'list',
+ icons: defaultIcons,
t: ((value: string) => value) as TranslationContextValue['t'],
userMuteActive: false,
});
@@ -472,6 +484,7 @@ describe('getChannelActionItems', () => {
isBlocked: undefined,
isDirectChat: false,
isPinned: false,
+ icons: defaultIcons,
t: ((value: string) => value) as TranslationContextValue['t'],
userMuteActive: false,
});
@@ -490,6 +503,7 @@ describe('getChannelActionItems', () => {
isDirectChat: true,
isPinned: false,
surface: 'list',
+ icons: defaultIcons,
t: ((value: string) => value) as TranslationContextValue['t'],
userMuteActive: true,
});
@@ -516,6 +530,7 @@ describe('getChannelActionItems', () => {
isDirectChat: false,
isPinned: false,
surface: 'list',
+ icons: defaultIcons,
t: ((value: string) => value) as TranslationContextValue['t'],
userMuteActive: false,
});
diff --git a/package/src/hooks/actions/__tests__/useChannelMemberActionItems.test.tsx b/package/src/hooks/actions/__tests__/useChannelMemberActionItems.test.tsx
index aac6326a68..3a08164972 100644
--- a/package/src/hooks/actions/__tests__/useChannelMemberActionItems.test.tsx
+++ b/package/src/hooks/actions/__tests__/useChannelMemberActionItems.test.tsx
@@ -6,6 +6,7 @@ import type { Channel, ChannelMemberResponse, Mute } from 'stream-chat';
import * as useMutedUsersModule from '../../../components/ChannelList/hooks/useMutedUsers';
import { ChatProvider } from '../../../contexts/chatContext/ChatContext';
+import { defaultIcons } from '../../../contexts/componentsContext/defaultComponents';
import type { TranslationContextValue } from '../../../contexts/translationContext/TranslationContext';
import * as TranslationContext from '../../../contexts/translationContext/TranslationContext';
import * as useChannelOwnCapabilitiesModule from '../../useChannelOwnCapabilities';
@@ -227,6 +228,7 @@ describe('useChannelMemberActionItems', () => {
channelActions,
isBlocked: false,
isCurrentUser: false,
+ icons: defaultIcons,
member,
ownCapabilities: [],
t: expect.any(Function),
@@ -253,6 +255,7 @@ describe('buildDefaultChannelMemberActionItems', () => {
isCurrentUser: false,
member,
ownCapabilities: undefined,
+ icons: defaultIcons,
t: ((value: string) => value) as TranslationContextValue['t'],
userActions: actions,
userMuteActive: false,
@@ -273,6 +276,7 @@ describe('buildDefaultChannelMemberActionItems', () => {
isCurrentUser: true,
member,
ownCapabilities: undefined,
+ icons: defaultIcons,
t: ((value: string) => value) as TranslationContextValue['t'],
userActions: actions,
userMuteActive: false,
@@ -290,6 +294,7 @@ describe('buildDefaultChannelMemberActionItems', () => {
isCurrentUser: false,
member,
ownCapabilities: ['update-channel-members'],
+ icons: defaultIcons,
t: ((value: string) => value) as TranslationContextValue['t'],
userActions: actions,
userMuteActive: false,
@@ -310,6 +315,7 @@ describe('buildDefaultChannelMemberActionItems', () => {
isCurrentUser: false,
member,
ownCapabilities: undefined,
+ icons: defaultIcons,
t: ((value: string) => value) as TranslationContextValue['t'],
userActions: actions,
userMuteActive: true,
@@ -329,6 +335,7 @@ describe('buildDefaultChannelMemberActionItems', () => {
isCurrentUser: false,
member,
ownCapabilities: undefined,
+ icons: defaultIcons,
t: ((value: string) => value) as TranslationContextValue['t'],
userActions: actions,
userMuteActive: true,
@@ -347,6 +354,7 @@ describe('buildDefaultChannelMemberActionItems', () => {
isCurrentUser: false,
member,
ownCapabilities: undefined,
+ icons: defaultIcons,
t: ((value: string) => value) as TranslationContextValue['t'],
userActions: actions,
userMuteActive: false,
@@ -360,6 +368,7 @@ describe('buildDefaultChannelMemberActionItems', () => {
isCurrentUser: false,
member,
ownCapabilities: undefined,
+ icons: defaultIcons,
t: ((value: string) => value) as TranslationContextValue['t'],
userActions: actions,
userMuteActive: false,
diff --git a/package/src/hooks/actions/useChannelActionItems.tsx b/package/src/hooks/actions/useChannelActionItems.tsx
index 4f3a868eb0..04aed4012c 100644
--- a/package/src/hooks/actions/useChannelActionItems.tsx
+++ b/package/src/hooks/actions/useChannelActionItems.tsx
@@ -14,13 +14,28 @@ import {
import { useIsChannelMuted } from '../../components/ChannelPreview/hooks/useIsChannelMuted';
import { useUserMuteActive } from '../../components/Message/hooks/useUserMuteActive';
import { useTheme, useTranslationContext } from '../../contexts';
+import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext';
+import type { IconsMap } from '../../contexts/componentsContext/defaultComponents';
import type { TranslationContextValue } from '../../contexts/translationContext/TranslationContext';
-import { IconProps, Mute, BlockUser, Delete, Pin, Sound, Unpin } from '../../icons';
-import { ArrowBoxLeft } from '../../icons/leave';
+import { IconProps } from '../../icons';
import { useChannelMembershipState } from '../useChannelMembershipState';
import { useIsDirectChat } from '../useIsDirectChat';
import { useStateStore } from '../useStateStore';
+// Lazily resolved to avoid a static import cycle with defaultComponents.ts
+// (defaultComponents → ChannelDetails*/ChannelPreview* → this file). Statically
+// importing defaultIcons corrupts module-init order; require it at call time
+// instead. Same pattern as ComponentsContext's getDefaults().
+let cachedDefaultIcons: IconsMap | undefined;
+const getDefaultIcons = (): IconsMap => {
+ if (!cachedDefaultIcons) {
+ cachedDefaultIcons = (
+ require('../../contexts/componentsContext/defaultComponents') as { defaultIcons: IconsMap }
+ ).defaultIcons;
+ }
+ return cachedDefaultIcons;
+};
+
export type ChannelActionItem = ActionItem<
'mute' | 'muteUser' | 'block' | 'leave' | 'deleteChannel' | 'pin' | string
> & {
@@ -63,6 +78,7 @@ export type ChannelActionItemsParams = {
actions: ChannelActions;
channel: Channel;
channelMuteActive: boolean;
+ icons: IconsMap;
isArchived: boolean;
isBlocked: boolean | undefined;
isDirectChat: boolean;
@@ -73,7 +89,7 @@ export type ChannelActionItemsParams = {
};
export type BuildDefaultChannelActionItems = (
- channelActionItemsParams: ChannelActionItemsParams,
+ channelActionItemsParams: Omit & { icons?: IconsMap },
) => ChannelActionItem[];
const ChannelActionsIcon = ({
@@ -104,6 +120,7 @@ export const buildDefaultChannelActionItems: BuildDefaultChannelActionItems = (
unpin,
},
channelMuteActive,
+ icons = getDefaultIcons(),
isBlocked,
isDirectChat,
isPinned,
@@ -119,9 +136,9 @@ export const buildDefaultChannelActionItems: BuildDefaultChannelActionItems = (
action: channelMuteActive ? unmuteChannel : muteChannel,
Icon: (props) =>
channelMuteActive ? (
-
+
) : (
- ,
+ Icon: (props) => ,
id: 'pin',
label: isDirectChat
? isPinned
@@ -164,10 +181,10 @@ export const buildDefaultChannelActionItems: BuildDefaultChannelActionItems = (
action: userMuteActive ? unmuteUser : muteUser,
Icon: (props) =>
userMuteActive ? (
-
+
) : (
,
+ Icon: (props) => ,
id: 'block',
label: isBlocked ? t('Unblock User') : t('Block User'),
placement: 'sheet',
@@ -245,7 +262,7 @@ export const buildDefaultChannelActionItems: BuildDefaultChannelActionItems = (
actionItems.push({
action: leaveWithConfirmation,
- Icon: (props) => ,
+ Icon: (props) => ,
id: 'leave',
label: isDirectChat ? t('Leave Chat') : t('Leave Group'),
placement: 'sheet',
@@ -274,7 +291,7 @@ export const buildDefaultChannelActionItems: BuildDefaultChannelActionItems = (
},
]);
},
- Icon: (props) => ,
+ Icon: (props) => ,
id: 'deleteChannel',
label: isDirectChat ? t('Delete Chat') : t('Delete Group'),
placement: 'sheet',
@@ -307,6 +324,7 @@ export const useChannelActionItems = ({
getChannelActionItems: getChannelActionItemsProp = getChannelActionItems,
}: UseChannelActionItemsParams) => {
const { t } = useTranslationContext();
+ const { icons } = useComponentsContext();
const membership = useChannelMembershipState(channel);
const channelActions = useChannelActions(channel);
const isDirectChat = useIsDirectChat(channel);
@@ -331,6 +349,7 @@ export const useChannelActionItems = ({
actions: channelActions,
channel,
channelMuteActive,
+ icons,
isArchived,
isBlocked,
isDirectChat,
@@ -343,6 +362,7 @@ export const useChannelActionItems = ({
channel,
channelActions,
channelMuteActive,
+ icons,
isArchived,
isBlocked,
isDirectChat,
diff --git a/package/src/hooks/actions/useChannelMemberActionItems.tsx b/package/src/hooks/actions/useChannelMemberActionItems.tsx
index 25c056c550..d119d84ad8 100644
--- a/package/src/hooks/actions/useChannelMemberActionItems.tsx
+++ b/package/src/hooks/actions/useChannelMemberActionItems.tsx
@@ -9,16 +9,33 @@ import { useUserActions, UserActions } from './useUserActions';
import { useUserMuteActive } from '../../components/Message/hooks/useUserMuteActive';
import { useTheme, useTranslationContext } from '../../contexts';
+import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext';
+import type { IconsMap } from '../../contexts/componentsContext/defaultComponents';
import type { TranslationContextValue } from '../../contexts/translationContext/TranslationContext';
-import { BlockUser, IconProps, Mute, Sound, UserDelete } from '../../icons';
+import { IconProps } from '../../icons';
import { useChannelOwnCapabilities } from '../useChannelOwnCapabilities';
import { useStateStore } from '../useStateStore';
+// Lazily resolved to avoid a static import cycle with defaultComponents.ts
+// (defaultComponents → ChannelDetails*/ChannelPreview* → this file). Statically
+// importing defaultIcons corrupts module-init order; require it at call time
+// instead. Same pattern as ComponentsContext's getDefaults().
+let cachedDefaultIcons: IconsMap | undefined;
+const getDefaultIcons = (): IconsMap => {
+ if (!cachedDefaultIcons) {
+ cachedDefaultIcons = (
+ require('../../contexts/componentsContext/defaultComponents') as { defaultIcons: IconsMap }
+ ).defaultIcons;
+ }
+ return cachedDefaultIcons;
+};
+
export type ChannelMemberActionItem = ActionItem<'muteUser' | 'block' | string>;
export type ChannelMemberActionItemsParams = {
channel: Channel;
channelActions: ChannelActions;
+ icons: IconsMap;
isBlocked: boolean;
isCurrentUser: boolean;
member: ChannelMemberResponse;
@@ -29,7 +46,9 @@ export type ChannelMemberActionItemsParams = {
};
export type BuildDefaultChannelMemberActionItems = (
- channelMemberActionItemsParams: ChannelMemberActionItemsParams,
+ channelMemberActionItemsParams: Omit & {
+ icons?: IconsMap;
+ },
) => ChannelMemberActionItem[];
const ChannelMemberActionsIcon = ({
@@ -48,6 +67,7 @@ export const buildDefaultChannelMemberActionItems: BuildDefaultChannelMemberActi
) => {
const {
channelActions: { removeMembers },
+ icons = getDefaultIcons(),
isBlocked,
isCurrentUser,
member,
@@ -69,10 +89,10 @@ export const buildDefaultChannelMemberActionItems: BuildDefaultChannelMemberActi
action: userMuteActive ? unmuteUser : muteUser,
Icon: (props) =>
userMuteActive ? (
-
+
) : (
,
+ Icon: (props) => ,
id: 'block',
label: isBlocked ? t('Unblock User') : t('Block User'),
type: isBlocked ? 'standard' : 'destructive',
@@ -135,7 +155,7 @@ export const buildDefaultChannelMemberActionItems: BuildDefaultChannelMemberActi
],
);
},
- Icon: (props) => ,
+ Icon: (props) => ,
id: 'removeMember',
label: t('Remove User'),
type: 'destructive',
@@ -169,6 +189,7 @@ export const useChannelMemberActionItems = ({
getChannelMemberActionItems: getChannelMemberActionItemsProp = getChannelMemberActionItems,
}: UseChannelMemberActionItemsParams) => {
const { t } = useTranslationContext();
+ const { icons } = useComponentsContext();
const userActions = useUserActions(member.user);
const channelActions = useChannelActions(channel);
@@ -189,6 +210,7 @@ export const useChannelMemberActionItems = ({
() => ({
channel,
channelActions,
+ icons,
isBlocked,
isCurrentUser,
member,
@@ -200,6 +222,7 @@ export const useChannelMemberActionItems = ({
[
channel,
channelActions,
+ icons,
isBlocked,
isCurrentUser,
member,
diff --git a/package/src/hooks/messagePreview/useMessagePreviewIcon.tsx b/package/src/hooks/messagePreview/useMessagePreviewIcon.tsx
index 3136cc8f07..7ee95d5152 100644
--- a/package/src/hooks/messagePreview/useMessagePreviewIcon.tsx
+++ b/package/src/hooks/messagePreview/useMessagePreviewIcon.tsx
@@ -1,15 +1,8 @@
import { DraftMessage, LocalMessage, MessageResponse } from 'stream-chat';
+import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext';
import { useGroupedAttachments } from '../../hooks/messagePreview/useGroupedAttachments';
-import { File } from '../../icons/file';
-import { PhotoIcon } from '../../icons/image';
-import { Link } from '../../icons/link';
-import { MapPin } from '../../icons/location';
-import { CircleBan } from '../../icons/no-sign';
-import { PollIcon } from '../../icons/poll';
-import { VideoIcon } from '../../icons/video';
-import { Mic } from '../../icons/voice';
import { FileTypes } from '../../types/types';
export const useMessagePreviewIcon = ({
@@ -17,6 +10,7 @@ export const useMessagePreviewIcon = ({
}: {
message?: LocalMessage | MessageResponse | DraftMessage | null;
}) => {
+ const { icons } = useComponentsContext();
const { giphys, audios, images, videos, files, voiceRecordings } = useGroupedAttachments(
message?.attachments,
);
@@ -36,39 +30,39 @@ export const useMessagePreviewIcon = ({
);
if (message.type === 'deleted') {
- return CircleBan;
+ return icons.CircleBan;
}
if (message.poll_id) {
- return PollIcon;
+ return icons.PollIcon;
}
if (message.shared_location) {
- return MapPin;
+ return icons.MapPin;
}
if (hasLink) {
- return Link;
+ return icons.Link;
}
if (onlyAudio || onlyVoiceRecordings) {
- return Mic;
+ return icons.Mic;
}
if (onlyVideos) {
- return VideoIcon;
+ return icons.VideoIcon;
}
if (onlyImages) {
- return PhotoIcon;
+ return icons.PhotoIcon;
}
if (giphys?.length) {
- return File;
+ return icons.File;
}
if (files?.length || images?.length || videos?.length || audios?.length) {
- return File;
+ return icons.File;
}
return null;
diff --git a/package/src/icons/arrow-left.tsx b/package/src/icons/arrow-left.tsx
deleted file mode 100644
index e39d201a97..0000000000
--- a/package/src/icons/arrow-left.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import React from 'react';
-import { I18nManager } from 'react-native';
-import Svg, { Path } from 'react-native-svg';
-
-import { IconProps } from './utils/base';
-
-export const ArrowLeft = ({ height, size, style, width, ...rest }: IconProps) => (
-
-);
diff --git a/yarn.lock b/yarn.lock
index 0f37ab5a8d..a31c62eb27 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1587,7 +1587,7 @@ __metadata:
languageName: node
linkType: hard
-"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.25.2, @babel/types@npm:^7.26.0, @babel/types@npm:^7.27.3, @babel/types@npm:^7.28.2, @babel/types@npm:^7.28.6, @babel/types@npm:^7.29.0, @babel/types@npm:^7.29.7, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4":
+"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.25.2, @babel/types@npm:^7.26.0, @babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3, @babel/types@npm:^7.28.2, @babel/types@npm:^7.28.6, @babel/types@npm:^7.29.0, @babel/types@npm:^7.29.7, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4":
version: 7.29.7
resolution: "@babel/types@npm:7.29.7"
dependencies:
@@ -17743,7 +17743,7 @@ __metadata:
languageName: node
linkType: hard
-"react-native-gesture-handler@npm:^2.26.0, react-native-gesture-handler@npm:^2.31.2, react-native-gesture-handler@npm:~2.31.1":
+"react-native-gesture-handler@npm:^2.26.0, react-native-gesture-handler@npm:~2.31.1":
version: 2.31.2
resolution: "react-native-gesture-handler@npm:2.31.2"
dependencies:
@@ -17771,6 +17771,19 @@ __metadata:
languageName: node
linkType: hard
+"react-native-gesture-handler@npm:^3.0.2":
+ version: 3.0.2
+ resolution: "react-native-gesture-handler@npm:3.0.2"
+ dependencies:
+ "@types/react-test-renderer": "npm:^19.1.0"
+ invariant: "npm:^2.2.4"
+ peerDependencies:
+ react: "*"
+ react-native: "*"
+ checksum: 10c0/06d2b1a621f73c148c4000692e55d5f2a44d44229cabc79829de0b0b3dbf3bcf4bc00e8635f82bd1062d7eac87f39ca5213eb1041e23a5543947e64773eaddfb
+ languageName: node
+ linkType: hard
+
"react-native-haptic-feedback@npm:^2.3.3":
version: 2.3.4
resolution: "react-native-haptic-feedback@npm:2.3.4"
@@ -17909,6 +17922,20 @@ __metadata:
languageName: node
linkType: hard
+"react-native-reanimated@npm:4.5.1":
+ version: 4.5.1
+ resolution: "react-native-reanimated@npm:4.5.1"
+ dependencies:
+ react-native-is-edge-to-edge: "npm:^1.3.1"
+ semver: "npm:^7.7.3"
+ peerDependencies:
+ react: "*"
+ react-native: 0.83 - 0.86
+ react-native-worklets: 0.10.x
+ checksum: 10c0/86c64529fc5d59fb1a9083e0934c957c95c7319149efe02f92252237aa1fb208958da1c42705447c12b1d6bff3c23a354e10a05787995082eda1c284703ec0bc
+ languageName: node
+ linkType: hard
+
"react-native-reanimated@npm:^4.0.1, react-native-reanimated@npm:^4.4.0":
version: 4.4.0
resolution: "react-native-reanimated@npm:4.4.0"
@@ -18041,7 +18068,7 @@ __metadata:
languageName: node
linkType: hard
-"react-native-worklets@npm:0.8.3, react-native-worklets@npm:^0.8.3":
+"react-native-worklets@npm:0.8.3":
version: 0.8.3
resolution: "react-native-worklets@npm:0.8.3"
dependencies:
@@ -18065,6 +18092,31 @@ __metadata:
languageName: node
linkType: hard
+"react-native-worklets@npm:^0.10.2":
+ version: 0.10.2
+ resolution: "react-native-worklets@npm:0.10.2"
+ dependencies:
+ "@babel/plugin-transform-arrow-functions": "npm:^7.27.1"
+ "@babel/plugin-transform-class-properties": "npm:^7.28.6"
+ "@babel/plugin-transform-classes": "npm:^7.28.6"
+ "@babel/plugin-transform-nullish-coalescing-operator": "npm:^7.28.6"
+ "@babel/plugin-transform-optional-chaining": "npm:^7.28.6"
+ "@babel/plugin-transform-shorthand-properties": "npm:^7.27.1"
+ "@babel/plugin-transform-template-literals": "npm:^7.27.1"
+ "@babel/plugin-transform-unicode-regex": "npm:^7.27.1"
+ "@babel/preset-typescript": "npm:^7.28.5"
+ "@babel/types": "npm:^7.27.1"
+ convert-source-map: "npm:^2.0.0"
+ semver: "npm:^7.7.4"
+ peerDependencies:
+ "@babel/core": "*"
+ "@react-native/metro-config": "*"
+ react: "*"
+ react-native: 0.83 - 0.86
+ checksum: 10c0/5c4aa9940fb63cd1b0585b618fff84716c028011c9235e10d451b6436c7fb4dec9474a5dd9fa38d74aedd4a5b46f916470fb73aedaca4b0ba47b364d26fbf193
+ languageName: node
+ linkType: hard
+
"react-native-worklets@npm:^0.4.1":
version: 0.4.2
resolution: "react-native-worklets@npm:0.4.2"
@@ -18870,20 +18922,20 @@ __metadata:
react: "npm:19.2.3"
react-native: "npm:0.85.3"
react-native-blob-util: "npm:^0.24.9"
- react-native-gesture-handler: "npm:^2.31.2"
+ react-native-gesture-handler: "npm:^3.0.2"
react-native-haptic-feedback: "npm:^3.0.0"
react-native-image-picker: "npm:^8.2.1"
react-native-maps: "npm:^1.27.2"
react-native-nitro-modules: "npm:0.31.3"
react-native-nitro-sound: "npm:0.2.9"
- react-native-reanimated: "npm:4.3.1"
+ react-native-reanimated: "npm:4.5.1"
react-native-safe-area-context: "npm:^5.8.0"
react-native-screens: "npm:^4.25.2"
react-native-share: "npm:^12.3.1"
react-native-svg: "npm:^15.15.5"
react-native-teleport: "npm:^1.1.7"
react-native-video: "npm:^6.19.2"
- react-native-worklets: "npm:^0.8.3"
+ react-native-worklets: "npm:^0.10.2"
stream-chat: "npm:^9.50.1"
stream-chat-react-native: "workspace:^"
stream-chat-react-native-core: "workspace:^"