Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions backend/api/chatHash/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
25 changes: 23 additions & 2 deletions client/src/components/ChatContainer/ChatHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@
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 {
onStartCall: () => void;
}

export const ChatHeader: React.FC<ChatHeaderProps> = ({ onStartCall }) => {
const { isConnected, channelHash } = useChat();
const { isConnected, channelHash, deleteChannel } = useChat();
const [hashCopied, setHashCopied] = useState(false);

const handleCopyHash = () => {
Expand All @@ -36,6 +36,18 @@ export const ChatHeader: React.FC<ChatHeaderProps> = ({ 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 (
<header className={`chat-header glass ${isConnected ? 'active' : ''}`}>
<div className="header-info">
Expand Down Expand Up @@ -81,6 +93,15 @@ export const ChatHeader: React.FC<ChatHeaderProps> = ({ onStartCall }) => {
>
<PhoneIcon size={20} />
</Button>
<Button
className="btn--icon"
variant="danger"
onClick={handleDelete}
title="Delete Chat"
disabled={!channelHash}
>
<TrashIcon size={20} />
</Button>
</div>
</header>
);
Expand Down
21 changes: 18 additions & 3 deletions client/src/components/SetupOverlay/SetupOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
isHidden: boolean;
}

type ViewType = 'initial' | 'create' | 'join';
type ViewType = 'initial' | 'create' | 'join' | 'deleted';

export const SetupOverlay: React.FC<SetupOverlayProps> = ({ onSetupComplete, isHidden }) => {
const { createNewChannel } = useChat();
Expand Down Expand Up @@ -88,8 +88,13 @@
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);
Expand Down Expand Up @@ -127,6 +132,16 @@
/>
)}

{view === 'deleted' && (
<div style={{ textAlign: 'center', margin: '2rem 0' }}>
<h2 style={{ color: '#ef4444', marginBottom: '1rem' }}>Channel Deleted</h2>
<p style={{ opacity: 0.8, marginBottom: '2rem' }}>This secure channel has been permanently deleted and can no longer be accessed.</p>
<button className="btn btn--primary" onClick={handleBack}>
Return Home
</button>

Check warning on line 141 in client/src/components/SetupOverlay/SetupOverlay.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add an explicit "type" attribute to this button.

See more on https://sonarcloud.io/project/issues?id=muke1908_chat-e2ee&issues=AZ-ETL6K-k2f51dfHEx5&open=AZ-ETL6K-k2f51dfHEx5&pullRequest=471
</div>
)}

{status && <div className="setup-status">{status}</div>}
</div>
</div>
Expand Down
17 changes: 17 additions & 0 deletions client/src/components/common/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,20 @@ export const EndCallIcon: React.FC<IconProps> = ({ size = 24, className = '' })
<path d="M10.68 13.31a16 16 0 0 0 3.41 2.6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7 2 2 0 0 1 1.72 2v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 3.41 2.6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path>
</svg>
);

export const TrashIcon: React.FC<IconProps> = ({ size = 24, className = '' }) => (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
width={size}
height={size}
className={className}
>
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
);
25 changes: 25 additions & 0 deletions client/src/context/ChatContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand All @@ -199,6 +223,7 @@ export const ChatProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
endCall,
addMessage,
setCallDuration,
deleteChannel,
};

return <ChatContext.Provider value={value}>{children}</ChatContext.Provider>;
Expand Down
1 change: 1 addition & 0 deletions client/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export interface ChatContextType {
endCall: () => Promise<void>;
addMessage: (message: Message) => void;
setCallDuration: (duration: number) => void;
deleteChannel: () => Promise<void>;
}

// Common component props
Expand Down
Loading