Files
ds-chat/frontend/src/context/AuthContext.tsx
T
ksmithandClaude Sonnet 5 520b971247 Add adjustable text size and emoji size preferences (#71)
Text was too small on high-DPI screens with no in-app fix beyond
browser zoom. Adds a text-size setting (scales the whole app via a
root font-size percentage), auto-large rendering for emoji-only
messages, and an independent emoji-size preference that also scales
reaction pills without affecting the emoji picker's fixed-size grid.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-30 18:30:49 -06:00

109 lines
3.8 KiB
TypeScript

import { createContext, useContext, useEffect, useState, type ReactNode } from 'react'
import * as authApi from '../api/auth'
import { ApiError, NetworkError } from '../api/client'
import { clearLastUser, loadLastUser, saveLastUser } from '../lib/lastUser'
import { unsubscribeFromPush } from '../lib/push'
import { applyTextScale, applyTheme } from '../lib/theme'
import type { User } from '../types'
interface AuthContextValue {
user: User | null
loading: boolean
offline: boolean
login: (usernameOrEmail: string, password: string) => Promise<void>
logout: () => Promise<void>
updateUser: (user: User) => void
}
const AuthContext = createContext<AuthContextValue | undefined>(undefined)
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null)
const [loading, setLoading] = useState(true)
const [offline, setOffline] = useState(false)
useEffect(() => {
applyTheme(user?.theme ?? null, user?.active_custom_theme?.colors ?? null)
}, [user?.theme, user?.active_custom_theme])
useEffect(() => {
applyTextScale(user?.text_scale ?? null)
}, [user?.text_scale])
useEffect(() => {
authApi
.me()
.then((u) => {
setUser(u)
setOffline(false)
saveLastUser(u)
})
.catch((err) => {
// A confirmed 401 means the server is reachable and says "not
// logged in" -- that's the only case that should actually clear the
// cached identity. Everything else (a real network failure, or the
// backend being down behind a reverse proxy that answers with its
// own 502/503/504 -- both happen in production, not just literal
// offline) means we simply couldn't get a confirmed answer.
// /api/auth/me is NetworkOnly by design (never trust a stale
// "who am I" as the *source of truth*), but treating "couldn't
// check" the same as "confirmed logged out" would lock users out of
// the cached rooms/messages entirely whenever the backend is
// unreachable. Every real action still re-checks the actual session
// cookie server-side, so falling back here can't grant anything.
if (err instanceof ApiError && err.status === 401) {
clearLastUser()
return
}
if (!(err instanceof NetworkError)) {
console.error('Failed to load current user', err)
}
const cached = loadLastUser()
setUser(cached)
setOffline(cached !== null)
})
.finally(() => setLoading(false))
}, [])
async function login(usernameOrEmail: string, password: string) {
const u = await authApi.login(usernameOrEmail, password)
setUser(u)
setOffline(false)
saveLastUser(u)
}
async function logout() {
// Best-effort, and must run before the session cookie is cleared below
// -- the unsubscribe call is authenticated. Otherwise this browser's
// push subscription (both the server-side row and the registration
// itself) outlives the session, so the account being logged out of
// keeps silently receiving pushes for as long as this browser stays
// installed/open, with no way for the user to tell why.
try {
await unsubscribeFromPush()
} catch {
// Not fatal -- logging out must still proceed even if this failed.
}
await authApi.logout()
setUser(null)
clearLastUser()
}
function updateUser(u: User) {
setUser(u)
saveLastUser(u)
}
return (
<AuthContext.Provider value={{ user, loading, offline, login, logout, updateUser }}>
{children}
</AuthContext.Provider>
)
}
export function useAuth(): AuthContextValue {
const ctx = useContext(AuthContext)
if (!ctx) throw new Error('useAuth must be used within an AuthProvider')
return ctx
}