@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.
Files changed (53) hide show
  1. package/dist/account/account-page.js +3 -0
  2. package/dist/account/blocks/connected-accounts-block.js +1 -1
  3. package/dist/account/blocks/personal-mail-block.js +36 -0
  4. package/dist/account/messages.js +4 -0
  5. package/dist/account/sign-in-providers-section.js +3 -3
  6. package/dist/app/app.d.ts +1 -1
  7. package/dist/app/app.js +22 -5
  8. package/dist/app/pages/render-node.js +2 -0
  9. package/dist/auth/auth-provider.js +7 -0
  10. package/dist/calls/call-check.js +2 -2
  11. package/dist/calls/call-controls.js +16 -4
  12. package/dist/calls/call-guests-panel.js +200 -0
  13. package/dist/calls/call-lobby-notice.js +34 -0
  14. package/dist/calls/call-message.js +4 -3
  15. package/dist/calls/call-people.js +3 -2
  16. package/dist/calls/call-stage.js +4 -2
  17. package/dist/calls/call-view.d.ts +9 -3
  18. package/dist/calls/call-view.js +37 -6
  19. package/dist/calls/call-waiting.js +20 -0
  20. package/dist/calls/guest-admission.js +40 -0
  21. package/dist/calls/guest-call-link.js +19 -0
  22. package/dist/calls/guest-call-page.js +82 -0
  23. package/dist/calls/messages.js +69 -3
  24. package/dist/calls/use-call-session.d.ts +7 -0
  25. package/dist/calls/use-call-session.js +27 -2
  26. package/dist/calls/use-guest-request.js +24 -0
  27. package/dist/data/api-provider.d.ts +8 -0
  28. package/dist/index.js +1 -1
  29. package/dist/organizations/messages.js +6 -0
  30. package/dist/organizations/organization-page.js +33 -1
  31. package/dist/package.js +1 -1
  32. package/dist/shell/assets/app-CKCVGFSR.js +9 -0
  33. package/dist/shell/assets/calls-DRnrKDC9.js +2 -0
  34. package/dist/shell/assets/{index-D8oHLXjS.js → index-BK-wuwB0.js} +2 -2
  35. package/dist/shell/assets/index-Be8nCyRW.css +2 -0
  36. package/dist/shell/assets/{records-map-DOXcutuQ.js → records-map-Bd5scHzJ.js} +2 -2
  37. package/dist/shell/assets/shared-protobase-ui-CU8QfOuc.js +2 -0
  38. package/dist/shell/index.html +4 -4
  39. package/dist/shell/protobase.shell.json +2 -2
  40. package/dist/shell/protobase.sw.js +2 -2
  41. package/dist/shell-source-maps/assets/app-CKCVGFSR.js.map +1 -0
  42. package/dist/shell-source-maps/assets/calls-DRnrKDC9.js.map +1 -0
  43. package/dist/shell-source-maps/assets/{index-D8oHLXjS.js.map → index-BK-wuwB0.js.map} +1 -1
  44. package/dist/shell-source-maps/assets/{records-map-DOXcutuQ.js.map → records-map-Bd5scHzJ.js.map} +1 -1
  45. package/dist/shell-source-maps/assets/shared-protobase-ui-CU8QfOuc.js.map +1 -0
  46. package/package.json +4 -4
  47. package/dist/shell/assets/app-B_ojAtG3.js +0 -9
  48. package/dist/shell/assets/calls-C63xSTRj.js +0 -2
  49. package/dist/shell/assets/index-BgcF_WZ0.css +0 -2
  50. package/dist/shell/assets/shared-protobase-ui-CX80Ectk.js +0 -2
  51. package/dist/shell-source-maps/assets/app-B_ojAtG3.js.map +0 -1
  52. package/dist/shell-source-maps/assets/calls-C63xSTRj.js.map +0 -1
  53. package/dist/shell-source-maps/assets/shared-protobase-ui-CX80Ectk.js.map +0 -1
