@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
|
@@ -43,6 +43,27 @@ const globals_1 = require("../../globals");
|
|
|
43
43
|
const device_details_modal_1 = require("./device-details-modal");
|
|
44
44
|
const group_details_modal_1 = require("./group-details-modal");
|
|
45
45
|
const usage_graph_1 = require("./usage-graph");
|
|
46
|
+
/** Format bytes to human-readable string (KB, MB, GB) */
|
|
47
|
+
function formatBytes(bytes) {
|
|
48
|
+
if (bytes < 1024)
|
|
49
|
+
return `${bytes} B`;
|
|
50
|
+
if (bytes < 1024 * 1024)
|
|
51
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
52
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
53
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
54
|
+
return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`;
|
|
55
|
+
}
|
|
56
|
+
/** Format transfer rate to human-readable string (KB/s or MB/s) */
|
|
57
|
+
function formatRate(mbps) {
|
|
58
|
+
if (mbps === undefined || mbps === 0)
|
|
59
|
+
return '0 KB/s';
|
|
60
|
+
// Show KB/s if less than 0.1 MB/s (100 KB/s)
|
|
61
|
+
if (mbps < 0.1) {
|
|
62
|
+
const kbps = mbps * 1024;
|
|
63
|
+
return `${kbps.toFixed(1)} KB/s`;
|
|
64
|
+
}
|
|
65
|
+
return `${mbps.toFixed(2)} MB/s`;
|
|
66
|
+
}
|
|
46
67
|
function NetworkViewerList() {
|
|
47
68
|
const [overview, setOverview] = (0, react_1.useState)(null);
|
|
48
69
|
const [connections, setConnections] = (0, react_1.useState)([]);
|
|
@@ -238,22 +259,6 @@ function NetworkViewerList() {
|
|
|
238
259
|
{ maxValue: 80, color: 'rgba(255, 193, 7, 1)' },
|
|
239
260
|
{ maxValue: Infinity, color: 'rgba(220, 53, 69, 1)' }
|
|
240
261
|
] }))))),
|
|
241
|
-
overview.groups.length > 0 && (react_1.default.createElement("div", { className: "card mb-4" },
|
|
242
|
-
react_1.default.createElement("div", { className: "card-body" },
|
|
243
|
-
react_1.default.createElement("h5", { className: "card-title" },
|
|
244
|
-
"Groups: ",
|
|
245
|
-
overview.groups.length),
|
|
246
|
-
react_1.default.createElement("div", { className: "table-responsive" },
|
|
247
|
-
react_1.default.createElement("table", { className: "table table-sm" },
|
|
248
|
-
react_1.default.createElement("thead", null,
|
|
249
|
-
react_1.default.createElement("tr", null,
|
|
250
|
-
react_1.default.createElement("th", null, "Group Name"),
|
|
251
|
-
react_1.default.createElement("th", null, "Connected Devices"),
|
|
252
|
-
react_1.default.createElement("th", null, "Total Members"))),
|
|
253
|
-
react_1.default.createElement("tbody", null, overview.groups.map(group => (react_1.default.createElement("tr", { key: group.groupId },
|
|
254
|
-
react_1.default.createElement("td", null, group.groupName),
|
|
255
|
-
react_1.default.createElement("td", null, group.connectedDevices),
|
|
256
|
-
react_1.default.createElement("td", null, group.totalMembers)))))))))),
|
|
257
262
|
react_1.default.createElement("div", { className: "card mb-4" },
|
|
258
263
|
react_1.default.createElement("div", { className: "card-body" },
|
|
259
264
|
react_1.default.createElement("h5", { className: "card-title" }, "Active Connections"),
|
|
@@ -270,6 +275,7 @@ function NetworkViewerList() {
|
|
|
270
275
|
react_1.default.createElement("th", null, "Shared Groups"),
|
|
271
276
|
react_1.default.createElement("th", null, "Latency"),
|
|
272
277
|
react_1.default.createElement("th", null, "Error Rate"),
|
|
278
|
+
react_1.default.createElement("th", null, "Throughput"),
|
|
273
279
|
react_1.default.createElement("th", null, "Actions"))),
|
|
274
280
|
react_1.default.createElement("tbody", null, connections.map(conn => {
|
|
275
281
|
return (react_1.default.createElement("tr", { key: conn.deviceId },
|
|
@@ -300,6 +306,20 @@ function NetworkViewerList() {
|
|
|
300
306
|
conn.errorRate < 0.2 ? 'warning' : 'danger'}` },
|
|
301
307
|
(conn.errorRate * 100).toFixed(1),
|
|
302
308
|
"%")),
|
|
309
|
+
react_1.default.createElement("td", null,
|
|
310
|
+
react_1.default.createElement("small", null,
|
|
311
|
+
react_1.default.createElement("span", { className: "text-success" },
|
|
312
|
+
"\u2191",
|
|
313
|
+
formatRate(conn.sendRateMBps)),
|
|
314
|
+
' / ',
|
|
315
|
+
react_1.default.createElement("span", { className: "text-primary" },
|
|
316
|
+
"\u2193",
|
|
317
|
+
formatRate(conn.receiveRateMBps))),
|
|
318
|
+
react_1.default.createElement("br", null),
|
|
319
|
+
react_1.default.createElement("small", { className: "text-muted" },
|
|
320
|
+
formatBytes(conn.bytesSent || 0),
|
|
321
|
+
" / ",
|
|
322
|
+
formatBytes(conn.bytesReceived || 0))),
|
|
303
323
|
react_1.default.createElement("td", null,
|
|
304
324
|
react_1.default.createElement("button", { className: "btn btn-sm btn-outline-primary me-2", onClick: () => handleViewDetails(conn.deviceId), title: "View Details" },
|
|
305
325
|
react_1.default.createElement("i", { className: "bi bi-info-circle" })),
|
|
@@ -34,12 +34,12 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.SettingsPage = void 0;
|
|
37
|
-
const react_1 = __importStar(require("react"));
|
|
38
37
|
const peers_sdk_1 = require("@peers-app/peers-sdk");
|
|
39
|
-
const
|
|
40
|
-
const color_mode_dropdown_1 = require("./color-mode-dropdown");
|
|
38
|
+
const react_1 = __importStar(require("react"));
|
|
41
39
|
const input_1 = require("../../components/input");
|
|
40
|
+
const tooltip_1 = require("../../components/tooltip");
|
|
42
41
|
const hooks_1 = require("../../hooks");
|
|
42
|
+
const color_mode_dropdown_1 = require("./color-mode-dropdown");
|
|
43
43
|
const SettingsPage = () => {
|
|
44
44
|
return (react_1.default.createElement("div", { className: 'container-fluid' },
|
|
45
45
|
react_1.default.createElement(color_mode_dropdown_1.ColorModeDropdown, null),
|
|
@@ -71,13 +71,19 @@ const DeviceName = () => {
|
|
|
71
71
|
}, [deviceId]);
|
|
72
72
|
const handleSave = async () => {
|
|
73
73
|
try {
|
|
74
|
-
if (!deviceId)
|
|
74
|
+
if (!deviceId) {
|
|
75
|
+
console.error('No device id');
|
|
75
76
|
return;
|
|
77
|
+
}
|
|
76
78
|
const userContext = await (0, peers_sdk_1.getUserContext)();
|
|
77
79
|
const devicesTable = (0, peers_sdk_1.Devices)(userContext.userDataContext);
|
|
78
|
-
const device = await devicesTable.get(deviceId)
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
const device = await devicesTable.get(deviceId) || {
|
|
81
|
+
deviceId,
|
|
82
|
+
userId: userContext.userId,
|
|
83
|
+
firstSeen: new Date(),
|
|
84
|
+
lastSeen: new Date(),
|
|
85
|
+
trustLevel: peers_sdk_1.TrustLevel.NewDevice,
|
|
86
|
+
};
|
|
81
87
|
device.name = deviceName() || undefined;
|
|
82
88
|
await devicesTable.save(device);
|
|
83
89
|
currentDeviceName(deviceName());
|
|
@@ -103,7 +103,7 @@ const SetupUser = () => {
|
|
|
103
103
|
};
|
|
104
104
|
// Step 1: Select User Type
|
|
105
105
|
if (step === 'select-user-type' && isExistingUser === null) {
|
|
106
|
-
return (react_1.default.createElement("div", { className: "container-fluid d-flex align-items-start justify-content-center", style: {
|
|
106
|
+
return (react_1.default.createElement("div", { className: "container-fluid d-flex align-items-start justify-content-center", style: { paddingTop: '20px', backgroundColor: isDark ? '#212529' : '#f8f9fa' } },
|
|
107
107
|
react_1.default.createElement("div", { className: "card shadow", style: { maxWidth: '500px', width: '100%', backgroundColor: isDark ? '#2d3238' : '#ffffff' } },
|
|
108
108
|
react_1.default.createElement("div", { className: "card-body p-4 p-md-5" },
|
|
109
109
|
react_1.default.createElement("div", { className: "text-center mb-4" },
|
|
@@ -125,7 +125,7 @@ const SetupUser = () => {
|
|
|
125
125
|
}
|
|
126
126
|
// Step 2: New User Confirmation
|
|
127
127
|
if (isExistingUser === false) {
|
|
128
|
-
return (react_1.default.createElement("div", { className: "container-fluid d-flex align-items-start justify-content-center", style: {
|
|
128
|
+
return (react_1.default.createElement("div", { className: "container-fluid d-flex align-items-start justify-content-center", style: { paddingTop: '20px', backgroundColor: isDark ? '#212529' : '#f8f9fa' } },
|
|
129
129
|
react_1.default.createElement("div", { className: "card shadow", style: { maxWidth: '500px', width: '100%', backgroundColor: isDark ? '#2d3238' : '#ffffff' } },
|
|
130
130
|
react_1.default.createElement("div", { className: "card-body p-4 p-md-5" },
|
|
131
131
|
react_1.default.createElement("div", { className: "text-center mb-4" },
|
|
@@ -147,12 +147,14 @@ const SetupUser = () => {
|
|
|
147
147
|
}
|
|
148
148
|
// Step 3: Existing User Sign In
|
|
149
149
|
if (isExistingUser === true) {
|
|
150
|
-
return (react_1.default.createElement("div", { className: "container-fluid d-flex align-items-start justify-content-center", style: {
|
|
150
|
+
return (react_1.default.createElement("div", { className: "container-fluid d-flex align-items-start justify-content-center", style: { paddingTop: '20px', backgroundColor: isDark ? '#212529' : '#f8f9fa' } },
|
|
151
151
|
react_1.default.createElement("div", { className: "card shadow", style: { maxWidth: '500px', width: '100%', backgroundColor: isDark ? '#2d3238' : '#ffffff' } },
|
|
152
152
|
react_1.default.createElement("div", { className: "card-body p-4 p-md-5" },
|
|
153
|
-
react_1.default.createElement("div", { className: "text-center mb-
|
|
154
|
-
react_1.default.createElement("
|
|
155
|
-
|
|
153
|
+
react_1.default.createElement("div", { className: "text-center mb-2" },
|
|
154
|
+
react_1.default.createElement("h3", { className: `fw-bold mb-2 ${isDark ? 'text-light' : ''}` },
|
|
155
|
+
react_1.default.createElement("span", null, "Sign In"),
|
|
156
|
+
"\u00A0\u00A0",
|
|
157
|
+
react_1.default.createElement("i", { className: "bi bi-box-arrow-in-right text-primary" })),
|
|
156
158
|
react_1.default.createElement("p", { className: isDark ? 'text-light opacity-75' : 'text-muted' }, "Enter your existing credentials")),
|
|
157
159
|
error && (react_1.default.createElement("div", { className: "alert alert-danger", role: "alert" },
|
|
158
160
|
react_1.default.createElement("i", { className: "bi bi-exclamation-triangle me-2" }),
|
|
@@ -18,5 +18,6 @@ export { contactsApp } from './contacts.app';
|
|
|
18
18
|
export { consoleLogsApp } from './console-logs.app';
|
|
19
19
|
export { networkViewerApp } from './network-viewer.app';
|
|
20
20
|
export { dataExplorerApp } from './data-explorer.app';
|
|
21
|
+
export { mobileSettingsApp } from './mobile-settings.app';
|
|
21
22
|
export declare const systemApps: IAppNav[];
|
|
22
23
|
export declare const systemPackage: IPackage;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.systemPackage = exports.systemApps = exports.dataExplorerApp = exports.networkViewerApp = exports.consoleLogsApp = exports.contactsApp = exports.groupsApp = exports.settingsApp = exports.profileApp = exports.predicatesApp = exports.knowledgeFramesApp = exports.knowledgeValuesApp = exports.threadsApp = exports.packagesApp = exports.typesApp = exports.variablesApp = exports.eventsApp = exports.workflowsApp = exports.toolsApp = exports.assistantsApp = exports.searchApp = void 0;
|
|
3
|
+
exports.systemPackage = exports.systemApps = exports.mobileSettingsApp = exports.dataExplorerApp = exports.networkViewerApp = exports.consoleLogsApp = exports.contactsApp = exports.groupsApp = exports.settingsApp = exports.profileApp = exports.predicatesApp = exports.knowledgeFramesApp = exports.knowledgeValuesApp = exports.threadsApp = exports.packagesApp = exports.typesApp = exports.variablesApp = exports.eventsApp = exports.workflowsApp = exports.toolsApp = exports.assistantsApp = exports.searchApp = void 0;
|
|
4
4
|
// Import all system apps
|
|
5
5
|
var search_app_1 = require("./search.app");
|
|
6
6
|
Object.defineProperty(exports, "searchApp", { enumerable: true, get: function () { return search_app_1.searchApp; } });
|
|
@@ -40,6 +40,8 @@ var network_viewer_app_1 = require("./network-viewer.app");
|
|
|
40
40
|
Object.defineProperty(exports, "networkViewerApp", { enumerable: true, get: function () { return network_viewer_app_1.networkViewerApp; } });
|
|
41
41
|
var data_explorer_app_1 = require("./data-explorer.app");
|
|
42
42
|
Object.defineProperty(exports, "dataExplorerApp", { enumerable: true, get: function () { return data_explorer_app_1.dataExplorerApp; } });
|
|
43
|
+
var mobile_settings_app_1 = require("./mobile-settings.app");
|
|
44
|
+
Object.defineProperty(exports, "mobileSettingsApp", { enumerable: true, get: function () { return mobile_settings_app_1.mobileSettingsApp; } });
|
|
43
45
|
// Import individual apps
|
|
44
46
|
const search_app_2 = require("./search.app");
|
|
45
47
|
const assistants_app_2 = require("./assistants.app");
|
|
@@ -60,6 +62,11 @@ const contacts_app_2 = require("./contacts.app");
|
|
|
60
62
|
const console_logs_app_2 = require("./console-logs.app");
|
|
61
63
|
const network_viewer_app_2 = require("./network-viewer.app");
|
|
62
64
|
const data_explorer_app_2 = require("./data-explorer.app");
|
|
65
|
+
const mobile_settings_app_2 = require("./mobile-settings.app");
|
|
66
|
+
// Helper to check if running in React Native
|
|
67
|
+
function isReactNative() {
|
|
68
|
+
return typeof window.__NATIVE_THEME !== 'undefined';
|
|
69
|
+
}
|
|
63
70
|
// Collection of all system apps
|
|
64
71
|
exports.systemApps = [
|
|
65
72
|
// Core Navigation & Search
|
|
@@ -82,6 +89,8 @@ exports.systemApps = [
|
|
|
82
89
|
// User & Settings Apps
|
|
83
90
|
profile_app_2.profileApp,
|
|
84
91
|
settings_app_2.settingsApp,
|
|
92
|
+
// Mobile Settings (only in React Native)
|
|
93
|
+
...(isReactNative() ? [mobile_settings_app_2.mobileSettingsApp] : []),
|
|
85
94
|
// System Tools & Debugging
|
|
86
95
|
console_logs_app_2.consoleLogsApp,
|
|
87
96
|
network_viewer_app_2.networkViewerApp,
|
|
@@ -134,7 +134,7 @@ function TabsLayoutInternal() {
|
|
|
134
134
|
borderBottomWidth: '1px',
|
|
135
135
|
borderBottomColor: _colorMode === 'light' ? '#dee2e6' : '#495057'
|
|
136
136
|
} }, isMobile ? (react_1.default.createElement(MobileTabsHeader, { tabs: tabs, activeTab: activeTab, onSwitch: switchTab, onClose: closeTab, colorMode: _colorMode })) : (react_1.default.createElement("div", { className: "d-flex align-items-center px-1", style: { height: '36px' } },
|
|
137
|
-
react_1.default.createElement(group_switcher_1.GroupSwitcher, { colorMode: _colorMode }),
|
|
137
|
+
react_1.default.createElement(group_switcher_1.GroupSwitcher, { colorMode: _colorMode, isMobile: false }),
|
|
138
138
|
react_1.default.createElement("button", { className: "btn btn-sm me-2 d-flex align-items-center", onClick: command_palette_1.openCommandPalette, title: "Search everything (Cmd+K)", style: {
|
|
139
139
|
padding: '4px 8px',
|
|
140
140
|
fontSize: '12px',
|
|
@@ -158,47 +158,52 @@ function TabsLayoutInternal() {
|
|
|
158
158
|
react_1.default.createElement(TabContent, { tab: tab, isMobile: isMobile, isActive: activeTab === tab.tabId })))))));
|
|
159
159
|
}
|
|
160
160
|
function MobileTabsHeader({ tabs, activeTab, onSwitch, onClose, colorMode }) {
|
|
161
|
-
const [
|
|
161
|
+
const [showMenuDropdown, setShowMenuDropdown] = react_1.default.useState(false);
|
|
162
162
|
const currentTab = tabs.find(t => t.tabId === activeTab);
|
|
163
163
|
const nonLauncherTabs = tabs.filter(t => t.packageId !== 'launcher');
|
|
164
|
-
return (react_1.default.createElement("div", { className: "d-flex align-items-center justify-content-between px-2", style: { height: '36px' } },
|
|
164
|
+
return (react_1.default.createElement("div", { className: "d-flex align-items-center justify-content-between px-2 position-relative", style: { height: '36px' } },
|
|
165
165
|
react_1.default.createElement("div", { className: "d-flex align-items-center gap-1" },
|
|
166
|
-
react_1.default.createElement(group_switcher_1.GroupSwitcher, { colorMode: colorMode }),
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
e.currentTarget.style.backgroundColor = colorMode === 'light' ? '#f8f9fa' : '#495057';
|
|
174
|
-
e.currentTarget.style.color = colorMode === 'light' ? '#0d6efd' : '#ffffff';
|
|
175
|
-
}, onMouseLeave: (e) => {
|
|
176
|
-
e.currentTarget.style.backgroundColor = 'transparent';
|
|
177
|
-
e.currentTarget.style.color = colorMode === 'light' ? '#6c757d' : '#adb5bd';
|
|
178
|
-
} },
|
|
179
|
-
react_1.default.createElement("i", { className: "bi-search" })),
|
|
180
|
-
react_1.default.createElement("button", { className: `btn btn-sm ${colorMode === 'light' ? 'btn-outline-primary' : 'btn-outline-light'}`, onClick: () => onSwitch('launcher'), style: { minWidth: '36px' } },
|
|
181
|
-
react_1.default.createElement("i", { className: "bi-grid-3x3-gap" }))),
|
|
182
|
-
react_1.default.createElement("div", { className: "d-flex align-items-center flex-grow-1 justify-content-center" },
|
|
166
|
+
react_1.default.createElement(group_switcher_1.GroupSwitcher, { colorMode: colorMode, isMobile: true })),
|
|
167
|
+
react_1.default.createElement("div", { className: "d-flex align-items-center justify-content-center position-absolute", style: {
|
|
168
|
+
left: '50%',
|
|
169
|
+
transform: 'translateX(-50%)',
|
|
170
|
+
pointerEvents: 'none',
|
|
171
|
+
maxWidth: 'calc(100% - 140px)'
|
|
172
|
+
} },
|
|
183
173
|
currentTab?.iconClassName && currentTab.packageId !== 'launcher' && (react_1.default.createElement("i", { className: `${currentTab.iconClassName} me-2` })),
|
|
184
|
-
react_1.default.createElement("span", { className: "fw-medium text-truncate
|
|
185
|
-
react_1.default.createElement("div", { className: "d-flex align-items-center
|
|
186
|
-
react_1.default.createElement("
|
|
187
|
-
react_1.default.createElement("
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
174
|
+
react_1.default.createElement("span", { className: "fw-medium text-truncate small" }, currentTab?.title || 'Apps')),
|
|
175
|
+
react_1.default.createElement("div", { className: "d-flex align-items-center" },
|
|
176
|
+
react_1.default.createElement("div", { className: "dropdown" },
|
|
177
|
+
react_1.default.createElement("button", { className: `btn btn-sm ${colorMode === 'light' ? 'btn-outline-dark' : 'btn-outline-light'}`, onClick: () => setShowMenuDropdown(!showMenuDropdown), style: { minWidth: '36px' } },
|
|
178
|
+
react_1.default.createElement("i", { className: "bi-list" }),
|
|
179
|
+
nonLauncherTabs.length > 0 && (react_1.default.createElement("span", { className: "ms-1" }, nonLauncherTabs.length))),
|
|
180
|
+
showMenuDropdown && (react_1.default.createElement("div", { className: `dropdown-menu show position-absolute ${colorMode === 'light' ? '' : 'dropdown-menu-dark'}`, style: { right: 0, top: '100%', zIndex: 1000, minWidth: '250px' } },
|
|
181
|
+
react_1.default.createElement("div", { className: `dropdown-item d-flex align-items-center ${activeTab === 'launcher' ? 'active' : ''}`, style: { cursor: 'pointer' }, onClick: () => {
|
|
182
|
+
onSwitch('launcher');
|
|
183
|
+
setShowMenuDropdown(false);
|
|
184
|
+
} },
|
|
185
|
+
react_1.default.createElement("i", { className: "bi-grid-3x3-gap me-2" }),
|
|
186
|
+
react_1.default.createElement("span", null, "Apps")),
|
|
187
|
+
react_1.default.createElement("div", { className: "dropdown-item d-flex align-items-center", style: { cursor: 'pointer' }, onClick: () => {
|
|
188
|
+
(0, command_palette_1.openCommandPalette)();
|
|
189
|
+
setShowMenuDropdown(false);
|
|
190
|
+
} },
|
|
191
|
+
react_1.default.createElement("i", { className: "bi-search me-2" }),
|
|
192
|
+
react_1.default.createElement("span", null, "Search")),
|
|
193
|
+
nonLauncherTabs.length > 0 && (react_1.default.createElement("div", { className: "dropdown-divider" })),
|
|
194
|
+
nonLauncherTabs.slice().reverse().map(tab => (react_1.default.createElement("div", { key: tab.tabId, className: `dropdown-item d-flex align-items-center justify-content-between ${activeTab === tab.tabId ? 'active' : ''}`, style: { cursor: 'pointer' }, onClick: () => {
|
|
195
|
+
onSwitch(tab.tabId);
|
|
196
|
+
setShowMenuDropdown(false);
|
|
197
|
+
} },
|
|
198
|
+
react_1.default.createElement("div", { className: "d-flex align-items-center" },
|
|
199
|
+
tab.iconClassName && react_1.default.createElement("i", { className: `${tab.iconClassName} me-2` }),
|
|
200
|
+
react_1.default.createElement("span", null, tab.title)),
|
|
201
|
+
react_1.default.createElement("button", { className: "btn btn-sm p-0 ms-2", style: { width: '20px', height: '20px' }, onClick: (e) => {
|
|
202
|
+
e.stopPropagation();
|
|
203
|
+
onClose(tab.tabId);
|
|
204
|
+
} },
|
|
205
|
+
react_1.default.createElement("i", { className: "bi-x" }))))))))),
|
|
206
|
+
showMenuDropdown && (react_1.default.createElement("div", { className: "position-fixed w-100 h-100", style: { top: 0, left: 0, zIndex: 999 }, onClick: () => setShowMenuDropdown(false) }))));
|
|
202
207
|
}
|
|
203
208
|
function TabHeader({ tab, isActive, onSwitch, onClose, colorMode }) {
|
|
204
209
|
const activeClass = isActive
|
|
@@ -318,6 +323,23 @@ function AppLauncherTab({ isMobile }) {
|
|
|
318
323
|
.map(path => filteredApps.find(app => app.path === path))
|
|
319
324
|
.filter(Boolean);
|
|
320
325
|
const openApp = (appItem) => {
|
|
326
|
+
// Check if this is the mobile-settings app and we're in React Native
|
|
327
|
+
if (appItem.path === 'mobile-settings' && typeof window.__NATIVE_THEME !== 'undefined') {
|
|
328
|
+
// Use expo-linking to navigate to native screen
|
|
329
|
+
// @ts-ignore
|
|
330
|
+
if (window.ReactNativeWebView) {
|
|
331
|
+
// @ts-ignore
|
|
332
|
+
window.ReactNativeWebView.postMessage(JSON.stringify({
|
|
333
|
+
type: 'navigate',
|
|
334
|
+
path: 'mobile-settings'
|
|
335
|
+
}));
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
// Fallback to deep link
|
|
339
|
+
window.location.href = 'peers://mobile-settings';
|
|
340
|
+
}
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
321
343
|
(0, tabs_state_1.goToTabPath)(appItem.path);
|
|
322
344
|
};
|
|
323
345
|
return (react_1.default.createElement("div", { className: `container-fluid ${isMobile ? 'p-2' : 'p-4'}`, style: { maxHeight: '100%', overflowY: 'auto' } },
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IAppNav, IPackage } from "@peers-app/peers-sdk";
|
|
1
|
+
import { IAppNav, IPackage, Observable } from "@peers-app/peers-sdk";
|
|
2
2
|
export interface TabState {
|
|
3
3
|
tabId: string;
|
|
4
4
|
packageId?: string;
|
|
@@ -7,9 +7,15 @@ export interface TabState {
|
|
|
7
7
|
iconClassName?: string;
|
|
8
8
|
}
|
|
9
9
|
export declare const launcherApp: TabState;
|
|
10
|
-
export declare const activeTabs:
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
export declare const activeTabs: Observable<TabState[]> & {
|
|
11
|
+
loadingPromise: Promise<void>;
|
|
12
|
+
};
|
|
13
|
+
export declare const activeTabId: Observable<string> & {
|
|
14
|
+
loadingPromise: Promise<void>;
|
|
15
|
+
};
|
|
16
|
+
export declare const recentlyUsedApps: Observable<string[]> & {
|
|
17
|
+
loadingPromise: Promise<void>;
|
|
18
|
+
};
|
|
13
19
|
export declare const initializedTabs: Set<string>;
|
|
14
20
|
export declare function goToTabPath(path: string): void;
|
|
15
21
|
export declare const handleMainPathChanged: (oldPath: string, newPath: string, setNewMainPath: ((path: string) => any)) => void;
|
|
@@ -14,16 +14,53 @@ exports.launcherApp = {
|
|
|
14
14
|
title: 'Apps',
|
|
15
15
|
iconClassName: 'bi-grid-3x3-gap',
|
|
16
16
|
};
|
|
17
|
-
//
|
|
18
|
-
|
|
17
|
+
// Persistent vars for storage (write-only, no subscription to avoid oscillation)
|
|
18
|
+
const _persistentActiveTabs = (0, peers_sdk_1.groupDeviceVar)('activeTabs', {
|
|
19
19
|
defaultValue: [exports.launcherApp],
|
|
20
20
|
});
|
|
21
|
-
|
|
21
|
+
const _persistentActiveTabId = (0, peers_sdk_1.groupDeviceVar)('activeTabId', {
|
|
22
22
|
defaultValue: 'launcher',
|
|
23
23
|
});
|
|
24
|
-
|
|
24
|
+
const _persistentRecentlyUsedApps = (0, peers_sdk_1.groupUserVar)('recentlyUsedApps', {
|
|
25
25
|
defaultValue: [],
|
|
26
26
|
});
|
|
27
|
+
// In-memory observables for UI state (loaded once, then write-through to persistent vars)
|
|
28
|
+
exports.activeTabs = (() => {
|
|
29
|
+
const obs = (0, peers_sdk_1.observable)([exports.launcherApp]);
|
|
30
|
+
// Write-through to persistent var on change
|
|
31
|
+
obs.subscribe(value => {
|
|
32
|
+
_persistentActiveTabs(value);
|
|
33
|
+
});
|
|
34
|
+
// Load initial value once
|
|
35
|
+
const loadingPromise = _persistentActiveTabs.loadingPromise.then(() => {
|
|
36
|
+
obs(_persistentActiveTabs());
|
|
37
|
+
});
|
|
38
|
+
return Object.assign(obs, { loadingPromise });
|
|
39
|
+
})();
|
|
40
|
+
exports.activeTabId = (() => {
|
|
41
|
+
const obs = (0, peers_sdk_1.observable)('launcher');
|
|
42
|
+
// Write-through to persistent var on change
|
|
43
|
+
obs.subscribe(value => {
|
|
44
|
+
_persistentActiveTabId(value);
|
|
45
|
+
});
|
|
46
|
+
// Load initial value once
|
|
47
|
+
const loadingPromise = _persistentActiveTabId.loadingPromise.then(() => {
|
|
48
|
+
obs(_persistentActiveTabId());
|
|
49
|
+
});
|
|
50
|
+
return Object.assign(obs, { loadingPromise });
|
|
51
|
+
})();
|
|
52
|
+
exports.recentlyUsedApps = (() => {
|
|
53
|
+
const obs = (0, peers_sdk_1.observable)([]);
|
|
54
|
+
// Write-through to persistent var on change
|
|
55
|
+
obs.subscribe(value => {
|
|
56
|
+
_persistentRecentlyUsedApps(value);
|
|
57
|
+
});
|
|
58
|
+
// Load initial value once
|
|
59
|
+
const loadingPromise = _persistentRecentlyUsedApps.loadingPromise.then(() => {
|
|
60
|
+
obs(_persistentRecentlyUsedApps());
|
|
61
|
+
});
|
|
62
|
+
return Object.assign(obs, { loadingPromise });
|
|
63
|
+
})();
|
|
27
64
|
exports.initializedTabs = new Set();
|
|
28
65
|
function goToTabPath(path) {
|
|
29
66
|
const tab = (0, exports.activeTabs)().find(t => t.path === path);
|
|
@@ -225,26 +225,59 @@ const UILoader = (args) => {
|
|
|
225
225
|
return react_1.default.createElement(UIAsyncLoader, { ...args });
|
|
226
226
|
};
|
|
227
227
|
const uiLoadingPromises = {};
|
|
228
|
+
// Check if we're running in a React Native WebView (has injectUIBundle available)
|
|
229
|
+
const isReactNativeWebView = typeof window.ReactNativeWebView !== 'undefined';
|
|
228
230
|
function loadUIBundle(pkg, forceRefresh) {
|
|
229
231
|
// Dynamically import the bundle
|
|
230
232
|
let importPromise = uiLoadingPromises[pkg.packageId];
|
|
231
233
|
if (!importPromise || forceRefresh) {
|
|
234
|
+
const sTime = Date.now();
|
|
232
235
|
console.log(`loading ui bundle for ${pkg.name}`);
|
|
233
236
|
importPromise = new Promise(async (resolve, reject) => {
|
|
234
237
|
try {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
+
if (!pkg.uiBundleFileId) {
|
|
239
|
+
resolve();
|
|
240
|
+
return;
|
|
238
241
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
242
|
+
// Use fast injection path for React Native WebView
|
|
243
|
+
if (isReactNativeWebView && peers_sdk_1.rpcServerCalls.injectUIBundle) {
|
|
244
|
+
// Set up listeners for bundle load completion
|
|
245
|
+
const _window = window;
|
|
246
|
+
_window.__peersUIs = _window.__peersUIs || {};
|
|
247
|
+
const loadPromise = new Promise((resolveLoad, rejectLoad) => {
|
|
248
|
+
const fileId = pkg.uiBundleFileId;
|
|
249
|
+
_window.__peersUIBundleLoaded = (loadedFileId) => {
|
|
250
|
+
if (loadedFileId === fileId) {
|
|
251
|
+
// Copy loaded UIs to our local registry
|
|
252
|
+
Object.keys(_window.__peersUIs || {}).forEach(peersUIId => {
|
|
253
|
+
peersUIs[peersUIId] = _window.__peersUIs[peersUIId];
|
|
254
|
+
});
|
|
255
|
+
resolveLoad();
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
_window.__peersUIBundleError = (errorFileId, errorMsg) => {
|
|
259
|
+
if (errorFileId === fileId) {
|
|
260
|
+
rejectLoad(new Error(errorMsg));
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
});
|
|
264
|
+
await peers_sdk_1.rpcServerCalls.injectUIBundle(pkg.uiBundleFileId);
|
|
265
|
+
await loadPromise;
|
|
266
|
+
console.log(`finished loading ui bundle for ${pkg.name}: ${(Date.now() - sTime).toFixed(0)}ms`);
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
// Fallback: use postMessage-based getFileContents (slower for large bundles)
|
|
270
|
+
let bundleCode = await peers_sdk_1.rpcServerCalls.getFileContents(pkg.uiBundleFileId);
|
|
271
|
+
if (bundleCode) {
|
|
272
|
+
const exportUIs = (peerUIs) => {
|
|
273
|
+
peerUIs?.uis?.forEach(ui => {
|
|
274
|
+
peersUIs[ui.peersUIId] = ui;
|
|
275
|
+
});
|
|
276
|
+
};
|
|
277
|
+
const bundleFunction = new Function('exportUIs', bundleCode);
|
|
278
|
+
await bundleFunction(exportUIs);
|
|
279
|
+
}
|
|
280
|
+
console.log(`finished loading ui bundle for ${pkg.name}: ${(Date.now() - sTime).toFixed(0)}ms, ${(bundleCode.length / 1000).toFixed(0)} KB`);
|
|
248
281
|
}
|
|
249
282
|
resolve();
|
|
250
283
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peers-app/peers-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/peers-app/peers-ui.git"
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
29
|
"bootstrap": "^5.3.3",
|
|
30
|
-
"@peers-app/peers-sdk": "^0.
|
|
30
|
+
"@peers-app/peers-sdk": "^0.8.1",
|
|
31
31
|
"react": "^18.0.0",
|
|
32
32
|
"react-dom": "^18.0.0"
|
|
33
33
|
},
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"jest": "^29.7.0",
|
|
58
58
|
"jest-environment-jsdom": "^30.0.5",
|
|
59
59
|
"path-browserify": "^1.0.1",
|
|
60
|
-
"@peers-app/peers-sdk": "0.
|
|
60
|
+
"@peers-app/peers-sdk": "0.8.1",
|
|
61
61
|
"react": "^18.0.0",
|
|
62
62
|
"react-dom": "^18.0.0",
|
|
63
63
|
"string-width": "^7.1.0",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { observable } from "@peers-app/peers-sdk";
|
|
2
2
|
import { goToTabPath } from '../tabs-layout/tabs-state';
|
|
3
3
|
|
|
4
4
|
export interface Command {
|
|
@@ -12,14 +12,10 @@ export interface Command {
|
|
|
12
12
|
isAvailable?: () => boolean;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
// Command palette state (
|
|
16
|
-
export const isCommandPaletteOpen =
|
|
17
|
-
defaultValue: false,
|
|
18
|
-
});
|
|
15
|
+
// Command palette state (in-memory only, no persistence needed)
|
|
16
|
+
export const isCommandPaletteOpen = observable<boolean>(false);
|
|
19
17
|
|
|
20
|
-
export const commandSearchQuery =
|
|
21
|
-
defaultValue: '',
|
|
22
|
-
});
|
|
18
|
+
export const commandSearchQuery = observable<string>('');
|
|
23
19
|
|
|
24
20
|
// Command registry
|
|
25
21
|
const registeredCommands = new Map<string, Command>();
|
|
@@ -4,9 +4,10 @@ import { usePromise } from '../hooks';
|
|
|
4
4
|
|
|
5
5
|
interface GroupSwitcherProps {
|
|
6
6
|
colorMode: string;
|
|
7
|
+
isMobile?: boolean;
|
|
7
8
|
}
|
|
8
9
|
|
|
9
|
-
export function GroupSwitcher({ colorMode }: GroupSwitcherProps) {
|
|
10
|
+
export function GroupSwitcher({ colorMode, isMobile = false }: GroupSwitcherProps) {
|
|
10
11
|
const [showDropdown, setShowDropdown] = useState(false);
|
|
11
12
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
|
12
13
|
const [allGroups, setAllGroups] = useState<IGroup[]>([]);
|
|
@@ -62,16 +63,17 @@ export function GroupSwitcher({ colorMode }: GroupSwitcherProps) {
|
|
|
62
63
|
<>
|
|
63
64
|
<div className="dropdown">
|
|
64
65
|
<button
|
|
65
|
-
className=
|
|
66
|
+
className={`btn btn-sm ${isMobile ? '' : 'me-2'} d-flex align-items-center`}
|
|
66
67
|
onClick={() => setShowDropdown(!showDropdown)}
|
|
67
68
|
title={`Current group: ${getGroupName(currentGroup)}`}
|
|
68
69
|
style={{
|
|
69
|
-
padding: '4px 8px',
|
|
70
|
+
padding: isMobile ? '4px' : '4px 8px',
|
|
70
71
|
fontSize: '12px',
|
|
71
72
|
borderRadius: '6px',
|
|
72
73
|
border: 'none',
|
|
73
74
|
background: 'transparent',
|
|
74
|
-
color: isDark ? '#adb5bd' : '#6c757d'
|
|
75
|
+
color: isDark ? '#adb5bd' : '#6c757d',
|
|
76
|
+
minWidth: isMobile ? '36px' : undefined
|
|
75
77
|
}}
|
|
76
78
|
onMouseEnter={(e) => {
|
|
77
79
|
e.currentTarget.style.backgroundColor = isDark ? '#495057' : '#f8f9fa';
|
|
@@ -82,10 +84,12 @@ export function GroupSwitcher({ colorMode }: GroupSwitcherProps) {
|
|
|
82
84
|
e.currentTarget.style.color = isDark ? '#adb5bd' : '#6c757d';
|
|
83
85
|
}}
|
|
84
86
|
>
|
|
85
|
-
<i className={`${getGroupIcon(currentGroup)} me-1`} style={{ fontSize: '14px' }} />
|
|
86
|
-
|
|
87
|
-
{
|
|
88
|
-
|
|
87
|
+
<i className={`${getGroupIcon(currentGroup)} ${isMobile ? '' : 'me-1'}`} style={{ fontSize: '14px' }} />
|
|
88
|
+
{!isMobile && (
|
|
89
|
+
<span className="text-truncate" style={{ maxWidth: '80px' }}>
|
|
90
|
+
{getGroupName(currentGroup)}
|
|
91
|
+
</span>
|
|
92
|
+
)}
|
|
89
93
|
<i className="bi-chevron-down ms-1" style={{ fontSize: '10px' }} />
|
|
90
94
|
</button>
|
|
91
95
|
|
|
@@ -127,6 +127,10 @@ export function ContactList() {
|
|
|
127
127
|
<i className="bi-person-fill-check me-2" />
|
|
128
128
|
Contacts
|
|
129
129
|
</h4>
|
|
130
|
+
<a href="#contacts/connect" className="btn btn-primary btn-sm">
|
|
131
|
+
<i className="bi-person-plus me-1" />
|
|
132
|
+
Connect to New User
|
|
133
|
+
</a>
|
|
130
134
|
</div>
|
|
131
135
|
|
|
132
136
|
<div className="input-group mb-3">
|