@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.
- package/dist/components/left-bar.js +1 -3
- package/dist/components/router.d.ts +1 -0
- package/dist/components/router.js +2 -5
- package/dist/screens/groups/group-invite-listener.d.ts +15 -0
- package/dist/screens/groups/group-invite-listener.js +189 -0
- package/dist/screens/groups/group-members.js +3 -1
- package/dist/screens/join-group/index.d.ts +2 -0
- package/dist/screens/join-group/index.js +19 -0
- package/dist/screens/join-group/join-group.d.ts +13 -0
- package/dist/screens/join-group/join-group.js +202 -0
- package/dist/screens/settings/settings-page.js +76 -28
- package/dist/screens/welcome-modal.d.ts +7 -0
- package/dist/screens/welcome-modal.js +193 -0
- package/dist/system-apps/index.d.ts +1 -0
- package/dist/system-apps/index.js +5 -1
- package/dist/system-apps/join-group.app.d.ts +2 -0
- package/dist/system-apps/join-group.app.js +9 -0
- package/dist/tabs-layout/tabs-layout.js +14 -3
- package/package.json +3 -3
- package/src/components/left-bar.tsx +1 -2
- package/src/components/router.tsx +2 -5
- package/src/screens/groups/group-invite-listener.tsx +285 -0
- package/src/screens/groups/group-members.tsx +7 -1
- package/src/screens/join-group/index.ts +4 -0
- package/src/screens/join-group/join-group.tsx +294 -0
- package/src/screens/settings/settings-page.tsx +127 -40
- package/src/screens/welcome-modal.tsx +258 -0
- package/src/system-apps/index.ts +3 -0
- package/src/system-apps/join-group.app.ts +8 -0
- package/src/tabs-layout/tabs-layout.tsx +21 -3
|
@@ -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: "#
|
|
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,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
|
+
});
|