Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions src/components/Accessibility/NotificationAnnouncer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,7 @@ const defaultNotificationFilter: NotificationAnnouncementFilter = () => true;
* satisfies this and no extra setup is needed.
*
* Rendered **without** a provider ancestor it mounts successfully but **announces nothing** β€”
* `useAriaLiveAnnouncer()` returns a no-op and logs a `console.warn` ("… called outside of an
* AriaLiveAnnouncerProvider"). This is intentional post-F4: the component no longer carries a
* `useAriaLiveAnnouncer()` returns a no-op. This is intentional post-F4: the component no longer carries a
* fallback live region, so a single announcer owns all output and there are no duplicate/competing
* regions. If you render it outside `Chat`, wrap it in `AriaLiveAnnouncerProvider` + `AriaLiveOutlet`
* yourself.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export const useIncomingMessageAnnouncements = ({
threadList = false,
}: UseIncomingMessageAnnouncementsParams) => {
const announce = useAriaLiveAnnouncer();
const { t } = useTranslationContext('useIncomingMessageAnnouncements');
const { t } = useTranslationContext();
const lastAnnouncementTimestampRef = useRef(0);
const flushTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
const announcedMessageIdsRef = useRef(new Set<string>());
Expand Down
19 changes: 6 additions & 13 deletions src/components/Accessibility/useAriaLiveAnnouncer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,9 @@ export const AriaLiveAnnouncerContext = createContext<
AriaLiveAnnouncerContextValue | undefined
>(undefined);

export const useAriaLiveAnnouncer = () => {
const contextValue = useContext(AriaLiveAnnouncerContext);

if (!contextValue) {
console.warn(
'The useAriaLiveAnnouncer hook was called outside of an AriaLiveAnnouncerProvider.',
);

return noopAnnounce;
}

return contextValue.announce;
};
/**
* Announcements are a progressive enhancement: outside an `AriaLiveAnnouncerProvider` this is a
* no-op rather than an error.
*/
export const useAriaLiveAnnouncer = () =>
useContext(AriaLiveAnnouncerContext)?.announce ?? noopAnnounce;
2 changes: 1 addition & 1 deletion src/components/Attachment/AttachmentActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const UnMemoizedAttachmentActions = (props: AttachmentActionsProps) => {
title,
type,
} = props;
const { t } = useTranslationContext('UnMemoizedAttachmentActions');
const { t } = useTranslationContext();
const { announceInteraction } = useInteractionAnnouncements();
const { reserve: reserveFocusReturn, restore: restoreFocusReturn } = useFocusReturn();
const buttonRefs = useRef<Array<HTMLButtonElement | null>>([]);
Expand Down
7 changes: 4 additions & 3 deletions src/components/Attachment/Audio.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useContext } from 'react';
import type { Attachment, VoiceRecordingAttachment } from 'stream-chat';

import {
Expand All @@ -8,7 +8,7 @@ import {
import type { AudioPlayerState } from '../AudioPlayback/AudioPlayer';
import { useAudioPlayer } from '../AudioPlayback/WithAudioPlayback';
import { useStateStore } from '../../store';
import { useComponentContext, useMessageContext } from '../../context';
import { MessageContext, useComponentContext } from '../../context';
import type { AudioPlayer } from '../AudioPlayback/AudioPlayer';
import { PlayButton } from '../Button/PlayButton';
import { FileIcon } from '../FileIcon';
Expand Down Expand Up @@ -102,7 +102,8 @@ export const Audio = (props: AudioProps) => {
* with the default SDK components, but can be done with custom API calls.In this case all the Audio
* widgets will share the state.
*/
const { message } = useMessageContext() ?? {};
// also rendered from composer previews, where there is no message
const { message } = useContext(MessageContext) ?? {};
const threadInstance = useThreadContext();

const audioPlayer = useAudioPlayer({
Expand Down
7 changes: 4 additions & 3 deletions src/components/Attachment/LinkPreview/CardAudio.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { type AudioPlayerState, ProgressBar, useAudioPlayer } from '../../AudioPlayback';
import { useMessageContext } from '../../../context';
import { MessageContext } from '../../../context';
import { useStateStore } from '../../../store';
import { PlayButton } from '../../Button';
import type { AudioProps } from '../Audio';
import React from 'react';
import React, { useContext } from 'react';
import { IconLink } from '../../Icons';
import { SafeAnchor } from '../../SafeAnchor';
import type { CardProps } from './Card';
Expand Down Expand Up @@ -56,7 +56,8 @@ const AudioWidget = ({ mimeType, src }: { src: string; mimeType?: string }) => {
* with the default SDK components, but can be done with custom API calls.In this case all the Audio
* widgets will share the state.
*/
const { message } = useMessageContext() ?? {};
// also rendered from link previews, where there is no message
const { message } = useContext(MessageContext) ?? {};
const threadInstance = useThreadContext();

const audioPlayer = useAudioPlayer({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import clsx from 'clsx';
import React from 'react';

export const UnableToRenderCard = ({ type }: { type?: Attachment['type'] }) => {
const { t } = useTranslationContext('Card');
const { t } = useTranslationContext();

return (
<div
Expand Down
22 changes: 19 additions & 3 deletions src/components/Attachment/ModalGallery.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useState } from 'react';
import React, { useCallback, useContext, useMemo, useState } from 'react';
import clsx from 'clsx';

import type { BaseImageProps } from '../BaseImage';
Expand All @@ -7,7 +7,11 @@ import { BaseImage as DefaultBaseImage } from '../BaseImage';
import { Gallery as DefaultGallery, GalleryUI } from '../Gallery';
import { LoadingIndicator } from '../Loading';
import { GlobalModal, type ModalCloseSource } from '../Modal';
import { useComponentContext, useTranslationContext } from '../../context';
import {
MessageContext,
useComponentContext,
useTranslationContext,
} from '../../context';
import { IconRetry } from '../Icons';
import { VideoThumbnail } from '../VideoPlayer/VideoThumbnail';

Expand Down Expand Up @@ -56,8 +60,20 @@ export const ModalGallery = ({
Gallery = DefaultGallery,
Modal = GlobalModal,
} = useComponentContext();
// ModalGallery is also usable standalone, outside a message
const { message } = useContext(MessageContext) ?? {};
const [modalOpen, setModalOpen] = useState(false);
const [selectedIndex, setSelectedIndex] = useState(0);
// the gallery header renders outside this provider, so sender and timestamp travel on the items
const itemsWithSender = useMemo(
() =>
items.map((item) => ({
...item,
createdAt: item.createdAt ?? message?.created_at,
user: item.user ?? message?.user ?? undefined,
})),
[items, message?.created_at, message?.user],
);
const usesDefaultBaseImage = BaseImage === DefaultBaseImage;

const closeModal = useCallback(() => {
Expand Down Expand Up @@ -114,7 +130,7 @@ export const ModalGallery = ({
closeOnBackgroundClick={closeOnBackgroundClick}
GalleryUI={GalleryUI}
initialIndex={selectedIndex}
items={items}
items={itemsWithSender}
onRequestClose={closeModal}
/>
</Modal>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Attachment/UnsupportedAttachment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export type UnsupportedAttachmentProps = {
};

export const UnsupportedAttachment = () => {
const { t } = useTranslationContext('UnsupportedAttachment');
const { t } = useTranslationContext();
return (
<div
className='str-chat__message-attachment-unsupported'
Expand Down
7 changes: 4 additions & 3 deletions src/components/Attachment/VoiceRecording.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React from 'react';
import React, { useContext } from 'react';
import type { Attachment, VoiceRecordingAttachment } from 'stream-chat';

import { FileSizeIndicator as DefaultFileSizeIndicator } from './components';
import { FileIcon } from '../FileIcon';
import {
MessageContext,
useComponentContext,
useMessageContext,
useTranslationContext,
} from '../../context';
import {
Expand Down Expand Up @@ -122,7 +122,8 @@ export const VoiceRecordingPlayer = ({
* with the default SDK components, but can be done with custom API calls.In this case all the Audio
* widgets will share the state.
*/
const { message } = useMessageContext() ?? {};
// also rendered from composer previews, where there is no message
const { message } = useContext(MessageContext) ?? {};
const threadInstance = useThreadContext();

const audioPlayer = useAudioPlayer({
Expand Down
2 changes: 1 addition & 1 deletion src/components/AudioPlayback/components/ProgressBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const ProgressBar = ({
secondsElapsed,
seek,
}: ProgressBarProps) => {
const { t } = useTranslationContext('ProgressBar');
const { t } = useTranslationContext();
const {
handleDrag,
handleDragStart,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export const WaveProgressBar = ({
seek,
waveformData,
}: WaveProgressBarProps) => {
const { t } = useTranslationContext('WaveProgressBar');
const { t } = useTranslationContext();
const [trackAxisX, setTrackAxisX] = useState<{
barCount: number;
barWidth: number;
Expand Down
2 changes: 1 addition & 1 deletion src/components/Avatar/AvatarStack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function AvatarStack({
badgeSize?: BadgeSize;
capLimit?: number;
}) {
const { Avatar = DefaultAvatar } = useComponentContext(AvatarStack.name);
const { Avatar = DefaultAvatar } = useComponentContext();

const displayInfoToRender = useMemo(
() => (displayInfo.length > capLimit ? displayInfo.slice(0, capLimit) : displayInfo),
Expand Down
5 changes: 2 additions & 3 deletions src/components/Channel/Channel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,8 @@ const ChannelInner = (
const { LoadingErrorIndicator, LoadingIndicator = DefaultLoadingIndicator } =
useComponentContext();

const { client, latestMessageDatesByChannels, searchController } =
useChatContext('Channel');
const { t } = useTranslationContext('Channel');
const { client, latestMessageDatesByChannels, searchController } = useChatContext();
const { t } = useTranslationContext();
const windowsEmojiClass = useImageFlagEmojisOnWindowsClass();

const channelConfig = useChannelConfig({ cid: channel.cid });
Expand Down
2 changes: 1 addition & 1 deletion src/components/Channel/hooks/useChannelConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const channelConfigsSelector = (value: ChannelConfigsState) => ({

// todo: why is channel config stored on client?
export const useChannelConfig = ({ cid }: { cid: string | undefined }) => {
const { client } = useChatContext('useChannelConfig');
const { client } = useChatContext();
const channelConfigsState = useStateStore(client.configsStore, channelConfigsSelector);

if (!cid) return undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useChatContext } from '../../../context/ChatContext';
import type { ChatContextValue } from '../../../context/ChatContext';

export const useImageFlagEmojisOnWindowsClass = () => {
const { useImageFlagEmojisOnWindows } = useChatContext('Channel');
const { useImageFlagEmojisOnWindows } = useChatContext();
return useImageFlagEmojisOnWindows && navigator.userAgent.match(/Win/)
? 'str-chat--windows-flags'
: '';
Expand Down
2 changes: 1 addition & 1 deletion src/components/ChannelHeader/ChannelHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const textComposerTypingSelector = ({ typing }: TextComposerState) => ({ typing
const ChannelHeaderSubtitle = () => {
const channel = useChannel();
const channelConfig = useChannelConfig({ cid: channel.cid });
const { client } = useChatContext('ChannelHeaderSubtitle');
const { client } = useChatContext();
const messageComposer = useMessageComposerController();
const { typing = {} } =
useStateStore(messageComposer.textComposer?.state, textComposerTypingSelector) ?? {};
Expand Down
16 changes: 11 additions & 5 deletions src/components/ChannelListItem/ChannelListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
useWorkspaceNavigation,
} from '../../context';
import { useChannelMembershipState } from '../ChannelList';
import { requireContext } from '../../context/requireContext';

export type ChannelListItemUIProps = ChannelListItemProps & {
/**
Expand Down Expand Up @@ -73,11 +74,16 @@ export type ChannelListItemProps = {
watchers?: { limit?: number; offset?: number };
};

const ChannelListItemContext = React.createContext<{ channel: Channel }>({
channel: null as unknown as Channel,
});
const ChannelListItemContext = React.createContext<{ channel: Channel } | undefined>(
undefined,
);

export const useChannelListItemContext = () => useContext(ChannelListItemContext);
export const useChannelListItemContext = () =>
requireContext(
useContext(ChannelListItemContext),
'useChannelListItemContext',
'ChannelListItemUI',
);

const lastMessageSelector = ({ lastMessage }: MessagePaginatorAggregateState) => ({
lastMessage: lastMessage ?? undefined,
Expand All @@ -86,7 +92,7 @@ const lastMessageSelector = ({ lastMessage }: MessagePaginatorAggregateState) =>
export const ChannelListItem = (props: ChannelListItemProps) => {
const { active, channel, channelUpdateCount } = props;
const { ChannelListItemUI = DefaultChannelListItemUI } = useComponentContext();
const { client } = useChatContext('ChannelPreview');
const { client } = useChatContext();
// Active = THIS channel is currently open in the workspace. Keyed on the channel's own
// cid (never "the first channel slot"), so multiple open channels each highlight independently.
const channelOpenInSlot = useWorkspaceNavigation().isChannelActive(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export type ChannelListItemTimestampProps = {
export function ChannelListItemTimestamp({
previewedMessage,
}: ChannelListItemTimestampProps) {
const { t, tDateTimeParser } = useTranslationContext('ChannelListItemTimestamp');
const { t, tDateTimeParser } = useTranslationContext();

const timestamp = previewedMessage?.created_at;
const normalizedTimestamp =
Expand Down
4 changes: 2 additions & 2 deletions src/components/ChannelListItem/ChannelListItemUI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ const UnMemoizedChannelListItemUI = (props: ChannelListItemUIProps) => {
ChannelListItemActionButtons = DefaultChannelListItemActionButtons,
SummarizedMessagePreview = DefaultSummarizedMessagePreview,
} = useComponentContext();
const { client, isMessageAIGenerated } = useChatContext('ChannelListItemUI');
const { t, tDateTimeParser, userLanguage } = useTranslationContext('ChannelListItemUI');
const { client, isMessageAIGenerated } = useChatContext();
const { t, tDateTimeParser, userLanguage } = useTranslationContext();
const { openChannel } = useWorkspaceNavigation();
const { announceInteraction } = useInteractionAnnouncements();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ function computeChannelDisplayName(
export const useChannelDisplayName = (
channel: Channel | undefined,
): string | undefined => {
const { client } = useChatContext('useChannelDisplayName');
const { t } = useTranslationContext('useChannelDisplayName');
const { client } = useChatContext();
const { t } = useTranslationContext();
const directMessageLabel = t(
'channelListItem.channelDisplayName.directMessage.label',
'Direct message',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const getMuteStatus = (channel: Channel) =>
: { createdAt: null, expiresAt: null, muted: false };

export const useIsChannelMuted = (channel: Channel) => {
const { client } = useChatContext('useIsChannelMuted');
const { client } = useChatContext();

const [muted, setMuted] = useState(() => getMuteStatus(channel));

Expand Down
2 changes: 1 addition & 1 deletion src/components/DateSeparator/DateSeparator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const UnMemoizedDateSeparator = (props: DateSeparatorProps) => {
...restTimestampFormatterOptions
} = props;

const { t, tDateTimeParser } = useTranslationContext('DateSeparator');
const { t, tDateTimeParser } = useTranslationContext();

const formattedDate = getDateString({
calendar,
Expand Down
3 changes: 2 additions & 1 deletion src/components/Dialog/components/ContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { useComponentContext, useTranslationContext } from '../../../context';
import { createRovingFocusKeyDownHandler } from '../../../a11y/a11yUtils';
import { VisuallyHidden } from '../../VisuallyHidden';
import { useStableId } from '../../UtilityComponents/useStableId';
import { requireContext } from '../../../context/requireContext';

/**
* ContextMenu module
Expand Down Expand Up @@ -589,7 +590,7 @@ const ContextMenuContext = React.createContext<ContextMenuContextValue | undefin
);

export const useContextMenuContext = () =>
useContext(ContextMenuContext) as ContextMenuContextValue;
requireContext(useContext(ContextMenuContext), 'useContextMenuContext', 'ContextMenu');

type ContextMenuLevel = {
focusRestoreRequest?: ContextMenuFocusRestoreRequest;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export type EmptyStateIndicatorProps = {
const UnMemoizedEmptyStateIndicator = (props: EmptyStateIndicatorProps) => {
const { listType, messageText } = props;

const { t } = useTranslationContext('EmptyStateIndicator');
const { t } = useTranslationContext();

if (listType === 'thread') return null;

Expand Down
2 changes: 1 addition & 1 deletion src/components/Form/SwitchField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export const SwitchField = ({
// Read the announcer optionally: a SwitchField may render outside an announcer provider (e.g. in
// isolation), where it simply stays silent rather than warning.
const announce = useContext(AriaLiveAnnouncerContext)?.announce;
const { t } = useTranslationContext('SwitchField');
const { t } = useTranslationContext();

const [uncontrolledChecked, setUncontrolledChecked] = useState(Boolean(defaultChecked));
const isControlled = checked !== undefined;
Expand Down
Loading
Loading