@peers-app/peers-ui 0.25.3 → 0.25.4
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/screens/invites/accept-invite.js +44 -2
- package/dist/screens/invites/accept-invite.test.d.ts +1 -0
- package/dist/screens/invites/accept-invite.test.js +103 -0
- package/dist/screens/invites/invite-classify.test.js +17 -0
- package/dist/screens/invites/pending-invites-list.js +7 -1
- package/package.json +3 -3
- package/src/screens/invites/accept-invite.test.tsx +115 -0
- package/src/screens/invites/accept-invite.tsx +49 -2
- package/src/screens/invites/invite-classify.test.ts +20 -1
- package/src/screens/invites/pending-invites-list.tsx +9 -1
|
@@ -21,14 +21,17 @@ function PreviewCard(props) {
|
|
|
21
21
|
function ResultPanel(props) {
|
|
22
22
|
const { result, preview } = props;
|
|
23
23
|
const accepted = result.status === "accepted";
|
|
24
|
+
const openGroup = preview.type === "group" && preview.group?.mode === "open";
|
|
24
25
|
const message = accepted
|
|
25
26
|
? preview.type === "group"
|
|
26
27
|
? `You joined ${preview.group?.name}.`
|
|
27
28
|
: "Contact added."
|
|
28
29
|
: preview.type === "group"
|
|
29
|
-
?
|
|
30
|
+
? openGroup
|
|
31
|
+
? `Finishing your automatic join to ${preview.group?.name}.`
|
|
32
|
+
: "Join request sent. You will be added once it is approved."
|
|
30
33
|
: "Request sent. They will see it and confirm.";
|
|
31
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className: "text-center", children: [(0, jsx_runtime_1.jsxs)("output", { className: `alert d-block ${accepted ? "alert-success" : "alert-info"}`, children: [message, result.deliveryState === "queued" ? ((0, jsx_runtime_1.jsx)("div", { className: "small mt-1", children: "
|
|
34
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "text-center", children: [(0, jsx_runtime_1.jsxs)("output", { className: `alert d-block ${accepted ? "alert-success" : "alert-info"}`, children: [message, result.deliveryState === "queued" ? ((0, jsx_runtime_1.jsx)("div", { className: "small mt-1", children: "Waiting for a route; Peers will retry automatically." })) : null] }), (0, jsx_runtime_1.jsxs)("div", { className: "d-flex gap-2 justify-content-center", children: [accepted && result.groupId ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-primary", onClick: () => (0, globals_1.mainContentPath)(`identity/groups/${result.groupId}`), children: "Open group" })) : null, (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-outline-secondary", onClick: props.onDone, children: "Done" })] })] }));
|
|
32
35
|
}
|
|
33
36
|
/**
|
|
34
37
|
* Paste or scan an invite, review what it grants, then accept it. Also consumes a token
|
|
@@ -73,6 +76,45 @@ function AcceptInvite(props) {
|
|
|
73
76
|
});
|
|
74
77
|
return () => sub.dispose();
|
|
75
78
|
}, [props.initialToken, runPreview]);
|
|
79
|
+
const resultInviteId = result?.inviteId;
|
|
80
|
+
const resultStatus = result?.status;
|
|
81
|
+
(0, react_1.useEffect)(() => {
|
|
82
|
+
if (!resultInviteId || resultStatus !== "pending") {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
let disposed = false;
|
|
86
|
+
let unsubscribe;
|
|
87
|
+
void (0, peers_sdk_1.getUserContext)().then(async (userContext) => {
|
|
88
|
+
if (disposed) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const table = (0, peers_sdk_1.Invites)(userContext.userDataContext);
|
|
92
|
+
const refresh = async () => {
|
|
93
|
+
const row = await table.get(resultInviteId);
|
|
94
|
+
if (!disposed && row) {
|
|
95
|
+
setResult((current) => current?.inviteId === resultInviteId
|
|
96
|
+
? {
|
|
97
|
+
...current,
|
|
98
|
+
status: row.status,
|
|
99
|
+
deliveryState: row.deliveryState,
|
|
100
|
+
groupId: row.groupId ?? current.groupId,
|
|
101
|
+
}
|
|
102
|
+
: current);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
const subscription = table.dataChanged.subscribe((event) => {
|
|
106
|
+
if (event.dataObject.inviteId === resultInviteId) {
|
|
107
|
+
void refresh();
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
unsubscribe = () => subscription.unsubscribe();
|
|
111
|
+
await refresh();
|
|
112
|
+
});
|
|
113
|
+
return () => {
|
|
114
|
+
disposed = true;
|
|
115
|
+
unsubscribe?.();
|
|
116
|
+
};
|
|
117
|
+
}, [resultInviteId, resultStatus]);
|
|
76
118
|
const accept = async () => {
|
|
77
119
|
setError("");
|
|
78
120
|
setBusy(true);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
7
|
+
const peers_sdk_1 = require("@peers-app/peers-sdk");
|
|
8
|
+
const react_1 = require("@testing-library/react");
|
|
9
|
+
const user_event_1 = __importDefault(require("@testing-library/user-event"));
|
|
10
|
+
const accept_invite_1 = require("./accept-invite");
|
|
11
|
+
const invites_ipc_1 = require("./invites-ipc");
|
|
12
|
+
jest.mock("@peers-app/peers-sdk", () => {
|
|
13
|
+
const actual = jest.requireActual("@peers-app/peers-sdk");
|
|
14
|
+
return {
|
|
15
|
+
...actual,
|
|
16
|
+
getUserContext: jest.fn(actual.getUserContext),
|
|
17
|
+
Invites: jest.fn(),
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
jest.mock("./invites-ipc", () => ({
|
|
21
|
+
inviteTools: jest.fn(),
|
|
22
|
+
}));
|
|
23
|
+
jest.mock("../../globals", () => ({
|
|
24
|
+
mainContentPath: jest.fn(),
|
|
25
|
+
}));
|
|
26
|
+
const preview = {
|
|
27
|
+
type: "group",
|
|
28
|
+
inviter: {
|
|
29
|
+
userId: "abcdefghijklmnopqrstuvwxy",
|
|
30
|
+
publicKey: "a".repeat(43),
|
|
31
|
+
publicBoxKey: "b".repeat(43),
|
|
32
|
+
displayName: "Ada",
|
|
33
|
+
},
|
|
34
|
+
expiresAt: "2026-10-01T00:00:00.000Z",
|
|
35
|
+
expired: false,
|
|
36
|
+
knownContact: false,
|
|
37
|
+
group: {
|
|
38
|
+
groupId: "zyxwvutsrqponmlkjihgfedcb",
|
|
39
|
+
name: "Neighbors",
|
|
40
|
+
role: 20,
|
|
41
|
+
mode: "open",
|
|
42
|
+
alreadyMember: false,
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
const pendingResult = {
|
|
46
|
+
inviteId: "openjoinrequest0000000001",
|
|
47
|
+
status: "pending",
|
|
48
|
+
deliveryState: "queued",
|
|
49
|
+
groupId: preview.group?.groupId,
|
|
50
|
+
};
|
|
51
|
+
function invite(status, deliveryState) {
|
|
52
|
+
return {
|
|
53
|
+
inviteId: pendingResult.inviteId,
|
|
54
|
+
direction: "in",
|
|
55
|
+
type: "group",
|
|
56
|
+
status,
|
|
57
|
+
mode: "open",
|
|
58
|
+
counterpart: preview.inviter,
|
|
59
|
+
groupId: preview.group?.groupId,
|
|
60
|
+
groupName: preview.group?.name,
|
|
61
|
+
role: preview.group?.role,
|
|
62
|
+
epoch: 0,
|
|
63
|
+
recipientUserId: preview.inviter.userId,
|
|
64
|
+
deliveryState,
|
|
65
|
+
createdAt: new Date("2026-09-22T00:00:00.000Z"),
|
|
66
|
+
expiresAt: new Date(preview.expiresAt),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
describe("AcceptInvite", () => {
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
jest.clearAllMocks();
|
|
72
|
+
});
|
|
73
|
+
it("shows automatic progress and reacts when the delayed approval arrives", async () => {
|
|
74
|
+
let row = invite("pending", "queued");
|
|
75
|
+
let onDataChanged;
|
|
76
|
+
const table = {
|
|
77
|
+
get: jest.fn(async () => row),
|
|
78
|
+
dataChanged: {
|
|
79
|
+
subscribe: jest.fn((listener) => {
|
|
80
|
+
onDataChanged = listener;
|
|
81
|
+
return { unsubscribe: jest.fn() };
|
|
82
|
+
}),
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
peers_sdk_1.getUserContext.mockResolvedValue({ userDataContext: {} });
|
|
86
|
+
peers_sdk_1.Invites.mockReturnValue(table);
|
|
87
|
+
invites_ipc_1.inviteTools.mockResolvedValue({
|
|
88
|
+
previewInvite: jest.fn().mockResolvedValue({ preview }),
|
|
89
|
+
acceptInvite: jest.fn().mockResolvedValue({ result: pendingResult }),
|
|
90
|
+
});
|
|
91
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(accept_invite_1.AcceptInvite, { initialToken: "signed-token" }));
|
|
92
|
+
await user_event_1.default.click(await react_1.screen.findByRole("button", { name: "Join" }));
|
|
93
|
+
expect(await react_1.screen.findByText("Finishing your automatic join to Neighbors.")).toBeVisible();
|
|
94
|
+
expect(react_1.screen.getByText("Waiting for a route; Peers will retry automatically.")).toBeVisible();
|
|
95
|
+
expect(react_1.screen.queryByText(/once it is approved/i)).not.toBeInTheDocument();
|
|
96
|
+
row = invite("accepted", "sent");
|
|
97
|
+
await (0, react_1.act)(async () => {
|
|
98
|
+
onDataChanged?.({ dataObject: row });
|
|
99
|
+
});
|
|
100
|
+
await (0, react_1.waitFor)(() => expect(react_1.screen.getByText("You joined Neighbors.")).toBeVisible());
|
|
101
|
+
expect(react_1.screen.getByRole("button", { name: "Open group" })).toBeEnabled();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -95,4 +95,21 @@ describe("classifyInvite", () => {
|
|
|
95
95
|
status: "accepted",
|
|
96
96
|
}))).toBe("history");
|
|
97
97
|
});
|
|
98
|
+
it("describes an open join as automatic rather than approval-gated", () => {
|
|
99
|
+
const openJoin = invite({
|
|
100
|
+
inviteId: "openjoinrequest0000000001",
|
|
101
|
+
direction: "in",
|
|
102
|
+
type: "group",
|
|
103
|
+
mode: "open",
|
|
104
|
+
counterpart: { ...counterpart, displayName: "Ada" },
|
|
105
|
+
recipientUserId: counterpart.userId,
|
|
106
|
+
groupName: "Neighbors",
|
|
107
|
+
deliveryState: "queued",
|
|
108
|
+
});
|
|
109
|
+
expect((0, pending_invites_list_1.describeInviteRow)(openJoin)).toEqual({
|
|
110
|
+
title: "Joining Neighbors",
|
|
111
|
+
detail: "Completing automatically; no approval is required.",
|
|
112
|
+
});
|
|
113
|
+
expect((0, pending_invites_list_1.classifyInvite)(openJoin)).toBe("waiting");
|
|
114
|
+
});
|
|
98
115
|
});
|
|
@@ -88,6 +88,12 @@ function describeInviteRow(row) {
|
|
|
88
88
|
}
|
|
89
89
|
if (row.direction === "in") {
|
|
90
90
|
if (row.recipientUserId) {
|
|
91
|
+
if (row.mode === "open") {
|
|
92
|
+
return {
|
|
93
|
+
title: `Joining ${group}`,
|
|
94
|
+
detail: "Completing automatically; no approval is required.",
|
|
95
|
+
};
|
|
96
|
+
}
|
|
91
97
|
return { title: `Request to join ${group}`, detail: `Waiting for ${who} to approve.` };
|
|
92
98
|
}
|
|
93
99
|
if (row.mode === "direct") {
|
|
@@ -191,5 +197,5 @@ function InviteRow({ row }) {
|
|
|
191
197
|
setBusy(undefined);
|
|
192
198
|
}
|
|
193
199
|
};
|
|
194
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className: "list-group-item", children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex justify-content-between align-items-center gap-2 flex-wrap", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex-grow-1", children: [(0, jsx_runtime_1.jsxs)("div", { className: "fw-semibold", children: [(0, jsx_runtime_1.jsx)("span", { className: "badge text-bg-light border me-2 text-capitalize", children: row.type }), title] }), detail ? (0, jsx_runtime_1.jsx)("small", { className: "text-muted d-block", children: detail }) : null, row.deliveryState === "queued" ? ((0, jsx_runtime_1.jsx)("small", { className: "text-warning d-block", children: "
|
|
200
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "list-group-item", children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex justify-content-between align-items-center gap-2 flex-wrap", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex-grow-1", children: [(0, jsx_runtime_1.jsxs)("div", { className: "fw-semibold", children: [(0, jsx_runtime_1.jsx)("span", { className: "badge text-bg-light border me-2 text-capitalize", children: row.type }), title] }), detail ? (0, jsx_runtime_1.jsx)("small", { className: "text-muted d-block", children: detail }) : null, row.deliveryState === "queued" ? ((0, jsx_runtime_1.jsx)("small", { className: "text-warning d-block", children: "Waiting for a route; Peers will retry automatically" })) : null, row.status !== "pending" ? ((0, jsx_runtime_1.jsx)("small", { className: "text-muted d-block text-capitalize", children: row.status })) : null] }), (0, jsx_runtime_1.jsx)("div", { className: "d-flex gap-1", children: actionsForInvite(row).map((action) => ((0, jsx_runtime_1.jsx)("button", { type: "button", className: `btn btn-sm ${ACTION_LABELS[action].className}`, disabled: busy !== undefined, onClick: () => void run(action), children: busy === action ? "…" : ACTION_LABELS[action].text }, action))) })] }), error ? (0, jsx_runtime_1.jsx)("div", { className: "alert alert-danger py-1 px-2 small mt-2 mb-0", children: error }) : null] }));
|
|
195
201
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peers-app/peers-ui",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.4",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/peers-app/peers-ui.git"
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"lint:fix": "biome check --write ."
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
|
-
"@peers-app/peers-sdk": "^0.25.
|
|
31
|
+
"@peers-app/peers-sdk": "^0.25.4",
|
|
32
32
|
"bootstrap": "^5.3.3",
|
|
33
33
|
"react": "^18.0.0",
|
|
34
34
|
"react-dom": "^18.0.0"
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"@babel/preset-react": "^7.24.1",
|
|
40
40
|
"@babel/preset-typescript": "^7.27.1",
|
|
41
41
|
"@biomejs/biome": "2.3.11",
|
|
42
|
-
"@peers-app/peers-sdk": "0.25.
|
|
42
|
+
"@peers-app/peers-sdk": "0.25.4",
|
|
43
43
|
"@testing-library/dom": "^10.4.0",
|
|
44
44
|
"@testing-library/jest-dom": "^6.6.3",
|
|
45
45
|
"@testing-library/react": "^16.3.0",
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getUserContext,
|
|
3
|
+
type IAcceptInviteResult,
|
|
4
|
+
type IInvite,
|
|
5
|
+
type IInvitePreview,
|
|
6
|
+
Invites,
|
|
7
|
+
} from "@peers-app/peers-sdk";
|
|
8
|
+
import { act, render, screen, waitFor } from "@testing-library/react";
|
|
9
|
+
import userEvent from "@testing-library/user-event";
|
|
10
|
+
import { AcceptInvite } from "./accept-invite";
|
|
11
|
+
import { inviteTools } from "./invites-ipc";
|
|
12
|
+
|
|
13
|
+
jest.mock("@peers-app/peers-sdk", () => {
|
|
14
|
+
const actual = jest.requireActual<typeof import("@peers-app/peers-sdk")>("@peers-app/peers-sdk");
|
|
15
|
+
return {
|
|
16
|
+
...actual,
|
|
17
|
+
getUserContext: jest.fn(actual.getUserContext),
|
|
18
|
+
Invites: jest.fn(),
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
jest.mock("./invites-ipc", () => ({
|
|
23
|
+
inviteTools: jest.fn(),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
jest.mock("../../globals", () => ({
|
|
27
|
+
mainContentPath: jest.fn(),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
const preview: IInvitePreview = {
|
|
31
|
+
type: "group",
|
|
32
|
+
inviter: {
|
|
33
|
+
userId: "abcdefghijklmnopqrstuvwxy",
|
|
34
|
+
publicKey: "a".repeat(43),
|
|
35
|
+
publicBoxKey: "b".repeat(43),
|
|
36
|
+
displayName: "Ada",
|
|
37
|
+
},
|
|
38
|
+
expiresAt: "2026-10-01T00:00:00.000Z",
|
|
39
|
+
expired: false,
|
|
40
|
+
knownContact: false,
|
|
41
|
+
group: {
|
|
42
|
+
groupId: "zyxwvutsrqponmlkjihgfedcb",
|
|
43
|
+
name: "Neighbors",
|
|
44
|
+
role: 20,
|
|
45
|
+
mode: "open",
|
|
46
|
+
alreadyMember: false,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const pendingResult: IAcceptInviteResult = {
|
|
51
|
+
inviteId: "openjoinrequest0000000001",
|
|
52
|
+
status: "pending",
|
|
53
|
+
deliveryState: "queued",
|
|
54
|
+
groupId: preview.group?.groupId,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function invite(status: IInvite["status"], deliveryState: IInvite["deliveryState"]): IInvite {
|
|
58
|
+
return {
|
|
59
|
+
inviteId: pendingResult.inviteId,
|
|
60
|
+
direction: "in",
|
|
61
|
+
type: "group",
|
|
62
|
+
status,
|
|
63
|
+
mode: "open",
|
|
64
|
+
counterpart: preview.inviter,
|
|
65
|
+
groupId: preview.group?.groupId,
|
|
66
|
+
groupName: preview.group?.name,
|
|
67
|
+
role: preview.group?.role,
|
|
68
|
+
epoch: 0,
|
|
69
|
+
recipientUserId: preview.inviter.userId,
|
|
70
|
+
deliveryState,
|
|
71
|
+
createdAt: new Date("2026-09-22T00:00:00.000Z"),
|
|
72
|
+
expiresAt: new Date(preview.expiresAt),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
describe("AcceptInvite", () => {
|
|
77
|
+
beforeEach(() => {
|
|
78
|
+
jest.clearAllMocks();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("shows automatic progress and reacts when the delayed approval arrives", async () => {
|
|
82
|
+
let row = invite("pending", "queued");
|
|
83
|
+
let onDataChanged: ((event: { dataObject: IInvite }) => void) | undefined;
|
|
84
|
+
const table = {
|
|
85
|
+
get: jest.fn(async () => row),
|
|
86
|
+
dataChanged: {
|
|
87
|
+
subscribe: jest.fn((listener: typeof onDataChanged) => {
|
|
88
|
+
onDataChanged = listener;
|
|
89
|
+
return { unsubscribe: jest.fn() };
|
|
90
|
+
}),
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
(getUserContext as jest.Mock).mockResolvedValue({ userDataContext: {} });
|
|
94
|
+
(Invites as jest.Mock).mockReturnValue(table);
|
|
95
|
+
(inviteTools as jest.Mock).mockResolvedValue({
|
|
96
|
+
previewInvite: jest.fn().mockResolvedValue({ preview }),
|
|
97
|
+
acceptInvite: jest.fn().mockResolvedValue({ result: pendingResult }),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
render(<AcceptInvite initialToken="signed-token" />);
|
|
101
|
+
await userEvent.click(await screen.findByRole("button", { name: "Join" }));
|
|
102
|
+
|
|
103
|
+
expect(await screen.findByText("Finishing your automatic join to Neighbors.")).toBeVisible();
|
|
104
|
+
expect(screen.getByText("Waiting for a route; Peers will retry automatically.")).toBeVisible();
|
|
105
|
+
expect(screen.queryByText(/once it is approved/i)).not.toBeInTheDocument();
|
|
106
|
+
|
|
107
|
+
row = invite("accepted", "sent");
|
|
108
|
+
await act(async () => {
|
|
109
|
+
onDataChanged?.({ dataObject: row });
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
await waitFor(() => expect(screen.getByText("You joined Neighbors.")).toBeVisible());
|
|
113
|
+
expect(screen.getByRole("button", { name: "Open group" })).toBeEnabled();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getRoleLabel,
|
|
3
|
+
getUserContext,
|
|
3
4
|
type IAcceptInviteResult,
|
|
4
5
|
type IInvitePreview,
|
|
6
|
+
Invites,
|
|
5
7
|
TrustLevel,
|
|
6
8
|
} from "@peers-app/peers-sdk";
|
|
7
9
|
import { useCallback, useEffect, useState } from "react";
|
|
@@ -59,19 +61,22 @@ function ResultPanel(props: {
|
|
|
59
61
|
}) {
|
|
60
62
|
const { result, preview } = props;
|
|
61
63
|
const accepted = result.status === "accepted";
|
|
64
|
+
const openGroup = preview.type === "group" && preview.group?.mode === "open";
|
|
62
65
|
const message = accepted
|
|
63
66
|
? preview.type === "group"
|
|
64
67
|
? `You joined ${preview.group?.name}.`
|
|
65
68
|
: "Contact added."
|
|
66
69
|
: preview.type === "group"
|
|
67
|
-
?
|
|
70
|
+
? openGroup
|
|
71
|
+
? `Finishing your automatic join to ${preview.group?.name}.`
|
|
72
|
+
: "Join request sent. You will be added once it is approved."
|
|
68
73
|
: "Request sent. They will see it and confirm.";
|
|
69
74
|
return (
|
|
70
75
|
<div className="text-center">
|
|
71
76
|
<output className={`alert d-block ${accepted ? "alert-success" : "alert-info"}`}>
|
|
72
77
|
{message}
|
|
73
78
|
{result.deliveryState === "queued" ? (
|
|
74
|
-
<div className="small mt-1">
|
|
79
|
+
<div className="small mt-1">Waiting for a route; Peers will retry automatically.</div>
|
|
75
80
|
) : null}
|
|
76
81
|
</output>
|
|
77
82
|
<div className="d-flex gap-2 justify-content-center">
|
|
@@ -136,6 +141,48 @@ export function AcceptInvite(props: AcceptInviteProps) {
|
|
|
136
141
|
return () => sub.dispose();
|
|
137
142
|
}, [props.initialToken, runPreview]);
|
|
138
143
|
|
|
144
|
+
const resultInviteId = result?.inviteId;
|
|
145
|
+
const resultStatus = result?.status;
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
if (!resultInviteId || resultStatus !== "pending") {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
let disposed = false;
|
|
151
|
+
let unsubscribe: (() => void) | undefined;
|
|
152
|
+
void getUserContext().then(async (userContext) => {
|
|
153
|
+
if (disposed) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const table = Invites(userContext.userDataContext);
|
|
157
|
+
const refresh = async () => {
|
|
158
|
+
const row = await table.get(resultInviteId);
|
|
159
|
+
if (!disposed && row) {
|
|
160
|
+
setResult((current) =>
|
|
161
|
+
current?.inviteId === resultInviteId
|
|
162
|
+
? {
|
|
163
|
+
...current,
|
|
164
|
+
status: row.status,
|
|
165
|
+
deliveryState: row.deliveryState,
|
|
166
|
+
groupId: row.groupId ?? current.groupId,
|
|
167
|
+
}
|
|
168
|
+
: current,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
const subscription = table.dataChanged.subscribe((event) => {
|
|
173
|
+
if (event.dataObject.inviteId === resultInviteId) {
|
|
174
|
+
void refresh();
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
unsubscribe = () => subscription.unsubscribe();
|
|
178
|
+
await refresh();
|
|
179
|
+
});
|
|
180
|
+
return () => {
|
|
181
|
+
disposed = true;
|
|
182
|
+
unsubscribe?.();
|
|
183
|
+
};
|
|
184
|
+
}, [resultInviteId, resultStatus]);
|
|
185
|
+
|
|
139
186
|
const accept = async () => {
|
|
140
187
|
setError("");
|
|
141
188
|
setBusy(true);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { IInvite } from "@peers-app/peers-sdk";
|
|
2
|
-
import { actionsForInvite, classifyInvite } from "./pending-invites-list";
|
|
2
|
+
import { actionsForInvite, classifyInvite, describeInviteRow } from "./pending-invites-list";
|
|
3
3
|
|
|
4
4
|
function invite(
|
|
5
5
|
partial: Partial<IInvite> & Pick<IInvite, "inviteId" | "direction" | "type" | "mode">,
|
|
@@ -107,4 +107,23 @@ describe("classifyInvite", () => {
|
|
|
107
107
|
),
|
|
108
108
|
).toBe("history");
|
|
109
109
|
});
|
|
110
|
+
|
|
111
|
+
it("describes an open join as automatic rather than approval-gated", () => {
|
|
112
|
+
const openJoin = invite({
|
|
113
|
+
inviteId: "openjoinrequest0000000001",
|
|
114
|
+
direction: "in",
|
|
115
|
+
type: "group",
|
|
116
|
+
mode: "open",
|
|
117
|
+
counterpart: { ...counterpart, displayName: "Ada" },
|
|
118
|
+
recipientUserId: counterpart.userId,
|
|
119
|
+
groupName: "Neighbors",
|
|
120
|
+
deliveryState: "queued",
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(describeInviteRow(openJoin)).toEqual({
|
|
124
|
+
title: "Joining Neighbors",
|
|
125
|
+
detail: "Completing automatically; no approval is required.",
|
|
126
|
+
});
|
|
127
|
+
expect(classifyInvite(openJoin)).toBe("waiting");
|
|
128
|
+
});
|
|
110
129
|
});
|
|
@@ -104,6 +104,12 @@ export function describeInviteRow(row: IInvite): { title: string; detail: string
|
|
|
104
104
|
}
|
|
105
105
|
if (row.direction === "in") {
|
|
106
106
|
if (row.recipientUserId) {
|
|
107
|
+
if (row.mode === "open") {
|
|
108
|
+
return {
|
|
109
|
+
title: `Joining ${group}`,
|
|
110
|
+
detail: "Completing automatically; no approval is required.",
|
|
111
|
+
};
|
|
112
|
+
}
|
|
107
113
|
return { title: `Request to join ${group}`, detail: `Waiting for ${who} to approve.` };
|
|
108
114
|
}
|
|
109
115
|
if (row.mode === "direct") {
|
|
@@ -222,7 +228,9 @@ export function InviteRow({ row }: { row: IInvite }) {
|
|
|
222
228
|
</div>
|
|
223
229
|
{detail ? <small className="text-muted d-block">{detail}</small> : null}
|
|
224
230
|
{row.deliveryState === "queued" ? (
|
|
225
|
-
<small className="text-warning d-block">
|
|
231
|
+
<small className="text-warning d-block">
|
|
232
|
+
Waiting for a route; Peers will retry automatically
|
|
233
|
+
</small>
|
|
226
234
|
) : null}
|
|
227
235
|
{row.status !== "pending" ? (
|
|
228
236
|
<small className="text-muted d-block text-capitalize">{row.status}</small>
|