@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
|
@@ -8,8 +8,8 @@ export interface Command {
|
|
|
8
8
|
action: () => void;
|
|
9
9
|
isAvailable?: () => boolean;
|
|
10
10
|
}
|
|
11
|
-
export declare const isCommandPaletteOpen: import("@peers-app/peers-sdk").
|
|
12
|
-
export declare const commandSearchQuery: import("@peers-app/peers-sdk").
|
|
11
|
+
export declare const isCommandPaletteOpen: import("@peers-app/peers-sdk").Observable<boolean>;
|
|
12
|
+
export declare const commandSearchQuery: import("@peers-app/peers-sdk").Observable<string>;
|
|
13
13
|
export declare function registerCommand(command: Command): void;
|
|
14
14
|
export declare function unregisterCommand(commandId: string): void;
|
|
15
15
|
export declare function getCommand(commandId: string): Command | undefined;
|
|
@@ -47,13 +47,9 @@ exports.initializeCommandPalette = initializeCommandPalette;
|
|
|
47
47
|
exports.destroyCommandPalette = destroyCommandPalette;
|
|
48
48
|
const peers_sdk_1 = require("@peers-app/peers-sdk");
|
|
49
49
|
const tabs_state_1 = require("../tabs-layout/tabs-state");
|
|
50
|
-
// Command palette state (
|
|
51
|
-
exports.isCommandPaletteOpen = (0, peers_sdk_1.
|
|
52
|
-
|
|
53
|
-
});
|
|
54
|
-
exports.commandSearchQuery = (0, peers_sdk_1.deviceVar)('commandSearchQuery', {
|
|
55
|
-
defaultValue: '',
|
|
56
|
-
});
|
|
50
|
+
// Command palette state (in-memory only, no persistence needed)
|
|
51
|
+
exports.isCommandPaletteOpen = (0, peers_sdk_1.observable)(false);
|
|
52
|
+
exports.commandSearchQuery = (0, peers_sdk_1.observable)('');
|
|
57
53
|
// Command registry
|
|
58
54
|
const registeredCommands = new Map();
|
|
59
55
|
// Detect platform for keyboard shortcuts
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
interface GroupSwitcherProps {
|
|
3
3
|
colorMode: string;
|
|
4
|
+
isMobile?: boolean;
|
|
4
5
|
}
|
|
5
|
-
export declare function GroupSwitcher({ colorMode }: GroupSwitcherProps): React.JSX.Element;
|
|
6
|
+
export declare function GroupSwitcher({ colorMode, isMobile }: GroupSwitcherProps): React.JSX.Element;
|
|
6
7
|
export {};
|
|
@@ -37,7 +37,7 @@ exports.GroupSwitcher = GroupSwitcher;
|
|
|
37
37
|
const peers_sdk_1 = require("@peers-app/peers-sdk");
|
|
38
38
|
const react_1 = __importStar(require("react"));
|
|
39
39
|
const hooks_1 = require("../hooks");
|
|
40
|
-
function GroupSwitcher({ colorMode }) {
|
|
40
|
+
function GroupSwitcher({ colorMode, isMobile = false }) {
|
|
41
41
|
const [showDropdown, setShowDropdown] = (0, react_1.useState)(false);
|
|
42
42
|
const [showCreateModal, setShowCreateModal] = (0, react_1.useState)(false);
|
|
43
43
|
const [allGroups, setAllGroups] = (0, react_1.useState)([]);
|
|
@@ -84,13 +84,14 @@ function GroupSwitcher({ colorMode }) {
|
|
|
84
84
|
const isDark = colorMode === 'dark';
|
|
85
85
|
return (react_1.default.createElement(react_1.default.Fragment, null,
|
|
86
86
|
react_1.default.createElement("div", { className: "dropdown" },
|
|
87
|
-
react_1.default.createElement("button", { className:
|
|
88
|
-
padding: '4px 8px',
|
|
87
|
+
react_1.default.createElement("button", { className: `btn btn-sm ${isMobile ? '' : 'me-2'} d-flex align-items-center`, onClick: () => setShowDropdown(!showDropdown), title: `Current group: ${getGroupName(currentGroup)}`, style: {
|
|
88
|
+
padding: isMobile ? '4px' : '4px 8px',
|
|
89
89
|
fontSize: '12px',
|
|
90
90
|
borderRadius: '6px',
|
|
91
91
|
border: 'none',
|
|
92
92
|
background: 'transparent',
|
|
93
|
-
color: isDark ? '#adb5bd' : '#6c757d'
|
|
93
|
+
color: isDark ? '#adb5bd' : '#6c757d',
|
|
94
|
+
minWidth: isMobile ? '36px' : undefined
|
|
94
95
|
}, onMouseEnter: (e) => {
|
|
95
96
|
e.currentTarget.style.backgroundColor = isDark ? '#495057' : '#f8f9fa';
|
|
96
97
|
e.currentTarget.style.color = isDark ? '#ffffff' : '#0d6efd';
|
|
@@ -98,8 +99,8 @@ function GroupSwitcher({ colorMode }) {
|
|
|
98
99
|
e.currentTarget.style.backgroundColor = 'transparent';
|
|
99
100
|
e.currentTarget.style.color = isDark ? '#adb5bd' : '#6c757d';
|
|
100
101
|
} },
|
|
101
|
-
react_1.default.createElement("i", { className: `${getGroupIcon(currentGroup)} me-1`, style: { fontSize: '14px' } }),
|
|
102
|
-
react_1.default.createElement("span", { className: "text-truncate", style: { maxWidth: '80px' } }, getGroupName(currentGroup)),
|
|
102
|
+
react_1.default.createElement("i", { className: `${getGroupIcon(currentGroup)} ${isMobile ? '' : 'me-1'}`, style: { fontSize: '14px' } }),
|
|
103
|
+
!isMobile && (react_1.default.createElement("span", { className: "text-truncate", style: { maxWidth: '80px' } }, getGroupName(currentGroup))),
|
|
103
104
|
react_1.default.createElement("i", { className: "bi-chevron-down ms-1", style: { fontSize: '10px' } })),
|
|
104
105
|
showDropdown && (react_1.default.createElement("div", { className: `dropdown-menu show position-absolute ${isDark ? 'dropdown-menu-dark' : ''}`, style: {
|
|
105
106
|
left: 0,
|
package/dist/globals.d.ts
CHANGED
|
@@ -14,8 +14,8 @@ export declare const isDesktop: import("@peers-app/peers-sdk").Observable<boolea
|
|
|
14
14
|
export declare const groups: import("@peers-app/peers-sdk").Observable<{
|
|
15
15
|
name: string;
|
|
16
16
|
description: string;
|
|
17
|
-
signature: string;
|
|
18
17
|
publicKey: string;
|
|
18
|
+
signature: string;
|
|
19
19
|
publicBoxKey: string;
|
|
20
20
|
groupId: string;
|
|
21
21
|
founderUserId: string;
|
|
@@ -142,7 +142,10 @@ function ContactList() {
|
|
|
142
142
|
react_1.default.createElement("div", { className: "d-flex justify-content-between align-items-center mb-3" },
|
|
143
143
|
react_1.default.createElement("h4", null,
|
|
144
144
|
react_1.default.createElement("i", { className: "bi-person-fill-check me-2" }),
|
|
145
|
-
"Contacts")
|
|
145
|
+
"Contacts"),
|
|
146
|
+
react_1.default.createElement("a", { href: "#contacts/connect", className: "btn btn-primary btn-sm" },
|
|
147
|
+
react_1.default.createElement("i", { className: "bi-person-plus me-1" }),
|
|
148
|
+
"Connect to New User")),
|
|
146
149
|
react_1.default.createElement("div", { className: "input-group mb-3" },
|
|
147
150
|
react_1.default.createElement(input_1.Input, { value: searchTextObs, className: "form-control", placeholder: "Search or create contacts", autoFocus: true, onKeyUp: handleSearchSubmit })),
|
|
148
151
|
react_1.default.createElement("div", { className: "peers-list-container" },
|
|
@@ -17,5 +17,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
17
17
|
// Import all contact screen components to ensure they register their routes
|
|
18
18
|
require("./contact-list");
|
|
19
19
|
require("./contact-details");
|
|
20
|
+
require("./user-connect");
|
|
20
21
|
__exportStar(require("./contact-list"), exports);
|
|
21
22
|
__exportStar(require("./contact-details"), exports);
|
|
23
|
+
__exportStar(require("./user-connect"), exports);
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.UserConnect = UserConnect;
|
|
37
|
+
const peers_sdk_1 = require("@peers-app/peers-sdk");
|
|
38
|
+
const react_1 = __importStar(require("react"));
|
|
39
|
+
const globals_1 = require("../../globals");
|
|
40
|
+
const hooks_1 = require("../../hooks");
|
|
41
|
+
const ui_loader_1 = require("../../ui-router/ui-loader");
|
|
42
|
+
function UserConnect() {
|
|
43
|
+
const [mode, setMode] = (0, react_1.useState)('select');
|
|
44
|
+
const [status, setStatus] = (0, react_1.useState)('idle');
|
|
45
|
+
const [connectionCode, setConnectionCode] = (0, react_1.useState)('');
|
|
46
|
+
const [inputCode, setInputCode] = (0, react_1.useState)('');
|
|
47
|
+
const [result, setResult] = (0, react_1.useState)(null);
|
|
48
|
+
const [error, setError] = (0, react_1.useState)('');
|
|
49
|
+
const [copied, setCopied] = (0, react_1.useState)(false);
|
|
50
|
+
// Subscribe to userConnectStatus from the device layer
|
|
51
|
+
const [connectStatus] = (0, hooks_1.useObservable)(peers_sdk_1.userConnectStatus);
|
|
52
|
+
// Also set up a direct subscription after loading is complete
|
|
53
|
+
(0, react_1.useEffect)(() => {
|
|
54
|
+
let disposed = false;
|
|
55
|
+
let subscription;
|
|
56
|
+
peers_sdk_1.userConnectStatus.loadingPromise.then(() => {
|
|
57
|
+
if (disposed)
|
|
58
|
+
return;
|
|
59
|
+
subscription = peers_sdk_1.userConnectStatus.subscribe(() => {
|
|
60
|
+
// Force a re-render by checking the current value
|
|
61
|
+
const currentStatus = (0, peers_sdk_1.userConnectStatus)();
|
|
62
|
+
if (currentStatus && typeof currentStatus === 'string') {
|
|
63
|
+
handleConnectStatusChange(currentStatus);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
return () => {
|
|
68
|
+
disposed = true;
|
|
69
|
+
subscription?.dispose();
|
|
70
|
+
};
|
|
71
|
+
}, []);
|
|
72
|
+
// Handle connect status changes
|
|
73
|
+
const handleConnectStatusChange = (0, react_1.useCallback)(async (connectStatusValue) => {
|
|
74
|
+
if (!connectStatusValue || status === 'success')
|
|
75
|
+
return;
|
|
76
|
+
if (connectStatusValue.startsWith('Error:')) {
|
|
77
|
+
// Error status
|
|
78
|
+
setError(connectStatusValue.replace('Error: ', ''));
|
|
79
|
+
setStatus('error');
|
|
80
|
+
}
|
|
81
|
+
else if (connectStatusValue.length > 0) {
|
|
82
|
+
// Success - connectStatus is the remote userId
|
|
83
|
+
const remoteUserId = connectStatusValue;
|
|
84
|
+
try {
|
|
85
|
+
const userContext = await (0, peers_sdk_1.getUserContext)();
|
|
86
|
+
const me = await (0, peers_sdk_1.getMe)();
|
|
87
|
+
const remoteUser = await (0, peers_sdk_1.Users)(userContext.userDataContext).get(remoteUserId);
|
|
88
|
+
if (!remoteUser) {
|
|
89
|
+
setError('Could not find connected user');
|
|
90
|
+
setStatus('error');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const remoteDevice = await (0, peers_sdk_1.Devices)(userContext.userDataContext).findOne({ userId: remoteUserId });
|
|
94
|
+
if (!remoteDevice) {
|
|
95
|
+
setError('Could not find connected device but connection was successful');
|
|
96
|
+
setStatus('error');
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
// Build user connect info for confirmation hash
|
|
100
|
+
const myInfo = {
|
|
101
|
+
userId: me.userId,
|
|
102
|
+
publicKey: me.publicKey,
|
|
103
|
+
publicBoxKey: me.publicBoxKey,
|
|
104
|
+
deviceId: userContext.deviceId(),
|
|
105
|
+
};
|
|
106
|
+
const remoteInfo = {
|
|
107
|
+
userId: remoteUser.userId,
|
|
108
|
+
publicKey: remoteUser.publicKey,
|
|
109
|
+
publicBoxKey: remoteUser.publicBoxKey,
|
|
110
|
+
deviceId: remoteDevice.deviceId,
|
|
111
|
+
};
|
|
112
|
+
const confirmationHash = (0, peers_sdk_1.generateConfirmationHash)(myInfo, remoteInfo);
|
|
113
|
+
setResult({ remoteUser, confirmationHash });
|
|
114
|
+
setStatus('success');
|
|
115
|
+
// Clear the codes
|
|
116
|
+
(0, peers_sdk_1.userConnectCodeOffer)('');
|
|
117
|
+
(0, peers_sdk_1.userConnectCodeAnswer)('');
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
setError(err.message || 'Failed to complete connection');
|
|
121
|
+
setStatus('error');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}, [status]);
|
|
125
|
+
// React to userConnectStatus changes from useObservable
|
|
126
|
+
(0, react_1.useEffect)(() => {
|
|
127
|
+
if (connectStatus && typeof connectStatus === 'string') {
|
|
128
|
+
handleConnectStatusChange(connectStatus);
|
|
129
|
+
}
|
|
130
|
+
}, [connectStatus, handleConnectStatusChange]);
|
|
131
|
+
// Clean up on unmount
|
|
132
|
+
(0, react_1.useEffect)(() => {
|
|
133
|
+
return () => {
|
|
134
|
+
if (mode === 'initiate' && status === 'waiting') {
|
|
135
|
+
(0, peers_sdk_1.userConnectCodeOffer)('');
|
|
136
|
+
(0, peers_sdk_1.userConnectCodeAnswer)('');
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}, [mode, status]);
|
|
140
|
+
const handleInitiate = (0, react_1.useCallback)(async () => {
|
|
141
|
+
setMode('initiate');
|
|
142
|
+
setStatus('waiting');
|
|
143
|
+
setError('');
|
|
144
|
+
(0, peers_sdk_1.userConnectStatus)(''); // Clear any previous status
|
|
145
|
+
// Generate the connection code
|
|
146
|
+
const code = (0, peers_sdk_1.generateConnectionCode)();
|
|
147
|
+
(0, peers_sdk_1.userConnectCodeOffer)(code.code);
|
|
148
|
+
const formattedCode = (0, peers_sdk_1.formatConnectionCode)(code.code);
|
|
149
|
+
setConnectionCode(formattedCode);
|
|
150
|
+
}, []);
|
|
151
|
+
const handleRespond = (0, react_1.useCallback)(async () => {
|
|
152
|
+
if (inputCode.replace(/[^0-9A-Za-z]/g, '').length !== 12) {
|
|
153
|
+
setError('Please enter a valid 12-character connection code');
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
setStatus('waiting');
|
|
157
|
+
setError('');
|
|
158
|
+
(0, peers_sdk_1.userConnectStatus)(''); // Clear any previous status
|
|
159
|
+
(0, peers_sdk_1.userConnectCodeAnswer)(inputCode);
|
|
160
|
+
}, [inputCode]);
|
|
161
|
+
const handleCancel = (0, react_1.useCallback)(async () => {
|
|
162
|
+
(0, peers_sdk_1.userConnectCodeOffer)('');
|
|
163
|
+
(0, peers_sdk_1.userConnectCodeAnswer)('');
|
|
164
|
+
(0, peers_sdk_1.userConnectStatus)('');
|
|
165
|
+
setMode('select');
|
|
166
|
+
setStatus('idle');
|
|
167
|
+
setConnectionCode('');
|
|
168
|
+
setError('');
|
|
169
|
+
}, []);
|
|
170
|
+
const handleReset = (0, react_1.useCallback)(() => {
|
|
171
|
+
(0, peers_sdk_1.userConnectCodeOffer)('');
|
|
172
|
+
(0, peers_sdk_1.userConnectCodeAnswer)('');
|
|
173
|
+
(0, peers_sdk_1.userConnectStatus)('');
|
|
174
|
+
setMode('select');
|
|
175
|
+
setStatus('idle');
|
|
176
|
+
setConnectionCode('');
|
|
177
|
+
setInputCode('');
|
|
178
|
+
setResult(null);
|
|
179
|
+
setError('');
|
|
180
|
+
setCopied(false);
|
|
181
|
+
}, []);
|
|
182
|
+
const handleSaveContact = (0, react_1.useCallback)(async () => {
|
|
183
|
+
if (!result)
|
|
184
|
+
return;
|
|
185
|
+
try {
|
|
186
|
+
const userContext = await (0, peers_sdk_1.getUserContext)();
|
|
187
|
+
// Contact is already saved by the device layer, just set trust level
|
|
188
|
+
await (0, peers_sdk_1.setUserTrustLevel)(result.remoteUser.userId, peers_sdk_1.TrustLevel.Trusted, userContext.userDataContext);
|
|
189
|
+
// Navigate to contact details
|
|
190
|
+
(0, globals_1.mainContentPath)(`contacts/${result.remoteUser.userId}`);
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
setError(err.message || 'Failed to save contact');
|
|
194
|
+
}
|
|
195
|
+
}, [result]);
|
|
196
|
+
// Copy connection code to clipboard
|
|
197
|
+
const handleCopyCode = (0, react_1.useCallback)(async () => {
|
|
198
|
+
if (!connectionCode)
|
|
199
|
+
return;
|
|
200
|
+
try {
|
|
201
|
+
await navigator.clipboard.writeText(connectionCode);
|
|
202
|
+
setCopied(true);
|
|
203
|
+
setTimeout(() => setCopied(false), 2000);
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
// Fallback for older browsers
|
|
207
|
+
const textArea = document.createElement('textarea');
|
|
208
|
+
textArea.value = connectionCode;
|
|
209
|
+
document.body.appendChild(textArea);
|
|
210
|
+
textArea.select();
|
|
211
|
+
document.execCommand('copy');
|
|
212
|
+
document.body.removeChild(textArea);
|
|
213
|
+
setCopied(true);
|
|
214
|
+
setTimeout(() => setCopied(false), 2000);
|
|
215
|
+
}
|
|
216
|
+
}, [connectionCode]);
|
|
217
|
+
// Format input code as user types
|
|
218
|
+
const handleCodeInput = (value) => {
|
|
219
|
+
// Remove non-alphanumeric characters
|
|
220
|
+
const cleaned = value.toUpperCase().replace(/[^0-9A-Z]/g, '');
|
|
221
|
+
// Format as XXXX-YYYY-ZZZZ
|
|
222
|
+
let formatted = '';
|
|
223
|
+
for (let i = 0; i < cleaned.length && i < 12; i++) {
|
|
224
|
+
if (i === 4 || i === 8)
|
|
225
|
+
formatted += '-';
|
|
226
|
+
formatted += cleaned[i];
|
|
227
|
+
}
|
|
228
|
+
setInputCode(formatted);
|
|
229
|
+
};
|
|
230
|
+
return (react_1.default.createElement("div", { className: "container-fluid p-3" },
|
|
231
|
+
react_1.default.createElement("div", { className: "d-flex justify-content-between align-items-center mb-4" },
|
|
232
|
+
react_1.default.createElement("h4", null,
|
|
233
|
+
react_1.default.createElement("i", { className: "bi-person-plus-fill me-2" }),
|
|
234
|
+
"Connect to New User"),
|
|
235
|
+
mode !== 'select' && (react_1.default.createElement("button", { className: "btn btn-outline-secondary btn-sm", onClick: handleReset },
|
|
236
|
+
react_1.default.createElement("i", { className: "bi-arrow-left me-1" }),
|
|
237
|
+
"Back"))),
|
|
238
|
+
mode === 'select' && (react_1.default.createElement("div", { className: "row g-3" },
|
|
239
|
+
react_1.default.createElement("div", { className: "col-md-6" },
|
|
240
|
+
react_1.default.createElement("div", { className: "card h-100 border-primary", style: { cursor: 'pointer' }, onClick: handleInitiate },
|
|
241
|
+
react_1.default.createElement("div", { className: "card-body text-center p-4" },
|
|
242
|
+
react_1.default.createElement("i", { className: "bi-qr-code display-4 text-primary mb-3" }),
|
|
243
|
+
react_1.default.createElement("h5", { className: "card-title" }, "Create Connection Code"),
|
|
244
|
+
react_1.default.createElement("p", { className: "card-text text-muted" }, "Generate a code to share with someone who wants to connect with you")))),
|
|
245
|
+
react_1.default.createElement("div", { className: "col-md-6" },
|
|
246
|
+
react_1.default.createElement("div", { className: "card h-100 border-success", style: { cursor: 'pointer' }, onClick: () => setMode('respond') },
|
|
247
|
+
react_1.default.createElement("div", { className: "card-body text-center p-4" },
|
|
248
|
+
react_1.default.createElement("i", { className: "bi-keyboard display-4 text-success mb-3" }),
|
|
249
|
+
react_1.default.createElement("h5", { className: "card-title" }, "Enter Connection Code"),
|
|
250
|
+
react_1.default.createElement("p", { className: "card-text text-muted" }, "Enter a code that someone shared with you to connect")))))),
|
|
251
|
+
mode === 'initiate' && status === 'waiting' && (react_1.default.createElement("div", { className: "text-center" },
|
|
252
|
+
react_1.default.createElement("div", { className: "mb-4" },
|
|
253
|
+
react_1.default.createElement("p", { className: "text-muted" }, "Share this code with the person you want to connect with:")),
|
|
254
|
+
react_1.default.createElement("div", { className: "mb-4", style: { maxWidth: '400px', margin: '0 auto' } },
|
|
255
|
+
react_1.default.createElement("div", { className: "input-group" },
|
|
256
|
+
react_1.default.createElement("input", { type: "text", className: "form-control form-control-lg text-center font-monospace", value: connectionCode || 'XXXX-YYYY-ZZZZ', readOnly: true, style: { letterSpacing: '0.15em', fontSize: '1.5rem' } }),
|
|
257
|
+
react_1.default.createElement("button", { className: "btn btn-outline-primary", onClick: handleCopyCode, title: "Copy to clipboard" },
|
|
258
|
+
react_1.default.createElement("i", { className: copied ? "bi-check-lg" : "bi-clipboard" }))),
|
|
259
|
+
copied && (react_1.default.createElement("small", { className: "text-success mt-1 d-block" }, "Copied!"))),
|
|
260
|
+
react_1.default.createElement("div", { className: "mb-4" },
|
|
261
|
+
react_1.default.createElement("div", { className: "spinner-border spinner-border-sm text-primary me-2", role: "status" }),
|
|
262
|
+
react_1.default.createElement("span", { className: "text-muted" }, "Waiting for connection...")),
|
|
263
|
+
react_1.default.createElement("p", { className: "text-muted small" }, "This code will expire in 10 minutes"),
|
|
264
|
+
react_1.default.createElement("button", { className: "btn btn-outline-secondary", onClick: handleCancel }, "Cancel"))),
|
|
265
|
+
mode === 'respond' && status !== 'success' && (react_1.default.createElement("div", { className: "text-center" },
|
|
266
|
+
react_1.default.createElement("div", { className: "mb-4" },
|
|
267
|
+
react_1.default.createElement("p", { className: "text-muted" }, "Enter the connection code shared with you:")),
|
|
268
|
+
react_1.default.createElement("div", { className: "mb-4", style: { maxWidth: '400px', margin: '0 auto' } },
|
|
269
|
+
react_1.default.createElement("input", { type: "text", className: "form-control form-control-lg text-center font-monospace", placeholder: "XXXX-YYYY-ZZZZ", value: inputCode, onChange: (e) => handleCodeInput(e.target.value), maxLength: 14, style: { letterSpacing: '0.15em', fontSize: '1.5rem' }, autoFocus: true })),
|
|
270
|
+
error && (react_1.default.createElement("div", { className: "alert alert-danger", style: { maxWidth: '400px', margin: '0 auto 1rem' } }, error)),
|
|
271
|
+
react_1.default.createElement("button", { className: "btn btn-primary btn-lg", onClick: handleRespond, disabled: status === 'waiting' || inputCode.replace(/[^0-9A-Z]/gi, '').length !== 12 }, status === 'waiting' ? (react_1.default.createElement(react_1.default.Fragment, null,
|
|
272
|
+
react_1.default.createElement("span", { className: "spinner-border spinner-border-sm me-2", role: "status" }),
|
|
273
|
+
"Connecting...")) : (react_1.default.createElement(react_1.default.Fragment, null,
|
|
274
|
+
react_1.default.createElement("i", { className: "bi-link-45deg me-2" }),
|
|
275
|
+
"Connect"))))),
|
|
276
|
+
status === 'success' && result && (react_1.default.createElement("div", { className: "text-center" },
|
|
277
|
+
react_1.default.createElement("div", { className: "mb-4" },
|
|
278
|
+
react_1.default.createElement("i", { className: "bi-check-circle-fill text-success display-3" })),
|
|
279
|
+
react_1.default.createElement("h5", { className: "mb-4" }, "Connection Successful!"),
|
|
280
|
+
react_1.default.createElement("div", { className: "card mb-4", style: { maxWidth: '400px', margin: '0 auto' } },
|
|
281
|
+
react_1.default.createElement("div", { className: "card-body" },
|
|
282
|
+
react_1.default.createElement("p", { className: "text-muted mb-2" }, "Verify with the other person that you both see:"),
|
|
283
|
+
react_1.default.createElement("h3", { className: "font-monospace text-primary mb-3", style: { letterSpacing: '0.2em' } }, result.confirmationHash),
|
|
284
|
+
react_1.default.createElement("hr", null),
|
|
285
|
+
react_1.default.createElement("div", { className: "text-start" },
|
|
286
|
+
react_1.default.createElement("small", { className: "text-muted" }, "Name:"),
|
|
287
|
+
react_1.default.createElement("p", { className: "mb-2" }, result.remoteUser.name),
|
|
288
|
+
react_1.default.createElement("small", { className: "text-muted" }, "User ID:"),
|
|
289
|
+
react_1.default.createElement("p", { className: "font-monospace small mb-0" }, result.remoteUser.userId)))),
|
|
290
|
+
react_1.default.createElement("div", { className: "d-flex gap-2 justify-content-center" },
|
|
291
|
+
react_1.default.createElement("button", { className: "btn btn-primary", onClick: handleSaveContact },
|
|
292
|
+
react_1.default.createElement("i", { className: "bi-check-lg me-2" }),
|
|
293
|
+
"Trust & View Contact"),
|
|
294
|
+
react_1.default.createElement("button", { className: "btn btn-outline-secondary", onClick: handleReset }, "Connect Another")))),
|
|
295
|
+
status === 'error' && (react_1.default.createElement("div", { className: "text-center" },
|
|
296
|
+
react_1.default.createElement("div", { className: "mb-4" },
|
|
297
|
+
react_1.default.createElement("i", { className: "bi-x-circle-fill text-danger display-3" })),
|
|
298
|
+
react_1.default.createElement("h5", { className: "mb-4" }, "Connection Failed"),
|
|
299
|
+
react_1.default.createElement("div", { className: "alert alert-danger", style: { maxWidth: '400px', margin: '0 auto 1rem' } }, error),
|
|
300
|
+
react_1.default.createElement("button", { className: "btn btn-primary", onClick: handleReset }, "Try Again")))));
|
|
301
|
+
}
|
|
302
|
+
(0, ui_loader_1.registerInternalPeersUI)({
|
|
303
|
+
peersUIId: '000user00connect0screen01',
|
|
304
|
+
component: UserConnect,
|
|
305
|
+
routes: [
|
|
306
|
+
{
|
|
307
|
+
isMatch: (props, context) => context.path === 'contacts/connect',
|
|
308
|
+
uiCategory: 'screen',
|
|
309
|
+
priority: 3
|
|
310
|
+
}
|
|
311
|
+
]
|
|
312
|
+
});
|
|
@@ -38,6 +38,27 @@ const react_1 = __importStar(require("react"));
|
|
|
38
38
|
const peers_sdk_1 = require("@peers-app/peers-sdk");
|
|
39
39
|
const loading_indicator_1 = require("../../components/loading-indicator");
|
|
40
40
|
const trust_level_badge_1 = require("../../components/trust-level-badge");
|
|
41
|
+
/** Format bytes to human-readable string (KB, MB, GB) */
|
|
42
|
+
function formatBytes(bytes) {
|
|
43
|
+
if (bytes < 1024)
|
|
44
|
+
return `${bytes} B`;
|
|
45
|
+
if (bytes < 1024 * 1024)
|
|
46
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
47
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
48
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
49
|
+
return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`;
|
|
50
|
+
}
|
|
51
|
+
/** Format transfer rate to human-readable string (KB/s or MB/s) */
|
|
52
|
+
function formatRate(mbps) {
|
|
53
|
+
if (mbps === undefined || mbps === 0)
|
|
54
|
+
return '0 KB/s';
|
|
55
|
+
// Show KB/s if less than 0.1 MB/s (100 KB/s)
|
|
56
|
+
if (mbps < 0.1) {
|
|
57
|
+
const kbps = mbps * 1024;
|
|
58
|
+
return `${kbps.toFixed(1)} KB/s`;
|
|
59
|
+
}
|
|
60
|
+
return `${mbps.toFixed(2)} MB/s`;
|
|
61
|
+
}
|
|
41
62
|
function DeviceDetailsModal({ deviceId, onClose, onDisconnect }) {
|
|
42
63
|
const [device, setDevice] = (0, react_1.useState)(null);
|
|
43
64
|
const [loading, setLoading] = (0, react_1.useState)(true);
|
|
@@ -133,6 +154,29 @@ function DeviceDetailsModal({ deviceId, onClose, onDisconnect }) {
|
|
|
133
154
|
react_1.default.createElement("strong", null, "Path:"),
|
|
134
155
|
react_1.default.createElement("br", null),
|
|
135
156
|
react_1.default.createElement("small", { className: "text-muted" }, device.throughDeviceIds.join(' → ')))))),
|
|
157
|
+
react_1.default.createElement("div", { className: "mb-4" },
|
|
158
|
+
react_1.default.createElement("h6", { className: "border-bottom pb-2 mb-3" }, "Throughput"),
|
|
159
|
+
react_1.default.createElement("div", { className: "row" },
|
|
160
|
+
react_1.default.createElement("div", { className: "col-md-6 mb-3" },
|
|
161
|
+
react_1.default.createElement("strong", null, "Send Rate:"),
|
|
162
|
+
react_1.default.createElement("br", null),
|
|
163
|
+
react_1.default.createElement("span", { className: "text-success" },
|
|
164
|
+
"\u2191 ",
|
|
165
|
+
formatRate(device.sendRateMBps))),
|
|
166
|
+
react_1.default.createElement("div", { className: "col-md-6 mb-3" },
|
|
167
|
+
react_1.default.createElement("strong", null, "Receive Rate:"),
|
|
168
|
+
react_1.default.createElement("br", null),
|
|
169
|
+
react_1.default.createElement("span", { className: "text-primary" },
|
|
170
|
+
"\u2193 ",
|
|
171
|
+
formatRate(device.receiveRateMBps))),
|
|
172
|
+
react_1.default.createElement("div", { className: "col-md-6 mb-3" },
|
|
173
|
+
react_1.default.createElement("strong", null, "Total Sent:"),
|
|
174
|
+
react_1.default.createElement("br", null),
|
|
175
|
+
react_1.default.createElement("span", null, formatBytes(device.bytesSent || 0))),
|
|
176
|
+
react_1.default.createElement("div", { className: "col-md-6 mb-3" },
|
|
177
|
+
react_1.default.createElement("strong", null, "Total Received:"),
|
|
178
|
+
react_1.default.createElement("br", null),
|
|
179
|
+
react_1.default.createElement("span", null, formatBytes(device.bytesReceived || 0))))),
|
|
136
180
|
device.sharedGroups.length > 0 && (react_1.default.createElement("div", { className: "mb-4" },
|
|
137
181
|
react_1.default.createElement("h6", { className: "border-bottom pb-2 mb-3" },
|
|
138
182
|
"Shared Groups (",
|
|
@@ -40,8 +40,12 @@ const loading_indicator_1 = require("../../components/loading-indicator");
|
|
|
40
40
|
const trust_level_badge_1 = require("../../components/trust-level-badge");
|
|
41
41
|
function GroupDetailsModal({ groupId, onClose }) {
|
|
42
42
|
const [group, setGroup] = (0, react_1.useState)(null);
|
|
43
|
+
const [syncInfo, setSyncInfo] = (0, react_1.useState)(null);
|
|
43
44
|
const [loading, setLoading] = (0, react_1.useState)(true);
|
|
44
45
|
const [notFound, setNotFound] = (0, react_1.useState)(false);
|
|
46
|
+
const [downloading, setDownloading] = (0, react_1.useState)(false);
|
|
47
|
+
const [downloadProgress, setDownloadProgress] = (0, react_1.useState)(null);
|
|
48
|
+
const [downloadResult, setDownloadResult] = (0, react_1.useState)(null);
|
|
45
49
|
const loadData = async () => {
|
|
46
50
|
try {
|
|
47
51
|
const api = window.electronAPI?.networkViewer;
|
|
@@ -49,7 +53,10 @@ function GroupDetailsModal({ groupId, onClose }) {
|
|
|
49
53
|
console.warn('Network Viewer API not available');
|
|
50
54
|
return;
|
|
51
55
|
}
|
|
52
|
-
const groupData = await
|
|
56
|
+
const [groupData, syncData] = await Promise.all([
|
|
57
|
+
api.getGroupDetails(groupId),
|
|
58
|
+
api.getGroupSyncStatus(groupId)
|
|
59
|
+
]);
|
|
53
60
|
if (!groupData) {
|
|
54
61
|
setNotFound(true);
|
|
55
62
|
}
|
|
@@ -57,6 +64,9 @@ function GroupDetailsModal({ groupId, onClose }) {
|
|
|
57
64
|
setGroup(groupData);
|
|
58
65
|
setNotFound(false);
|
|
59
66
|
}
|
|
67
|
+
if (syncData && syncData.length > 0) {
|
|
68
|
+
setSyncInfo(syncData[0]);
|
|
69
|
+
}
|
|
60
70
|
}
|
|
61
71
|
catch (error) {
|
|
62
72
|
console.error('Error loading group details:', error);
|
|
@@ -66,6 +76,38 @@ function GroupDetailsModal({ groupId, onClose }) {
|
|
|
66
76
|
setLoading(false);
|
|
67
77
|
}
|
|
68
78
|
};
|
|
79
|
+
const handleDownloadDatabase = async (deviceId) => {
|
|
80
|
+
try {
|
|
81
|
+
setDownloading(true);
|
|
82
|
+
setDownloadProgress('Preparing download...');
|
|
83
|
+
setDownloadResult(null);
|
|
84
|
+
const api = window.electronAPI?.networkViewer;
|
|
85
|
+
if (!api?.downloadRemoteDatabase) {
|
|
86
|
+
setDownloadResult({
|
|
87
|
+
success: false,
|
|
88
|
+
message: 'Download API not available'
|
|
89
|
+
});
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
setDownloadProgress('Downloading database...');
|
|
93
|
+
const result = await api.downloadRemoteDatabase(groupId, deviceId);
|
|
94
|
+
setDownloadResult(result);
|
|
95
|
+
if (result.success) {
|
|
96
|
+
setDownloadProgress(null);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
console.error('Error downloading database:', error);
|
|
101
|
+
setDownloadResult({
|
|
102
|
+
success: false,
|
|
103
|
+
message: error?.message || 'Download failed'
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
setDownloading(false);
|
|
108
|
+
setDownloadProgress(null);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
69
111
|
(0, react_1.useEffect)(() => {
|
|
70
112
|
loadData();
|
|
71
113
|
}, [groupId]);
|
|
@@ -123,5 +165,41 @@ function GroupDetailsModal({ groupId, onClose }) {
|
|
|
123
165
|
react_1.default.createElement("td", null,
|
|
124
166
|
react_1.default.createElement("span", { className: "badge bg-primary" }, member.role)),
|
|
125
167
|
react_1.default.createElement("td", null,
|
|
126
|
-
react_1.default.createElement(trust_level_badge_1.TrustLevelBadge, { level: member.trustLevel || peers_sdk_1.TrustLevel.Unknown }))))))))))
|
|
168
|
+
react_1.default.createElement(trust_level_badge_1.TrustLevelBadge, { level: member.trustLevel || peers_sdk_1.TrustLevel.Unknown })))))))))),
|
|
169
|
+
react_1.default.createElement("div", { className: "mb-4" },
|
|
170
|
+
react_1.default.createElement("h6", { className: "border-bottom pb-2 mb-3" },
|
|
171
|
+
react_1.default.createElement("i", { className: "bi bi-download me-2" }),
|
|
172
|
+
"Download Database (Fast Sync)"),
|
|
173
|
+
react_1.default.createElement("p", { className: "text-muted small mb-3" }, "Download the entire database from a connected device. This is useful for fast initial sync on a new device. The database will be saved as a backup file."),
|
|
174
|
+
syncInfo && syncInfo.connectedDevices.length > 0 ? (react_1.default.createElement("div", null,
|
|
175
|
+
react_1.default.createElement("label", { className: "form-label" }, "Select a connected device:"),
|
|
176
|
+
react_1.default.createElement("div", { className: "d-flex flex-wrap gap-2" }, syncInfo.connectedDevices.map((device) => (react_1.default.createElement("button", { key: device.deviceId, className: "btn btn-outline-primary btn-sm", onClick: () => handleDownloadDatabase(device.deviceId), disabled: downloading }, downloading ? (react_1.default.createElement(react_1.default.Fragment, null,
|
|
177
|
+
react_1.default.createElement("span", { className: "spinner-border spinner-border-sm me-1", role: "status", "aria-hidden": "true" }),
|
|
178
|
+
"Downloading...")) : (react_1.default.createElement(react_1.default.Fragment, null,
|
|
179
|
+
react_1.default.createElement("i", { className: "bi bi-cloud-download me-1" }),
|
|
180
|
+
device.deviceId.substring(0, 8),
|
|
181
|
+
"...",
|
|
182
|
+
react_1.default.createElement("span", { className: "badge bg-secondary ms-1" },
|
|
183
|
+
Math.round(device.latencyMs),
|
|
184
|
+
"ms"))))))))) : (react_1.default.createElement("div", { className: "alert alert-info mb-0" },
|
|
185
|
+
react_1.default.createElement("i", { className: "bi bi-info-circle me-2" }),
|
|
186
|
+
"No devices are currently connected to this group. Connect to a device first to download its database.")),
|
|
187
|
+
downloadProgress && (react_1.default.createElement("div", { className: "mt-3" },
|
|
188
|
+
react_1.default.createElement("div", { className: "d-flex align-items-center" },
|
|
189
|
+
react_1.default.createElement("span", { className: "spinner-border spinner-border-sm me-2", role: "status", "aria-hidden": "true" }),
|
|
190
|
+
react_1.default.createElement("span", null, downloadProgress)))),
|
|
191
|
+
downloadResult && (react_1.default.createElement("div", { className: `alert mt-3 mb-0 ${downloadResult.success ? 'alert-success' : 'alert-danger'}` }, downloadResult.success ? (react_1.default.createElement(react_1.default.Fragment, null,
|
|
192
|
+
react_1.default.createElement("i", { className: "bi bi-check-circle me-2" }),
|
|
193
|
+
react_1.default.createElement("strong", null, "Download complete!"),
|
|
194
|
+
react_1.default.createElement("br", null),
|
|
195
|
+
react_1.default.createElement("small", null,
|
|
196
|
+
"File: ",
|
|
197
|
+
react_1.default.createElement("code", null, downloadResult.filePath),
|
|
198
|
+
react_1.default.createElement("br", null),
|
|
199
|
+
"Size: ",
|
|
200
|
+
downloadResult.size ? `${(downloadResult.size / (1024 * 1024)).toFixed(2)} MB` : 'Unknown'))) : (react_1.default.createElement(react_1.default.Fragment, null,
|
|
201
|
+
react_1.default.createElement("i", { className: "bi bi-exclamation-circle me-2" }),
|
|
202
|
+
react_1.default.createElement("strong", null, "Download failed:"),
|
|
203
|
+
" ",
|
|
204
|
+
downloadResult.message)))))))))));
|
|
127
205
|
}
|