Merge pull request #309 from Monadical-SAS/feat-sharing

Introduce share mode
This commit is contained in:
2023-11-24 11:52:26 +01:00
committed by GitHub
26 changed files with 467 additions and 228 deletions

View File

@@ -0,0 +1,33 @@
"""add share_mode
Revision ID: 0fea6d96b096
Revises: f819277e5169
Create Date: 2023-11-07 11:12:21.614198
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "0fea6d96b096"
down_revision: Union[str, None] = "f819277e5169"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
"transcript",
sa.Column("share_mode", sa.String(), server_default="private", nullable=False),
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("transcript", "share_mode")
# ### end Alembic commands ###

View File

@@ -2,10 +2,11 @@ import json
from contextlib import asynccontextmanager
from datetime import datetime
from pathlib import Path
from typing import Any
from typing import Any, Literal
from uuid import uuid4
import sqlalchemy
from fastapi import HTTPException
from pydantic import BaseModel, Field
from reflector.db import database, metadata
from reflector.processors.types import Word as ProcessorWord
@@ -36,6 +37,12 @@ transcripts = sqlalchemy.Table(
),
# with user attached, optional
sqlalchemy.Column("user_id", sqlalchemy.String),
sqlalchemy.Column(
"share_mode",
sqlalchemy.String,
nullable=False,
server_default="private",
),
)
@@ -120,6 +127,7 @@ class Transcript(BaseModel):
events: list[TranscriptEvent] = []
source_language: str = "en"
target_language: str = "en"
share_mode: Literal["private", "semi-private", "public"] = "private"
audio_location: str = "local"
def add_event(self, event: str, data: BaseModel) -> TranscriptEvent:
@@ -217,6 +225,7 @@ class TranscriptController:
order_by: str | None = None,
filter_empty: bool | None = False,
filter_recording: bool | None = False,
return_query: bool = False,
) -> list[Transcript]:
"""
Get all transcripts
@@ -243,6 +252,9 @@ class TranscriptController:
if filter_recording:
query = query.filter(transcripts.c.status != "recording")
if return_query:
return query
results = await database.fetch_all(query)
return results
@@ -258,6 +270,47 @@ class TranscriptController:
return None
return Transcript(**result)
async def get_by_id_for_http(
self,
transcript_id: str,
user_id: str | None,
) -> Transcript:
"""
Get a transcript by ID for HTTP request.
If not found, it will raise a 404 error.
If the user is not allowed to access the transcript, it will raise a 403 error.
This method checks the share mode of the transcript and the user_id
to determine if the user can access the transcript.
"""
query = transcripts.select().where(transcripts.c.id == transcript_id)
result = await database.fetch_one(query)
if not result:
raise HTTPException(status_code=404, detail="Transcript not found")
# if the transcript is anonymous, share mode is not checked
transcript = Transcript(**result)
if transcript.user_id is None:
return transcript
if transcript.share_mode == "private":
# in private mode, only the owner can access the transcript
if transcript.user_id == user_id:
return transcript
elif transcript.share_mode == "semi-private":
# in semi-private mode, only the owner and the users with the link
# can access the transcript
if user_id is not None:
return transcript
elif transcript.share_mode == "public":
# in public mode, everyone can access the transcript
return transcript
raise HTTPException(status_code=403, detail="Transcript access denied")
async def add(
self,
name: str,

View File

@@ -1,5 +1,5 @@
from datetime import datetime, timedelta
from typing import Annotated, Optional
from typing import Annotated, Literal, Optional
import httpx
import reflector.auth as auth
@@ -13,7 +13,8 @@ from fastapi import (
WebSocketDisconnect,
status,
)
from fastapi_pagination import Page, paginate
from fastapi_pagination import Page
from fastapi_pagination.ext.databases import paginate
from jose import jwt
from pydantic import BaseModel, Field
from reflector.db.transcripts import (
@@ -49,6 +50,7 @@ def create_access_token(data: dict, expires_delta: timedelta):
class GetTranscript(BaseModel):
id: str
user_id: str | None
name: str
status: str
locked: bool
@@ -57,6 +59,7 @@ class GetTranscript(BaseModel):
short_summary: str | None
long_summary: str | None
created_at: datetime
share_mode: str = Field("private")
source_language: str | None
target_language: str | None
@@ -73,6 +76,7 @@ class UpdateTranscript(BaseModel):
title: Optional[str] = Field(None)
short_summary: Optional[str] = Field(None)
long_summary: Optional[str] = Field(None)
share_mode: Optional[Literal["public", "semi-private", "private"]] = Field(None)
class DeletionStatus(BaseModel):
@@ -83,12 +87,19 @@ class DeletionStatus(BaseModel):
async def transcripts_list(
user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user_optional)],
):
from reflector.db import database
if not user and not settings.PUBLIC_MODE:
raise HTTPException(status_code=401, detail="Not authenticated")
user_id = user["sub"] if user else None
return paginate(
await transcripts_controller.get_all(user_id=user_id, order_by="-created_at")
return await paginate(
database,
await transcripts_controller.get_all(
user_id=user_id,
order_by="-created_at",
return_query=True,
),
)
@@ -166,10 +177,9 @@ async def transcript_get(
user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user_optional)],
):
user_id = user["sub"] if user else None
transcript = await transcripts_controller.get_by_id(transcript_id, user_id=user_id)
if not transcript:
raise HTTPException(status_code=404, detail="Transcript not found")
return transcript
return await transcripts_controller.get_by_id_for_http(
transcript_id, user_id=user_id
)
@router.patch("/transcripts/{transcript_id}", response_model=GetTranscript)
@@ -193,6 +203,8 @@ async def transcript_update(
values["short_summary"] = info.short_summary
if info.title is not None:
values["title"] = info.title
if info.share_mode is not None:
values["share_mode"] = info.share_mode
await transcripts_controller.update(transcript, values)
return transcript
@@ -231,9 +243,27 @@ async def transcript_get_audio_mp3(
except jwt.JWTError:
raise unauthorized_exception
transcript = await transcripts_controller.get_by_id(transcript_id, user_id=user_id)
if not transcript:
raise HTTPException(status_code=404, detail="Transcript not found")
transcript = await transcripts_controller.get_by_id_for_http(
transcript_id, user_id=user_id
)
if transcript.audio_location == "storage":
# proxy S3 file, to prevent issue with CORS
url = await transcript.get_audio_url()
headers = {}
copy_headers = ["range", "accept-encoding"]
for header in copy_headers:
if header in request.headers:
headers[header] = request.headers[header]
async with httpx.AsyncClient() as client:
resp = await client.request(request.method, url, headers=headers)
return Response(
content=resp.content,
status_code=resp.status_code,
headers=resp.headers,
)
if transcript.audio_location == "storage":
# proxy S3 file, to prevent issue with CORS
@@ -254,7 +284,7 @@ async def transcript_get_audio_mp3(
)
if not transcript.audio_mp3_filename.exists():
raise HTTPException(status_code=404, detail="Audio not found")
raise HTTPException(status_code=500, detail="Audio not found")
truncated_id = str(transcript.id).split("-")[0]
filename = f"recording_{truncated_id}.mp3"
@@ -273,9 +303,9 @@ async def transcript_get_audio_waveform(
user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user_optional)],
) -> AudioWaveform:
user_id = user["sub"] if user else None
transcript = await transcripts_controller.get_by_id(transcript_id, user_id=user_id)
if not transcript:
raise HTTPException(status_code=404, detail="Transcript not found")
transcript = await transcripts_controller.get_by_id_for_http(
transcript_id, user_id=user_id
)
if not transcript.audio_waveform_filename.exists():
raise HTTPException(status_code=404, detail="Audio not found")
@@ -292,9 +322,9 @@ async def transcript_get_topics(
user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user_optional)],
):
user_id = user["sub"] if user else None
transcript = await transcripts_controller.get_by_id(transcript_id, user_id=user_id)
if not transcript:
raise HTTPException(status_code=404, detail="Transcript not found")
transcript = await transcripts_controller.get_by_id_for_http(
transcript_id, user_id=user_id
)
# convert to GetTranscriptTopic
return [
@@ -363,9 +393,9 @@ async def transcript_record_webrtc(
user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user_optional)],
):
user_id = user["sub"] if user else None
transcript = await transcripts_controller.get_by_id(transcript_id, user_id=user_id)
if not transcript:
raise HTTPException(status_code=404, detail="Transcript not found")
transcript = await transcripts_controller.get_by_id_for_http(
transcript_id, user_id=user_id
)
if transcript.locked:
raise HTTPException(status_code=400, detail="Transcript is locked")

