web: answer incoming calls from the browser notification
Owner follow-up to the softphone (spec 05 §5). - the incoming call notification comes from a service worker (public/sw.js) so it carries Answer and Decline buttons (Chromium); Answer and a plain click focus the softphone tab, Decline doesn't - shown whenever the tab isn't focused, not only when it's hidden; kept up while it rings and closed once it stops - the softphone panel says how to allow notifications when they're blocked
This commit is contained in:
1 parent
431ada51aa
commit
33d5f1a5f5
4 files changed
+100
-26
No files matched your search
+14
-14
@@ -5,19 +5,19 @@ proxy (`https://<your host>/me`). You need: a user with an extension that has th
|
||||
softphone on (say 101), a second phone (desk phone or phone app, say 102), and a third extension
|
||||
to transfer to (say 103, any phone). Headphones help with echo.
|
||||
|
||||
| # | Do this | Pass when |
|
||||
| --- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| 1 | Sign in as the user, click **Enable softphone**, allow the microphone. | Status turns **Ready**; Admin → Dashboard shows 101 online. |
|
||||
| 2 | Reload the page. | It comes back **Ready** without asking again. |
|
||||
| 3 | Open `/me` in a second tab. | The second tab says **Active in another tab**. Click **Use here**: it becomes **Ready** and the first tab says **Active in another tab**. Close the second tab and click **Use here** in the first. |
|
||||
| 4 | Open **Keypad**, type `102` (also try typing on the keyboard and pressing Enter), call. | 102 rings; when answered both sides hear each other; the card shows a running timer. |
|
||||
| 5 | Press **Mute**, talk, then **Unmute**. | 102 hears nothing while muted, then hears you again. |
|
||||
| 6 | Press **Hold**, then **Resume**. | 102 hears hold music, then you again; the card says **On hold** meanwhile. |
|
||||
| 7 | During the call open the call's **Keypad** and press `1 2 3`. If 102 is a voicemail menu or an IVR, it reacts; otherwise call `*97` instead and enter your PIN. | The tones are recognized. |
|
||||
| 8 | Hang up. From 102 call 101. | The **Incoming call** dialog shows 102's name, the ringtone plays; **Answer** connects with two-way audio. Hang up from 102: the card goes away. |
|
||||
| 9 | Switch to another tab or app, then have 102 call 101 again (first allow notifications with **Notify me of calls**). | A desktop notification appears; clicking it brings the tab back. **Decline** sends 102 to busy/voicemail. |
|
||||
| 10 | While on a call with 102, have 103 call 101. | A second, smaller **Incoming call** card appears (no dialog). **Answer** puts 102 on hold (hears music) and connects 103. Hang up both. |
|
||||
| 11 | Call 102, **Transfer** → type `103` → **Transfer now**. | 102 is connected to 103; the browser call ends. |
|
||||
| 12 | Call 102, **Transfer** → `103` → **Talk first**; talk to 103; **Complete transfer**. Repeat with **Cancel** instead. | Complete: 102 and 103 are connected, the browser is out. Cancel: 103's call ends and you're back with 102. Also check **Microphone/Speaker** selects list your devices and a change takes effect on the next call. |
|
||||
| # | Do this | Pass when |
|
||||
| --- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| 1 | Sign in as the user, click **Enable softphone**, allow the microphone. | Status turns **Ready**; Admin → Dashboard shows 101 online. |
|
||||
| 2 | Reload the page. | It comes back **Ready** without asking again. |
|
||||
| 3 | Open `/me` in a second tab. | The second tab says **Active in another tab**. Click **Use here**: it becomes **Ready** and the first tab says **Active in another tab**. Close the second tab and click **Use here** in the first. |
|
||||
| 4 | Open **Keypad**, type `102` (also try typing on the keyboard and pressing Enter), call. | 102 rings; when answered both sides hear each other; the card shows a running timer. |
|
||||
| 5 | Press **Mute**, talk, then **Unmute**. | 102 hears nothing while muted, then hears you again. |
|
||||
| 6 | Press **Hold**, then **Resume**. | 102 hears hold music, then you again; the card says **On hold** meanwhile. |
|
||||
| 7 | During the call open the call's **Keypad** and press `1 2 3`. If 102 is a voicemail menu or an IVR, it reacts; otherwise call `*97` instead and enter your PIN. | The tones are recognized. |
|
||||
| 8 | Hang up. From 102 call 101. | The **Incoming call** dialog shows 102's name, the ringtone plays; **Answer** connects with two-way audio. Hang up from 102: the card goes away. |
|
||||
| 9 | Switch to another tab or app, then have 102 call 101 again (first allow notifications with **Notify me of calls**). | A desktop notification appears; clicking it brings the tab back. In Chrome/Edge, its **Answer** button answers and brings the tab back, its **Decline** button declines; it closes by itself if 102 hangs up first. **Decline** sends 102 to busy/voicemail. |
|
||||
| 10 | While on a call with 102, have 103 call 101. | A second, smaller **Incoming call** card appears (no dialog). **Answer** puts 102 on hold (hears music) and connects 103. Hang up both. |
|
||||
| 11 | Call 102, **Transfer** → type `103` → **Transfer now**. | 102 is connected to 103; the browser call ends. |
|
||||
| 12 | Call 102, **Transfer** → `103` → **Talk first**; talk to 103; **Complete transfer**. Repeat with **Cancel** instead. | Complete: 102 and 103 are connected, the browser is out. Cancel: 103's call ends and you're back with 102. Also check **Microphone/Speaker** selects list your devices and a change takes effect on the next call. |
|
||||
|
||||
Record the result (browser, version, pass/fail per step) in the PR or issue for M5-T2.
|
||||
@@ -0,0 +1,34 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// The softphone's incoming call notification (05 §5). Only a service worker's notifications can
|
||||
// carry buttons. The softphone tab asks for one here; Answer/Decline go back to that tab only.
|
||||
/* global self */
|
||||
self.addEventListener('message', (e) => {
|
||||
const call = e.data?.notifyCall;
|
||||
if (!call) return;
|
||||
e.waitUntil(
|
||||
self.registration.showNotification('Incoming call', {
|
||||
body: call.body,
|
||||
tag: call.id,
|
||||
requireInteraction: true,
|
||||
actions: [
|
||||
{ action: 'answer', title: 'Answer' },
|
||||
{ action: 'decline', title: 'Decline' },
|
||||
],
|
||||
data: { id: call.id, tab: e.source.id },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('notificationclick', (e) => {
|
||||
e.notification.close();
|
||||
const { id, tab } = e.notification.data;
|
||||
e.waitUntil(
|
||||
self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then(async (all) => {
|
||||
const client = all.find((c) => c.id === tab);
|
||||
if (!client) return;
|
||||
if (e.action) client.postMessage({ softphone: e.action, id });
|
||||
// Declining needs nothing from the tab; otherwise bring it back.
|
||||
if (e.action !== 'decline') await client.focus();
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -31,6 +31,19 @@ const remember = (on: boolean) => {
|
||||
}
|
||||
};
|
||||
|
||||
// The incoming call notification comes from public/sw.js so it can have Answer/Decline buttons.
|
||||
// No service worker (plain http, some private windows): no notification, the ringtone still plays.
|
||||
const sw = 'serviceWorker' in navigator ? navigator.serviceWorker : null;
|
||||
void sw?.register('/sw.js').catch(() => {});
|
||||
const notifyCall = (id: string, body: string) =>
|
||||
void sw?.ready.then((r) => r.active?.postMessage({ notifyCall: { id, body } }));
|
||||
// ponytail: a call that stops ringing before its notification is up leaves it showing; its
|
||||
// buttons then do nothing (answer/decline ignore unknown calls). Close it from sw.js if that bites.
|
||||
const closeCallNotes = (id?: string) =>
|
||||
void sw?.ready
|
||||
.then((r) => r.getNotifications(id ? { tag: id } : {}))
|
||||
.then((ns) => ns.forEach((n) => n.close()));
|
||||
|
||||
onTakeOver(async () => {
|
||||
await softphone.stop();
|
||||
softphone.setElsewhere();
|
||||
@@ -95,30 +108,51 @@ export function SoftphoneDock() {
|
||||
startSoftphone().catch((e: Error) => setProblem(e.message));
|
||||
}, []);
|
||||
|
||||
// Ring while anything is ringing here; tell a hidden tab with a notification.
|
||||
// Ring while anything is ringing here; tell an unfocused tab with a notification that stays
|
||||
// up until the call stops ringing (answered, declined, or the caller hung up).
|
||||
const incoming = calls.filter((c) => c.state === 'incoming');
|
||||
const ringingIds = incoming.map((c) => c.id).join(',');
|
||||
const notified = useRef(new Set<string>());
|
||||
useEffect(() => {
|
||||
if (ringingIds) startRinging();
|
||||
else stopRinging();
|
||||
for (const c of softphone.state.calls.filter((x) => x.state === 'incoming')) {
|
||||
const ringing = softphone.state.calls.filter((x) => x.state === 'incoming');
|
||||
for (const id of notified.current) {
|
||||
if (ringing.some((c) => c.id === id)) continue;
|
||||
closeCallNotes(id);
|
||||
notified.current.delete(id);
|
||||
}
|
||||
for (const c of ringing) {
|
||||
if (notified.current.has(c.id)) continue;
|
||||
notified.current.add(c.id);
|
||||
if (
|
||||
document.hidden &&
|
||||
!document.hasFocus() &&
|
||||
typeof Notification !== 'undefined' &&
|
||||
Notification.permission === 'granted'
|
||||
) {
|
||||
const n = new Notification('Incoming call', { body: nameOf(c), tag: c.id });
|
||||
n.onclick = () => {
|
||||
window.focus();
|
||||
n.close();
|
||||
};
|
||||
}
|
||||
)
|
||||
notifyCall(c.id, nameOf(c));
|
||||
}
|
||||
}, [ringingIds, nameOf]);
|
||||
useEffect(() => stopRinging, []);
|
||||
useEffect(
|
||||
() => () => {
|
||||
stopRinging();
|
||||
closeCallNotes();
|
||||
},
|
||||
[],
|
||||
);
|
||||
// The notification's Answer/Decline buttons (sw.js); stale ids are ignored by the engine.
|
||||
useEffect(() => {
|
||||
if (!sw) return;
|
||||
const onMessage = (e: MessageEvent<{ softphone?: string; id?: string }>) => {
|
||||
const { softphone: action, id } = e.data ?? {};
|
||||
if (!id) return;
|
||||
if (action === 'answer') run(softphone.answer(id));
|
||||
else if (action === 'decline') run(softphone.decline(id));
|
||||
};
|
||||
sw.addEventListener('message', onMessage);
|
||||
sw.startMessages();
|
||||
return () => sw.removeEventListener('message', onMessage);
|
||||
}, []);
|
||||
|
||||
async function enable() {
|
||||
setProblem(null);
|
||||
@@ -499,6 +533,12 @@ function Devices() {
|
||||
Notify me of calls
|
||||
</Button>
|
||||
)}
|
||||
{perm === 'denied' && (
|
||||
<p className="text-zinc-600 dark:text-zinc-400">
|
||||
Call notifications are blocked. To get them, allow notifications for this site in your
|
||||
browser’s site settings (the icon left of the address), then reload the page.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user