@protobase/ui 0.62.5 → 0.63.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/app.d.ts +1 -1
- package/dist/app/app.js +22 -5
- 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-BsTdlWO_.js +9 -0
- package/dist/shell/assets/calls-BxifWSFQ.js +2 -0
- package/dist/shell/assets/index-Be8nCyRW.css +2 -0
- package/dist/shell/assets/{index-C6bDRZz0.js → index-Bl1d-Zon.js} +2 -2
- package/dist/shell/assets/{records-map-B5MPNRQc.js → records-map-C29oyYdz.js} +2 -2
- package/dist/shell/assets/shared-protobase-ui-DLQLiVpe.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-BsTdlWO_.js.map +1 -0
- package/dist/shell-source-maps/assets/calls-BxifWSFQ.js.map +1 -0
- package/dist/shell-source-maps/assets/{index-C6bDRZz0.js.map → index-Bl1d-Zon.js.map} +1 -1
- package/dist/shell-source-maps/assets/{records-map-B5MPNRQc.js.map → records-map-C29oyYdz.js.map} +1 -1
- package/dist/shell-source-maps/assets/shared-protobase-ui-DLQLiVpe.js.map +1 -0
- package/package.json +4 -4
- package/dist/shell/assets/app-z3__9x3P.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-COFhNI_R.js +0 -2
- package/dist/shell-source-maps/assets/app-z3__9x3P.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-COFhNI_R.js.map +0 -1
package/dist/app/app.d.ts
CHANGED
|
@@ -35,7 +35,7 @@ export type AppProps = {
|
|
|
35
35
|
nativeBridge?: NativeBridge;
|
|
36
36
|
};
|
|
37
37
|
/**
|
|
38
|
-
* The admin: a public form at `/-/forms/<name>` for anyone, the sign-in route first, `/-/sign-in`, or browsing as a
|
|
38
|
+
* The admin: a public form at `/-/forms/<name>` for anyone, a guest link's call at `/-/call-guest` for its guest, the sign-in route first, `/-/sign-in`, or browsing as a
|
|
39
39
|
* visitor when the project's anonymous role reads, then a sidebar from `/meta`, `/:page` composed pages, `/:resource`
|
|
40
40
|
* lists and `/:resource/:key` records. Fills its parent, in the theme this browser chose, light, dark or the system's.
|
|
41
41
|
*/
|
package/dist/app/app.js
CHANGED
|
@@ -32,6 +32,8 @@ import { useLiveUpdates } from "../data/use-live-updates.js";
|
|
|
32
32
|
import { useOpenedRecord } from "../data/use-record.js";
|
|
33
33
|
import { PublicFormPage } from "../forms/public-form-page.js";
|
|
34
34
|
import { useFormAddress } from "../forms/use-form-address.js";
|
|
35
|
+
import { useGuestCallAddress } from "../calls/guest-call-link.js";
|
|
36
|
+
import { GuestCallPage } from "../calls/guest-call-page.js";
|
|
35
37
|
import { ThemeProvider, useTheme } from "../theme/theme-provider.js";
|
|
36
38
|
import { AssistantButton, AssistantPanel } from "./assistant-shell.js";
|
|
37
39
|
import { bottomBarFromMeta } from "./bottom-bar-from-meta.js";
|
|
@@ -332,8 +334,20 @@ const PublicFormGate = ({ baseUrl, client, ui, children }) => {
|
|
|
332
334
|
})
|
|
333
335
|
});
|
|
334
336
|
};
|
|
337
|
+
const GuestCallGate = ({ baseUrl, children }) => {
|
|
338
|
+
const address = useGuestCallAddress();
|
|
339
|
+
const { basePath } = useRouter();
|
|
340
|
+
if (!address) return children;
|
|
341
|
+
const api = new URL(baseUrl ?? "/api/v1", window.location.href);
|
|
342
|
+
const systemPath = `${api.origin}${api.pathname.slice(0, api.pathname.lastIndexOf("/"))}`;
|
|
343
|
+
return /* @__PURE__ */ jsx(GuestCallPage, {
|
|
344
|
+
link: address.token,
|
|
345
|
+
systemPath,
|
|
346
|
+
base: `${window.location.origin}${basePath}`
|
|
347
|
+
});
|
|
348
|
+
};
|
|
335
349
|
/**
|
|
336
|
-
* The admin: a public form at `/-/forms/<name>` for anyone, the sign-in route first, `/-/sign-in`, or browsing as a
|
|
350
|
+
* The admin: a public form at `/-/forms/<name>` for anyone, a guest link's call at `/-/call-guest` for its guest, the sign-in route first, `/-/sign-in`, or browsing as a
|
|
337
351
|
* visitor when the project's anonymous role reads, then a sidebar from `/meta`, `/:page` composed pages, `/:resource`
|
|
338
352
|
* lists and `/:resource/:key` records. Fills its parent, in the theme this browser chose, light, dark or the system's.
|
|
339
353
|
*/
|
|
@@ -356,11 +370,14 @@ const App = ({ auth, workspace, basePath, initialUrl, pluginLoader, locale, nati
|
|
|
356
370
|
baseUrl: rest.baseUrl,
|
|
357
371
|
client: rest.client,
|
|
358
372
|
ui: rest.ui,
|
|
359
|
-
children: /* @__PURE__ */ jsx(
|
|
360
|
-
|
|
361
|
-
children: /* @__PURE__ */ jsx(
|
|
373
|
+
children: /* @__PURE__ */ jsx(GuestCallGate, {
|
|
374
|
+
...rest.baseUrl && { baseUrl: rest.baseUrl },
|
|
375
|
+
children: /* @__PURE__ */ jsx(AuthGate, {
|
|
362
376
|
workspace,
|
|
363
|
-
|
|
377
|
+
children: /* @__PURE__ */ jsx(Authenticated, {
|
|
378
|
+
workspace,
|
|
379
|
+
...rest
|
|
380
|
+
})
|
|
364
381
|
})
|
|
365
382
|
})
|
|
366
383
|
}) })
|
package/dist/calls/call-check.js
CHANGED
|
@@ -13,7 +13,7 @@ const toggle = (on) => cn("size-10 rounded-full", !on && "bg-danger-soft text-da
|
|
|
13
13
|
* The check of the microphone and camera before joining: the member's own camera as the others will see it, or their
|
|
14
14
|
* initials, microphone and camera on and off, plain messages for a blocked or missing device, and Join.
|
|
15
15
|
*/
|
|
16
|
-
const CallCheck = ({ name, member, camera, microphoneOn, cameraOn, microphone, cameraAvailable, notices, onMicrophone, onCamera, onJoin, cancel }) => /* @__PURE__ */ jsxs("section", {
|
|
16
|
+
const CallCheck = ({ name, member, camera, microphoneOn, cameraOn, microphone, cameraAvailable, notices, onMicrophone, onCamera, onJoin, cancel, joinLabel = text("joinCall") }) => /* @__PURE__ */ jsxs("section", {
|
|
17
17
|
"aria-label": text("preview"),
|
|
18
18
|
className: "flex size-full min-h-0 flex-col items-center justify-center gap-3 bg-background p-4",
|
|
19
19
|
children: [
|
|
@@ -63,7 +63,7 @@ const CallCheck = ({ name, member, camera, microphoneOn, cameraOn, microphone, c
|
|
|
63
63
|
variant: "primary",
|
|
64
64
|
className: "h-10 rounded-full px-5",
|
|
65
65
|
onClick: onJoin,
|
|
66
|
-
children:
|
|
66
|
+
children: joinLabel
|
|
67
67
|
}),
|
|
68
68
|
/* @__PURE__ */ jsx(RouteLink, {
|
|
69
69
|
to: cancel,
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { cn } from "../lib/cn.js";
|
|
2
2
|
import { Button, buttonClass } from "../primitives/button.js";
|
|
3
3
|
import { RouteLink } from "../primitives/route-link.js";
|
|
4
|
+
import { CountBadge } from "../primitives/count-badge.js";
|
|
4
5
|
import { text } from "./messages.js";
|
|
5
|
-
import { Mic, MicOff, MonitorUp, MonitorX, PhoneOff, Video, VideoOff } from "lucide-react";
|
|
6
|
+
import { Mic, MicOff, MonitorUp, MonitorX, PhoneOff, UserPlus, Video, VideoOff } from "lucide-react";
|
|
6
7
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
8
|
//#region src/calls/call-controls.tsx
|
|
8
9
|
const toggle = (on) => cn("size-10 rounded-full", !on && "bg-danger-soft text-danger-text");
|
|
9
|
-
/** The member's controls: microphone, camera and screen share on and off, and Leave, a link out of the call. */
|
|
10
|
-
const CallControls = ({ microphoneOn, cameraOn, sharing, onMicrophone, onCamera, onShare, leave }) => /* @__PURE__ */ jsxs("div", {
|
|
10
|
+
/** The member's controls: microphone, camera and screen share on and off, the guests, and Leave, a link out of the call. */
|
|
11
|
+
const CallControls = ({ microphoneOn, cameraOn, sharing, onMicrophone, onCamera, onShare, guests, leave }) => /* @__PURE__ */ jsxs("div", {
|
|
11
12
|
role: "toolbar",
|
|
12
13
|
"aria-label": text("joinCall"),
|
|
13
|
-
className: "flex shrink-0 items-center justify-center gap-2 py-2",
|
|
14
|
+
className: "flex shrink-0 flex-wrap items-center justify-center gap-2 py-2",
|
|
14
15
|
children: [
|
|
15
16
|
/* @__PURE__ */ jsx(Button, {
|
|
16
17
|
size: "icon",
|
|
@@ -39,6 +40,17 @@ const CallControls = ({ microphoneOn, cameraOn, sharing, onMicrophone, onCamera,
|
|
|
39
40
|
onClick: onShare,
|
|
40
41
|
children: sharing ? /* @__PURE__ */ jsx(MonitorX, { className: "size-4" }) : /* @__PURE__ */ jsx(MonitorUp, { className: "size-4" })
|
|
41
42
|
}),
|
|
43
|
+
guests && /* @__PURE__ */ jsxs(Button, {
|
|
44
|
+
size: "icon",
|
|
45
|
+
"aria-label": text("guests"),
|
|
46
|
+
title: text("guests"),
|
|
47
|
+
className: "relative size-10 rounded-full",
|
|
48
|
+
onClick: guests.onOpen,
|
|
49
|
+
children: [/* @__PURE__ */ jsx(UserPlus, { className: "size-4" }), /* @__PURE__ */ jsx(CountBadge, {
|
|
50
|
+
count: guests.waiting,
|
|
51
|
+
className: "absolute -top-1 -right-1"
|
|
52
|
+
})]
|
|
53
|
+
}),
|
|
42
54
|
/* @__PURE__ */ jsxs(RouteLink, {
|
|
43
55
|
to: leave,
|
|
44
56
|
className: cn(buttonClass({ variant: "danger" }), "h-10 rounded-full px-4"),
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { Button } from "../primitives/button.js";
|
|
2
|
+
import { ResponsiveDialog } from "../primitives/responsive-dialog.js";
|
|
3
|
+
import { Input } from "../primitives/input.js";
|
|
4
|
+
import { Segmented } from "../primitives/segmented.js";
|
|
5
|
+
import { text } from "./messages.js";
|
|
6
|
+
import { useGuestRequest } from "./use-guest-request.js";
|
|
7
|
+
import { useState } from "react";
|
|
8
|
+
import { Check, Copy } from "lucide-react";
|
|
9
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
|
+
//#region src/calls/call-guests-panel.tsx
|
|
11
|
+
const sends = {
|
|
12
|
+
all: [
|
|
13
|
+
"camera",
|
|
14
|
+
"microphone",
|
|
15
|
+
"screen",
|
|
16
|
+
"screen-audio"
|
|
17
|
+
],
|
|
18
|
+
camera: ["camera", "microphone"],
|
|
19
|
+
microphone: ["microphone"]
|
|
20
|
+
};
|
|
21
|
+
const lasting = {
|
|
22
|
+
call: void 0,
|
|
23
|
+
hour: 36e5
|
|
24
|
+
};
|
|
25
|
+
const Row = ({ name, children }) => /* @__PURE__ */ jsxs("li", {
|
|
26
|
+
className: "flex items-center justify-between gap-2 py-1.5",
|
|
27
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
28
|
+
className: "min-w-0 truncate text-sm text-foreground",
|
|
29
|
+
children: name
|
|
30
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
31
|
+
className: "flex shrink-0 gap-1.5",
|
|
32
|
+
children
|
|
33
|
+
})]
|
|
34
|
+
});
|
|
35
|
+
/**
|
|
36
|
+
* A member's guests of the call: who waits in the lobby, to let in or turn away; the guests in the call, to remove; and
|
|
37
|
+
* the guest links, to make, copy and revoke, with what their guests may send. Names show as the guests wrote them.
|
|
38
|
+
*/
|
|
39
|
+
const CallGuestsPanel = ({ lobby, inCall, guests, linkOf, open, onClose }) => {
|
|
40
|
+
const [send, setSend] = useState("camera");
|
|
41
|
+
const [lasts, setLasts] = useState("call");
|
|
42
|
+
const [made, setMade] = useState();
|
|
43
|
+
const [copied, setCopied] = useState(false);
|
|
44
|
+
const { run, refused } = useGuestRequest();
|
|
45
|
+
const create = () => run(async () => {
|
|
46
|
+
const ms = lasting[lasts];
|
|
47
|
+
const { link, token } = await guests.link({
|
|
48
|
+
publish: sends[send],
|
|
49
|
+
...ms !== void 0 && { expiresAt: new Date(Date.now() + ms) }
|
|
50
|
+
});
|
|
51
|
+
setMade({
|
|
52
|
+
id: link.id,
|
|
53
|
+
url: linkOf(token)
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
const copy = (url) => void navigator.clipboard.writeText(url).then(() => {
|
|
57
|
+
setCopied(true);
|
|
58
|
+
setTimeout(() => setCopied(false), 2e3);
|
|
59
|
+
});
|
|
60
|
+
return /* @__PURE__ */ jsx(ResponsiveDialog, {
|
|
61
|
+
title: text("guests"),
|
|
62
|
+
description: text("addressesVisible"),
|
|
63
|
+
open,
|
|
64
|
+
onClose,
|
|
65
|
+
actions: /* @__PURE__ */ jsx(Button, {
|
|
66
|
+
onClick: onClose,
|
|
67
|
+
children: text("close")
|
|
68
|
+
}),
|
|
69
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
70
|
+
className: "flex flex-col gap-4",
|
|
71
|
+
children: [
|
|
72
|
+
refused && /* @__PURE__ */ jsx("p", {
|
|
73
|
+
role: "alert",
|
|
74
|
+
className: "rounded-md bg-warning-soft px-3 py-1.5 text-xs text-foreground",
|
|
75
|
+
children: text("guestRefused")
|
|
76
|
+
}),
|
|
77
|
+
lobby.guests.length > 0 && /* @__PURE__ */ jsxs("section", {
|
|
78
|
+
"aria-label": text("inLobby"),
|
|
79
|
+
children: [/* @__PURE__ */ jsx("h3", {
|
|
80
|
+
className: "text-xs font-medium text-muted-foreground",
|
|
81
|
+
children: text("inLobby")
|
|
82
|
+
}), /* @__PURE__ */ jsx("ul", { children: lobby.guests.map(({ guest, name }) => /* @__PURE__ */ jsxs(Row, {
|
|
83
|
+
name,
|
|
84
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
85
|
+
size: "sm",
|
|
86
|
+
variant: "primary",
|
|
87
|
+
onClick: () => run(() => guests.admit(guest)),
|
|
88
|
+
children: text("admit")
|
|
89
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
90
|
+
size: "sm",
|
|
91
|
+
onClick: () => run(() => guests.deny(guest)),
|
|
92
|
+
children: text("deny")
|
|
93
|
+
})]
|
|
94
|
+
}, guest)) })]
|
|
95
|
+
}),
|
|
96
|
+
inCall.length > 0 && /* @__PURE__ */ jsxs("section", {
|
|
97
|
+
"aria-label": text("guestsInCall"),
|
|
98
|
+
children: [/* @__PURE__ */ jsx("h3", {
|
|
99
|
+
className: "text-xs font-medium text-muted-foreground",
|
|
100
|
+
children: text("guestsInCall")
|
|
101
|
+
}), /* @__PURE__ */ jsx("ul", { children: inCall.map(({ guest, name }) => /* @__PURE__ */ jsx(Row, {
|
|
102
|
+
name,
|
|
103
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
104
|
+
size: "sm",
|
|
105
|
+
variant: "danger",
|
|
106
|
+
onClick: () => run(() => guests.remove(guest)),
|
|
107
|
+
children: text("remove")
|
|
108
|
+
})
|
|
109
|
+
}, guest)) })]
|
|
110
|
+
}),
|
|
111
|
+
/* @__PURE__ */ jsxs("section", {
|
|
112
|
+
"aria-label": text("guestLinks"),
|
|
113
|
+
className: "flex flex-col gap-2",
|
|
114
|
+
children: [
|
|
115
|
+
/* @__PURE__ */ jsx("h3", {
|
|
116
|
+
className: "text-xs font-medium text-muted-foreground",
|
|
117
|
+
children: text("guestLinks")
|
|
118
|
+
}),
|
|
119
|
+
lobby.links.length === 0 && /* @__PURE__ */ jsx("p", {
|
|
120
|
+
className: "text-sm text-muted-foreground",
|
|
121
|
+
children: text("noLinks")
|
|
122
|
+
}),
|
|
123
|
+
/* @__PURE__ */ jsx("ul", { children: lobby.links.map((link) => /* @__PURE__ */ jsxs("li", {
|
|
124
|
+
className: "flex flex-col gap-1.5 py-1.5",
|
|
125
|
+
children: [made?.id === link.id && /* @__PURE__ */ jsx(Input, {
|
|
126
|
+
readOnly: true,
|
|
127
|
+
value: made.url,
|
|
128
|
+
"aria-label": text("copyLink"),
|
|
129
|
+
onFocus: (event) => event.currentTarget.select(),
|
|
130
|
+
trailing: /* @__PURE__ */ jsx("button", {
|
|
131
|
+
type: "button",
|
|
132
|
+
"aria-label": copied ? text("copied") : text("copyLink"),
|
|
133
|
+
onClick: () => copy(made.url),
|
|
134
|
+
className: "inline-flex size-8 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-muted pointer-coarse:size-11",
|
|
135
|
+
children: copied ? /* @__PURE__ */ jsx(Check, { className: "size-4 text-success" }) : /* @__PURE__ */ jsx(Copy, { className: "size-4" })
|
|
136
|
+
})
|
|
137
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
138
|
+
className: "flex items-center justify-between gap-2 text-xs text-muted-foreground",
|
|
139
|
+
children: [link.expires_at ? text("linkUntil", new Date(link.expires_at).toLocaleString()) : text("linkUntilEnd"), /* @__PURE__ */ jsx(Button, {
|
|
140
|
+
size: "sm",
|
|
141
|
+
onClick: () => run(() => guests.revoke(link.id)),
|
|
142
|
+
children: text("revoke")
|
|
143
|
+
})]
|
|
144
|
+
})]
|
|
145
|
+
}, link.id)) }),
|
|
146
|
+
/* @__PURE__ */ jsxs("div", {
|
|
147
|
+
className: "flex flex-col items-start gap-2",
|
|
148
|
+
children: [
|
|
149
|
+
/* @__PURE__ */ jsxs("span", {
|
|
150
|
+
className: "flex flex-wrap items-center gap-2 text-xs text-muted-foreground",
|
|
151
|
+
children: [text("guestsMaySend"), /* @__PURE__ */ jsx(Segmented, {
|
|
152
|
+
label: text("guestsMaySend"),
|
|
153
|
+
value: send,
|
|
154
|
+
onChange: setSend,
|
|
155
|
+
options: [
|
|
156
|
+
{
|
|
157
|
+
value: "all",
|
|
158
|
+
label: text("sendAll")
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
value: "camera",
|
|
162
|
+
label: text("sendCamera")
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
value: "microphone",
|
|
166
|
+
label: text("sendMicrophone")
|
|
167
|
+
}
|
|
168
|
+
]
|
|
169
|
+
})]
|
|
170
|
+
}),
|
|
171
|
+
/* @__PURE__ */ jsxs("span", {
|
|
172
|
+
className: "flex flex-wrap items-center gap-2 text-xs text-muted-foreground",
|
|
173
|
+
children: [text("linkLasts"), /* @__PURE__ */ jsx(Segmented, {
|
|
174
|
+
label: text("linkLasts"),
|
|
175
|
+
value: lasts,
|
|
176
|
+
onChange: setLasts,
|
|
177
|
+
options: [{
|
|
178
|
+
value: "call",
|
|
179
|
+
label: text("linkUntilEnd")
|
|
180
|
+
}, {
|
|
181
|
+
value: "hour",
|
|
182
|
+
label: text("forAnHour")
|
|
183
|
+
}]
|
|
184
|
+
})]
|
|
185
|
+
}),
|
|
186
|
+
/* @__PURE__ */ jsx(Button, {
|
|
187
|
+
variant: "primary",
|
|
188
|
+
onClick: create,
|
|
189
|
+
children: text("newLink")
|
|
190
|
+
})
|
|
191
|
+
]
|
|
192
|
+
})
|
|
193
|
+
]
|
|
194
|
+
})
|
|
195
|
+
]
|
|
196
|
+
})
|
|
197
|
+
});
|
|
198
|
+
};
|
|
199
|
+
//#endregion
|
|
200
|
+
export { CallGuestsPanel };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Button } from "../primitives/button.js";
|
|
2
|
+
import { text } from "./messages.js";
|
|
3
|
+
import { useGuestRequest } from "./use-guest-request.js";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
//#region src/calls/call-lobby-notice.tsx
|
|
6
|
+
/** Who waits first in the lobby, with Let in and Turn away, so a member sees it without opening the guests. */
|
|
7
|
+
const CallLobbyNotice = ({ waiting, guests }) => {
|
|
8
|
+
const { run, refused } = useGuestRequest();
|
|
9
|
+
const first = waiting[0];
|
|
10
|
+
if (!first) return null;
|
|
11
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
12
|
+
role: "status",
|
|
13
|
+
className: "flex shrink-0 flex-wrap items-center gap-2 rounded-md bg-primary-soft px-3 py-1.5 text-xs text-foreground",
|
|
14
|
+
children: [
|
|
15
|
+
/* @__PURE__ */ jsx("span", {
|
|
16
|
+
className: "min-w-0 flex-1 truncate",
|
|
17
|
+
children: refused ? text("guestRefused") : text("waitingGuest", first.name)
|
|
18
|
+
}),
|
|
19
|
+
/* @__PURE__ */ jsx(Button, {
|
|
20
|
+
size: "sm",
|
|
21
|
+
variant: "primary",
|
|
22
|
+
onClick: () => run(() => guests.admit(first.guest)),
|
|
23
|
+
children: text("admit")
|
|
24
|
+
}),
|
|
25
|
+
/* @__PURE__ */ jsx(Button, {
|
|
26
|
+
size: "sm",
|
|
27
|
+
onClick: () => run(() => guests.deny(first.guest)),
|
|
28
|
+
children: text("deny")
|
|
29
|
+
})
|
|
30
|
+
]
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
//#endregion
|
|
34
|
+
export { CallLobbyNotice };
|
|
@@ -7,16 +7,17 @@ const said = {
|
|
|
7
7
|
removed: "removed",
|
|
8
8
|
full: "full",
|
|
9
9
|
refused: "refused",
|
|
10
|
-
unavailable: "unavailable"
|
|
10
|
+
unavailable: "unavailable",
|
|
11
|
+
denied: "denied"
|
|
11
12
|
};
|
|
12
|
-
/** Why the member is not in the call, in plain words, and, unless they were removed, a way to join again. */
|
|
13
|
+
/** Why the member is not in the call, in plain words, and, unless they were removed or refused, a way to join again. */
|
|
13
14
|
const CallMessage = ({ reason, onJoinAgain }) => /* @__PURE__ */ jsxs("section", {
|
|
14
15
|
role: "status",
|
|
15
16
|
className: "flex size-full min-h-0 flex-col items-center justify-center gap-3 bg-background p-4 text-center",
|
|
16
17
|
children: [/* @__PURE__ */ jsx("p", {
|
|
17
18
|
className: "text-sm text-foreground",
|
|
18
19
|
children: text(reason === "rejoin" ? "ended" : said[reason])
|
|
19
|
-
}), onJoinAgain && reason !== "removed" && reason !== "refused" && /* @__PURE__ */ jsx(Button, {
|
|
20
|
+
}), onJoinAgain && reason !== "removed" && reason !== "refused" && reason !== "denied" && /* @__PURE__ */ jsx(Button, {
|
|
20
21
|
onClick: onJoinAgain,
|
|
21
22
|
children: text("callAgain")
|
|
22
23
|
})]
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { text } from "./messages.js";
|
|
1
2
|
import { cannotConnect, connectionQuality } from "./call-layout.js";
|
|
2
3
|
//#region src/calls/call-people.ts
|
|
3
4
|
const received = (tracks, source) => tracks.find((track) => track.source === source && track.track);
|
|
@@ -6,10 +7,10 @@ const callPeople = ({ members, self, tracks, own, nameOf, speaker, connections,
|
|
|
6
7
|
return (members.some((member) => member.member === self) ? members : [{
|
|
7
8
|
member: self,
|
|
8
9
|
joined_at: ""
|
|
9
|
-
}, ...members]).map(({ member }) => {
|
|
10
|
+
}, ...members]).map(({ member, name }) => {
|
|
10
11
|
const shared = {
|
|
11
12
|
member,
|
|
12
|
-
name: nameOf(member),
|
|
13
|
+
name: member.startsWith("guest:") ? text("guestName", name ?? "") : name ?? nameOf(member),
|
|
13
14
|
...speaker === member && { speaking: true }
|
|
14
15
|
};
|
|
15
16
|
if (member === self) return {
|
package/dist/calls/call-stage.js
CHANGED
|
@@ -64,7 +64,7 @@ const Tiles = ({ layout, people, narrow }) => {
|
|
|
64
64
|
* share large with the people beside it, or under it on a phone; the member alone; then the controls. The others'
|
|
65
65
|
* sound plays unseen.
|
|
66
66
|
*/
|
|
67
|
-
const CallStage = ({ people, state, notices = [], controls, className }) => {
|
|
67
|
+
const CallStage = ({ people, state, notices = [], controls, aside, children, className }) => {
|
|
68
68
|
const { ref, width } = useWidth();
|
|
69
69
|
const narrow = width > 0 && width < 640;
|
|
70
70
|
const layout = callLayout({
|
|
@@ -87,6 +87,7 @@ const CallStage = ({ people, state, notices = [], controls, className }) => {
|
|
|
87
87
|
children: [message === text("reconnecting") && /* @__PURE__ */ jsx(Spinner, { className: "size-3" }), message]
|
|
88
88
|
}, message))
|
|
89
89
|
}),
|
|
90
|
+
aside,
|
|
90
91
|
/* @__PURE__ */ jsx("div", {
|
|
91
92
|
className: "min-h-0 flex-1",
|
|
92
93
|
children: state === "joining" ? /* @__PURE__ */ jsxs("p", {
|
|
@@ -103,7 +104,8 @@ const CallStage = ({ people, state, notices = [], controls, className }) => {
|
|
|
103
104
|
people.filter((person) => !person.self).flatMap((person) => [["microphone", person.microphone], ["screen-audio", person.screenAudio]].map(([source, track]) => track && {
|
|
104
105
|
key: `${person.member} ${source}`,
|
|
105
106
|
track
|
|
106
|
-
})).filter((audio) => audio !== void 0).map(({ key, track }) => /* @__PURE__ */ jsx(TrackAudio, { track }, key))
|
|
107
|
+
})).filter((audio) => audio !== void 0).map(({ key, track }) => /* @__PURE__ */ jsx(TrackAudio, { track }, key)),
|
|
108
|
+
children
|
|
107
109
|
]
|
|
108
110
|
});
|
|
109
111
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RouteTarget } from "../primitives/route-link.js";
|
|
2
|
+
import { CallSessionOptions } from "./use-call-session.js";
|
|
2
3
|
//#region src/calls/call-view.d.ts
|
|
3
4
|
export type CallViewProps = {
|
|
4
5
|
/** The socket of the app's function that admits the member, as `joinCall` takes it, such as `/api/functions/call?conversation=7`. */
|
|
@@ -7,14 +8,19 @@ export type CallViewProps = {
|
|
|
7
8
|
leave: RouteTarget;
|
|
8
9
|
/** A member's name, by the member the app's function admits them as, such as a user id; the member itself by default. */
|
|
9
10
|
nameOf?: (member: string) => string;
|
|
11
|
+
/** For a guest: their wait in the lobby after Join, which resolves with their token; see `GuestCallPage`. */
|
|
12
|
+
admission?: CallSessionOptions['admission'];
|
|
13
|
+
/** The member's own name before joining, when no one is signed in, as a guest's. */
|
|
14
|
+
name?: string;
|
|
10
15
|
className?: string;
|
|
11
16
|
};
|
|
12
17
|
/**
|
|
13
18
|
* A call, ready-made, that fills its parent, such as a column beside a conversation's messages: a check of the
|
|
14
19
|
* microphone and camera first, then, after Join, everyone in the call with the one speaking ringed, a screen share
|
|
15
20
|
* large, microphone, camera, screen share and Leave, the quality of each connection, Reconnecting while the link is
|
|
16
|
-
* restored, and plain words for a full call, a blocked device or someone the browser cannot connect to.
|
|
17
|
-
*
|
|
21
|
+
* restored, and plain words for a full call, a blocked device or someone the browser cannot connect to. A member the app
|
|
22
|
+
* lets invite also gets the call's guests: who waits in the lobby, and its guest links. Made for small peer-to-peer
|
|
23
|
+
* calls, at a desktop's width or a phone's.
|
|
18
24
|
*/
|
|
19
|
-
export declare const CallView: ({ url, leave, nameOf, className }: CallViewProps) => import("react").JSX.Element;
|
|
25
|
+
export declare const CallView: ({ url, leave, nameOf, admission, name, className }: CallViewProps) => import("react").JSX.Element;
|
|
20
26
|
//#endregion
|
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 };
|