@protobase/ui 0.62.4 → 0.63.0
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/account/account-page.js +3 -0
- package/dist/account/blocks/connected-accounts-block.js +1 -1
- package/dist/account/blocks/personal-mail-block.js +36 -0
- package/dist/account/messages.js +4 -0
- package/dist/account/sign-in-providers-section.js +3 -3
- package/dist/app/app.d.ts +1 -1
- package/dist/app/app.js +22 -5
- package/dist/app/pages/render-node.js +2 -0
- package/dist/auth/auth-provider.js +7 -0
- package/dist/calls/call-check.js +2 -2
- package/dist/calls/call-controls.js +16 -4
- package/dist/calls/call-guests-panel.js +200 -0
- package/dist/calls/call-lobby-notice.js +34 -0
- package/dist/calls/call-message.js +4 -3
- package/dist/calls/call-people.js +3 -2
- package/dist/calls/call-stage.js +4 -2
- package/dist/calls/call-view.d.ts +9 -3
- package/dist/calls/call-view.js +37 -6
- package/dist/calls/call-waiting.js +20 -0
- package/dist/calls/guest-admission.js +40 -0
- package/dist/calls/guest-call-link.js +19 -0
- package/dist/calls/guest-call-page.js +82 -0
- package/dist/calls/messages.js +69 -3
- package/dist/calls/use-call-session.d.ts +7 -0
- package/dist/calls/use-call-session.js +27 -2
- package/dist/calls/use-guest-request.js +24 -0
- package/dist/data/api-provider.d.ts +8 -0
- package/dist/index.js +1 -1
- package/dist/organizations/messages.js +6 -0
- package/dist/organizations/organization-page.js +33 -1
- package/dist/package.js +1 -1
- package/dist/shell/assets/app-CKCVGFSR.js +9 -0
- package/dist/shell/assets/calls-DRnrKDC9.js +2 -0
- package/dist/shell/assets/{index-D8oHLXjS.js → index-BK-wuwB0.js} +2 -2
- package/dist/shell/assets/index-Be8nCyRW.css +2 -0
- package/dist/shell/assets/{records-map-DOXcutuQ.js → records-map-Bd5scHzJ.js} +2 -2
- package/dist/shell/assets/shared-protobase-ui-CU8QfOuc.js +2 -0
- package/dist/shell/index.html +4 -4
- package/dist/shell/protobase.shell.json +2 -2
- package/dist/shell/protobase.sw.js +2 -2
- package/dist/shell-source-maps/assets/app-CKCVGFSR.js.map +1 -0
- package/dist/shell-source-maps/assets/calls-DRnrKDC9.js.map +1 -0
- package/dist/shell-source-maps/assets/{index-D8oHLXjS.js.map → index-BK-wuwB0.js.map} +1 -1
- package/dist/shell-source-maps/assets/{records-map-DOXcutuQ.js.map → records-map-Bd5scHzJ.js.map} +1 -1
- package/dist/shell-source-maps/assets/shared-protobase-ui-CU8QfOuc.js.map +1 -0
- package/package.json +4 -4
- package/dist/shell/assets/app-B_ojAtG3.js +0 -9
- package/dist/shell/assets/calls-C63xSTRj.js +0 -2
- package/dist/shell/assets/index-BgcF_WZ0.css +0 -2
- package/dist/shell/assets/shared-protobase-ui-CX80Ectk.js +0 -2
- package/dist/shell-source-maps/assets/app-B_ojAtG3.js.map +0 -1
- package/dist/shell-source-maps/assets/calls-C63xSTRj.js.map +0 -1
- package/dist/shell-source-maps/assets/shared-protobase-ui-CX80Ectk.js.map +0 -1
package/dist/calls/call-view.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { useOptionalAuth } from "../auth/auth-provider.js";
|
|
2
|
+
import { useRouter } from "../app/router.js";
|
|
2
3
|
import { text } from "./messages.js";
|
|
3
4
|
import { CallCheck } from "./call-check.js";
|
|
5
|
+
import { CallGuestsPanel } from "./call-guests-panel.js";
|
|
6
|
+
import { CallLobbyNotice } from "./call-lobby-notice.js";
|
|
4
7
|
import { CallMessage } from "./call-message.js";
|
|
5
8
|
import { callPeople } from "./call-people.js";
|
|
6
9
|
import { CallStage } from "./call-stage.js";
|
|
10
|
+
import { CallWaiting } from "./call-waiting.js";
|
|
11
|
+
import { guestCallLink } from "./guest-call-link.js";
|
|
7
12
|
import { useActiveSpeaker } from "./use-audio-levels.js";
|
|
8
13
|
import { useCallSession } from "./use-call-session.js";
|
|
9
14
|
import { useState } from "react";
|
|
@@ -19,11 +24,14 @@ const deviceNotices = (devices, refusedSending) => [
|
|
|
19
24
|
* A call, ready-made, that fills its parent, such as a column beside a conversation's messages: a check of the
|
|
20
25
|
* microphone and camera first, then, after Join, everyone in the call with the one speaking ringed, a screen share
|
|
21
26
|
* large, microphone, camera, screen share and Leave, the quality of each connection, Reconnecting while the link is
|
|
22
|
-
* restored, and plain words for a full call, a blocked device or someone the browser cannot connect to.
|
|
23
|
-
*
|
|
27
|
+
* restored, and plain words for a full call, a blocked device or someone the browser cannot connect to. A member the app
|
|
28
|
+
* lets invite also gets the call's guests: who waits in the lobby, and its guest links. Made for small peer-to-peer
|
|
29
|
+
* calls, at a desktop's width or a phone's.
|
|
24
30
|
*/
|
|
25
|
-
const CallView = ({ url, leave, nameOf = (member) => member, className }) => {
|
|
26
|
-
const session = useCallSession(url);
|
|
31
|
+
const CallView = ({ url, leave, nameOf = (member) => member, admission, name, className }) => {
|
|
32
|
+
const session = useCallSession(url, { ...admission && { admission } });
|
|
33
|
+
const [guestsOpen, setGuestsOpen] = useState(false);
|
|
34
|
+
const { basePath } = useRouter();
|
|
27
35
|
const auth = useOptionalAuth();
|
|
28
36
|
const user = auth?.state.kind === "signed-in" ? auth.state.user : void 0;
|
|
29
37
|
const self = session.self ?? user?.id ?? "";
|
|
@@ -43,8 +51,10 @@ const CallView = ({ url, leave, nameOf = (member) => member, className }) => {
|
|
|
43
51
|
reason: phase.reason,
|
|
44
52
|
onJoinAgain: () => void session.join()
|
|
45
53
|
});
|
|
54
|
+
if (phase.kind === "waiting") return /* @__PURE__ */ jsx(CallWaiting, { leave });
|
|
46
55
|
if (phase.kind === "check") return /* @__PURE__ */ jsx(CallCheck, {
|
|
47
|
-
name: user?.name ?? text("you"),
|
|
56
|
+
name: name ?? user?.name ?? text("you"),
|
|
57
|
+
...admission && { joinLabel: text("askToJoin") },
|
|
48
58
|
member: self,
|
|
49
59
|
...devices?.camera && { camera: devices.camera },
|
|
50
60
|
microphoneOn: session.microphoneOn,
|
|
@@ -57,6 +67,11 @@ const CallView = ({ url, leave, nameOf = (member) => member, className }) => {
|
|
|
57
67
|
onJoin: () => void session.join(),
|
|
58
68
|
cancel: leave
|
|
59
69
|
});
|
|
70
|
+
const { lobby, guests } = session;
|
|
71
|
+
const inCall = session.members.filter((member) => member.member.startsWith("guest:")).map((member) => ({
|
|
72
|
+
guest: member.member.slice(6),
|
|
73
|
+
name: member.name ?? ""
|
|
74
|
+
}));
|
|
60
75
|
const people = callPeople({
|
|
61
76
|
members: session.members,
|
|
62
77
|
self,
|
|
@@ -73,6 +88,10 @@ const CallView = ({ url, leave, nameOf = (member) => member, className }) => {
|
|
|
73
88
|
people,
|
|
74
89
|
state: phase.kind === "joining" ? "joining" : phase.resuming ? "resuming" : "joined",
|
|
75
90
|
notices,
|
|
91
|
+
...lobby && guests && { aside: /* @__PURE__ */ jsx(CallLobbyNotice, {
|
|
92
|
+
waiting: lobby.guests,
|
|
93
|
+
guests
|
|
94
|
+
}) },
|
|
76
95
|
controls: {
|
|
77
96
|
microphoneOn: session.microphoneOn && Boolean(devices?.microphone),
|
|
78
97
|
cameraOn: session.cameraOn && Boolean(devices?.camera),
|
|
@@ -80,8 +99,20 @@ const CallView = ({ url, leave, nameOf = (member) => member, className }) => {
|
|
|
80
99
|
onMicrophone: session.toggleMicrophone,
|
|
81
100
|
onCamera: session.toggleCamera,
|
|
82
101
|
...session.toggleScreen && { onShare: () => void session.toggleScreen() },
|
|
102
|
+
...lobby && { guests: {
|
|
103
|
+
waiting: lobby.guests.length,
|
|
104
|
+
onOpen: () => setGuestsOpen(true)
|
|
105
|
+
} },
|
|
83
106
|
leave
|
|
84
|
-
}
|
|
107
|
+
},
|
|
108
|
+
children: lobby && guests && /* @__PURE__ */ jsx(CallGuestsPanel, {
|
|
109
|
+
lobby,
|
|
110
|
+
inCall,
|
|
111
|
+
guests,
|
|
112
|
+
linkOf: (token) => guestCallLink(`${window.location.origin}${basePath}`, token),
|
|
113
|
+
open: guestsOpen,
|
|
114
|
+
onClose: () => setGuestsOpen(false)
|
|
115
|
+
})
|
|
85
116
|
});
|
|
86
117
|
};
|
|
87
118
|
//#endregion
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Spinner } from "../primitives/spinner.js";
|
|
2
|
+
import { RouteLink } from "../primitives/route-link.js";
|
|
3
|
+
import { text } from "./messages.js";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
//#region src/calls/call-waiting.tsx
|
|
6
|
+
/** A guest's wait in the lobby, until someone in the call lets them in, with Leave. */
|
|
7
|
+
const CallWaiting = ({ leave }) => /* @__PURE__ */ jsxs("section", {
|
|
8
|
+
role: "status",
|
|
9
|
+
className: "flex size-full min-h-0 flex-col items-center justify-center gap-3 bg-background p-4 text-center",
|
|
10
|
+
children: [/* @__PURE__ */ jsxs("p", {
|
|
11
|
+
className: "flex items-center gap-2 text-sm text-foreground",
|
|
12
|
+
children: [/* @__PURE__ */ jsx(Spinner, { className: "size-4" }), text("waiting")]
|
|
13
|
+
}), /* @__PURE__ */ jsx(RouteLink, {
|
|
14
|
+
to: leave,
|
|
15
|
+
className: "px-2 text-sm text-muted-foreground hover:text-foreground",
|
|
16
|
+
children: text("leave")
|
|
17
|
+
})]
|
|
18
|
+
});
|
|
19
|
+
//#endregion
|
|
20
|
+
export { CallWaiting };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
//#region src/calls/guest-admission.ts
|
|
2
|
+
/** Why a guest is not let into the call: turned away, the link or call is over, or the lobby cannot be reached now. */
|
|
3
|
+
var NotLetIn = class extends Error {
|
|
4
|
+
reason;
|
|
5
|
+
constructor(reason) {
|
|
6
|
+
super(`Not let into the call: ${reason}`);
|
|
7
|
+
this.reason = reason;
|
|
8
|
+
this.name = "NotLetIn";
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
/** How often a waiting guest asks after their wait. */
|
|
12
|
+
const waitEveryMs = 1e3;
|
|
13
|
+
const later = (ms, signal) => new Promise((resolve) => {
|
|
14
|
+
const timer = setTimeout(resolve, ms);
|
|
15
|
+
signal.addEventListener("abort", () => (clearTimeout(timer), resolve()), { once: true });
|
|
16
|
+
});
|
|
17
|
+
/**
|
|
18
|
+
* A guest's way into the call: they wait in the lobby of the link, by their name, asking once a second, until a member
|
|
19
|
+
* lets them in, with the guest's token for `joinCall`, or turns them away, or the link or call is over. Aborting leaves
|
|
20
|
+
* the lobby.
|
|
21
|
+
*/
|
|
22
|
+
const guestAdmission = async (lobby, link, name, signal) => {
|
|
23
|
+
const { LobbyRefused } = await import("@protobase/client/calls");
|
|
24
|
+
const refusal = (error) => {
|
|
25
|
+
if (!(error instanceof LobbyRefused)) return error;
|
|
26
|
+
return new NotLetIn(error.status === 404 ? "refused" : error.status === 429 ? "unavailable" : "ended");
|
|
27
|
+
};
|
|
28
|
+
const { token } = await lobby.enter(link, name).catch((error) => Promise.reject(refusal(error)));
|
|
29
|
+
while (!signal.aborted) {
|
|
30
|
+
const wait = await lobby.wait(token).catch((error) => Promise.reject(refusal(error)));
|
|
31
|
+
if (wait.state === "admitted") return token;
|
|
32
|
+
if (wait.state === "denied") throw new NotLetIn("denied");
|
|
33
|
+
if (wait.state === "ended") throw new NotLetIn("ended");
|
|
34
|
+
await later(waitEveryMs, signal);
|
|
35
|
+
}
|
|
36
|
+
await lobby.leave(token);
|
|
37
|
+
throw new NotLetIn("ended");
|
|
38
|
+
};
|
|
39
|
+
//#endregion
|
|
40
|
+
export { NotLetIn, guestAdmission, waitEveryMs };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useRouter } from "../app/router.js";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
//#region src/calls/guest-call-link.ts
|
|
4
|
+
/** The path a guest link opens, below the app's base path: its token follows in the fragment, which no server sees. */
|
|
5
|
+
const guestCallLinkPath = "/-/call-guest";
|
|
6
|
+
/** The guest link of a token, at the app's base, such as `https://chat.example.com`. */
|
|
7
|
+
const guestCallLink = (base, token) => `${base}${guestCallLinkPath}#${token}`;
|
|
8
|
+
/** The guest link of this path and fragment: its token, or `undefined` for any other page. */
|
|
9
|
+
const readGuestCallLink = (path, hash) => {
|
|
10
|
+
const token = hash.replace(/^#/, "");
|
|
11
|
+
return path === "/-/call-guest" && token ? { token } : void 0;
|
|
12
|
+
};
|
|
13
|
+
/** The guest link the address shows, `/-/call-guest#<token>`, as logical values; `undefined` on any other page. */
|
|
14
|
+
const useGuestCallAddress = () => {
|
|
15
|
+
const { path } = useRouter();
|
|
16
|
+
return useMemo(() => readGuestCallLink(path, globalThis.location?.hash ?? ""), [path]);
|
|
17
|
+
};
|
|
18
|
+
//#endregion
|
|
19
|
+
export { guestCallLink, guestCallLinkPath, readGuestCallLink, useGuestCallAddress };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Button } from "../primitives/button.js";
|
|
2
|
+
import { Input } from "../primitives/input.js";
|
|
3
|
+
import { text } from "./messages.js";
|
|
4
|
+
import { guestCallLink } from "./guest-call-link.js";
|
|
5
|
+
import { guestAdmission } from "./guest-admission.js";
|
|
6
|
+
import { CallView } from "./call-view.js";
|
|
7
|
+
import { useCallback, useState } from "react";
|
|
8
|
+
import { guestCallsPath } from "@protobase/schema/internal";
|
|
9
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
|
+
//#region src/calls/guest-call-page.tsx
|
|
11
|
+
/**
|
|
12
|
+
* The page a guest link opens, for someone with no account: their name, then the call view's check of their microphone
|
|
13
|
+
* and camera, Ask to join, the wait in the lobby until someone in the call lets them in, and the call. They reach
|
|
14
|
+
* nothing else of the app: the page asks only the lobby and the guest call socket.
|
|
15
|
+
*/
|
|
16
|
+
const GuestCallPage = ({ link, systemPath, base }) => {
|
|
17
|
+
const [draft, setDraft] = useState("");
|
|
18
|
+
const [name, setName] = useState();
|
|
19
|
+
const [left, setLeft] = useState(false);
|
|
20
|
+
const admission = useCallback(async (signal) => {
|
|
21
|
+
const { guestLobby } = await import("@protobase/client/calls");
|
|
22
|
+
return guestAdmission(guestLobby({ systemPath }), link, name, signal);
|
|
23
|
+
}, [
|
|
24
|
+
systemPath,
|
|
25
|
+
link,
|
|
26
|
+
name
|
|
27
|
+
]);
|
|
28
|
+
const submit = (event) => {
|
|
29
|
+
event.preventDefault();
|
|
30
|
+
if (draft.trim()) setName(draft.trim());
|
|
31
|
+
};
|
|
32
|
+
const here = guestCallLink(base, link);
|
|
33
|
+
if (left) return /* @__PURE__ */ jsx("p", {
|
|
34
|
+
role: "status",
|
|
35
|
+
className: "flex min-h-[100dvh] items-center justify-center bg-background p-4 text-sm text-foreground",
|
|
36
|
+
children: text("youLeft")
|
|
37
|
+
});
|
|
38
|
+
if (name === void 0) return /* @__PURE__ */ jsx("main", {
|
|
39
|
+
className: "flex min-h-[100dvh] items-center justify-center bg-background p-4",
|
|
40
|
+
children: /* @__PURE__ */ jsxs("form", {
|
|
41
|
+
onSubmit: submit,
|
|
42
|
+
className: "flex w-full max-w-sm flex-col gap-3",
|
|
43
|
+
children: [
|
|
44
|
+
/* @__PURE__ */ jsx("p", {
|
|
45
|
+
className: "text-sm text-foreground",
|
|
46
|
+
children: text("guestIntro")
|
|
47
|
+
}),
|
|
48
|
+
/* @__PURE__ */ jsxs("label", {
|
|
49
|
+
className: "flex flex-col gap-1 text-sm text-foreground",
|
|
50
|
+
children: [text("yourName"), /* @__PURE__ */ jsx(Input, {
|
|
51
|
+
autoFocus: true,
|
|
52
|
+
autoComplete: "name",
|
|
53
|
+
maxLength: 60,
|
|
54
|
+
value: draft,
|
|
55
|
+
onChange: (event) => setDraft(event.target.value)
|
|
56
|
+
})]
|
|
57
|
+
}),
|
|
58
|
+
/* @__PURE__ */ jsx(Button, {
|
|
59
|
+
type: "submit",
|
|
60
|
+
variant: "primary",
|
|
61
|
+
disabled: !draft.trim(),
|
|
62
|
+
children: text("continue")
|
|
63
|
+
})
|
|
64
|
+
]
|
|
65
|
+
})
|
|
66
|
+
});
|
|
67
|
+
return /* @__PURE__ */ jsx("main", {
|
|
68
|
+
className: "h-[100dvh]",
|
|
69
|
+
children: /* @__PURE__ */ jsx(CallView, {
|
|
70
|
+
url: guestCallsPath(systemPath, "socket"),
|
|
71
|
+
admission,
|
|
72
|
+
name,
|
|
73
|
+
nameOf: () => text("someone"),
|
|
74
|
+
leave: {
|
|
75
|
+
href: here,
|
|
76
|
+
onFollow: () => setLeft(true)
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
//#endregion
|
|
82
|
+
export { GuestCallPage };
|
package/dist/calls/messages.js
CHANGED
|
@@ -21,7 +21,7 @@ const text = defineMessages({
|
|
|
21
21
|
reconnecting: "Reconnecting…",
|
|
22
22
|
full: "This call is full",
|
|
23
23
|
removed: "You were removed from this call",
|
|
24
|
-
ended: "The call
|
|
24
|
+
ended: "The call ended",
|
|
25
25
|
refused: "You cannot join this call",
|
|
26
26
|
unavailable: "The call cannot be reached now. Try again in a moment.",
|
|
27
27
|
callAgain: "Join again",
|
|
@@ -34,7 +34,40 @@ const text = defineMessages({
|
|
|
34
34
|
sharing: (name) => `${name} is sharing their screen`,
|
|
35
35
|
youAreSharing: "You are sharing your screen",
|
|
36
36
|
speaking: "Speaking",
|
|
37
|
-
preview: "Your camera before you join"
|
|
37
|
+
preview: "Your camera before you join",
|
|
38
|
+
guestName: (name) => `${name} (guest)`,
|
|
39
|
+
denied: "Nobody in the call let you in",
|
|
40
|
+
waiting: "Waiting for someone in the call to let you in…",
|
|
41
|
+
askToJoin: "Ask to join",
|
|
42
|
+
someone: "Someone in the call",
|
|
43
|
+
guests: "Guests",
|
|
44
|
+
waitingGuest: (name) => `${name} is waiting to join`,
|
|
45
|
+
admit: "Let in",
|
|
46
|
+
deny: "Turn away",
|
|
47
|
+
remove: "Remove",
|
|
48
|
+
revoke: "Revoke",
|
|
49
|
+
newLink: "Create guest link",
|
|
50
|
+
copyLink: "Copy link",
|
|
51
|
+
copied: "Copied",
|
|
52
|
+
guestLinks: "Guest links",
|
|
53
|
+
noLinks: "No guest link yet. Anyone with a link waits here until someone in the call lets them in.",
|
|
54
|
+
inLobby: "Waiting to join",
|
|
55
|
+
guestsInCall: "Guests in the call",
|
|
56
|
+
guestsMaySend: "Guests may send",
|
|
57
|
+
sendAll: "Everything",
|
|
58
|
+
sendCamera: "Camera and mic",
|
|
59
|
+
sendMicrophone: "Mic only",
|
|
60
|
+
linkUntil: (until) => `Until ${until}`,
|
|
61
|
+
linkUntilEnd: "Until the call ends",
|
|
62
|
+
linkLasts: "The link works",
|
|
63
|
+
forAnHour: "For an hour",
|
|
64
|
+
addressesVisible: "In a call between browsers, a guest and the people in the call can see each other’s network addresses.",
|
|
65
|
+
guestRefused: "This could not be done: the guest or link is gone, the call is full, or guest links are off.",
|
|
66
|
+
close: "Close",
|
|
67
|
+
yourName: "Your name",
|
|
68
|
+
guestIntro: "You are invited to a call. Give your name, check your microphone and camera, and someone in the call lets you in.",
|
|
69
|
+
continue: "Continue",
|
|
70
|
+
youLeft: "You left the call"
|
|
38
71
|
},
|
|
39
72
|
nl: {
|
|
40
73
|
accept: "Opnemen",
|
|
@@ -69,7 +102,40 @@ const text = defineMessages({
|
|
|
69
102
|
sharing: (name) => `${name} deelt het scherm`,
|
|
70
103
|
youAreSharing: "Je deelt je scherm",
|
|
71
104
|
speaking: "Spreekt",
|
|
72
|
-
preview: "Je camera voordat je deelneemt"
|
|
105
|
+
preview: "Je camera voordat je deelneemt",
|
|
106
|
+
guestName: (name) => `${name} (gast)`,
|
|
107
|
+
denied: "Niemand in het gesprek heeft je binnengelaten",
|
|
108
|
+
waiting: "Wachten tot iemand in het gesprek je binnenlaat…",
|
|
109
|
+
askToJoin: "Vragen om deel te nemen",
|
|
110
|
+
someone: "Iemand in het gesprek",
|
|
111
|
+
guests: "Gasten",
|
|
112
|
+
waitingGuest: (name) => `${name} wacht om deel te nemen`,
|
|
113
|
+
admit: "Binnenlaten",
|
|
114
|
+
deny: "Weigeren",
|
|
115
|
+
remove: "Verwijderen",
|
|
116
|
+
revoke: "Intrekken",
|
|
117
|
+
newLink: "Gastlink maken",
|
|
118
|
+
copyLink: "Link kopiëren",
|
|
119
|
+
copied: "Gekopieerd",
|
|
120
|
+
guestLinks: "Gastlinks",
|
|
121
|
+
noLinks: "Nog geen gastlink. Wie een link heeft, wacht hier tot iemand in het gesprek hem binnenlaat.",
|
|
122
|
+
inLobby: "Wacht om deel te nemen",
|
|
123
|
+
guestsInCall: "Gasten in het gesprek",
|
|
124
|
+
guestsMaySend: "Gasten mogen sturen",
|
|
125
|
+
sendAll: "Alles",
|
|
126
|
+
sendCamera: "Camera en microfoon",
|
|
127
|
+
sendMicrophone: "Alleen geluid",
|
|
128
|
+
linkUntil: (until) => `Tot ${until}`,
|
|
129
|
+
linkUntilEnd: "Tot het gesprek afloopt",
|
|
130
|
+
linkLasts: "De link werkt",
|
|
131
|
+
forAnHour: "Een uur lang",
|
|
132
|
+
addressesVisible: "In een gesprek tussen browsers zien een gast en de mensen in het gesprek elkaars netwerkadressen.",
|
|
133
|
+
guestRefused: "Dit lukte niet: de gast of link is weg, het gesprek is vol, of gastlinks staan uit.",
|
|
134
|
+
close: "Sluiten",
|
|
135
|
+
yourName: "Je naam",
|
|
136
|
+
guestIntro: "Je bent uitgenodigd voor een gesprek. Geef je naam, controleer je microfoon en camera, en iemand in het gesprek laat je binnen.",
|
|
137
|
+
continue: "Verder",
|
|
138
|
+
youLeft: "Je hebt het gesprek verlaten"
|
|
73
139
|
}
|
|
74
140
|
});
|
|
75
141
|
//#endregion
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import "@protobase/client/calls";
|
|
2
|
+
//#region src/calls/use-call-session.d.ts
|
|
3
|
+
export type CallSessionOptions = {
|
|
4
|
+
/** For a guest: their wait in the lobby, which resolves with their token once let in, or rejects with `NotLetIn`. */
|
|
5
|
+
admission?: (signal: AbortSignal) => Promise<string>;
|
|
6
|
+
};
|
|
7
|
+
//#endregion
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { openDevices } from "./devices.js";
|
|
2
|
+
import { NotLetIn } from "./guest-admission.js";
|
|
2
3
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
4
|
//#region src/calls/use-call-session.ts
|
|
4
5
|
const connectionsEveryMs = 2e3;
|
|
@@ -8,10 +9,12 @@ const stop = (tracks) => {
|
|
|
8
9
|
/**
|
|
9
10
|
* The member's session in the call the app's function at `url` admits them to: their devices from the start, for the
|
|
10
11
|
* check before joining; then the call, which `join` joins after the click, with what they send and receive, and who
|
|
11
|
-
* is in it. Leaving the page leaves the call and stops the devices.
|
|
12
|
+
* is in it. A guest waits in the lobby first. Leaving the page leaves the call and stops the devices.
|
|
12
13
|
*/
|
|
13
|
-
const useCallSession = (url) => {
|
|
14
|
+
const useCallSession = (url, { admission } = {}) => {
|
|
14
15
|
const [phase, setPhase] = useState({ kind: "check" });
|
|
16
|
+
const [lobby, setLobby] = useState();
|
|
17
|
+
const waiting = useRef(new AbortController());
|
|
15
18
|
const [devices, setDevices] = useState();
|
|
16
19
|
const [microphoneOn, setMicrophoneOn] = useState(true);
|
|
17
20
|
const [cameraOn, setCameraOn] = useState(true);
|
|
@@ -40,6 +43,7 @@ const useCallSession = (url) => {
|
|
|
40
43
|
return () => {
|
|
41
44
|
cancelled = true;
|
|
42
45
|
gone.current = true;
|
|
46
|
+
waiting.current.abort();
|
|
43
47
|
call.current?.leave();
|
|
44
48
|
call.current = void 0;
|
|
45
49
|
stop(own.current);
|
|
@@ -54,11 +58,28 @@ const useCallSession = (url) => {
|
|
|
54
58
|
const timer = setInterval(() => void call.current?.connections().then(setConnections), connectionsEveryMs);
|
|
55
59
|
return () => clearInterval(timer);
|
|
56
60
|
}, [phase.kind]);
|
|
61
|
+
const admitted = async () => {
|
|
62
|
+
if (!admission) return {};
|
|
63
|
+
setPhase({ kind: "waiting" });
|
|
64
|
+
waiting.current = new AbortController();
|
|
65
|
+
const guest = await admission(waiting.current.signal).then((token) => token, (error) => {
|
|
66
|
+
if (!(error instanceof NotLetIn)) throw error;
|
|
67
|
+
setPhase({
|
|
68
|
+
kind: "left",
|
|
69
|
+
reason: error.reason
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
return guest === void 0 ? void 0 : { guest };
|
|
73
|
+
};
|
|
57
74
|
const join = useCallback(async () => {
|
|
75
|
+
const guest = await admitted();
|
|
76
|
+
if (!guest || gone.current) return;
|
|
58
77
|
setPhase({ kind: "joining" });
|
|
59
78
|
const { joinCall, PublishRefused } = await import("@protobase/client/calls");
|
|
60
79
|
const joined = await joinCall({
|
|
61
80
|
url,
|
|
81
|
+
...guest,
|
|
82
|
+
onLobby: setLobby,
|
|
62
83
|
onTracks: setTracks,
|
|
63
84
|
onMembers: setMembers,
|
|
64
85
|
onState: (state) => {
|
|
@@ -89,6 +110,7 @@ const useCallSession = (url) => {
|
|
|
89
110
|
});
|
|
90
111
|
}, [
|
|
91
112
|
url,
|
|
113
|
+
admission,
|
|
92
114
|
microphoneOn,
|
|
93
115
|
cameraOn
|
|
94
116
|
]);
|
|
@@ -147,6 +169,9 @@ const useCallSession = (url) => {
|
|
|
147
169
|
refusedSending,
|
|
148
170
|
seen: seen.current,
|
|
149
171
|
self: call.current?.participant,
|
|
172
|
+
/** The lobby, once the server tells a member who may invite of it. */
|
|
173
|
+
lobby,
|
|
174
|
+
guests: call.current?.guests,
|
|
150
175
|
join,
|
|
151
176
|
toggleMicrophone,
|
|
152
177
|
toggleCamera,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useCallback, useState } from "react";
|
|
2
|
+
//#region src/calls/use-guest-request.ts
|
|
3
|
+
/**
|
|
4
|
+
* Runs a member's guest request, and says when the server refused it, as `GuestRefused`; any other failure is thrown
|
|
5
|
+
* on, where the page's error handling sees it.
|
|
6
|
+
*/
|
|
7
|
+
const useGuestRequest = () => {
|
|
8
|
+
const [refused, setRefused] = useState(false);
|
|
9
|
+
return {
|
|
10
|
+
run: useCallback((request) => {
|
|
11
|
+
setRefused(false);
|
|
12
|
+
(async () => {
|
|
13
|
+
const { GuestRefused } = await import("@protobase/client/calls");
|
|
14
|
+
await request().catch((error) => {
|
|
15
|
+
if (!(error instanceof GuestRefused)) throw error;
|
|
16
|
+
setRefused(true);
|
|
17
|
+
});
|
|
18
|
+
})();
|
|
19
|
+
}, []),
|
|
20
|
+
refused
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
//#endregion
|
|
24
|
+
export { useGuestRequest };
|
|
@@ -83,6 +83,10 @@ export declare const useClient: () => {
|
|
|
83
83
|
list: () => Promise<import("@protobase/client").CallRing[]>;
|
|
84
84
|
decline: (call: string) => Promise<void>;
|
|
85
85
|
};
|
|
86
|
+
callGuests: {
|
|
87
|
+
allowed: () => Promise<boolean>;
|
|
88
|
+
setAllowed: (allowed: boolean) => Promise<void>;
|
|
89
|
+
};
|
|
86
90
|
};
|
|
87
91
|
/**
|
|
88
92
|
* The client of the API that serves `resource`: a source's for a resource the app draws from one, the app's own
|
|
@@ -168,6 +172,10 @@ export declare const useResourceClient: (resource: string) => {
|
|
|
168
172
|
list: () => Promise<import("@protobase/client").CallRing[]>;
|
|
169
173
|
decline: (call: string) => Promise<void>;
|
|
170
174
|
};
|
|
175
|
+
callGuests: {
|
|
176
|
+
allowed: () => Promise<boolean>;
|
|
177
|
+
setAllowed: (allowed: boolean) => Promise<void>;
|
|
178
|
+
};
|
|
171
179
|
};
|
|
172
180
|
export type ApiProviderProps = {
|
|
173
181
|
/** Defaults to `/api/v1` on the current origin. */
|
package/dist/index.js
CHANGED
|
@@ -35,6 +35,7 @@ import { usePlugin } from "./plugins/plugin-context.js";
|
|
|
35
35
|
import { pluginApi, protobaseVersion } from "./plugins/plugin-api.js";
|
|
36
36
|
import { useApiToken } from "./auth/use-api-token.js";
|
|
37
37
|
import { useRecord } from "./data/use-record.js";
|
|
38
|
+
import { CallView } from "./calls/call-view.js";
|
|
38
39
|
import { chatDayLabel, sameChatDay } from "./chat/chat-day.js";
|
|
39
40
|
import { ChatDivider } from "./chat/chat-divider.js";
|
|
40
41
|
import { ChatMessage } from "./chat/chat-message.js";
|
|
@@ -63,7 +64,6 @@ import { isConflict, useUpdateRecord } from "./data/use-update-record.js";
|
|
|
63
64
|
import { App } from "./app/app.js";
|
|
64
65
|
import { RecordLink } from "./app/displays/record-link.js";
|
|
65
66
|
import { useSignedInUser } from "./auth/use-signed-in-user.js";
|
|
66
|
-
import { CallView } from "./calls/call-view.js";
|
|
67
67
|
import { useRecordCallAddress } from "./calls/use-record-call-address.js";
|
|
68
68
|
import { RecordCall } from "./calls/record-call.js";
|
|
69
69
|
import { useRecordCalls } from "./calls/use-record-calls.js";
|
|
@@ -60,6 +60,9 @@ const text = defineMessages({
|
|
|
60
60
|
details: "Details",
|
|
61
61
|
detailsEditable: "The name people see, and the slug that names it in links.",
|
|
62
62
|
detailsReadOnly: "Owners and admins can change these.",
|
|
63
|
+
guestLinks: "Guest links to calls",
|
|
64
|
+
guestLinksDescription: "Someone in a call can share a link that lets a person from outside wait in its lobby and join that one call once let in. Turning guest links off revokes every link and takes every guest out at once. Owners and admins can change this.",
|
|
65
|
+
guestLinksAllowed: "Allow guest links",
|
|
63
66
|
handOver: "Hand over",
|
|
64
67
|
handOverDescription: "Make another member the owner. You stay as an admin.",
|
|
65
68
|
newOwner: "New owner",
|
|
@@ -149,6 +152,9 @@ const text = defineMessages({
|
|
|
149
152
|
details: "Gegevens",
|
|
150
153
|
detailsEditable: "De naam die mensen zien, en de slug die haar in links noemt.",
|
|
151
154
|
detailsReadOnly: "Eigenaren en beheerders kunnen dit wijzigen.",
|
|
155
|
+
guestLinks: "Gastlinks voor gesprekken",
|
|
156
|
+
guestLinksDescription: "Iemand in een gesprek kan een link delen waarmee iemand van buiten in de wachtruimte wacht en, eenmaal binnengelaten, aan dat ene gesprek deelneemt. Gastlinks uitzetten trekt elke link in en haalt elke gast meteen uit het gesprek. Eigenaren en beheerders kunnen dit wijzigen.",
|
|
157
|
+
guestLinksAllowed: "Gastlinks toestaan",
|
|
152
158
|
handOver: "Overdragen",
|
|
153
159
|
handOverDescription: "Maak een ander lid eigenaar. Jij blijft beheerder.",
|
|
154
160
|
newOwner: "Nieuwe eigenaar",
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { useAuth } from "../auth/auth-provider.js";
|
|
2
|
+
import { useClient } from "../data/api-provider.js";
|
|
3
|
+
import { useMeta } from "../data/use-meta.js";
|
|
2
4
|
import { Button } from "../primitives/button.js";
|
|
3
5
|
import { Dialog } from "../primitives/dialog.js";
|
|
4
6
|
import { Input } from "../primitives/input.js";
|
|
7
|
+
import { Switch } from "../primitives/switch.js";
|
|
5
8
|
import { PageHeader } from "../app-shell/page-header.js";
|
|
6
9
|
import { AccountSection, SectionError } from "../account/account-section.js";
|
|
7
10
|
import { useAction } from "./use-action.js";
|
|
@@ -146,6 +149,30 @@ const Delete = ({ organization, basePath }) => {
|
|
|
146
149
|
]
|
|
147
150
|
});
|
|
148
151
|
};
|
|
152
|
+
const GuestLinks = ({ editable }) => {
|
|
153
|
+
const client = useClient();
|
|
154
|
+
const [allowed, setAllowed] = useState();
|
|
155
|
+
const { busy, error, run } = useAction();
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
client.callGuests.allowed().then(setAllowed);
|
|
158
|
+
}, [client]);
|
|
159
|
+
if (allowed === void 0) return null;
|
|
160
|
+
return /* @__PURE__ */ jsxs(AccountSection, {
|
|
161
|
+
title: text("guestLinks"),
|
|
162
|
+
description: text("guestLinksDescription"),
|
|
163
|
+
children: [/* @__PURE__ */ jsx(SectionError, { message: error }), /* @__PURE__ */ jsxs("label", {
|
|
164
|
+
className: "flex items-center gap-2 text-sm text-foreground",
|
|
165
|
+
children: [/* @__PURE__ */ jsx(Switch, {
|
|
166
|
+
checked: allowed,
|
|
167
|
+
label: text("guestLinksAllowed"),
|
|
168
|
+
...editable && !busy && { onChange: (next) => void run(async () => {
|
|
169
|
+
await client.callGuests.setAllowed(next);
|
|
170
|
+
setAllowed(next);
|
|
171
|
+
}) }
|
|
172
|
+
}), text("guestLinksAllowed")]
|
|
173
|
+
})]
|
|
174
|
+
});
|
|
175
|
+
};
|
|
149
176
|
const Leave = ({ organization, basePath, owner }) => {
|
|
150
177
|
const { session } = useAuth();
|
|
151
178
|
const { busy, error, run } = useAction();
|
|
@@ -162,8 +189,12 @@ const Leave = ({ organization, basePath, owner }) => {
|
|
|
162
189
|
})]
|
|
163
190
|
});
|
|
164
191
|
};
|
|
165
|
-
/**
|
|
192
|
+
/**
|
|
193
|
+
* The organization one works in: its details, whether its calls take guests, and handing it over, leaving and deleting
|
|
194
|
+
* it, as one's role allows.
|
|
195
|
+
*/
|
|
166
196
|
const OrganizationPage = ({ current, basePath, onChanged }) => {
|
|
197
|
+
const calls = useMeta().data?.calls === true;
|
|
167
198
|
const organization = current?.organization;
|
|
168
199
|
if (!organization) return /* @__PURE__ */ jsx("div", {
|
|
169
200
|
className: "mx-auto w-full max-w-3xl px-4 py-6 md:px-6",
|
|
@@ -185,6 +216,7 @@ const OrganizationPage = ({ current, basePath, onChanged }) => {
|
|
|
185
216
|
editable: managesOrganization(current),
|
|
186
217
|
onSaved: onChanged
|
|
187
218
|
}, `${organization.name}/${organization.slug}`),
|
|
219
|
+
calls && current.role && /* @__PURE__ */ jsx(GuestLinks, { editable: managesOrganization(current) }),
|
|
188
220
|
owner && current.memberId && /* @__PURE__ */ jsx(HandOver, {
|
|
189
221
|
organization,
|
|
190
222
|
memberId: current.memberId
|
package/dist/package.js
CHANGED