Merge branch 'main' into mathieu/calendar-integration-rebased

This commit is contained in:
Igor Monadical
2025-09-16 15:53:18 -04:00
committed by GitHub
9 changed files with 108 additions and 114 deletions

View File

@@ -33,17 +33,27 @@ export default function Player(props: PlayerProps) {
const topicsRef = useRef(props.topics);
const [firstRender, setFirstRender] = useState<boolean>(true);
const keyHandler = (e) => {
if (e.key == " ") {
const shouldIgnoreHotkeys = (target: EventTarget | null) => {
return (
target instanceof HTMLInputElement ||
target instanceof HTMLTextAreaElement
);
};
const keyHandler = (e: KeyboardEvent) => {
if (e.key === " ") {
if (e.repeat) return;
if (shouldIgnoreHotkeys(e.target)) return;
e.preventDefault();
wavesurfer?.playPause();
}
};
useEffect(() => {
document.addEventListener("keyup", keyHandler);
document.addEventListener("keydown", keyHandler);
return () => {
document.removeEventListener("keyup", keyHandler);
document.removeEventListener("keydown", keyHandler);
};
});
}, [wavesurfer]);
// Waveform setup
useEffect(() => {

View File

@@ -14,8 +14,7 @@ import {
Checkbox,
Combobox,
Spinner,
useFilter,
useListCollection,
createListCollection,
} from "@chakra-ui/react";
import { TbBrandZulip } from "react-icons/tb";
import {
@@ -48,8 +47,6 @@ export default function ShareZulip(props: ShareZulipProps & BoxProps) {
const { data: topics = [] } = useZulipTopics(selectedStreamId);
const postToZulipMutation = useTranscriptPostToZulip();
const { contains } = useFilter({ sensitivity: "base" });
const streamItems = useMemo(() => {
return streams.map((stream: Stream) => ({
label: stream.name,
@@ -64,17 +61,21 @@ export default function ShareZulip(props: ShareZulipProps & BoxProps) {
}));
}, [topics]);
const { collection: streamItemsCollection, filter: streamItemsFilter } =
useListCollection({
initialItems: streamItems,
filter: contains,
});
const streamCollection = useMemo(
() =>
createListCollection({
items: streamItems,
}),
[streamItems],
);
const { collection: topicItemsCollection, filter: topicItemsFilter } =
useListCollection({
initialItems: topicItems,
filter: contains,
});
const topicCollection = useMemo(
() =>
createListCollection({
items: topicItems,
}),
[topicItems],
);
// Update selected stream ID when stream changes
useEffect(() => {
@@ -156,15 +157,12 @@ export default function ShareZulip(props: ShareZulipProps & BoxProps) {
<Flex align="center" gap={2}>
<Text>#</Text>
<Combobox.Root
collection={streamItemsCollection}
collection={streamCollection}
value={stream ? [stream] : []}
onValueChange={(e) => {
setTopic(undefined);
setStream(e.value[0]);
}}
onInputValueChange={(e) =>
streamItemsFilter(e.inputValue)
}
openOnClick={true}
positioning={{
strategy: "fixed",
@@ -181,7 +179,7 @@ export default function ShareZulip(props: ShareZulipProps & BoxProps) {
<Combobox.Positioner>
<Combobox.Content>
<Combobox.Empty>No streams found</Combobox.Empty>
{streamItemsCollection.items.map((item) => (
{streamItems.map((item) => (
<Combobox.Item key={item.value} item={item}>
{item.label}
</Combobox.Item>
@@ -197,12 +195,9 @@ export default function ShareZulip(props: ShareZulipProps & BoxProps) {
<Flex align="center" gap={2}>
<Text visibility="hidden">#</Text>
<Combobox.Root
collection={topicItemsCollection}
collection={topicCollection}
value={topic ? [topic] : []}
onValueChange={(e) => setTopic(e.value[0])}
onInputValueChange={(e) =>
topicItemsFilter(e.inputValue)
}
openOnClick
selectionBehavior="replace"
skipAnimationOnMount={true}
@@ -222,7 +217,7 @@ export default function ShareZulip(props: ShareZulipProps & BoxProps) {
<Combobox.Positioner>
<Combobox.Content>
<Combobox.Empty>No topics found</Combobox.Empty>
{topicItemsCollection.items.map((item) => (
{topicItems.map((item) => (
<Combobox.Item key={item.value} item={item}>
{item.label}
<Combobox.ItemIndicator />

View File

@@ -3,8 +3,10 @@
import createClient from "openapi-fetch";
import type { paths } from "../reflector-api";
import createFetchClient from "openapi-react-query";
import { assertExistsAndNonEmptyString } from "./utils";
import { assertExistsAndNonEmptyString, parseNonEmptyString } from "./utils";
import { isBuildPhase } from "./next";
import { getSession } from "next-auth/react";
import { assertExtendedToken } from "./types";
export const API_URL = !isBuildPhase
? assertExistsAndNonEmptyString(
@@ -21,29 +23,29 @@ export const client = createClient<paths>({
baseUrl: API_URL,
});
const waitForAuthTokenDefinitivePresenceOrAbscence = async () => {
let tries = 0;
let time = 0;
const STEP = 100;
while (currentAuthToken === undefined) {
await new Promise((resolve) => setTimeout(resolve, STEP));
time += STEP;
tries++;
// most likely first try is more than enough, if it's more there's already something weird happens
if (tries > 10) {
// even when there's no auth assumed at all, we probably should explicitly call configureApiAuth(null)
throw new Error(
`Could not get auth token definitive presence/absence in ${time}ms. not calling configureApiAuth?`,
);
}
// will assert presence/absence of login initially
const initialSessionPromise = getSession();
const waitForAuthTokenDefinitivePresenceOrAbsence = async () => {
const initialSession = await initialSessionPromise;
if (currentAuthToken === undefined) {
currentAuthToken =
initialSession === null
? null
: assertExtendedToken(initialSession).accessToken;
}
// otherwise already overwritten by external forces
return currentAuthToken;
};
client.use({
async onRequest({ request }) {
await waitForAuthTokenDefinitivePresenceOrAbscence();
if (currentAuthToken) {
request.headers.set("Authorization", `Bearer ${currentAuthToken}`);
const token = await waitForAuthTokenDefinitivePresenceOrAbsence();
if (token !== null) {
request.headers.set(
"Authorization",
`Bearer ${parseNonEmptyString(token)}`,
);
}
// XXX Only set Content-Type if not already set (FormData will set its own boundary)
// This is a work around for uploading file, we're passing a formdata

View File

@@ -21,7 +21,7 @@ export interface CustomSession extends Session {
// assumption that JWT is JWTWithAccessToken - we set it in jwt callback of auth; typing isn't strong around there
// but the assumption is crucial to auth working
export const assertExtendedToken = <T>(
t: T,
t: Exclude<T, null | undefined>,
): T & {
accessTokenExpires: number;
accessToken: string;
@@ -45,7 +45,7 @@ export const assertExtendedToken = <T>(
};
export const assertExtendedTokenAndUserId = <U, T extends { user?: U }>(
t: T,
t: Exclude<T, null | undefined>,
): T & {
accessTokenExpires: number;
accessToken: string;
@@ -55,7 +55,7 @@ export const assertExtendedTokenAndUserId = <U, T extends { user?: U }>(
} => {
const extendedToken = assertExtendedToken(t);
if (typeof (extendedToken.user as any)?.id === "string") {
return t as T & {
return t as Exclude<T, null | undefined> & {
accessTokenExpires: number;
accessToken: string;
user: U & {
@@ -67,7 +67,9 @@ export const assertExtendedTokenAndUserId = <U, T extends { user?: U }>(
};
// best attempt to check the session is valid
export const assertCustomSession = <S extends Session>(s: S): CustomSession => {
export const assertCustomSession = <T extends Session>(
s: Exclude<T, null | undefined>,
): CustomSession => {
const r = assertExtendedTokenAndUserId(s);
// no other checks for now
return r as CustomSession;