@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.
Files changed (39) hide show
  1. package/dist/command-palette/command-palette.d.ts +2 -2
  2. package/dist/command-palette/command-palette.js +3 -7
  3. package/dist/components/group-switcher.d.ts +2 -1
  4. package/dist/components/group-switcher.js +7 -6
  5. package/dist/globals.d.ts +1 -1
  6. package/dist/screens/contacts/contact-list.js +4 -1
  7. package/dist/screens/contacts/index.d.ts +2 -0
  8. package/dist/screens/contacts/index.js +2 -0
  9. package/dist/screens/contacts/user-connect.d.ts +2 -0
  10. package/dist/screens/contacts/user-connect.js +312 -0
  11. package/dist/screens/network-viewer/device-details-modal.js +44 -0
  12. package/dist/screens/network-viewer/group-details-modal.js +80 -2
  13. package/dist/screens/network-viewer/network-viewer.js +36 -16
  14. package/dist/screens/settings/settings-page.js +13 -7
  15. package/dist/screens/setup-user.js +8 -6
  16. package/dist/system-apps/index.d.ts +1 -0
  17. package/dist/system-apps/index.js +10 -1
  18. package/dist/system-apps/mobile-settings.app.d.ts +2 -0
  19. package/dist/system-apps/mobile-settings.app.js +8 -0
  20. package/dist/tabs-layout/tabs-layout.js +60 -38
  21. package/dist/tabs-layout/tabs-state.d.ts +10 -4
  22. package/dist/tabs-layout/tabs-state.js +41 -4
  23. package/dist/ui-router/ui-loader.js +45 -12
  24. package/package.json +3 -3
  25. package/src/command-palette/command-palette.ts +4 -8
  26. package/src/components/group-switcher.tsx +12 -8
  27. package/src/screens/contacts/contact-list.tsx +4 -0
  28. package/src/screens/contacts/index.ts +3 -1
  29. package/src/screens/contacts/user-connect.tsx +452 -0
  30. package/src/screens/network-viewer/device-details-modal.tsx +55 -0
  31. package/src/screens/network-viewer/group-details-modal.tsx +144 -1
  32. package/src/screens/network-viewer/network-viewer.tsx +36 -29
  33. package/src/screens/settings/settings-page.tsx +17 -9
  34. package/src/screens/setup-user.tsx +9 -6
  35. package/src/system-apps/index.ts +9 -0
  36. package/src/system-apps/mobile-settings.app.ts +8 -0
  37. package/src/tabs-layout/tabs-layout.tsx +108 -82
  38. package/src/tabs-layout/tabs-state.ts +54 -5
  39. 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 tooltip_1 = require("../../components/tooltip");
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
- if (!device)
80
- return;
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: { minHeight: '100vh', paddingTop: '80px', backgroundColor: isDark ? '#212529' : '#f8f9fa' } },
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: { minHeight: '100vh', paddingTop: '80px', backgroundColor: isDark ? '#212529' : '#f8f9fa' } },
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: { minHeight: '100vh', paddingTop: '80px', backgroundColor: isDark ? '#212529' : '#f8f9fa' } },
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-4" },
154
- react_1.default.createElement("i", { className: "bi bi-box-arrow-in-right text-primary fs-1 mb-3 d-block" }),
155
- react_1.default.createElement("h3", { className: `fw-bold mb-2 ${isDark ? 'text-light' : ''}` }, "Sign In"),
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,
@@ -0,0 +1,2 @@
1
+ import { IAppNav } from "@peers-app/peers-sdk";
2
+ export declare const mobileSettingsApp: IAppNav;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mobileSettingsApp = void 0;
4
+ exports.mobileSettingsApp = {
5
+ name: 'Mobile Settings',
6
+ iconClassName: 'bi-phone-fill',
7
+ navigationPath: 'mobile-settings'
8
+ };
@@ -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 [showTabDropdown, setShowTabDropdown] = react_1.default.useState(false);
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
- react_1.default.createElement("button", { className: "btn btn-sm", onClick: command_palette_1.openCommandPalette, title: "Search everything", style: {
168
- minWidth: '36px',
169
- border: 'none',
170
- background: 'transparent',
171
- color: colorMode === 'light' ? '#6c757d' : '#adb5bd'
172
- }, onMouseEnter: (e) => {
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 me-2 small" }, currentTab?.title || 'Apps')),
185
- react_1.default.createElement("div", { className: "d-flex align-items-center gap-2" }, nonLauncherTabs.length > 0 && (react_1.default.createElement("div", { className: "dropdown" },
186
- react_1.default.createElement("button", { className: `btn btn-sm ${colorMode === 'light' ? 'btn-outline-dark' : 'btn-outline-light'}`, onClick: () => setShowTabDropdown(!showTabDropdown) },
187
- react_1.default.createElement("i", { className: "bi-list" }),
188
- react_1.default.createElement("span", { className: "ms-1" }, nonLauncherTabs.length)),
189
- showTabDropdown && (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' } }, 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: () => {
190
- onSwitch(tab.tabId);
191
- setShowTabDropdown(false);
192
- } },
193
- react_1.default.createElement("div", { className: "d-flex align-items-center" },
194
- tab.iconClassName && react_1.default.createElement("i", { className: `${tab.iconClassName} me-2` }),
195
- react_1.default.createElement("span", null, tab.title)),
196
- tab.tabId !== "launcher" && (react_1.default.createElement("button", { className: "btn btn-sm p-0 ms-2", style: { width: '20px', height: '20px' }, onClick: (e) => {
197
- e.stopPropagation();
198
- onClose(tab.tabId);
199
- } },
200
- react_1.default.createElement("i", { className: "bi-x" }))))))))))),
201
- showTabDropdown && (react_1.default.createElement("div", { className: "position-fixed w-100 h-100", style: { top: 0, left: 0, zIndex: 999 }, onClick: () => setShowTabDropdown(false) }))));
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: import("@peers-app/peers-sdk").PersistentVar<TabState[]>;
11
- export declare const activeTabId: import("@peers-app/peers-sdk").PersistentVar<string>;
12
- export declare const recentlyUsedApps: import("@peers-app/peers-sdk").PersistentVar<string[]>;
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
- // Global persistent variables for tab state
18
- exports.activeTabs = (0, peers_sdk_1.groupDeviceVar)('activeTabs', {
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
- exports.activeTabId = (0, peers_sdk_1.groupDeviceVar)('activeTabId', {
21
+ const _persistentActiveTabId = (0, peers_sdk_1.groupDeviceVar)('activeTabId', {
22
22
  defaultValue: 'launcher',
23
23
  });
24
- exports.recentlyUsedApps = (0, peers_sdk_1.groupUserVar)('recentlyUsedApps', {
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
- let bundleCode = '';
236
- if (pkg.uiBundleFileId) {
237
- bundleCode = await peers_sdk_1.rpcServerCalls.getFileContents(pkg.uiBundleFileId);
238
+ if (!pkg.uiBundleFileId) {
239
+ resolve();
240
+ return;
238
241
  }
239
- if (bundleCode) {
240
- const exportUIs = (peerUIs) => {
241
- // TODO maybe add packageId that this came from
242
- peerUIs?.uis?.forEach(ui => {
243
- peersUIs[ui.peersUIId] = ui;
244
- });
245
- };
246
- const bundleFunction = new Function('exportUIs', bundleCode);
247
- await bundleFunction(exportUIs);
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.7.40",
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.7.40",
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.7.40",
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 { deviceVar } from "@peers-app/peers-sdk";
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 (using device scope but with temporary defaults)
16
- export const isCommandPaletteOpen = deviceVar<boolean>('commandPaletteOpen', {
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 = deviceVar<string>('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="btn btn-sm me-2 d-flex align-items-center"
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
- <span className="text-truncate" style={{ maxWidth: '80px' }}>
87
- {getGroupName(currentGroup)}
88
- </span>
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">