Add connection request approval flow and improve friends list UX

Connection Request Approval:
- Add 'Connection Requests' section showing pending incoming connections
- Host must explicitly accept or deny incoming connection requests
- Shows username before accepting (no auto-accept)
- Accept: moves to active chats and sends acknowledgment
- Decline: closes connection and removes from pending
- Toast notification when someone wants to chat

Friends List UX Improvements:
- Add ⏸ (pause) button to close active chat without removing friend
- Change ✕ to 🗑 (trash) icon for removing friends
- Add confirmation dialog before removing a friend
- Separate 'close chat' from 'remove friend' actions
- Clearer visual distinction between actions

This prevents accidental friend removal and gives hosts control over
who they connect with before establishing the chat.
This commit is contained in:
2025-12-10 20:36:27 +01:00
parent a08dd1dccc
commit 55e197a5c5

View File

@@ -90,6 +90,7 @@ export default function App() {
const [activeChats, setActiveChats] = useState({});
const [selectedChat, setSelectedChat] = useState(null);
const [messageInputs, setMessageInputs] = useState({});
const [pendingRequests, setPendingRequests] = useState({}); // Pending incoming connection requests
// Service - we publish one service that can accept multiple connections
const [myServicePublished, setMyServicePublished] = useState(false);
@@ -508,29 +509,24 @@ export default function App() {
if (msg.type === 'identify') {
// Peer identified themselves
peerUsername = msg.from;
console.log(`📡 New chat connection from: ${peerUsername}`);
console.log(`📡 New connection request from: ${peerUsername}`);
setActiveChats(prev => ({
// Add to pending requests for approval
setPendingRequests(prev => ({
...prev,
[peerUsername]: {
username: peerUsername,
channel: dc,
connection: pc,
messages: prev[peerUsername]?.messages || [],
status: 'connected',
role: 'host'
timestamp: Date.now()
}
}));
// Auto-select the incoming chat to show it immediately
setSelectedChat(peerUsername);
toast.success(`${peerUsername} connected to you!`);
// Send acknowledgment
dc.send(JSON.stringify({
type: 'identify_ack',
from: hostUsername
}));
// Show notification
toast(`${peerUsername} wants to chat with you`, {
duration: 5000,
icon: '👤'
});
} else if (msg.type === 'message' && peerUsername) {
// Chat message
setActiveChats(prev => ({
@@ -785,6 +781,69 @@ export default function App() {
}));
};
// Accept incoming connection request
const handleAcceptRequest = (username) => {
const request = pendingRequests[username];
if (!request) return;
console.log(`✅ Accepting connection request from: ${username}`);
// Move from pending to active chats
setActiveChats(prev => ({
...prev,
[username]: {
username: username,
channel: request.channel,
connection: request.connection,
messages: prev[username]?.messages || [],
status: 'connected',
role: 'host'
}
}));
// Remove from pending
setPendingRequests(prev => {
const updated = { ...prev };
delete updated[username];
return updated;
});
// Send acknowledgment
request.channel.send(JSON.stringify({
type: 'identify_ack',
from: myUsername
}));
// Auto-select the chat
setSelectedChat(username);
toast.success(`Connected with ${username}!`);
};
// Deny incoming connection request
const handleDenyRequest = (username) => {
const request = pendingRequests[username];
if (!request) return;
console.log(`❌ Denying connection request from: ${username}`);
// Close the connection
try {
request.channel.close();
request.connection.close();
} catch (err) {
console.error('Error closing connection:', err);
}
// Remove from pending
setPendingRequests(prev => {
const updated = { ...prev };
delete updated[username];
return updated;
});
toast.success(`Declined request from ${username}`);
};
// Send message
const handleSendMessage = (contact) => {
const text = messageInputs[contact];
@@ -977,11 +1036,81 @@ export default function App() {
</button>
</div>
{/* Pending Connection Requests */}
{Object.keys(pendingRequests).length > 0 && (
<div style={styles.contactsList}>
<div style={{...styles.contactsHeader, background: '#ff9800', color: 'white'}}>
Connection Requests ({Object.keys(pendingRequests).length})
</div>
{Object.keys(pendingRequests).map(username => (
<div
key={username}
style={{
...styles.contactItem,
flexDirection: 'column',
alignItems: 'stretch',
padding: '12px'
}}
>
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '8px' }}>
<div style={styles.contactAvatar}>
{username[0].toUpperCase()}
<span style={{
...styles.contactDot,
background: '#ff9800'
}}></span>
</div>
<div style={{ flex: 1 }}>
<div style={styles.contactName}>{username}</div>
<div style={styles.contactStatus}>
Wants to chat
</div>
</div>
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<button
onClick={() => handleAcceptRequest(username)}
style={{
flex: 1,
padding: '8px',
background: '#4caf50',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: 'pointer',
fontSize: '13px',
fontWeight: '500'
}}
>
Accept
</button>
<button
onClick={() => handleDenyRequest(username)}
style={{
flex: 1,
padding: '8px',
background: '#f44336',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: 'pointer',
fontSize: '13px',
fontWeight: '500'
}}
>
Decline
</button>
</div>
</div>
))}
</div>
)}
{/* Incoming Chats (not in contacts) */}
{Object.keys(activeChats).filter(username => !contacts.includes(username) && activeChats[username].status === 'connected').length > 0 && (
<div style={styles.contactsList}>
<div style={styles.contactsHeader}>
Incoming Chats ({Object.keys(activeChats).filter(username => !contacts.includes(username) && activeChats[username].status === 'connected').length})
Active Chats ({Object.keys(activeChats).filter(username => !contacts.includes(username) && activeChats[username].status === 'connected').length})
</div>
{Object.keys(activeChats)
.filter(username => !contacts.includes(username) && activeChats[username].status === 'connected')
@@ -1005,7 +1134,7 @@ export default function App() {
<div style={{ flex: 1 }}>
<div style={styles.contactName}>{contact}</div>
<div style={styles.contactStatus}>
Connected (incoming)
Connected
</div>
</div>
</div>
@@ -1053,15 +1182,51 @@ export default function App() {
{hasActiveChat ? 'Chatting' : isOnline ? 'Online' : 'Offline'}
</div>
</div>
{hasActiveChat && (
<button
onClick={(e) => {
e.stopPropagation();
// Close the active chat
const chat = activeChats[contact];
if (chat) {
try {
chat.channel?.close();
chat.connection?.close();
} catch (err) {
console.error('Error closing chat:', err);
}
setActiveChats(prev => {
const updated = { ...prev };
delete updated[contact];
return updated;
});
if (selectedChat === contact) {
setSelectedChat(null);
}
toast.success(`Chat with ${contact} closed`);
}
}}
style={{
...styles.removeBtn,
background: '#ff9800',
marginRight: '4px'
}}
title="Close chat"
>
</button>
)}
<button
onClick={(e) => {
e.stopPropagation();
handleRemoveContact(contact);
if (window.confirm(`Remove ${contact} from your friends list?`)) {
handleRemoveContact(contact);
}
}}
style={styles.removeBtn}
title="Remove friend"
>
🗑
</button>
</div>
);