diff --git a/backend/api/chatHash/index.ts b/backend/api/chatHash/index.ts index 5b63feba..4f541d9a 100644 --- a/backend/api/chatHash/index.ts +++ b/backend/api/chatHash/index.ts @@ -50,13 +50,16 @@ router.get( "/status/:channel", asyncHandler(async (req, res) => { const { channel } = req.params; - const { valid } = await channelValid(channel); + const { valid, state } = await channelValid(channel); if (!valid) { - return res.sendStatus(404).send("Invalid channel"); + if (state === CHANNEL_STATE.DELETED) { + return res.status(410).send({ error: "Channel deleted", state }); + } + return res.status(404).send({ error: "Invalid channel", state }); } - return res.send({ status: "ok" }); + return res.send({ status: "ok", state }); }) ); router.delete( diff --git a/client/src/components/ChatContainer/ChatHeader.tsx b/client/src/components/ChatContainer/ChatHeader.tsx index 43a8b3bd..e58ea5ba 100644 --- a/client/src/components/ChatContainer/ChatHeader.tsx +++ b/client/src/components/ChatContainer/ChatHeader.tsx @@ -5,7 +5,7 @@ import React, { useState } from 'react'; import { useChat } from '../../context/ChatContext'; import { Button } from '../common/Button'; -import { CopyIcon, ShareIcon, PhoneIcon } from '../common/icons'; +import { CopyIcon, ShareIcon, PhoneIcon, TrashIcon } from '../common/icons'; import './ChatHeader.css'; interface ChatHeaderProps { @@ -13,7 +13,7 @@ interface ChatHeaderProps { } export const ChatHeader: React.FC = ({ onStartCall }) => { - const { isConnected, channelHash } = useChat(); + const { isConnected, channelHash, deleteChannel } = useChat(); const [hashCopied, setHashCopied] = useState(false); const handleCopyHash = () => { @@ -36,6 +36,18 @@ export const ChatHeader: React.FC = ({ onStartCall }) => { } }; +const handleDelete = async () => { + if (!window.confirm('Are you sure you want to delete this secure channel? This cannot be undone.')) return; + + try { + await deleteChannel(); + window.location.hash = ''; // Clear URL hash + } catch (err) { + console.error('Failed to delete channel:', err); + alert((err as any).message || 'Failed to delete channel'); + } +}; + return (
@@ -81,6 +93,15 @@ export const ChatHeader: React.FC = ({ onStartCall }) => { > +
); diff --git a/client/src/components/SetupOverlay/SetupOverlay.tsx b/client/src/components/SetupOverlay/SetupOverlay.tsx index cff469fe..89dc2a0a 100644 --- a/client/src/components/SetupOverlay/SetupOverlay.tsx +++ b/client/src/components/SetupOverlay/SetupOverlay.tsx @@ -14,7 +14,7 @@ interface SetupOverlayProps { isHidden: boolean; } -type ViewType = 'initial' | 'create' | 'join'; +type ViewType = 'initial' | 'create' | 'join' | 'deleted'; export const SetupOverlay: React.FC = ({ onSetupComplete, isHidden }) => { const { createNewChannel } = useChat(); @@ -88,8 +88,13 @@ export const SetupOverlay: React.FC = ({ onSetupComplete, isH setIsLoading(true); setStatus('Connecting...'); await onSetupComplete(joinHash); - } catch (err) { - setStatus('Failed to join channel. Please check the hash and try again.'); + } catch (err: any) { + if (err.message === 'CHANNEL_DELETED') { + setView('deleted'); + setStatus(''); + } else { + setStatus('Failed to join channel. Please check the hash and try again.'); + } console.error('Join error:', err); } finally { setIsLoading(false); @@ -127,6 +132,16 @@ export const SetupOverlay: React.FC = ({ onSetupComplete, isH /> )} + {view === 'deleted' && ( +
+

Channel Deleted

+

This secure channel has been permanently deleted and can no longer be accessed.

+ +
+ )} + {status &&
{status}
} diff --git a/client/src/components/common/icons.tsx b/client/src/components/common/icons.tsx index c4a6cbaa..35289fa5 100644 --- a/client/src/components/common/icons.tsx +++ b/client/src/components/common/icons.tsx @@ -94,3 +94,20 @@ export const EndCallIcon: React.FC = ({ size = 24, className = '' }) ); + +export const TrashIcon: React.FC = ({ size = 24, className = '' }) => ( + + + + +); diff --git a/client/src/context/ChatContext.tsx b/client/src/context/ChatContext.tsx index 09e4043c..bb06da4c 100644 --- a/client/src/context/ChatContext.tsx +++ b/client/src/context/ChatContext.tsx @@ -57,6 +57,16 @@ export const ChatProvider: React.FC<{ children: ReactNode }> = ({ children }) => async (hash: string) => { if (!chat) throw new Error('Chat not initialized'); try { + // Check for channel status before joining + const baseUrl = process.env.CHATE2EE_API_URL || 'http://localhost:3001'; + const statusRes = await fetch(`${baseUrl}/api/chat-link/status/${encodeURIComponent(hash)}`); + if (statusRes.status === 410) { + throw new Error('CHANNEL_DELETED'); + } + if (!statusRes.ok) { + throw new Error('Failed to verify channel status'); + } + // Auto-generate User ID const newUserId = (utils as any).generateUUID(); setUserId(newUserId); @@ -181,6 +191,20 @@ export const ChatProvider: React.FC<{ children: ReactNode }> = ({ children }) => } }; + // Delete channel + const deleteChannel = useCallback(async () => { + if (!chat) return; + try { + await chat.delete(); + setIsConnected(false); + setChannelHash(''); + setMessages([]); + } catch (err) { + console.error('Failed to delete channel:', err); + throw err; + } + }, [chat]); + const value: ChatContextType = { chat, userId, @@ -199,6 +223,7 @@ export const ChatProvider: React.FC<{ children: ReactNode }> = ({ children }) => endCall, addMessage, setCallDuration, + deleteChannel, }; return {children}; diff --git a/client/src/types/index.ts b/client/src/types/index.ts index 8380d016..b908d065 100644 --- a/client/src/types/index.ts +++ b/client/src/types/index.ts @@ -49,6 +49,7 @@ export interface ChatContextType { endCall: () => Promise; addMessage: (message: Message) => void; setCallDuration: (duration: number) => void; + deleteChannel: () => Promise; } // Common component props