Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 64 additions & 61 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { ManageProductionsPage } from "./components/manage-productions-page/mana
import { CreateProductionPage } from "./components/create-production/create-production-page.tsx";
import { useSetupTokenRefresh } from "./hooks/use-reauth.tsx";
import { TUserSettings } from "./components/user-settings/types";
import { AutoJoinPage } from "./components/auto-join/auto-join-page.tsx";

const DisplayBoxPositioningContainer = styled(FlexContainer)`
justify-content: center;
Expand Down Expand Up @@ -107,68 +108,70 @@ const AppContent = ({
/>
</DisplayBoxPositioningContainer>
)}
{continueToApp && (
<>
{denied && (
<DisplayBoxPositioningContainer>
<DisplayWarning
text="To use this application it has to be granted access to audio devices. Reload browser and/or reset permissions to try
again."
title="Permissions have been denied"
/>
</DisplayBoxPositioningContainer>
)}
{!permission && !denied && (
<DisplayBoxPositioningContainer>
<DisplayWarning
text="To use this application it has to be granted access to audio devices."
title="Waiting for device permissions"
/>
</DisplayBoxPositioningContainer>
)}
{apiError && (
<DisplayBoxPositioningContainer>
<DisplayWarning
text="The server is not available. Reload page to try again."
title="Server not available"
/>
</DisplayBoxPositioningContainer>
)}
{permission && !denied && !apiError && userSettings && (
<Routes>
<>
<Route
path="/"
element={
<LandingPage setApiError={() => setApiError(true)} />
}
errorElement={<ErrorPage />}
/>
<Route
path="/create-production"
element={<CreateProductionPage />}
errorElement={<ErrorPage />}
/>
<Route
path="/manage-productions"
element={
<ManageProductionsPage
setApiError={() => setApiError(true)}
/>
}
errorElement={<ErrorPage />}
/>
<Route
path="/production-calls/production/:productionId/line/:lineId"
element={<CallsPage />}
errorElement={<ErrorPage />}
/>
<Route path="*" element={<NotFound />} />
</>
</Routes>
)}
</>
{continueToApp && denied && (
<DisplayBoxPositioningContainer>
<DisplayWarning
text="To use this application it has to be granted access to audio devices. Reload browser and/or reset permissions to try again."
title="Permissions have been denied"
/>
</DisplayBoxPositioningContainer>
)}
{continueToApp && !permission && !denied && (
<DisplayBoxPositioningContainer>
<DisplayWarning
text="To use this application it has to be granted access to audio devices."
title="Waiting for device permissions"
/>
</DisplayBoxPositioningContainer>
)}
{continueToApp && apiError && (
<DisplayBoxPositioningContainer>
<DisplayWarning
text="The server is not available. Reload page to try again."
title="Server not available"
/>
</DisplayBoxPositioningContainer>
)}

<Routes>
{permission && !denied && !apiError && userSettings && (
<>
<Route
path="/"
element={<LandingPage setApiError={() => setApiError(true)} />}
errorElement={<ErrorPage />}
/>
<Route
path="/create-production"
element={<CreateProductionPage />}
errorElement={<ErrorPage />}
/>
<Route
path="/manage-productions"
element={
<ManageProductionsPage setApiError={() => setApiError(true)} />
}
errorElement={<ErrorPage />}
/>
<Route
path="/auto-join"
element={<AutoJoinPage />}
errorElement={<ErrorPage />}
/>
<Route
path="/production-calls"
element={<CallsPage />}
errorElement={<ErrorPage />}
/>
<Route
path="/production-calls/production/:productionId/line/:lineId"
element={<CallsPage />}
errorElement={<ErrorPage />}
/>
</>
)}
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
);
};
Expand Down
11 changes: 7 additions & 4 deletions src/api/api.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { handleFetchRequest } from "./handle-fetch-request.ts";

const API_VERSION = import.meta.env.VITE_BACKEND_API_VERSION ?? "api/v1/";
const API_URL =
`${import.meta.env.VITE_BACKEND_URL.replace(/\/+$/, "")}/${API_VERSION}` ||
`${window.location.origin}/${API_VERSION}`;
const API_VERSION = (
import.meta.env.VITE_BACKEND_API_VERSION ?? "api/v1"
).replace(/\/+$/, "");
const BACKEND_URL = import.meta.env.VITE_BACKEND_URL;
const API_URL = BACKEND_URL
? `${BACKEND_URL.replace(/\/+$/, "")}/${API_VERSION}/`
: `${window.location.origin}/${API_VERSION}/`;
const API_KEY = import.meta.env.VITE_BACKEND_API_KEY;

type TCreateProductionOptions = {
Expand Down
104 changes: 104 additions & 0 deletions src/components/auto-join/auto-join-link-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { useEffect, useRef, useState } from "react";
import styled from "@emotion/styled";
import { CopyButton } from "../copy-button/copy-button";
import { DecorativeLabel, FormInput } from "../form-elements/form-elements";
import { Modal } from "../modal/modal";
import {
InputWrapper,
LinkLabel,
ModalHeader,
ModalText,
Wrapper,
} from "../generate-urls/generate-urls-components";

const CheckboxRow = styled.label`
display: flex;
align-items: center;
gap: 1rem;
font-size: 1.4rem;
color: rgba(255, 255, 255, 0.8);
cursor: pointer;
margin-top: 1.5rem;

