diff --git a/spot/entrypoints/popup/App.tsx b/spot/entrypoints/popup/App.tsx
index 3d9314f76..3317418aa 100644
--- a/spot/entrypoints/popup/App.tsx
+++ b/spot/entrypoints/popup/App.tsx
@@ -1,165 +1,49 @@
-import orLogo from "~/assets/orSpot.svg";
-import micOff from "~/assets/mic-off-red.svg";
-import micOn from "~/assets/mic-on-dark.svg";
+import { createEffect, onMount } from "solid-js";
import Login from "~/entrypoints/popup/Login";
import Settings from "~/entrypoints/popup/Settings";
-import { createSignal, createEffect, onMount } from "solid-js";
-import Dropdown from "~/entrypoints/popup/Dropdown";
-import Button from "~/entrypoints/popup/Button";
-import {
- ChevronSvg,
- RecordDesktopSvg,
- RecordTabSvg,
- HomePageSvg,
- SlackSvg,
- SettingsSvg,
-} from "./Icons";
-
-async function getAudioDevices() {
- try {
- await navigator.mediaDevices.getUserMedia({ audio: true });
- const devices = await navigator.mediaDevices.enumerateDevices();
- const audioDevices = devices
- .filter((device) => device.kind === "audioinput")
- .map((device) => ({ label: device.label, id: device.deviceId }));
-
- return { granted: true, audioDevices };
- } catch (error) {
- console.error("Error accessing audio devices:", error);
- const msg = error.message ?? "";
- return {
- granted: false,
- denied: msg.includes("denied"),
- audioDevices: [],
- };
- }
-}
-
-const orSite = () => {
- window.open("https://openreplay.com", "_blank");
-};
-
-function Header({ openSettings }: { openSettings: () => void }) {
- const openHomePage = async () => {
- const { settings } = await chrome.storage.local.get("settings");
- return window.open(`${settings.ingestPoint}/spots`, "_blank");
- };
- return (
-
-
-

-
- OpenReplay Spot
-
-
-
-
-
- );
-}
-
-const STATE = {
- empty: "empty",
- login: "login",
- ready: "ready",
- starting: "starting",
- recording: "recording",
-};
+import Header from "./components/Header";
+import RecordingControls from "./components/RecordingControls";
+import AudioPicker from "./components/AudioPicker";
+import { useAppState } from "./hooks/useAppState";
+import { useAudioDevices } from "./hooks/useAudioDevices";
+import { AppState } from "./types";
function App() {
- const [state, setState] = createSignal(STATE.empty);
- const [isSettingsOpen, setIsSettingsOpen] = createSignal(false);
- const [mic, setMic] = createSignal(false);
- const [selectedAudioDevice, setSelectedAudioDevice] = createSignal("");
- const [hasPermissions, setHasPermissions] = createSignal(false);
+ const {
+ state,
+ isSettingsOpen,
+ startRecording,
+ stopRecording,
+ openSettings,
+ closeSettings,
+ } = useAppState();
+ const {
+ audioDevices,
+ selectedAudioDevice,
+ mic,
+ hasPermissions,
+ isChecking,
+ checkAudioDevices,
+ handleMicToggle,
+ selectAudioDevice,
+ } = useAudioDevices();
+
+ // Listen for mic status updates from background
onMount(() => {
browser.runtime.onMessage.addListener((message) => {
- if (message.type === "popup:no-login") {
- setState(STATE.login);
- }
- if (message.type === "popup:login") {
- setState(STATE.ready);
- }
- if (message.type === "popup:stopped") {
- setState(STATE.ready);
- }
- if (message.type === "popup:started") {
- setState(STATE.recording);
- }
if (message.type === "popup:mic-status") {
setMic(message.status);
}
});
- void browser.runtime.sendMessage({ type: "popup:check-status" });
});
- const startRecording = async (reqTab: "tab" | "desktop") => {
- setState(STATE.starting);
- await browser.runtime.sendMessage({
- type: "popup:start",
- area: reqTab,
- mic: mic(),
- audioId: selectedAudioDevice(),
- permissions: hasPermissions(),
- });
- window.close();
+ const handleStartRecording = (area: "tab" | "desktop") => {
+ startRecording(area, mic(), selectedAudioDevice(), hasPermissions());
};
- const stopRecording = () => {
- void browser.runtime.sendMessage({
- type: "popup:stop",
- mic: mic(),
- audioId: selectedAudioDevice(),
- });
- };
-
- const toggleMic = async () => {
- setMic(!mic());
- };
-
- const openSettings = () => {
- setIsSettingsOpen(true);
- };
- const closeSettings = () => {
- setIsSettingsOpen(false);
+ const handleStopRecording = () => {
+ stopRecording(mic(), selectedAudioDevice());
};
return (
@@ -167,58 +51,30 @@ function App() {
{isSettingsOpen() ? (
) : (
-
+
- {state() === STATE.login ? (
+ {state() === AppState.LOGIN ? (
) : (
<>
- {state() === STATE.recording ? (
-
@@ -227,111 +83,4 @@ function App() {
);
}
-interface IAudioPicker {
- mic: () => boolean;
- toggleMic: () => void;
- selectedAudioDevice: () => string;
- setSelectedAudioDevice: (value: string) => void;
- setHasPermissions: (value: boolean) => void;
-}
-function AudioPicker(props: IAudioPicker) {
- const [audioDevices, setAudioDevices] = createSignal(
- [] as { label: string; id: string }[],
- );
- const [checkedAudioDevices, setCheckedAudioDevices] = createSignal(0);
-
- createEffect(() => {
- chrome.storage.local.get("audioPerm", (data) => {
- if (data.audioPerm && audioDevices().length === 0) {
- props.setHasPermissions(true);
- void checkAudioDevices();
- }
- });
- });
-
- const checkAudioDevices = async () => {
- const { granted, audioDevices, denied } = await getAudioDevices();
- if (!granted && !denied) {
- void browser.runtime.sendMessage({
- type: "popup:get-audio-perm",
- });
- browser.runtime.onMessage.addListener((message) => {
- if (message.type === "popup:audio-perm") {
- void checkAudioDevices();
- }
- });
- } else if (audioDevices.length > 0) {
- chrome.storage.local.set({ audioPerm: granted });
- setAudioDevices(audioDevices);
- props.setSelectedAudioDevice(audioDevices[0]?.id || "");
- }
- };
-
- const checkAudio = async () => {
- if (checkedAudioDevices() > 0) {
- return;
- }
- setCheckedAudioDevices(1);
- await checkAudioDevices();
- setCheckedAudioDevices(2);
- };
- const onSelect = (value) => {
- props.setSelectedAudioDevice(value);
- if (!props.mic()) {
- props.toggleMic();
- }
- };
-
- const onMicToggle = async () => {
- if (!audioDevices().length) {
- return await checkAudioDevices();
- }
- if (!props.selectedAudioDevice() && audioDevices().length) {
- onSelect(audioDevices()[0].id);
- } else {
- props.toggleMic();
- }
- };
-
- return (
-
-
-
)
-
-
- {audioDevices().length === 0 ? (
-
- {checkedAudioDevices() === 1
- ? "Loading audio devices"
- : "Grant microphone access"}
-
- ) : (
-
- )}
-
-
-
- );
-}
-
export default App;
diff --git a/spot/entrypoints/popup/components/AudioPicker.tsx b/spot/entrypoints/popup/components/AudioPicker.tsx
new file mode 100644
index 000000000..627b5c840
--- /dev/null
+++ b/spot/entrypoints/popup/components/AudioPicker.tsx
@@ -0,0 +1,57 @@
+import { Component, For } from "solid-js";
+import micOff from "~/assets/mic-off-red.svg";
+import micOn from "~/assets/mic-on-dark.svg";
+import Dropdown from "~/entrypoints/popup/Dropdown";
+import { ChevronSvg } from "../Icons";
+import { AudioDevice } from "../types";
+
+interface AudioPickerProps {
+ mic: () => boolean;
+ audioDevices: () => AudioDevice[];
+ selectedAudioDevice: () => string;
+ isChecking: () => boolean;
+ onMicToggle: () => void;
+ onCheckAudio: () => void;
+ onSelectDevice: (deviceId: string) => void;
+}
+
+const AudioPicker: Component
= (props) => {
+ return (
+
+
+
)
+
+
+
+ {props.audioDevices().length === 0 ? (
+
+ {props.isChecking()
+ ? "Loading audio devices"
+ : "Grant microphone access"}
+
+ ) : (
+
+ )}
+
+
+
+ );
+};
+
+export default AudioPicker;
diff --git a/spot/entrypoints/popup/components/Header.tsx b/spot/entrypoints/popup/components/Header.tsx
new file mode 100644
index 000000000..ec6f9d5b8
--- /dev/null
+++ b/spot/entrypoints/popup/components/Header.tsx
@@ -0,0 +1,73 @@
+import { Component } from "solid-js";
+import orLogo from "~/assets/orSpot.svg";
+import {
+ HomePageSvg,
+ SlackSvg,
+ SettingsSvg,
+} from "../Icons";
+
+interface HeaderProps {
+ openSettings: () => void;
+}
+
+const Header: Component = (props) => {
+ const openHomePage = async () => {
+ const { settings } = await chrome.storage.local.get("settings");
+ return window.open(`${settings.ingestPoint}/spots`, "_blank");
+ };
+
+ const openOrSite = () => {
+ window.open("https://openreplay.com", "_blank");
+ };
+
+ return (
+
+
+

+
+ OpenReplay Spot
+
+
+
+
+
+ );
+};
+
+export default Header;
diff --git a/spot/entrypoints/popup/components/RecordingControls.tsx b/spot/entrypoints/popup/components/RecordingControls.tsx
new file mode 100644
index 000000000..1fdd8fa61
--- /dev/null
+++ b/spot/entrypoints/popup/components/RecordingControls.tsx
@@ -0,0 +1,48 @@
+import { Component } from "solid-js";
+import { RecordTabSvg, RecordDesktopSvg } from "../Icons";
+import Button from "~/entrypoints/popup/Button";
+import { AppState, RecordingArea } from "../types";
+
+interface RecordingControlsProps {
+ state: AppState;
+ startRecording: (area: RecordingArea) => void;
+ stopRecording: () => void;
+}
+
+const RecordingControls: Component = (props) => {
+ return (
+ <>
+ {props.state === AppState.RECORDING && (
+
+ )}
+
+ {props.state === AppState.STARTING && (
+
+
Your recording is starting
+
+ )}
+
+ {props.state === AppState.READY && (
+
+ props.startRecording("tab")}
+ >
+
+ Record Tab
+
+
+ props.startRecording("desktop")}
+ >
+
+ Record Desktop
+
+
+ )}
+ >
+ );
+};
+
+export default RecordingControls;
diff --git a/spot/entrypoints/popup/hooks/useAppState.ts b/spot/entrypoints/popup/hooks/useAppState.ts
new file mode 100644
index 000000000..4c7b6a844
--- /dev/null
+++ b/spot/entrypoints/popup/hooks/useAppState.ts
@@ -0,0 +1,63 @@
+import { createSignal, onMount } from "solid-js";
+import { AppState, RecordingArea } from "../types";
+
+export function useAppState() {
+ const [state, setState] = createSignal(AppState.EMPTY);
+ const [isSettingsOpen, setIsSettingsOpen] = createSignal(false);
+
+ onMount(() => {
+ browser.runtime.onMessage.addListener((message) => {
+ if (message.type === "popup:no-login") {
+ setState(AppState.LOGIN);
+ }
+ if (message.type === "popup:login") {
+ setState(AppState.READY);
+ }
+ if (message.type === "popup:stopped") {
+ setState(AppState.READY);
+ }
+ if (message.type === "popup:started") {
+ setState(AppState.RECORDING);
+ }
+ });
+
+ void browser.runtime.sendMessage({ type: "popup:check-status" });
+ });
+
+ const startRecording = async (
+ area: RecordingArea,
+ mic: boolean,
+ audioId: string,
+ permissions: boolean
+ ) => {
+ setState(AppState.STARTING);
+ await browser.runtime.sendMessage({
+ type: "popup:start",
+ area,
+ mic,
+ audioId,
+ permissions,
+ });
+ window.close();
+ };
+
+ const stopRecording = (mic: boolean, audioId: string) => {
+ void browser.runtime.sendMessage({
+ type: "popup:stop",
+ mic,
+ audioId,
+ });
+ };
+
+ const openSettings = () => setIsSettingsOpen(true);
+ const closeSettings = () => setIsSettingsOpen(false);
+
+ return {
+ state,
+ isSettingsOpen,
+ startRecording,
+ stopRecording,
+ openSettings,
+ closeSettings,
+ };
+}
diff --git a/spot/entrypoints/popup/hooks/useAudioDevices.ts b/spot/entrypoints/popup/hooks/useAudioDevices.ts
new file mode 100644
index 000000000..25ae807d3
--- /dev/null
+++ b/spot/entrypoints/popup/hooks/useAudioDevices.ts
@@ -0,0 +1,100 @@
+import { createSignal, createEffect } from "solid-js";
+import { AudioDevice } from "../types";
+import { getAudioDevices } from "../utils/audio";
+
+export function useAudioDevices() {
+ const [audioDevices, setAudioDevices] = createSignal([]);
+ const [selectedAudioDevice, setSelectedAudioDevice] = createSignal("");
+ const [mic, setMic] = createSignal(false);
+ const [hasPermissions, setHasPermissions] = createSignal(false);
+ const [isChecking, setIsChecking] = createSignal(false);
+
+ createEffect(() => {
+ chrome.storage.local.get("audioPerm", (data) => {
+ if (data.audioPerm && audioDevices().length === 0) {
+ setHasPermissions(true);
+ checkAudioDevices().then(async (devices) => {
+ const { selectedAudioId, micOn } = await chrome.storage.local.get([
+ "selectedAudioId",
+ "micOn",
+ ]);
+
+ if (selectedAudioId) {
+ const selectedDevice = devices.find(
+ (device) => device.id === selectedAudioId
+ );
+ if (selectedDevice) {
+ setSelectedAudioDevice(selectedDevice.id);
+ }
+ }
+
+ if (micOn) {
+ toggleMic();
+ }
+ });
+ }
+ });
+ });
+
+ const checkAudioDevices = async (): Promise => {
+ setIsChecking(true);
+
+ const { granted, audioDevices, denied } = await getAudioDevices();
+
+ if (!granted && !denied) {
+ void browser.runtime.sendMessage({
+ type: "popup:get-audio-perm",
+ });
+
+ browser.runtime.onMessage.addListener((message) => {
+ if (message.type === "popup:audio-perm") {
+ void checkAudioDevices();
+ }
+ });
+ } else if (audioDevices.length > 0) {
+ chrome.storage.local.set({ audioPerm: granted });
+ setAudioDevices(audioDevices);
+ setSelectedAudioDevice(audioDevices[0]?.id || "");
+ }
+
+ setIsChecking(false);
+ return audioDevices;
+ };
+
+ const toggleMic = () => {
+ setMic(!mic());
+ };
+
+ const selectAudioDevice = (deviceId: string) => {
+ setSelectedAudioDevice(deviceId);
+ if (!mic()) {
+ toggleMic();
+ }
+ chrome.storage.local.set({ selectedAudioId: deviceId, micOn: true });
+ };
+
+ const handleMicToggle = async () => {
+ if (!audioDevices().length) {
+ return await checkAudioDevices();
+ }
+
+ if (!selectedAudioDevice() && audioDevices().length) {
+ selectAudioDevice(audioDevices()[0].id);
+ } else {
+ chrome.storage.local.set({ micOn: !mic() });
+ toggleMic();
+ }
+ };
+
+ return {
+ audioDevices,
+ selectedAudioDevice,
+ mic,
+ hasPermissions,
+ isChecking,
+ checkAudioDevices,
+ toggleMic,
+ selectAudioDevice,
+ handleMicToggle,
+ };
+}
diff --git a/spot/entrypoints/popup/types/index.ts b/spot/entrypoints/popup/types/index.ts
new file mode 100644
index 000000000..71f13490f
--- /dev/null
+++ b/spot/entrypoints/popup/types/index.ts
@@ -0,0 +1,14 @@
+export type AudioDevice = {
+ label: string;
+ id: string;
+};
+
+export enum AppState {
+ EMPTY = "empty",
+ LOGIN = "login",
+ READY = "ready",
+ STARTING = "starting",
+ RECORDING = "recording",
+}
+
+export type RecordingArea = "tab" | "desktop";
diff --git a/spot/entrypoints/popup/utils/audio.ts b/spot/entrypoints/popup/utils/audio.ts
new file mode 100644
index 000000000..dab6cf210
--- /dev/null
+++ b/spot/entrypoints/popup/utils/audio.ts
@@ -0,0 +1,24 @@
+import type { AudioDevice } from "../types";
+export async function getAudioDevices(): Promise<{
+ granted: boolean;
+ denied?: boolean;
+ audioDevices: AudioDevice[];
+}> {
+ try {
+ await navigator.mediaDevices.getUserMedia({ audio: true });
+ const devices = await navigator.mediaDevices.enumerateDevices();
+ const audioDevices = devices
+ .filter((device) => device.kind === "audioinput")
+ .map((device) => ({ label: device.label, id: device.deviceId }));
+
+ return { granted: true, audioDevices };
+ } catch (error) {
+ console.error("Error accessing audio devices:", error);
+ const msg = error.message ?? "";
+ return {
+ granted: false,
+ denied: msg.includes("denied"),
+ audioDevices: [],
+ };
+ }
+}