@strapi-community/plugin-io 5.0.4 → 5.1.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/README.md +47 -1
- package/dist/_chunks/LivePresencePanel-BeNq_EnQ.mjs +387 -0
- package/dist/_chunks/LivePresencePanel-CNaEK-Gk.js +389 -0
- package/dist/_chunks/{MonitoringPage-DLZdTZpg.mjs → MonitoringPage-Bn9XJSlg.mjs} +1 -1
- package/dist/_chunks/{MonitoringPage-HxHK1nFr.js → MonitoringPage-K5Y3hhKF.js} +1 -1
- package/dist/_chunks/{SettingsPage-88RdJkLy.js → SettingsPage-4OkXJAjU.js} +250 -148
- package/dist/_chunks/{SettingsPage-DBIu309c.mjs → SettingsPage-DMbMGU6J.mjs} +250 -148
- package/dist/_chunks/{index-BVQ20t1c.js → index--2NeIKGR.js} +15 -5
- package/dist/_chunks/{index-DLXtrAtk.mjs → index-CzvX8YTe.mjs} +15 -5
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +1120 -41
- package/dist/server/index.mjs +1119 -40
- package/package.json +13 -5
package/README.md
CHANGED
|
@@ -39,6 +39,12 @@ Add real-time capabilities to your Strapi application with WebSocket support. Au
|
|
|
39
39
|
- **Role-Based Access Control** - Built-in permission checks for JWT and API tokens
|
|
40
40
|
- **Multi-Client Support** - Handle 2500+ concurrent connections efficiently
|
|
41
41
|
|
|
42
|
+
### Live Presence (NEW)
|
|
43
|
+
- **Real-Time Presence Awareness** - See who else is editing the same content
|
|
44
|
+
- **Typing Indicator** - See when someone is typing and in which field
|
|
45
|
+
- **Admin Panel Sidebar** - Live presence panel integrated into Content Manager
|
|
46
|
+
- **Session-Based Auth** - Secure admin authentication for Socket.IO connections
|
|
47
|
+
|
|
42
48
|
### Developer Experience
|
|
43
49
|
- **Visual Admin Panel** - Configure everything through the Strapi admin interface
|
|
44
50
|
- **TypeScript Support** - Full type definitions for IntelliSense
|
|
@@ -833,6 +839,39 @@ Navigate to **Settings > Socket.IO > Monitoring** for live statistics:
|
|
|
833
839
|
- Send test events
|
|
834
840
|
- Reset statistics
|
|
835
841
|
|
|
842
|
+
### Live Presence Panel
|
|
843
|
+
|
|
844
|
+
When editing content in the Content Manager, a **Live Presence** panel appears in the sidebar showing:
|
|
845
|
+
|
|
846
|
+
- **Connection Status** - Live indicator showing real-time sync is active
|
|
847
|
+
- **Active Editors** - List of other users editing the same content
|
|
848
|
+
- **Typing Indicator** - Shows when someone is typing and in which field
|
|
849
|
+
|
|
850
|
+
**How It Works:**
|
|
851
|
+
|
|
852
|
+
1. When you open a content entry, the panel connects via Socket.IO
|
|
853
|
+
2. Other editors on the same entry appear in the panel
|
|
854
|
+
3. Typing in any field broadcasts a typing indicator to others
|
|
855
|
+
4. When you leave, others are notified
|
|
856
|
+
|
|
857
|
+
**Example Display:**
|
|
858
|
+
|
|
859
|
+
```
|
|
860
|
+
+-----------------------------+
|
|
861
|
+
| Live Presence |
|
|
862
|
+
+-----------------------------+
|
|
863
|
+
| [*] Live |
|
|
864
|
+
| Real-time sync active |
|
|
865
|
+
+-----------------------------+
|
|
866
|
+
| ALSO EDITING (1) |
|
|
867
|
+
| +-------------------------+ |
|
|
868
|
+
| | SA Sarah Admin | |
|
|
869
|
+
| | Typing in: title | |
|
|
870
|
+
| | [Typing...] | |
|
|
871
|
+
| +-------------------------+ |
|
|
872
|
+
+-----------------------------+
|
|
873
|
+
```
|
|
874
|
+
|
|
836
875
|
---
|
|
837
876
|
|
|
838
877
|
## Monitoring Service
|
|
@@ -1282,7 +1321,14 @@ Copyright (c) 2024 Strapi Community
|
|
|
1282
1321
|
|
|
1283
1322
|
## Changelog
|
|
1284
1323
|
|
|
1285
|
-
### v5.
|
|
1324
|
+
### v5.1.0 (Latest)
|
|
1325
|
+
- **Live Presence System** - Real-time presence awareness in Content Manager
|
|
1326
|
+
- **Typing Indicator** - See when others are typing and in which field
|
|
1327
|
+
- **Admin Panel Sidebar** - Live presence panel integrated into edit view
|
|
1328
|
+
- **Admin Session Authentication** - Secure session tokens for Socket.IO
|
|
1329
|
+
- **Admin JWT Strategy** - New authentication strategy for admin users
|
|
1330
|
+
|
|
1331
|
+
### v5.0.0
|
|
1286
1332
|
- Strapi v5 support
|
|
1287
1333
|
- Package renamed to `@strapi-community/plugin-io`
|
|
1288
1334
|
- Enhanced TypeScript support
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useRef, useState, useEffect, useMemo, useCallback } from "react";
|
|
3
|
+
import { u as useIntl } from "./index-CEh8vkxY.mjs";
|
|
4
|
+
import { Flex } from "@strapi/design-system";
|
|
5
|
+
import { useFetchClient } from "@strapi/strapi/admin";
|
|
6
|
+
import styled, { css, keyframes } from "styled-components";
|
|
7
|
+
import { io } from "socket.io-client";
|
|
8
|
+
import { P as PLUGIN_ID } from "./index-CzvX8YTe.mjs";
|
|
9
|
+
const pulse = keyframes`
|
|
10
|
+
0%, 100% {
|
|
11
|
+
box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.2);
|
|
12
|
+
transform: scale(1);
|
|
13
|
+
}
|
|
14
|
+
50% {
|
|
15
|
+
box-shadow: 0 0 0 6px rgba(34, 197, 94, 0.1);
|
|
16
|
+
transform: scale(1.1);
|
|
17
|
+
}
|
|
18
|
+
`;
|
|
19
|
+
const StatusCard = styled.div`
|
|
20
|
+
background: ${(props) => props.theme.colors.neutral0};
|
|
21
|
+
border: 1px solid ${({ $status, theme }) => $status === "connected" ? "rgba(34, 197, 94, 0.3)" : $status === "error" ? "rgba(239, 68, 68, 0.3)" : theme.colors.neutral200};
|
|
22
|
+
border-radius: 10px;
|
|
23
|
+
padding: 14px 16px;
|
|
24
|
+
display: flex;
|
|
25
|
+
align-items: center;
|
|
26
|
+
gap: 12px;
|
|
27
|
+
`;
|
|
28
|
+
const StatusDot = styled.div`
|
|
29
|
+
width: 12px;
|
|
30
|
+
height: 12px;
|
|
31
|
+
border-radius: 50%;
|
|
32
|
+
flex-shrink: 0;
|
|
33
|
+
background: ${({ $status }) => $status === "connected" ? "#22c55e" : $status === "connecting" ? "#f59e0b" : $status === "error" ? "#ef4444" : "#94a3b8"};
|
|
34
|
+
|
|
35
|
+
${({ $status }) => $status === "connected" && css`
|
|
36
|
+
animation: ${pulse} 2s ease-in-out infinite;
|
|
37
|
+
`}
|
|
38
|
+
`;
|
|
39
|
+
const StatusText = styled.div`
|
|
40
|
+
display: flex;
|
|
41
|
+
flex-direction: column;
|
|
42
|
+
gap: 2px;
|
|
43
|
+
`;
|
|
44
|
+
const StatusLabel = styled.span`
|
|
45
|
+
font-size: 14px;
|
|
46
|
+
font-weight: 600;
|
|
47
|
+
color: ${({ $status, theme }) => $status === "connected" ? theme.colors.success600 : $status === "connecting" ? theme.colors.warning600 : $status === "error" ? theme.colors.danger600 : theme.colors.neutral600};
|
|
48
|
+
`;
|
|
49
|
+
const StatusSubtext = styled.span`
|
|
50
|
+
font-size: 12px;
|
|
51
|
+
color: ${(props) => props.theme.colors.neutral500};
|
|
52
|
+
`;
|
|
53
|
+
const SectionTitle = styled.div`
|
|
54
|
+
font-size: 11px;
|
|
55
|
+
font-weight: 600;
|
|
56
|
+
color: ${(props) => props.theme.colors.neutral600};
|
|
57
|
+
text-transform: uppercase;
|
|
58
|
+
letter-spacing: 0.5px;
|
|
59
|
+
margin-bottom: 10px;
|
|
60
|
+
`;
|
|
61
|
+
const EditorItem = styled.div`
|
|
62
|
+
display: flex;
|
|
63
|
+
align-items: center;
|
|
64
|
+
gap: 12px;
|
|
65
|
+
padding: 12px 14px;
|
|
66
|
+
background: ${(props) => props.theme.colors.neutral0};
|
|
67
|
+
border-radius: 10px;
|
|
68
|
+
border: 1px solid ${(props) => props.theme.colors.neutral150};
|
|
69
|
+
transition: all 0.2s ease;
|
|
70
|
+
|
|
71
|
+
&:hover {
|
|
72
|
+
border-color: ${(props) => props.theme.colors.primary200};
|
|
73
|
+
box-shadow: 0 2px 8px rgba(73, 69, 255, 0.08);
|
|
74
|
+
transform: translateY(-1px);
|
|
75
|
+
}
|
|
76
|
+
`;
|
|
77
|
+
const EDITOR_COLORS = [
|
|
78
|
+
"linear-gradient(135deg, #4945ff 0%, #7b79ff 100%)",
|
|
79
|
+
"linear-gradient(135deg, #3b82f6 0%, #60a5fa 100%)",
|
|
80
|
+
"linear-gradient(135deg, #10b981 0%, #34d399 100%)",
|
|
81
|
+
"linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%)",
|
|
82
|
+
"linear-gradient(135deg, #ef4444 0%, #f87171 100%)",
|
|
83
|
+
"linear-gradient(135deg, #ec4899 0%, #f472b6 100%)"
|
|
84
|
+
];
|
|
85
|
+
const EditorAvatar = styled.div`
|
|
86
|
+
width: 36px;
|
|
87
|
+
height: 36px;
|
|
88
|
+
border-radius: 50%;
|
|
89
|
+
background: ${({ $color }) => $color || EDITOR_COLORS[0]};
|
|
90
|
+
color: white;
|
|
91
|
+
font-size: 12px;
|
|
92
|
+
font-weight: 700;
|
|
93
|
+
display: flex;
|
|
94
|
+
align-items: center;
|
|
95
|
+
justify-content: center;
|
|
96
|
+
flex-shrink: 0;
|
|
97
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
98
|
+
`;
|
|
99
|
+
const EditorInfo = styled.div`
|
|
100
|
+
flex: 1;
|
|
101
|
+
min-width: 0;
|
|
102
|
+
display: flex;
|
|
103
|
+
flex-direction: column;
|
|
104
|
+
gap: 2px;
|
|
105
|
+
`;
|
|
106
|
+
const EditorName = styled.span`
|
|
107
|
+
font-size: 13px;
|
|
108
|
+
font-weight: 600;
|
|
109
|
+
color: ${(props) => props.theme.colors.neutral800};
|
|
110
|
+
white-space: nowrap;
|
|
111
|
+
overflow: hidden;
|
|
112
|
+
text-overflow: ellipsis;
|
|
113
|
+
`;
|
|
114
|
+
const EditorEmail = styled.span`
|
|
115
|
+
font-size: 11px;
|
|
116
|
+
color: ${(props) => props.theme.colors.neutral500};
|
|
117
|
+
white-space: nowrap;
|
|
118
|
+
overflow: hidden;
|
|
119
|
+
text-overflow: ellipsis;
|
|
120
|
+
`;
|
|
121
|
+
const EditingBadge = styled.span`
|
|
122
|
+
font-size: 10px;
|
|
123
|
+
font-weight: 600;
|
|
124
|
+
color: #166534;
|
|
125
|
+
background: #dcfce7;
|
|
126
|
+
padding: 4px 8px;
|
|
127
|
+
border-radius: 12px;
|
|
128
|
+
flex-shrink: 0;
|
|
129
|
+
`;
|
|
130
|
+
const TypingBadge = styled.span`
|
|
131
|
+
font-size: 10px;
|
|
132
|
+
font-weight: 600;
|
|
133
|
+
color: #92400e;
|
|
134
|
+
background: #fef3c7;
|
|
135
|
+
padding: 4px 8px;
|
|
136
|
+
border-radius: 12px;
|
|
137
|
+
flex-shrink: 0;
|
|
138
|
+
`;
|
|
139
|
+
const EmptyState = styled.div`
|
|
140
|
+
text-align: center;
|
|
141
|
+
padding: 16px;
|
|
142
|
+
background: ${(props) => props.theme.colors.neutral100};
|
|
143
|
+
border-radius: 10px;
|
|
144
|
+
border: 1px dashed ${(props) => props.theme.colors.neutral300};
|
|
145
|
+
`;
|
|
146
|
+
const EmptyText = styled.span`
|
|
147
|
+
font-size: 13px;
|
|
148
|
+
color: ${(props) => props.theme.colors.neutral500};
|
|
149
|
+
`;
|
|
150
|
+
const getEditorInitials = (user = {}) => {
|
|
151
|
+
const first = (user.firstname?.[0] || user.username?.[0] || user.email?.[0] || "?").toUpperCase();
|
|
152
|
+
const last = (user.lastname?.[0] || "").toUpperCase();
|
|
153
|
+
return `${first}${last}`.trim();
|
|
154
|
+
};
|
|
155
|
+
const getEditorName = (user = {}) => {
|
|
156
|
+
if (user.firstname) {
|
|
157
|
+
return `${user.firstname} ${user.lastname || ""}`.trim();
|
|
158
|
+
}
|
|
159
|
+
return user.username || user.email || "Unknown";
|
|
160
|
+
};
|
|
161
|
+
const LivePresencePanel = ({ documentId, model, document }) => {
|
|
162
|
+
const { formatMessage } = useIntl();
|
|
163
|
+
const { post } = useFetchClient();
|
|
164
|
+
const t = (id, defaultMessage, values) => formatMessage({ id: `${PLUGIN_ID}.${id}`, defaultMessage }, values);
|
|
165
|
+
const socketRef = useRef(null);
|
|
166
|
+
const [sessionData, setSessionData] = useState(null);
|
|
167
|
+
const [presenceState, setPresenceState] = useState({
|
|
168
|
+
status: "initializing",
|
|
169
|
+
editors: [],
|
|
170
|
+
typingUsers: [],
|
|
171
|
+
error: null
|
|
172
|
+
});
|
|
173
|
+
const uid = model?.uid || model;
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
if (!uid || !documentId) {
|
|
176
|
+
setPresenceState((prev) => ({ ...prev, status: "disconnected", error: "No content" }));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
let cancelled = false;
|
|
180
|
+
const getSession = async () => {
|
|
181
|
+
try {
|
|
182
|
+
setPresenceState((prev) => ({ ...prev, status: "requesting" }));
|
|
183
|
+
const { data } = await post(`/${PLUGIN_ID}/presence/session`, {});
|
|
184
|
+
if (cancelled) return;
|
|
185
|
+
if (!data || !data.token) {
|
|
186
|
+
throw new Error("Invalid session response");
|
|
187
|
+
}
|
|
188
|
+
console.log(`[${PLUGIN_ID}] Presence session obtained for:`, data.user?.email);
|
|
189
|
+
setSessionData(data);
|
|
190
|
+
setPresenceState((prev) => ({ ...prev, status: "connecting" }));
|
|
191
|
+
} catch (error2) {
|
|
192
|
+
if (cancelled) return;
|
|
193
|
+
console.error(`[${PLUGIN_ID}] Failed to get presence session:`, error2);
|
|
194
|
+
setPresenceState((prev) => ({
|
|
195
|
+
...prev,
|
|
196
|
+
status: "error",
|
|
197
|
+
error: error2.message || "Failed to get session"
|
|
198
|
+
}));
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
getSession();
|
|
202
|
+
return () => {
|
|
203
|
+
cancelled = true;
|
|
204
|
+
};
|
|
205
|
+
}, [uid, documentId, post]);
|
|
206
|
+
useEffect(() => {
|
|
207
|
+
if (!sessionData?.token || !uid || !documentId) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const socketUrl = sessionData.wsUrl || `${window.location.protocol}//${window.location.host}`;
|
|
211
|
+
const socket = io(socketUrl, {
|
|
212
|
+
path: sessionData.wsPath || "/socket.io",
|
|
213
|
+
transports: ["websocket", "polling"],
|
|
214
|
+
auth: {
|
|
215
|
+
token: sessionData.token,
|
|
216
|
+
strategy: "admin-jwt",
|
|
217
|
+
isAdmin: true
|
|
218
|
+
},
|
|
219
|
+
reconnection: true,
|
|
220
|
+
reconnectionAttempts: 3
|
|
221
|
+
});
|
|
222
|
+
socketRef.current = socket;
|
|
223
|
+
let lastTypingEmit = 0;
|
|
224
|
+
const TYPING_THROTTLE = 2e3;
|
|
225
|
+
const getFieldName = (element) => {
|
|
226
|
+
const name = element.name || element.id || "";
|
|
227
|
+
const label = element.closest("label") || document.querySelector(`label[for="${element.id}"]`);
|
|
228
|
+
if (label) {
|
|
229
|
+
return label.textContent?.trim() || name;
|
|
230
|
+
}
|
|
231
|
+
const fieldWrapper = element.closest('[class*="Field"]');
|
|
232
|
+
if (fieldWrapper) {
|
|
233
|
+
const labelEl = fieldWrapper.querySelector('label, [class*="Label"]');
|
|
234
|
+
if (labelEl) {
|
|
235
|
+
return labelEl.textContent?.trim() || name;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return name || "unknown field";
|
|
239
|
+
};
|
|
240
|
+
const handleInput = (event) => {
|
|
241
|
+
const target = event.target;
|
|
242
|
+
if (!["INPUT", "TEXTAREA"].includes(target.tagName)) return;
|
|
243
|
+
const isInContentManager = target.closest('[class*="ContentLayout"]') || target.closest("main");
|
|
244
|
+
if (!isInContentManager) return;
|
|
245
|
+
const now = Date.now();
|
|
246
|
+
if (now - lastTypingEmit < TYPING_THROTTLE) return;
|
|
247
|
+
lastTypingEmit = now;
|
|
248
|
+
const fieldName = getFieldName(target);
|
|
249
|
+
if (socket.connected) {
|
|
250
|
+
socket.emit("presence:typing", { uid, documentId, fieldName });
|
|
251
|
+
console.log(`[${PLUGIN_ID}] Typing in field: ${fieldName}`);
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
if (typeof document !== "undefined" && typeof document.addEventListener === "function") {
|
|
255
|
+
document.addEventListener("input", handleInput, true);
|
|
256
|
+
}
|
|
257
|
+
socket.on("connect", () => {
|
|
258
|
+
console.log(`[${PLUGIN_ID}] Presence socket connected`);
|
|
259
|
+
setPresenceState((prev) => ({ ...prev, status: "connected", error: null }));
|
|
260
|
+
socket.emit("presence:join", { uid, documentId }, (response) => {
|
|
261
|
+
if (response?.success) {
|
|
262
|
+
setPresenceState((prev) => ({
|
|
263
|
+
...prev,
|
|
264
|
+
editors: (response.editors || []).map((e) => ({
|
|
265
|
+
...e,
|
|
266
|
+
isCurrentUser: e.socketId === socket.id
|
|
267
|
+
}))
|
|
268
|
+
}));
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
socket.on("disconnect", () => {
|
|
273
|
+
setPresenceState((prev) => ({ ...prev, status: "disconnected" }));
|
|
274
|
+
});
|
|
275
|
+
socket.on("connect_error", (err) => {
|
|
276
|
+
console.warn(`[${PLUGIN_ID}] Presence socket error:`, err.message);
|
|
277
|
+
setPresenceState((prev) => ({ ...prev, status: "error", error: err.message }));
|
|
278
|
+
});
|
|
279
|
+
socket.on("presence:update", (data) => {
|
|
280
|
+
if (data.uid === uid && data.documentId === documentId) {
|
|
281
|
+
setPresenceState((prev) => ({
|
|
282
|
+
...prev,
|
|
283
|
+
editors: (data.editors || []).map((e) => ({
|
|
284
|
+
...e,
|
|
285
|
+
isCurrentUser: e.socketId === socket.id
|
|
286
|
+
}))
|
|
287
|
+
}));
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
socket.on("presence:typing", (data) => {
|
|
291
|
+
if (data.uid === uid && data.documentId === documentId) {
|
|
292
|
+
setPresenceState((prev) => {
|
|
293
|
+
const newTyping = [...prev.typingUsers.filter((t2) => t2.user?.id !== data.user?.id)];
|
|
294
|
+
newTyping.push({ user: data.user, fieldName: data.fieldName, timestamp: Date.now() });
|
|
295
|
+
return { ...prev, typingUsers: newTyping };
|
|
296
|
+
});
|
|
297
|
+
setTimeout(() => {
|
|
298
|
+
setPresenceState((prev) => ({
|
|
299
|
+
...prev,
|
|
300
|
+
typingUsers: prev.typingUsers.filter((t2) => t2.user?.id !== data.user?.id)
|
|
301
|
+
}));
|
|
302
|
+
}, 3e3);
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
const heartbeat = setInterval(() => {
|
|
306
|
+
if (socket.connected) {
|
|
307
|
+
socket.emit("presence:heartbeat");
|
|
308
|
+
}
|
|
309
|
+
}, 3e4);
|
|
310
|
+
return () => {
|
|
311
|
+
clearInterval(heartbeat);
|
|
312
|
+
if (typeof document !== "undefined" && typeof document.removeEventListener === "function") {
|
|
313
|
+
document.removeEventListener("input", handleInput, true);
|
|
314
|
+
}
|
|
315
|
+
if (socket.connected) {
|
|
316
|
+
socket.emit("presence:leave", { uid, documentId });
|
|
317
|
+
}
|
|
318
|
+
socket.disconnect();
|
|
319
|
+
socketRef.current = null;
|
|
320
|
+
};
|
|
321
|
+
}, [sessionData, uid, documentId]);
|
|
322
|
+
const { status, editors, typingUsers, error } = presenceState;
|
|
323
|
+
const otherEditors = useMemo(() => {
|
|
324
|
+
return editors.filter((e) => !e.isCurrentUser);
|
|
325
|
+
}, [editors]);
|
|
326
|
+
const getUserTypingInfo = useCallback((userId) => {
|
|
327
|
+
const typing = typingUsers.find((t2) => t2.user?.id === userId);
|
|
328
|
+
return typing || null;
|
|
329
|
+
}, [typingUsers]);
|
|
330
|
+
useCallback((userId) => {
|
|
331
|
+
return typingUsers.some((t2) => t2.user?.id === userId);
|
|
332
|
+
}, [typingUsers]);
|
|
333
|
+
const statusLabel = useMemo(() => {
|
|
334
|
+
switch (status) {
|
|
335
|
+
case "connected":
|
|
336
|
+
return t("presence.live", "Live");
|
|
337
|
+
case "connecting":
|
|
338
|
+
return t("presence.connecting", "Connecting...");
|
|
339
|
+
case "requesting":
|
|
340
|
+
return t("presence.requesting", "Authenticating...");
|
|
341
|
+
case "initializing":
|
|
342
|
+
return t("presence.initializing", "Initializing...");
|
|
343
|
+
case "error":
|
|
344
|
+
return t("presence.error", "Connection Error");
|
|
345
|
+
case "disconnected":
|
|
346
|
+
return t("presence.disconnected", "Disconnected");
|
|
347
|
+
default:
|
|
348
|
+
return t("presence.offline", "Offline");
|
|
349
|
+
}
|
|
350
|
+
}, [status, t]);
|
|
351
|
+
const isConnected = status === "connected";
|
|
352
|
+
console.log(`[${PLUGIN_ID}] LivePresencePanel render:`, { uid, documentId, status, editors: otherEditors.length });
|
|
353
|
+
return {
|
|
354
|
+
title: t("presence.title", "Live Presence"),
|
|
355
|
+
content: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 4, alignItems: "stretch", style: { width: "100%" }, children: [
|
|
356
|
+
/* @__PURE__ */ jsxs(StatusCard, { $status: status, children: [
|
|
357
|
+
/* @__PURE__ */ jsx(StatusDot, { $status: status }),
|
|
358
|
+
/* @__PURE__ */ jsxs(StatusText, { children: [
|
|
359
|
+
/* @__PURE__ */ jsx(StatusLabel, { $status: status, children: statusLabel }),
|
|
360
|
+
/* @__PURE__ */ jsx(StatusSubtext, { children: isConnected ? t("presence.realtimeActive", "Real-time sync active") : error || t("presence.establishing", "Establishing connection...") })
|
|
361
|
+
] })
|
|
362
|
+
] }),
|
|
363
|
+
isConnected && otherEditors.length > 0 && /* @__PURE__ */ jsxs("div", { children: [
|
|
364
|
+
/* @__PURE__ */ jsx(SectionTitle, { children: t("presence.activeEditors", "Also Editing ({count})", { count: otherEditors.length }) }),
|
|
365
|
+
/* @__PURE__ */ jsx(Flex, { direction: "column", gap: 2, alignItems: "stretch", children: otherEditors.map((editor, idx) => {
|
|
366
|
+
const user = editor.user || {};
|
|
367
|
+
const typingInfo = getUserTypingInfo(user.id);
|
|
368
|
+
return /* @__PURE__ */ jsxs(EditorItem, { children: [
|
|
369
|
+
/* @__PURE__ */ jsx(EditorAvatar, { $color: EDITOR_COLORS[idx % EDITOR_COLORS.length], children: getEditorInitials(user) }),
|
|
370
|
+
/* @__PURE__ */ jsxs(EditorInfo, { children: [
|
|
371
|
+
/* @__PURE__ */ jsx(EditorName, { children: getEditorName(user) }),
|
|
372
|
+
typingInfo?.fieldName ? /* @__PURE__ */ jsxs(EditorEmail, { children: [
|
|
373
|
+
"Typing in: ",
|
|
374
|
+
typingInfo.fieldName
|
|
375
|
+
] }) : user.email && user.firstname ? /* @__PURE__ */ jsx(EditorEmail, { children: user.email }) : null
|
|
376
|
+
] }),
|
|
377
|
+
typingInfo ? /* @__PURE__ */ jsx(TypingBadge, { children: t("presence.typing", "Typing...") }) : /* @__PURE__ */ jsx(EditingBadge, { children: t("presence.editing", "Editing") })
|
|
378
|
+
] }, editor.socketId || idx);
|
|
379
|
+
}) })
|
|
380
|
+
] }),
|
|
381
|
+
isConnected && otherEditors.length === 0 && /* @__PURE__ */ jsx(EmptyState, { children: /* @__PURE__ */ jsx(EmptyText, { children: t("presence.workingAlone", "You are the only editor") }) })
|
|
382
|
+
] })
|
|
383
|
+
};
|
|
384
|
+
};
|
|
385
|
+
export {
|
|
386
|
+
LivePresencePanel as default
|
|
387
|
+
};
|