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
26 changes: 9 additions & 17 deletions package/src/components/ChannelPreview/ChannelPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,28 @@ import type { Channel } from 'stream-chat';
import { ChannelSwipableWrapper } from './ChannelSwipableWrapper';
import { useChannelPreviewData } from './hooks/useChannelPreviewData';

import {
ChannelsContextValue,
useChannelsContext,
} from '../../contexts/channelsContext/ChannelsContext';
import { useChannelsContext } from '../../contexts/channelsContext/ChannelsContext';
import { ChatContextValue, useChatContext } from '../../contexts/chatContext/ChatContext';
import { useComponentsContext } from '../../contexts/componentsContext/ComponentsContext';
import { useTranslatedMessage } from '../../hooks/useTranslatedMessage';

export type ChannelPreviewProps = Partial<Pick<ChatContextValue, 'client'>> &
Partial<Pick<ChannelsContextValue, 'forceUpdate'>> & {
/**
* Instance of Channel from stream-chat package.
*/
channel: Channel;
};
export type ChannelPreviewProps = Partial<Pick<ChatContextValue, 'client'>> & {
/**
* Instance of Channel from stream-chat package.
*/
channel: Channel;
};

export const ChannelPreview = (props: ChannelPreviewProps) => {
const { channel, client: propClient, forceUpdate: propForceUpdate } = props;
const { channel, client: propClient } = props;

const { client: contextClient } = useChatContext();
const { getChannelActionItems, swipeActionsEnabled } = useChannelsContext();
const { ChannelPreview } = useComponentsContext();

const client = propClient || contextClient;

const { muted, pinned, unread, lastMessage } = useChannelPreviewData(
channel,
client,
propForceUpdate,
);
const { muted, pinned, unread, lastMessage } = useChannelPreviewData(channel, client);

const translatedLastMessage = useTranslatedMessage(lastMessage);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@ const initChannelFromData = async (
return channel;
};

// TODO(#27/#8): the unread-count assertions below exercise the pre-reactive, mock-driven mechanism
// (mocking `channel.countUnread()` + dispatching `notification.mark_read`/`mark_unread` so the old WS
// listeners refreshed). `useChannelPreviewData` now sources unread reactively from
// `channel.state.readStore` and no longer calls `countUnread()`, so these need reworking to seed
// `readStore` and dispatch the events that actually mutate it (`message.read`, `message.new`).
// Deferred with the rest of the portal-blocked test staleness (jest cannot currently run — the local
// stream-chat-js portal breaks module resolution); fix when the portal is removed.
describe('ChannelPreview', () => {
const clientUser = generateUser();
let chatClient: StreamChat;
Expand Down
119 changes: 15 additions & 104 deletions package/src/components/ChannelPreview/hooks/useChannelPreviewData.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,19 @@
import { useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect } from 'react';

import throttle from 'lodash/throttle';
import type {
Channel,
Event,
LocalMessage,
MessagePaginatorAggregateState,
MessageResponse,
ReadState,
StreamChat,
} from 'stream-chat';

import { useIsChannelMuted } from './useIsChannelMuted';
import { useIsChannelPinned } from './useIsChannelPinned';

import { useChannelsContext } from '../../../contexts';
import { useStateStore } from '../../../hooks';

const refreshUnreadCountThrottleTimeout = 400;
const refreshUnreadCountThrottleOptions = { leading: true, trailing: true };

export type LastMessageType = LocalMessage | MessageResponse;

// The last message is sourced reactively from the paginator's `aggregateState` — a store kept
Expand All @@ -30,114 +25,30 @@ const lastMessageSelector = (state: MessagePaginatorAggregateState) => ({
lastMessage: state.lastMessage ?? undefined,
});

export const useChannelPreviewData = (
channel: Channel,
client: StreamChat,
forceUpdateOverride?: number,
) => {
const [, setForceUpdate] = useState(0);
export const useChannelPreviewData = (channel: Channel, client: StreamChat) => {
const userId = client.userID;
const { lastMessage } =
useStateStore(channel.messagePaginator.aggregateState, lastMessageSelector) ?? {};
const [unread, setUnread] = useState(channel.countUnread());
const { muted } = useIsChannelMuted(channel);
const pinned = useIsChannelPinned(channel);
const { forceUpdate: contextForceUpdate } = useChannelsContext();
const channelListForceUpdate = forceUpdateOverride ?? contextForceUpdate;

const refreshUnreadCount = useMemo(
() =>
throttle(
() => {
if (muted) {
setUnread(0);
} else {
setUnread(channel.countUnread());
}
},
refreshUnreadCountThrottleTimeout,
refreshUnreadCountThrottleOptions,
),
[channel, muted],
// unread
const ownUnreadSelector = useCallback(
(state: ReadState) => ({
unread: userId ? (state.read[userId]?.unread_messages ?? 0) : 0,
}),
[userId],
);
const { unread: reactiveUnread } =
useStateStore(channel.state.readStore, ownUnreadSelector) ?? {};
// muted channels always render a zeroed unread count
const unread = muted ? 0 : (reactiveUnread ?? 0);

// drafts
useEffect(() => {
const unsubscribe = channel.messageComposer.registerDraftEventSubscriptions();
return () => unsubscribe();
}, [channel.messageComposer]);

useEffect(() => {
const { unsubscribe } = client.on('notification.mark_read', () => {
refreshUnreadCount();
});
return unsubscribe;
}, [client, refreshUnreadCount]);

/**
* This effect listens for the `notification.mark_read` event and sets the unread count to 0
*/
useEffect(() => {
const handleReadEvent = (event: Event) => {
if (!event.cid) {
return;
}
if (channel.cid !== event.cid) {
return;
}
if (event?.user?.id === client.userID) {
setUnread(0);
} else if (event?.user?.id) {
setForceUpdate((prev) => prev + 1);
}
};
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]);

/**
* This effect listens for the `notification.mark_unread` event and updates the unread count
*/
useEffect(() => {
const handleUnreadEvent = (event: Event) => {
if (!event.cid) {
return;
}
if (channel.cid !== event.cid) {
return;
}
if (event.user?.id !== client.user?.id) {
return;
}
setUnread(channel.countUnread());
};
const { unsubscribe } = client.on('notification.mark_unread', handleUnreadEvent);
return unsubscribe;
}, [client, channel]);

/**
* Keep the unread count in sync with events that can change it. The last message itself is sourced
* reactively from the paginator (see `lastMessageSelector`), so these listeners only refresh unread.
*/
useEffect(() => {
refreshUnreadCount();

const handleEvent = () => {
refreshUnreadCount();
};

const listeners = [
channel.on('message.new', handleEvent),
channel.on('message.undeleted', handleEvent),
channel.on('channel.truncated', handleEvent),
];

return () => listeners.forEach((l) => l.unsubscribe());
}, [channel, refreshUnreadCount, channelListForceUpdate]);

return { lastMessage, muted, pinned, unread };
};
Loading