Skip to content

Commit 6919dce

Browse files
authored
Merge pull request #243 from pineplace/implememnt-permissions-request-page
Implememnt permissions request page
2 parents f35046d + db5deb7 commit 6919dce

9 files changed

Lines changed: 339 additions & 20 deletions

File tree

public/permissions.html

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
<!DOCTYPE html>
2-
<html lang="en">
2+
<html lang="en" style="width: 100%; height: 100%">
33

44
<head>
55
<meta charset="UTF-8">
6+
<link href="./klack_tailwind_global.css" rel="stylesheet">
7+
<title>klack: Permissions request</title>
68
</head>
79

8-
<body>
10+
<body style="width: 100%; height: 100%; margin: 0; padding: 0;">
11+
<div id="root" style="width: 100%; height: 100%;"></div>
912
<script type="module" src="./permissions.bundle.mjs"></script>
1013
</body>
1114

public/static/Lookup_512.png

17.8 KB
Loading
Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,74 @@
1-
chrome.runtime.onInstalled.addListener((_details) => {
2-
console.log("Handle 'chrome.runtime.onInstalled'");
3-
(async () => {
4-
await chrome.tabs.create({
1+
import {
2+
Message,
3+
MessageResponse,
4+
MessageResponseType,
5+
MessageType,
6+
} from "@/shared/messaging";
7+
import { storage } from "@/shared/storage";
8+
9+
class PermissionsGiver {
10+
static async openPermissionsTab() {
11+
console.log("PermissionsGiver.openPermissionsTab()");
12+
const tab = await chrome.tabs.create({
513
active: true,
614
url: chrome.runtime.getURL("permissions.html"),
715
});
16+
await storage.permissions.tabId.set(tab.id || 0);
17+
}
18+
19+
static async closePermissionsTab() {
20+
console.log("PermissionsGiver.closePermissionsTab()");
21+
await chrome.tabs.remove(await storage.permissions.tabId.get());
22+
await storage.permissions.tabId.set(0);
23+
}
24+
}
25+
26+
chrome.runtime.onInstalled.addListener((_details) => {
27+
console.log("Handle 'chrome.runtime.onInstalled'");
28+
(async () => {
29+
await PermissionsGiver.openPermissionsTab();
830
})().catch((err) => {
931
console.error(
1032
`Error in 'chrome.runtime.onInstalled' handler: ${(err as Error).toString()}`,
1133
);
1234
});
1335
});
36+
37+
chrome.runtime.onMessage.addListener(
38+
(
39+
message: Message,
40+
_sender,
41+
senderResponse: (response: MessageResponse) => void,
42+
) => {
43+
(async () => {
44+
const { type, target, options: _options } = message;
45+
if (target !== "background") {
46+
return;
47+
}
48+
switch (type) {
49+
case MessageType.PermissionsTabOpen:
50+
await PermissionsGiver.openPermissionsTab();
51+
break;
52+
case MessageType.PermissionsTabClose:
53+
await PermissionsGiver.closePermissionsTab();
54+
break;
55+
}
56+
})()
57+
.then(() => {
58+
senderResponse({
59+
type: MessageResponseType.ResultOk,
60+
} satisfies MessageResponse);
61+
})
62+
.catch((err) => {
63+
console.error(
64+
`Error in 'chrome.runtime.onMessage' handler: ${(err as Error).toString()}`,
65+
);
66+
senderResponse({
67+
type: MessageResponseType.ResultError,
68+
reason: (err as Error).toString(),
69+
} satisfies MessageResponse);
70+
});
71+
// NOTE: We need to return `true`, because we using `sendResponse` asynchronously
72+
return true;
73+
},
74+
);

src/shared/messaging.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export enum MessageType {
88
RecordingResume = "RecordingResume",
99
RecordingCancel = "RecordingCancel",
1010
RecordingSave = "RecordingSave",
11+
PermissionsTabOpen = "PermissionsPageOpen",
12+
PermissionsTabClose = "PermissionsPageClose",
1113
// offscreen
1214
RecorderCreate = "RecorderCreate",
1315
RecorderStart = "RecorderStart",
@@ -96,6 +98,18 @@ export const sender = {
9698
options,
9799
});
98100
},
101+
permissionsTabOpen: () => {
102+
return chrome.runtime.sendMessage<Message, MessageResponse>({
103+
type: MessageType.PermissionsTabOpen,
104+
target: "background",
105+
});
106+
},
107+
permissionsTabClose: () => {
108+
return chrome.runtime.sendMessage<Message, MessageResponse>({
109+
type: MessageType.PermissionsTabClose,
110+
target: "background",
111+
});
112+
},
99113
},
100114
offscreen: {
101115
recorderCreate: (options: RecorderCreateOptions) => {

src/shared/storage.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export enum StorageKey {
88
DevicesVideoEnabled = "devices.video.enabled",
99
DevicesVideoId = "devices.video.id",
1010
DevicesVideoName = "devices.video.name",
11+
PermissionsTabId = "permissions.tabId",
1112
RecordingState = "recording.state",
1213
RecordingDownloadId = "recording.download_id",
1314
UiCameraBubbleEnabled = "ui.cameraBubble.enabled",
@@ -34,6 +35,7 @@ type StorageValueTypeMap = {
3435
[StorageKey.DevicesVideoEnabled]: boolean;
3536
[StorageKey.DevicesVideoId]: string;
3637
[StorageKey.DevicesVideoName]: string;
38+
[StorageKey.PermissionsTabId]: number;
3739
[StorageKey.RecordingState]: RecordingState;
3840
[StorageKey.RecordingDownloadId]: number;
3941
[StorageKey.UiCameraBubbleEnabled]: boolean;
@@ -85,6 +87,9 @@ export const storage = {
8587
name: createStorageSetterGetter(StorageKey.DevicesVideoName),
8688
},
8789
},
90+
permissions: {
91+
tabId: createStorageSetterGetter(StorageKey.PermissionsTabId),
92+
},
8893
recording: {
8994
state: createStorageSetterGetter(StorageKey.RecordingState),
9095
downloadId: createStorageSetterGetter(StorageKey.RecordingDownloadId),
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { useEffect, useState } from "react";
2+
import {
3+
IconVideo,
4+
IconMicrophone,
5+
IconLoader2,
6+
IconCircleCheck,
7+
IconCircleX,
8+
} from "@tabler/icons-react";
9+
import lookup512 from "/static/Lookup_512.png";
10+
import { sender } from "@/shared/messaging";
11+
12+
export const Permissions = () => {
13+
const [videoPermissionState, setVideoPermissionState] =
14+
useState<PermissionState>("prompt");
15+
const [micPermissionsState, setMicPermissionsState] =
16+
useState<PermissionState>("prompt");
17+
18+
useEffect(() => {
19+
(async () => {
20+
const permissionsStatus = await navigator.permissions.query({
21+
name: "camera",
22+
});
23+
setVideoPermissionState(permissionsStatus.state);
24+
permissionsStatus.onchange = () => {
25+
setVideoPermissionState(permissionsStatus.state);
26+
};
27+
})().catch((err) => {
28+
console.error(
29+
`Can't get permissions status for video: ${(err as Error).toString()}`,
30+
);
31+
});
32+
}, []);
33+
34+
useEffect(() => {
35+
(async () => {
36+
const permissionsStatus = await navigator.permissions.query({
37+
name: "microphone",
38+
});
39+
setMicPermissionsState(permissionsStatus.state);
40+
permissionsStatus.onchange = () => {
41+
setMicPermissionsState(permissionsStatus.state);
42+
};
43+
})().catch((err) => {
44+
console.error(
45+
`Can't get permissions status for video: ${(err as Error).toString()}`,
46+
);
47+
});
48+
}, []);
49+
50+
useEffect(() => {
51+
if (
52+
videoPermissionState !== "granted" ||
53+
micPermissionsState !== "granted"
54+
) {
55+
return;
56+
}
57+
setTimeout(() => {
58+
sender.background.permissionsTabClose().catch((err) => {
59+
console.error(
60+
`Can't send event to background: ${(err as Error).toString()}`,
61+
);
62+
});
63+
}, 3 * 1000);
64+
}, [micPermissionsState, videoPermissionState]);
65+
66+
useEffect(() => {
67+
(async () => {
68+
await navigator.mediaDevices.getUserMedia({
69+
video: true,
70+
});
71+
})().catch((err) => {
72+
console.error(
73+
`Error on 'getUserMedia' for video device permissions: ${(err as Error).toString()}`,
74+
);
75+
});
76+
}, []);
77+
78+
useEffect(() => {
79+
(async () => {
80+
await navigator.mediaDevices.getUserMedia({
81+
audio: true,
82+
});
83+
})().catch((err) => {
84+
console.error(
85+
`Can't on 'getUserMedia' for audio device permissions: ${(err as Error).toString()}`,
86+
);
87+
});
88+
}, []);
89+
90+
return (
91+
<div className="bg-klack-charcoal-800 flex h-full w-full flex-col items-center justify-center gap-25">
92+
<img src={lookup512} />
93+
<div className="font-dosis w-[665px] text-center text-3xl text-white">
94+
{videoPermissionState === "denied" ||
95+
micPermissionsState === "denied" ? (
96+
<p>
97+
You denied access to required devices. Please enable them in your
98+
browser settings and close this tab.
99+
</p>
100+
) : (
101+
<p>
102+
Now the browser will ask for your permission to use your camera and
103+
microphone. Please do not close this tab, it will close
104+
automatically.
105+
</p>
106+
)}
107+
</div>
108+
<div className="flex flex-row items-center gap-[150px]">
109+
<div className="flex flex-row items-center gap-[10px]">
110+
<IconVideo
111+
className="stroke-white"
112+
stroke={2}
113+
size={64}
114+
/>
115+
{videoPermissionState === "prompt" ? (
116+
<IconLoader2
117+
className="animate-spin stroke-white"
118+
stroke={2}
119+
size={32}
120+
/>
121+
) : videoPermissionState === "granted" ? (
122+
<IconCircleCheck
123+
className="fill-klack-emerald-400 stroke-white"
124+
stroke={2}
125+
size={48}
126+
/>
127+
) : (
128+
<IconCircleX
129+
className="fill-klack-red-500 stroke-white"
130+
stroke={2}
131+
size={48}
132+
/>
133+
)}
134+
</div>
135+
<div className="flex flex-row items-center gap-[10px]">
136+
<IconMicrophone
137+
className="stroke-white"
138+
stroke={2}
139+
size={64}
140+
/>
141+
{micPermissionsState === "prompt" ? (
142+
<IconLoader2
143+
className="animate-spin stroke-white"
144+
stroke={2}
145+
size={32}
146+
/>
147+
) : micPermissionsState === "granted" ? (
148+
<IconCircleCheck
149+
className="fill-klack-emerald-400 stroke-white"
150+
stroke={2}
151+
size={48}
152+
/>
153+
) : (
154+
<IconCircleX
155+
className="fill-klack-red-500 stroke-white"
156+
stroke={2}
157+
size={48}
158+
/>
159+
)}
160+
</div>
161+
</div>
162+
</div>
163+
);
164+
};
Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1-
(async () => {
2-
await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
3-
})().catch((err) => console.error((err as Error).toString()));
1+
import React from "react";
2+
import ReactDOM from "react-dom/client";
3+
import { Permissions } from "./Permissions";
4+
5+
const root = document.getElementById("root");
6+
7+
if (!root) {
8+
console.error("Can't find element with id 'root");
9+
} else {
10+
ReactDOM.createRoot(root).render(React.createElement(Permissions));
11+
}

0 commit comments

Comments
 (0)