nucleus-core-ts 0.9.914 → 0.9.916
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/.build-ok +1 -1
- package/dist/fe/components/ChatPanel/components/ChatPanel.js +4 -4
- package/dist/fe/components/ChatPanel/components/ConversationList.js +3 -3
- package/dist/fe/components/ChatPanel/components/MessageComposer.js +1 -1
- package/dist/fe/components/ChatPanel/components/MessageThread.js +18 -17
- package/dist/fe/components/ChatPanel/helpers.d.ts +23 -3
- package/dist/fe/components/ChatPanel/helpers.js +31 -13
- package/dist/fe/components/ChatPanel/hooks/useChat.js +16 -11
- package/dist/fe/components/ChatPanel/labels.d.ts +69 -0
- package/dist/fe/components/ChatPanel/labels.js +33 -1
- package/dist/fe/components/IntegrationsPage/components/MappingScheduleFields.js +54 -1
- package/dist/fe/components/IntegrationsPage/components/nextRuns.d.ts +29 -0
- package/dist/fe/components/IntegrationsPage/components/nextRuns.js +50 -0
- package/dist/fe/components/IntegrationsPage/text/index.d.ts +6 -0
- package/dist/fe/components/IntegrationsPage/text/index.js +5 -0
- package/dist/fe/components/NucleusTextInput/components/NucleusTextInput.d.ts +1 -1
- package/dist/fe/components/NucleusTextInput/components/NucleusTextInput.js +3 -2
- package/dist/fe/components/NucleusTextInput/components/PasswordStrengthIndicator.d.ts +19 -2
- package/dist/fe/components/NucleusTextInput/components/PasswordStrengthIndicator.js +16 -7
- package/dist/fe/components/NucleusTextInput/types/index.d.ts +8 -0
- package/dist/fe/components/ResetPasswordPage/components/ResetPasswordForm.js +5 -5
- package/dist/fe/components/ResetPasswordPage/components/ResetPasswordPage.js +3 -3
- package/dist/fe/components/ResetPasswordPage/labels.d.ts +10 -0
- package/dist/fe/components/ResetPasswordPage/labels.js +6 -0
- package/dist/fe/components/SetPasswordPage/components/PasswordStrengthIndicator.d.ts +4 -1
- package/dist/fe/components/SetPasswordPage/components/PasswordStrengthIndicator.js +13 -8
- package/dist/fe/components/SetPasswordPage/components/SetPasswordForm.js +10 -10
- package/dist/fe/components/SetPasswordPage/components/SetPasswordPage.js +4 -4
- package/dist/fe/components/SetPasswordPage/labels.d.ts +35 -0
- package/dist/fe/components/SetPasswordPage/labels.js +19 -1
- package/dist/index.js +1 -1
- package/dist/src/Shared/cron.d.ts +30 -0
- package/dist/src/Shared/cron.js +55 -0
- package/package.json +1 -1
package/dist/.build-ok
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.9.
|
|
1
|
+
0.9.916
|
|
@@ -46,11 +46,11 @@ export function ChatPanel(props) {
|
|
|
46
46
|
}).then((res)=>res.json()).then((data)=>{
|
|
47
47
|
if (data?.data) {
|
|
48
48
|
store.addMessage(data.data);
|
|
49
|
-
store.bumpConversationPreview(conversationId, data.data.content ??
|
|
49
|
+
store.bumpConversationPreview(conversationId, data.data.content ?? say.attachmentPreview, data.data.createdAt);
|
|
50
50
|
}
|
|
51
51
|
store.setSending(false);
|
|
52
52
|
}).catch(()=>{
|
|
53
|
-
store.setError(
|
|
53
|
+
store.setError(say.uploadFailed);
|
|
54
54
|
store.setSending(false);
|
|
55
55
|
});
|
|
56
56
|
};
|
|
@@ -65,7 +65,7 @@ export function ChatPanel(props) {
|
|
|
65
65
|
children: [
|
|
66
66
|
/*#__PURE__*/ _jsx("span", {
|
|
67
67
|
className: theme.sidebar.title,
|
|
68
|
-
children: props.title ??
|
|
68
|
+
children: props.title ?? say.panelTitle
|
|
69
69
|
}),
|
|
70
70
|
(props.directory || props.onNewConversation) && /*#__PURE__*/ _jsx("button", {
|
|
71
71
|
type: "button",
|
|
@@ -79,7 +79,7 @@ export function ChatPanel(props) {
|
|
|
79
79
|
stroke: "currentColor",
|
|
80
80
|
children: [
|
|
81
81
|
/*#__PURE__*/ _jsx("title", {
|
|
82
|
-
children:
|
|
82
|
+
children: say.newConversationIconTitle
|
|
83
83
|
}),
|
|
84
84
|
/*#__PURE__*/ _jsx("path", {
|
|
85
85
|
strokeLinecap: "round",
|
|
@@ -23,7 +23,7 @@ export function ConversationList({ conversations, activeId, currentUserId, isLoa
|
|
|
23
23
|
return /*#__PURE__*/ _jsx("div", {
|
|
24
24
|
className: theme.sidebar.list,
|
|
25
25
|
children: conversations.map((conversation)=>{
|
|
26
|
-
const title = conversationTitle(conversation, currentUserId, resolveUserName);
|
|
26
|
+
const title = conversationTitle(conversation, currentUserId, say, resolveUserName);
|
|
27
27
|
const unread = conversation.myParticipant?.unreadCount ?? conversation.unreadCount;
|
|
28
28
|
const otherId = otherParticipantId(conversation, currentUserId);
|
|
29
29
|
const avatarUrl = conversation.avatarUrl ?? (otherId ? resolveUserAvatar?.(otherId) : undefined);
|
|
@@ -52,7 +52,7 @@ export function ConversationList({ conversations, activeId, currentUserId, isLoa
|
|
|
52
52
|
}),
|
|
53
53
|
/*#__PURE__*/ _jsx("span", {
|
|
54
54
|
className: theme.conversation.time,
|
|
55
|
-
children: formatRelativeTime(conversation.lastMessageAt)
|
|
55
|
+
children: formatRelativeTime(conversation.lastMessageAt, say)
|
|
56
56
|
})
|
|
57
57
|
]
|
|
58
58
|
}),
|
|
@@ -61,7 +61,7 @@ export function ConversationList({ conversations, activeId, currentUserId, isLoa
|
|
|
61
61
|
children: [
|
|
62
62
|
/*#__PURE__*/ _jsx("span", {
|
|
63
63
|
className: theme.conversation.preview,
|
|
64
|
-
children: conversation.lastMessagePreview ||
|
|
64
|
+
children: conversation.lastMessagePreview || say.noMessagesPreview
|
|
65
65
|
}),
|
|
66
66
|
unread > 0 && /*#__PURE__*/ _jsx("span", {
|
|
67
67
|
className: theme.conversation.badge,
|
|
@@ -4,20 +4,20 @@ import { useEffect, useRef } from 'react';
|
|
|
4
4
|
import { cn } from '../../../utils/cn';
|
|
5
5
|
import { conversationTitle, formatClockTime } from '../helpers';
|
|
6
6
|
import { DEFAULT_CHAT_LABELS } from '../labels';
|
|
7
|
-
function Attachments({ message, theme }) {
|
|
7
|
+
function Attachments({ message, theme, say }) {
|
|
8
8
|
if (message.attachments.length === 0) return null;
|
|
9
9
|
return /*#__PURE__*/ _jsx("span", {
|
|
10
10
|
className: theme.attachment.grid,
|
|
11
11
|
children: message.attachments.map((attachment)=>attachment.kind === 'image' ? /*#__PURE__*/ _jsx("img", {
|
|
12
12
|
src: attachment.url,
|
|
13
|
-
alt: attachment.originalName ??
|
|
13
|
+
alt: attachment.originalName ?? say.imageAttachment,
|
|
14
14
|
className: theme.attachment.image
|
|
15
15
|
}, attachment.id) : /*#__PURE__*/ _jsx("a", {
|
|
16
16
|
href: attachment.url,
|
|
17
17
|
target: "_blank",
|
|
18
18
|
rel: "noreferrer",
|
|
19
19
|
className: theme.attachment.file,
|
|
20
|
-
children: attachment.originalName ??
|
|
20
|
+
children: attachment.originalName ?? say.downloadFile
|
|
21
21
|
}, attachment.id))
|
|
22
22
|
});
|
|
23
23
|
}
|
|
@@ -40,16 +40,19 @@ export function MessageThread({ conversation, messages, currentUserId, typing, h
|
|
|
40
40
|
children: say.selectConversation
|
|
41
41
|
});
|
|
42
42
|
}
|
|
43
|
-
const title = conversationTitle(conversation, currentUserId, resolveUserName);
|
|
43
|
+
const title = conversationTitle(conversation, currentUserId, say, resolveUserName);
|
|
44
44
|
const activeParticipants = conversation.participants.filter((p)=>!p.leftAt);
|
|
45
|
-
const subtitle = conversation.type === 'group' ?
|
|
45
|
+
const subtitle = conversation.type === 'group' ? say.participantCount(activeParticipants.length) : undefined;
|
|
46
46
|
const otherReaders = conversation.participants.filter((p)=>p.userId !== currentUserId && !p.leftAt);
|
|
47
47
|
const isReadByOthers = (message)=>{
|
|
48
48
|
const created = Date.parse(message.createdAt);
|
|
49
49
|
return otherReaders.some((p)=>p.lastReadAt != null && Date.parse(p.lastReadAt) >= created);
|
|
50
50
|
};
|
|
51
51
|
const typingUsers = typing.filter((t)=>t.conversationId === conversation.id && Date.now() - t.at < 6000);
|
|
52
|
-
|
|
52
|
+
// The whole sentence comes from one label, ellipsis included. Building it as
|
|
53
|
+
// `${name} is typing` + '...' would leave "is typing" untranslatable, and in
|
|
54
|
+
// Turkish the verb goes after the name anyway.
|
|
55
|
+
const typingLabel = typingUsers.length === 1 ? say.typingOne(resolveUserName?.(typingUsers[0]?.userId ?? '') ?? say.someone) : say.typingMany;
|
|
53
56
|
return /*#__PURE__*/ _jsxs("div", {
|
|
54
57
|
className: theme.thread.base,
|
|
55
58
|
children: [
|
|
@@ -76,7 +79,7 @@ export function MessageThread({ conversation, messages, currentUserId, typing, h
|
|
|
76
79
|
type: "button",
|
|
77
80
|
className: theme.thread.loadMore,
|
|
78
81
|
onClick: onLoadOlder,
|
|
79
|
-
children: isLoading ?
|
|
82
|
+
children: isLoading ? say.loadingMessages : say.loadEarlier
|
|
80
83
|
}),
|
|
81
84
|
messages.length === 0 && !isLoading ? /*#__PURE__*/ _jsx("div", {
|
|
82
85
|
className: theme.thread.empty,
|
|
@@ -90,7 +93,7 @@ export function MessageThread({ conversation, messages, currentUserId, typing, h
|
|
|
90
93
|
children: [
|
|
91
94
|
conversation.type === 'group' && !own && message.senderId && /*#__PURE__*/ _jsx("div", {
|
|
92
95
|
className: theme.message.sender,
|
|
93
|
-
children: resolveUserName?.(message.senderId) ??
|
|
96
|
+
children: resolveUserName?.(message.senderId) ?? say.unknownSender
|
|
94
97
|
}),
|
|
95
98
|
message.deletedAt ? /*#__PURE__*/ _jsx("span", {
|
|
96
99
|
className: theme.message.deleted,
|
|
@@ -102,7 +105,8 @@ export function MessageThread({ conversation, messages, currentUserId, typing, h
|
|
|
102
105
|
}),
|
|
103
106
|
/*#__PURE__*/ _jsx(Attachments, {
|
|
104
107
|
message: message,
|
|
105
|
-
theme: theme
|
|
108
|
+
theme: theme,
|
|
109
|
+
say: say
|
|
106
110
|
})
|
|
107
111
|
]
|
|
108
112
|
}),
|
|
@@ -110,15 +114,15 @@ export function MessageThread({ conversation, messages, currentUserId, typing, h
|
|
|
110
114
|
className: theme.message.meta,
|
|
111
115
|
children: [
|
|
112
116
|
/*#__PURE__*/ _jsx("span", {
|
|
113
|
-
children: formatClockTime(message.createdAt)
|
|
117
|
+
children: formatClockTime(message.createdAt, say)
|
|
114
118
|
}),
|
|
115
119
|
message.editedAt && /*#__PURE__*/ _jsx("span", {
|
|
116
120
|
className: theme.message.edited,
|
|
117
|
-
children:
|
|
121
|
+
children: say.edited
|
|
118
122
|
}),
|
|
119
123
|
own && !message.deletedAt && /*#__PURE__*/ _jsx("span", {
|
|
120
124
|
className: cn(theme.message.receipt, isReadByOthers(message) && theme.message.receiptRead),
|
|
121
|
-
title: isReadByOthers(message) ?
|
|
125
|
+
title: isReadByOthers(message) ? say.receiptRead : say.receiptSent,
|
|
122
126
|
children: isReadByOthers(message) ? '✓✓' : '✓'
|
|
123
127
|
})
|
|
124
128
|
]
|
|
@@ -129,12 +133,9 @@ export function MessageThread({ conversation, messages, currentUserId, typing, h
|
|
|
129
133
|
})
|
|
130
134
|
]
|
|
131
135
|
}),
|
|
132
|
-
typingUsers.length > 0 && /*#__PURE__*/
|
|
136
|
+
typingUsers.length > 0 && /*#__PURE__*/ _jsx("div", {
|
|
133
137
|
className: theme.typing,
|
|
134
|
-
children:
|
|
135
|
-
typingLabel,
|
|
136
|
-
"..."
|
|
137
|
-
]
|
|
138
|
+
children: typingLabel
|
|
138
139
|
})
|
|
139
140
|
]
|
|
140
141
|
});
|
|
@@ -1,6 +1,26 @@
|
|
|
1
|
+
import type { ChatPanelLabels } from './labels';
|
|
1
2
|
import type { ChatConversationDTO } from './types';
|
|
2
3
|
export declare function otherParticipantId(conversation: ChatConversationDTO, currentUserId: string): string | null;
|
|
3
|
-
export declare function conversationTitle(conversation: ChatConversationDTO, currentUserId: string, resolveUserName?: (userId: string) => string | undefined): string;
|
|
4
|
+
export declare function conversationTitle(conversation: ChatConversationDTO, currentUserId: string, labels: ChatPanelLabels, resolveUserName?: (userId: string) => string | undefined): string;
|
|
4
5
|
export declare function initialsOf(name: string): string;
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* The age of a message, in the panel's own words.
|
|
8
|
+
*
|
|
9
|
+
* The units used to be English letters written into the source (`5m`, `3h`,
|
|
10
|
+
* `2d`), which no installation could change, and the fallback past a week was a
|
|
11
|
+
* bare `toLocaleDateString()` — no locale, so the string came out in the
|
|
12
|
+
* SERVER's locale during the server render and the BROWSER's after hydration.
|
|
13
|
+
* The same row rendered twice, differently, and React tore. Both halves now
|
|
14
|
+
* come from the labels: the units as functions of the number, the date under
|
|
15
|
+
* the named `locale`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function formatRelativeTime(iso: string | null, labels: ChatPanelLabels): string;
|
|
18
|
+
/**
|
|
19
|
+
* The clock beneath a message.
|
|
20
|
+
*
|
|
21
|
+
* `hour12` is pinned as well as the locale: left to the environment, a locale
|
|
22
|
+
* that prints "2:32 PM" on one machine prints "14:32" on the other, which is
|
|
23
|
+
* the same hydration tear by a different route. A 24-hour clock is the one both
|
|
24
|
+
* ends can agree on without asking anybody.
|
|
25
|
+
*/
|
|
26
|
+
export declare function formatClockTime(iso: string, labels: ChatPanelLabels): string;
|
|
@@ -3,33 +3,51 @@ export function otherParticipantId(conversation, currentUserId) {
|
|
|
3
3
|
const other = conversation.participants.find((p)=>p.userId !== currentUserId);
|
|
4
4
|
return other?.userId ?? null;
|
|
5
5
|
}
|
|
6
|
-
export function conversationTitle(conversation, currentUserId, resolveUserName) {
|
|
7
|
-
if (conversation.type === 'group') return conversation.title ||
|
|
6
|
+
export function conversationTitle(conversation, currentUserId, labels, resolveUserName) {
|
|
7
|
+
if (conversation.type === 'group') return conversation.title || labels.groupChat;
|
|
8
8
|
const otherId = otherParticipantId(conversation, currentUserId);
|
|
9
|
-
if (otherId) return resolveUserName?.(otherId) ||
|
|
10
|
-
return conversation.title ||
|
|
9
|
+
if (otherId) return resolveUserName?.(otherId) || labels.directMessage;
|
|
10
|
+
return conversation.title || labels.directMessage;
|
|
11
11
|
}
|
|
12
12
|
export function initialsOf(name) {
|
|
13
13
|
const parts = name.trim().split(/\s+/).slice(0, 2);
|
|
14
14
|
const initials = parts.map((p)=>p[0]?.toUpperCase() ?? '').join('');
|
|
15
15
|
return initials || '?';
|
|
16
16
|
}
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* The age of a message, in the panel's own words.
|
|
19
|
+
*
|
|
20
|
+
* The units used to be English letters written into the source (`5m`, `3h`,
|
|
21
|
+
* `2d`), which no installation could change, and the fallback past a week was a
|
|
22
|
+
* bare `toLocaleDateString()` — no locale, so the string came out in the
|
|
23
|
+
* SERVER's locale during the server render and the BROWSER's after hydration.
|
|
24
|
+
* The same row rendered twice, differently, and React tore. Both halves now
|
|
25
|
+
* come from the labels: the units as functions of the number, the date under
|
|
26
|
+
* the named `locale`.
|
|
27
|
+
*/ export function formatRelativeTime(iso, labels) {
|
|
18
28
|
if (!iso) return '';
|
|
19
29
|
const date = new Date(iso);
|
|
20
30
|
const diffMs = Date.now() - date.getTime();
|
|
21
31
|
const mins = Math.floor(diffMs / 60000);
|
|
22
32
|
const hours = Math.floor(diffMs / 3600000);
|
|
23
33
|
const days = Math.floor(diffMs / 86400000);
|
|
24
|
-
if (mins < 1) return
|
|
25
|
-
if (mins < 60) return
|
|
26
|
-
if (hours < 24) return
|
|
27
|
-
if (days < 7) return
|
|
28
|
-
return date.toLocaleDateString();
|
|
34
|
+
if (mins < 1) return labels.justNow;
|
|
35
|
+
if (mins < 60) return labels.minutesAgo(mins);
|
|
36
|
+
if (hours < 24) return labels.hoursAgo(hours);
|
|
37
|
+
if (days < 7) return labels.daysAgo(days);
|
|
38
|
+
return date.toLocaleDateString(labels.locale);
|
|
29
39
|
}
|
|
30
|
-
|
|
31
|
-
|
|
40
|
+
/**
|
|
41
|
+
* The clock beneath a message.
|
|
42
|
+
*
|
|
43
|
+
* `hour12` is pinned as well as the locale: left to the environment, a locale
|
|
44
|
+
* that prints "2:32 PM" on one machine prints "14:32" on the other, which is
|
|
45
|
+
* the same hydration tear by a different route. A 24-hour clock is the one both
|
|
46
|
+
* ends can agree on without asking anybody.
|
|
47
|
+
*/ export function formatClockTime(iso, labels) {
|
|
48
|
+
return new Date(iso).toLocaleTimeString(labels.locale, {
|
|
32
49
|
hour: '2-digit',
|
|
33
|
-
minute: '2-digit'
|
|
50
|
+
minute: '2-digit',
|
|
51
|
+
hour12: false
|
|
34
52
|
});
|
|
35
53
|
}
|
|
@@ -1,23 +1,28 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { useEffect, useEffectEvent, useRef } from 'react';
|
|
3
3
|
import { usePubSub } from '../../../../src/Client/PubSub';
|
|
4
|
+
import { DEFAULT_CHAT_LABELS } from '../labels';
|
|
4
5
|
import { useChatStore } from '../store';
|
|
5
6
|
const TYPING_THROTTLE_MS = 2500;
|
|
6
|
-
function errMsg(error) {
|
|
7
|
-
return error instanceof Error ? error.message :
|
|
7
|
+
function errMsg(error, fallback) {
|
|
8
|
+
return error instanceof Error ? error.message : fallback;
|
|
8
9
|
}
|
|
9
|
-
function previewFor(message) {
|
|
10
|
+
function previewFor(message, say) {
|
|
10
11
|
if (message.content?.trim()) {
|
|
11
12
|
const trimmed = message.content.trim();
|
|
12
13
|
return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
|
|
13
14
|
}
|
|
14
15
|
if (message.attachments.length > 0) {
|
|
15
|
-
return message.attachments.length > 1 ?
|
|
16
|
+
return message.attachments.length > 1 ? say.attachmentsPreview(message.attachments.length) : say.attachmentPreview;
|
|
16
17
|
}
|
|
17
18
|
return '';
|
|
18
19
|
}
|
|
19
20
|
export function useChat(props) {
|
|
20
21
|
const { currentUserId, actions, topic = 'chat', wsUrl, wsPath } = props;
|
|
22
|
+
const say = {
|
|
23
|
+
...DEFAULT_CHAT_LABELS,
|
|
24
|
+
...props.labels
|
|
25
|
+
};
|
|
21
26
|
const store = useChatStore();
|
|
22
27
|
const lastSeenEventIdRef = useRef(null);
|
|
23
28
|
const eventsInitializedRef = useRef(false);
|
|
@@ -43,7 +48,7 @@ export function useChat(props) {
|
|
|
43
48
|
store.setLoadingConversations(false);
|
|
44
49
|
},
|
|
45
50
|
onErrorHandle: (error)=>{
|
|
46
|
-
store.setError(errMsg(error));
|
|
51
|
+
store.setError(errMsg(error, say.genericError));
|
|
47
52
|
store.setLoadingConversations(false);
|
|
48
53
|
}
|
|
49
54
|
});
|
|
@@ -89,7 +94,7 @@ export function useChat(props) {
|
|
|
89
94
|
markReadNow(conversationId);
|
|
90
95
|
},
|
|
91
96
|
onErrorHandle: (error)=>{
|
|
92
|
-
store.setError(errMsg(error));
|
|
97
|
+
store.setError(errMsg(error, say.genericError));
|
|
93
98
|
store.setLoadingMessages(false);
|
|
94
99
|
}
|
|
95
100
|
});
|
|
@@ -151,12 +156,12 @@ export function useChat(props) {
|
|
|
151
156
|
onAfterHandle: (data)=>{
|
|
152
157
|
if (data?.data) {
|
|
153
158
|
store.addMessage(data.data);
|
|
154
|
-
store.bumpConversationPreview(active, previewFor(data.data), data.data.createdAt);
|
|
159
|
+
store.bumpConversationPreview(active, previewFor(data.data, say), data.data.createdAt);
|
|
155
160
|
}
|
|
156
161
|
store.setSending(false);
|
|
157
162
|
},
|
|
158
163
|
onErrorHandle: (error)=>{
|
|
159
|
-
store.setError(errMsg(error));
|
|
164
|
+
store.setError(errMsg(error, say.genericError));
|
|
160
165
|
store.setSending(false);
|
|
161
166
|
}
|
|
162
167
|
});
|
|
@@ -188,7 +193,7 @@ export function useChat(props) {
|
|
|
188
193
|
openConversation(data.data.id);
|
|
189
194
|
}
|
|
190
195
|
},
|
|
191
|
-
onErrorHandle: (error)=>store.setError(errMsg(error))
|
|
196
|
+
onErrorHandle: (error)=>store.setError(errMsg(error, say.genericError))
|
|
192
197
|
});
|
|
193
198
|
});
|
|
194
199
|
const createGroup = useEffectEvent((title, participantIds)=>{
|
|
@@ -204,7 +209,7 @@ export function useChat(props) {
|
|
|
204
209
|
openConversation(data.data.id);
|
|
205
210
|
}
|
|
206
211
|
},
|
|
207
|
-
onErrorHandle: (error)=>store.setError(errMsg(error))
|
|
212
|
+
onErrorHandle: (error)=>store.setError(errMsg(error, say.genericError))
|
|
208
213
|
});
|
|
209
214
|
});
|
|
210
215
|
const handleRealtime = useEffectEvent((payload)=>{
|
|
@@ -215,7 +220,7 @@ export function useChat(props) {
|
|
|
215
220
|
const message = payload.message;
|
|
216
221
|
const conversationId = payload.conversationId;
|
|
217
222
|
if (!message || !conversationId) return;
|
|
218
|
-
store.bumpConversationPreview(conversationId, previewFor(message), message.createdAt);
|
|
223
|
+
store.bumpConversationPreview(conversationId, previewFor(message, say), message.createdAt);
|
|
219
224
|
store.clearTyping(conversationId, message.senderId ?? '');
|
|
220
225
|
if (conversationId === active) {
|
|
221
226
|
store.addMessage(message);
|
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
* each other.
|
|
10
10
|
*
|
|
11
11
|
* Defaults stay English, so nothing that does not pass labels changes.
|
|
12
|
+
*
|
|
13
|
+
* Anything that interpolates a value is a FUNCTION of that value, never a
|
|
14
|
+
* translated fragment glued to an untranslated one: "3 participants" is
|
|
15
|
+
* "3 katılımcı" in Turkish but "typing" moves to the end of its sentence, and
|
|
16
|
+
* a concatenation would produce a sentence in neither language.
|
|
12
17
|
*/
|
|
13
18
|
export type ChatPanelLabels = {
|
|
14
19
|
/** While the conversation list is still arriving. */
|
|
@@ -36,5 +41,69 @@ export type ChatPanelLabels = {
|
|
|
36
41
|
newMessageTitle: string;
|
|
37
42
|
searchPeople: string;
|
|
38
43
|
noPeopleFound: string;
|
|
44
|
+
/** The sidebar heading, when the consumer passes no `title` prop. */
|
|
45
|
+
panelTitle: string;
|
|
46
|
+
/** The accessible name of the plus icon inside the "new conversation" button. */
|
|
47
|
+
newConversationIconTitle: string;
|
|
48
|
+
/** The send button. */
|
|
49
|
+
send: string;
|
|
50
|
+
/** A conversation row whose last message does not exist yet. */
|
|
51
|
+
noMessagesPreview: string;
|
|
52
|
+
/** A group with no title of its own. */
|
|
53
|
+
groupChat: string;
|
|
54
|
+
/** A one-to-one conversation whose other person cannot be named. */
|
|
55
|
+
directMessage: string;
|
|
56
|
+
/** A sender whose name the host could not resolve. */
|
|
57
|
+
unknownSender: string;
|
|
58
|
+
/** The group thread's subtitle. */
|
|
59
|
+
participantCount: (count: number) => string;
|
|
60
|
+
/** The load-older button, while the page is in flight. */
|
|
61
|
+
loadingMessages: string;
|
|
62
|
+
/** The load-older button, at rest. */
|
|
63
|
+
loadEarlier: string;
|
|
64
|
+
/** One named person is typing. Carries its own trailing ellipsis. */
|
|
65
|
+
typingOne: (name: string) => string;
|
|
66
|
+
/** More than one person is typing. Carries its own trailing ellipsis. */
|
|
67
|
+
typingMany: string;
|
|
68
|
+
/** Stands in for a typist whose name the host could not resolve. */
|
|
69
|
+
someone: string;
|
|
70
|
+
/** Marks a message its sender changed after sending. */
|
|
71
|
+
edited: string;
|
|
72
|
+
/** The delivery tick's tooltip, once somebody else has read the message. */
|
|
73
|
+
receiptRead: string;
|
|
74
|
+
/** The delivery tick's tooltip, before anybody has read it. */
|
|
75
|
+
receiptSent: string;
|
|
76
|
+
/** Alt text for an image attachment that arrived without a file name. */
|
|
77
|
+
imageAttachment: string;
|
|
78
|
+
/** The link on a non-image attachment that arrived without a file name. */
|
|
79
|
+
downloadFile: string;
|
|
80
|
+
/** A conversation row previewing a message that is only a file. */
|
|
81
|
+
attachmentPreview: string;
|
|
82
|
+
/** A conversation row previewing a message that is only files. */
|
|
83
|
+
attachmentsPreview: (count: number) => string;
|
|
84
|
+
/** Shown when an attachment upload fails. */
|
|
85
|
+
uploadFailed: string;
|
|
86
|
+
/** Shown when a request fails without saying why. */
|
|
87
|
+
genericError: string;
|
|
88
|
+
/**
|
|
89
|
+
* The relative stamps on conversation rows. Kept short on purpose — they sit
|
|
90
|
+
* in a column beside a name that must not be pushed out of the row.
|
|
91
|
+
*/
|
|
92
|
+
justNow: string;
|
|
93
|
+
minutesAgo: (minutes: number) => string;
|
|
94
|
+
hoursAgo: (hours: number) => string;
|
|
95
|
+
daysAgo: (days: number) => string;
|
|
96
|
+
/**
|
|
97
|
+
* The BCP-47 tag the panel formats dates and clock times with.
|
|
98
|
+
*
|
|
99
|
+
* It is a label rather than a guess because the guess tore: calling
|
|
100
|
+
* `toLocaleDateString()` with no argument reads the SERVER's locale while the
|
|
101
|
+
* page renders on the server and the BROWSER's once React takes over, so one
|
|
102
|
+
* message produced two different strings and hydration mismatched. Naming the
|
|
103
|
+
* locale makes both passes agree. Clock times are additionally forced to
|
|
104
|
+
* `hour12: false` for the same reason — whether a locale prints "14:32" or
|
|
105
|
+
* "2:32 PM" must not depend on which machine did the printing.
|
|
106
|
+
*/
|
|
107
|
+
locale: string;
|
|
39
108
|
};
|
|
40
109
|
export declare const DEFAULT_CHAT_LABELS: ChatPanelLabels;
|
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
* each other.
|
|
10
10
|
*
|
|
11
11
|
* Defaults stay English, so nothing that does not pass labels changes.
|
|
12
|
+
*
|
|
13
|
+
* Anything that interpolates a value is a FUNCTION of that value, never a
|
|
14
|
+
* translated fragment glued to an untranslated one: "3 participants" is
|
|
15
|
+
* "3 katılımcı" in Turkish but "typing" moves to the end of its sentence, and
|
|
16
|
+
* a concatenation would produce a sentence in neither language.
|
|
12
17
|
*/ export const DEFAULT_CHAT_LABELS = {
|
|
13
18
|
loadingConversations: 'Loading conversations...',
|
|
14
19
|
noConversations: 'No conversations yet',
|
|
@@ -23,5 +28,32 @@
|
|
|
23
28
|
close: 'Close',
|
|
24
29
|
newMessageTitle: 'New message',
|
|
25
30
|
searchPeople: 'Search people...',
|
|
26
|
-
noPeopleFound: 'No people found'
|
|
31
|
+
noPeopleFound: 'No people found',
|
|
32
|
+
panelTitle: 'Messages',
|
|
33
|
+
newConversationIconTitle: 'New',
|
|
34
|
+
send: 'Send',
|
|
35
|
+
noMessagesPreview: 'No messages yet',
|
|
36
|
+
groupChat: 'Group chat',
|
|
37
|
+
directMessage: 'Direct message',
|
|
38
|
+
unknownSender: 'User',
|
|
39
|
+
participantCount: (count)=>`${count} participants`,
|
|
40
|
+
loadingMessages: 'Loading...',
|
|
41
|
+
loadEarlier: 'Load earlier messages',
|
|
42
|
+
typingOne: (name)=>`${name} is typing...`,
|
|
43
|
+
typingMany: 'Several people are typing...',
|
|
44
|
+
someone: 'Someone',
|
|
45
|
+
edited: '· edited',
|
|
46
|
+
receiptRead: 'Read',
|
|
47
|
+
receiptSent: 'Sent',
|
|
48
|
+
imageAttachment: 'Image',
|
|
49
|
+
downloadFile: 'Download file',
|
|
50
|
+
attachmentPreview: 'Attachment',
|
|
51
|
+
attachmentsPreview: (count)=>`${count} attachments`,
|
|
52
|
+
uploadFailed: 'Failed to upload attachment',
|
|
53
|
+
genericError: 'Something went wrong',
|
|
54
|
+
justNow: 'now',
|
|
55
|
+
minutesAgo: (minutes)=>`${minutes}m`,
|
|
56
|
+
hoursAgo: (hours)=>`${hours}h`,
|
|
57
|
+
daysAgo: (days)=>`${days}d`,
|
|
58
|
+
locale: 'en-US'
|
|
27
59
|
};
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
-
import { useId } from 'react';
|
|
3
|
+
import { useEffect, useId, useState } from 'react';
|
|
4
4
|
import { describeCron, parseCron } from '../../../../src/Shared/cron';
|
|
5
5
|
import { useLabels } from '../text/context';
|
|
6
6
|
import { integrationsPageTheme } from '../theme';
|
|
7
|
+
import { describeNextRuns } from './nextRuns';
|
|
7
8
|
import { Field, Notice, Section } from './Primitives';
|
|
8
9
|
/**
|
|
9
10
|
* Running this import without anybody clicking anything.
|
|
@@ -131,6 +132,10 @@ export function MappingScheduleFields({ enabled, cron, timezone, needsPassword,
|
|
|
131
132
|
children: preset.label
|
|
132
133
|
}, preset.cron))
|
|
133
134
|
}),
|
|
135
|
+
cron.trim() !== '' ? /*#__PURE__*/ _jsx(NextRunsList, {
|
|
136
|
+
cron: cron,
|
|
137
|
+
timezone: timezone
|
|
138
|
+
}) : null,
|
|
134
139
|
needsPassword ? /*#__PURE__*/ _jsx(Notice, {
|
|
135
140
|
title: t.cannotRunTitle,
|
|
136
141
|
tone: "warning",
|
|
@@ -147,3 +152,51 @@ export function MappingScheduleFields({ enabled, cron, timezone, needsPassword,
|
|
|
147
152
|
})
|
|
148
153
|
});
|
|
149
154
|
}
|
|
155
|
+
/** The next few firings, listed. Its own component so it can hold the clock. */ function NextRunsList({ cron, timezone }) {
|
|
156
|
+
const labels = useLabels();
|
|
157
|
+
/*
|
|
158
|
+
* The clock is read after mount, never during render.
|
|
159
|
+
*
|
|
160
|
+
* "From now" on the server and "from now" in the browser are different
|
|
161
|
+
* instants, so rendering this straight away would produce two different
|
|
162
|
+
* lists for one page and tear on hydration.
|
|
163
|
+
*/ const [from, setFrom] = useState(null);
|
|
164
|
+
useEffect(()=>setFrom(new Date()), [
|
|
165
|
+
cron,
|
|
166
|
+
timezone
|
|
167
|
+
]);
|
|
168
|
+
if (!from) return null;
|
|
169
|
+
const { runs, error } = describeNextRuns({
|
|
170
|
+
expression: cron,
|
|
171
|
+
timeZone: timezone,
|
|
172
|
+
from,
|
|
173
|
+
count: 5
|
|
174
|
+
});
|
|
175
|
+
if (error) {
|
|
176
|
+
return /*#__PURE__*/ _jsx(Notice, {
|
|
177
|
+
title: labels.schedulePreview.badTitle,
|
|
178
|
+
tone: "warning",
|
|
179
|
+
children: error
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
if (runs.length === 0) return /*#__PURE__*/ _jsx("p", {
|
|
183
|
+
className: theme.field.hint,
|
|
184
|
+
children: labels.schedulePreview.none
|
|
185
|
+
});
|
|
186
|
+
return /*#__PURE__*/ _jsxs("div", {
|
|
187
|
+
className: "flex flex-col gap-1",
|
|
188
|
+
children: [
|
|
189
|
+
/*#__PURE__*/ _jsx("p", {
|
|
190
|
+
className: theme.field.label,
|
|
191
|
+
children: labels.schedulePreview.title
|
|
192
|
+
}),
|
|
193
|
+
/*#__PURE__*/ _jsx("ul", {
|
|
194
|
+
className: "flex flex-col gap-0.5",
|
|
195
|
+
children: runs.map((run)=>/*#__PURE__*/ _jsx("li", {
|
|
196
|
+
className: theme.field.hint,
|
|
197
|
+
children: run.label
|
|
198
|
+
}, run.iso))
|
|
199
|
+
})
|
|
200
|
+
]
|
|
201
|
+
});
|
|
202
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The next few moments this schedule will actually fire, written out.
|
|
3
|
+
*
|
|
4
|
+
* A cron expression is a promise about a time nobody is awake for, and it is
|
|
5
|
+
* checked by reading it — which is exactly how "0 2 * * *" and "2 0 * * *"
|
|
6
|
+
* survive review. Worse, the hour is in a timezone that is written in another
|
|
7
|
+
* box: an expression that reads correctly can still fire at four in the
|
|
8
|
+
* afternoon because the zone beside it says UTC and the company is in Istanbul.
|
|
9
|
+
*
|
|
10
|
+
* None of that is visible until the night it does not happen, and by then the
|
|
11
|
+
* operator is asleep. Printing the real firings turns the expression back into
|
|
12
|
+
* something a person can compare against what they meant.
|
|
13
|
+
*
|
|
14
|
+
* The times come from the SAME function the scheduler uses, so this cannot
|
|
15
|
+
* flatter an expression the scheduler would read differently.
|
|
16
|
+
*/
|
|
17
|
+
export type NextRun = {
|
|
18
|
+
iso: string;
|
|
19
|
+
label: string;
|
|
20
|
+
};
|
|
21
|
+
export declare function describeNextRuns(input: {
|
|
22
|
+
expression: string;
|
|
23
|
+
timeZone?: string | null;
|
|
24
|
+
from: Date;
|
|
25
|
+
count?: number;
|
|
26
|
+
}): {
|
|
27
|
+
runs: NextRun[];
|
|
28
|
+
error?: string;
|
|
29
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { nextFireTimes } from '../../../../src/Shared/cron';
|
|
2
|
+
/**
|
|
3
|
+
* A fixed, unambiguous format rather than the reader's locale.
|
|
4
|
+
*
|
|
5
|
+
* `toLocaleString()` here would render the server's locale on the server and
|
|
6
|
+
* the browser's on the client — two different strings for one row, and a
|
|
7
|
+
* hydration tear. It would also print 8/5/2026, which in Turkey reads as the
|
|
8
|
+
* 8th of May. Year-first with the zone spelled out cannot be misread in either
|
|
9
|
+
* direction.
|
|
10
|
+
*/ function partsIn(when, timeZone) {
|
|
11
|
+
const parts = new Intl.DateTimeFormat('en-CA', {
|
|
12
|
+
timeZone,
|
|
13
|
+
year: 'numeric',
|
|
14
|
+
month: '2-digit',
|
|
15
|
+
day: '2-digit',
|
|
16
|
+
hour: '2-digit',
|
|
17
|
+
minute: '2-digit',
|
|
18
|
+
weekday: 'short',
|
|
19
|
+
hour12: false
|
|
20
|
+
}).formatToParts(when);
|
|
21
|
+
const out = {};
|
|
22
|
+
for (const part of parts)out[part.type] = part.value;
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
export function describeNextRuns(input) {
|
|
26
|
+
const zone = input.timeZone?.trim() || 'UTC';
|
|
27
|
+
const { times, error } = nextFireTimes({
|
|
28
|
+
expression: input.expression,
|
|
29
|
+
timeZone: zone,
|
|
30
|
+
from: input.from,
|
|
31
|
+
count: input.count ?? 5
|
|
32
|
+
});
|
|
33
|
+
if (error) return {
|
|
34
|
+
runs: [],
|
|
35
|
+
error
|
|
36
|
+
};
|
|
37
|
+
const runs = times.map((when)=>{
|
|
38
|
+
// `nextFireTimes` has already refused an unusable zone, so this cannot
|
|
39
|
+
// throw here — and if it ever did, printing confident times the scheduler
|
|
40
|
+
// will not honour would be worse than the crash.
|
|
41
|
+
const at = partsIn(when, zone);
|
|
42
|
+
return {
|
|
43
|
+
iso: when.toISOString(),
|
|
44
|
+
label: `${at.year}-${at.month}-${at.day} ${at.weekday} ${at.hour}:${at.minute} (${zone})`
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
return {
|
|
48
|
+
runs
|
|
49
|
+
};
|
|
50
|
+
}
|