@peers-app/peers-ui 0.8.5 → 0.8.7

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.
@@ -47,9 +47,7 @@ const LeftBarContent = () => {
47
47
  react_1.default.createElement("i", { className: "bi bi-three-dots-vertical", style: { fontSize: '12pt' } })),
48
48
  react_1.default.createElement("ul", { className: "dropdown-menu dropdown-menu-end dropdown-menu-dark text-small shadow", style: { border: '1px grey solid' }, "aria-labelledby": "dropdownUser1" },
49
49
  react_1.default.createElement("li", null,
50
- react_1.default.createElement("a", { className: "dropdown-item", href: "#profile" }, "Profile")),
51
- react_1.default.createElement("li", null,
52
- react_1.default.createElement("a", { className: "dropdown-item", href: "#settings" }, "Settings"))))),
50
+ react_1.default.createElement("a", { className: "dropdown-item", href: "#settings" }, "Profile & Settings"))))),
53
51
  react_1.default.createElement("hr", { className: 'p-0' }),
54
52
  react_1.default.createElement(MenuSection, { menuItems: [
55
53
  // { text: "Shell", icon: "bi bi-robot" },
@@ -2,6 +2,7 @@ import React from "react";
2
2
  import "../screens/console-logs/console-logs-list";
3
3
  import "../screens/groups";
4
4
  import "../screens/contacts";
5
+ import "../screens/join-group";
5
6
  import "../screens/network-viewer";
6
7
  import "../screens/data-explorer";
7
8
  export declare function Router({ path: providedPath }?: {
@@ -41,7 +41,6 @@ const react_1 = __importDefault(require("react"));
41
41
  const globals = __importStar(require("../globals"));
42
42
  const assistant_details_1 = require("../screens/assistants/assistant-details");
43
43
  const assistant_list_1 = require("../screens/assistants/assistant-list");
44
- const profile_1 = require("../screens/profile");
45
44
  // import { TaskDetails } from "../screens/tasks/task-details";
46
45
  // import { TaskList } from "../screens/tasks/task-list";
47
46
  const tool_details_1 = require("../screens/tools/tool-details");
@@ -73,6 +72,7 @@ const global_search_1 = require("../screens/search/global-search");
73
72
  require("../screens/console-logs/console-logs-list");
74
73
  require("../screens/groups");
75
74
  require("../screens/contacts");
75
+ require("../screens/join-group");
76
76
  require("../screens/network-viewer");
77
77
  require("../screens/data-explorer");
78
78
  function Router({ path: providedPath } = {}) {
@@ -94,12 +94,9 @@ function Router({ path: providedPath } = {}) {
94
94
  if (path === 'search') {
95
95
  return react_1.default.createElement(global_search_1.GlobalSearch, null);
96
96
  }
97
- if (path === 'settings') {
97
+ if (path === 'settings' || path === 'profile') {
98
98
  return react_1.default.createElement(settings_page_1.SettingsPage, null);
99
99
  }
100
- if (path === 'profile') {
101
- return react_1.default.createElement(profile_1.Profile, null);
102
- }
103
100
  if (path === 'threads' || path === '') {
104
101
  return react_1.default.createElement(channel_view_1.ChannelMessages, null);
105
102
  }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Component for admins to prepare group invitations and manage join requests.
3
+ *
4
+ * Shows in the group details Members tab for users with Admin role or above.
5
+ *
6
+ * Communicates with device layer through pvars (not direct imports).
7
+ */
8
+ import React from "react";
9
+ import { UserContext } from "@peers-app/peers-sdk";
10
+ interface GroupInviteListenerProps {
11
+ groupId: string;
12
+ userContext: UserContext;
13
+ }
14
+ export declare const GroupInviteListener: (props: GroupInviteListenerProps) => React.JSX.Element;
15
+ export {};
@@ -0,0 +1,189 @@
1
+ "use strict";
2
+ /**
3
+ * Component for admins to prepare group invitations and manage join requests.
4
+ *
5
+ * Shows in the group details Members tab for users with Admin role or above.
6
+ *
7
+ * Communicates with device layer through pvars (not direct imports).
8
+ */
9
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ var desc = Object.getOwnPropertyDescriptor(m, k);
12
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
13
+ desc = { enumerable: true, get: function() { return m[k]; } };
14
+ }
15
+ Object.defineProperty(o, k2, desc);
16
+ }) : (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ o[k2] = m[k];
19
+ }));
20
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
21
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
22
+ }) : function(o, v) {
23
+ o["default"] = v;
24
+ });
25
+ var __importStar = (this && this.__importStar) || (function () {
26
+ var ownKeys = function(o) {
27
+ ownKeys = Object.getOwnPropertyNames || function (o) {
28
+ var ar = [];
29
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
30
+ return ar;
31
+ };
32
+ return ownKeys(o);
33
+ };
34
+ return function (mod) {
35
+ if (mod && mod.__esModule) return mod;
36
+ var result = {};
37
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
38
+ __setModuleDefault(result, mod);
39
+ return result;
40
+ };
41
+ })();
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.GroupInviteListener = void 0;
44
+ const react_1 = __importStar(require("react"));
45
+ const peers_sdk_1 = require("@peers-app/peers-sdk");
46
+ const hooks_1 = require("../../hooks");
47
+ const GroupInviteListener = (props) => {
48
+ const { groupId } = props;
49
+ // State
50
+ const [password, setPassword] = (0, react_1.useState)("");
51
+ const [isListening, setIsListening] = (0, react_1.useState)(false);
52
+ const [copied, setCopied] = (0, react_1.useState)(false);
53
+ const [approvalRole, setApprovalRole] = (0, react_1.useState)(peers_sdk_1.GroupMemberRole.Reader);
54
+ const [processingRequestId, setProcessingRequestId] = (0, react_1.useState)(null);
55
+ // Observables
56
+ const [listeners] = (0, hooks_1.useObservable)(peers_sdk_1.groupInviteListeners);
57
+ const [requests] = (0, hooks_1.useObservable)(peers_sdk_1.groupInviteRequests);
58
+ const [status] = (0, hooks_1.useObservable)(peers_sdk_1.groupInviteStatus);
59
+ // Check if we're currently listening for this group
60
+ const currentListener = listeners?.[groupId];
61
+ const pendingRequests = (requests || []).filter(r => r.groupId === groupId);
62
+ // Sync local state with listener state
63
+ (0, react_1.useEffect)(() => {
64
+ if (currentListener) {
65
+ setPassword(currentListener.password);
66
+ setIsListening(true);
67
+ }
68
+ else {
69
+ setIsListening(false);
70
+ }
71
+ }, [currentListener]);
72
+ // Generate a new password
73
+ const handleGeneratePassword = (0, react_1.useCallback)(() => {
74
+ const newPassword = (0, peers_sdk_1.generateInvitePassword)();
75
+ setPassword(newPassword);
76
+ }, []);
77
+ // Start listening for invitations (set pvar, device layer reacts)
78
+ const handleStartListening = (0, react_1.useCallback)(() => {
79
+ if (!password.trim() || password.length < 4) {
80
+ alert("Password must be at least 4 characters");
81
+ return;
82
+ }
83
+ // Set the action pvar - device layer will process it
84
+ (0, peers_sdk_1.groupInviteStartListening)({ groupId, password });
85
+ }, [groupId, password]);
86
+ // Stop listening (set pvar, device layer reacts)
87
+ const handleStopListening = (0, react_1.useCallback)(() => {
88
+ // Set the action pvar - device layer will process it
89
+ (0, peers_sdk_1.groupInviteStopListening)(groupId);
90
+ }, [groupId]);
91
+ // Copy password to clipboard
92
+ const handleCopyPassword = (0, react_1.useCallback)(async () => {
93
+ const success = await (0, peers_sdk_1.copyToClipboard)(password);
94
+ if (success) {
95
+ setCopied(true);
96
+ setTimeout(() => setCopied(false), 2000);
97
+ }
98
+ }, [password]);
99
+ // Approve a join request (set pvar, device layer reacts)
100
+ const handleApprove = (0, react_1.useCallback)((request) => {
101
+ setProcessingRequestId(request.requestId);
102
+ // Set the action pvar - device layer will process it
103
+ (0, peers_sdk_1.groupInviteProcessRequest)({
104
+ requestId: request.requestId,
105
+ approved: true,
106
+ role: approvalRole
107
+ });
108
+ // Clear processing state after a short delay
109
+ setTimeout(() => setProcessingRequestId(null), 1000);
110
+ }, [approvalRole]);
111
+ // Deny a join request (set pvar, device layer reacts)
112
+ const handleDeny = (0, react_1.useCallback)((request) => {
113
+ setProcessingRequestId(request.requestId);
114
+ // Set the action pvar - device layer will process it
115
+ (0, peers_sdk_1.groupInviteProcessRequest)({
116
+ requestId: request.requestId,
117
+ approved: false,
118
+ role: peers_sdk_1.GroupMemberRole.None
119
+ });
120
+ // Clear processing state after a short delay
121
+ setTimeout(() => setProcessingRequestId(null), 1000);
122
+ }, []);
123
+ return (react_1.default.createElement("div", { className: "card mb-3" },
124
+ react_1.default.createElement("div", { className: "card-header" },
125
+ react_1.default.createElement("h6", { className: "mb-0" },
126
+ react_1.default.createElement("i", { className: "bi-person-plus-fill me-2" }),
127
+ "Invite New Members")),
128
+ react_1.default.createElement("div", { className: "card-body" },
129
+ react_1.default.createElement("div", { className: "mb-3" },
130
+ react_1.default.createElement("label", { className: "form-label small text-muted" }, "Invitation Password"),
131
+ react_1.default.createElement("div", { className: "input-group" },
132
+ react_1.default.createElement("input", { type: "text", className: "form-control font-monospace", value: password, onChange: (e) => setPassword(e.target.value), placeholder: "Enter or generate a password...", disabled: isListening }),
133
+ react_1.default.createElement("button", { className: "btn btn-outline-secondary", onClick: handleGeneratePassword, disabled: isListening, title: "Generate random password" },
134
+ react_1.default.createElement("i", { className: "bi-shuffle" })),
135
+ react_1.default.createElement("button", { className: "btn btn-outline-secondary", onClick: handleCopyPassword, disabled: !password, title: "Copy password" },
136
+ react_1.default.createElement("i", { className: copied ? "bi-check-lg text-success" : "bi-clipboard" }))),
137
+ react_1.default.createElement("small", { className: "text-muted" }, "Share this password with people you want to invite. They can enter it in the \"Join Group\" screen.")),
138
+ react_1.default.createElement("div", { className: "d-flex gap-2" }, !isListening ? (react_1.default.createElement("button", { className: "btn btn-primary", onClick: handleStartListening, disabled: !password || password.length < 4 },
139
+ react_1.default.createElement("i", { className: "bi-broadcast me-2" }),
140
+ "Start Listening")) : (react_1.default.createElement("button", { className: "btn btn-danger", onClick: handleStopListening },
141
+ react_1.default.createElement("i", { className: "bi-stop-circle me-2" }),
142
+ "Stop Listening"))),
143
+ isListening && (react_1.default.createElement("div", { className: "alert alert-success mt-3 mb-0 d-flex align-items-center" },
144
+ react_1.default.createElement("div", { className: "spinner-border spinner-border-sm me-2", role: "status" },
145
+ react_1.default.createElement("span", { className: "visually-hidden" }, "Listening...")),
146
+ react_1.default.createElement("div", null,
147
+ react_1.default.createElement("strong", null, "Listening for join requests"),
148
+ react_1.default.createElement("br", null),
149
+ react_1.default.createElement("small", null,
150
+ "Password: ",
151
+ react_1.default.createElement("code", null, password))))),
152
+ status && (react_1.default.createElement("div", { className: "alert alert-info mt-3 mb-0" },
153
+ react_1.default.createElement("i", { className: "bi-info-circle me-2" }),
154
+ status)),
155
+ pendingRequests.length > 0 && (react_1.default.createElement("div", { className: "mt-3" },
156
+ react_1.default.createElement("h6", { className: "mb-2" },
157
+ react_1.default.createElement("i", { className: "bi-inbox-fill me-2" }),
158
+ "Pending Requests (",
159
+ pendingRequests.length,
160
+ ")"),
161
+ react_1.default.createElement("div", { className: "mb-2" },
162
+ react_1.default.createElement("label", { className: "form-label small text-muted" }, "Role for new members:"),
163
+ react_1.default.createElement("select", { className: "form-select form-select-sm", value: approvalRole, onChange: (e) => setApprovalRole(Number(e.target.value)), style: { width: "auto" } },
164
+ react_1.default.createElement("option", { value: peers_sdk_1.GroupMemberRole.Reader }, "Reader"),
165
+ react_1.default.createElement("option", { value: peers_sdk_1.GroupMemberRole.Writer }, "Writer"),
166
+ react_1.default.createElement("option", { value: peers_sdk_1.GroupMemberRole.Admin }, "Admin"))),
167
+ react_1.default.createElement("div", { className: "list-group" }, pendingRequests.map((request) => (react_1.default.createElement("div", { key: request.requestId, className: "list-group-item" },
168
+ react_1.default.createElement("div", { className: "d-flex justify-content-between align-items-center" },
169
+ react_1.default.createElement("div", null,
170
+ react_1.default.createElement("div", { className: "d-flex align-items-center" },
171
+ react_1.default.createElement("i", { className: "bi-person-fill me-2" }),
172
+ react_1.default.createElement("div", null,
173
+ react_1.default.createElement("strong", null, request.requester.name || request.requester.userId),
174
+ request.requester.name && (react_1.default.createElement("small", { className: "text-muted d-block" }, request.requester.userId)))),
175
+ react_1.default.createElement("small", { className: "text-muted" },
176
+ "Device: ",
177
+ request.requester.deviceId.slice(0, 8),
178
+ "...",
179
+ " | ",
180
+ "Received: ",
181
+ new Date(request.receivedAt).toLocaleTimeString())),
182
+ react_1.default.createElement("div", { className: "d-flex gap-2" },
183
+ react_1.default.createElement("button", { className: "btn btn-success btn-sm", onClick: () => handleApprove(request), disabled: processingRequestId === request.requestId }, processingRequestId === request.requestId ? (react_1.default.createElement("span", { className: "spinner-border spinner-border-sm" })) : (react_1.default.createElement(react_1.default.Fragment, null,
184
+ react_1.default.createElement("i", { className: "bi-check-lg me-1" }),
185
+ "Approve"))),
186
+ react_1.default.createElement("button", { className: "btn btn-outline-danger btn-sm", onClick: () => handleDeny(request), disabled: processingRequestId === request.requestId },
187
+ react_1.default.createElement("i", { className: "bi-x-lg" })))))))))))));
188
+ };
189
+ exports.GroupInviteListener = GroupInviteListener;
@@ -10,6 +10,7 @@ const loading_indicator_1 = require("../../components/loading-indicator");
10
10
  const trust_level_badge_1 = require("../../components/trust-level-badge");