input[type="checkbox"] {
width: 1.6rem;
height: 1.6rem;
cursor: pointer;
accent-color: #59cbe8;
}
`;

const COMPANION_SUFFIX = "&companion=ws://127.0.0.1:12345";

type TAutoJoinLinkModalProps = {
url: string;
onClose: () => void;
};

export const AutoJoinLinkModal = ({
url,
onClose,
}: TAutoJoinLinkModalProps) => {
const modalRef = useRef<HTMLDivElement>(null);
const [includeCompanion, setIncludeCompanion] = useState(false);
const [username, setUsername] = useState("");

const usernameParam = username
? `&username=${encodeURIComponent(username)}`
: "";
const displayUrl = `${url}${usernameParam}${includeCompanion ? COMPANION_SUFFIX : ""}`;

useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
modalRef.current &&
!modalRef.current.contains(event.target as Node)
) {
onClose();
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, [onClose]);

return (
<Modal onClose={onClose}>
<div ref={modalRef}>
<ModalHeader>Auto Join Link</ModalHeader>
<ModalText>
Share this link to let someone join the same set of calls
automatically.
</ModalText>
<Wrapper>
<InputWrapper>
<LinkLabel>
<DecorativeLabel>Username (optional)</DecorativeLabel>
<FormInput
type="text"
placeholder="Leave empty to use saved settings"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</LinkLabel>
</InputWrapper>
</Wrapper>
<CheckboxRow>
<input
type="checkbox"
checked={includeCompanion}
onChange={(e) => setIncludeCompanion(e.target.checked)}
/>
Include companion connection
</CheckboxRow>
<Wrapper>
<InputWrapper>
<LinkLabel>
<FormInput readOnly value={displayUrl} />
</LinkLabel>
<CopyButton urls={[displayUrl]} className="share-line-link-modal" />
</InputWrapper>
</Wrapper>
</div>
</Modal>
);
};
65 changes: 65 additions & 0 deletions src/components/auto-join/auto-join-page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { useEffect, useRef } from "react";
import { useSearchParams, useNavigate } from "react-router";
import { useGlobalState } from "../../global-state/context-provider";
import { useInitiateProductionCall } from "../../hooks/use-initiate-production-call";
import { AUTO_JOIN_STORAGE_KEY, TAutoJoinCall } from "../../utils/auto-join";

export const AutoJoinPage = () => {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const [{ userSettings, devices }, dispatch] = useGlobalState();
const { initiateProductionCall } = useInitiateProductionCall({ dispatch });
const hasInitiated = useRef(false);

useEffect(() => {
if (hasInitiated.current) return;

const callsParam = searchParams.get("calls") ?? "";
const usernameParam = searchParams.get("username");
const companionParam = searchParams.get("companion");
const username = usernameParam || userSettings?.username || "Auto";

const calls: TAutoJoinCall[] = callsParam
.split(",")
.map((pair) => pair.split(":"))
.filter(([p, l]) => p && l)
.map(([productionId, lineId]) => ({ productionId, lineId }));

if (calls.length === 0) {
navigate("/");
return;
}

hasInitiated.current = true;

localStorage.setItem(AUTO_JOIN_STORAGE_KEY, JSON.stringify(calls));

if (companionParam) {
localStorage.setItem("companion_auto_connect", companionParam);
}

const audiooutput = userSettings?.audiooutput;
const audioinput =
userSettings?.audioinput || devices?.input?.[0]?.deviceId;

Promise.all(
calls.map((call) =>
initiateProductionCall({
payload: {
joinProductionOptions: {
productionId: call.productionId,
lineId: call.lineId,
username,
audioinput,
lineUsedForProgramOutput: false,
isProgramUser: false,
},
audiooutput,
},
})
)
).then(() => navigate("/production-calls"));
}, [searchParams, userSettings, devices, navigate, initiateProductionCall]);

return null;
};
Loading
Loading