Private
Public Access
Site-wide, any user can upload -- usable both as reactions and inline in message text via :shortcode:, alongside the existing built-in Unicode picker. A :shortcode: reference is stored/sent as literal text (same as the built-in shortcode convention) and resolved to an image at render time, so it degrades to plain text if the emoji is later deleted. Backend: new custom_emoji table (shortcode unique, sized to fit MessageReaction.emoji's existing column alongside its colons), upload/ list/delete endpoints (delete restricted to uploader or site admin). Frontend: a CustomEmojiProvider context feeds a new "Custom" category in the emoji picker (inline upload + hover-to-remove), extends the composer's shortcode autocomplete, and a shared EmojiGlyph resolver renders custom emoji wherever a value can appear -- message text, reaction pills, and the picker itself. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
89 lines
2.7 KiB
TypeScript
89 lines
2.7 KiB
TypeScript
import { Navigate, Route, Routes } from 'react-router-dom'
|
|
import { AuthProvider, useAuth } from './context/AuthContext'
|
|
import { ChatSocketProvider } from './context/ChatSocketContext'
|
|
import { CustomEmojiProvider } from './context/CustomEmojiContext'
|
|
import { AdminRoute } from './components/AdminRoute'
|
|
import { DesktopNotificationBridge } from './components/DesktopNotificationBridge'
|
|
import { ProtectedRoute } from './components/ProtectedRoute'
|
|
import { UpdateBanner } from './components/UpdateBanner'
|
|
import { LoginPage } from './pages/LoginPage'
|
|
import { SignupPage } from './pages/SignupPage'
|
|
import { ForgotPasswordPage } from './pages/ForgotPasswordPage'
|
|
import { ResetPasswordPage } from './pages/ResetPasswordPage'
|
|
import { ChatShellPage } from './pages/ChatShellPage'
|
|
import { AdminPage } from './pages/AdminPage'
|
|
import { HelpPage } from './pages/HelpPage'
|
|
|
|
function AppRoutes() {
|
|
const routes = (
|
|
<Routes>
|
|
<Route path="/login" element={<LoginPage />} />
|
|
<Route path="/signup" element={<SignupPage />} />
|
|
<Route path="/forgot-password" element={<ForgotPasswordPage />} />
|
|
<Route path="/reset-password" element={<ResetPasswordPage />} />
|
|
<Route
|
|
path="/rooms"
|
|
element={
|
|
<ProtectedRoute>
|
|
<ChatShellPage />
|
|
</ProtectedRoute>
|
|
}
|
|
/>
|
|
<Route
|
|
path="/rooms/:roomId"
|
|
element={
|
|
<ProtectedRoute>
|
|
<ChatShellPage />
|
|
</ProtectedRoute>
|
|
}
|
|
/>
|
|
<Route
|
|
path="/admin"
|
|
element={
|
|
<AdminRoute>
|
|
<AdminPage />
|
|
</AdminRoute>
|
|
}
|
|
/>
|
|
<Route
|
|
path="/help"
|
|
element={
|
|
<ProtectedRoute>
|
|
<HelpPage />
|
|
</ProtectedRoute>
|
|
}
|
|
/>
|
|
<Route path="*" element={<Navigate to="/rooms" replace />} />
|
|
</Routes>
|
|
)
|
|
|
|
// Only while actually logged in -- and keyed by user id so switching
|
|
// which account is logged in (same tab) tears down and re-establishes a
|
|
// fresh connection rather than an old one lingering under a new
|
|
// identity. Wraps every authenticated route (not just ChatShellPage),
|
|
// since the connection backs the presence indicator and cross-page
|
|
// signals (e.g. "added to a room") that matter regardless of which page
|
|
// is currently open.
|
|
const { user } = useAuth()
|
|
if (!user) return routes
|
|
return (
|
|
<ChatSocketProvider key={user.id}>
|
|
<CustomEmojiProvider>
|
|
<DesktopNotificationBridge />
|
|
{routes}
|
|
</CustomEmojiProvider>
|
|
</ChatSocketProvider>
|
|
)
|
|
}
|
|
|
|
function App() {
|
|
return (
|
|
<AuthProvider>
|
|
<UpdateBanner />
|
|
<AppRoutes />
|
|
</AuthProvider>
|
|
)
|
|
}
|
|
|
|
export default App
|