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
7 changes: 6 additions & 1 deletion examples/SampleApp/metro.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,18 @@ const extraNodeModules = uniqueModules.reduce((acc, item) => {
acc[item.packageName] = item.modulePath;
return acc;
}, {});
// Point `stream-chat` at the local checkout that is portal-linked via the root package.json
// `resolutions`. We set it after the reduce (rather than seeding the reduce's initial value)
// because stream-chat is a direct dependency of this app, so `uniqueModules` would otherwise
// overwrite the seed.
extraNodeModules['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;
1 change: 1 addition & 0 deletions examples/SampleApp/src/hooks/useChatClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export const useChatClient = () => {
unsubscribePushListenersRef.current?.();
const client = StreamChat.getInstance(config.apiKey, {
timeout: 6000,
isLocalUnreadCountEnabled: true, // TEST: localized unread count — revert before committing
// logger: (type, msg) => console.log(type, msg)
});
setChatClient(client);
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"examples/TypeScriptMessaging"
],
"resolutions": {
"stream-chat": "portal:/Users/isekovanic/Projects/stream-chat-js",
"@types/react": "^19.2.0",
"@shopify/flash-list": "patch:@shopify/flash-list@npm%3A2.3.1#~/.yarn/patches/@shopify-flash-list-npm-2.3.1-8b5fd40241.patch"
},
Expand Down
44 changes: 34 additions & 10 deletions package/src/components/Channel/Channel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -511,12 +511,12 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
const styles = useStyles();
const [deleted, setDeleted] = useState<boolean>(false);
const [error, setError] = useState<Error | boolean>(false);
const [lastRead, setLastRead] = useState<Date | undefined>();
const lastReadRef = useRef<Date | undefined>(undefined);
const [thread, setThread] = useState<LocalMessage | null>(threadProps || null);
const [threadHasMore, setThreadHasMore] = useState(true);
const [threadLoadingMore, setThreadLoadingMore] = useState(false);
const [channelUnreadStateStore] = useState(new ChannelUnreadStateStore());
const [messageInputHeightStore] = useState(new MessageInputHeightStore());
const [channelUnreadStateStore] = useState(() => new ChannelUnreadStateStore());
const [messageInputHeightStore] = useState(() => new MessageInputHeightStore());
// TODO: Think if we can remove this and just rely on the channelUnreadStateStore everywhere.
const setChannelUnreadState = useCallback(
(data: ChannelUnreadStateStoreType['channelUnreadState']) => {
Expand Down Expand Up @@ -690,7 +690,15 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
return;
}

if (event.type === 'message.read_locally') {
// Local unread reset (read events disabled, e.g. livestreams): the count is already updated
// in the client state, and the preview badge / unread divider are handled elsewhere, so
// there is nothing to copy into channel state here.
return;
}

if (event.type === 'message.read' || event.type === 'notification.mark_read') {
console.log('READ EVENT MENTION ?!', event);
setReadThrottled();
return;
}
Expand All @@ -703,7 +711,7 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
useEffect(() => {
let listener: ReturnType<typeof channel.on>;
const initChannel = async () => {
setLastRead(new Date());
lastReadRef.current = new Date();
const unreadCount = channel.countUnread();
const shouldLoadAtFirstUnread = shouldLoadInitialChannelAtFirstUnreadMessage(unreadCount);
if (!channel || !shouldSyncChannel) {
Expand Down Expand Up @@ -812,7 +820,25 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
const markReadInternal: ChannelContextValue['markRead'] = throttle(
async (options?: MarkReadFunctionOptions) => {
const { updateChannelUnreadState = true } = options ?? {};
if (!channel || channel?.disconnected || !clientChannelConfig?.read_events) {
if (!channel || channel?.disconnected) {
return;
}

// When read events are disabled (e.g. livestreams) we cannot mark read on the backend. If the
// client opted into a local unread count, reset it locally instead so the user's "caught up"
// state is reflected without a server round trip.
if (!clientChannelConfig?.read_events) {
if (client.options.isLocalUnreadCountEnabled) {
const event = channel.markReadLocally();
if (updateChannelUnreadState && event && lastReadRef.current) {
setChannelUnreadState({
last_read: lastReadRef.current,
last_read_message_id: event.last_read_message_id,
unread_messages: 0,
});
lastReadRef.current = new Date();
}
}
return;
}

Expand All @@ -821,13 +847,13 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
} else {
try {
const response = await channel.markRead();
if (updateChannelUnreadState && response && lastRead) {
if (updateChannelUnreadState && response && lastReadRef.current) {
setChannelUnreadState({
last_read: lastRead,
last_read: lastReadRef.current,
last_read_message_id: response?.event.last_read_message_id,
unread_messages: 0,
});
setLastRead(new Date());
lastReadRef.current = new Date();
}
} catch (err) {
console.log('Error marking channel as read:', err);
Expand Down Expand Up @@ -1578,7 +1604,6 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
hideStickyDateHeader,
highlightedMessageId,
isChannelActive: shouldSyncChannel,
lastRead,
loadChannelAroundMessage,
loadChannelAtFirstUnreadMessage,
loading: channelMessagesState.loading,
Expand All @@ -1590,7 +1615,6 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
reloadChannel,
scrollToFirstUnreadThreshold,
setChannelUnreadState,
setLastRead,
setTargetedMessage,
hasPendingInitialTargetLoad,
targetedMessage,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export const useCreateChannelContext = ({
hideStickyDateHeader,
highlightedMessageId,
isChannelActive,
lastRead,
loadChannelAroundMessage,
loadChannelAtFirstUnreadMessage,
loading,
Expand All @@ -25,7 +24,6 @@ export const useCreateChannelContext = ({
reloadChannel,
scrollToFirstUnreadThreshold,
setChannelUnreadState,
setLastRead,
setTargetedMessage,
hasPendingInitialTargetLoad,
targetedMessage,
Expand All @@ -35,7 +33,6 @@ export const useCreateChannelContext = ({
watchers,
}: ChannelContextValue) => {
const channelId = channel?.id;
const lastReadTime = lastRead?.getTime();
const membersLength = Object.keys(members).length;

const readUsers = Object.values(read);
Expand All @@ -56,7 +53,6 @@ export const useCreateChannelContext = ({
hideStickyDateHeader,
highlightedMessageId,
isChannelActive,
lastRead,
loadChannelAroundMessage,
loadChannelAtFirstUnreadMessage,
loading,
Expand All @@ -68,7 +64,6 @@ export const useCreateChannelContext = ({
reloadChannel,
scrollToFirstUnreadThreshold,
setChannelUnreadState,
setLastRead,
setTargetedMessage,
hasPendingInitialTargetLoad,
targetedMessage,
Expand All @@ -84,7 +79,6 @@ export const useCreateChannelContext = ({
error,
isChannelActive,
highlightedMessageId,
lastReadTime,
loading,
membersLength,
readUsersLength,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,14 @@ export const useChannelPreviewData = (
setForceUpdate((prev) => prev + 1);
}
};
const { unsubscribe } = client.on('message.read', handleReadEvent);
return unsubscribe;
const readSubscription = client.on('message.read', handleReadEvent);
// `message.read_locally` is the client-only equivalent emitted by `channel.markReadLocally()` when
// read events are disabled (e.g. livestreams with `isLocalUnreadCountEnabled`).
const localReadSubscription = client.on('message.read_locally', handleReadEvent);
return () => {
readSubscription.unsubscribe();
localReadSubscription.unsubscribe();
};
}, [client, channel]);

/**
Expand Down
6 changes: 5 additions & 1 deletion package/src/components/MessageList/MessageFlashList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -660,9 +660,13 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) =>
const lastReadMessageId = channelUnreadState?.last_read_message_id;
const lastReadMessageVisible = viewableItems.some((item) => item.item.id === lastReadMessageId);

// Read-events-disabled channels (e.g. livestreams) still surface the unread notification when the
// client opted into a local unread count, so the gate accepts either source.
const unreadNotificationSupported = readEvents || client.options.isLocalUnreadCountEnabled;

if (
!viewableItems.length ||
!readEvents ||
!unreadNotificationSupported ||
lastReadMessageVisible ||
attachmentPickerStore.state.getLatestValue().selectedPicker === 'images'
) {
Expand Down
6 changes: 5 additions & 1 deletion package/src/components/MessageList/MessageList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -550,9 +550,13 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
(item) => item.item.message.id === lastReadMessageId,
);

// Read-events-disabled channels (e.g. livestreams) still surface the unread notification when the
// client opted into a local unread count, so the gate accepts either source.
const unreadNotificationSupported = readEvents || client.options.isLocalUnreadCountEnabled;

if (
!viewableItems.length ||
!readEvents ||
!unreadNotificationSupported ||
lastReadMessageVisible ||
attachmentPickerStore.state.getLatestValue().selectedPicker === 'images'
) {
Expand Down
8 changes: 5 additions & 3 deletions package/src/components/Thread/__tests__/Thread.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,9 @@ describe('Thread', () => {
threadResponses as unknown as Parameters<typeof channel.state.addMessagesSorted>[0],
);

let setLastRead: ((date?: Date) => void) | undefined;
let setChannelUnreadState:
| React.ContextType<typeof ChannelContext>['setChannelUnreadState']
| undefined;

const { getByText, toJSON } = render(
<ChannelsStateProvider>
Expand All @@ -161,7 +163,7 @@ describe('Thread', () => {
<Channel channel={channel} thread={thread} threadList>
<ChannelContext.Consumer>
{(c) => {
setLastRead = c.setLastRead;
setChannelUnreadState = c.setChannelUnreadState;
return <Thread />;
}}
</ChannelContext.Consumer>
Expand All @@ -178,7 +180,7 @@ describe('Thread', () => {
expect(getByText('Message6')).toBeTruthy();
});

act(() => setLastRead!(new Date('2020-08-17T18:08:03.196Z')));
act(() => setChannelUnreadState!(undefined));

const snapshot = toJSON() as unknown as {
children: Array<{
Expand Down
3 changes: 0 additions & 3 deletions package/src/contexts/channelContext/ChannelContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,6 @@ export type ChannelContextValue = {
reloadChannel: () => Promise<void>;
scrollToFirstUnreadThreshold: number;
setChannelUnreadState: (data: ChannelUnreadStateStoreType['channelUnreadState']) => void;
setLastRead: React.Dispatch<React.SetStateAction<Date | undefined>>;
setTargetedMessage: (messageId?: string) => void;
/**
* Returns true when Channel is about to load an initial targeted message.
Expand All @@ -131,8 +130,6 @@ export type ChannelContextValue = {
*/
highlightedMessageId?: string;
isChannelActive?: boolean;

lastRead?: Date;
loading?: boolean;
/**
* Maximum time in milliseconds that should occur between messages
Expand Down
9 changes: 4 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -19683,9 +19683,9 @@ __metadata:
languageName: unknown
linkType: soft

"stream-chat@npm:^9.47.1":
version: 9.47.1
resolution: "stream-chat@npm:9.47.1"
"stream-chat@portal:/Users/isekovanic/Projects/stream-chat-js::locator=root%40workspace%3A.":
version: 0.0.0-use.local
resolution: "stream-chat@portal:/Users/isekovanic/Projects/stream-chat-js::locator=root%40workspace%3A."
dependencies:
"@types/jsonwebtoken": "npm:^9.0.8"
"@types/ws": "npm:^8.18.1"
Expand All @@ -19701,9 +19701,8 @@ __metadata:
built: true
husky:
built: true
checksum: 10c0/32578f59b8b439454b4a23038417477a16b74ba5677245346d5b0f1cf61139e9b4067e83a77196e1dd661ae2207842df30b345bf4f570872ce9b0cc4463f8eb7
languageName: node
linkType: hard
linkType: soft

"stream-combiner2@npm:~1.1.1":
version: 1.1.1
Expand Down
Loading