@protobase/ui 0.62.1 → 0.62.2

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 (60) hide show
  1. package/dist/app/app.js +6 -1
  2. package/dist/app/current-address.d.ts +1 -0
  3. package/dist/app/current-address.js +2 -1
  4. package/dist/calls/call-check.js +78 -0
  5. package/dist/calls/call-controls.js +50 -0
  6. package/dist/calls/call-layout.js +31 -0
  7. package/dist/calls/call-mark.d.ts +13 -0
  8. package/dist/calls/call-mark.js +57 -0
  9. package/dist/calls/call-message.js +25 -0
  10. package/dist/calls/call-people.js +46 -0
  11. package/dist/calls/call-rings.js +78 -0
  12. package/dist/calls/call-stage.js +111 -0
  13. package/dist/calls/call-tile.js +60 -0
  14. package/dist/calls/call-view.d.ts +20 -0
  15. package/dist/calls/call-view.js +88 -0
  16. package/dist/calls/devices.js +55 -0
  17. package/dist/calls/messages.js +76 -0
  18. package/dist/calls/record-call.d.ts +12 -0
  19. package/dist/calls/record-call.js +18 -0
  20. package/dist/calls/ring-tone.js +35 -0
  21. package/dist/calls/ringing.js +16 -0
  22. package/dist/calls/track-media.js +36 -0
  23. package/dist/calls/use-audio-levels.js +43 -0
  24. package/dist/calls/use-call-session.js +157 -0
  25. package/dist/calls/use-record-call-address.d.ts +17 -0
  26. package/dist/calls/use-record-call-address.js +30 -0
  27. package/dist/calls/use-record-calls.d.ts +172 -0
  28. package/dist/calls/use-record-calls.js +43 -0
  29. package/dist/calls/use-rings.js +52 -0
  30. package/dist/calls/use-width.js +18 -0
  31. package/dist/data/api-provider.d.ts +8 -0
  32. package/dist/data/meta-data.d.ts +2 -0
  33. package/dist/data/query-keys.js +8 -0
  34. package/dist/data/use-meta.js +1 -0
  35. package/dist/index.d.ts +6 -1
  36. package/dist/index.js +6 -1
  37. package/dist/package.js +1 -1
  38. package/dist/primitives/route-link.d.ts +13 -1
  39. package/dist/shell/assets/app-i5V50MJx.js +9 -0
  40. package/dist/shell/assets/calls-DwVdIhNM.js +2 -0
  41. package/dist/shell/assets/index-BgcF_WZ0.css +2 -0
  42. package/dist/shell/assets/{index-CphPkbp2.js → index-jTcckwc-.js} +2 -2
  43. package/dist/shell/assets/{records-map-BVL8Bz3b.js → records-map-idUZ9NK2.js} +2 -2
  44. package/dist/shell/assets/shared-protobase-ui-55TVcKm3.js +2 -0
  45. package/dist/shell/index.html +4 -4
  46. package/dist/shell/protobase.shell.json +2 -2
  47. package/dist/shell/protobase.sw.js +2 -2
  48. package/dist/shell-source-maps/assets/app-i5V50MJx.js.map +1 -0
  49. package/dist/shell-source-maps/assets/calls-DwVdIhNM.js.map +1 -0
  50. package/dist/shell-source-maps/assets/{index-CphPkbp2.js.map → index-jTcckwc-.js.map} +1 -1
  51. package/dist/shell-source-maps/assets/{records-map-BVL8Bz3b.js.map → records-map-idUZ9NK2.js.map} +1 -1
  52. package/dist/shell-source-maps/assets/shared-protobase-ui-55TVcKm3.js.map +1 -0
  53. package/dist/toasts/toast-provider.js +3 -1
  54. package/dist/toasts/toast.js +6 -4
  55. package/package.json +4 -4
  56. package/dist/shell/assets/app-C-udyrvj.js +0 -9
  57. package/dist/shell/assets/index-Iii7afo2.css +0 -2
  58. package/dist/shell/assets/shared-protobase-ui-CmaYdEFn.js +0 -2
  59. package/dist/shell-source-maps/assets/app-C-udyrvj.js.map +0 -1
  60. package/dist/shell-source-maps/assets/shared-protobase-ui-CmaYdEFn.js.map +0 -1
