stream-chat-react 12.8.1 → 12.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/ChannelList/hooks/useChannelListShape.d.ts +3 -3
- package/dist/components/ChannelList/hooks/useChannelListShape.js +52 -45
- package/dist/components/ChannelList/hooks/useChannelMembershipState.d.ts +3 -2
- package/dist/components/ChannelList/hooks/useChannelMembershipState.js +6 -15
- package/dist/components/ChannelList/hooks/useSelectedChannelState.d.ts +11 -0
- package/dist/components/ChannelList/hooks/useSelectedChannelState.js +20 -0
- package/dist/components/ChannelList/utils.d.ts +16 -6
- package/dist/components/ChannelList/utils.js +44 -18
- package/dist/components/Chat/hooks/useChat.js +1 -1
- package/dist/components/ChatAutoComplete/ChatAutoComplete.d.ts +2 -0
- package/dist/components/ChatAutoComplete/ChatAutoComplete.js +1 -1
- package/dist/components/MessageList/VirtualizedMessageListComponents.js +12 -19
- package/dist/components/MessageList/hooks/useMarkRead.js +3 -6
- package/dist/components/MessageList/renderMessages.js +14 -21
- package/dist/components/MessageList/utils.d.ts +9 -0
- package/dist/components/MessageList/utils.js +8 -0
- package/dist/index.browser.cjs +193 -114
- package/dist/index.browser.cjs.map +4 -4
- package/dist/index.node.cjs +195 -114
- package/dist/index.node.cjs.map +4 -4
- package/package.json +6 -4
|
@@ -24,7 +24,7 @@ type HandleNotificationAddedToChannelParameters<SCG extends ExtendableGenerics>
|
|
|
24
24
|
} & Required<Pick<ChannelListProps<SCG>, 'sort'>>;
|
|
25
25
|
type HandleMemberUpdatedParameters<SCG extends ExtendableGenerics> = BaseParameters<SCG> & {
|
|
26
26
|
lockChannelOrder: boolean;
|
|
27
|
-
} & Required<Pick<ChannelListProps<SCG>, 'sort'>>;
|
|
27
|
+
} & Required<Pick<ChannelListProps<SCG>, 'sort' | 'filters'>>;
|
|
28
28
|
type HandleChannelDeletedParameters<SCG extends ExtendableGenerics> = BaseParameters<SCG> & RepeatedParameters<SCG>;
|
|
29
29
|
type HandleChannelHiddenParameters<SCG extends ExtendableGenerics> = BaseParameters<SCG> & RepeatedParameters<SCG>;
|
|
30
30
|
type HandleChannelVisibleParameters<SCG extends ExtendableGenerics> = BaseParameters<SCG> & RepeatedParameters<SCG>;
|
|
@@ -37,9 +37,9 @@ export declare const useChannelListShapeDefaults: <SCG extends ExtendableGeneric
|
|
|
37
37
|
handleChannelTruncated: ({ customHandler, event, setChannels }: HandleChannelTruncatedParameters<SCG>) => void;
|
|
38
38
|
handleChannelUpdated: ({ customHandler, event, setChannels }: HandleChannelUpdatedParameters<SCG>) => void;
|
|
39
39
|
handleChannelVisible: ({ customHandler, event, setChannels }: HandleChannelVisibleParameters<SCG>) => Promise<void>;
|
|
40
|
-
handleMemberUpdated: ({ event, lockChannelOrder, setChannels, sort }: HandleMemberUpdatedParameters<SCG>) => void;
|
|
40
|
+
handleMemberUpdated: ({ event, filters, lockChannelOrder, setChannels, sort, }: HandleMemberUpdatedParameters<SCG>) => void;
|
|
41
41
|
handleMessageNew: ({ allowNewMessagesFromUnfilteredChannels, customHandler, event, filters, lockChannelOrder, setChannels, sort, }: HandleMessageNewParameters<SCG>) => void;
|
|
42
|
-
handleNotificationAddedToChannel: ({ allowNewMessagesFromUnfilteredChannels, customHandler, event, setChannels, }: HandleNotificationAddedToChannelParameters<SCG>) => Promise<void>;
|
|
42
|
+
handleNotificationAddedToChannel: ({ allowNewMessagesFromUnfilteredChannels, customHandler, event, setChannels, sort, }: HandleNotificationAddedToChannelParameters<SCG>) => Promise<void>;
|
|
43
43
|
handleNotificationMessageNew: ({ allowNewMessagesFromUnfilteredChannels, customHandler, event, filters, setChannels, sort, }: HandleNotificationMessageNewParameters<SCG>) => Promise<void>;
|
|
44
44
|
handleNotificationRemovedFromChannel: ({ customHandler, event, setChannels, }: HandleNotificationRemovedFromChannelParameters<SCG>) => void;
|
|
45
45
|
handleUserPresenceChanged: ({ event, setChannels }: HandleUserPresenceChangedParameters<SCG>) => void;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// const defaults = useChannelListShapeDefaults();
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
3
3
|
import uniqBy from 'lodash.uniqby';
|
|
4
|
-
import { findLastPinnedChannelIndex, isChannelArchived, isChannelPinned, moveChannelUpwards, shouldConsiderArchivedChannels, shouldConsiderPinnedChannels, } from '../utils';
|
|
4
|
+
import { extractSortValue, findLastPinnedChannelIndex, isChannelArchived, isChannelPinned, moveChannelUpwards, shouldConsiderArchivedChannels, shouldConsiderPinnedChannels, } from '../utils';
|
|
5
5
|
import { useChatContext } from '../../../context';
|
|
6
6
|
import { getChannel } from '../../../utils';
|
|
7
7
|
const shared = ({ customHandler, event, setChannels, }) => {
|
|
@@ -22,39 +22,37 @@ export const useChannelListShapeDefaults = () => {
|
|
|
22
22
|
if (typeof customHandler === 'function') {
|
|
23
23
|
return customHandler(setChannels, event);
|
|
24
24
|
}
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
const channelType = event.channel_type;
|
|
26
|
+
const channelId = event.channel_id;
|
|
27
|
+
if (!channelType || !channelId)
|
|
28
|
+
return;
|
|
29
|
+
setChannels((currentChannels) => {
|
|
30
|
+
const targetChannel = client.channel(channelType, channelId);
|
|
31
|
+
const targetChannelIndex = currentChannels.indexOf(targetChannel);
|
|
27
32
|
const targetChannelExistsWithinList = targetChannelIndex >= 0;
|
|
28
|
-
const targetChannel = channels[targetChannelIndex];
|
|
29
33
|
const isTargetChannelPinned = isChannelPinned(targetChannel);
|
|
30
34
|
const isTargetChannelArchived = isChannelArchived(targetChannel);
|
|
31
35
|
const considerArchivedChannels = shouldConsiderArchivedChannels(filters);
|
|
32
36
|
const considerPinnedChannels = shouldConsiderPinnedChannels(sort);
|
|
33
37
|
if (
|
|
34
|
-
// target channel is archived
|
|
35
|
-
(isTargetChannelArchived &&
|
|
36
|
-
// target channel is
|
|
37
|
-
(
|
|
38
|
+
// filter is defined, target channel is archived and filter option is set to false
|
|
39
|
+
(considerArchivedChannels && isTargetChannelArchived && !filters.archived) ||
|
|
40
|
+
// filter is defined, target channel isn't archived and filter option is set to true
|
|
41
|
+
(considerArchivedChannels && !isTargetChannelArchived && filters.archived) ||
|
|
42
|
+
// sort option is defined, target channel is pinned
|
|
43
|
+
(considerPinnedChannels && isTargetChannelPinned) ||
|
|
38
44
|
// list order is locked
|
|
39
45
|
lockChannelOrder ||
|
|
40
46
|
// target channel is not within the loaded list and loading from cache is disallowed
|
|
41
47
|
(!targetChannelExistsWithinList && !allowNewMessagesFromUnfilteredChannels)) {
|
|
42
|
-
return
|
|
43
|
-
}
|
|
44
|
-
// we either have the channel to move or we pull it from the cache (or instantiate) if it's allowed
|
|
45
|
-
const channelToMove = channels[targetChannelIndex] ??
|
|
46
|
-
(allowNewMessagesFromUnfilteredChannels && event.channel_type
|
|
47
|
-
? client.channel(event.channel_type, event.channel_id)
|
|
48
|
-
: null);
|
|
49
|
-
if (channelToMove) {
|
|
50
|
-
return moveChannelUpwards({
|
|
51
|
-
channels,
|
|
52
|
-
channelToMove,
|
|
53
|
-
channelToMoveIndexWithinChannels: targetChannelIndex,
|
|
54
|
-
sort,
|
|
55
|
-
});
|
|
48
|
+
return currentChannels;
|
|
56
49
|
}
|
|
57
|
-
return
|
|
50
|
+
return moveChannelUpwards({
|
|
51
|
+
channels: currentChannels,
|
|
52
|
+
channelToMove: targetChannel,
|
|
53
|
+
channelToMoveIndexWithinChannels: targetChannelIndex,
|
|
54
|
+
sort,
|
|
55
|
+
});
|
|
58
56
|
});
|
|
59
57
|
}, [client]);
|
|
60
58
|
const handleNotificationMessageNew = useCallback(async ({ allowNewMessagesFromUnfilteredChannels, customHandler, event, filters, setChannels, sort, }) => {
|
|
@@ -70,7 +68,7 @@ export const useChannelListShapeDefaults = () => {
|
|
|
70
68
|
type: event.channel.type,
|
|
71
69
|
});
|
|
72
70
|
const considerArchivedChannels = shouldConsiderArchivedChannels(filters);
|
|
73
|
-
if (isChannelArchived(channel) && considerArchivedChannels) {
|
|
71
|
+
if (isChannelArchived(channel) && considerArchivedChannels && !filters.archived) {
|
|
74
72
|
return;
|
|
75
73
|
}
|
|
76
74
|
if (!allowNewMessagesFromUnfilteredChannels) {
|
|
@@ -83,25 +81,31 @@ export const useChannelListShapeDefaults = () => {
|
|
|
83
81
|
sort,
|
|
84
82
|
}));
|
|
85
83
|
}, [client]);
|
|
86
|
-
const handleNotificationAddedToChannel = useCallback(async ({ allowNewMessagesFromUnfilteredChannels, customHandler, event, setChannels, }) => {
|
|
84
|
+
const handleNotificationAddedToChannel = useCallback(async ({ allowNewMessagesFromUnfilteredChannels, customHandler, event, setChannels, sort, }) => {
|
|
87
85
|
if (typeof customHandler === 'function') {
|
|
88
86
|
return customHandler(setChannels, event);
|
|
89
87
|
}
|
|
90
|
-
if (
|
|
91
|
-
|
|
92
|
-
client,
|
|
93
|
-
id: event.channel.id,
|
|
94
|
-
members: event.channel.members?.reduce((acc, { user, user_id }) => {
|
|
95
|
-
const userId = user_id || user?.id;
|
|
96
|
-
if (userId) {
|
|
97
|
-
acc.push(userId);
|
|
98
|
-
}
|
|
99
|
-
return acc;
|
|
100
|
-
}, []),
|
|
101
|
-
type: event.channel.type,
|
|
102
|
-
});
|
|
103
|
-
setChannels((channels) => uniqBy([channel, ...channels], 'cid'));
|
|
88
|
+
if (!event.channel || !allowNewMessagesFromUnfilteredChannels) {
|
|
89
|
+
return;
|
|
104
90
|
}
|
|
91
|
+
const channel = await getChannel({
|
|
92
|
+
client,
|
|
93
|
+
id: event.channel.id,
|
|
94
|
+
members: event.channel.members?.reduce((newMembers, { user, user_id }) => {
|
|
95
|
+
const userId = user_id || user?.id;
|
|
96
|
+
if (userId)
|
|
97
|
+
newMembers.push(userId);
|
|
98
|
+
return newMembers;
|
|
99
|
+
}, []),
|
|
100
|
+
type: event.channel.type,
|
|
101
|
+
});
|
|
102
|
+
// membership has been reset (target channel shouldn't be pinned nor archived)
|
|
103
|
+
setChannels((channels) => moveChannelUpwards({
|
|
104
|
+
channels,
|
|
105
|
+
channelToMove: channel,
|
|
106
|
+
channelToMoveIndexWithinChannels: -1,
|
|
107
|
+
sort,
|
|
108
|
+
}));
|
|
105
109
|
}, [client]);
|
|
106
110
|
const handleNotificationRemovedFromChannel = useCallback(({ customHandler, event, setChannels, }) => {
|
|
107
111
|
if (typeof customHandler === 'function') {
|
|
@@ -109,21 +113,22 @@ export const useChannelListShapeDefaults = () => {
|
|
|
109
113
|
}
|
|
110
114
|
setChannels((channels) => channels.filter((channel) => channel.cid !== event.channel?.cid));
|
|
111
115
|
}, []);
|
|
112
|
-
const handleMemberUpdated = useCallback(({ event, lockChannelOrder, setChannels, sort }) => {
|
|
116
|
+
const handleMemberUpdated = useCallback(({ event, filters, lockChannelOrder, setChannels, sort, }) => {
|
|
113
117
|
if (!event.member?.user || event.member.user.id !== client.userID || !event.channel_type) {
|
|
114
118
|
return;
|
|
115
119
|
}
|
|
116
|
-
const member = event.member;
|
|
117
120
|
const channelType = event.channel_type;
|
|
118
121
|
const channelId = event.channel_id;
|
|
119
122
|
const considerPinnedChannels = shouldConsiderPinnedChannels(sort);
|
|
120
|
-
|
|
121
|
-
const pinnedAtSort =
|
|
123
|
+
const considerArchivedChannels = shouldConsiderArchivedChannels(filters);
|
|
124
|
+
const pinnedAtSort = extractSortValue({ atIndex: 0, sort, targetKey: 'pinned_at' });
|
|
122
125
|
setChannels((currentChannels) => {
|
|
123
126
|
const targetChannel = client.channel(channelType, channelId);
|
|
124
127
|
// assumes that channel instances are not changing
|
|
125
128
|
const targetChannelIndex = currentChannels.indexOf(targetChannel);
|
|
126
129
|
const targetChannelExistsWithinList = targetChannelIndex >= 0;
|
|
130
|
+
const isTargetChannelArchived = isChannelArchived(targetChannel);
|
|
131
|
+
const isTargetChannelPinned = isChannelPinned(targetChannel);
|
|
127
132
|
// handle pinning
|
|
128
133
|
if (!considerPinnedChannels || lockChannelOrder)
|
|
129
134
|
return currentChannels;
|
|
@@ -132,14 +137,15 @@ export const useChannelListShapeDefaults = () => {
|
|
|
132
137
|
newChannels.splice(targetChannelIndex, 1);
|
|
133
138
|
}
|
|
134
139
|
// handle archiving (remove channel)
|
|
135
|
-
if (
|
|
140
|
+
if ((considerArchivedChannels && isTargetChannelArchived && !filters.archived) ||
|
|
141
|
+
(considerArchivedChannels && !isTargetChannelArchived && filters.archived)) {
|
|
136
142
|
return newChannels;
|
|
137
143
|
}
|
|
138
144
|
let lastPinnedChannelIndex = null;
|
|
139
145
|
// calculate last pinned channel index only if `pinned_at` sort is set to
|
|
140
146
|
// ascending order or if it's in descending order while the pin is being removed, otherwise
|
|
141
147
|
// we move to the top (index 0)
|
|
142
|
-
if (pinnedAtSort === 1 || (pinnedAtSort === -1 && !
|
|
148
|
+
if (pinnedAtSort === 1 || (pinnedAtSort === -1 && !isTargetChannelPinned)) {
|
|
143
149
|
lastPinnedChannelIndex = findLastPinnedChannelIndex({ channels: newChannels });
|
|
144
150
|
}
|
|
145
151
|
const newTargetChannelIndex = typeof lastPinnedChannelIndex === 'number' ? lastPinnedChannelIndex + 1 : 0;
|
|
@@ -311,6 +317,7 @@ export const usePrepareShapeHandlers = ({ allowNewMessagesFromUnfilteredChannels
|
|
|
311
317
|
case 'member.updated':
|
|
312
318
|
defaults.handleMemberUpdated({
|
|
313
319
|
event,
|
|
320
|
+
filters,
|
|
314
321
|
lockChannelOrder,
|
|
315
322
|
setChannels,
|
|
316
323
|
sort,
|
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
import type { Channel, ExtendableGenerics } from 'stream-chat';
|
|
2
|
-
export declare
|
|
1
|
+
import type { Channel, ChannelMemberResponse, ExtendableGenerics } from 'stream-chat';
|
|
2
|
+
export declare function useChannelMembershipState<SCG extends ExtendableGenerics>(channel: Channel<SCG>): ChannelMemberResponse<SCG>;
|
|
3
|
+
export declare function useChannelMembershipState<SCG extends ExtendableGenerics>(channel?: Channel<SCG> | undefined): ChannelMemberResponse<SCG> | undefined;
|
|
@@ -1,15 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
if (!channel)
|
|
8
|
-
return;
|
|
9
|
-
const subscriptions = ['member.updated'].map((v) => client.on(v, () => {
|
|
10
|
-
setMembership(channel.state.membership);
|
|
11
|
-
}));
|
|
12
|
-
return () => subscriptions.forEach((subscription) => subscription.unsubscribe());
|
|
13
|
-
}, [client, channel]);
|
|
14
|
-
return membership;
|
|
15
|
-
};
|
|
1
|
+
import { useSelectedChannelState } from './useSelectedChannelState';
|
|
2
|
+
const selector = (c) => c.state.membership;
|
|
3
|
+
const keys = ['member.updated'];
|
|
4
|
+
export function useChannelMembershipState(channel) {
|
|
5
|
+
return useSelectedChannelState({ channel, selector, stateChangeEventKeys: keys });
|
|
6
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Channel, EventTypes, ExtendableGenerics } from 'stream-chat';
|
|
2
|
+
export declare function useSelectedChannelState<SCG extends ExtendableGenerics, O>(_: {
|
|
3
|
+
channel: Channel<SCG>;
|
|
4
|
+
selector: (channel: Channel<SCG>) => O;
|
|
5
|
+
stateChangeEventKeys?: EventTypes[];
|
|
6
|
+
}): O;
|
|
7
|
+
export declare function useSelectedChannelState<SCG extends ExtendableGenerics, O>(_: {
|
|
8
|
+
selector: (channel: Channel<SCG>) => O;
|
|
9
|
+
channel?: Channel<SCG> | undefined;
|
|
10
|
+
stateChangeEventKeys?: EventTypes[];
|
|
11
|
+
}): O | undefined;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { useSyncExternalStore } from 'use-sync-external-store/shim';
|
|
3
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
4
|
+
const noop = () => { };
|
|
5
|
+
export function useSelectedChannelState({ channel, stateChangeEventKeys = ['all'], selector, }) {
|
|
6
|
+
const subscribe = useCallback((onStoreChange) => {
|
|
7
|
+
if (!channel)
|
|
8
|
+
return noop;
|
|
9
|
+
const subscriptions = stateChangeEventKeys.map((et) => channel.on(et, () => {
|
|
10
|
+
onStoreChange(selector(channel));
|
|
11
|
+
}));
|
|
12
|
+
return () => subscriptions.forEach((subscription) => subscription.unsubscribe());
|
|
13
|
+
}, [channel, selector, stateChangeEventKeys]);
|
|
14
|
+
const getSnapshot = useCallback(() => {
|
|
15
|
+
if (!channel)
|
|
16
|
+
return undefined;
|
|
17
|
+
return selector(channel);
|
|
18
|
+
}, [channel, selector]);
|
|
19
|
+
return useSyncExternalStore(subscribe, getSnapshot);
|
|
20
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Channel, ChannelSort, ExtendableGenerics } from 'stream-chat';
|
|
1
|
+
import type { Channel, ChannelSort, ChannelSortBase, ExtendableGenerics } from 'stream-chat';
|
|
2
2
|
import type { DefaultStreamChatGenerics } from '../../types/types';
|
|
3
3
|
import type { ChannelListProps } from './ChannelList';
|
|
4
4
|
export declare const MAX_QUERY_CHANNELS_LIMIT = 30;
|
|
@@ -29,18 +29,28 @@ type MoveChannelUpwardsParams<SCG extends DefaultStreamChatGenerics = DefaultStr
|
|
|
29
29
|
*/
|
|
30
30
|
channelToMoveIndexWithinChannels?: number;
|
|
31
31
|
};
|
|
32
|
-
/**
|
|
33
|
-
* This function should not be used to move pinned already channels.
|
|
34
|
-
*/
|
|
35
32
|
export declare const moveChannelUpwards: <SCG extends DefaultStreamChatGenerics = DefaultStreamChatGenerics>({ channels, channelToMove, channelToMoveIndexWithinChannels, sort, }: MoveChannelUpwardsParams<SCG>) => Channel<SCG>[];
|
|
36
33
|
/**
|
|
37
|
-
* Returns true only if
|
|
34
|
+
* Returns `true` only if object with `pinned_at` property is first within the `sort` array
|
|
35
|
+
* or if `pinned_at` key of the `sort` object gets picked first when using `for...in` looping mechanism
|
|
36
|
+
* and value of the `pinned_at` is either `1` or `-1`.
|
|
38
37
|
*/
|
|
39
38
|
export declare const shouldConsiderPinnedChannels: <SCG extends ExtendableGenerics>(sort: ChannelListProps<SCG>['sort']) => boolean;
|
|
39
|
+
export declare const extractSortValue: <SCG extends ExtendableGenerics>({ atIndex, sort, targetKey, }: {
|
|
40
|
+
atIndex: number;
|
|
41
|
+
targetKey: keyof ChannelSortBase<SCG>;
|
|
42
|
+
sort?: ChannelListProps<SCG>['sort'];
|
|
43
|
+
}) => NonNullable<ChannelSortBase<SCG>["created_at" | "pinned_at" | "updated_at" | keyof SCG["channelType"] | "has_unread" | "last_message_at" | "last_updated" | "member_count" | "unread_count"]> | null;
|
|
40
44
|
/**
|
|
41
|
-
* Returns `true` only if `archived` property is
|
|
45
|
+
* Returns `true` only if `archived` property is of type `boolean` within `filters` object.
|
|
42
46
|
*/
|
|
43
47
|
export declare const shouldConsiderArchivedChannels: <SCG extends ExtendableGenerics>(filters: ChannelListProps<SCG>['filters']) => boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Returns `true` only if `pinned_at` property is of type `string` within `membership` object.
|
|
50
|
+
*/
|
|
44
51
|
export declare const isChannelPinned: <SCG extends ExtendableGenerics>(channel: Channel<SCG>) => boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Returns `true` only if `archived_at` property is of type `string` within `membership` object.
|
|
54
|
+
*/
|
|
45
55
|
export declare const isChannelArchived: <SCG extends ExtendableGenerics>(channel: Channel<SCG>) => boolean;
|
|
46
56
|
export {};
|
|
@@ -31,9 +31,6 @@ export function findLastPinnedChannelIndex({ channels, }) {
|
|
|
31
31
|
}
|
|
32
32
|
return lastPinnedChannelIndex;
|
|
33
33
|
}
|
|
34
|
-
/**
|
|
35
|
-
* This function should not be used to move pinned already channels.
|
|
36
|
-
*/
|
|
37
34
|
export const moveChannelUpwards = ({ channels, channelToMove, channelToMoveIndexWithinChannels, sort, }) => {
|
|
38
35
|
// get index of channel to move up
|
|
39
36
|
const targetChannelIndex = channelToMoveIndexWithinChannels ??
|
|
@@ -44,8 +41,10 @@ export const moveChannelUpwards = ({ channels, channelToMove, channelToMoveIndex
|
|
|
44
41
|
// receive messages and are not pinned should move upwards but only under the last pinned channel
|
|
45
42
|
// in the list
|
|
46
43
|
const considerPinnedChannels = shouldConsiderPinnedChannels(sort);
|
|
47
|
-
|
|
44
|
+
const isTargetChannelPinned = isChannelPinned(channelToMove);
|
|
45
|
+
if (targetChannelAlreadyAtTheTop || (considerPinnedChannels && isTargetChannelPinned)) {
|
|
48
46
|
return channels;
|
|
47
|
+
}
|
|
49
48
|
const newChannels = [...channels];
|
|
50
49
|
// target channel index is known, remove it from the list
|
|
51
50
|
if (targetChannelExistsWithinList) {
|
|
@@ -62,35 +61,62 @@ export const moveChannelUpwards = ({ channels, channelToMove, channelToMoveIndex
|
|
|
62
61
|
return newChannels;
|
|
63
62
|
};
|
|
64
63
|
/**
|
|
65
|
-
* Returns true only if
|
|
64
|
+
* Returns `true` only if object with `pinned_at` property is first within the `sort` array
|
|
65
|
+
* or if `pinned_at` key of the `sort` object gets picked first when using `for...in` looping mechanism
|
|
66
|
+
* and value of the `pinned_at` is either `1` or `-1`.
|
|
66
67
|
*/
|
|
67
68
|
export const shouldConsiderPinnedChannels = (sort) => {
|
|
68
|
-
|
|
69
|
+
const value = extractSortValue({ atIndex: 0, sort, targetKey: 'pinned_at' });
|
|
70
|
+
if (typeof value !== 'number')
|
|
69
71
|
return false;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (!
|
|
74
|
-
return
|
|
75
|
-
|
|
72
|
+
return Math.abs(value) === 1;
|
|
73
|
+
};
|
|
74
|
+
export const extractSortValue = ({ atIndex, sort, targetKey, }) => {
|
|
75
|
+
if (!sort)
|
|
76
|
+
return null;
|
|
77
|
+
let option = null;
|
|
78
|
+
if (Array.isArray(sort)) {
|
|
79
|
+
option = sort[atIndex] ?? null;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
let index = 0;
|
|
83
|
+
for (const key in sort) {
|
|
84
|
+
if (index !== atIndex) {
|
|
85
|
+
index++;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (key !== targetKey) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
option = sort;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return option?.[targetKey] ?? null;
|
|
76
96
|
};
|
|
77
97
|
/**
|
|
78
|
-
* Returns `true` only if `archived` property is
|
|
98
|
+
* Returns `true` only if `archived` property is of type `boolean` within `filters` object.
|
|
79
99
|
*/
|
|
80
100
|
export const shouldConsiderArchivedChannels = (filters) => {
|
|
81
101
|
if (!filters)
|
|
82
102
|
return false;
|
|
83
|
-
return
|
|
103
|
+
return typeof filters.archived === 'boolean';
|
|
84
104
|
};
|
|
105
|
+
/**
|
|
106
|
+
* Returns `true` only if `pinned_at` property is of type `string` within `membership` object.
|
|
107
|
+
*/
|
|
85
108
|
export const isChannelPinned = (channel) => {
|
|
86
109
|
if (!channel)
|
|
87
110
|
return false;
|
|
88
|
-
const
|
|
89
|
-
return
|
|
111
|
+
const membership = channel.state.membership;
|
|
112
|
+
return typeof membership.pinned_at === 'string';
|
|
90
113
|
};
|
|
114
|
+
/**
|
|
115
|
+
* Returns `true` only if `archived_at` property is of type `string` within `membership` object.
|
|
116
|
+
*/
|
|
91
117
|
export const isChannelArchived = (channel) => {
|
|
92
118
|
if (!channel)
|
|
93
119
|
return false;
|
|
94
|
-
const
|
|
95
|
-
return
|
|
120
|
+
const membership = channel.state.membership;
|
|
121
|
+
return typeof membership.archived_at === 'string';
|
|
96
122
|
};
|
|
@@ -28,7 +28,7 @@ export const useChat = ({ client, defaultLanguage = 'en', i18nInstance, initialN
|
|
|
28
28
|
if (!userAgent.includes('stream-chat-react')) {
|
|
29
29
|
// result looks like: 'stream-chat-react-2.3.2-stream-chat-javascript-client-browser-2.2.2'
|
|
30
30
|
// the upper-case text between double underscores is replaced with the actual semantic version of the library
|
|
31
|
-
client.setUserAgent(`stream-chat-react-12.
|
|
31
|
+
client.setUserAgent(`stream-chat-react-12.9.0-${userAgent}`);
|
|
32
32
|
}
|
|
33
33
|
client.threads.registerSubscriptions();
|
|
34
34
|
client.polls.registerSubscriptions();
|
|
@@ -53,6 +53,8 @@ export type SuggestionListProps<StreamChatGenerics extends DefaultStreamChatGene
|
|
|
53
53
|
};
|
|
54
54
|
}>;
|
|
55
55
|
export type ChatAutoCompleteProps<T extends UnknownType = UnknownType> = {
|
|
56
|
+
/** Override the default disabled state of the underlying `textarea` component. */
|
|
57
|
+
disabled?: boolean;
|
|
56
58
|
/** Function to override the default submit handler on the underlying `textarea` component */
|
|
57
59
|
handleSubmit?: (event: React.BaseSyntheticEvent) => void;
|
|
58
60
|
/** Function to run on blur of the underlying `textarea` component */
|
|
@@ -28,6 +28,6 @@ const UnMemoizedChatAutoComplete = (props) => {
|
|
|
28
28
|
innerRef.current = ref;
|
|
29
29
|
}
|
|
30
30
|
}, [innerRef]);
|
|
31
|
-
return (React.createElement(AutoCompleteTextarea, { additionalTextareaProps: messageInput.additionalTextareaProps, "aria-label": cooldownRemaining ? t('Slow Mode ON') : placeholder, className: 'str-chat__textarea__textarea str-chat__message-textarea', closeCommandsList: messageInput.closeCommandsList, closeMentionsList: messageInput.closeMentionsList, containerClassName: 'str-chat__textarea str-chat__message-textarea-react-host', disabled: disabled || !!cooldownRemaining, disableMentions: messageInput.disableMentions, grow: messageInput.grow, handleSubmit: props.handleSubmit || messageInput.handleSubmit, innerRef: updateInnerRef, loadingComponent: LoadingIndicator, maxRows: messageInput.maxRows, minChar: 0, minRows: messageInput.minRows, onBlur: props.onBlur, onChange: props.onChange || messageInput.handleChange, onFocus: props.onFocus, onPaste: props.onPaste || messageInput.onPaste, placeholder: cooldownRemaining ? t('Slow Mode ON') : placeholder, replaceWord: emojiReplace, rows: props.rows || 1, shouldSubmit: messageInput.shouldSubmit, showCommandsList: messageInput.showCommandsList, showMentionsList: messageInput.showMentionsList, SuggestionItem: SuggestionItem, SuggestionList: SuggestionList, trigger: messageInput.autocompleteTriggers || {}, value: props.value || messageInput.text }));
|
|
31
|
+
return (React.createElement(AutoCompleteTextarea, { additionalTextareaProps: messageInput.additionalTextareaProps, "aria-label": cooldownRemaining ? t('Slow Mode ON') : placeholder, className: 'str-chat__textarea__textarea str-chat__message-textarea', closeCommandsList: messageInput.closeCommandsList, closeMentionsList: messageInput.closeMentionsList, containerClassName: 'str-chat__textarea str-chat__message-textarea-react-host', disabled: (props.disabled ?? disabled) || !!cooldownRemaining, disableMentions: messageInput.disableMentions, grow: messageInput.grow, handleSubmit: props.handleSubmit || messageInput.handleSubmit, innerRef: updateInnerRef, loadingComponent: LoadingIndicator, maxRows: messageInput.maxRows, minChar: 0, minRows: messageInput.minRows, onBlur: props.onBlur, onChange: props.onChange || messageInput.handleChange, onFocus: props.onFocus, onPaste: props.onPaste || messageInput.onPaste, placeholder: cooldownRemaining ? t('Slow Mode ON') : placeholder, replaceWord: emojiReplace, rows: props.rows || 1, shouldSubmit: messageInput.shouldSubmit, showCommandsList: messageInput.showCommandsList, showMentionsList: messageInput.showMentionsList, SuggestionItem: SuggestionItem, SuggestionList: SuggestionList, trigger: messageInput.autocompleteTriggers || {}, value: props.value || messageInput.text }));
|
|
32
32
|
};
|
|
33
33
|
export const ChatAutoComplete = React.memo(UnMemoizedChatAutoComplete);
|
|
@@ -5,7 +5,7 @@ import { EmptyStateIndicator as DefaultEmptyStateIndicator } from '../EmptyState
|
|
|
5
5
|
import { LoadingIndicator as DefaultLoadingIndicator } from '../Loading';
|
|
6
6
|
import { isMessageEdited, Message } from '../Message';
|
|
7
7
|
import { useComponentContext } from '../../context';
|
|
8
|
-
import { isDateSeparatorMessage } from './utils';
|
|
8
|
+
import { getIsFirstUnreadMessage, isDateSeparatorMessage } from './utils';
|
|
9
9
|
const PREPEND_OFFSET = 10 ** 7;
|
|
10
10
|
export function calculateItemIndex(virtuosoIndex, numItemsPrepended) {
|
|
11
11
|
return virtuosoIndex + numItemsPrepended - PREPEND_OFFSET;
|
|
@@ -72,24 +72,17 @@ export const messageRenderer = (virtuosoIndex, _data, virtuosoContext) => {
|
|
|
72
72
|
(maybePrevMessage && isMessageEdited(maybePrevMessage)));
|
|
73
73
|
const endOfGroup = shouldGroupByUser &&
|
|
74
74
|
(message.user?.id !== maybeNextMessage?.user?.id || isMessageEdited(message));
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
lastReadTimestamp &&
|
|
85
|
-
createdAtTimestamp > lastReadTimestamp &&
|
|
86
|
-
isFirstMessage);
|
|
87
|
-
const showUnreadSeparatorAbove = !lastReadMessageId && isFirstUnreadMessage;
|
|
88
|
-
const showUnreadSeparatorBelow = isLastReadMessage && !isNewestMessage && (firstUnreadMessageId || !!unreadMessageCount);
|
|
75
|
+
const isFirstUnreadMessage = getIsFirstUnreadMessage({
|
|
76
|
+
firstUnreadMessageId,
|
|
77
|
+
isFirstMessage: streamMessageIndex === 0,
|
|
78
|
+
lastReadDate,
|
|
79
|
+
lastReadMessageId,
|
|
80
|
+
message,
|
|
81
|
+
previousMessage: streamMessageIndex ? messageList[streamMessageIndex - 1] : undefined,
|
|
82
|
+
unreadMessageCount,
|
|
83
|
+
});
|
|
89
84
|
return (React.createElement(React.Fragment, null,
|
|
90
|
-
|
|
85
|
+
isFirstUnreadMessage && (React.createElement("div", { className: 'str-chat__unread-messages-separator-wrapper' },
|
|
91
86
|
React.createElement(UnreadMessagesSeparator, { unreadCount: unreadMessageCount }))),
|
|
92
|
-
React.createElement(Message, { additionalMessageInputProps: additionalMessageInputProps, autoscrollToBottom: virtuosoRef.current?.autoscrollToBottom, closeReactionSelectorOnClick: closeReactionSelectorOnClick, customMessageActions: customMessageActions, endOfGroup: endOfGroup, firstOfGroup: firstOfGroup, formatDate: formatDate, groupedByUser: groupedByUser, groupStyles: [messageGroupStyles[message.id] ?? ''], lastReceivedId: lastReceivedMessageId, message: message, Message: MessageUIComponent, messageActions: messageActions, openThread: openThread, reactionDetailsSort: reactionDetailsSort, readBy: ownMessagesReadByOthers[message.id] || [], sortReactionDetails: sortReactionDetails, sortReactions: sortReactions, threadList: threadList })
|
|
93
|
-
showUnreadSeparatorBelow && (React.createElement("div", { className: 'str-chat__unread-messages-separator-wrapper' },
|
|
94
|
-
React.createElement(UnreadMessagesSeparator, { unreadCount: unreadMessageCount })))));
|
|
87
|
+
React.createElement(Message, { additionalMessageInputProps: additionalMessageInputProps, autoscrollToBottom: virtuosoRef.current?.autoscrollToBottom, closeReactionSelectorOnClick: closeReactionSelectorOnClick, customMessageActions: customMessageActions, endOfGroup: endOfGroup, firstOfGroup: firstOfGroup, formatDate: formatDate, groupedByUser: groupedByUser, groupStyles: [messageGroupStyles[message.id] ?? ''], lastReceivedId: lastReceivedMessageId, message: message, Message: MessageUIComponent, messageActions: messageActions, openThread: openThread, reactionDetailsSort: reactionDetailsSort, readBy: ownMessagesReadByOthers[message.id] || [], sortReactionDetails: sortReactionDetails, sortReactions: sortReactions, threadList: threadList })));
|
|
95
88
|
};
|
|
@@ -26,7 +26,6 @@ export const useMarkRead = ({ isMessageListScrolledToBottom, messageListIsThread
|
|
|
26
26
|
markRead();
|
|
27
27
|
};
|
|
28
28
|
const handleMessageNew = (event) => {
|
|
29
|
-
const newMessageToCurrentChannel = event.cid === channel.cid;
|
|
30
29
|
const isOwnMessage = event.user?.id && event.user.id === client.user?.id;
|
|
31
30
|
const mainChannelUpdated = !event.message?.parent_id || event.message?.show_in_channel;
|
|
32
31
|
if (isOwnMessage)
|
|
@@ -45,13 +44,11 @@ export const useMarkRead = ({ isMessageListScrolledToBottom, messageListIsThread
|
|
|
45
44
|
};
|
|
46
45
|
});
|
|
47
46
|
}
|
|
48
|
-
else if (
|
|
49
|
-
mainChannelUpdated &&
|
|
50
|
-
shouldMarkRead(channel.countUnread())) {
|
|
47
|
+
else if (mainChannelUpdated && shouldMarkRead(channel.countUnread())) {
|
|
51
48
|
markRead();
|
|
52
49
|
}
|
|
53
50
|
};
|
|
54
|
-
|
|
51
|
+
channel.on('message.new', handleMessageNew);
|
|
55
52
|
document.addEventListener('visibilitychange', onVisibilityChange);
|
|
56
53
|
const hasScrolledToBottom = previousRenderMessageListScrolledToBottom.current !== isMessageListScrolledToBottom &&
|
|
57
54
|
isMessageListScrolledToBottom;
|
|
@@ -59,7 +56,7 @@ export const useMarkRead = ({ isMessageListScrolledToBottom, messageListIsThread
|
|
|
59
56
|
markRead();
|
|
60
57
|
previousRenderMessageListScrolledToBottom.current = isMessageListScrolledToBottom;
|
|
61
58
|
return () => {
|
|
62
|
-
|
|
59
|
+
channel.off('message.new', handleMessageNew);
|
|
63
60
|
document.removeEventListener('visibilitychange', onVisibilityChange);
|
|
64
61
|
};
|
|
65
62
|
}, [
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { Fragment } from 'react';
|
|
2
|
-
import { isDateSeparatorMessage } from './utils';
|
|
2
|
+
import { getIsFirstUnreadMessage, isDateSeparatorMessage } from './utils';
|
|
3
3
|
import { Message } from '../Message';
|
|
4
4
|
import { DateSeparator as DefaultDateSeparator } from '../DateSeparator';
|
|
5
5
|
import { EventComponent as DefaultMessageSystem } from '../EventComponent';
|
|
@@ -9,6 +9,7 @@ export function defaultRenderMessages({ channelUnreadUiState, components, custom
|
|
|
9
9
|
const { DateSeparator = DefaultDateSeparator, HeaderComponent, MessageSystem = DefaultMessageSystem, UnreadMessagesSeparator = DefaultUnreadMessagesSeparator, } = components;
|
|
10
10
|
const renderedMessages = [];
|
|
11
11
|
let firstMessage;
|
|
12
|
+
let previousMessage = undefined;
|
|
12
13
|
for (let index = 0; index < messages.length; index++) {
|
|
13
14
|
const message = messages[index];
|
|
14
15
|
if (isDateSeparatorMessage(message)) {
|
|
@@ -29,29 +30,21 @@ export function defaultRenderMessages({ channelUnreadUiState, components, custom
|
|
|
29
30
|
}
|
|
30
31
|
const groupStyles = messageGroupStyles[message.id] || '';
|
|
31
32
|
const messageClass = customClasses?.message || `str-chat__li str-chat__li--${groupStyles}`;
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
!!lastReadTimestamp &&
|
|
42
|
-
createdAtTimestamp > lastReadTimestamp &&
|
|
43
|
-
isFirstMessage);
|
|
44
|
-
const showUnreadSeparatorAbove = !channelUnreadUiState?.last_read_message_id && isFirstUnreadMessage;
|
|
45
|
-
const showUnreadSeparatorBelow = isLastReadMessage &&
|
|
46
|
-
!isNewestMessage &&
|
|
47
|
-
(channelUnreadUiState?.first_unread_message_id || !!channelUnreadUiState?.unread_messages); // this part has to be here as we do not mark channel read when sending a message
|
|
33
|
+
const isFirstUnreadMessage = getIsFirstUnreadMessage({
|
|
34
|
+
firstUnreadMessageId: channelUnreadUiState?.first_unread_message_id,
|
|
35
|
+
isFirstMessage: !!firstMessage?.id && firstMessage.id === message.id,
|
|
36
|
+
lastReadDate: channelUnreadUiState?.last_read,
|
|
37
|
+
lastReadMessageId: channelUnreadUiState?.last_read_message_id,
|
|
38
|
+
message,
|
|
39
|
+
previousMessage,
|
|
40
|
+
unreadMessageCount: channelUnreadUiState?.unread_messages,
|
|
41
|
+
});
|
|
48
42
|
renderedMessages.push(React.createElement(Fragment, { key: message.id || message.created_at },
|
|
49
|
-
|
|
43
|
+
isFirstUnreadMessage && UnreadMessagesSeparator && (React.createElement("li", { className: 'str-chat__li str-chat__unread-messages-separator-wrapper' },
|
|
50
44
|
React.createElement(UnreadMessagesSeparator, { unreadCount: channelUnreadUiState?.unread_messages }))),
|
|
51
45
|
React.createElement("li", { className: messageClass, "data-message-id": message.id, "data-testid": messageClass },
|
|
52
|
-
React.createElement(Message, { groupStyles: [groupStyles], lastReceivedId: lastReceivedId, message: message, readBy: readData[message.id] || [], ...messageProps }))
|
|
53
|
-
|
|
54
|
-
React.createElement(UnreadMessagesSeparator, { unreadCount: channelUnreadUiState?.unread_messages })))));
|
|
46
|
+
React.createElement(Message, { groupStyles: [groupStyles], lastReceivedId: lastReceivedId, message: message, readBy: readData[message.id] || [], ...messageProps }))));
|
|
47
|
+
previousMessage = message;
|
|
55
48
|
}
|
|
56
49
|
}
|
|
57
50
|
return renderedMessages;
|
|
@@ -68,4 +68,13 @@ type DateSeparatorMessage = {
|
|
|
68
68
|
unread: boolean;
|
|
69
69
|
};
|
|
70
70
|
export declare function isDateSeparatorMessage<StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics>(message: StreamMessage<StreamChatGenerics>): message is DateSeparatorMessage;
|
|
71
|
+
export declare const getIsFirstUnreadMessage: ({ firstUnreadMessageId, isFirstMessage, lastReadDate, lastReadMessageId, message, previousMessage, unreadMessageCount, }: {
|
|
72
|
+
isFirstMessage: boolean;
|
|
73
|
+
message: StreamMessage;
|
|
74
|
+
firstUnreadMessageId?: string;
|
|
75
|
+
lastReadDate?: Date;
|
|
76
|
+
lastReadMessageId?: string;
|
|
77
|
+
previousMessage?: StreamMessage;
|
|
78
|
+
unreadMessageCount?: number;
|
|
79
|
+
}) => boolean;
|
|
71
80
|
export {};
|
|
@@ -241,3 +241,11 @@ export const hasNotMoreMessages = (returnedCountMessages, limit) => returnedCoun
|
|
|
241
241
|
export function isDateSeparatorMessage(message) {
|
|
242
242
|
return message.customType === CUSTOM_MESSAGE_TYPE.date && !!message.date && isDate(message.date);
|
|
243
243
|
}
|
|
244
|
+
export const getIsFirstUnreadMessage = ({ firstUnreadMessageId, isFirstMessage, lastReadDate, lastReadMessageId, message, previousMessage, unreadMessageCount = 0, }) => {
|
|
245
|
+
const createdAtTimestamp = message.created_at && new Date(message.created_at).getTime();
|
|
246
|
+
const lastReadTimestamp = lastReadDate?.getTime();
|
|
247
|
+
const messageIsUnread = !!createdAtTimestamp && !!lastReadTimestamp && createdAtTimestamp > lastReadTimestamp;
|
|
248
|
+
const previousMessageIsLastRead = !!lastReadMessageId && lastReadMessageId === previousMessage?.id;
|
|
249
|
+
return (firstUnreadMessageId === message.id ||
|
|
250
|
+
(!!unreadMessageCount && messageIsUnread && (isFirstMessage || previousMessageIsLastRead)));
|
|
251
|
+
};
|