infra: add linting and formatting for js projects (#230)

* wip

* wip

* wip

* format

* wip(mobile): lint and format

* cleanup

* nits

* nits

* idiomatic react

* nit

* format all root files
This commit was merged in pull request #230.
This commit is contained in:
Arjun Patel
2026-06-02 07:44:24 -07:00
committed by GitHub
parent 2fe562ce2b
commit a8a0b7db1b
258 changed files with 7822 additions and 5195 deletions
+14 -16
View File
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useState } from "react";
import { usePusherClient } from "@/lib/pusher-provider";
import type { ChannelMessage } from "@/lib/pusher-client";
import { useCallback, useEffect, useState } from 'react';
import { usePusherClient } from '@/lib/pusher-provider';
import type { ChannelMessage } from '@/lib/pusher-client';
interface UseChannelResult {
/** Current set of humanIds present in the channel */
@@ -23,11 +23,7 @@ export function useChannel(channelId: string | null): UseChannelResult {
const [messages, setMessages] = useState<ChannelMessage[]>([]);
useEffect(() => {
if (!client || !channelId) {
setPresence([]);
setMessages([]);
return;
}
if (!client || !channelId) return;
client.subscribe(channelId);
@@ -58,17 +54,19 @@ export function useChannel(channelId: string | null): UseChannelResult {
}
};
client.on(channelId, "subscribed", onSubscribed);
client.on(channelId, "join", onJoin);
client.on(channelId, "leave", onLeave);
client.on(channelId, "message", onMessage);
client.on(channelId, 'subscribed', onSubscribed);
client.on(channelId, 'join', onJoin);
client.on(channelId, 'leave', onLeave);
client.on(channelId, 'message', onMessage);
return () => {
client.off(channelId, "subscribed", onSubscribed);
client.off(channelId, "join", onJoin);
client.off(channelId, "leave", onLeave);
client.off(channelId, "message", onMessage);
client.off(channelId, 'subscribed', onSubscribed);
client.off(channelId, 'join', onJoin);
client.off(channelId, 'leave', onLeave);
client.off(channelId, 'message', onMessage);
client.unsubscribe(channelId);
setPresence([]);
setMessages([]);
};
}, [client, channelId]);
+1 -1
View File
@@ -1,4 +1,4 @@
import { useCallback, useLayoutEffect, useRef } from "react";
import { useCallback, useLayoutEffect, useRef } from 'react';
// Polyfill for React's `useEffectEvent` (canary). The returned function has a
// stable identity but always sees the latest closure — exactly what
+4 -4
View File
@@ -1,10 +1,10 @@
import { useQuery } from "@tanstack/react-query";
import { apiClient } from "@/api/client";
import { useAuthStore } from "@/stores/auth-store";
import { useQuery } from '@tanstack/react-query';
import { apiClient } from '@/api/client';
import { useAuthStore } from '@/stores/auth-store';
export function useNetworks() {
return useQuery({
queryKey: ["networks"],
queryKey: ['networks'],
queryFn: () => apiClient.listNetworks(),
meta: { toastOnError: true },
});
+37 -33
View File
@@ -1,20 +1,20 @@
import { useState, useEffect } from "react";
import { useQuery } from "@tanstack/react-query";
import type { QueryFieldFilterConstraint } from "firebase/firestore";
import { useState, useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import type { QueryFieldFilterConstraint } from 'firebase/firestore';
import {
subscribeToParticle,
subscribeToParticleChildren,
subscribeToLatestChild,
getParticle,
getParticleChildren,
} from "@/lib/firestore-particles";
import type { Particle } from "@/api/types";
} from '@/lib/firestore-particles';
import type { Particle } from '@/api/types';
import {
type ParticlePath,
toFirestoreDocPath,
toFirestoreChildrenPath,
} from "@/lib/particle-path";
import { logError } from "@/lib/errors";
} from '@/lib/particle-path';
import { logError } from '@/lib/errors';
interface UseLiveParticleResult {
particle: Particle | null;
@@ -28,10 +28,6 @@ export function useLiveParticle(path: ParticlePath): UseLiveParticleResult {
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
setIsLoading(true);
setError(null);
setParticle(null);
const docPath = toFirestoreDocPath(path);
const unsubscribe = subscribeToParticle(
docPath,
@@ -45,7 +41,12 @@ export function useLiveParticle(path: ParticlePath): UseLiveParticleResult {
},
);
return unsubscribe;
return () => {
unsubscribe();
setIsLoading(true);
setError(null);
setParticle(null);
};
}, [path]);
return { particle, isLoading, error };
@@ -59,7 +60,7 @@ interface UseLiveParticleChildrenResult {
interface UseLiveParticleChildrenParams {
orderByField?: string;
orderDirection?: "asc" | "desc";
orderDirection?: 'asc' | 'desc';
visibilityScopes?: string[];
onAdded?: (child: Particle) => void;
onRemoved?: (child: Particle, updatedChildren: Particle[]) => void;
@@ -71,8 +72,8 @@ interface UseLiveParticleChildrenParams {
export function useLiveParticleChildren(
path: ParticlePath | undefined,
{
orderByField = "created_at",
orderDirection = "desc",
orderByField = 'created_at',
orderDirection = 'desc',
visibilityScopes,
onAdded,
onRemoved,
@@ -85,15 +86,7 @@ export function useLiveParticleChildren(
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
if (!path) {
setChildren([]);
setIsLoading(false);
return;
}
setIsLoading(true);
setError(null);
setChildren([]);
if (!path) return;
const collectionPath = toFirestoreChildrenPath(path);
@@ -103,7 +96,7 @@ export function useLiveParticleChildren(
setIsLoading(false);
},
onError: (err) => {
logError(err, { scope: "firestore.particle-children", path });
logError(err, { scope: 'firestore.particle-children', path });
setError(err);
setIsLoading(false);
},
@@ -116,13 +109,23 @@ export function useLiveParticleChildren(
limit,
});
return unsubscribe;
return () => {
unsubscribe();
setChildren([]);
setError(null);
setIsLoading(true);
};
// The hook intentionally keys only on path/whereFilter/limit — desktop
// does the same. Visibility scope changes are absorbed by the active
// listener; reordering causes a re-subscription.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [path, whereFilter, limit]);
// No path: nothing to load, so report an empty non-loading state.
if (!path) {
return { children: [], isLoading: false, error: null };
}
return { children, isLoading, error };
}
@@ -138,9 +141,6 @@ export function useLiveLatestChild(
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
setIsLoading(true);
setLatestChild(null);
const unsubscribe = subscribeToLatestChild(
toFirestoreChildrenPath(path),
(data) => {
@@ -148,12 +148,16 @@ export function useLiveLatestChild(
setIsLoading(false);
},
(err) => {
logError(err, { scope: "firestore.latest-child", path });
logError(err, { scope: 'firestore.latest-child', path });
setIsLoading(false);
},
);
return unsubscribe;
return () => {
unsubscribe();
setIsLoading(true);
setLatestChild(null);
};
}, [path]);
return { latestChild, isLoading };
@@ -161,7 +165,7 @@ export function useLiveLatestChild(
export function useParticle(path?: ParticlePath) {
return useQuery({
queryKey: ["particle", path],
queryKey: ['particle', path],
queryFn: async () => {
if (!path) return null;
const docPath = toFirestoreDocPath(path);
@@ -174,7 +178,7 @@ export function useParticle(path?: ParticlePath) {
export function useParticleChildren(path?: ParticlePath) {
return useQuery({
queryKey: ["particle-children", path],
queryKey: ['particle-children', path],
queryFn: async () => {
if (!path) return [];
const collectionPath = toFirestoreChildrenPath(path);
+24 -22
View File
@@ -1,12 +1,12 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { where, type QueryFieldFilterConstraint } from "firebase/firestore";
import { useLiveParticleChildren } from "@/hooks/use-particle";
import { useAuthStore } from "@/stores/auth-store";
import { parseParticlePath, type ParticlePath } from "@/lib/particle-path";
import type { Particle, StreamProperties } from "@/api/types";
import { useCallback, useMemo, useState } from 'react';
import { where, type QueryFieldFilterConstraint } from 'firebase/firestore';
import { useLiveParticleChildren } from '@/hooks/use-particle';
import { useAuthStore } from '@/stores/auth-store';
import { parseParticlePath, type ParticlePath } from '@/lib/particle-path';
import type { Particle, StreamProperties } from '@/api/types';
export type StreamParticle = Particle & {
type: "stream";
type: 'stream';
properties: StreamProperties;
};
@@ -15,8 +15,8 @@ const CLOSED_PAGE_INCREMENT = 50;
// Stable where-constraint references so the Firestore subscription only
// re-attaches when the tab actually changes, not on every render.
const OPEN_STATUS_FILTER = where("status", "==", "open");
const CLOSED_STATUS_FILTER = where("status", "==", "closed");
const OPEN_STATUS_FILTER = where('status', '==', 'open');
const CLOSED_STATUS_FILTER = where('status', '==', 'closed');
function useVisibilityScopes(userId?: string, networkId?: string) {
return useMemo(() => {
@@ -33,7 +33,7 @@ interface UseStreamParticlesOptions {
* by active work — full realtime coverage is needed for autoplay/huddles).
* Closed streams are paginated via `loadMore`.
*/
status: "open" | "closed";
status: 'open' | 'closed';
}
interface UseStreamParticlesResult {
@@ -56,38 +56,40 @@ export function useStreamParticles(
const visibilityScopes = useVisibilityScopes(user?.id, networkId);
const [closedLimit, setClosedLimit] = useState(CLOSED_INITIAL_PAGE_SIZE);
const [prevStatus, setPrevStatus] = useState(status);
// Every time the user switches back to the closed tab, start with a fresh
// window. Avoids an ever-growing subscription across a long session.
useEffect(() => {
if (status === "closed") {
// Switching back to the closed tab starts a fresh window, avoiding an
// ever-growing subscription across a long session.
if (status !== prevStatus) {
setPrevStatus(status);
if (status === 'closed') {
setClosedLimit(CLOSED_INITIAL_PAGE_SIZE);
}
}, [status]);
}
const whereFilter: QueryFieldFilterConstraint =
status === "open" ? OPEN_STATUS_FILTER : CLOSED_STATUS_FILTER;
const limit = status === "closed" ? closedLimit : undefined;
status === 'open' ? OPEN_STATUS_FILTER : CLOSED_STATUS_FILTER;
const limit = status === 'closed' ? closedLimit : undefined;
const { children, isLoading, error } = useLiveParticleChildren(path, {
orderByField: "last_child_created_at",
orderDirection: "desc",
orderByField: 'last_child_created_at',
orderDirection: 'desc',
visibilityScopes,
whereFilter,
limit,
});
const streams = useMemo(
() => children.filter((c): c is StreamParticle => c.type === "stream"),
() => children.filter((c): c is StreamParticle => c.type === 'stream'),
[children],
);
// Heuristic: if we got back as many items as we asked for, assume there
// might be more. Clicking load-more when there are no more is a no-op.
const canLoadMore = status === "closed" && streams.length >= closedLimit;
const canLoadMore = status === 'closed' && streams.length >= closedLimit;
const loadMore = useCallback(() => {
if (status !== "closed") return;
if (status !== 'closed') return;
setClosedLimit((prev) => prev + CLOSED_PAGE_INCREMENT);
}, [status]);
+57 -45
View File
@@ -1,15 +1,15 @@
import { useCallback, useEffect, useMemo, useReducer, useRef } from "react";
import { useAuthStore } from "@/stores/auth-store";
import type { Particle } from "@/api/types";
import { useLiveParticleChildren } from "@/hooks/use-particle";
import { toFirestoreDocPath, type ParticlePath } from "@/lib/particle-path";
import { updateStreamPlaybackMarker } from "@/lib/firestore-particles";
import { logError } from "@/lib/errors";
import { useEvent } from "@/hooks/use-event";
import { useCallback, useEffect, useMemo, useReducer, useRef } from 'react';
import { useAuthStore } from '@/stores/auth-store';
import type { Particle } from '@/api/types';
import { useLiveParticleChildren } from '@/hooks/use-particle';
import { toFirestoreDocPath, type ParticlePath } from '@/lib/particle-path';
import { updateStreamPlaybackMarker } from '@/lib/firestore-particles';
import { logError } from '@/lib/errors';
import { useEvent } from '@/hooks/use-event';
// --- Playback reducer (ID-based) ---
type PlaybackStatus = "idle" | "playing" | "ended";
type PlaybackStatus = 'idle' | 'playing' | 'ended';
interface PlaybackState {
currentParticleId: string | null;
@@ -18,19 +18,19 @@ interface PlaybackState {
}
type PlaybackAction =
| { type: "INIT"; particleId: string }
| { type: "SET_PARTICLE"; particleId: string }
| { type: "END" }
| { type: "PARTICLE_ADDED"; particleId: string }
| { type: 'INIT'; particleId: string }
| { type: 'SET_PARTICLE'; particleId: string }
| { type: 'END' }
| { type: 'PARTICLE_ADDED'; particleId: string }
| {
type: "PARTICLE_REMOVED";
type: 'PARTICLE_REMOVED';
removedParticleId: string;
fallbackParticleId: string | null;
};
const initialState: PlaybackState = {
currentParticleId: null,
status: "idle",
status: 'idle',
initialized: false,
};
@@ -39,39 +39,39 @@ function playbackReducer(
action: PlaybackAction,
): PlaybackState {
switch (action.type) {
case "INIT":
case 'INIT':
return {
currentParticleId: action.particleId,
status: "playing",
status: 'playing',
initialized: true,
};
case "SET_PARTICLE":
case 'SET_PARTICLE':
return {
...state,
currentParticleId: action.particleId,
status: "playing",
status: 'playing',
};
case "END":
return { ...state, status: "ended" };
case "PARTICLE_ADDED":
if (state.status === "ended") {
case 'END':
return { ...state, status: 'ended' };
case 'PARTICLE_ADDED':
if (state.status === 'ended') {
return {
...state,
currentParticleId: action.particleId,
status: "playing",
status: 'playing',
};
}
return state;
case "PARTICLE_REMOVED":
case 'PARTICLE_REMOVED':
if (action.removedParticleId !== state.currentParticleId) return state;
if (action.fallbackParticleId) {
return {
...state,
currentParticleId: action.fallbackParticleId,
status: "playing",
status: 'playing',
};
}
return { ...state, currentParticleId: null, status: "idle" };
return { ...state, currentParticleId: null, status: 'idle' };
}
}
@@ -90,33 +90,40 @@ interface UseStreamPlaybackResult {
}
export function useStreamPlayback(
streamParticle: Particle & { type: "stream" },
streamParticle: Particle & { type: 'stream' },
path: ParticlePath,
): UseStreamPlaybackResult {
const userId = useAuthStore((s) => s.user?.id);
const [state, dispatch] = useReducer(playbackReducer, initialState);
// Track which stream we initialized for, so navigating to a sibling resets cleanly.
const initializedForRef = useRef<string | null>(null);
// Latest currentIndex for onParticleRemoved, so it reads the current value
// without recreating the callback (which would re-subscribe the listener).
const currentIndexRef = useRef(0);
const onParticleAdded = useCallback((particle: Particle) => {
dispatch({ type: "PARTICLE_ADDED", particleId: particle.id });
dispatch({ type: 'PARTICLE_ADDED', particleId: particle.id });
}, []);
const onParticleRemoved = useEvent(
const onParticleRemoved = useCallback(
(removed: Particle, updatedChildren: Particle[]) => {
const fallbackIndex = Math.min(currentIndex, updatedChildren.length - 1);
const fallbackIndex = Math.min(
currentIndexRef.current,
updatedChildren.length - 1,
);
const fallback = updatedChildren[Math.max(0, fallbackIndex)];
dispatch({
type: "PARTICLE_REMOVED",
type: 'PARTICLE_REMOVED',
removedParticleId: removed.id,
fallbackParticleId: fallback?.id ?? null,
});
},
[],
);
const { children } = useLiveParticleChildren(path, {
orderByField: "created_at",
orderDirection: "asc",
orderByField: 'created_at',
orderDirection: 'asc',
onAdded: onParticleAdded,
onRemoved: onParticleRemoved,
});
@@ -129,10 +136,15 @@ export function useStreamPlayback(
const currentParticle = currentIndex !== -1 ? children[currentIndex] : null;
// Keep the latest-index ref in sync for onParticleRemoved (above).
useEffect(() => {
currentIndexRef.current = currentIndex;
}, [currentIndex]);
const initFallback = useEvent(() => {
if (state.initialized || children.length === 0) return;
initializedForRef.current = streamParticle.id;
dispatch({ type: "INIT", particleId: children[0].id });
dispatch({ type: 'INIT', particleId: children[0].id });
});
// --- Init logic: runs on every children change until initialized ---
@@ -149,11 +161,11 @@ export function useStreamPlayback(
if (children.length === 0) return;
const playbackPosition = streamParticle.playback_markers?.[userId ?? ""];
const playbackPosition = streamParticle.playback_markers?.[userId ?? ''];
if (!playbackPosition) {
initializedForRef.current = streamParticle.id;
dispatch({ type: "INIT", particleId: children[0].id });
dispatch({ type: 'INIT', particleId: children[0].id });
return;
}
@@ -163,12 +175,12 @@ export function useStreamPlayback(
if (found) {
initializedForRef.current = streamParticle.id;
dispatch({ type: "INIT", particleId: found.id });
dispatch({ type: 'INIT', particleId: found.id });
return;
} else {
initializedForRef.current = streamParticle.id;
dispatch({
type: "INIT",
type: 'INIT',
particleId: children[children.length - 1].id,
});
}
@@ -201,7 +213,7 @@ export function useStreamPlayback(
lastPersistedMarkerRef.current = currentTime;
const streamDocPath = toFirestoreDocPath(path);
updateStreamPlaybackMarker(streamDocPath, userId, currentTime).catch(
(err) => logError(err, { scope: "playback.marker", path }),
(err) => logError(err, { scope: 'playback.marker', path }),
);
// streamParticle.playback_markers is read at effect time; not in deps to
// avoid double-writes when the snapshot we just persisted echoes back.
@@ -213,18 +225,18 @@ export function useStreamPlayback(
if (currentIndex === -1) return;
if (currentIndex < children.length - 1) {
dispatch({
type: "SET_PARTICLE",
type: 'SET_PARTICLE',
particleId: children[currentIndex + 1].id,
});
} else {
dispatch({ type: "END" });
dispatch({ type: 'END' });
}
}, [children, currentIndex]);
const prev = useCallback(() => {
if (currentIndex <= 0) return;
dispatch({
type: "SET_PARTICLE",
type: 'SET_PARTICLE',
particleId: children[currentIndex - 1].id,
});
}, [children, currentIndex]);
@@ -232,7 +244,7 @@ export function useStreamPlayback(
const goTo = useCallback(
(index: number) => {
if (index >= 0 && index < children.length) {
dispatch({ type: "SET_PARTICLE", particleId: children[index].id });
dispatch({ type: 'SET_PARTICLE', particleId: children[index].id });
}
},
[children],
@@ -241,7 +253,7 @@ export function useStreamPlayback(
// If the particle isn't in `children` yet (e.g. just-created), the live
// query will resolve it shortly and the derived index/particle will catch up.
const goToParticle = useCallback((particleId: string) => {
dispatch({ type: "SET_PARTICLE", particleId });
dispatch({ type: 'SET_PARTICLE', particleId });
}, []);
return {
+2 -2
View File
@@ -1,5 +1,5 @@
import { useEffect, useId } from "react";
import { usePlaybackPauseStore } from "@/stores/playback-pause-store";
import { useEffect, useId } from 'react';
import { usePlaybackPauseStore } from '@/stores/playback-pause-store';
/**
* Suspend stream playback while `active` is true. The hook owns its own
@@ -1,7 +1,7 @@
import { useMemo } from "react";
import type { Transcript } from "@/api/types";
import { useMemo } from 'react';
import type { Transcript } from '@/api/types';
type Sentence = Transcript["paragraphs"][number]["sentences"][number];
type Sentence = Transcript['paragraphs'][number]['sentences'][number];
interface TranscriptPlaybackState {
/** The sentence currently being spoken, or null if between sentences */