137 lines
7.3 KiB
JavaScript
137 lines
7.3 KiB
JavaScript
import React, { useState } from 'react';
|
|
import { network } from '../p2p/index.js';
|
|
|
|
export default function FriendsView({ dms }) {
|
|
const [activeTab, setActiveTab] = useState('pending');
|
|
const [searchUsername, setSearchUsername] = useState('');
|
|
const[searchStatus, setSearchStatus] = useState(''); // 'searching', 'queued', 'found', 'error'
|
|
|
|
const pendingIncoming = Object.entries(dms).filter(([_, data]) => data.status === 'pending_incoming');
|
|
const pendingOutgoing = Object.entries(dms).filter(([_, data]) => data.status === 'pending_outgoing');
|
|
|
|
const handleAddFriend = async (e) => {
|
|
e.preventDefault();
|
|
const target = searchUsername.trim().toLowerCase();
|
|
if (!target) return;
|
|
if (target === network.username) {
|
|
setSearchStatus('error');
|
|
return;
|
|
}
|
|
|
|
setSearchStatus('searching');
|
|
|
|
const result = await network.searchUser(target);
|
|
|
|
if (result) {
|
|
await network.sendDMRequest(result.pubKey, result.profile);
|
|
setSearchStatus('found');
|
|
setSearchUsername('');
|
|
} else {
|
|
await network.queueFriendRequest(target);
|
|
setSearchStatus('queued');
|
|
setSearchUsername('');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="flex-1 flex flex-col bg-base min-w-0">
|
|
<div className="h-14 shadow-sm flex items-center px-4 border-b border-surface gap-6 shrink-0 bg-panel z-10">
|
|
<div className="flex items-center gap-2 text-text font-bold">
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" className="text-muted"><path d="M12 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8zm0 2a9.985 9.985 0 0 0-8 4 9.985 9.985 0 0 0 8 4 9.985 9.985 0 0 0 8-4 9.985 9.985 0 0 0-8-4z"/></svg>
|
|
Contacts
|
|
</div>
|
|
<div className="w-[1px] h-6 bg-surface"></div>
|
|
<div className="flex gap-4">
|
|
<button
|
|
onClick={() => setActiveTab('pending')}
|
|
className={`px-3 py-1 rounded font-medium text-sm transition-colors ${activeTab === 'pending' ? 'bg-surface text-text' : 'text-muted hover:bg-surface/50 hover:text-text'}`}
|
|
>
|
|
Pending {pendingIncoming.length > 0 && <span className="bg-red-500 text-white text-xs px-1.5 rounded-full ml-1">{pendingIncoming.length}</span>}
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveTab('add')}
|
|
className={`px-3 py-1 rounded font-medium text-sm transition-colors ${activeTab === 'add' ? 'bg-accent text-white' : 'bg-accent/20 text-accent hover:bg-accent/30'}`}
|
|
>
|
|
Add Contact
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex-1 p-6 overflow-y-auto">
|
|
{activeTab === 'pending' && (
|
|
<div>
|
|
<h2 className="text-xs font-bold text-muted uppercase mb-4">Pending Requests — {pendingIncoming.length + pendingOutgoing.length}</h2>
|
|
|
|
<div className="space-y-2">
|
|
{pendingIncoming.map(([pubKey, data]) => (
|
|
<div key={pubKey} className="flex items-center justify-between p-3 hover:bg-panel rounded-lg border-t border-surface group transition-colors">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 rounded-md bg-indigo-500 flex items-center justify-center text-white font-bold overflow-hidden">
|
|
{data.profile?.avatar ? <img src={data.profile.avatar} className="w-full h-full object-cover"/> : data.profile?.displayName?.substring(0, 2).toUpperCase()}
|
|
</div>
|
|
<div className="flex flex-col">
|
|
<span className="text-text font-bold">{data.profile?.displayName}</span>
|
|
<span className="text-xs text-muted">@{data.profile?.username} • Incoming Contact Request</span>
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<button onClick={() => network.acceptDMRequest(pubKey)} className="w-9 h-9 rounded-full bg-surface flex items-center justify-center text-green-500 hover:bg-green-500 hover:text-white transition-colors border border-panel" title="Accept">
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{pendingOutgoing.map(([pubKey, data]) => (
|
|
<div key={pubKey} className="flex items-center justify-between p-3 hover:bg-panel rounded-lg border-t border-surface group transition-colors">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 rounded-md bg-surface flex items-center justify-center text-muted font-bold overflow-hidden border border-panel">
|
|
{data.profile?.avatar ? <img src={data.profile.avatar} className="w-full h-full object-cover"/> : data.profile?.displayName?.substring(0, 2).toUpperCase()}
|
|
</div>
|
|
<div className="flex flex-col">
|
|
<span className="text-text font-bold">{data.profile?.displayName}</span>
|
|
<span className="text-xs text-muted">@{data.profile?.username} • Outgoing Contact Request</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{pendingIncoming.length === 0 && pendingOutgoing.length === 0 && (
|
|
<div className="text-center text-muted mt-10">No pending requests.</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'add' && (
|
|
<div className="max-w-2xl">
|
|
<h2 className="text-text font-bold mb-2">ADD CONTACT</h2>
|
|
<p className="text-sm text-muted mb-4">You can add a contact with their username. It's case sensitive!</p>
|
|
|
|
<form onSubmit={handleAddFriend} className="relative flex items-center">
|
|
<input
|
|
type="text"
|
|
value={searchUsername}
|
|
onChange={(e) => setSearchUsername(e.target.value)}
|
|
placeholder="You can add a contact with their username."
|
|
className="w-full bg-panel text-text rounded-lg p-4 pr-40 outline-none focus:ring-1 focus:ring-accent border border-surface"
|
|
/>
|
|
<button
|
|
type="submit"
|
|
disabled={!searchUsername.trim() || searchStatus === 'searching'}
|
|
className="absolute right-2 bg-accent hover:opacity-90 text-white px-4 py-2 rounded text-sm font-medium transition-opacity disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
Send Request
|
|
</button>
|
|
</form>
|
|
|
|
{searchStatus === 'searching' && <p className="text-accent text-sm mt-2">Searching network...</p>}
|
|
{searchStatus === 'found' && <p className="text-green-500 text-sm mt-2">Success! Your contact request was sent.</p>}
|
|
{searchStatus === 'queued' && <p className="text-yellow-500 text-sm mt-2">User is currently offline. We queued your request and will send it automatically when they come online!</p>}
|
|
{searchStatus === 'error' && <p className="text-red-500 text-sm mt-2">You cannot send a contact request to yourself.</p>}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
} |