v2: global player, infinite scroll, image picker, threaded comments
This commit is contained in:
@@ -4,11 +4,27 @@ import { AuthContext, type AuthContextValue } from "./AuthContext.ts";
|
||||
|
||||
import { type AuthResponse, deserializeAuthResponse } from "../model.ts";
|
||||
|
||||
function isTokenExpired(token: string): boolean {
|
||||
try {
|
||||
const payload = JSON.parse(atob(token.split(".")[1]));
|
||||
return typeof payload.exp === "number" && payload.exp * 1000 < Date.now();
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [authResponse, setAuthResponse] = useState<AuthResponse | null>(() => {
|
||||
const stored = localStorage.getItem("authResponse");
|
||||
if (!stored) return null;
|
||||
|
||||
return stored ? deserializeAuthResponse(JSON.parse(stored)) : null;
|
||||
const parsed = deserializeAuthResponse(JSON.parse(stored));
|
||||
if (isTokenExpired(parsed.token)) {
|
||||
localStorage.removeItem("authResponse");
|
||||
return null;
|
||||
}
|
||||
|
||||
return parsed;
|
||||
});
|
||||
|
||||
const value: AuthContextValue = { authResponse, setAuthResponse };
|
||||
|
||||
19
src/contexts/PlayerContext.ts
Normal file
19
src/contexts/PlayerContext.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { createContext } from "react";
|
||||
|
||||
export interface PlayerItem {
|
||||
embedUrl: string;
|
||||
title?: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface PlayerContextValue {
|
||||
current: PlayerItem | null;
|
||||
play(item: PlayerItem): void;
|
||||
stop(): void;
|
||||
}
|
||||
|
||||
export const PlayerContext = createContext<PlayerContextValue>({
|
||||
current: null,
|
||||
play: () => {},
|
||||
stop: () => {},
|
||||
});
|
||||
14
src/contexts/PlayerProvider.tsx
Normal file
14
src/contexts/PlayerProvider.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { useState } from "react";
|
||||
import { PlayerContext, type PlayerItem } from "./PlayerContext.ts";
|
||||
|
||||
export function PlayerProvider({ children }: { children: React.ReactNode }) {
|
||||
const [current, setCurrent] = useState<PlayerItem | null>(null);
|
||||
|
||||
return (
|
||||
<PlayerContext.Provider
|
||||
value={{ current, play: setCurrent, stop: () => setCurrent(null) }}
|
||||
>
|
||||
{children}
|
||||
</PlayerContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createContext } from "react";
|
||||
import type { Dump, OnlineUser, Playlist } from "../model.ts";
|
||||
import type { Comment, Dump, OnlineUser, Playlist } from "../model.ts";
|
||||
|
||||
export interface VoteEvent {
|
||||
dumpId: string;
|
||||
@@ -15,6 +15,13 @@ export interface PlaylistEvent {
|
||||
dumpIds?: string[];
|
||||
}
|
||||
|
||||
export interface CommentEvent {
|
||||
type: "created" | "deleted";
|
||||
dumpId: string;
|
||||
comment?: Comment;
|
||||
commentId?: string;
|
||||
}
|
||||
|
||||
export interface WSContextValue {
|
||||
onlineUsers: OnlineUser[];
|
||||
voteCounts: Record<string, number>;
|
||||
@@ -22,8 +29,10 @@ export interface WSContextValue {
|
||||
recentDumps: Dump[];
|
||||
deletedDumpIds: Set<string>;
|
||||
lastVoteEvent: VoteEvent | null;
|
||||
lastDumpEvent: Dump | null;
|
||||
lastPlaylistEvent: PlaylistEvent | null;
|
||||
deletedPlaylistIds: Set<string>;
|
||||
lastCommentEvent: CommentEvent | null;
|
||||
castVote: (dumpId: string) => void;
|
||||
removeVote: (dumpId: string) => void;
|
||||
}
|
||||
@@ -35,8 +44,10 @@ export const WSContext = createContext<WSContextValue>({
|
||||
recentDumps: [],
|
||||
deletedDumpIds: new Set(),
|
||||
lastVoteEvent: null,
|
||||
lastDumpEvent: null,
|
||||
lastPlaylistEvent: null,
|
||||
deletedPlaylistIds: new Set(),
|
||||
lastCommentEvent: null,
|
||||
castVote: () => {},
|
||||
removeVote: () => {},
|
||||
});
|
||||
|
||||
@@ -7,14 +7,25 @@ import {
|
||||
useState,
|
||||
} from "react";
|
||||
import {
|
||||
type CommentEvent,
|
||||
type PlaylistEvent,
|
||||
type VoteEvent,
|
||||
WSContext,
|
||||
type WSContextValue,
|
||||
} from "./WSContext.ts";
|
||||
import { WS_URL } from "../config/api.ts";
|
||||
import type { Dump, OnlineUser, RawDump, RawPlaylist } from "../model.ts";
|
||||
import { deserializeDump, deserializePlaylist } from "../model.ts";
|
||||
import type {
|
||||
Dump,
|
||||
OnlineUser,
|
||||
RawComment,
|
||||
RawDump,
|
||||
RawPlaylist,
|
||||
} from "../model.ts";
|
||||
import {
|
||||
deserializeComment,
|
||||
deserializeDump,
|
||||
deserializePlaylist,
|
||||
} from "../model.ts";
|
||||
|
||||
interface WSProviderProps {
|
||||
children: ReactNode;
|
||||
@@ -31,12 +42,16 @@ export function WSProvider({ children, token }: WSProviderProps) {
|
||||
const [recentDumps, setRecentDumps] = useState<Dump[]>([]);
|
||||
const [deletedDumpIds, setDeletedDumpIds] = useState<Set<string>>(new Set());
|
||||
const [lastVoteEvent, setLastVoteEvent] = useState<VoteEvent | null>(null);
|
||||
const [lastDumpEvent, setLastDumpEvent] = useState<Dump | null>(null);
|
||||
const [lastPlaylistEvent, setLastPlaylistEvent] = useState<
|
||||
PlaylistEvent | null
|
||||
>(null);
|
||||
const [deletedPlaylistIds, setDeletedPlaylistIds] = useState<Set<string>>(
|
||||
new Set(),
|
||||
);
|
||||
const [lastCommentEvent, setLastCommentEvent] = useState<CommentEvent | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
// Refs to avoid stale closures in event handlers
|
||||
const voteCountsRef = useRef(voteCounts);
|
||||
@@ -112,6 +127,12 @@ export function WSProvider({ children, token }: WSProviderProps) {
|
||||
break;
|
||||
}
|
||||
|
||||
case "dump_updated": {
|
||||
const dump = deserializeDump(msg.dump as RawDump);
|
||||
setLastDumpEvent(dump);
|
||||
break;
|
||||
}
|
||||
|
||||
case "dump_deleted": {
|
||||
const dumpId = msg.dumpId as string;
|
||||
setDeletedDumpIds((prev) => new Set([...prev, dumpId]));
|
||||
@@ -177,6 +198,25 @@ export function WSProvider({ children, token }: WSProviderProps) {
|
||||
break;
|
||||
}
|
||||
|
||||
case "comment_created": {
|
||||
const comment = deserializeComment(msg.comment as RawComment);
|
||||
setLastCommentEvent({
|
||||
type: "created",
|
||||
dumpId: comment.dumpId,
|
||||
comment,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "comment_deleted": {
|
||||
const { commentId, dumpId } = msg as {
|
||||
commentId: string;
|
||||
dumpId: string;
|
||||
};
|
||||
setLastCommentEvent({ type: "deleted", dumpId, commentId });
|
||||
break;
|
||||
}
|
||||
|
||||
case "error":
|
||||
// On error, revert any pending optimistic update for the affected dump
|
||||
// (the revert timeout will handle it)
|
||||
@@ -276,8 +316,10 @@ export function WSProvider({ children, token }: WSProviderProps) {
|
||||
recentDumps,
|
||||
deletedDumpIds,
|
||||
lastVoteEvent,
|
||||
lastDumpEvent,
|
||||
lastPlaylistEvent,
|
||||
deletedPlaylistIds,
|
||||
lastCommentEvent,
|
||||
castVote,
|
||||
removeVote,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user