11
11
  const typeahead_1 = require("../../components/typeahead");
12
12
  const hooks_1 = require("../../hooks");
13
+ const group_invite_listener_1 = require("./group-invite-listener");
13
14
  const GroupMembersUI = (props) => {
14
15
  const { groupId, userContext } = props;
15
16
  // Get the group's data context and tables
@@ -231,9 +232,10 @@ const GroupMembersUI = (props) => {
231
232
  react_1.default.createElement("strong", null,
232
233
  "Founder: ",
233
234
  founderData.founderUser?.name || founderData.founderUserId))))),
235
+ canEdit && (react_1.default.createElement(group_invite_listener_1.GroupInviteListener, { groupId: groupId, userContext: userContext })),
234
236
  canEdit && (react_1.default.createElement("div", { className: "card mb-3" },
235
237
  react_1.default.createElement("div", { className: "card-body" },
236
- react_1.default.createElement("h6", { className: "card-title" }, "Add Member"),
238
+ react_1.default.createElement("h6", { className: "card-title" }, "Add Existing Contact as Member"),
237
239
  react_1.default.createElement("div", { className: "row g-2" },
238
240
  react_1.default.createElement("div", { className: "col-md-6" },
239
241
  react_1.default.createElement(typeahead_1.Typeahead, { placeholder: "Search contacts to add...", searchFn: searchContacts, onSelectionChange: (items) => setSelectedContacts(items), renderItem: renderContactItem, renderBadge: renderContactBadge, selectedItems: selectedContacts.map(contact => ({ ...contact, id: contact.userId })), multiSelect: true, disabled: addingMember })),
@@ -0,0 +1,2 @@
1
+ import './join-group';
2
+ export * from './join-group';
@@ -0,0 +1,19 @@
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 __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ // Import to ensure route registration
18
+ require("./join-group");
19
+ __exportStar(require("./join-group"), exports);
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Join Group screen - allows users to join a group by entering an invitation password.
3
+ *
4
+ * Flow:
5
+ * 1. User enters the invitation password
6
+ * 2. System discovers all devices listening for that password
7
+ * 3. User selects a group/device to join
8
+ * 4. System sends join request and waits for approval
9
+ *
10
+ * Communicates with device layer through pvars (not direct imports).
11
+ */
12
+ import React from "react";
13
+ export declare const JoinGroup: () => React.JSX.Element;
@@ -0,0 +1,202 @@
1
+ "use strict";
2
+ /**
3
+ * Join Group screen - allows users to join a group by entering an invitation password.
4
+ *
5
+ * Flow:
6
+ * 1. User enters the invitation password
7
+ * 2. System discovers all devices listening for that password
8
+ * 3. User selects a group/device to join
9
+ * 4. System sends join request and waits for approval
10
+ *
11
+ * Communicates with device layer through pvars (not direct imports).
12
+ */
13
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ var desc = Object.getOwnPropertyDescriptor(m, k);
16
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
17
+ desc = { enumerable: true, get: function() { return m[k]; } };
18
+ }
19
+ Object.defineProperty(o, k2, desc);
20
+ }) : (function(o, m, k, k2) {
21
+ if (k2 === undefined) k2 = k;
22
+ o[k2] = m[k];
23
+ }));
24
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
25
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
26
+ }) : function(o, v) {
27
+ o["default"] = v;
28
+ });
29
+ var __importStar = (this && this.__importStar) || (function () {
30
+ var ownKeys = function(o) {
31
+ ownKeys = Object.getOwnPropertyNames || function (o) {
32
+ var ar = [];
33
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
34
+ return ar;
35
+ };
36
+ return ownKeys(o);
37
+ };
38
+ return function (mod) {
39
+ if (mod && mod.__esModule) return mod;
40
+ var result = {};
41
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
42
+ __setModuleDefault(result, mod);
43
+ return result;
44
+ };
45
+ })();
46
+ Object.defineProperty(exports, "__esModule", { value: true });
47
+ exports.JoinGroup = void 0;
48
+ const react_1 = __importStar(require("react"));
49
+ const peers_sdk_1 = require("@peers-app/peers-sdk");
50
+ const loading_indicator_1 = require("../../components/loading-indicator");
51
+ const hooks_1 = require("../../hooks");
52
+ const ui_loader_1 = require("../../ui-router/ui-loader");
53
+ const globals_1 = require("../../globals");
54
+ const JoinGroup = () => {
55
+ const userContext = (0, hooks_1.usePromise)(() => (0, peers_sdk_1.getUserContext)());
56
+ // State
57
+ const [password, setPassword] = (0, react_1.useState)("");
58
+ const [isSearching, setIsSearching] = (0, react_1.useState)(false);
59
+ const [isJoining, setIsJoining] = (0, react_1.useState)(false);
60
+ const [error, setError] = (0, react_1.useState)(null);
61
+ const [currentJoiningGroup, setCurrentJoiningGroup] = (0, react_1.useState)(null);
62
+ // Observable state from device layer
63
+ const [status] = (0, hooks_1.useObservable)(peers_sdk_1.groupJoinStatus);
64
+ const [discoveredListeners] = (0, hooks_1.useObservable)(peers_sdk_1.groupInviteDiscoveryResult);
65
+ const [joinResult] = (0, hooks_1.useObservable)(peers_sdk_1.groupInviteJoinResult);
66
+ // Watch for join results
67
+ (0, react_1.useEffect)(() => {
68
+ if (isJoining && joinResult) {
69
+ setIsJoining(false);
70
+ if (joinResult.success && joinResult.groupId) {
71
+ // Navigate to the group after a brief delay
72
+ setTimeout(() => {
73
+ (0, globals_1.mainContentPath)(`groups/${joinResult.groupId}`);
74
+ }, 2000);
75
+ }
76
+ }
77
+ }, [joinResult, isJoining]);
78
+ // Search for groups (set pvar, device layer reacts)
79
+ const handleSearch = (0, react_1.useCallback)(() => {
80
+ if (!(0, peers_sdk_1.isValidInvitePassword)(password)) {
81
+ setError("Password must be at least 4 characters");
82
+ return;
83
+ }
84
+ setError(null);
85
+ setIsSearching(true);
86
+ // Clear previous results and trigger discovery
87
+ (0, peers_sdk_1.groupInviteDiscoveryResult)([]);
88
+ (0, peers_sdk_1.groupInviteJoinResult)(null);
89
+ (0, peers_sdk_1.groupInviteDiscover)(password);
90
+ }, [password]);
91
+ // Join a group (set pvar, device layer reacts)
92
+ const handleJoin = (0, react_1.useCallback)((listener) => {
93
+ setIsJoining(true);
94
+ setError(null);
95
+ setCurrentJoiningGroup(listener);
96
+ // Clear previous result and trigger join request
97
+ (0, peers_sdk_1.groupInviteJoinResult)(null);
98
+ (0, peers_sdk_1.groupInviteSendRequest)({ listener, password });
99
+ }, [password]);
100
+ // Reset state
101
+ const handleReset = (0, react_1.useCallback)(() => {
102
+ setPassword("");
103
+ setError(null);
104
+ setCurrentJoiningGroup(null);
105
+ setIsSearching(false);
106
+ setIsJoining(false);
107
+ // Clear pvars
108
+ (0, peers_sdk_1.groupInviteDiscoveryResult)([]);
109
+ (0, peers_sdk_1.groupInviteJoinResult)(null);
110
+ }, []);
111
+ // Handle Enter key
112
+ const handleKeyDown = (0, react_1.useCallback)((e) => {
113
+ if (e.key === "Enter" && password && !isSearching) {
114
+ handleSearch();
115
+ }
116
+ }, [password, isSearching, handleSearch]);
117
+ if (!userContext) {
118
+ return react_1.default.createElement(loading_indicator_1.LoadingIndicator, null);
119
+ }
120
+ return (react_1.default.createElement("div", { className: "container-fluid p-4", style: { maxWidth: "600px" } },
121
+ react_1.default.createElement("div", { className: "text-center mb-4" },
122
+ react_1.default.createElement("h2", null,
123
+ react_1.default.createElement("i", { className: "bi-box-arrow-in-right me-2" }),
124
+ "Join Group"),
125
+ react_1.default.createElement("p", { className: "text-muted" }, "Enter the invitation password to find and join a group")),
126
+ joinResult?.success && (react_1.default.createElement("div", { className: "text-center" },
127
+ react_1.default.createElement("div", { className: "mb-4" },
128
+ react_1.default.createElement("i", { className: "bi-check-circle-fill text-success", style: { fontSize: "4rem" } })),
129
+ react_1.default.createElement("h4", { className: "text-success mb-3" }, "Successfully Joined!"),
130
+ react_1.default.createElement("p", { className: "text-muted" }, "Redirecting to the group..."),
131
+ react_1.default.createElement("button", { className: "btn btn-outline-primary", onClick: handleReset },
132
+ react_1.default.createElement("i", { className: "bi-plus-lg me-2" }),
133
+ "Join Another Group"))),
134
+ !joinResult?.success && (react_1.default.createElement(react_1.default.Fragment, null,
135
+ react_1.default.createElement("div", { className: "card mb-4" },
136
+ react_1.default.createElement("div", { className: "card-body" },
137
+ react_1.default.createElement("label", { className: "form-label" },
138
+ react_1.default.createElement("i", { className: "bi-key-fill me-2" }),
139
+ "Invitation Password"),
140
+ react_1.default.createElement("div", { className: "input-group" },
141
+ react_1.default.createElement("input", { type: "text", className: "form-control form-control-lg font-monospace", value: password, onChange: (e) => setPassword(e.target.value), onKeyDown: handleKeyDown, placeholder: "Enter password (e.g., apple-banana-cherry-date)", disabled: isSearching || isJoining, autoFocus: true }),
142
+ react_1.default.createElement("button", { className: "btn btn-primary", onClick: handleSearch, disabled: !password || isSearching || isJoining }, isSearching ? (react_1.default.createElement(react_1.default.Fragment, null,
143
+ react_1.default.createElement("span", { className: "spinner-border spinner-border-sm me-2" }),
144
+ "Searching...")) : (react_1.default.createElement(react_1.default.Fragment, null,
145
+ react_1.default.createElement("i", { className: "bi-search me-2" }),
146
+ "Search")))),
147
+ react_1.default.createElement("small", { className: "text-muted" }, "Ask the group admin for the invitation password"))),
148
+ error && (react_1.default.createElement("div", { className: "alert alert-danger" },
149
+ react_1.default.createElement("i", { className: "bi-exclamation-triangle me-2" }),
150
+ error)),
151
+ status && !error && (react_1.default.createElement("div", { className: "alert alert-info" },
152
+ react_1.default.createElement("i", { className: "bi-info-circle me-2" }),
153
+ status)),
154
+ joinResult && !joinResult.success && (react_1.default.createElement("div", { className: "alert alert-warning" },
155
+ react_1.default.createElement("i", { className: "bi-exclamation-circle me-2" }),
156
+ joinResult.error || "Join request was not approved")),
157
+ discoveredListeners && discoveredListeners.length > 0 && (react_1.default.createElement("div", { className: "card" },
158
+ react_1.default.createElement("div", { className: "card-header" },
159
+ react_1.default.createElement("h6", { className: "mb-0" },
160
+ react_1.default.createElement("i", { className: "bi-broadcast me-2" }),
161
+ "Found Groups (",
162
+ discoveredListeners.length,
163
+ ")")),
164
+ react_1.default.createElement("div", { className: "list-group list-group-flush" }, discoveredListeners.map((listener, index) => (react_1.default.createElement("div", { key: `${listener.deviceId}-${index}`, className: "list-group-item" },
165
+ react_1.default.createElement("div", { className: "d-flex justify-content-between align-items-center" },
166
+ react_1.default.createElement("div", null,
167
+ react_1.default.createElement("div", { className: "d-flex align-items-center mb-1" },
168
+ react_1.default.createElement("i", { className: `${listener.group.iconClassName || "bi-people-fill"} me-2` }),
169
+ react_1.default.createElement("strong", null, listener.group.name)),
170
+ listener.group.description && (react_1.default.createElement("p", { className: "text-muted mb-1 small" }, listener.group.description)),
171
+ react_1.default.createElement("small", { className: "text-muted" },
172
+ react_1.default.createElement("i", { className: "bi-person me-1" }),
173
+ "Invited by: ",
174
+ listener.name || listener.userId)),
175
+ react_1.default.createElement("button", { className: "btn btn-success", onClick: () => handleJoin(listener), disabled: isJoining }, isJoining ? (react_1.default.createElement(react_1.default.Fragment, null,
176
+ react_1.default.createElement("span", { className: "spinner-border spinner-border-sm me-2" }),
177
+ "Joining...")) : (react_1.default.createElement(react_1.default.Fragment, null,
178
+ react_1.default.createElement("i", { className: "bi-box-arrow-in-right me-2" }),
179
+ "Join")))))))))),
180
+ isSearching && (react_1.default.createElement("div", { className: "text-center py-4" },
181
+ react_1.default.createElement("div", { className: "spinner-border text-primary mb-3", role: "status" },
182
+ react_1.default.createElement("span", { className: "visually-hidden" }, "Searching...")),
183
+ react_1.default.createElement("p", { className: "text-muted" }, "Searching for groups..."))),
184
+ isJoining && (react_1.default.createElement("div", { className: "text-center py-4" },
185
+ react_1.default.createElement("div", { className: "spinner-border text-success mb-3", role: "status" },
186
+ react_1.default.createElement("span", { className: "visually-hidden" }, "Joining...")),
187
+ react_1.default.createElement("p", { className: "text-muted" }, "Waiting for approval..."),
188
+ react_1.default.createElement("small", { className: "text-muted" }, "The group admin needs to approve your request")))))));
189
+ };
190
+ exports.JoinGroup = JoinGroup;
191
+ // Register the route
192
+ (0, ui_loader_1.registerInternalPeersUI)({
193
+ peersUIId: "00join0group0screen000000",
194
+ component: exports.JoinGroup,
195
+ routes: [
196
+ {
197
+ isMatch: (props, context) => context.path === "join-group",
198
+ uiCategory: "screen",
199
+ priority: 2,
200
+ },
201
+ ],
202
+ });