feat: Add new emoji assets and an UpdateBanner component.
Some checks failed
Build and Release / build-and-release (push) Failing after 3m28s

This commit is contained in:
Bryan1029384756
2026-02-13 12:20:40 -06:00
parent 63d4208933
commit fe869a3222
3855 changed files with 10226 additions and 15543 deletions

View File

@@ -0,0 +1,167 @@
import React, { useState } from 'react';
import { useQuery } from 'convex/react';
import { api } from '../../../../convex/_generated/api';
import Avatar from './Avatar';
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' }}>
<div style={{
width: 24,
height: 24,
overflow: 'hidden',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0
}}>
<img
src={friendsIcon}
alt=""
style={{
width: 24,
height: 24,
transform: 'translateX(-1000px)',
filter: `drop-shadow(1000px 0 0 var(--interactive-normal))`
}}
/>
</div>
</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;