@lobehub/chat 0.161.5 → 0.161.6
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/CHANGELOG.md +17 -0
- package/package.json +1 -1
- package/src/app/(main)/chat/(workspace)/@topic/default.tsx +9 -2
- package/src/app/(main)/chat/(workspace)/@topic/features/{TopicListContent/SkeletonList.tsx → SkeletonList.tsx} +2 -0
- package/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/index.tsx +13 -24
- package/src/store/chat/slices/topic/action.test.ts +96 -2
- package/src/store/chat/slices/topic/action.ts +11 -1
- package/src/store/chat/slices/topic/initialState.ts +2 -0
- package/src/store/chat/slices/topic/selectors.test.ts +62 -10
- package/src/store/chat/slices/topic/selectors.ts +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
### [Version 0.161.6](https://github.com/lobehub/lobe-chat/compare/v0.161.5...v0.161.6)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2024-05-22**</sup>
|
|
8
|
+
|
|
9
|
+
<br/>
|
|
10
|
+
|
|
11
|
+
<details>
|
|
12
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
13
|
+
|
|
14
|
+
</details>
|
|
15
|
+
|
|
16
|
+
<div align="right">
|
|
17
|
+
|
|
18
|
+
[](#readme-top)
|
|
19
|
+
|
|
20
|
+
</div>
|
|
21
|
+
|
|
5
22
|
### [Version 0.161.5](https://github.com/lobehub/lobe-chat/compare/v0.161.4...v0.161.5)
|
|
6
23
|
|
|
7
24
|
<sup>Released on **2024-05-22**</sup>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/chat",
|
|
3
|
-
"version": "0.161.
|
|
3
|
+
"version": "0.161.6",
|
|
4
4
|
"description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"framework",
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
// import TopicListContent from './features/TopicListContent';
|
|
2
|
+
import React, { Suspense, lazy } from 'react';
|
|
3
|
+
|
|
1
4
|
import { isMobileDevice } from '@/utils/responsive';
|
|
2
5
|
|
|
3
6
|
import Desktop from './_layout/Desktop';
|
|
4
7
|
import Mobile from './_layout/Mobile';
|
|
8
|
+
import SkeletonList from './features/SkeletonList';
|
|
5
9
|
import SystemRole from './features/SystemRole';
|
|
6
|
-
|
|
10
|
+
|
|
11
|
+
const TopicContent = lazy(() => import('./features/TopicListContent'));
|
|
7
12
|
|
|
8
13
|
const Topic = () => {
|
|
9
14
|
const mobile = isMobileDevice();
|
|
@@ -14,7 +19,9 @@ const Topic = () => {
|
|
|
14
19
|
<>
|
|
15
20
|
{!mobile && <SystemRole />}
|
|
16
21
|
<Layout>
|
|
17
|
-
<
|
|
22
|
+
<Suspense fallback={<SkeletonList />}>
|
|
23
|
+
<TopicContent />
|
|
24
|
+
</Suspense>
|
|
18
25
|
</Layout>
|
|
19
26
|
</>
|
|
20
27
|
);
|
|
@@ -3,20 +3,19 @@
|
|
|
3
3
|
import { EmptyCard } from '@lobehub/ui';
|
|
4
4
|
import { useThemeMode } from 'antd-style';
|
|
5
5
|
import isEqual from 'fast-deep-equal';
|
|
6
|
-
import React, {
|
|
6
|
+
import React, { memo, useCallback, useMemo, useRef } from 'react';
|
|
7
7
|
import { useTranslation } from 'react-i18next';
|
|
8
8
|
import { Flexbox } from 'react-layout-kit';
|
|
9
9
|
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
|
|
10
10
|
|
|
11
11
|
import { imageUrl } from '@/const/url';
|
|
12
|
-
import { isServerMode } from '@/const/version';
|
|
13
12
|
import { useChatStore } from '@/store/chat';
|
|
14
13
|
import { topicSelectors } from '@/store/chat/selectors';
|
|
15
14
|
import { useSessionStore } from '@/store/session';
|
|
16
15
|
import { useUserStore } from '@/store/user';
|
|
17
16
|
import { ChatTopic } from '@/types/topic';
|
|
18
17
|
|
|
19
|
-
import { Placeholder, SkeletonList } from '
|
|
18
|
+
import { Placeholder, SkeletonList } from '../SkeletonList';
|
|
20
19
|
import TopicItem from './TopicItem';
|
|
21
20
|
|
|
22
21
|
const TopicListContent = memo(() => {
|
|
@@ -34,21 +33,19 @@ const TopicListContent = memo(() => {
|
|
|
34
33
|
s.updateGuideState,
|
|
35
34
|
]);
|
|
36
35
|
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
} as ChatTopic,
|
|
44
|
-
...topicSelectors.displayTopics(s),
|
|
36
|
+
const activeTopicList = useChatStore(topicSelectors.displayTopics, isEqual);
|
|
37
|
+
|
|
38
|
+
const topics = useMemo(
|
|
39
|
+
() => [
|
|
40
|
+
{ favorite: false, id: 'default', title: t('topic.defaultTitle') } as ChatTopic,
|
|
41
|
+
...(activeTopicList || []),
|
|
45
42
|
],
|
|
46
|
-
|
|
43
|
+
[activeTopicList],
|
|
47
44
|
);
|
|
48
45
|
|
|
49
46
|
const [sessionId] = useSessionStore((s) => [s.activeId]);
|
|
50
47
|
|
|
51
|
-
|
|
48
|
+
useFetchTopics(sessionId);
|
|
52
49
|
|
|
53
50
|
const itemContent = useCallback(
|
|
54
51
|
(index: number, { id, favorite, title }: ChatTopic) =>
|
|
@@ -62,13 +59,9 @@ const TopicListContent = memo(() => {
|
|
|
62
59
|
|
|
63
60
|
const activeIndex = topics.findIndex((topic) => topic.id === activeTopicId);
|
|
64
61
|
|
|
65
|
-
// first time loading
|
|
66
|
-
if (!topicsInit) return <SkeletonList />;
|
|
67
|
-
|
|
68
|
-
// in server mode and re-loading
|
|
69
|
-
if (isServerMode && isLoading) return <SkeletonList />;
|
|
62
|
+
// first time loading or has no data
|
|
63
|
+
if (!topicsInit || !activeTopicList) return <SkeletonList />;
|
|
70
64
|
|
|
71
|
-
// in client mode no need the loading state for better UX
|
|
72
65
|
return (
|
|
73
66
|
<>
|
|
74
67
|
{topicLength === 0 && visible && (
|
|
@@ -111,8 +104,4 @@ const TopicListContent = memo(() => {
|
|
|
111
104
|
|
|
112
105
|
TopicListContent.displayName = 'TopicListContent';
|
|
113
106
|
|
|
114
|
-
export default
|
|
115
|
-
<Suspense fallback={<SkeletonList />}>
|
|
116
|
-
<TopicListContent />
|
|
117
|
-
</Suspense>
|
|
118
|
-
));
|
|
107
|
+
export default TopicListContent;
|
|
@@ -12,12 +12,14 @@ import { ChatTopic } from '@/types/topic';
|
|
|
12
12
|
|
|
13
13
|
import { useChatStore } from '../../store';
|
|
14
14
|
|
|
15
|
+
vi.mock('zustand/traditional');
|
|
15
16
|
// Mock topicService 和 messageService
|
|
16
17
|
vi.mock('@/services/topic', () => ({
|
|
17
18
|
topicService: {
|
|
18
19
|
removeTopics: vi.fn(),
|
|
19
20
|
removeAllTopic: vi.fn(),
|
|
20
21
|
removeTopic: vi.fn(),
|
|
22
|
+
cloneTopic: vi.fn(),
|
|
21
23
|
createTopic: vi.fn(),
|
|
22
24
|
updateTopicFavorite: vi.fn(),
|
|
23
25
|
updateTopicTitle: vi.fn(),
|
|
@@ -31,9 +33,23 @@ vi.mock('@/services/topic', () => ({
|
|
|
31
33
|
vi.mock('@/services/message', () => ({
|
|
32
34
|
messageService: {
|
|
33
35
|
removeMessages: vi.fn(),
|
|
36
|
+
getMessages: vi.fn(),
|
|
34
37
|
},
|
|
35
38
|
}));
|
|
36
39
|
|
|
40
|
+
vi.mock('@/components/AntdStaticMethods', () => ({
|
|
41
|
+
message: {
|
|
42
|
+
loading: vi.fn(),
|
|
43
|
+
success: vi.fn(),
|
|
44
|
+
error: vi.fn(),
|
|
45
|
+
destroy: vi.fn(),
|
|
46
|
+
},
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
vi.mock('i18next', () => ({
|
|
50
|
+
t: vi.fn((key, params) => (params.title ? key + '_' + params.title : key)),
|
|
51
|
+
}));
|
|
52
|
+
|
|
37
53
|
beforeEach(() => {
|
|
38
54
|
// Setup initial state and mocks before each test
|
|
39
55
|
vi.clearAllMocks();
|
|
@@ -222,7 +238,7 @@ describe('topic action', () => {
|
|
|
222
238
|
expect(result.current.data).toEqual(topics);
|
|
223
239
|
});
|
|
224
240
|
expect(useChatStore.getState().topicsInit).toBeTruthy();
|
|
225
|
-
expect(useChatStore.getState().
|
|
241
|
+
expect(useChatStore.getState().topicMaps).toEqual({ [sessionId]: topics });
|
|
226
242
|
});
|
|
227
243
|
});
|
|
228
244
|
describe('useSearchTopics', () => {
|
|
@@ -411,7 +427,7 @@ describe('topic action', () => {
|
|
|
411
427
|
const topics = [{ id: 'topic-1', title: 'Test Topic' }] as ChatTopic[];
|
|
412
428
|
const { result } = renderHook(() => useChatStore());
|
|
413
429
|
await act(async () => {
|
|
414
|
-
useChatStore.setState({ topics });
|
|
430
|
+
useChatStore.setState({ topicMaps: { test: topics }, activeId: 'test' });
|
|
415
431
|
});
|
|
416
432
|
|
|
417
433
|
// Mock the `updateTopicTitleInSummary` and `refreshTopic` for spying
|
|
@@ -437,4 +453,82 @@ describe('topic action', () => {
|
|
|
437
453
|
// TODO: need to test with fetchPresetTaskResult
|
|
438
454
|
});
|
|
439
455
|
});
|
|
456
|
+
describe('createTopic', () => {
|
|
457
|
+
it('should create a new topic and update the store', async () => {
|
|
458
|
+
const { result } = renderHook(() => useChatStore());
|
|
459
|
+
const activeId = 'test-session-id';
|
|
460
|
+
const newTopicId = 'new-topic-id';
|
|
461
|
+
const messages = [{ id: 'message-1' }, { id: 'message-2' }] as ChatMessage[];
|
|
462
|
+
|
|
463
|
+
await act(async () => {
|
|
464
|
+
useChatStore.setState({
|
|
465
|
+
activeId,
|
|
466
|
+
messagesMap: {
|
|
467
|
+
[messageMapKey(activeId)]: messages,
|
|
468
|
+
},
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
const createTopicSpy = vi.spyOn(topicService, 'createTopic').mockResolvedValue(newTopicId);
|
|
473
|
+
const refreshTopicSpy = vi.spyOn(result.current, 'refreshTopic');
|
|
474
|
+
|
|
475
|
+
await act(async () => {
|
|
476
|
+
const topicId = await result.current.createTopic();
|
|
477
|
+
expect(topicId).toBe(newTopicId);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
expect(createTopicSpy).toHaveBeenCalledWith({
|
|
481
|
+
sessionId: activeId,
|
|
482
|
+
messages: messages.map((m) => m.id),
|
|
483
|
+
title: 'topic.defaultTitle',
|
|
484
|
+
});
|
|
485
|
+
expect(refreshTopicSpy).toHaveBeenCalled();
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
describe('duplicateTopic', () => {
|
|
489
|
+
it('should duplicate a topic and switch to the new topic', async () => {
|
|
490
|
+
const { result } = renderHook(() => useChatStore());
|
|
491
|
+
const topicId = 'topic-1';
|
|
492
|
+
const newTopicId = 'new-topic-id';
|
|
493
|
+
const topics = [{ id: topicId, title: 'Original Topic' }] as ChatTopic[];
|
|
494
|
+
|
|
495
|
+
await act(async () => {
|
|
496
|
+
useChatStore.setState({ activeId: 'abc', topicMaps: { abc: topics } });
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
const cloneTopicSpy = vi.spyOn(topicService, 'cloneTopic').mockResolvedValue(newTopicId);
|
|
500
|
+
const refreshTopicSpy = vi.spyOn(result.current, 'refreshTopic');
|
|
501
|
+
const switchTopicSpy = vi.spyOn(result.current, 'switchTopic');
|
|
502
|
+
|
|
503
|
+
await act(async () => {
|
|
504
|
+
await result.current.duplicateTopic(topicId);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
expect(cloneTopicSpy).toHaveBeenCalledWith(topicId, 'duplicateTitle_Original Topic');
|
|
508
|
+
expect(refreshTopicSpy).toHaveBeenCalled();
|
|
509
|
+
expect(switchTopicSpy).toHaveBeenCalledWith(newTopicId);
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
describe('autoRenameTopicTitle', () => {
|
|
513
|
+
it('should auto-rename the topic title based on the messages', async () => {
|
|
514
|
+
const { result } = renderHook(() => useChatStore());
|
|
515
|
+
const topicId = 'topic-1';
|
|
516
|
+
const activeId = 'test-session-id';
|
|
517
|
+
const messages = [{ id: 'message-1', content: 'Hello' }] as ChatMessage[];
|
|
518
|
+
|
|
519
|
+
await act(async () => {
|
|
520
|
+
useChatStore.setState({ activeId });
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
const getMessagesSpy = vi.spyOn(messageService, 'getMessages').mockResolvedValue(messages);
|
|
524
|
+
const summaryTopicTitleSpy = vi.spyOn(result.current, 'summaryTopicTitle');
|
|
525
|
+
|
|
526
|
+
await act(async () => {
|
|
527
|
+
await result.current.autoRenameTopicTitle(topicId);
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
expect(getMessagesSpy).toHaveBeenCalledWith(activeId, topicId);
|
|
531
|
+
expect(summaryTopicTitleSpy).toHaveBeenCalledWith(topicId, messages);
|
|
532
|
+
});
|
|
533
|
+
});
|
|
440
534
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/interface */
|
|
2
2
|
// Note: To make the code more logic and readable, we just disable the auto sort key eslint rule
|
|
3
3
|
// DON'T REMOVE THE FIRST LINE
|
|
4
|
+
import isEqual from 'fast-deep-equal';
|
|
4
5
|
import { t } from 'i18next';
|
|
5
6
|
import { produce } from 'immer';
|
|
6
7
|
import useSWR, { SWRResponse, mutate } from 'swr';
|
|
@@ -191,7 +192,16 @@ export const chatTopic: StateCreator<
|
|
|
191
192
|
suspense: true,
|
|
192
193
|
fallbackData: [],
|
|
193
194
|
onSuccess: (topics) => {
|
|
194
|
-
|
|
195
|
+
const nextMap = { ...get().topicMaps, [sessionId]: topics };
|
|
196
|
+
|
|
197
|
+
// no need to update map if the topics have been init and the map is the same
|
|
198
|
+
if (get().topicsInit && isEqual(nextMap, get().topicMaps)) return;
|
|
199
|
+
|
|
200
|
+
set(
|
|
201
|
+
{ topicMaps: nextMap, topicsInit: true },
|
|
202
|
+
false,
|
|
203
|
+
n('useFetchTopics(success)', { sessionId }),
|
|
204
|
+
);
|
|
195
205
|
},
|
|
196
206
|
},
|
|
197
207
|
),
|
|
@@ -6,6 +6,7 @@ export interface ChatTopicState {
|
|
|
6
6
|
isSearchingTopic: boolean;
|
|
7
7
|
searchTopics: ChatTopic[];
|
|
8
8
|
topicLoadingIds: string[];
|
|
9
|
+
topicMaps: Record<string, ChatTopic[]>;
|
|
9
10
|
topicRenamingId?: string;
|
|
10
11
|
topicSearchKeywords: string;
|
|
11
12
|
topics: ChatTopic[];
|
|
@@ -20,6 +21,7 @@ export const initialTopicState: ChatTopicState = {
|
|
|
20
21
|
isSearchingTopic: false,
|
|
21
22
|
searchTopics: [],
|
|
22
23
|
topicLoadingIds: [],
|
|
24
|
+
topicMaps: {},
|
|
23
25
|
topicSearchKeywords: '',
|
|
24
26
|
topics: [],
|
|
25
27
|
topicsInit: false,
|
|
@@ -8,23 +8,25 @@ import { topicSelectors } from '../../selectors';
|
|
|
8
8
|
|
|
9
9
|
const initialStore = initialState as ChatStore;
|
|
10
10
|
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
const topicMaps = {
|
|
12
|
+
test: [
|
|
13
|
+
{ id: 'topic1', name: 'Topic 1', favorite: true },
|
|
14
|
+
{ id: 'topic2', name: 'Topic 2' },
|
|
15
|
+
],
|
|
16
|
+
};
|
|
15
17
|
|
|
16
18
|
describe('topicSelectors', () => {
|
|
17
19
|
describe('currentTopics', () => {
|
|
18
|
-
it('should return
|
|
20
|
+
it('should return undefined if there are no topics with activeId', () => {
|
|
19
21
|
const topics = topicSelectors.currentTopics(initialStore);
|
|
20
|
-
expect(topics).
|
|
22
|
+
expect(topics).toBeUndefined();
|
|
21
23
|
});
|
|
22
24
|
|
|
23
25
|
it('should return all current topics from the store', () => {
|
|
24
|
-
const state = merge(initialStore, {
|
|
26
|
+
const state = merge(initialStore, { topicMaps, activeId: 'test' });
|
|
25
27
|
|
|
26
28
|
const topics = topicSelectors.currentTopics(state);
|
|
27
|
-
expect(topics).toEqual(
|
|
29
|
+
expect(topics).toEqual(topicMaps.test);
|
|
28
30
|
});
|
|
29
31
|
});
|
|
30
32
|
|
|
@@ -35,9 +37,59 @@ describe('topicSelectors', () => {
|
|
|
35
37
|
});
|
|
36
38
|
|
|
37
39
|
it('should return the number of current topics', () => {
|
|
38
|
-
const state = merge(initialStore, {
|
|
40
|
+
const state = merge(initialStore, { topicMaps, activeId: 'test' });
|
|
39
41
|
const length = topicSelectors.currentTopicLength(state);
|
|
40
|
-
expect(length).toBe(
|
|
42
|
+
expect(length).toBe(topicMaps.test.length);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('currentActiveTopic', () => {
|
|
47
|
+
it('should return undefined if there is no active topic', () => {
|
|
48
|
+
const topic = topicSelectors.currentActiveTopic(initialStore);
|
|
49
|
+
expect(topic).toBeUndefined();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should return the current active topic', () => {
|
|
53
|
+
const state = merge(initialStore, { topicMaps, activeId: 'test', activeTopicId: 'topic1' });
|
|
54
|
+
const topic = topicSelectors.currentActiveTopic(state);
|
|
55
|
+
expect(topic).toEqual(topicMaps.test[0]);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('currentUnFavTopics', () => {
|
|
60
|
+
it('should return all unfavorited topics', () => {
|
|
61
|
+
const state = merge(initialStore, { topics: topicMaps.test });
|
|
62
|
+
const topics = topicSelectors.currentUnFavTopics(state);
|
|
63
|
+
expect(topics).toEqual([topicMaps.test[1]]);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('displayTopics', () => {
|
|
68
|
+
it('should return current topics if not searching', () => {
|
|
69
|
+
const state = merge(initialStore, { topicMaps, activeId: 'test' });
|
|
70
|
+
const topics = topicSelectors.displayTopics(state);
|
|
71
|
+
expect(topics).toEqual(topicMaps.test);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should return search topics if searching', () => {
|
|
75
|
+
const searchTopics = [{ id: 'search1', name: 'Search 1' }];
|
|
76
|
+
const state = merge(initialStore, { isSearchingTopic: true, searchTopics });
|
|
77
|
+
const topics = topicSelectors.displayTopics(state);
|
|
78
|
+
expect(topics).toEqual(searchTopics);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('getTopicById', () => {
|
|
83
|
+
it('should return undefined if topic is not found', () => {
|
|
84
|
+
const state = merge(initialStore, { topicMaps, activeId: 'test' });
|
|
85
|
+
const topic = topicSelectors.getTopicById('notfound')(state);
|
|
86
|
+
expect(topic).toBeUndefined();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should return the topic with the given id', () => {
|
|
90
|
+
const state = merge(initialStore, { topicMaps, activeId: 'test' });
|
|
91
|
+
const topic = topicSelectors.getTopicById('topic1')(state);
|
|
92
|
+
expect(topic).toEqual(topicMaps.test[0]);
|
|
41
93
|
});
|
|
42
94
|
});
|
|
43
95
|
});
|
|
@@ -2,24 +2,24 @@ import { ChatTopic } from '@/types/topic';
|
|
|
2
2
|
|
|
3
3
|
import { ChatStore } from '../../store';
|
|
4
4
|
|
|
5
|
-
const currentTopics = (s: ChatStore): ChatTopic[] => s.
|
|
5
|
+
const currentTopics = (s: ChatStore): ChatTopic[] | undefined => s.topicMaps[s.activeId];
|
|
6
6
|
|
|
7
7
|
const currentActiveTopic = (s: ChatStore): ChatTopic | undefined => {
|
|
8
|
-
return s
|
|
8
|
+
return currentTopics(s)?.find((topic) => topic.id === s.activeTopicId);
|
|
9
9
|
};
|
|
10
10
|
const searchTopics = (s: ChatStore): ChatTopic[] => s.searchTopics;
|
|
11
11
|
|
|
12
|
-
const displayTopics = (s: ChatStore): ChatTopic[] =>
|
|
12
|
+
const displayTopics = (s: ChatStore): ChatTopic[] | undefined =>
|
|
13
13
|
s.isSearchingTopic ? searchTopics(s) : currentTopics(s);
|
|
14
14
|
|
|
15
15
|
const currentUnFavTopics = (s: ChatStore): ChatTopic[] => s.topics.filter((s) => !s.favorite);
|
|
16
16
|
|
|
17
|
-
const currentTopicLength = (s: ChatStore): number => currentTopics(s)
|
|
17
|
+
const currentTopicLength = (s: ChatStore): number => currentTopics(s)?.length || 0;
|
|
18
18
|
|
|
19
19
|
const getTopicById =
|
|
20
20
|
(id: string) =>
|
|
21
21
|
(s: ChatStore): ChatTopic | undefined =>
|
|
22
|
-
currentTopics(s)
|
|
22
|
+
currentTopics(s)?.find((topic) => topic.id === id);
|
|
23
23
|
|
|
24
24
|
export const topicSelectors = {
|
|
25
25
|
currentActiveTopic,
|