mirror of
https://github.com/xtr-dev/rondevu-demo.git
synced 2025-12-16 13:03:24 +00:00
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:
201
src/App.jsx
201
src/App.jsx
@@ -90,6 +90,7 @@ export default function App() {
|
|||||||
const [activeChats, setActiveChats] = useState({});
|
const [activeChats, setActiveChats] = useState({});
|
||||||
const [selectedChat, setSelectedChat] = useState(null);
|
const [selectedChat, setSelectedChat] = useState(null);
|
||||||
const [messageInputs, setMessageInputs] = useState({});
|
const [messageInputs, setMessageInputs] = useState({});
|
||||||
|
const [pendingRequests, setPendingRequests] = useState({}); // Pending incoming connection requests
|
||||||
|
|
||||||
// Service - we publish one service that can accept multiple connections
|
// Service - we publish one service that can accept multiple connections
|
||||||
const [myServicePublished, setMyServicePublished] = useState(false);
|
const [myServicePublished, setMyServicePublished] = useState(false);
|
||||||
@@ -508,29 +509,24 @@ export default function App() {
|
|||||||
if (msg.type === 'identify') {
|
if (msg.type === 'identify') {
|
||||||
// Peer identified themselves
|
// Peer identified themselves
|
||||||
peerUsername = msg.from;
|
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,
|
...prev,
|
||||||
[peerUsername]: {
|
[peerUsername]: {
|
||||||
username: peerUsername,
|
username: peerUsername,
|
||||||
channel: dc,
|
channel: dc,
|
||||||
connection: pc,
|
connection: pc,
|
||||||
messages: prev[peerUsername]?.messages || [],
|
timestamp: Date.now()
|
||||||
status: 'connected',
|
|
||||||
role: 'host'
|
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Auto-select the incoming chat to show it immediately
|
// Show notification
|
||||||
setSelectedChat(peerUsername);
|
toast(`${peerUsername} wants to chat with you`, {
|
||||||
toast.success(`${peerUsername} connected to you!`);
|
duration: 5000,
|
||||||
|
icon: '👤'
|
||||||
// Send acknowledgment
|
});
|
||||||
dc.send(JSON.stringify({
|
|
||||||
type: 'identify_ack',
|
|
||||||
from: hostUsername
|
|
||||||
}));
|
|
||||||
} else if (msg.type === 'message' && peerUsername) {
|
} else if (msg.type === 'message' && peerUsername) {
|
||||||
// Chat message
|
// Chat message
|
||||||
setActiveChats(prev => ({
|
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
|
// Send message
|
||||||
const handleSendMessage = (contact) => {
|
const handleSendMessage = (contact) => {
|
||||||
const text = messageInputs[contact];
|
const text = messageInputs[contact];
|
||||||
@@ -977,11 +1036,81 @@ export default function App() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</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) */}
|
{/* Incoming Chats (not in contacts) */}
|
||||||
{Object.keys(activeChats).filter(username => !contacts.includes(username) && activeChats[username].status === 'connected').length > 0 && (
|
{Object.keys(activeChats).filter(username => !contacts.includes(username) && activeChats[username].status === 'connected').length > 0 && (
|
||||||
<div style={styles.contactsList}>
|
<div style={styles.contactsList}>
|
||||||
<div style={styles.contactsHeader}>
|
<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>
|
</div>
|
||||||
{Object.keys(activeChats)
|
{Object.keys(activeChats)
|
||||||
.filter(username => !contacts.includes(username) && activeChats[username].status === 'connected')
|
.filter(username => !contacts.includes(username) && activeChats[username].status === 'connected')
|
||||||
@@ -1005,7 +1134,7 @@ export default function App() {
|
|||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<div style={styles.contactName}>{contact}</div>
|
<div style={styles.contactName}>{contact}</div>
|
||||||
<div style={styles.contactStatus}>
|
<div style={styles.contactStatus}>
|
||||||
Connected (incoming)
|
Connected
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1053,15 +1182,51 @@ export default function App() {
|
|||||||
{hasActiveChat ? 'Chatting' : isOnline ? 'Online' : 'Offline'}
|
{hasActiveChat ? 'Chatting' : isOnline ? 'Online' : 'Offline'}
|
||||||
</div>
|
</div>
|
||||||
</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
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleRemoveContact(contact);
|
if (window.confirm(`Remove ${contact} from your friends list?`)) {
|
||||||
|
handleRemoveContact(contact);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
style={styles.removeBtn}
|
style={styles.removeBtn}
|
||||||
title="Remove friend"
|
title="Remove friend"
|
||||||
>
|
>
|
||||||
✕
|
🗑
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user