@@ -0,0 +1,88 @@
1
+ import { useOptionalAuth } from "../auth/auth-provider.js";
2
+ import { text } from "./messages.js";
3
+ import { CallCheck } from "./call-check.js";
4
+ import { CallMessage } from "./call-message.js";
5
+ import { callPeople } from "./call-people.js";
6
+ import { CallStage } from "./call-stage.js";
7
+ import { useActiveSpeaker } from "./use-audio-levels.js";
8
+ import { useCallSession } from "./use-call-session.js";
9
+ import { useState } from "react";
10
+ import { jsx } from "react/jsx-runtime";
11
+ //#region src/calls/call-view.tsx
12
+ const deviceNotices = (devices, refusedSending) => [
13
+ ...devices?.blocked.microphone ? [text("noMicrophone")] : [],
14
+ ...devices?.blocked.camera ? [text("noCamera")] : [],
15
+ ...devices?.none ? [text("noDevices")] : [],
16
+ ...refusedSending ? [text("refused")] : []
17
+ ];
18
+ /**
19
+ * A call, ready-made, that fills its parent, such as a column beside a conversation's messages: a check of the
20
+ * microphone and camera first, then, after Join, everyone in the call with the one speaking ringed, a screen share
21
+ * 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. Made for small
23
+ * peer-to-peer calls, at a desktop's width or a phone's.
24
+ */
25
+ const CallView = ({ url, leave, nameOf = (member) => member, className }) => {
26
+ const session = useCallSession(url);
27
+ const auth = useOptionalAuth();
28
+ const user = auth?.state.kind === "signed-in" ? auth.state.user : void 0;
29
+ const self = session.self ?? user?.id ?? "";
30
+ const [now] = useState(() => Date.now());
31
+ const { devices, phase } = session;
32
+ const own = {
33
+ ...devices?.microphone && { microphone: devices.microphone },
34
+ ...devices?.camera && { camera: devices.camera },
35
+ ...session.screen && { screen: session.screen.video },
36
+ microphoneOn: session.microphoneOn,
37
+ cameraOn: session.cameraOn
38
+ };
39
+ const voices = new Map([...devices?.microphone && session.microphoneOn ? [[self, devices.microphone]] : [], ...session.tracks.filter((track) => track.source === "microphone" && track.track && !track.muted).map((track) => [track.member, track.track])]);
40
+ const speaker = useActiveSpeaker(phase.kind === "in" ? voices : /* @__PURE__ */ new Map());
41
+ const notices = deviceNotices(devices, session.refusedSending);
42
+ if (phase.kind === "left") return /* @__PURE__ */ jsx(CallMessage, {
43
+ reason: phase.reason,
44
+ onJoinAgain: () => void session.join()
45
+ });
46
+ if (phase.kind === "check") return /* @__PURE__ */ jsx(CallCheck, {
47
+ name: user?.name ?? text("you"),
48
+ member: self,
49
+ ...devices?.camera && { camera: devices.camera },
50
+ microphoneOn: session.microphoneOn,
51
+ cameraOn: session.cameraOn,
52
+ microphone: Boolean(devices?.microphone),
53
+ cameraAvailable: Boolean(devices?.camera),
54
+ notices,
55
+ onMicrophone: session.toggleMicrophone,
56
+ onCamera: session.toggleCamera,
57
+ onJoin: () => void session.join(),
58
+ cancel: leave
59
+ });
60
+ const people = callPeople({
61
+ members: session.members,
62
+ self,
63
+ tracks: session.tracks,
64
+ own,
65
+ nameOf,
66
+ ...speaker && { speaker },
67
+ connections: session.connections,
68
+ seen: session.seen,
69
+ now: Math.max(now, Date.now())
70
+ });
71
+ return /* @__PURE__ */ jsx(CallStage, {
72
+ ...className && { className },
73
+ people,
74
+ state: phase.kind === "joining" ? "joining" : phase.resuming ? "resuming" : "joined",
75
+ notices,
76
+ controls: {
77
+ microphoneOn: session.microphoneOn && Boolean(devices?.microphone),
78
+ cameraOn: session.cameraOn && Boolean(devices?.camera),
79
+ sharing: Boolean(session.screen),
80
+ onMicrophone: session.toggleMicrophone,
81
+ onCamera: session.toggleCamera,
82
+ ...session.toggleScreen && { onShare: () => void session.toggleScreen() },
83
+ leave
84
+ }
85
+ });
86
+ };
87
+ //#endregion
88
+ export { CallView };
@@ -0,0 +1,55 @@
1
+ //#region src/calls/devices.ts
2
+ const blockedNames = /* @__PURE__ */ new Set(["NotAllowedError", "SecurityError"]);
3
+ const missingNames = /* @__PURE__ */ new Set([
4
+ "NotFoundError",
5
+ "NotReadableError",
6
+ "OverconstrainedError"
7
+ ]);
8
+ const one = async (media, kind) => {
9
+ try {
10
+ return {
11
+ track: (await media.getUserMedia({ [kind]: true })).getTracks()[0],
12
+ blocked: false
13
+ };
14
+ } catch (error) {
15
+ if (!(error instanceof DOMException)) throw error;
16
+ if (blockedNames.has(error.name)) return { blocked: true };
17
+ if (missingNames.has(error.name)) return { blocked: false };
18
+ throw error;
19
+ }
20
+ };
21
+ /**
22
+ * Asks for the microphone and the camera at once, so the browser asks the person once; when that is refused, each on
23
+ * its own, to tell which one is blocked or missing.
24
+ */
25
+ const openDevices = async (media = navigator.mediaDevices) => {
26
+ try {
27
+ const stream = await media.getUserMedia({
28
+ audio: true,
29
+ video: true
30
+ });
31
+ return {
32
+ microphone: stream.getAudioTracks()[0],
33
+ camera: stream.getVideoTracks()[0],
34
+ blocked: {
35
+ microphone: false,
36
+ camera: false
37
+ },
38
+ none: false
39
+ };
40
+ } catch (error) {
41
+ if (!(error instanceof DOMException) || !(blockedNames.has(error.name) || missingNames.has(error.name))) throw error;
42
+ }
43
+ const [audio, video] = [await one(media, "audio"), await one(media, "video")];
44
+ return {
45
+ ...audio.track && { microphone: audio.track },
46
+ ...video.track && { camera: video.track },
47
+ blocked: {
48
+ microphone: audio.blocked,
49
+ camera: video.blocked
50
+ },
51
+ none: !audio.track && !video.track && !audio.blocked && !video.blocked
52
+ };
53
+ };
54
+ //#endregion
55
+ export { openDevices };
@@ -0,0 +1,76 @@
1
+ import { defineMessages } from "../locale/messages.js";
2
+ //#region src/calls/messages.ts
3
+ const text = defineMessages({
4
+ en: {
5
+ accept: "Accept",
6
+ decline: "Decline",
7
+ startCall: "Start call",
8
+ join: "Join",
9
+ inProgress: "Call in progress",
10
+ people: (count) => count === 1 ? "1 person" : `${count} people`,
11
+ joinCall: "Join call",
12
+ leave: "Leave",
13
+ microphone: "Microphone",
14
+ camera: "Camera",
15
+ shareScreen: "Share screen",
16
+ stopSharing: "Stop sharing",
17
+ muted: "Muted",
18
+ you: "You",
19
+ alone: "You are the only one here",
20
+ connecting: "Joining…",
21
+ reconnecting: "Reconnecting…",
22
+ full: "This call is full",
23
+ removed: "You were removed from this call",
24
+ ended: "The call has ended",
25
+ refused: "You cannot join this call",
26
+ unavailable: "The call cannot be reached now. Try again in a moment.",
27
+ callAgain: "Join again",
28
+ noMicrophone: "Your microphone is blocked: allow it in the browser to be heard.",
29
+ noCamera: "Your camera is blocked: allow it in the browser to be seen.",
30
+ noDevices: "No camera or microphone was found.",
31
+ cannotConnect: (name) => `Cannot connect to ${name}`,
32
+ poorConnection: "Poor connection",
33
+ goodConnection: "Good connection",
34
+ sharing: (name) => `${name} is sharing their screen`,
35
+ youAreSharing: "You are sharing your screen",
36
+ speaking: "Speaking",
37
+ preview: "Your camera before you join"
38
+ },
39
+ nl: {
40
+ accept: "Opnemen",
41
+ decline: "Weigeren",
42
+ startCall: "Gesprek starten",
43
+ join: "Deelnemen",
44
+ inProgress: "Gesprek bezig",
45
+ people: (count) => count === 1 ? "1 persoon" : `${count} personen`,
46
+ joinCall: "Deelnemen aan gesprek",
47
+ leave: "Verlaten",
48
+ microphone: "Microfoon",
49
+ camera: "Camera",
50
+ shareScreen: "Scherm delen",
51
+ stopSharing: "Stoppen met delen",
52
+ muted: "Gedempt",
53
+ you: "Jij",
54
+ alone: "Je bent de enige hier",
55
+ connecting: "Deelnemen…",
56
+ reconnecting: "Opnieuw verbinden…",
57
+ full: "Dit gesprek is vol",
58
+ removed: "Je bent uit dit gesprek gehaald",
59
+ ended: "Het gesprek is afgelopen",
60
+ refused: "Je kunt niet aan dit gesprek deelnemen",
61
+ unavailable: "Het gesprek is nu niet bereikbaar. Probeer het zo opnieuw.",
62
+ callAgain: "Opnieuw deelnemen",
63
+ noMicrophone: "Je microfoon is geblokkeerd: sta hem toe in de browser om gehoord te worden.",
64
+ noCamera: "Je camera is geblokkeerd: sta hem toe in de browser om gezien te worden.",
65
+ noDevices: "Er is geen camera of microfoon gevonden.",
66
+ cannotConnect: (name) => `Kan geen verbinding maken met ${name}`,
67
+ poorConnection: "Slechte verbinding",
68
+ goodConnection: "Goede verbinding",
69
+ sharing: (name) => `${name} deelt het scherm`,
70
+ youAreSharing: "Je deelt je scherm",
71
+ speaking: "Spreekt",
72
+ preview: "Je camera voordat je deelneemt"
73
+ }
74
+ });
75
+ //#endregion
76
+ export { text };
@@ -0,0 +1,12 @@
1
+ import { CallViewProps } from "./call-view.js";
2
+ //#region src/calls/record-call.d.ts
3
+ export type RecordCallProps = Omit<CallViewProps, 'leave'> & {
4
+ resource: string;
5
+ recordKey: string;
6
+ };
7
+ /**
8
+ * The record's call view while the app's address joins it (`call=join`, as Accept and Join open it), and nothing
9
+ * otherwise: put it beside the record's own content, such as on a full-area record page. Leave goes back to the record.
10
+ */
11
+ export declare const RecordCall: ({ resource, recordKey, ...rest }: RecordCallProps) => import("react").JSX.Element | null;
12
+ //#endregion
@@ -0,0 +1,18 @@
1
+ import { CallView } from "./call-view.js";
2
+ import { useRecordCallAddress } from "./use-record-call-address.js";
3
+ import { jsx } from "react/jsx-runtime";
4
+ //#region src/calls/record-call.tsx
5
+ /**
6
+ * The record's call view while the app's address joins it (`call=join`, as Accept and Join open it), and nothing
7
+ * otherwise: put it beside the record's own content, such as on a full-area record page. Leave goes back to the record.
8
+ */
9
+ const RecordCall = ({ resource, recordKey, ...rest }) => {
10
+ const { joining, leave } = useRecordCallAddress(resource, recordKey);
11
+ if (!joining) return null;
12
+ return /* @__PURE__ */ jsx(CallView, {
13
+ leave,
14
+ ...rest
15
+ }, `${resource}/${recordKey}`);
16
+ };
17
+ //#endregion
18
+ export { RecordCall };
@@ -0,0 +1,35 @@
1
+ import { useEffect } from "react";
2
+ //#region src/calls/ring-tone.ts
3
+ const toneHz = [440, 480];
4
+ const ringMs = 400;
5
+ const cycleMs = 3e3;
6
+ /**
7
+ * Plays a ring while `on`, through Web Audio. A browser that has not let the page play sound yet, before the person
8
+ * clicked or typed in it, keeps the context suspended and plays nothing: the ring still shows.
9
+ */
10
+ const useRingTone = (on) => {
11
+ useEffect(() => {
12
+ if (!on || typeof AudioContext === "undefined") return;
13
+ const context = new AudioContext();
14
+ const ring = () => {
15
+ const gain = context.createGain();
16
+ gain.gain.value = .08;
17
+ gain.connect(context.destination);
18
+ for (const [index, start] of [0, ringMs * 1.5 / 1e3].entries()) for (const hz of toneHz) {
19
+ const oscillator = context.createOscillator();
20
+ oscillator.frequency.value = hz;
21
+ oscillator.connect(gain);
22
+ oscillator.start(context.currentTime + start);
23
+ oscillator.stop(context.currentTime + start + ringMs / 1e3 / (index + 1));
24
+ }
25
+ };
26
+ ring();
27
+ const timer = setInterval(ring, cycleMs);
28
+ return () => {
29
+ clearInterval(timer);
30
+ context.close();
31
+ };
32
+ }, [on]);
33
+ };
34
+ //#endregion
35
+ export { useRingTone };
@@ -0,0 +1,16 @@
1
+ //#region src/calls/ringing.ts
2
+ /** The rings as read at `readAt`, each with the time it stops ringing here. */
3
+ const ringsUntil = (rings, readAt) => rings.map((ring) => ({
4
+ ...ring,
5
+ until: readAt + ring.remaining_ms
6
+ }));
7
+ /** The rings still ringing at `now`, and the next time one stops, to look again then. */
8
+ const ringingAt = (rings, now) => {
9
+ const ringing = rings.filter((ring) => ring.until > now);
10
+ return {
11
+ ringing,
12
+ next: ringing.length === 0 ? void 0 : Math.min(...ringing.map((ring) => ring.until))
13
+ };
14
+ };
15
+ //#endregion
16
+ export { ringingAt, ringsUntil };
@@ -0,0 +1,36 @@
1
+ import { useCallback } from "react";
2
+ import { jsx } from "react/jsx-runtime";
3
+ //#region src/calls/track-media.tsx
4
+ const keepPlaying = (element, track) => {
5
+ element.srcObject = new MediaStream([track]);
6
+ const play = () => {
7
+ if (track.readyState !== "live") return;
8
+ element.play().catch((error) => {
9
+ if (error instanceof DOMException && (error.name === "AbortError" || error.name === "NotAllowedError")) return;
10
+ throw error;
11
+ });
12
+ };
13
+ element.addEventListener("pause", play);
14
+ return () => element.removeEventListener("pause", play);
15
+ };
16
+ /** A video element showing the track, muted, inline, playing as soon as it can and for as long as the track is live. */
17
+ const TrackVideo = ({ track, ...rest }) => {
18
+ const ref = useCallback((element) => element ? keepPlaying(element, track) : void 0, [track]);
19
+ return /* @__PURE__ */ jsx("video", {
20
+ ref,
21
+ autoPlay: true,
22
+ playsInline: true,
23
+ muted: true,
24
+ ...rest
25
+ });
26
+ };
27
+ /** Plays the track's sound, unseen, for as long as the track is live. */
28
+ const TrackAudio = ({ track }) => {
29
+ const ref = useCallback((element) => element ? keepPlaying(element, track) : void 0, [track]);
30
+ return /* @__PURE__ */ jsx("audio", {
31
+ ref,
32
+ autoPlay: true
33
+ });
34
+ };
35
+ //#endregion
36
+ export { TrackAudio, TrackVideo };
@@ -0,0 +1,43 @@
1
+ import { activeSpeaker } from "./call-layout.js";
2
+ import { useEffect, useState } from "react";
3
+ //#region src/calls/use-audio-levels.ts
4
+ const readEveryMs = 200;
5
+ const holdMs = 1e3;
6
+ const levelOf = (analyser, samples) => {
7
+ analyser.getFloatTimeDomainData(samples);
8
+ let sum = 0;
9
+ for (const sample of samples) sum += sample * sample;
10
+ return Math.min(1, Math.sqrt(sum / samples.length) * 4);
11
+ };
12
+ /** Who of these members is speaking, by the sound of their microphone tracks, read in the browser through Web Audio. */
13
+ const useActiveSpeaker = (tracks) => {
14
+ const [speaker, setSpeaker] = useState();
15
+ const ids = [...tracks].map(([member, track]) => `${member}:${track.id}`).join(",");
16
+ useEffect(() => {
17
+ if (tracks.size === 0 || typeof AudioContext === "undefined") return;
18
+ const context = new AudioContext();
19
+ const analysers = new Map([...tracks].map(([member, track]) => {
20
+ const analyser = context.createAnalyser();
21
+ analyser.fftSize = 512;
22
+ context.createMediaStreamSource(new MediaStream([track])).connect(analyser);
23
+ return [member, analyser];
24
+ }));
25
+ const samples = /* @__PURE__ */ new Float32Array(512);
26
+ let last;
27
+ const timer = setInterval(() => {
28
+ const loudest = activeSpeaker(new Map([...analysers].map(([member, analyser]) => [member, levelOf(analyser, samples)])));
29
+ if (loudest) last = {
30
+ member: loudest,
31
+ at: Date.now()
32
+ };
33
+ setSpeaker(last && Date.now() - last.at < holdMs ? last.member : void 0);
34
+ }, readEveryMs);
35
+ return () => {
36
+ clearInterval(timer);
37
+ context.close();
38
+ };
39
+ }, [ids]);
40
+ return speaker;
41
+ };
42
+ //#endregion
43
+ export { useActiveSpeaker };
@@ -0,0 +1,157 @@
1
+ import { openDevices } from "./devices.js";
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
+ //#region src/calls/use-call-session.ts
4
+ const connectionsEveryMs = 2e3;
5
+ const stop = (tracks) => {
6
+ for (const track of tracks) track?.stop();
7
+ };
8
+ /**
9
+ * The member's session in the call the app's function at `url` admits them to: their devices from the start, for the
10
+ * 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
+ */
13
+ const useCallSession = (url) => {
14
+ const [phase, setPhase] = useState({ kind: "check" });
15
+ const [devices, setDevices] = useState();
16
+ const [microphoneOn, setMicrophoneOn] = useState(true);
17
+ const [cameraOn, setCameraOn] = useState(true);
18
+ const [tracks, setTracks] = useState([]);
19
+ const [members, setMembers] = useState([]);
20
+ const [connections, setConnections] = useState([]);
21
+ const [screen, setScreen] = useState();
22
+ const [refusedSending, setRefusedSending] = useState(false);
23
+ const call = useRef(void 0);
24
+ const gone = useRef(false);
25
+ const own = useRef([]);
26
+ const seen = useRef(/* @__PURE__ */ new Map());
27
+ const opening = useRef(Promise.resolve(void 0));
28
+ useEffect(() => {
29
+ let cancelled = false;
30
+ gone.current = false;
31
+ opening.current = openDevices().then((opened) => {
32
+ if (cancelled) {
33
+ stop([opened.microphone, opened.camera]);
34
+ return;
35
+ }
36
+ own.current.push(opened.microphone, opened.camera);
37
+ setDevices(opened);
38
+ return opened;
39
+ });
40
+ return () => {
41
+ cancelled = true;
42
+ gone.current = true;
43
+ call.current?.leave();
44
+ call.current = void 0;
45
+ stop(own.current);
46
+ own.current = [];
47
+ };
48
+ }, []);
49
+ useEffect(() => {
50
+ for (const { member } of members) if (!seen.current.has(member)) seen.current.set(member, Date.now());
51
+ }, [members]);
52
+ useEffect(() => {
53
+ if (phase.kind !== "in") return;
54
+ const timer = setInterval(() => void call.current?.connections().then(setConnections), connectionsEveryMs);
55
+ return () => clearInterval(timer);
56
+ }, [phase.kind]);
57
+ const join = useCallback(async () => {
58
+ setPhase({ kind: "joining" });
59
+ const { joinCall, PublishRefused } = await import("@protobase/client/calls");
60
+ const joined = await joinCall({
61
+ url,
62
+ onTracks: setTracks,
63
+ onMembers: setMembers,
64
+ onState: (state) => {
65
+ if (state.state === "joined" || state.state === "resuming") setPhase({
66
+ kind: "in",
67
+ resuming: state.state === "resuming"
68
+ });
69
+ if (state.state === "left") setPhase({
70
+ kind: "left",
71
+ reason: state.reason
72
+ });
73
+ }
74
+ }).then((made) => made, () => void 0);
75
+ if (!joined) return;
76
+ if (gone.current) return joined.leave();
77
+ call.current = joined;
78
+ const opened = await opening.current;
79
+ const sending = {
80
+ ...opened?.microphone && { microphone: opened.microphone },
81
+ ...opened?.camera && { camera: opened.camera }
82
+ };
83
+ await joined.publish(sending).then(() => {
84
+ if (sending.microphone) joined.mute("microphone", !microphoneOn);
85
+ if (sending.camera) joined.mute("camera", !cameraOn);
86
+ }, (error) => {
87
+ if (!(error instanceof PublishRefused)) throw error;
88
+ setRefusedSending(true);
89
+ });
90
+ }, [
91
+ url,
92
+ microphoneOn,
93
+ cameraOn
94
+ ]);
95
+ const toggleMicrophone = useCallback(() => {
96
+ const next = !microphoneOn;
97
+ setMicrophoneOn(next);
98
+ if (devices?.microphone) devices.microphone.enabled = next;
99
+ call.current?.mute("microphone", !next);
100
+ }, [microphoneOn, devices]);
101
+ const toggleCamera = useCallback(() => {
102
+ const next = !cameraOn;
103
+ setCameraOn(next);
104
+ if (devices?.camera) devices.camera.enabled = next;
105
+ call.current?.mute("camera", !next);
106
+ }, [cameraOn, devices]);
107
+ const stopSharing = useCallback(async (shared) => {
108
+ setScreen(void 0);
109
+ stop([shared.video, shared.audio]);
110
+ await call.current?.unpublish("screen");
111
+ if (shared.audio) await call.current?.unpublish("screen-audio");
112
+ }, []);
113
+ const toggleScreen = useCallback(async () => {
114
+ if (screen) return stopSharing(screen);
115
+ let display;
116
+ try {
117
+ display = await navigator.mediaDevices.getDisplayMedia({
118
+ video: true,
119
+ audio: true
120
+ });
121
+ } catch (error) {
122
+ if (error instanceof DOMException && error.name === "NotAllowedError") return;
123
+ throw error;
124
+ }
125
+ const shared = {
126
+ video: display.getVideoTracks()[0],
127
+ ...display.getAudioTracks()[0] && { audio: display.getAudioTracks()[0] }
128
+ };
129
+ own.current.push(shared.video, shared.audio);
130
+ shared.video.addEventListener("ended", () => void stopSharing(shared));
131
+ await call.current?.publish({
132
+ screen: shared.video,
133
+ ...shared.audio && { "screen-audio": shared.audio }
134
+ });
135
+ setScreen(shared);
136
+ }, [screen, stopSharing]);
137
+ const canShare = typeof navigator !== "undefined" && typeof navigator.mediaDevices?.getDisplayMedia === "function";
138
+ return {
139
+ phase,
140
+ devices,
141
+ microphoneOn,
142
+ cameraOn,
143
+ tracks,
144
+ members,
145
+ connections,
146
+ screen,
147
+ refusedSending,
148
+ seen: seen.current,
149
+ self: call.current?.participant,
150
+ join,
151
+ toggleMicrophone,
152
+ toggleCamera,
153
+ ...canShare && { toggleScreen }
154
+ };
155
+ };
156
+ //#endregion
157
+ export { useCallSession };
@@ -0,0 +1,17 @@
1
+ //#region src/calls/use-record-call-address.d.ts
2
+ /**
3
+ * A record's call as the app's address holds it: whether the page joins it (`call=join`), the link that joins it, as a
4
+ * ring's Accept and a call mark's Join open it, and the link back to the record without the call, as Leave opens it.
5
+ */
6
+ export declare const useRecordCallAddress: (resource: string, recordKey: string) => {
7
+ joining: boolean;
8
+ join: {
9
+ href: string;
10
+ onFollow: () => void;
11
+ };
12
+ leave: {
13
+ href: string;
14
+ onFollow: () => void;
15
+ };
16
+ };
17
+ //#endregion
@@ -0,0 +1,30 @@
1
+ import { useAppAddress } from "../app/use-app-address.js";
2
+ //#region src/calls/use-record-call-address.ts
3
+ /**
4
+ * A record's call as the app's address holds it: whether the page joins it (`call=join`), the link that joins it, as a
5
+ * ring's Accept and a call mark's Join open it, and the link back to the record without the call, as Leave opens it.
6
+ */
7
+ const useRecordCallAddress = (resource, recordKey) => {
8
+ const { address, href, open } = useAppAddress();
9
+ const joining = address?.kind === "record" && address.resource === resource && address.key === recordKey && address.call === "join";
10
+ const target = (to) => ({
11
+ href: to,
12
+ onFollow: () => open(to)
13
+ });
14
+ return {
15
+ joining,
16
+ join: target(href({
17
+ kind: "record",
18
+ resource,
19
+ key: recordKey,
20
+ call: "join"
21
+ })),
22
+ leave: target(href({
23
+ kind: "record",
24
+ resource,
25
+ key: recordKey
26
+ }))
27
+ };
28
+ };
29
+ //#endregion
30
+ export { useRecordCallAddress };