Skip to content
Open
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
13 changes: 8 additions & 5 deletions examples/SampleApp/metro.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,17 +65,20 @@ const uniqueModules = dependencyPackageNames.map((packageName) => {
const blockList = uniqueModules.map(({ blockPattern }) => blockPattern);

// provide the path for the unique modules
const extraNodeModules = uniqueModules.reduce((acc, item) => {
acc[item.packageName] = item.modulePath;
return acc;
}, {});
const extraNodeModules = uniqueModules.reduce(
(acc, item) => {
acc[item.packageName] = item.modulePath;
return acc;
},
{ 'stream-chat': '/Users/isekovanic/Projects/stream-chat-js' },
);

config.resolver.blockList = exclusionList(blockList);
config.resolver.extraNodeModules = extraNodeModules;

config.resolver.nodeModulesPaths = [PATH.resolve(__dirname, 'node_modules')];

// add the package dir for metro to access the package folder
config.watchFolders = [packageDirPath];
config.watchFolders = [packageDirPath, '/Users/isekovanic/Projects/stream-chat-js'];

module.exports = config;
4 changes: 2 additions & 2 deletions examples/SampleApp/src/components/ChatScreenHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Image, StyleSheet, TouchableOpacity } from 'react-native';
import type { DrawerNavigationProp } from '@react-navigation/drawer';
import { CompositeNavigationProp, useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useChatContext } from 'stream-chat-react-native';
import { useWSConnectionState } from 'stream-chat-react-native';

import { NetworkDownIndicator } from './NetworkDownIndicator';
import { RoundButton } from './RoundButton';
Expand Down Expand Up @@ -34,7 +34,7 @@ export const ChatScreenHeader: React.FC<{ title?: string }> = ({ title = 'Stream

const navigation = useNavigation<ChatScreenHeaderNavigationProp>();
const { chatClient } = useAppContext();
const { isOnline } = useChatContext();
const isOnline = !!useWSConnectionState()?.isOnline;

return (
<ScreenHeader
Expand Down
5 changes: 3 additions & 2 deletions examples/SampleApp/src/components/FastImageAdapter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import { ImageProps } from 'react-native';

import FastImage from '@d11/react-native-fast-image';
import type { FastImageProps } from '@d11/react-native-fast-image';
import { useChatContext } from 'stream-chat-react-native';
import { useNetworkConnectionState } from 'stream-chat-react-native';

type FastImageAdapterProps = Omit<ImageProps, 'source'> &
Pick<FastImageProps, 'source' | 'transition'>;

export const FastImageAdapter = React.memo((props: ImageProps) => {
const { isOnline } = useChatContext();
// The device's network, not the socket: these are plain HTTP image fetches.
const isOnline = useNetworkConnectionState()?.isOnline;
const {
source,
transition = FastImage.transition.fade,
Expand Down
14 changes: 7 additions & 7 deletions examples/SampleApp/src/screens/ChannelScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,20 @@ import { RouteProp, useFocusEffect, useNavigation } from '@react-navigation/nati
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import type { LocalMessage, Channel as StreamChatChannel, StreamChat } from 'stream-chat';
import {
AITypingIndicatorView,
AlsoSentToChannelHeaderPressPayload,
Channel,
ChannelAvatar,
MessageActionsParams,
MessageComposer,
MessageList,
MessageFlashList,
MessageList,
PortalWhileClosingView,
useAttachmentPickerContext,
useChannelPreviewDisplayName,
useChatContext,
useTheme,
AITypingIndicatorView,
useTranslationContext,
MessageActionsParams,
ChannelAvatar,
PortalWhileClosingView,
useWSConnectionState,
} from 'stream-chat-react-native';

import { ThreadType } from 'stream-chat-react-native-core';
Expand Down Expand Up @@ -60,7 +60,7 @@ const ChannelHeader: React.FC<ChannelHeaderProps> = ({ channel }) => {
const { closePicker } = useAttachmentPickerContext();
const membersStatus = useChannelMembersStatus(channel);
const displayName = useChannelPreviewDisplayName(channel);
const { isOnline } = useChatContext();
const isOnline = !!useWSConnectionState()?.isOnline;
const { chatClient } = useAppContext();
const navigation = useNavigation<ChannelScreenNavigationProp>();

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"examples/ExpoMessaging"
],
"resolutions": {
"@types/react": "^19.2.0"
"@types/react": "^19.2.0",
"stream-chat": "portal:/Users/isekovanic/Projects/stream-chat-js"
},
"engines": {
"node": ">=22.13.0"
Expand Down
26 changes: 20 additions & 6 deletions package/src/__tests__/offline-support/optimistic-update.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,18 @@ const getOfflineDb = (client: StreamChat): TestOfflineDb =>
// request fails" tests below therefore force this offline case (they still pass an errored API mock,
// but the offline short-circuit is what queues the task), NOT a raw 500.
const markConnectionUnhealthy = (client: StreamChat) => {
(client.wsConnection as unknown as { isHealthy: boolean }).isHealthy = false;
// `_setStatus` is the SDK's own documented hook for faking socket status in tests — there is no
// public setter, because only the socket itself is supposed to write this.
// eslint-disable-next-line no-underscore-dangle
client.wsConnection._setStatus({ isOnline: false });
};

/** The counterpart of {@link markConnectionUnhealthy}, for tests that go offline and then reconnect. */
const markConnectionHealthy = (client: StreamChat) => {
(client.wsConnection as unknown as { isHealthy: boolean }).isHealthy = true;
// `_setStatus` is the SDK's own documented hook for faking socket status in tests — there is no
// public setter, because only the socket itself is supposed to write this.
// eslint-disable-next-line no-underscore-dangle
client.wsConnection._setStatus({ isOnline: true, connectionId: 'dummy_connection_id' });
};

// React flushes passive effects child-first, so the test-callback effect below runs BEFORE `Channel`'s
Expand Down Expand Up @@ -212,10 +218,10 @@ export const OptimisticUpdates = () => {
channels: [channelResponse] as unknown as Parameters<typeof upsertChannels>[0]['channels'],
isLatestMessagesSet: true,
});
chatClient.wsConnection = {
isHealthy: true,
onlineStatusChanged: jest.fn(),
} as unknown as StreamChat['wsConnection'];
// `getTestClientWithUser` already marks the socket up on the real `WSConnection`. Replacing the
// object wholesale would drop its `state` store, which `markConnectionHealthy` /
// `markConnectionUnhealthy` and the connection hooks both read.
markConnectionHealthy(chatClient);
});

afterEach(() => {
Expand Down Expand Up @@ -1157,6 +1163,14 @@ export const OptimisticUpdates = () => {
});

describe('pending task execution', () => {
// Every test in this block drives a real reconnect, and since `connection.changed` gained its
// `connection` discriminator these events actually reach `ConnectionRecoveryManager` — so each
// one now runs a genuine recovery (`channel.reload()`) plus real SQLite pending-task I/O on top
// of a full render. That lands around 2s alone but exceeded the 5s default under the suite's
// parallel load, flaking roughly one run in two. The work is legitimate, not a hang; the
// default was simply sized for the old no-op path.
jest.setTimeout(20_000);

it('pending task should be executed after connection is recovered', async () => {
const message = channel.messagePaginator.headItems[0];
const reaction = generateReaction();
Expand Down
17 changes: 11 additions & 6 deletions package/src/components/Accessibility/NotificationAnnouncer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { useEffect, useRef } from 'react';
import { useAccessibilityAnnouncer } from './useAccessibilityAnnouncer';

import { useAccessibilityContext } from '../../contexts/accessibilityContext/AccessibilityContext';
import { useChatContext } from '../../contexts/chatContext/ChatContext';
import { useTranslationContext } from '../../contexts/translationContext/TranslationContext';
import { useNetworkConnectionState } from '../Chat/hooks/useNetworkConnectionState';
import { useWSConnectionState } from '../Chat/hooks/useWSConnectionState';

/**
* Mirrors stream-chat-react's `<NotificationAnnouncer />`. RN does not yet have a
Expand All @@ -18,7 +19,11 @@ import { useTranslationContext } from '../../contexts/translationContext/Transla
*/
export const NotificationAnnouncer = () => {
const { announceConnectionState, enabled } = useAccessibilityContext();
const { connectionRecovering, isOnline } = useChatContext();
const isNetworkOnline = useNetworkConnectionState()?.isOnline;
const isWSOnline = useWSConnectionState()?.isOnline;
// The socket is what 'connected' means to a chat user; the device network only decides which
// of the two offline messages is truthful.
const isOnline = !!isWSOnline;
const announce = useAccessibilityAnnouncer();
const { t } = useTranslationContext();
const previousIsOnlineRef = useRef<boolean | null | undefined>(undefined);
Expand All @@ -36,13 +41,13 @@ export const NotificationAnnouncer = () => {
announce(t('a11y.connection.connected.accessibilityLabel', 'Connected'), 'polite');
} else {
announce(
connectionRecovering
? t('a11y.connection.reconnecting.accessibilityLabel', 'Reconnecting')
: t('a11y.connection.offline.accessibilityLabel', 'Offline'),
isNetworkOnline === false
? t('a11y.connection.offline.accessibilityLabel', 'Offline')
: t('a11y.connection.reconnecting.accessibilityLabel', 'Reconnecting'),
'assertive',
);
}
}, [announce, announceConnectionState, connectionRecovering, enabled, isOnline, t]);
}, [announce, announceConnectionState, enabled, isNetworkOnline, isOnline, t]);

return null;
};
12 changes: 8 additions & 4 deletions package/src/components/Channel/Channel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ import { patchMessageTextCommand } from '../../utils/patchMessageTextCommand';
import { getFileNameFromPath, isLocalUrl, ReactionData } from '../../utils/utils';
import { NotificationAnnouncer } from '../Accessibility/NotificationAnnouncer';
import { AttachmentPicker } from '../AttachmentPicker/AttachmentPicker';
import { useWSConnectionState } from '../Chat/hooks/useWSConnectionState';
import type { KeyboardCompatibleViewProps } from '../KeyboardCompatibleView/KeyboardCompatibleView';
import { useMarkRead } from '../MessageList/hooks/useMarkRead';
import { Emoji } from '../MessageMenu/EmojiPickerList';
Expand Down Expand Up @@ -171,8 +172,7 @@ export type ChannelPropsWithContext = Pick<ChannelContextValue, 'channel'> &
| 'maximumMessageLimit'
>
> &
Pick<ChatContextValue, 'client' | 'enableOfflineSupport' | 'isOnline'> &
Partial<
Pick<ChatContextValue, 'client' | 'enableOfflineSupport'> & { isOnline: boolean } & Partial<
Pick<
InputMessageInputContextValue,
| 'additionalTextInputProps'
Expand Down Expand Up @@ -691,7 +691,10 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
// landed, so `hasMoreHead` read here reflects the refreshed window. Channel view only, and only
// when that window is at the newest, only if the user has paginated up into older history so leave
// their read state alone.
const { unsubscribe } = client.on('connection.recovered', () => {
const { unsubscribe } = client.on('connection.recovered', (event) => {
if (event.connection !== 'ws') {
return;
}
if (thread || channel.messagePaginator.hasMoreHead) {
return;
}
Expand Down Expand Up @@ -1088,7 +1091,8 @@ export type ChannelProps = Partial<Omit<ChannelPropsWithContext, 'channel' | 'th
* @example ./Channel.md
*/
export const Channel = (props: PropsWithChildren<ChannelProps>) => {
const { client, enableOfflineSupport, isOnline, isMessageAIGenerated } = useChatContext();
const { client, enableOfflineSupport, isMessageAIGenerated } = useChatContext();
const isOnline = !!useWSConnectionState()?.isOnline;
const { t } = useTranslationContext();
const notificationHostId =
props.notificationHostId ??
Expand Down
10 changes: 7 additions & 3 deletions package/src/components/ChannelList/ChannelListView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,23 @@ import {
ChannelsContextValue,
useChannelsContext,
} from '../../contexts/channelsContext/ChannelsContext';
import { useChatContext } from '../../contexts/chatContext/ChatContext';
import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext';
import { useDebugContext } from '../../contexts/debugContext/DebugContext';
import { useTheme } from '../../contexts/themeContext/ThemeContext';

import { useStableCallback } from '../../hooks';
import { ChannelPreview } from '../ChannelPreview/ChannelPreview';
import { useNetworkConnectionState } from '../Chat/hooks/useNetworkConnectionState';
import { useWSConnectionState } from '../Chat/hooks/useWSConnectionState';

export type ChannelListViewPropsWithContext = Omit<
ChannelsContextValue,
'maxUnreadCount' | 'numberOfSkeletons' | 'onSelect'
>;

const StatusIndicator = () => {
const { isOnline } = useChatContext();
const isNetworkOnline = useNetworkConnectionState()?.isOnline;
const isWSOnline = useWSConnectionState()?.isOnline;
const styles = useStyles();
const { error, loadingChannels, refreshList } = useChannelsContext();
const { ChannelListHeaderErrorIndicator, ChannelListHeaderNetworkDownIndicator } =
Expand All @@ -33,7 +35,9 @@ const StatusIndicator = () => {
return null;
}

if (!isOnline) {
// `=== false` for the network (unknown must not read as offline), plain falsy for the socket
// (always a boolean).
if (isNetworkOnline === false || !isWSOnline) {
return (
<View style={styles.statusIndicator}>
<ChannelListHeaderNetworkDownIndicator />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import type { Channel, StreamChat, UserResponse } from 'stream-chat';

import type { ChannelsContextValue } from '../../../contexts/channelsContext/ChannelsContext';
import { ChannelsProvider } from '../../../contexts/channelsContext/ChannelsContext';
import { ChatContext, ChatProvider } from '../../../contexts/chatContext/ChatContext';
import { getOrCreateChannelApi } from '../../../mock-builders/api/getOrCreateChannel';
import { useMockedApis } from '../../../mock-builders/api/useMockedApis';
import { generateChannelResponse } from '../../../mock-builders/generator/channel';
Expand All @@ -27,20 +26,14 @@ const queryChannelsOverride: ChannelListQueryChannelsOverride = () =>
*/
const Component = () => (
<Chat client={chatClient}>
<ChatContext.Consumer>
{(context) => (
<ChatProvider value={{ ...context, isOnline: true }}>
<ChannelList
filters={{
members: {
$in: ['vishal', 'neil'],
},
}}
queryChannelsOverride={queryChannelsOverride}
/>
</ChatProvider>
)}
</ChatContext.Consumer>
<ChannelList
filters={{
members: {
$in: ['vishal', 'neil'],
},
}}
queryChannelsOverride={queryChannelsOverride}
/>
</Chat>
);

Expand All @@ -58,36 +51,30 @@ const ComponentWithContextOverrides = ({
loadingChannels: boolean;
}) => (
<Chat client={chatClient}>
<ChatContext.Consumer>
{(context) => (
<ChatProvider value={{ ...context, isOnline: true }}>
<ChannelsProvider
value={
{
additionalFlatListProps: {},
channelListInitialized: !loadingChannels && !error,
channels: error ? null : [],
error: error ? new Error('test error') : undefined,
forceUpdate: 0,
hasNextPage: false,
loadingChannels,
loadingNextPage: false,
loadMoreThreshold: 0.1,
loadNextPage: noop,
maxUnreadCount: 255,
numberOfSkeletons: 8,
refreshing: false,
refreshList: noop,
reloadList: noop,
setFlatListRef: noop,
} as unknown as ChannelsContextValue
}
>
<ChannelListView />
</ChannelsProvider>
</ChatProvider>
)}
</ChatContext.Consumer>
<ChannelsProvider
value={
{
additionalFlatListProps: {},
channelListInitialized: !loadingChannels && !error,
channels: error ? null : [],
error: error ? new Error('test error') : undefined,
forceUpdate: 0,
hasNextPage: false,
loadingChannels,
loadingNextPage: false,
loadMoreThreshold: 0.1,
loadNextPage: noop,
maxUnreadCount: 255,
numberOfSkeletons: 8,
refreshing: false,
refreshList: noop,
reloadList: noop,
setFlatListRef: noop,
} as unknown as ChannelsContextValue
}
>
<ChannelListView />
</ChannelsProvider>
</Chat>
);

Expand Down
Loading
Loading