@peers-app/peers-ui 0.7.40 → 0.8.1
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/command-palette/command-palette.d.ts +2 -2
- package/dist/command-palette/command-palette.js +3 -7
- package/dist/components/group-switcher.d.ts +2 -1
- package/dist/components/group-switcher.js +7 -6
- package/dist/globals.d.ts +1 -1
- package/dist/screens/contacts/contact-list.js +4 -1
- package/dist/screens/contacts/index.d.ts +2 -0
- package/dist/screens/contacts/index.js +2 -0
- package/dist/screens/contacts/user-connect.d.ts +2 -0
- package/dist/screens/contacts/user-connect.js +312 -0
- package/dist/screens/network-viewer/device-details-modal.js +44 -0
- package/dist/screens/network-viewer/group-details-modal.js +80 -2
- package/dist/screens/network-viewer/network-viewer.js +36 -16
- package/dist/screens/settings/settings-page.js +13 -7
- package/dist/screens/setup-user.js +8 -6
- package/dist/system-apps/index.d.ts +1 -0
- package/dist/system-apps/index.js +10 -1
- package/dist/system-apps/mobile-settings.app.d.ts +2 -0
- package/dist/system-apps/mobile-settings.app.js +8 -0
- package/dist/tabs-layout/tabs-layout.js +60 -38
- package/dist/tabs-layout/tabs-state.d.ts +10 -4
- package/dist/tabs-layout/tabs-state.js +41 -4
- package/dist/ui-router/ui-loader.js +45 -12
- package/package.json +3 -3
- package/src/command-palette/command-palette.ts +4 -8
- package/src/components/group-switcher.tsx +12 -8
- package/src/screens/contacts/contact-list.tsx +4 -0
- package/src/screens/contacts/index.ts +3 -1
- package/src/screens/contacts/user-connect.tsx +452 -0
- package/src/screens/network-viewer/device-details-modal.tsx +55 -0
- package/src/screens/network-viewer/group-details-modal.tsx +144 -1
- package/src/screens/network-viewer/network-viewer.tsx +36 -29
- package/src/screens/settings/settings-page.tsx +17 -9
- package/src/screens/setup-user.tsx +9 -6
- package/src/system-apps/index.ts +9 -0
- package/src/system-apps/mobile-settings.app.ts +8 -0
- package/src/tabs-layout/tabs-layout.tsx +108 -82
- package/src/tabs-layout/tabs-state.ts +54 -5
- package/src/ui-router/ui-loader.tsx +50 -11
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Import all contact screen components to ensure they register their routes
|
|
2
2
|
import './contact-list';
|
|
3
3
|
import './contact-details';
|
|
4
|
+
import './user-connect';
|
|
4
5
|
|
|
5
6
|
export * from "./contact-list";
|
|
6
|
-
export * from "./contact-details";
|
|
7
|
+
export * from "./contact-details";
|
|
8
|
+
export * from "./user-connect";
|
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatConnectionCode,
|
|
3
|
+
generateConnectionCode,
|
|
4
|
+
generateConfirmationHash,
|
|
5
|
+
getMe,
|
|
6
|
+
getUserContext,
|
|
7
|
+
IUser,
|
|
8
|
+
IUserConnectInfo,
|
|
9
|
+
setUserTrustLevel,
|
|
10
|
+
TrustLevel,
|
|
11
|
+
userConnectCodeOffer,
|
|
12
|
+
userConnectCodeAnswer,
|
|
13
|
+
userConnectStatus,
|
|
14
|
+
Users,
|
|
15
|
+
Devices
|
|
16
|
+
} from "@peers-app/peers-sdk";
|
|
17
|
+
import React, { useState, useEffect, useCallback } from 'react';
|
|
18
|
+
import { mainContentPath } from '../../globals';
|
|
19
|
+
import { useObservable } from '../../hooks';
|
|
20
|
+
import { registerInternalPeersUI } from '../../ui-router/ui-loader';
|
|
21
|
+
|
|
22
|
+
type ConnectionMode = 'select' | 'initiate' | 'respond';
|
|
23
|
+
type ConnectionStatus = 'idle' | 'waiting' | 'success' | 'error';
|
|
24
|
+
|
|
25
|
+
interface ConnectionResult {
|
|
26
|
+
remoteUser: IUser;
|
|
27
|
+
confirmationHash: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function UserConnect() {
|
|
31
|
+
const [mode, setMode] = useState<ConnectionMode>('select');
|
|
32
|
+
const [status, setStatus] = useState<ConnectionStatus>('idle');
|
|
33
|
+
const [connectionCode, setConnectionCode] = useState<string>('');
|
|
34
|
+
const [inputCode, setInputCode] = useState<string>('');
|
|
35
|
+
const [result, setResult] = useState<ConnectionResult | null>(null);
|
|
36
|
+
const [error, setError] = useState<string>('');
|
|
37
|
+
const [copied, setCopied] = useState(false);
|
|
38
|
+
|
|
39
|
+
// Subscribe to userConnectStatus from the device layer
|
|
40
|
+
const [connectStatus] = useObservable(userConnectStatus);
|
|
41
|
+
|
|
42
|
+
// Also set up a direct subscription after loading is complete
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
let disposed = false;
|
|
45
|
+
let subscription: { dispose: () => void } | undefined;
|
|
46
|
+
|
|
47
|
+
userConnectStatus.loadingPromise.then(() => {
|
|
48
|
+
if (disposed) return;
|
|
49
|
+
subscription = userConnectStatus.subscribe(() => {
|
|
50
|
+
// Force a re-render by checking the current value
|
|
51
|
+
const currentStatus = userConnectStatus();
|
|
52
|
+
if (currentStatus && typeof currentStatus === 'string') {
|
|
53
|
+
handleConnectStatusChange(currentStatus);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return () => {
|
|
59
|
+
disposed = true;
|
|
60
|
+
subscription?.dispose();
|
|
61
|
+
};
|
|
62
|
+
}, []);
|
|
63
|
+
|
|
64
|
+
// Handle connect status changes
|
|
65
|
+
const handleConnectStatusChange = useCallback(async (connectStatusValue: string) => {
|
|
66
|
+
if (!connectStatusValue || status === 'success') return;
|
|
67
|
+
|
|
68
|
+
if (connectStatusValue.startsWith('Error:')) {
|
|
69
|
+
// Error status
|
|
70
|
+
setError(connectStatusValue.replace('Error: ', ''));
|
|
71
|
+
setStatus('error');
|
|
72
|
+
} else if (connectStatusValue.length > 0) {
|
|
73
|
+
// Success - connectStatus is the remote userId
|
|
74
|
+
const remoteUserId = connectStatusValue;
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const userContext = await getUserContext();
|
|
78
|
+
const me = await getMe();
|
|
79
|
+
const remoteUser = await Users(userContext.userDataContext).get(remoteUserId);
|
|
80
|
+
|
|
81
|
+
if (!remoteUser) {
|
|
82
|
+
setError('Could not find connected user');
|
|
83
|
+
setStatus('error');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const remoteDevice = await Devices(userContext.userDataContext).findOne({ userId: remoteUserId });
|
|
88
|
+
if (!remoteDevice) {
|
|
89
|
+
setError('Could not find connected device but connection was successful');
|
|
90
|
+
setStatus('error');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Build user connect info for confirmation hash
|
|
95
|
+
const myInfo: IUserConnectInfo = {
|
|
96
|
+
userId: me.userId,
|
|
97
|
+
publicKey: me.publicKey,
|
|
98
|
+
publicBoxKey: me.publicBoxKey,
|
|
99
|
+
deviceId: userContext.deviceId(),
|
|
100
|
+
};
|
|
101
|
+
const remoteInfo: IUserConnectInfo = {
|
|
102
|
+
userId: remoteUser.userId,
|
|
103
|
+
publicKey: remoteUser.publicKey,
|
|
104
|
+
publicBoxKey: remoteUser.publicBoxKey,
|
|
105
|
+
deviceId: remoteDevice.deviceId,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const confirmationHash = generateConfirmationHash(myInfo, remoteInfo);
|
|
109
|
+
|
|
110
|
+
setResult({ remoteUser, confirmationHash });
|
|
111
|
+
setStatus('success');
|
|
112
|
+
|
|
113
|
+
// Clear the codes
|
|
114
|
+
userConnectCodeOffer('');
|
|
115
|
+
userConnectCodeAnswer('');
|
|
116
|
+
} catch (err: any) {
|
|
117
|
+
setError(err.message || 'Failed to complete connection');
|
|
118
|
+
setStatus('error');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}, [status]);
|
|
122
|
+
|
|
123
|
+
// React to userConnectStatus changes from useObservable
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
if (connectStatus && typeof connectStatus === 'string') {
|
|
126
|
+
handleConnectStatusChange(connectStatus);
|
|
127
|
+
}
|
|
128
|
+
}, [connectStatus, handleConnectStatusChange]);
|
|
129
|
+
|
|
130
|
+
// Clean up on unmount
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
return () => {
|
|
133
|
+
if (mode === 'initiate' && status === 'waiting') {
|
|
134
|
+
userConnectCodeOffer('');
|
|
135
|
+
userConnectCodeAnswer('');
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}, [mode, status]);
|
|
139
|
+
|
|
140
|
+
const handleInitiate = useCallback(async () => {
|
|
141
|
+
setMode('initiate');
|
|
142
|
+
setStatus('waiting');
|
|
143
|
+
setError('');
|
|
144
|
+
userConnectStatus(''); // Clear any previous status
|
|
145
|
+
|
|
146
|
+
// Generate the connection code
|
|
147
|
+
const code = generateConnectionCode();
|
|
148
|
+
userConnectCodeOffer(code.code);
|
|
149
|
+
const formattedCode = formatConnectionCode(code.code);
|
|
150
|
+
setConnectionCode(formattedCode);
|
|
151
|
+
}, []);
|
|
152
|
+
|
|
153
|
+
const handleRespond = useCallback(async () => {
|
|
154
|
+
if (inputCode.replace(/[^0-9A-Za-z]/g, '').length !== 12) {
|
|
155
|
+
setError('Please enter a valid 12-character connection code');
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
setStatus('waiting');
|
|
160
|
+
setError('');
|
|
161
|
+
userConnectStatus(''); // Clear any previous status
|
|
162
|
+
userConnectCodeAnswer(inputCode);
|
|
163
|
+
}, [inputCode]);
|
|
164
|
+
|
|
165
|
+
const handleCancel = useCallback(async () => {
|
|
166
|
+
userConnectCodeOffer('');
|
|
167
|
+
userConnectCodeAnswer('');
|
|
168
|
+
userConnectStatus('');
|
|
169
|
+
setMode('select');
|
|
170
|
+
setStatus('idle');
|
|
171
|
+
setConnectionCode('');
|
|
172
|
+
setError('');
|
|
173
|
+
}, []);
|
|
174
|
+
|
|
175
|
+
const handleReset = useCallback(() => {
|
|
176
|
+
userConnectCodeOffer('');
|
|
177
|
+
userConnectCodeAnswer('');
|
|
178
|
+
userConnectStatus('');
|
|
179
|
+
setMode('select');
|
|
180
|
+
setStatus('idle');
|
|
181
|
+
setConnectionCode('');
|
|
182
|
+
setInputCode('');
|
|
183
|
+
setResult(null);
|
|
184
|
+
setError('');
|
|
185
|
+
setCopied(false);
|
|
186
|
+
}, []);
|
|
187
|
+
|
|
188
|
+
const handleSaveContact = useCallback(async () => {
|
|
189
|
+
if (!result) return;
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
const userContext = await getUserContext();
|
|
193
|
+
|
|
194
|
+
// Contact is already saved by the device layer, just set trust level
|
|
195
|
+
await setUserTrustLevel(result.remoteUser.userId, TrustLevel.Trusted, userContext.userDataContext);
|
|
196
|
+
|
|
197
|
+
// Navigate to contact details
|
|
198
|
+
mainContentPath(`contacts/${result.remoteUser.userId}`);
|
|
199
|
+
} catch (err: any) {
|
|
200
|
+
setError(err.message || 'Failed to save contact');
|
|
201
|
+
}
|
|
202
|
+
}, [result]);
|
|
203
|
+
|
|
204
|
+
// Copy connection code to clipboard
|
|
205
|
+
const handleCopyCode = useCallback(async () => {
|
|
206
|
+
if (!connectionCode) return;
|
|
207
|
+
try {
|
|
208
|
+
await navigator.clipboard.writeText(connectionCode);
|
|
209
|
+
setCopied(true);
|
|
210
|
+
setTimeout(() => setCopied(false), 2000);
|
|
211
|
+
} catch (err) {
|
|
212
|
+
// Fallback for older browsers
|
|
213
|
+
const textArea = document.createElement('textarea');
|
|
214
|
+
textArea.value = connectionCode;
|
|
215
|
+
document.body.appendChild(textArea);
|
|
216
|
+
textArea.select();
|
|
217
|
+
document.execCommand('copy');
|
|
218
|
+
document.body.removeChild(textArea);
|
|
219
|
+
setCopied(true);
|
|
220
|
+
setTimeout(() => setCopied(false), 2000);
|
|
221
|
+
}
|
|
222
|
+
}, [connectionCode]);
|
|
223
|
+
|
|
224
|
+
// Format input code as user types
|
|
225
|
+
const handleCodeInput = (value: string) => {
|
|
226
|
+
// Remove non-alphanumeric characters
|
|
227
|
+
const cleaned = value.toUpperCase().replace(/[^0-9A-Z]/g, '');
|
|
228
|
+
|
|
229
|
+
// Format as XXXX-YYYY-ZZZZ
|
|
230
|
+
let formatted = '';
|
|
231
|
+
for (let i = 0; i < cleaned.length && i < 12; i++) {
|
|
232
|
+
if (i === 4 || i === 8) formatted += '-';
|
|
233
|
+
formatted += cleaned[i];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
setInputCode(formatted);
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
return (
|
|
240
|
+
<div className="container-fluid p-3">
|
|
241
|
+
<div className="d-flex justify-content-between align-items-center mb-4">
|
|
242
|
+
<h4>
|
|
243
|
+
<i className="bi-person-plus-fill me-2" />
|
|
244
|
+
Connect to New User
|
|
245
|
+
</h4>
|
|
246
|
+
{mode !== 'select' && (
|
|
247
|
+
<button className="btn btn-outline-secondary btn-sm" onClick={handleReset}>
|
|
248
|
+
<i className="bi-arrow-left me-1" />
|
|
249
|
+
Back
|
|
250
|
+
</button>
|
|
251
|
+
)}
|
|
252
|
+
</div>
|
|
253
|
+
|
|
254
|
+
{/* Mode Selection */}
|
|
255
|
+
{mode === 'select' && (
|
|
256
|
+
<div className="row g-3">
|
|
257
|
+
<div className="col-md-6">
|
|
258
|
+
<div
|
|
259
|
+
className="card h-100 border-primary"
|
|
260
|
+
style={{ cursor: 'pointer' }}
|
|
261
|
+
onClick={handleInitiate}
|
|
262
|
+
>
|
|
263
|
+
<div className="card-body text-center p-4">
|
|
264
|
+
<i className="bi-qr-code display-4 text-primary mb-3" />
|
|
265
|
+
<h5 className="card-title">Create Connection Code</h5>
|
|
266
|
+
<p className="card-text text-muted">
|
|
267
|
+
Generate a code to share with someone who wants to connect with you
|
|
268
|
+
</p>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
<div className="col-md-6">
|
|
273
|
+
<div
|
|
274
|
+
className="card h-100 border-success"
|
|
275
|
+
style={{ cursor: 'pointer' }}
|
|
276
|
+
onClick={() => setMode('respond')}
|
|
277
|
+
>
|
|
278
|
+
<div className="card-body text-center p-4">
|
|
279
|
+
<i className="bi-keyboard display-4 text-success mb-3" />
|
|
280
|
+
<h5 className="card-title">Enter Connection Code</h5>
|
|
281
|
+
<p className="card-text text-muted">
|
|
282
|
+
Enter a code that someone shared with you to connect
|
|
283
|
+
</p>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
)}
|
|
289
|
+
|
|
290
|
+
{/* Initiate Mode - Show Generated Code */}
|
|
291
|
+
{mode === 'initiate' && status === 'waiting' && (
|
|
292
|
+
<div className="text-center">
|
|
293
|
+
<div className="mb-4">
|
|
294
|
+
<p className="text-muted">Share this code with the person you want to connect with:</p>
|
|
295
|
+
</div>
|
|
296
|
+
|
|
297
|
+
<div className="mb-4" style={{ maxWidth: '400px', margin: '0 auto' }}>
|
|
298
|
+
<div className="input-group">
|
|
299
|
+
<input
|
|
300
|
+
type="text"
|
|
301
|
+
className="form-control form-control-lg text-center font-monospace"
|
|
302
|
+
value={connectionCode || 'XXXX-YYYY-ZZZZ'}
|
|
303
|
+
readOnly
|
|
304
|
+
style={{ letterSpacing: '0.15em', fontSize: '1.5rem' }}
|
|
305
|
+
/>
|
|
306
|
+
<button
|
|
307
|
+
className="btn btn-outline-primary"
|
|
308
|
+
onClick={handleCopyCode}
|
|
309
|
+
title="Copy to clipboard"
|
|
310
|
+
>
|
|
311
|
+
<i className={copied ? "bi-check-lg" : "bi-clipboard"} />
|
|
312
|
+
</button>
|
|
313
|
+
</div>
|
|
314
|
+
{copied && (
|
|
315
|
+
<small className="text-success mt-1 d-block">Copied!</small>
|
|
316
|
+
)}
|
|
317
|
+
</div>
|
|
318
|
+
|
|
319
|
+
<div className="mb-4">
|
|
320
|
+
<div className="spinner-border spinner-border-sm text-primary me-2" role="status" />
|
|
321
|
+
<span className="text-muted">Waiting for connection...</span>
|
|
322
|
+
</div>
|
|
323
|
+
|
|
324
|
+
<p className="text-muted small">
|
|
325
|
+
This code will expire in 10 minutes
|
|
326
|
+
</p>
|
|
327
|
+
|
|
328
|
+
<button className="btn btn-outline-secondary" onClick={handleCancel}>
|
|
329
|
+
Cancel
|
|
330
|
+
</button>
|
|
331
|
+
</div>
|
|
332
|
+
)}
|
|
333
|
+
|
|
334
|
+
{/* Respond Mode - Enter Code */}
|
|
335
|
+
{mode === 'respond' && status !== 'success' && (
|
|
336
|
+
<div className="text-center">
|
|
337
|
+
<div className="mb-4">
|
|
338
|
+
<p className="text-muted">Enter the connection code shared with you:</p>
|
|
339
|
+
</div>
|
|
340
|
+
|
|
341
|
+
<div className="mb-4" style={{ maxWidth: '400px', margin: '0 auto' }}>
|
|
342
|
+
<input
|
|
343
|
+
type="text"
|
|
344
|
+
className="form-control form-control-lg text-center font-monospace"
|
|
345
|
+
placeholder="XXXX-YYYY-ZZZZ"
|
|
346
|
+
value={inputCode}
|
|
347
|
+
onChange={(e) => handleCodeInput(e.target.value)}
|
|
348
|
+
maxLength={14}
|
|
349
|
+
style={{ letterSpacing: '0.15em', fontSize: '1.5rem' }}
|
|
350
|
+
autoFocus
|
|
351
|
+
/>
|
|
352
|
+
</div>
|
|
353
|
+
|
|
354
|
+
{error && (
|
|
355
|
+
<div className="alert alert-danger" style={{ maxWidth: '400px', margin: '0 auto 1rem' }}>
|
|
356
|
+
{error}
|
|
357
|
+
</div>
|
|
358
|
+
)}
|
|
359
|
+
|
|
360
|
+
<button
|
|
361
|
+
className="btn btn-primary btn-lg"
|
|
362
|
+
onClick={handleRespond}
|
|
363
|
+
disabled={status === 'waiting' || inputCode.replace(/[^0-9A-Z]/gi, '').length !== 12}
|
|
364
|
+
>
|
|
365
|
+
{status === 'waiting' ? (
|
|
366
|
+
<>
|
|
367
|
+
<span className="spinner-border spinner-border-sm me-2" role="status" />
|
|
368
|
+
Connecting...
|
|
369
|
+
</>
|
|
370
|
+
) : (
|
|
371
|
+
<>
|
|
372
|
+
<i className="bi-link-45deg me-2" />
|
|
373
|
+
Connect
|
|
374
|
+
</>
|
|
375
|
+
)}
|
|
376
|
+
</button>
|
|
377
|
+
</div>
|
|
378
|
+
)}
|
|
379
|
+
|
|
380
|
+
{/* Success State */}
|
|
381
|
+
{status === 'success' && result && (
|
|
382
|
+
<div className="text-center">
|
|
383
|
+
<div className="mb-4">
|
|
384
|
+
<i className="bi-check-circle-fill text-success display-3" />
|
|
385
|
+
</div>
|
|
386
|
+
|
|
387
|
+
<h5 className="mb-4">Connection Successful!</h5>
|
|
388
|
+
|
|
389
|
+
<div className="card mb-4" style={{ maxWidth: '400px', margin: '0 auto' }}>
|
|
390
|
+
<div className="card-body">
|
|
391
|
+
<p className="text-muted mb-2">Verify with the other person that you both see:</p>
|
|
392
|
+
<h3 className="font-monospace text-primary mb-3" style={{ letterSpacing: '0.2em' }}>
|
|
393
|
+
{result.confirmationHash}
|
|
394
|
+
</h3>
|
|
395
|
+
|
|
396
|
+
<hr />
|
|
397
|
+
|
|
398
|
+
<div className="text-start">
|
|
399
|
+
<small className="text-muted">Name:</small>
|
|
400
|
+
<p className="mb-2">{result.remoteUser.name}</p>
|
|
401
|
+
|
|
402
|
+
<small className="text-muted">User ID:</small>
|
|
403
|
+
<p className="font-monospace small mb-0">{result.remoteUser.userId}</p>
|
|
404
|
+
</div>
|
|
405
|
+
</div>
|
|
406
|
+
</div>
|
|
407
|
+
|
|
408
|
+
<div className="d-flex gap-2 justify-content-center">
|
|
409
|
+
<button className="btn btn-primary" onClick={handleSaveContact}>
|
|
410
|
+
<i className="bi-check-lg me-2" />
|
|
411
|
+
Trust & View Contact
|
|
412
|
+
</button>
|
|
413
|
+
<button className="btn btn-outline-secondary" onClick={handleReset}>
|
|
414
|
+
Connect Another
|
|
415
|
+
</button>
|
|
416
|
+
</div>
|
|
417
|
+
</div>
|
|
418
|
+
)}
|
|
419
|
+
|
|
420
|
+
{/* Error State */}
|
|
421
|
+
{status === 'error' && (
|
|
422
|
+
<div className="text-center">
|
|
423
|
+
<div className="mb-4">
|
|
424
|
+
<i className="bi-x-circle-fill text-danger display-3" />
|
|
425
|
+
</div>
|
|
426
|
+
|
|
427
|
+
<h5 className="mb-4">Connection Failed</h5>
|
|
428
|
+
|
|
429
|
+
<div className="alert alert-danger" style={{ maxWidth: '400px', margin: '0 auto 1rem' }}>
|
|
430
|
+
{error}
|
|
431
|
+
</div>
|
|
432
|
+
|
|
433
|
+
<button className="btn btn-primary" onClick={handleReset}>
|
|
434
|
+
Try Again
|
|
435
|
+
</button>
|
|
436
|
+
</div>
|
|
437
|
+
)}
|
|
438
|
+
</div>
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
registerInternalPeersUI({
|
|
443
|
+
peersUIId: '000user00connect0screen01',
|
|
444
|
+
component: UserConnect,
|
|
445
|
+
routes: [
|
|
446
|
+
{
|
|
447
|
+
isMatch: (props, context) => context.path === 'contacts/connect',
|
|
448
|
+
uiCategory: 'screen',
|
|
449
|
+
priority: 3
|
|
450
|
+
}
|
|
451
|
+
]
|
|
452
|
+
});
|
|
@@ -3,6 +3,25 @@ import { TrustLevel } from '@peers-app/peers-sdk';
|
|
|
3
3
|
import { LoadingIndicator } from '../../components/loading-indicator';
|
|
4
4
|
import { TrustLevelBadge } from '../../components/trust-level-badge';
|
|
5
5
|
|
|
6
|
+
/** Format bytes to human-readable string (KB, MB, GB) */
|
|
7
|
+
function formatBytes(bytes: number): string {
|
|
8
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
9
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
10
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
11
|
+
return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Format transfer rate to human-readable string (KB/s or MB/s) */
|
|
15
|
+
function formatRate(mbps: number | undefined): string {
|
|
16
|
+
if (mbps === undefined || mbps === 0) return '0 KB/s';
|
|
17
|
+
// Show KB/s if less than 0.1 MB/s (100 KB/s)
|
|
18
|
+
if (mbps < 0.1) {
|
|
19
|
+
const kbps = mbps * 1024;
|
|
20
|
+
return `${kbps.toFixed(1)} KB/s`;
|
|
21
|
+
}
|
|
22
|
+
return `${mbps.toFixed(2)} MB/s`;
|
|
23
|
+
}
|
|
24
|
+
|
|
6
25
|
// TypeScript type definitions (matching backend)
|
|
7
26
|
interface IDeviceDetails {
|
|
8
27
|
deviceId: string;
|
|
@@ -30,6 +49,11 @@ interface IDeviceDetails {
|
|
|
30
49
|
isPreferred: boolean;
|
|
31
50
|
};
|
|
32
51
|
};
|
|
52
|
+
// Throughput stats
|
|
53
|
+
bytesSent?: number;
|
|
54
|
+
bytesReceived?: number;
|
|
55
|
+
sendRateMBps?: number;
|
|
56
|
+
receiveRateMBps?: number;
|
|
33
57
|
}
|
|
34
58
|
|
|
35
59
|
interface IDeviceDetailsModalProps {
|
|
@@ -190,6 +214,37 @@ export function DeviceDetailsModal({ deviceId, onClose, onDisconnect }: IDeviceD
|
|
|
190
214
|
</div>
|
|
191
215
|
</div>
|
|
192
216
|
|
|
217
|
+
{/* Throughput Stats */}
|
|
218
|
+
<div className="mb-4">
|
|
219
|
+
<h6 className="border-bottom pb-2 mb-3">Throughput</h6>
|
|
220
|
+
<div className="row">
|
|
221
|
+
<div className="col-md-6 mb-3">
|
|
222
|
+
<strong>Send Rate:</strong>
|
|
223
|
+
<br />
|
|
224
|
+
<span className="text-success">
|
|
225
|
+
↑ {formatRate(device.sendRateMBps)}
|
|
226
|
+
</span>
|
|
227
|
+
</div>
|
|
228
|
+
<div className="col-md-6 mb-3">
|
|
229
|
+
<strong>Receive Rate:</strong>
|
|
230
|
+
<br />
|
|
231
|
+
<span className="text-primary">
|
|
232
|
+
↓ {formatRate(device.receiveRateMBps)}
|
|
233
|
+
</span>
|
|
234
|
+
</div>
|
|
235
|
+
<div className="col-md-6 mb-3">
|
|
236
|
+
<strong>Total Sent:</strong>
|
|
237
|
+
<br />
|
|
238
|
+
<span>{formatBytes(device.bytesSent || 0)}</span>
|
|
239
|
+
</div>
|
|
240
|
+
<div className="col-md-6 mb-3">
|
|
241
|
+
<strong>Total Received:</strong>
|
|
242
|
+
<br />
|
|
243
|
+
<span>{formatBytes(device.bytesReceived || 0)}</span>
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
|
|
193
248
|
{/* Shared Groups */}
|
|
194
249
|
{device.sharedGroups.length > 0 && (
|
|
195
250
|
<div className="mb-4">
|