150 lines
7.1 KiB
JavaScript
150 lines
7.1 KiB
JavaScript
import React, { useState } from 'react';
|
|
import { useQuery } from 'convex/react';
|
|
import { api } from '../../../../convex/_generated/api';
|
|
import Avatar from './Avatar';
|
|
import ColoredIcon from './ColoredIcon';
|
|
import { useOnlineUsers } from '../contexts/PresenceContext';
|
|
import friendsIcon from '../assets/icons/friends.svg';
|
|
|
|
const FriendsView = ({ onOpenDM }) => {
|
|
const [activeTab, setActiveTab] = useState('Online');
|
|
const [addFriendSearch, setAddFriendSearch] = useState('');
|
|
|
|
const myId = localStorage.getItem('userId');
|
|
const { resolveStatus } = useOnlineUsers();
|
|
|
|
const allUsers = useQuery(api.auth.getPublicKeys) || [];
|
|
const users = allUsers.filter(u => u.id !== myId);
|
|
|
|
const getUserColor = (username) => {
|
|
if (!username) return '#747f8d';
|
|
const colors = ['#5865F2', '#EBA7CD', '#57F287', '#FEE75C', '#EB459E', '#ED4245'];
|
|
let hash = 0;
|
|
for (let i = 0; i < username.length; i++) {
|
|
hash = username.charCodeAt(i) + ((hash << 5) - hash);
|
|
}
|
|
return colors[Math.abs(hash) % colors.length];
|
|
};
|
|
|
|
const STATUS_COLORS = {
|
|
online: '#3ba55c',
|
|
idle: '#faa61a',
|
|
dnd: '#ed4245',
|
|
invisible: '#747f8d',
|
|
offline: '#747f8d',
|
|
};
|
|
|
|
const filteredUsers = activeTab === 'Online'
|
|
? users.filter(u => resolveStatus(u.status, u.id) !== 'offline')
|
|
: activeTab === 'Add Friend'
|
|
? users.filter(u => u.username?.toLowerCase().includes(addFriendSearch.toLowerCase()))
|
|
: users;
|
|
|
|
return (
|
|
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, backgroundColor: 'var(--bg-primary)', height: '100vh' }}>
|
|
{/* Top Bar */}
|
|
<div style={{
|
|
height: '48px',
|
|
borderBottom: '1px solid var(--border-subtle)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
padding: '0 16px',
|
|
color: '#fff',
|
|
fontWeight: 'bold',
|
|
flexShrink: 0
|
|
}}>
|
|
<div style={{ display: 'flex', alignItems: 'center', marginRight: '16px', paddingRight: '16px', borderRight: '1px solid var(--border-subtle)' }}>
|
|
<div style={{ marginRight: '12px' }}>
|
|
<ColoredIcon src={friendsIcon} color="var(--interactive-normal)" size="24px" />
|
|
</div>
|
|
Friends
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', gap: '16px', alignItems: 'center' }}>
|
|
{['Online', 'All'].map(tab => (
|
|
<div
|
|
key={tab}
|
|
onClick={() => setActiveTab(tab)}
|
|
className="friends-tab"
|
|
style={{
|
|
cursor: 'pointer',
|
|
color: activeTab === tab ? 'var(--header-primary)' : 'var(--header-secondary)',
|
|
backgroundColor: activeTab === tab ? 'rgba(255,255,255,0.06)' : 'transparent',
|
|
padding: '2px 8px',
|
|
borderRadius: '4px'
|
|
}}
|
|
>
|
|
{tab}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* List Header */}
|
|
<div style={{ padding: '16px 20px 8px' }}>
|
|
<div style={{
|
|
fontSize: '12px',
|
|
fontWeight: 'bold',
|
|
color: 'var(--header-secondary)',
|
|
textTransform: 'uppercase'
|
|
}}>
|
|
{activeTab === 'Add Friend' ? 'USERS' : activeTab} — {filteredUsers.length}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Friends List */}
|
|
<div style={{ flex: 1, overflowY: 'auto', padding: '0 20px' }}>
|
|
{filteredUsers.map(user => {
|
|
const effectiveStatus = resolveStatus(user.status, user.id);
|
|
return (
|
|
<div
|
|
key={user.id}
|
|
className="friend-item"
|
|
>
|
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
|
<div style={{ position: 'relative', marginRight: '12px' }}>
|
|
<Avatar username={user.username} avatarUrl={user.avatarUrl} size={32} />
|
|
<div style={{
|
|
position: 'absolute', bottom: -2, right: -2,
|
|
width: 10, height: 10, borderRadius: '50%',
|
|
backgroundColor: STATUS_COLORS[effectiveStatus] || STATUS_COLORS.offline,
|
|
border: '2px solid var(--bg-primary)'
|
|
}} />
|
|
</div>
|
|
<div>
|
|
<div style={{ color: 'var(--header-primary)', fontWeight: '600' }}>
|
|
{user.username ?? 'Unknown'}
|
|
</div>
|
|
<div style={{ color: 'var(--header-secondary)', fontSize: '12px' }}>
|
|
{effectiveStatus === 'dnd' ? 'Do Not Disturb' : effectiveStatus.charAt(0).toUpperCase() + effectiveStatus.slice(1)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', gap: '8px' }}>
|
|
<div
|
|
className="friend-action-btn"
|
|
onClick={() => onOpenDM && onOpenDM(user.id, user.username)}
|
|
>
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M4.79805 3C3.80445 3 2.99805 3.8055 2.99805 4.8V15.6C2.99805 16.5936 3.80445 17.4 4.79805 17.4H8.39805L11.998 21L15.598 17.4H19.198C20.1925 17.4 20.998 16.5936 20.998 15.6V4.8C20.998 3.8055 20.1925 3 19.198 3H4.79805Z" />
|
|
</svg>
|
|
</div>
|
|
<div className="friend-action-btn">
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M12 16C13.1046 16 14 15.1046 14 14C14 12.8954 13.1046 12 12 12C10.8954 12 10 12.8954 10 14C10 15.1046 10.8954 16 12 16Z" />
|
|
<path d="M12 10C13.1046 10 14 9.10457 14 8C14 6.89543 13.1046 6 12 6C10.8954 6 10 6.89543 10 8C10 9.10457 10.8954 10 12 10Z" />
|
|
<path d="M12 22C13.1046 22 14 21.1046 14 20C14 18.8954 13.1046 18 12 18C10.8954 18 10 18.8954 10 20C10 21.1046 10.8954 22 12 22Z" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default FriendsView;
|