@@ -18,11 +18,13 @@ const useAccountPage = () => {
18
18
  const auth = useAuth();
19
19
  const { mcp, accountDeletion } = auth;
20
20
  const linkable = linkableProviders(auth).length > 0;
21
+ const mailboxes = auth.personalMail.length > 0;
21
22
  return useMemo(() => {
22
23
  if (!account) return void 0;
23
24
  const hidden = /* @__PURE__ */ new Set([
24
25
  ...mcp ? [] : ["ConnectedApps"],
25
26
  ...linkable ? [] : ["ConnectedAccounts"],
27
+ ...mailboxes ? [] : ["PersonalMail"],
26
28
  ...accountDeletion ? [] : ["DeleteAccount"]
27
29
  ]);
28
30
  return {
@@ -33,6 +35,7 @@ const useAccountPage = () => {
33
35
  account,
34
36
  mcp,
35
37
  linkable,
38
+ mailboxes,
36
39
  accountDeletion
37
40
  ]);
38
41
  };
@@ -34,4 +34,4 @@ const ConnectedAccountsBlock = () => {
34
34
  return linkable.length > 0 ? /* @__PURE__ */ jsx(Linked, { providers: linkable }) : null;
35
35
  };
36
36
  //#endregion
37
- export { ConnectedAccountsBlock, linkableProviders };
37
+ export { ConnectedAccountsBlock, linkableProviders, takeLinkError };
@@ -0,0 +1,36 @@
1
+ import { text } from "../messages.js";
2
+ import { useAuth } from "../../auth/auth-provider.js";
3
+ import { providerMessage } from "../../auth/auth-messages.js";
4
+ import { SignInProvidersSection } from "../sign-in-providers-section.js";
5
+ import { AccountLoading } from "./account-loading.js";
6
+ import { useAccountData } from "./use-account-data.js";
7
+ import { takeLinkError } from "./connected-accounts-block.js";
8
+ import { useCallback, useState } from "react";
9
+ import { jsx } from "react/jsx-runtime";
10
+ //#region src/account/blocks/personal-mail-block.tsx
11
+ const Mailboxes = ({ links }) => {
12
+ const { session, linkPersonalMail } = useAuth();
13
+ const { account } = session;
14
+ const [linkError] = useState(takeLinkError);
15
+ const { data: linked, error } = useAccountData(useCallback(() => account.linkedPersonalMail(links), [account, links]), text("couldNotReadMethods"));
16
+ if (!linked) return /* @__PURE__ */ jsx(AccountLoading, { error });
17
+ const connect = (id) => linkPersonalMail(links.find((link) => link.id === id));
18
+ return /* @__PURE__ */ jsx(SignInProvidersSection, {
19
+ title: text("mailbox"),
20
+ description: text("mailboxDescription"),
21
+ providers: links.map(({ id, name }) => ({
22
+ id,
23
+ name
24
+ })),
25
+ linked,
26
+ onConnect: connect,
27
+ error: linkError && providerMessage(linkError, links.length === 1 ? links[0].name : text("theProvider"))
28
+ });
29
+ };
30
+ /** The `PersonalMail` block: the mailboxes the signed-in person can link for the app to send mail from, hidden when the app has none. */
31
+ const PersonalMailBlock = () => {
32
+ const { personalMail } = useAuth();
33
+ return personalMail.length > 0 ? /* @__PURE__ */ jsx(Mailboxes, { links: personalMail }) : null;
34
+ };
35
+ //#endregion
36
+ export { PersonalMailBlock };
@@ -71,6 +71,8 @@ const text = defineMessages({
71
71
  connectedAccountsDescription: "Sign in with these instead of your password. Connecting one asks you to sign in there; its address may differ from yours here.",
72
72
  connected: "Connected",
73
73
  connect: (name) => `Connect ${name}`,
74
+ mailbox: "Mailbox",
75
+ mailboxDescription: "Mail this app sends for you goes out from your own mailbox. Connecting one asks you to sign in there and allow sending mail; it cannot read your mail.",
74
76
  stopped: (time) => `Stopped ${time}`,
75
77
  ended: (time) => `Ended ${time}`,
76
78
  signedInUntil: (time) => `Signed in until ${time}`,
@@ -234,6 +236,8 @@ const text = defineMessages({
234
236
  connectedAccountsDescription: "Log hiermee in in plaats van met je wachtwoord. Koppelen vraagt je daar in te loggen; het adres daar kan anders zijn dan hier.",
235
237
  connected: "Gekoppeld",
236
238
  connect: (name) => `${name} koppelen`,
239
+ mailbox: "Mailbox",
240
+ mailboxDescription: "Mail die deze app voor je verstuurt, gaat uit vanuit je eigen mailbox. Koppelen vraagt je daar in te loggen en het versturen van mail toe te staan; je mail lezen kan de app niet.",
237
241
  stopped: (time) => `Gestopt ${time}`,
238
242
  ended: (time) => `Afgelopen ${time}`,
239
243
  signedInUntil: (time) => `Ingelogd tot ${time}`,
@@ -9,7 +9,7 @@ import { AuthError } from "@protobase/client";
9
9
  import { jsx, jsxs } from "react/jsx-runtime";
10
10
  //#region src/account/sign-in-providers-section.tsx
11
11
  /** The providers someone can sign in with besides a password, and a way to connect one whatever address it has. */
12
- const SignInProvidersSection = ({ providers, linked, onConnect, error: returned }) => {
12
+ const SignInProvidersSection = ({ providers, linked, onConnect, error: returned, title = text("connectedAccounts"), description = text("connectedAccountsDescription") }) => {
13
13
  const [busy, setBusy] = useState();
14
14
  const [error, setError] = useState(returned);
15
15
  const connect = async (provider) => {
@@ -24,8 +24,8 @@ const SignInProvidersSection = ({ providers, linked, onConnect, error: returned
24
24
  }
25
25
  };
26
26
  return /* @__PURE__ */ jsxs(AccountSection, {
27
- title: text("connectedAccounts"),
28
- description: text("connectedAccountsDescription"),
27
+ title,
28
+ description,
29
29
  children: [/* @__PURE__ */ jsx(SectionError, { message: error }), /* @__PURE__ */ jsx("ul", {
30
30
  className: "divide-y rounded-md border",
31
31
  children: providers.map((provider) => /* @__PURE__ */ jsxs("li", {
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(AuthGate, {
360
- workspace,
361
- children: /* @__PURE__ */ jsx(Authenticated, {
373
+ children: /* @__PURE__ */ jsx(GuestCallGate, {
374
+ ...rest.baseUrl && { baseUrl: rest.baseUrl },
375
+ children: /* @__PURE__ */ jsx(AuthGate, {
362
376
  workspace,
363
- ...rest
377
+ children: /* @__PURE__ */ jsx(Authenticated, {
378
+ workspace,
379
+ ...rest
380
+ })
364
381
  })
365
382
  })
366
383
  }) })
@@ -23,6 +23,7 @@ import { TableBlock } from "./blocks/table.js";
23
23
  import { TabBlock, TabsBlock } from "./blocks/tabs.js";
24
24
  import { UsersBlock } from "../../users/users-block.js";
25
25
  import { ConnectedAccountsBlock } from "../../account/blocks/connected-accounts-block.js";
26
+ import { PersonalMailBlock } from "../../account/blocks/personal-mail-block.js";
26
27
  import { ConnectedAppsBlock } from "../../account/blocks/connected-apps-block.js";
27
28
  import { DeleteAccountBlock } from "../../account/blocks/delete-account-block.js";
28
29
  import { DevicesBlock } from "../../account/blocks/devices-block.js";
@@ -61,6 +62,7 @@ const blocks = {
61
62
  Passkeys: PasskeysBlock,
62
63
  TwoFactor: TwoFactorBlock,
63
64
  ConnectedAccounts: ConnectedAccountsBlock,
65
+ PersonalMail: PersonalMailBlock,
64
66
  Devices: DevicesBlock,
65
67
  ConnectedApps: ConnectedAppsBlock,
66
68
  DeleteAccount: DeleteAccountBlock
@@ -31,6 +31,7 @@ const AuthProvider = ({ session: given, origin, nativeBridge, children }) => {
31
31
  const [passkeyDomains, setPasskeyDomains] = useState([]);
32
32
  const [socialProviders, setSocialProviders] = useState([]);
33
33
  const [platformSignIn, setPlatformSignIn] = useState();
34
+ const [personalMail, setPersonalMail] = useState([]);
34
35
  const [samlProviders, setSamlProviders] = useState([]);
35
36
  const [staffSignIn, setStaffSignIn] = useState();
36
37
  const [organizations, setOrganizations] = useState();
@@ -117,6 +118,7 @@ const AuthProvider = ({ session: given, origin, nativeBridge, children }) => {
117
118
  setPasskeyDomains(status.passkeyDomains ?? []);
118
119
  setSocialProviders(status.socialProviders);
119
120
  setPlatformSignIn(status.platformSignIn);
121
+ setPersonalMail(status.personalMail ?? []);
120
122
  setSamlProviders(status.samlProviders);
121
123
  setStaffSignIn(status.staffSignIn);
122
124
  setOrganizations(status.organizations);
@@ -183,6 +185,10 @@ const AuthProvider = ({ session: given, origin, nativeBridge, children }) => {
183
185
  linkProvider: async (provider) => {
184
186
  window.location.assign(await session.account.linkProvider(provider, new URL(withoutSignInLink(window.location.href), window.location.href).href));
185
187
  },
188
+ personalMail,
189
+ linkPersonalMail: async (link) => {
190
+ window.location.assign(await session.account.linkPersonalMail(link, new URL(withoutSignInLink(window.location.href), window.location.href).href));
191
+ },
186
192
  stopStaffSession: async () => {
187
193
  await push.forget();
188
194
  await session.staff.stop();
@@ -213,6 +219,7 @@ const AuthProvider = ({ session: given, origin, nativeBridge, children }) => {
213
219
  passkeyDomains,
214
220
  socialProviders,
215
221
  platformSignIn,
222
+ personalMail,
216
223
  samlProviders,
217
224
  staffSignIn,
218
225
  organizations,
@@ -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: text("joinCall")
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 {
@@ -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. Made for small
17
- * peer-to-peer calls, at a desktop's width or a phone's.
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