View File

@@ -1,11 +1,18 @@
"use client";
import { FiefAuthProvider } from "@fief/fief/nextjs/react";
import { createContext } from "react";
export default function FiefWrapper({ children }) {
export const CookieContext = createContext<{ hasAuthCookie: boolean }>({
hasAuthCookie: false,
});
export default function FiefWrapper({ children, hasAuthCookie }) {
return (
<CookieContext.Provider value={{ hasAuthCookie }}>
<FiefAuthProvider currentUserPath="/api/current-user">
{children}
</FiefAuthProvider>
</CookieContext.Provider>
);
}

View File

@@ -11,6 +11,9 @@ import About from "../(aboutAndPrivacy)/about";
import Privacy from "../(aboutAndPrivacy)/privacy";
import { DomainContextProvider } from "./domainContext";
import { getConfig } from "../lib/edgeConfig";
import { ErrorBoundary } from "@sentry/nextjs";
import { cookies } from "next/dist/client/components/headers";
import { SESSION_COOKIE_NAME } from "../lib/fief";
const poppins = Poppins({ subsets: ["latin"], weight: ["200", "400", "600"] });
@@ -70,12 +73,14 @@ type LayoutProps = {
export default async function RootLayout({ children, params }: LayoutProps) {
const config = await getConfig(params.domain);
const { requireLogin, privacy, browse } = config.features;
const hasAuthCookie = !!cookies().get(SESSION_COOKIE_NAME);
return (
<html lang="en">
<body className={poppins.className + " h-screen relative"}>
<FiefWrapper>
<FiefWrapper hasAuthCookie={hasAuthCookie}>
<DomainContextProvider config={config}>
<ErrorBoundary fallback={<p>"something went really wrong"</p>}>
<ErrorProvider>
<ErrorMessage />
<div
@@ -150,6 +155,7 @@ export default async function RootLayout({ children, params }: LayoutProps) {
{children}
</div>
</ErrorProvider>
</ErrorBoundary>
</DomainContextProvider>
</FiefWrapper>
</body>

View File

@@ -14,6 +14,9 @@ import QRCode from "react-qr-code";
import TranscriptTitle from "../transcriptTitle";
import Player from "../player";
import WaveformLoading from "../waveformLoading";
import { useRouter } from "next/navigation";
import { faSpinner } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
type TranscriptDetails = {
params: {
@@ -21,26 +24,16 @@ type TranscriptDetails = {
};
};
const protectedPath = true;
export default function TranscriptDetails(details: TranscriptDetails) {
const transcriptId = details.params.transcriptId;
const router = useRouter();
const transcript = useTranscript(protectedPath, transcriptId);
const topics = useTopics(protectedPath, transcriptId);
const waveform = useWaveform(protectedPath, transcriptId);
const transcript = useTranscript(transcriptId);
const topics = useTopics(transcriptId);
const waveform = useWaveform(transcriptId);
const useActiveTopic = useState<Topic | null>(null);
const mp3 = useMp3(transcriptId);
if (transcript?.error || topics?.error) {
return (
<Modal
title="Transcription Not Found"
text="A trascription with this ID does not exist."
/>
);
}
useEffect(() => {
const statusToRedirect = ["idle", "recording", "processing"];
if (statusToRedirect.includes(transcript.response?.status)) {
@@ -48,8 +41,8 @@ export default function TranscriptDetails(details: TranscriptDetails) {
// Shallow redirection does not work on NextJS 13
// https://github.com/vercel/next.js/discussions/48110
// https://github.com/vercel/next.js/discussions/49540
// router.push(newUrl, undefined, { shallow: true });
history.replaceState({}, "", newUrl);
router.push(newUrl, undefined);
// history.replaceState({}, "", newUrl);
}
}, [transcript.response?.status]);
@@ -60,16 +53,24 @@ export default function TranscriptDetails(details: TranscriptDetails) {
.replace(/ +/g, " ")
.trim() || "";
if (transcript.error || topics?.error) {
return (
<Modal
title="Transcription Not Found"
text="A trascription with this ID does not exist."
/>
);
}
if (transcript?.loading || topics?.loading) {
return <Modal title="Loading" text={"Loading transcript..."} />;
}
return (
<>
{transcript?.loading || topics?.loading ? (
<Modal title="Loading" text={"Loading transcript..."} />
) : (
<>
<div className="flex flex-col">
{transcript?.response?.title && (
<TranscriptTitle
protectedPath={protectedPath}
title={transcript.response.title}
transcriptId={transcript.response.id}
/>
@@ -99,7 +100,6 @@ export default function TranscriptDetails(details: TranscriptDetails) {
<section className=" bg-blue-400/20 rounded-lg md:rounded-xl p-2 md:px-4 h-full">
{transcript.response.longSummary ? (
<FinalSummary
protectedPath={protectedPath}
fullTranscript={fullTranscript}
summary={transcript.response.longSummary}
transcriptId={transcript.response.id}
@@ -110,8 +110,8 @@ export default function TranscriptDetails(details: TranscriptDetails) {
<p>Loading Transcript</p>
) : (
<p>
There was an error generating the final summary, please
come back later
There was an error generating the final summary, please come
back later
</p>
)}
</div>
@@ -127,13 +127,15 @@ export default function TranscriptDetails(details: TranscriptDetails) {
/>
</div>
<div className="flex-grow max-w-full">
<ShareLink />
<ShareLink
transcriptId={transcript?.response?.id}
userId={transcript?.response?.userId}
shareMode={transcript?.response?.shareMode}
/>
</div>
</section>
</div>
</div>
</>
)}
</>
);
}

View File

@@ -15,7 +15,7 @@ import { faGear } from "@fortawesome/free-solid-svg-icons";
import { lockWakeState, releaseWakeState } from "../../../../lib/wakeLock";
import { useRouter } from "next/navigation";
import Player from "../../player";
import useMp3, { Mp3Response } from "../../useMp3";
import useMp3 from "../../useMp3";
import WaveformLoading from "../../waveformLoading";
type TranscriptDetails = {
@@ -39,8 +39,8 @@ const TranscriptRecord = (details: TranscriptDetails) => {
}
}, []);
const transcript = useTranscript(true, details.params.transcriptId);
const webRTC = useWebRTC(stream, details.params.transcriptId, true);
const transcript = useTranscript(details.params.transcriptId);
const webRTC = useWebRTC(stream, details.params.transcriptId);
const webSockets = useWebSockets(details.params.transcriptId);
const { audioDevices, getAudioStream } = useAudioDevice();

View File

@@ -19,7 +19,7 @@ const useCreateTranscript = (): CreateTranscript => {
const [loading, setLoading] = useState<boolean>(false);
const [error, setErrorState] = useState<Error | null>(null);
const { setError } = useError();
const api = getApi(true);
const api = getApi();
const create = (params: V1TranscriptsCreateRequest["createTranscript"]) => {
if (loading || !api) return;

View File

@@ -5,7 +5,6 @@ import "../../styles/markdown.css";
import getApi from "../../lib/getApi";
type FinalSummaryProps = {
protectedPath: boolean;
summary: string;
fullTranscript: string;
transcriptId: string;
@@ -18,7 +17,7 @@ export default function FinalSummary(props: FinalSummaryProps) {
const [isEditMode, setIsEditMode] = useState(false);
const [preEditSummary, setPreEditSummary] = useState(props.summary);
const [editedSummary, setEditedSummary] = useState(props.summary);
const api = getApi(props.protectedPath);
const api = getApi();
const updateSummary = async (newSummary: string, transcriptId: string) => {
if (!api) return;

View File

@@ -1,15 +1,39 @@
import React, { useState, useRef, useEffect, use } from "react";
import { featureEnabled } from "../domainContext";
import getApi from "../../lib/getApi";
import { useFiefUserinfo } from "@fief/fief/nextjs/react";
import SelectSearch from "react-select-search";
import "react-select-search/style.css";
import "../../styles/button.css";
import "../../styles/form.scss";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSpinner } from "@fortawesome/free-solid-svg-icons";
const ShareLink = () => {
type ShareLinkProps = {
transcriptId: string;
userId: string | null;
shareMode: string;
};
const ShareLink = (props: ShareLinkProps) => {
const [isCopied, setIsCopied] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const [currentUrl, setCurrentUrl] = useState<string>("");
const requireLogin = featureEnabled("requireLogin");
const [isOwner, setIsOwner] = useState(false);
const [shareMode, setShareMode] = useState(props.shareMode);
const [shareLoading, setShareLoading] = useState(false);
const userinfo = useFiefUserinfo();
const api = getApi();
useEffect(() => {
setCurrentUrl(window.location.href);
}, []);
useEffect(() => {
setIsOwner(!!(requireLogin && userinfo?.sub === props.userId));
}, [userinfo, props.userId]);
const handleCopyClick = () => {
if (inputRef.current) {
let text_to_copy = inputRef.current.value;
@@ -23,6 +47,18 @@ const ShareLink = () => {
}
};
const updateShareMode = async (selectedShareMode: string) => {
if (!api) return;
setShareLoading(true);
const updatedTranscript = await api.v1TranscriptUpdate({
transcriptId: props.transcriptId,
updateTranscript: {
shareMode: selectedShareMode,
},
});
setShareMode(updatedTranscript.shareMode);
setShareLoading(false);
};
const privacyEnabled = featureEnabled("privacy");
return (
@@ -30,18 +66,61 @@ const ShareLink = () => {
className="p-2 md:p-4 rounded"
style={{ background: "rgba(96, 165, 250, 0.2)" }}
>
{requireLogin && (
<div className="text-sm mb-2">
{shareMode === "private" && (
<p>This transcript is private and can only be accessed by you.</p>
)}
{shareMode === "semi-private" && (
<p>
This transcript is secure. Only authenticated users can access it.
</p>
)}
{shareMode === "public" && (
<p>This transcript is public. Everyone can access it.</p>
)}
{isOwner && api && (
<div className="relative">
<SelectSearch
className="select-search--top select-search"
options={[
{ name: "Private", value: "private" },
{ name: "Secure", value: "semi-private" },
{ name: "Public", value: "public" },
]}
value={shareMode}
onChange={updateShareMode}
closeOnSelect={true}
/>
{shareLoading && (
<div className="h-4 w-4 absolute top-1/3 right-3 z-10">
<FontAwesomeIcon
icon={faSpinner}
className="animate-spin-slow text-gray-600 flex-grow rounded-lg md:rounded-xl h-4 w-4"
/>
</div>
)}
</div>
)}
</div>
)}
{!requireLogin && (
<>
{privacyEnabled ? (
<p className="text-sm mb-2">
You can share this link with others. Anyone with the link will have
access to the page, including the full audio recording, for the next 7
Share this link to grant others access to this page. The link
includes the full audio recording and is valid for the next 7
days.
</p>
) : (
<p className="text-sm mb-2">
You can share this link with others. Anyone with the link will have
access to the page, including the full audio recording.
Share this link to allow others to view this page and listen to
the full audio recording.
</p>
)}
</>
)}
<div className="flex items-center">
<input
type="text"

View File

@@ -2,7 +2,6 @@ import { useState } from "react";
import getApi from "../../lib/getApi";
type TranscriptTitle = {
protectedPath: boolean;
title: string;
transcriptId: string;
};
@@ -11,7 +10,7 @@ const TranscriptTitle = (props: TranscriptTitle) => {
const [displayedTitle, setDisplayedTitle] = useState(props.title);
const [preEditTitle, setPreEditTitle] = useState(props.title);
const [isEditing, setIsEditing] = useState(false);
const api = getApi(props.protectedPath);
const api = getApi();
const updateTitle = async (newTitle: string, transcriptId: string) => {
if (!api) return;

View File

@@ -13,29 +13,33 @@ const useMp3 = (id: string, waiting?: boolean): Mp3Response => {
const [media, setMedia] = useState<HTMLMediaElement | null>(null);
const [later, setLater] = useState(waiting);
const [loading, setLoading] = useState<boolean>(false);
const api = getApi(true);
const api = getApi();
const { api_url } = useContext(DomainContext);
const accessTokenInfo = useFiefAccessTokenInfo();
const [serviceWorkerReady, setServiceWorkerReady] = useState(false);
const [serviceWorker, setServiceWorker] =
useState<ServiceWorkerRegistration | null>(null);
useEffect(() => {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/service-worker.js").then(() => {
setServiceWorkerReady(true);
navigator.serviceWorker.register("/service-worker.js").then((worker) => {
setServiceWorker(worker);
});
}
return () => {
serviceWorker?.unregister();
};
}, []);
useEffect(() => {
if (!navigator.serviceWorker) return;
if (!navigator.serviceWorker.controller) return;
if (!serviceWorkerReady) return;
if (!serviceWorker) return;
// Send the token to the service worker
navigator.serviceWorker.controller.postMessage({
type: "SET_AUTH_TOKEN",
token: accessTokenInfo?.access_token,
});
}, [navigator.serviceWorker, serviceWorkerReady, accessTokenInfo]);
}, [navigator.serviceWorker, !serviceWorker, accessTokenInfo]);
useEffect(() => {
if (!id || !api || later) return;

View File

@@ -14,12 +14,12 @@ type TranscriptTopics = {
error: Error | null;
};
const useTopics = (protectedPath, id: string): TranscriptTopics => {
const useTopics = (id: string): TranscriptTopics => {
const [topics, setTopics] = useState<Topic[] | null>(null);
const [loading, setLoading] = useState<boolean>(false);
const [error, setErrorState] = useState<Error | null>(null);
const { setError } = useError();
const api = getApi(protectedPath);
const api = getApi();
useEffect(() => {
if (!id || !api) return;

View File

@@ -24,14 +24,13 @@ type SuccessTranscript = {
};
const useTranscript = (
protectedPath: boolean,
id: string | null,
): ErrorTranscript | LoadingTranscript | SuccessTranscript => {
const [response, setResponse] = useState<GetTranscript | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setErrorState] = useState<Error | null>(null);
const { setError } = useError();
const api = getApi(protectedPath);
const api = getApi();
useEffect(() => {
if (!id || !api) return;

View File

@@ -15,7 +15,7 @@ const useTranscriptList = (page: number): TranscriptList => {
const [loading, setLoading] = useState<boolean>(true);
const [error, setErrorState] = useState<Error | null>(null);
const { setError } = useError();
const api = getApi(true);
const api = getApi();
useEffect(() => {
if (!api) return;

View File

@@ -1,8 +1,5 @@
import { useEffect, useState } from "react";
import {
DefaultApi,
V1TranscriptGetAudioWaveformRequest,
} from "../../api/apis/DefaultApi";
import { V1TranscriptGetAudioWaveformRequest } from "../../api/apis/DefaultApi";
import { AudioWaveform } from "../../api";
import { useError } from "../../(errors)/errorContext";
import getApi from "../../lib/getApi";
@@ -14,12 +11,12 @@ type AudioWaveFormResponse = {
error: Error | null;
};
const useWaveform = (protectedPath, id: string): AudioWaveFormResponse => {
const useWaveform = (id: string): AudioWaveFormResponse => {
const [waveform, setWaveform] = useState<AudioWaveform | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setErrorState] = useState<Error | null>(null);
const { setError } = useError();
const api = getApi(protectedPath);
const api = getApi();
useEffect(() => {
if (!id || !api) return;

View File

@@ -10,11 +10,10 @@ import getApi from "../../lib/getApi";
const useWebRTC = (
stream: MediaStream | null,
transcriptId: string | null,
protectedPath,
): Peer => {
const [peer, setPeer] = useState<Peer | null>(null);
const { setError } = useError();
const api = getApi(protectedPath);
const api = getApi();
useEffect(() => {
if (!stream || !transcriptId) {

View File

@@ -402,6 +402,7 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => {
console.debug("WebSocket connection closed");
switch (event.code) {
case 1000: // Normal Closure:
case 1005: // Closure by client FF
default:
setError(
new Error(`WebSocket closed unexpectedly with code: ${event.code}`),

View File

@@ -5,7 +5,7 @@ export default () => (
<div className="flex flex-grow items-center justify-center h-20">
<FontAwesomeIcon
icon={faSpinner}
className="animate-spin-slow text-gray-600 flex-grow rounded-lg md:rounded-xl h-10"
className="animate-spin-slow text-gray-600 flex-grow rounded-lg md:rounded-xl h-10 w-10"
/>
</div>
);

View File

@@ -25,6 +25,12 @@ export interface GetTranscript {
* @memberof GetTranscript
*/
id: any | null;
/**
*
* @type {any}
* @memberof GetTranscript
*/
userId: any | null;
/**
*
* @type {any}
@@ -73,6 +79,12 @@ export interface GetTranscript {
* @memberof GetTranscript
*/
createdAt: any | null;
/**
*
* @type {any}
* @memberof GetTranscript
*/
shareMode?: any | null;
/**
*
* @type {any}
@@ -93,6 +105,7 @@ export interface GetTranscript {
export function instanceOfGetTranscript(value: object): boolean {
let isInstance = true;
isInstance = isInstance && "id" in value;
isInstance = isInstance && "userId" in value;
isInstance = isInstance && "name" in value;
isInstance = isInstance && "status" in value;
isInstance = isInstance && "locked" in value;
@@ -120,6 +133,7 @@ export function GetTranscriptFromJSONTyped(
}
return {
id: json["id"],
userId: json["user_id"],
name: json["name"],
status: json["status"],
locked: json["locked"],
@@ -128,6 +142,7 @@ export function GetTranscriptFromJSONTyped(
shortSummary: json["short_summary"],
longSummary: json["long_summary"],
createdAt: json["created_at"],
shareMode: !exists(json, "share_mode") ? undefined : json["share_mode"],
sourceLanguage: json["source_language"],
targetLanguage: json["target_language"],
};
@@ -142,6 +157,7 @@ export function GetTranscriptToJSON(value?: GetTranscript | null): any {
}
return {
id: value.id,
user_id: value.userId,
name: value.name,
status: value.status,
locked: value.locked,
@@ -150,6 +166,7 @@ export function GetTranscriptToJSON(value?: GetTranscript | null): any {
short_summary: value.shortSummary,
long_summary: value.longSummary,
created_at: value.createdAt,
share_mode: value.shareMode,
source_language: value.sourceLanguage,
target_language: value.targetLanguage,
};

View File

@@ -49,6 +49,12 @@ export interface UpdateTranscript {
* @memberof UpdateTranscript
*/
longSummary?: any | null;
/**
*
* @type {any}
* @memberof UpdateTranscript
*/
shareMode?: any | null;
}
/**
@@ -81,6 +87,7 @@ export function UpdateTranscriptFromJSONTyped(
longSummary: !exists(json, "long_summary")
? undefined
: json["long_summary"],
shareMode: !exists(json, "share_mode") ? undefined : json["share_mode"],
};
}
@@ -97,5 +104,6 @@ export function UpdateTranscriptToJSON(value?: UpdateTranscript | null): any {
title: value.title,
short_summary: value.shortSummary,
long_summary: value.longSummary,
share_mode: value.shareMode,
};
}

View File

@@ -3,9 +3,9 @@ import { isDevelopment } from "./utils";
const localConfig = {
features: {
requireLogin: false,
requireLogin: true,
privacy: true,
browse: false,
browse: true,
},
api_url: "http://127.0.0.1:1250",
websocket_url: "ws://127.0.0.1:1250",

View File

@@ -1,5 +1,8 @@
function shouldShowError(error: Error | null | undefined) {
if (error?.name == "ResponseError" && error["response"].status == 404)
if (
error?.name == "ResponseError" &&
(error["response"].status == 404 || error["response"].status == 403)
)
return false;
if (error?.name == "FetchError") return false;
return true;

View File

@@ -66,10 +66,6 @@ export const getFiefAuthMiddleware = async (url) => {
matcher: "/transcripts",
parameters: {},
},
{
matcher: "/transcripts/((?!new).*)",
parameters: {},
},
{
matcher: "/browse",
parameters: {},

View File

@@ -4,17 +4,19 @@ import { DefaultApi } from "../api/apis/DefaultApi";
import { useFiefAccessTokenInfo } from "@fief/fief/nextjs/react";
import { useContext, useEffect, useState } from "react";
import { DomainContext, featureEnabled } from "../[domain]/domainContext";
import { CookieContext } from "../(auth)/fiefWrapper";
export default function getApi(protectedPath: boolean): DefaultApi | undefined {
export default function getApi(): DefaultApi | undefined {
const accessTokenInfo = useFiefAccessTokenInfo();
const api_url = useContext(DomainContext).api_url;
const requireLogin = featureEnabled("requireLogin");
const [api, setApi] = useState<DefaultApi>();
const { hasAuthCookie } = useContext(CookieContext);
if (!api_url) throw new Error("no API URL");
useEffect(() => {
if (protectedPath && requireLogin && !accessTokenInfo) {
if (hasAuthCookie && requireLogin && !accessTokenInfo) {
return;
}
@@ -25,7 +27,7 @@ export default function getApi(protectedPath: boolean): DefaultApi | undefined {
: undefined,
});
setApi(new DefaultApi(apiConfiguration));
}, [!accessTokenInfo, protectedPath]);
}, [!accessTokenInfo, hasAuthCookie]);
return api;
}

View File

@@ -35,3 +35,8 @@ body.is-light-mode .input-container {
max-width: 100%;
width: auto;
}
body .select-search-container .select-search--top.select-search-select {
top: auto;
bottom: 46px;
}