Remove Websocket code since we are only using SSE
This commit is contained in:
@@ -1,106 +0,0 @@
|
||||
import { Circle, SignalWifi4Bar, SignalWifiOff } from '@mui/icons-material'
|
||||
import { Box, Chip, Tooltip, Typography } from '@mui/joy'
|
||||
import { useWebSocketContext } from '../contexts/WebSocketContext'
|
||||
|
||||
const WebSocketConnectionStatus = ({
|
||||
variant = 'minimal',
|
||||
showError = false,
|
||||
sx = {},
|
||||
}) => {
|
||||
const { isConnected, isConnecting, error, getConnectionStatus } =
|
||||
useWebSocketContext()
|
||||
|
||||
const getStatusColor = () => {
|
||||
if (isConnected) return 'success'
|
||||
if (isConnecting) return 'warning'
|
||||
return 'danger'
|
||||
}
|
||||
|
||||
const getStatusIcon = () => {
|
||||
if (isConnected) return <SignalWifi4Bar />
|
||||
if (isConnecting) return <Circle />
|
||||
return <SignalWifiOff />
|
||||
}
|
||||
|
||||
const getStatusText = () => {
|
||||
if (isConnected) return 'Connected'
|
||||
if (isConnecting) return 'Connecting...'
|
||||
return 'Disconnected'
|
||||
}
|
||||
|
||||
const getTooltipText = () => {
|
||||
const status = getConnectionStatus()
|
||||
if (error) return `Real-time updates: ${status} - ${error}`
|
||||
if (!isConnected && !isConnecting) {
|
||||
return `Real-time updates: ${status} - Join a circle to enable real-time updates`
|
||||
}
|
||||
return `Real-time updates: ${status}`
|
||||
}
|
||||
|
||||
if (variant === 'minimal') {
|
||||
return (
|
||||
<Tooltip title={getTooltipText()} size='sm'>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.5,
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
<Circle
|
||||
sx={{
|
||||
fontSize: 8,
|
||||
color:
|
||||
getStatusColor() === 'success'
|
||||
? 'success.main'
|
||||
: getStatusColor() === 'warning'
|
||||
? 'warning.main'
|
||||
: 'danger.main',
|
||||
}}
|
||||
/>
|
||||
{showError && error && (
|
||||
<Typography level='body-xs' color='danger'>
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
if (variant === 'chip') {
|
||||
return (
|
||||
<Tooltip title={getTooltipText()} size='sm'>
|
||||
<Chip
|
||||
color={getStatusColor()}
|
||||
size='sm'
|
||||
variant='soft'
|
||||
startDecorator={getStatusIcon()}
|
||||
sx={sx}
|
||||
>
|
||||
{getStatusText()}
|
||||
</Chip>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
// Full variant
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, ...sx }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{getStatusIcon()}
|
||||
<Typography level='body-sm' color={getStatusColor()}>
|
||||
{getStatusText()}
|
||||
</Typography>
|
||||
</Box>
|
||||
{showError && error && (
|
||||
<Typography level='body-xs' color='danger'>
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default WebSocketConnectionStatus
|
||||
@@ -1,143 +0,0 @@
|
||||
import { Sync, SyncDisabled } from '@mui/icons-material'
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
Chip,
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
FormLabel,
|
||||
Switch,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { useWebSocketContext } from '../contexts/WebSocketContext'
|
||||
import { useUserProfile } from '../queries/UserQueries'
|
||||
import { isPlusAccount } from '../utils/Helpers'
|
||||
import WebSocketConnectionStatus from './WebSocketConnectionStatus'
|
||||
|
||||
const WebSocketSettings = () => {
|
||||
const { data: userProfile } = useUserProfile()
|
||||
const {
|
||||
isConnected,
|
||||
isConnecting,
|
||||
error,
|
||||
getConnectionStatus,
|
||||
toggleWebSocketEnabled,
|
||||
isWebSocketEnabled,
|
||||
} = useWebSocketContext()
|
||||
|
||||
const handleToggle = () => {
|
||||
if (!isPlusAccount(userProfile)) {
|
||||
return // Don't allow toggle for non-Plus users
|
||||
}
|
||||
const currentlyEnabled = isWebSocketEnabled()
|
||||
toggleWebSocketEnabled(!currentlyEnabled)
|
||||
}
|
||||
|
||||
const getStatusDescription = () => {
|
||||
if (!isPlusAccount(userProfile)) {
|
||||
return 'Real-time updates are not available in the Basic plan. Upgrade to Plus to receive instant notifications when chores are updated.'
|
||||
}
|
||||
|
||||
if (!isWebSocketEnabled()) {
|
||||
return 'Real-time updates are disabled. Enable to see live changes when you or other circle members complete, skip, or modify chores.'
|
||||
}
|
||||
|
||||
if (isConnected) {
|
||||
return "Real-time updates are working. You'll see live changes when you or other circle members complete, skip, or modify chores."
|
||||
}
|
||||
|
||||
if (isConnecting) {
|
||||
return 'Connecting to real-time updates...'
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return `Real-time updates are enabled but not working: ${error}`
|
||||
}
|
||||
|
||||
return 'Real-time updates are enabled but not currently connected.'
|
||||
}
|
||||
|
||||
return (
|
||||
<Card sx={{ mt: 2, p: 3 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
|
||||
{isWebSocketEnabled() && isPlusAccount(userProfile) ? (
|
||||
<Sync color={isConnected ? 'success' : 'disabled'} />
|
||||
) : (
|
||||
<SyncDisabled color='disabled' />
|
||||
)}
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography level='title-md'>
|
||||
Real-time Updates
|
||||
{!isPlusAccount(userProfile) && (
|
||||
<Chip variant='soft' color='warning' sx={{ ml: 1 }}>
|
||||
Plus Feature
|
||||
</Chip>
|
||||
)}
|
||||
</Typography>
|
||||
<Typography level='body-sm' color='neutral'>
|
||||
Get instant notifications when chores are updated
|
||||
</Typography>
|
||||
</Box>
|
||||
{isWebSocketEnabled() && isPlusAccount(userProfile) && (
|
||||
<WebSocketConnectionStatus variant='chip' />
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<FormControl orientation='horizontal' sx={{ mb: 2 }}>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<FormLabel>Enable Real-time Updates</FormLabel>
|
||||
<FormHelperText sx={{ mt: 0 }}>
|
||||
{getStatusDescription()}
|
||||
</FormHelperText>
|
||||
</Box>
|
||||
<Switch
|
||||
checked={Boolean(isWebSocketEnabled() && isPlusAccount(userProfile))}
|
||||
onChange={handleToggle}
|
||||
disabled={!isPlusAccount(userProfile)}
|
||||
color={
|
||||
isWebSocketEnabled() && isPlusAccount(userProfile)
|
||||
? 'success'
|
||||
: 'neutral'
|
||||
}
|
||||
variant='solid'
|
||||
endDecorator={
|
||||
isWebSocketEnabled() && isPlusAccount(userProfile) ? 'On' : 'Off'
|
||||
}
|
||||
slotProps={{ endDecorator: { sx: { minWidth: 24 } } }}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
{isWebSocketEnabled() && isPlusAccount(userProfile) && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 1 }}>
|
||||
<Typography level='body-xs' color='neutral'>
|
||||
Status:
|
||||
</Typography>
|
||||
<Chip
|
||||
size='sm'
|
||||
variant='soft'
|
||||
color={
|
||||
isConnected ? 'success' : isConnecting ? 'warning' : 'danger'
|
||||
}
|
||||
>
|
||||
{getConnectionStatus()}
|
||||
</Chip>
|
||||
{error && (
|
||||
<Typography level='body-xs' color='danger'>
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{!isPlusAccount(userProfile) && (
|
||||
<Typography level='body-sm' color='warning' sx={{ mt: 1 }}>
|
||||
Real-time updates are not available in the Basic plan. Upgrade to Plus
|
||||
to receive instant notifications when you or other circle members
|
||||
complete, skip, or modify chores.
|
||||
</Typography>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default WebSocketSettings
|
||||
@@ -4,7 +4,6 @@ import QueryContext from './QueryContext'
|
||||
import RouterContext from './RouterContext'
|
||||
import SSEProvider from './SSEContext'
|
||||
import ThemeContext from './ThemeContext'
|
||||
import WebSocketProvider from './WebSocketContext'
|
||||
|
||||
const Contexts = ({ children }) => {
|
||||
const contexts = [
|
||||
@@ -13,7 +12,6 @@ const Contexts = ({ children }) => {
|
||||
QueryContext,
|
||||
NotificationProvider,
|
||||
SSEProvider,
|
||||
WebSocketProvider,
|
||||
RouterContext,
|
||||
]
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import { createContext, useContext } from 'react'
|
||||
import { useWebSocket } from '../hooks/useWebSocket'
|
||||
|
||||
const WebSocketContext = createContext({
|
||||
connectionState: 3, // CLOSED
|
||||
isConnected: false,
|
||||
isConnecting: false,
|
||||
lastEvent: null,
|
||||
error: null,
|
||||
connect: () => {},
|
||||
disconnect: () => {},
|
||||
getConnectionStatus: () => 'disconnected',
|
||||
})
|
||||
|
||||
export const useWebSocketContext = () => {
|
||||
return useContext(WebSocketContext)
|
||||
}
|
||||
|
||||
export const WebSocketProvider = ({ children }) => {
|
||||
const webSocketState = useWebSocket()
|
||||
|
||||
return (
|
||||
<WebSocketContext.Provider value={webSocketState}>
|
||||
{children}
|
||||
</WebSocketContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default WebSocketProvider
|
||||
@@ -1,313 +0,0 @@
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { apiManager, isTokenValid } from '../utils/TokenManager'
|
||||
|
||||
const WEBSOCKET_STATES = {
|
||||
CONNECTING: 0,
|
||||
OPEN: 1,
|
||||
CLOSING: 2,
|
||||
CLOSED: 3,
|
||||
}
|
||||
|
||||
const RECONNECT_INTERVALS = [1000, 2000, 5000, 10000, 30000] // Progressive backoff
|
||||
|
||||
export const useWebSocket = () => {
|
||||
const [connectionState, setConnectionState] = useState(
|
||||
WEBSOCKET_STATES.CLOSED,
|
||||
)
|
||||
const [lastEvent, setLastEvent] = useState(null)
|
||||
const [error, setError] = useState(null)
|
||||
|
||||
const wsRef = useRef(null)
|
||||
const reconnectTimeoutRef = useRef(null)
|
||||
const reconnectAttemptsRef = useRef(0)
|
||||
const isManuallyClosedRef = useRef(false)
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const getWebSocketUrl = useCallback(() => {
|
||||
const token = localStorage.getItem('ca_token')
|
||||
if (!token || !isTokenValid()) {
|
||||
console.log('WebSocket: No valid authentication token')
|
||||
return null
|
||||
}
|
||||
|
||||
const apiUrl = apiManager.getApiURL()
|
||||
|
||||
// Convert HTTP/HTTPS to WebSocket protocol and remove /api/v1 suffix
|
||||
let wsUrl = apiUrl.replace(/\/api\/v1$/, '')
|
||||
if (wsUrl.startsWith('http://')) {
|
||||
wsUrl = wsUrl.replace('http://', 'ws://')
|
||||
} else if (wsUrl.startsWith('https://')) {
|
||||
wsUrl = wsUrl.replace('https://', 'wss://')
|
||||
} else {
|
||||
const isHttps = window.location.protocol === 'https:'
|
||||
wsUrl = `${isHttps ? 'wss:' : 'ws:'}//${wsUrl}`
|
||||
}
|
||||
|
||||
// Let backend determine circle from authenticated user
|
||||
wsUrl = `${wsUrl}/api/v1/realtime/ws?token=${token}`
|
||||
|
||||
return wsUrl
|
||||
}, [])
|
||||
|
||||
const handleWebSocketMessage = useCallback(
|
||||
event => {
|
||||
try {
|
||||
const eventData = JSON.parse(event.data)
|
||||
setLastEvent(eventData)
|
||||
|
||||
console.debug('WebSocket event received:', eventData.type, eventData)
|
||||
|
||||
// Handle different event types and update React Query cache accordingly
|
||||
switch (eventData.type) {
|
||||
case 'chore.created':
|
||||
case 'chore.updated':
|
||||
case 'chore.completed':
|
||||
case 'chore.skipped':
|
||||
case 'chore.deleted':
|
||||
// Invalidate chores queries to refetch data
|
||||
queryClient.invalidateQueries(['chores'])
|
||||
|
||||
// If it's a specific chore event, also invalidate that chore's details
|
||||
if (eventData.data.chore?.id) {
|
||||
queryClient.invalidateQueries(['chore', eventData.data.chore.id])
|
||||
queryClient.invalidateQueries([
|
||||
'choreDetails',
|
||||
eventData.data.chore.id,
|
||||
])
|
||||
}
|
||||
// expire the history so feed on dashboard gert updated :
|
||||
// need to find a better way to do this as we don't need to do it with every single update for anything
|
||||
// but not sure if i can do it with the fall-through switch case in javascript :)
|
||||
queryClient.invalidateQueries(['choresHistory', 7])
|
||||
|
||||
break
|
||||
|
||||
case 'subtask.updated':
|
||||
case 'subtask.completed':
|
||||
// Invalidate the specific chore that contains this subtask
|
||||
if (eventData.data.choreId) {
|
||||
queryClient.invalidateQueries(['chore', eventData.data.choreId])
|
||||
queryClient.invalidateQueries([
|
||||
'choreDetails',
|
||||
eventData.data.choreId,
|
||||
])
|
||||
}
|
||||
// Also invalidate general chores list
|
||||
queryClient.invalidateQueries(['chores'])
|
||||
break
|
||||
|
||||
case 'heartbeat':
|
||||
// Heartbeat events don't need cache invalidation
|
||||
console.debug('Heartbeat!')
|
||||
break
|
||||
|
||||
case 'connection.established':
|
||||
console.log('WebSocket connection established')
|
||||
setError(null)
|
||||
break
|
||||
|
||||
case 'error':
|
||||
console.error('WebSocket error event:', eventData.data)
|
||||
setError(eventData.data.message || 'WebSocket error occurred')
|
||||
break
|
||||
|
||||
default:
|
||||
console.log('Unknown WebSocket event type:', eventData.type)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to parse WebSocket message:', err)
|
||||
setError('Failed to parse server message')
|
||||
}
|
||||
},
|
||||
[queryClient],
|
||||
)
|
||||
|
||||
const createWebSocketConnection = useCallback(
|
||||
wsUrl => {
|
||||
try {
|
||||
setConnectionState(WEBSOCKET_STATES.CONNECTING)
|
||||
isManuallyClosedRef.current = false
|
||||
|
||||
// Use query parameter authentication (token already included in URL)
|
||||
wsRef.current = new WebSocket(wsUrl)
|
||||
|
||||
wsRef.current.onopen = () => {
|
||||
setConnectionState(WEBSOCKET_STATES.OPEN)
|
||||
setError(null)
|
||||
reconnectAttemptsRef.current = 0
|
||||
}
|
||||
|
||||
wsRef.current.onmessage = handleWebSocketMessage
|
||||
|
||||
wsRef.current.onerror = error => {
|
||||
console.error('WebSocket error:', error)
|
||||
setError('Connection error occurred')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to create WebSocket connection:', err)
|
||||
setError('Failed to establish connection')
|
||||
setConnectionState(WEBSOCKET_STATES.CLOSED)
|
||||
}
|
||||
},
|
||||
[handleWebSocketMessage],
|
||||
)
|
||||
|
||||
const scheduleReconnect = useCallback(() => {
|
||||
if (reconnectTimeoutRef.current) {
|
||||
clearTimeout(reconnectTimeoutRef.current)
|
||||
}
|
||||
|
||||
const attemptIndex = Math.min(
|
||||
reconnectAttemptsRef.current,
|
||||
RECONNECT_INTERVALS.length - 1,
|
||||
)
|
||||
const delay = RECONNECT_INTERVALS[attemptIndex]
|
||||
|
||||
console.log(
|
||||
`Scheduling WebSocket reconnect in ${delay}ms (attempt ${reconnectAttemptsRef.current + 1})`,
|
||||
)
|
||||
|
||||
reconnectTimeoutRef.current = setTimeout(() => {
|
||||
reconnectAttemptsRef.current++
|
||||
// Trigger reconnection
|
||||
const wsUrl = getWebSocketUrl()
|
||||
if (wsUrl && wsRef.current?.readyState !== WEBSOCKET_STATES.OPEN) {
|
||||
createWebSocketConnection(wsUrl)
|
||||
}
|
||||
}, delay)
|
||||
}, [getWebSocketUrl, createWebSocketConnection])
|
||||
|
||||
// Set up the onclose handler separately to avoid circular dependency
|
||||
useEffect(() => {
|
||||
if (wsRef.current) {
|
||||
wsRef.current.onclose = event => {
|
||||
console.log('WebSocket connection closed:', event.code, event.reason)
|
||||
setConnectionState(WEBSOCKET_STATES.CLOSED)
|
||||
|
||||
// Handle different close codes
|
||||
if (event.code === 4000) {
|
||||
setError('Authentication failed - please refresh the page')
|
||||
return // Don't attempt to reconnect for auth failures
|
||||
} else if (event.code === 4001) {
|
||||
setError('Authorization failed - check circle access')
|
||||
return // Don't attempt to reconnect for auth failures
|
||||
}
|
||||
|
||||
// Attempt to reconnect if not manually closed
|
||||
if (!isManuallyClosedRef.current && event.code !== 1000) {
|
||||
scheduleReconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [scheduleReconnect])
|
||||
|
||||
const connect = useCallback(() => {
|
||||
if (wsRef.current?.readyState === WEBSOCKET_STATES.OPEN) {
|
||||
console.log('WebSocket: Already connected')
|
||||
return // Already connected
|
||||
}
|
||||
|
||||
const wsUrl = getWebSocketUrl()
|
||||
console.log('WebSocket connect - URL:', wsUrl)
|
||||
|
||||
if (!wsUrl) {
|
||||
console.log(
|
||||
'Cannot connect to WebSocket: missing URL, token, or user profile',
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
createWebSocketConnection(wsUrl)
|
||||
}, [getWebSocketUrl, createWebSocketConnection])
|
||||
|
||||
const disconnect = useCallback(() => {
|
||||
isManuallyClosedRef.current = true
|
||||
|
||||
if (reconnectTimeoutRef.current) {
|
||||
clearTimeout(reconnectTimeoutRef.current)
|
||||
reconnectTimeoutRef.current = null
|
||||
}
|
||||
|
||||
if (wsRef.current) {
|
||||
wsRef.current.close(1000, 'Manual disconnect')
|
||||
wsRef.current = null
|
||||
}
|
||||
|
||||
setConnectionState(WEBSOCKET_STATES.CLOSED)
|
||||
}, [])
|
||||
|
||||
const toggleWebSocketEnabled = useCallback(
|
||||
enabled => {
|
||||
localStorage.setItem('websocket_enabled', enabled.toString())
|
||||
if (enabled && isTokenValid()) {
|
||||
connect()
|
||||
} else {
|
||||
disconnect()
|
||||
}
|
||||
},
|
||||
[connect, disconnect],
|
||||
)
|
||||
|
||||
const isWebSocketEnabled = useCallback(() => {
|
||||
return localStorage.getItem('websocket_enabled') !== 'false'
|
||||
}, [])
|
||||
|
||||
// Auto-connect when WebSocket is enabled and token is valid
|
||||
useEffect(() => {
|
||||
// Check if WebSocket is enabled in settings
|
||||
const isWebSocketEnabledSetting =
|
||||
localStorage.getItem('websocket_enabled') !== 'false'
|
||||
console.log('WebSocket enabled in settings:', isWebSocketEnabledSetting)
|
||||
|
||||
if (isTokenValid() && isWebSocketEnabledSetting) {
|
||||
console.log('WebSocket: Conditions met, attempting to connect')
|
||||
connect()
|
||||
} else {
|
||||
console.log('WebSocket: Conditions not met, disconnecting')
|
||||
disconnect()
|
||||
}
|
||||
|
||||
// Cleanup on unmount
|
||||
return () => {
|
||||
disconnect()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []) // Only run once on mount
|
||||
|
||||
// Cleanup timeouts on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (reconnectTimeoutRef.current) {
|
||||
clearTimeout(reconnectTimeoutRef.current)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
return {
|
||||
connectionState,
|
||||
isConnected: connectionState === WEBSOCKET_STATES.OPEN,
|
||||
isConnecting: connectionState === WEBSOCKET_STATES.CONNECTING,
|
||||
lastEvent,
|
||||
error,
|
||||
connect,
|
||||
disconnect,
|
||||
toggleWebSocketEnabled,
|
||||
isWebSocketEnabled,
|
||||
// Helper function to check connection status
|
||||
getConnectionStatus: () => {
|
||||
switch (connectionState) {
|
||||
case WEBSOCKET_STATES.CONNECTING:
|
||||
return 'connecting'
|
||||
case WEBSOCKET_STATES.OPEN:
|
||||
return 'connected'
|
||||
case WEBSOCKET_STATES.CLOSING:
|
||||
return 'disconnecting'
|
||||
case WEBSOCKET_STATES.CLOSED:
|
||||
default:
|
||||
return 'disconnected'
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user