Files
ds-chat/frontend/src/components/TopBar.tsx
T
ksmithandClaude Sonnet 5 14dc340174 Rename project from KeepItTalking to DS Chat
Renames the app's display name everywhere (page titles, PWA manifest,
TopBar, email subject lines, HMAC signature header) and its internal
technical slug from chatapp to ds-chat/ds_chat: the Python package name
and console script, the systemd unit and its user/group/paths, the deploy
scripts, the Docker container names, and the Postgres database name.

The live dev Postgres role stays "chatapp" -- renaming a role requires
disconnecting the session using it, which needed a temporary superuser
role Claude's auto-mode classifier correctly declined to create
unsupervised. Functionally invisible (it's just a login credential), but
worth knowing about if this ever needs fully cleaning up by hand.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 21:11:41 -06:00

116 lines
3.7 KiB
TypeScript

import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import logo from '../assets/logo.png'
import { getUserAvatarUrl } from '../api/users'
import { useAuth } from '../context/AuthContext'
import { hashIndex } from '../lib/avatar'
import { getPushSubscriptionStatus, isPushSupported, subscribeToPush, unsubscribeFromPush } from '../lib/push'
import { ProfileModal } from './ProfileModal'
import { UserAvatar } from './UserAvatar'
import './TopBar.css'
export function TopBar() {
const { user, logout } = useAuth()
const navigate = useNavigate()
const [menuOpen, setMenuOpen] = useState(false)
const [profileModalOpen, setProfileModalOpen] = useState(false)
const [pushSubscribed, setPushSubscribed] = useState(false)
const [pushBusy, setPushBusy] = useState(false)
const [pushError, setPushError] = useState<string | null>(null)
useEffect(() => {
getPushSubscriptionStatus().then(setPushSubscribed)
}, [])
async function handleTogglePush() {
setPushBusy(true)
setPushError(null)
try {
if (pushSubscribed) {
await unsubscribeFromPush()
setPushSubscribed(false)
} else {
await subscribeToPush()
setPushSubscribed(true)
}
} catch (err) {
setPushError(err instanceof Error ? err.message : String(err))
} finally {
setPushBusy(false)
}
}
if (!user) return null
return (
<header className="top-bar">
<div className="top-bar-brand">
<img src={logo} alt="" className="top-bar-logo" />
<span className="top-bar-title">DS Chat</span>
</div>
<div className="top-bar-user">
<button
type="button"
className="top-bar-avatar"
onClick={() => setMenuOpen((v) => !v)}
aria-expanded={menuOpen}
aria-label="Account menu"
>
<UserAvatar
username={user.username}
colorIndex={hashIndex(user.username)}
size={30}
avatarUrl={user.avatar_filename ? getUserAvatarUrl(user.id, user.avatar_filename) : null}
/>
</button>
{menuOpen && (
<>
<div className="top-bar-menu-scrim" onClick={() => setMenuOpen(false)} />
<div className="top-bar-menu" role="menu">
<div className="top-bar-menu-username">{user.display_name || user.username}</div>
<button
type="button"
role="menuitem"
onClick={() => {
setMenuOpen(false)
setProfileModalOpen(true)
}}
>
Profile settings
</button>
{user.is_site_admin && (
<button
type="button"
role="menuitem"
onClick={() => {
setMenuOpen(false)
navigate('/admin')
}}
>
Admin
</button>
)}
{isPushSupported() && (
<button
type="button"
role="menuitem"
onClick={handleTogglePush}
disabled={pushBusy}
>
{pushSubscribed ? 'Disable notifications' : 'Enable notifications'}
</button>
)}
{pushError && <div className="top-bar-menu-error">{pushError}</div>}
<button type="button" role="menuitem" onClick={() => logout()}>
Log out
</button>
</div>
</>
)}
</div>
{profileModalOpen && <ProfileModal onClose={() => setProfileModalOpen(false)} />}
</header>
)
}