@tuturuuu/ui 0.4.0 → 0.5.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/CHANGELOG.md +21 -0
- package/package.json +6 -6
- package/src/components/ui/chat/chat-agent-details-operations-panel.test.tsx +62 -0
- package/src/components/ui/chat/chat-agent-details-zalo-personal-panel.tsx +8 -2
- package/src/components/ui/chat/chat-sidebar-conversation-groups.tsx +332 -0
- package/src/components/ui/chat/chat-sidebar-groups.test.ts +44 -0
- package/src/components/ui/chat/chat-sidebar-panel.test.tsx +2 -0
- package/src/components/ui/chat/chat-sidebar-panel.tsx +6 -0
- package/src/components/ui/chat/chat-sidebar-sections.ts +199 -0
- package/src/components/ui/chat/chat-sidebar.tsx +11 -258
- package/src/components/ui/chat/chat-workspace.tsx +5 -0
- package/src/components/ui/chat/utils.ts +7 -0
- package/src/components/ui/custom/settings/task-settings.tsx +76 -0
- package/src/components/ui/custom/settings/task-sound-settings.test.tsx +126 -0
- package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +29 -18
- package/src/components/ui/finance/transactions/transaction-card.tsx +75 -43
- package/src/components/ui/finance/transactions/transfer-merge.test.ts +90 -0
- package/src/components/ui/finance/transactions/transfer-merge.ts +52 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-adjustment-dialog.tsx +172 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-amount.tsx +32 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-delete-dialog.tsx +50 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-dialog.tsx +138 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-panel.tsx +196 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-sections.tsx +201 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoints.test.tsx +277 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-total-check-dialog.tsx +189 -0
- package/src/components/ui/finance/wallets/query-invalidation.ts +2 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +7 -3
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +10 -0
- package/src/components/ui/finance/wallets/wallets-page.test.tsx +7 -0
- package/src/components/ui/finance/wallets/wallets-page.tsx +21 -5
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-move.test.tsx +14 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operations.ts +29 -0
- package/src/components/ui/tu-do/my-tasks/__tests__/use-task-context-actions.test.ts +11 -0
- package/src/components/ui/tu-do/my-tasks/use-task-context-actions.ts +10 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.test.tsx +141 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +208 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/quick-settings-popover.tsx +26 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-list-selector.tsx +36 -20
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.test.tsx +24 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.tsx +18 -3
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.test.ts +84 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +5 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.tsx +300 -172
- package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +411 -323
- package/src/components/ui/tu-do/shared/task-sound-effects.test.ts +189 -0
- package/src/components/ui/tu-do/shared/task-sound-effects.tsx +468 -0
- package/src/hooks/__tests__/use-task-actions.test.tsx +61 -0
- package/src/hooks/use-task-actions.ts +45 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type { ChatConversation } from '@tuturuuu/internal-api';
|
|
2
|
+
import type { ChatConversationScope } from './utils';
|
|
3
|
+
|
|
4
|
+
export type ChatConversationSectionLabels = {
|
|
5
|
+
ai: string;
|
|
6
|
+
channel: string;
|
|
7
|
+
direct: string;
|
|
8
|
+
group: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type ChatConversationSourceLabels = {
|
|
12
|
+
external: string;
|
|
13
|
+
zaloPersonal: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export interface ChatConversationSourceGroup {
|
|
17
|
+
conversations: ChatConversation[];
|
|
18
|
+
id: string;
|
|
19
|
+
label: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ChatConversationSection {
|
|
23
|
+
conversations: ChatConversation[];
|
|
24
|
+
label: string | null;
|
|
25
|
+
sectionType: ChatConversation['type'];
|
|
26
|
+
sourceGroups: ChatConversationSourceGroup[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const DEFAULT_SOURCE_LABELS = {
|
|
30
|
+
external: 'External source',
|
|
31
|
+
zaloPersonal: 'Zalo Personal',
|
|
32
|
+
} as const satisfies ChatConversationSourceLabels;
|
|
33
|
+
|
|
34
|
+
export function getChatConversationSections({
|
|
35
|
+
conversations,
|
|
36
|
+
labels,
|
|
37
|
+
scope,
|
|
38
|
+
sourceLabels = DEFAULT_SOURCE_LABELS,
|
|
39
|
+
}: {
|
|
40
|
+
conversations: ChatConversation[];
|
|
41
|
+
labels: ChatConversationSectionLabels;
|
|
42
|
+
scope?: ChatConversationScope;
|
|
43
|
+
sourceLabels?: ChatConversationSourceLabels;
|
|
44
|
+
}): ChatConversationSection[] {
|
|
45
|
+
if (scope === 'workspaces') {
|
|
46
|
+
return [
|
|
47
|
+
createChatConversationSection({
|
|
48
|
+
conversations: conversations.filter(
|
|
49
|
+
(conversation) => conversation.type === 'channel'
|
|
50
|
+
),
|
|
51
|
+
label: labels.channel,
|
|
52
|
+
sectionType: 'channel',
|
|
53
|
+
sourceLabels,
|
|
54
|
+
}),
|
|
55
|
+
createChatConversationSection({
|
|
56
|
+
conversations: conversations.filter(
|
|
57
|
+
(conversation) => conversation.type === 'ai'
|
|
58
|
+
),
|
|
59
|
+
label: labels.ai,
|
|
60
|
+
sectionType: 'ai',
|
|
61
|
+
sourceLabels,
|
|
62
|
+
}),
|
|
63
|
+
];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (scope === 'personal') {
|
|
67
|
+
return [
|
|
68
|
+
createChatConversationSection({
|
|
69
|
+
conversations: conversations.filter(
|
|
70
|
+
(conversation) => conversation.type === 'direct'
|
|
71
|
+
),
|
|
72
|
+
label: labels.direct,
|
|
73
|
+
sectionType: 'direct',
|
|
74
|
+
sourceLabels,
|
|
75
|
+
}),
|
|
76
|
+
createChatConversationSection({
|
|
77
|
+
conversations: conversations.filter(
|
|
78
|
+
(conversation) => conversation.type === 'group'
|
|
79
|
+
),
|
|
80
|
+
label: labels.group,
|
|
81
|
+
sectionType: 'group',
|
|
82
|
+
sourceLabels,
|
|
83
|
+
}),
|
|
84
|
+
createChatConversationSection({
|
|
85
|
+
conversations: conversations.filter(
|
|
86
|
+
(conversation) => conversation.type === 'channel'
|
|
87
|
+
),
|
|
88
|
+
label: labels.channel,
|
|
89
|
+
sectionType: 'channel',
|
|
90
|
+
sourceLabels,
|
|
91
|
+
}),
|
|
92
|
+
createChatConversationSection({
|
|
93
|
+
conversations: conversations.filter(
|
|
94
|
+
(conversation) => conversation.type === 'ai'
|
|
95
|
+
),
|
|
96
|
+
label: labels.ai,
|
|
97
|
+
sectionType: 'ai',
|
|
98
|
+
sourceLabels,
|
|
99
|
+
}),
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return [
|
|
104
|
+
createChatConversationSection({
|
|
105
|
+
conversations,
|
|
106
|
+
label: null,
|
|
107
|
+
sectionType: 'direct',
|
|
108
|
+
sourceLabels,
|
|
109
|
+
}),
|
|
110
|
+
];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function getChatConversationSourceGroups({
|
|
114
|
+
conversations,
|
|
115
|
+
labels = DEFAULT_SOURCE_LABELS,
|
|
116
|
+
}: {
|
|
117
|
+
conversations: ChatConversation[];
|
|
118
|
+
labels?: ChatConversationSourceLabels;
|
|
119
|
+
}) {
|
|
120
|
+
const groups = new Map<string, ChatConversationSourceGroup>();
|
|
121
|
+
|
|
122
|
+
for (const conversation of conversations) {
|
|
123
|
+
const sourceGroup = getChatConversationSourceGroup(conversation, labels);
|
|
124
|
+
|
|
125
|
+
if (!sourceGroup) continue;
|
|
126
|
+
|
|
127
|
+
const group = groups.get(sourceGroup.id);
|
|
128
|
+
|
|
129
|
+
if (group) {
|
|
130
|
+
group.conversations.push(conversation);
|
|
131
|
+
} else {
|
|
132
|
+
groups.set(sourceGroup.id, {
|
|
133
|
+
...sourceGroup,
|
|
134
|
+
conversations: [conversation],
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return [...groups.values()];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function createChatConversationSection({
|
|
143
|
+
conversations,
|
|
144
|
+
label,
|
|
145
|
+
sectionType,
|
|
146
|
+
sourceLabels,
|
|
147
|
+
}: {
|
|
148
|
+
conversations: ChatConversation[];
|
|
149
|
+
label: string | null;
|
|
150
|
+
sectionType: ChatConversation['type'];
|
|
151
|
+
sourceLabels: ChatConversationSourceLabels;
|
|
152
|
+
}): ChatConversationSection {
|
|
153
|
+
const sourceGroups = getChatConversationSourceGroups({
|
|
154
|
+
conversations,
|
|
155
|
+
labels: sourceLabels,
|
|
156
|
+
});
|
|
157
|
+
const sourceConversationIds = new Set(
|
|
158
|
+
sourceGroups.flatMap((group) =>
|
|
159
|
+
group.conversations.map((conversation) => conversation.id)
|
|
160
|
+
)
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
conversations: conversations.filter(
|
|
165
|
+
(conversation) => !sourceConversationIds.has(conversation.id)
|
|
166
|
+
),
|
|
167
|
+
label,
|
|
168
|
+
sectionType,
|
|
169
|
+
sourceGroups,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function getChatConversationSourceGroup(
|
|
174
|
+
conversation: ChatConversation,
|
|
175
|
+
labels: ChatConversationSourceLabels
|
|
176
|
+
): Omit<ChatConversationSourceGroup, 'conversations'> | null {
|
|
177
|
+
const metadata = conversation.metadata ?? {};
|
|
178
|
+
|
|
179
|
+
if (metadata.source !== 'ai-agent-external-thread') return null;
|
|
180
|
+
|
|
181
|
+
const adapter = readString(metadata.adapter);
|
|
182
|
+
|
|
183
|
+
if (adapter !== 'zalo') return null;
|
|
184
|
+
|
|
185
|
+
const agentId = readString(metadata.agentId) ?? 'unknown-agent';
|
|
186
|
+
const channelId =
|
|
187
|
+
readString(metadata.channelId) ??
|
|
188
|
+
readString(metadata.externalChannelId) ??
|
|
189
|
+
'unknown-channel';
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
id: `external:${adapter}:${agentId}:${channelId}`,
|
|
193
|
+
label: labels.zaloPersonal,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function readString(value: unknown) {
|
|
198
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
199
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
4
3
|
import {
|
|
5
4
|
Bot,
|
|
6
5
|
Funnel,
|
|
@@ -13,7 +12,7 @@ import {
|
|
|
13
12
|
import type { ChatConversation, ChatMessage } from '@tuturuuu/internal-api';
|
|
14
13
|
import { cn } from '@tuturuuu/utils/format';
|
|
15
14
|
import { useTranslations } from 'next-intl';
|
|
16
|
-
import { type ReactNode,
|
|
15
|
+
import { type ReactNode, useState } from 'react';
|
|
17
16
|
import { Button } from '../button';
|
|
18
17
|
import {
|
|
19
18
|
DropdownMenu,
|
|
@@ -24,12 +23,18 @@ import {
|
|
|
24
23
|
DropdownMenuTrigger,
|
|
25
24
|
} from '../dropdown-menu';
|
|
26
25
|
import { Input } from '../input';
|
|
27
|
-
import {
|
|
26
|
+
import { ConversationGroups } from './chat-sidebar-conversation-groups';
|
|
27
|
+
import { SearchResultList } from './chat-sidebar-items';
|
|
28
28
|
import type {
|
|
29
29
|
ChatConversationArchiveFilter,
|
|
30
30
|
ChatConversationScope,
|
|
31
31
|
} from './utils';
|
|
32
32
|
|
|
33
|
+
export {
|
|
34
|
+
getChatConversationSections,
|
|
35
|
+
getChatConversationSourceGroups,
|
|
36
|
+
} from './chat-sidebar-sections';
|
|
37
|
+
|
|
33
38
|
interface ChatSidebarProps {
|
|
34
39
|
actions?: ReactNode;
|
|
35
40
|
archiveFilter?: ChatConversationArchiveFilter;
|
|
@@ -51,6 +56,7 @@ interface ChatSidebarProps {
|
|
|
51
56
|
scope?: ChatConversationScope;
|
|
52
57
|
showControls?: boolean;
|
|
53
58
|
showTitle?: boolean;
|
|
59
|
+
sourceGroupStorageKey?: string | null;
|
|
54
60
|
searchValue: string;
|
|
55
61
|
selectedConversationId?: string | null;
|
|
56
62
|
}
|
|
@@ -76,6 +82,7 @@ export function ChatSidebar({
|
|
|
76
82
|
scope,
|
|
77
83
|
showControls = true,
|
|
78
84
|
showTitle = true,
|
|
85
|
+
sourceGroupStorageKey,
|
|
79
86
|
searchValue,
|
|
80
87
|
selectedConversationId,
|
|
81
88
|
}: ChatSidebarProps) {
|
|
@@ -154,6 +161,7 @@ export function ChatSidebar({
|
|
|
154
161
|
archiveFilter={archiveFilter}
|
|
155
162
|
scope={scope}
|
|
156
163
|
selectedConversationId={selectedConversationId}
|
|
164
|
+
sourceGroupStorageKey={sourceGroupStorageKey}
|
|
157
165
|
/>
|
|
158
166
|
) : (
|
|
159
167
|
<div className="p-6 text-center">
|
|
@@ -171,261 +179,6 @@ export function ChatSidebar({
|
|
|
171
179
|
);
|
|
172
180
|
}
|
|
173
181
|
|
|
174
|
-
type ConversationListItem =
|
|
175
|
-
| { key: string; label: string; type: 'archive-label' }
|
|
176
|
-
| {
|
|
177
|
-
key: string;
|
|
178
|
-
label: string;
|
|
179
|
-
sectionType: ChatConversation['type'];
|
|
180
|
-
type: 'group-label';
|
|
181
|
-
}
|
|
182
|
-
| { conversation: ChatConversation; key: string; type: 'conversation' }
|
|
183
|
-
| { key: string; type: 'loader' };
|
|
184
|
-
|
|
185
|
-
type ChatConversationSectionLabels = {
|
|
186
|
-
ai: string;
|
|
187
|
-
channel: string;
|
|
188
|
-
direct: string;
|
|
189
|
-
group: string;
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
export function getChatConversationSections({
|
|
193
|
-
conversations,
|
|
194
|
-
labels,
|
|
195
|
-
scope,
|
|
196
|
-
}: {
|
|
197
|
-
conversations: ChatConversation[];
|
|
198
|
-
labels: ChatConversationSectionLabels;
|
|
199
|
-
scope?: ChatConversationScope;
|
|
200
|
-
}) {
|
|
201
|
-
if (scope === 'workspaces') {
|
|
202
|
-
return [
|
|
203
|
-
{
|
|
204
|
-
conversations: conversations.filter(
|
|
205
|
-
(conversation) => conversation.type === 'channel'
|
|
206
|
-
),
|
|
207
|
-
label: labels.channel,
|
|
208
|
-
sectionType: 'channel' as const,
|
|
209
|
-
},
|
|
210
|
-
{
|
|
211
|
-
conversations: conversations.filter(
|
|
212
|
-
(conversation) => conversation.type === 'ai'
|
|
213
|
-
),
|
|
214
|
-
label: labels.ai,
|
|
215
|
-
sectionType: 'ai' as const,
|
|
216
|
-
},
|
|
217
|
-
];
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
if (scope === 'personal') {
|
|
221
|
-
return [
|
|
222
|
-
{
|
|
223
|
-
conversations: conversations.filter(
|
|
224
|
-
(conversation) => conversation.type === 'direct'
|
|
225
|
-
),
|
|
226
|
-
label: labels.direct,
|
|
227
|
-
sectionType: 'direct' as const,
|
|
228
|
-
},
|
|
229
|
-
{
|
|
230
|
-
conversations: conversations.filter(
|
|
231
|
-
(conversation) => conversation.type === 'group'
|
|
232
|
-
),
|
|
233
|
-
label: labels.group,
|
|
234
|
-
sectionType: 'group' as const,
|
|
235
|
-
},
|
|
236
|
-
{
|
|
237
|
-
conversations: conversations.filter(
|
|
238
|
-
(conversation) => conversation.type === 'channel'
|
|
239
|
-
),
|
|
240
|
-
label: labels.channel,
|
|
241
|
-
sectionType: 'channel' as const,
|
|
242
|
-
},
|
|
243
|
-
{
|
|
244
|
-
conversations: conversations.filter(
|
|
245
|
-
(conversation) => conversation.type === 'ai'
|
|
246
|
-
),
|
|
247
|
-
label: labels.ai,
|
|
248
|
-
sectionType: 'ai' as const,
|
|
249
|
-
},
|
|
250
|
-
];
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
return [
|
|
254
|
-
{
|
|
255
|
-
conversations,
|
|
256
|
-
label: null,
|
|
257
|
-
sectionType: 'direct' as const,
|
|
258
|
-
},
|
|
259
|
-
];
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function ConversationGroups({
|
|
263
|
-
archiveFilter,
|
|
264
|
-
conversations,
|
|
265
|
-
currentUserId,
|
|
266
|
-
hasMoreConversations,
|
|
267
|
-
isFetchingMoreConversations,
|
|
268
|
-
onArchiveConversation,
|
|
269
|
-
onLoadMoreConversations,
|
|
270
|
-
onPinConversation,
|
|
271
|
-
onSelectConversation,
|
|
272
|
-
scope,
|
|
273
|
-
selectedConversationId,
|
|
274
|
-
}: {
|
|
275
|
-
archiveFilter: ChatConversationArchiveFilter;
|
|
276
|
-
conversations: ChatConversation[];
|
|
277
|
-
currentUserId: string;
|
|
278
|
-
hasMoreConversations?: boolean;
|
|
279
|
-
isFetchingMoreConversations?: boolean;
|
|
280
|
-
onArchiveConversation?: (conversationId: string) => void;
|
|
281
|
-
onLoadMoreConversations?: () => Promise<unknown> | undefined;
|
|
282
|
-
onPinConversation?: (conversationId: string, pinned: boolean) => void;
|
|
283
|
-
onSelectConversation: (conversationId: string) => void;
|
|
284
|
-
scope?: ChatConversationScope;
|
|
285
|
-
selectedConversationId?: string | null;
|
|
286
|
-
}) {
|
|
287
|
-
const t = useTranslations('chat');
|
|
288
|
-
const parentRef = useRef<HTMLDivElement | null>(null);
|
|
289
|
-
const groups = useMemo(
|
|
290
|
-
() =>
|
|
291
|
-
getChatConversationSections({
|
|
292
|
-
conversations,
|
|
293
|
-
labels: {
|
|
294
|
-
ai: t('ai_agents'),
|
|
295
|
-
channel: t('channels'),
|
|
296
|
-
direct: t('direct_messages'),
|
|
297
|
-
group: t('groups'),
|
|
298
|
-
},
|
|
299
|
-
scope,
|
|
300
|
-
}),
|
|
301
|
-
[conversations, scope, t]
|
|
302
|
-
);
|
|
303
|
-
const items = useMemo<ConversationListItem[]>(() => {
|
|
304
|
-
const next: ConversationListItem[] = [];
|
|
305
|
-
if (archiveFilter !== 'active') {
|
|
306
|
-
next.push({
|
|
307
|
-
key: 'archive-label',
|
|
308
|
-
label:
|
|
309
|
-
archiveFilter === 'archived'
|
|
310
|
-
? t('showing_archived_chats')
|
|
311
|
-
: t('showing_all_chats'),
|
|
312
|
-
type: 'archive-label',
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
for (const [index, group] of groups.entries()) {
|
|
317
|
-
if (group.conversations.length === 0) continue;
|
|
318
|
-
if (group.label) {
|
|
319
|
-
next.push({
|
|
320
|
-
key: `group-${group.label}-${index}`,
|
|
321
|
-
label: group.label,
|
|
322
|
-
sectionType: group.sectionType,
|
|
323
|
-
type: 'group-label',
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
for (const conversation of group.conversations) {
|
|
328
|
-
next.push({
|
|
329
|
-
conversation,
|
|
330
|
-
key: conversation.id,
|
|
331
|
-
type: 'conversation',
|
|
332
|
-
});
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
if (hasMoreConversations) next.push({ key: 'loader', type: 'loader' });
|
|
337
|
-
return next;
|
|
338
|
-
}, [archiveFilter, groups, hasMoreConversations, t]);
|
|
339
|
-
|
|
340
|
-
const virtualizer = useVirtualizer({
|
|
341
|
-
count: items.length,
|
|
342
|
-
estimateSize: (index) => {
|
|
343
|
-
const item = items[index];
|
|
344
|
-
if (item?.type === 'conversation') return 36;
|
|
345
|
-
if (item?.type === 'loader') return 44;
|
|
346
|
-
return 30;
|
|
347
|
-
},
|
|
348
|
-
getItemKey: (index) => items[index]?.key ?? index,
|
|
349
|
-
getScrollElement: () => parentRef.current,
|
|
350
|
-
overscan: 8,
|
|
351
|
-
});
|
|
352
|
-
const virtualItems = virtualizer.getVirtualItems();
|
|
353
|
-
|
|
354
|
-
function maybeLoadMore(event: UIEvent<HTMLDivElement>) {
|
|
355
|
-
if (!(hasMoreConversations && onLoadMoreConversations)) return;
|
|
356
|
-
if (isFetchingMoreConversations) return;
|
|
357
|
-
|
|
358
|
-
const target = event.currentTarget;
|
|
359
|
-
const distanceToEnd =
|
|
360
|
-
target.scrollHeight - target.scrollTop - target.clientHeight;
|
|
361
|
-
if (distanceToEnd < 180) {
|
|
362
|
-
void onLoadMoreConversations();
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
return (
|
|
367
|
-
<div
|
|
368
|
-
className="h-full overflow-y-auto overflow-x-hidden overscroll-contain p-2"
|
|
369
|
-
onScroll={maybeLoadMore}
|
|
370
|
-
ref={parentRef}
|
|
371
|
-
>
|
|
372
|
-
<div
|
|
373
|
-
className="relative"
|
|
374
|
-
style={{ height: `${virtualizer.getTotalSize()}px` }}
|
|
375
|
-
>
|
|
376
|
-
{virtualItems.map((virtualItem) => {
|
|
377
|
-
const item = items[virtualItem.index];
|
|
378
|
-
if (!item) return null;
|
|
379
|
-
|
|
380
|
-
return (
|
|
381
|
-
<div
|
|
382
|
-
className="absolute inset-x-0 top-0"
|
|
383
|
-
data-index={virtualItem.index}
|
|
384
|
-
key={virtualItem.key}
|
|
385
|
-
ref={virtualizer.measureElement}
|
|
386
|
-
style={{ transform: `translateY(${virtualItem.start}px)` }}
|
|
387
|
-
>
|
|
388
|
-
{item.type === 'archive-label' ? (
|
|
389
|
-
<p className="px-2 py-1 text-muted-foreground text-xs">
|
|
390
|
-
{item.label}
|
|
391
|
-
</p>
|
|
392
|
-
) : item.type === 'group-label' ? (
|
|
393
|
-
<h3 className="flex items-center gap-1.5 px-2 py-1.5 font-medium text-muted-foreground text-xs uppercase">
|
|
394
|
-
<ConversationSectionIcon type={item.sectionType} />
|
|
395
|
-
{item.label}
|
|
396
|
-
</h3>
|
|
397
|
-
) : item.type === 'loader' ? (
|
|
398
|
-
<div className="flex items-center justify-center py-2 text-muted-foreground text-xs">
|
|
399
|
-
<LoaderCircle className="mr-2 size-3.5 animate-spin" />
|
|
400
|
-
{t('loading_conversations')}
|
|
401
|
-
</div>
|
|
402
|
-
) : (
|
|
403
|
-
<ConversationRow
|
|
404
|
-
conversation={item.conversation}
|
|
405
|
-
currentUserId={currentUserId}
|
|
406
|
-
isSelected={item.conversation.id === selectedConversationId}
|
|
407
|
-
onArchiveConversation={onArchiveConversation}
|
|
408
|
-
onPinConversation={onPinConversation}
|
|
409
|
-
onSelectConversation={onSelectConversation}
|
|
410
|
-
/>
|
|
411
|
-
)}
|
|
412
|
-
</div>
|
|
413
|
-
);
|
|
414
|
-
})}
|
|
415
|
-
</div>
|
|
416
|
-
</div>
|
|
417
|
-
);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
function ConversationSectionIcon({ type }: { type: ChatConversation['type'] }) {
|
|
421
|
-
const className = 'size-3.5 shrink-0';
|
|
422
|
-
|
|
423
|
-
if (type === 'channel') return <Hash className={className} />;
|
|
424
|
-
if (type === 'ai') return <Bot className={className} />;
|
|
425
|
-
if (type === 'group') return <Users className={className} />;
|
|
426
|
-
return <MessageCircle className={className} />;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
182
|
export function ChatConversationFilterMenu({
|
|
430
183
|
archiveFilter,
|
|
431
184
|
className,
|
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
filterChatConversations,
|
|
49
49
|
getChatConversationTypesForScope,
|
|
50
50
|
getChatSelectionStorageKey,
|
|
51
|
+
getChatSourceGroupStorageKey,
|
|
51
52
|
getConversationTitle,
|
|
52
53
|
isReadOnlyChatConversation,
|
|
53
54
|
normalizeChatConversationScope,
|
|
@@ -123,6 +124,9 @@ export function ChatWorkspace({
|
|
|
123
124
|
const selectionStorageKey = conversationScope
|
|
124
125
|
? getChatSelectionStorageKey(wsId, conversationScope)
|
|
125
126
|
: null;
|
|
127
|
+
const sourceGroupStorageKey = conversationScope
|
|
128
|
+
? getChatSourceGroupStorageKey(wsId, conversationScope)
|
|
129
|
+
: null;
|
|
126
130
|
const requestedConversationPending = Boolean(
|
|
127
131
|
requestedConversationId &&
|
|
128
132
|
!conversationIds.has(requestedConversationId) &&
|
|
@@ -491,6 +495,7 @@ export function ChatWorkspace({
|
|
|
491
495
|
searchResults={searchResults}
|
|
492
496
|
searchValue={searchValue}
|
|
493
497
|
selectedConversationId={activeConversationId}
|
|
498
|
+
sourceGroupStorageKey={sourceGroupStorageKey}
|
|
494
499
|
scope={conversationScope ?? undefined}
|
|
495
500
|
/>
|
|
496
501
|
) : null}
|
|
@@ -244,6 +244,13 @@ export function getChatSelectionStorageKey(
|
|
|
244
244
|
return `tuturuuu.chat.selectedConversation.${wsId}.${scope}`;
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
+
export function getChatSourceGroupStorageKey(
|
|
248
|
+
wsId: string,
|
|
249
|
+
scope: ChatConversationScope
|
|
250
|
+
) {
|
|
251
|
+
return `tuturuuu.chat.collapsedSourceGroups.${wsId}.${scope}`;
|
|
252
|
+
}
|
|
253
|
+
|
|
247
254
|
export function resolveChatConversationSelection({
|
|
248
255
|
conversationIds,
|
|
249
256
|
requestedConversationId,
|
|
@@ -21,6 +21,12 @@ import { Switch } from '@tuturuuu/ui/switch';
|
|
|
21
21
|
import { useTranslations } from 'next-intl';
|
|
22
22
|
import { useEffect } from 'react';
|
|
23
23
|
import { TASKS_SHOW_REVIEW_DUE_DATES_CONFIG_ID } from '../../tu-do/shared/task-due-date-visibility';
|
|
24
|
+
import {
|
|
25
|
+
clampTaskSoundEffectsVolume,
|
|
26
|
+
DEFAULT_TASK_SOUND_EFFECTS_VOLUME,
|
|
27
|
+
TASK_SOUND_EFFECTS_ENABLED_CONFIG_ID,
|
|
28
|
+
TASK_SOUND_EFFECTS_VOLUME_CONFIG_ID,
|
|
29
|
+
} from '../../tu-do/shared/task-sound-effects';
|
|
24
30
|
|
|
25
31
|
interface TaskSettingsData {
|
|
26
32
|
task_auto_assign_to_self: boolean;
|
|
@@ -74,6 +80,18 @@ export function TaskSettings({ workspace }: TaskSettingsProps) {
|
|
|
74
80
|
isLoading: showReviewDueDatesLoading,
|
|
75
81
|
isPending: showReviewDueDatesPending,
|
|
76
82
|
} = useUserBooleanConfig(TASKS_SHOW_REVIEW_DUE_DATES_CONFIG_ID, false);
|
|
83
|
+
const {
|
|
84
|
+
value: soundEffectsEnabled,
|
|
85
|
+
setValue: setSoundEffectsEnabled,
|
|
86
|
+
isLoading: soundEffectsEnabledLoading,
|
|
87
|
+
isPending: soundEffectsEnabledPending,
|
|
88
|
+
} = useUserBooleanConfig(TASK_SOUND_EFFECTS_ENABLED_CONFIG_ID, true);
|
|
89
|
+
const { data: soundEffectsVolume, isLoading: soundEffectsVolumeLoading } =
|
|
90
|
+
useUserConfig(
|
|
91
|
+
TASK_SOUND_EFFECTS_VOLUME_CONFIG_ID,
|
|
92
|
+
String(DEFAULT_TASK_SOUND_EFFECTS_VOLUME)
|
|
93
|
+
);
|
|
94
|
+
const updateSoundEffectsVolume = useUpdateUserConfig();
|
|
77
95
|
|
|
78
96
|
const { data: submitShortcut, isLoading: submitShortcutLoading } =
|
|
79
97
|
useUserConfig('TASK_SUBMIT_SHORTCUT', 'enter');
|
|
@@ -128,9 +146,19 @@ export function TaskSettings({ workspace }: TaskSettingsProps) {
|
|
|
128
146
|
updateSettings.mutate({ fade_completed_tasks: checked });
|
|
129
147
|
};
|
|
130
148
|
|
|
149
|
+
const handleSoundEffectsVolumeChange = (value: string) => {
|
|
150
|
+
updateSoundEffectsVolume.mutate({
|
|
151
|
+
configId: TASK_SOUND_EFFECTS_VOLUME_CONFIG_ID,
|
|
152
|
+
value: String(clampTaskSoundEffectsVolume(value)),
|
|
153
|
+
});
|
|
154
|
+
};
|
|
155
|
+
|
|
131
156
|
const effectiveAutoAssignValue = isPersonalWorkspace
|
|
132
157
|
? true
|
|
133
158
|
: (settings?.task_auto_assign_to_self ?? false);
|
|
159
|
+
const normalizedSoundEffectsVolume = String(
|
|
160
|
+
clampTaskSoundEffectsVolume(soundEffectsVolume)
|
|
161
|
+
);
|
|
134
162
|
|
|
135
163
|
return (
|
|
136
164
|
<div className="space-y-8">
|
|
@@ -163,6 +191,54 @@ export function TaskSettings({ workspace }: TaskSettingsProps) {
|
|
|
163
191
|
/>
|
|
164
192
|
</SettingItemTab>
|
|
165
193
|
<Separator />
|
|
194
|
+
<SettingItemTab
|
|
195
|
+
title={t('sound_effects')}
|
|
196
|
+
description={t('sound_effects_description')}
|
|
197
|
+
>
|
|
198
|
+
<Switch
|
|
199
|
+
aria-label={t('sound_effects')}
|
|
200
|
+
checked={soundEffectsEnabled}
|
|
201
|
+
onCheckedChange={setSoundEffectsEnabled}
|
|
202
|
+
disabled={soundEffectsEnabledLoading || soundEffectsEnabledPending}
|
|
203
|
+
/>
|
|
204
|
+
</SettingItemTab>
|
|
205
|
+
<Separator />
|
|
206
|
+
<SettingItemTab
|
|
207
|
+
title={t('sound_effects_volume')}
|
|
208
|
+
description={t('sound_effects_volume_description')}
|
|
209
|
+
>
|
|
210
|
+
<Select
|
|
211
|
+
value={normalizedSoundEffectsVolume}
|
|
212
|
+
onValueChange={handleSoundEffectsVolumeChange}
|
|
213
|
+
disabled={
|
|
214
|
+
soundEffectsVolumeLoading ||
|
|
215
|
+
updateSoundEffectsVolume.isPending ||
|
|
216
|
+
!soundEffectsEnabled
|
|
217
|
+
}
|
|
218
|
+
>
|
|
219
|
+
<SelectTrigger
|
|
220
|
+
aria-label={t('sound_effects_volume')}
|
|
221
|
+
className="w-36"
|
|
222
|
+
>
|
|
223
|
+
<SelectValue />
|
|
224
|
+
</SelectTrigger>
|
|
225
|
+
<SelectContent>
|
|
226
|
+
<SelectItem value="15">
|
|
227
|
+
{t('sound_effects_volume_soft')}
|
|
228
|
+
</SelectItem>
|
|
229
|
+
<SelectItem value="35">
|
|
230
|
+
{t('sound_effects_volume_balanced')}
|
|
231
|
+
</SelectItem>
|
|
232
|
+
<SelectItem value="60">
|
|
233
|
+
{t('sound_effects_volume_lively')}
|
|
234
|
+
</SelectItem>
|
|
235
|
+
<SelectItem value="85">
|
|
236
|
+
{t('sound_effects_volume_bold')}
|
|
237
|
+
</SelectItem>
|
|
238
|
+
</SelectContent>
|
|
239
|
+
</Select>
|
|
240
|
+
</SettingItemTab>
|
|
241
|
+
<Separator />
|
|
166
242
|
<SettingItemTab
|
|
167
243
|
title={t('draft_mode')}
|
|
168
244
|
description={t('draft_mode_description')}
|