fix: repair invite links, social features, messaging + Discord UI overhaul

Phase 1 - Feature Repair:
- Fix member kick/leave: add missing db.delete() call in servers.ts
- Stabilize invite codes: return existing code instead of regenerating
- Fix user search: use LIKE instead of exact match in social.ts
- Wire DM button on FriendsPage to create/navigate to DM channels
- Add cancel outgoing friend request (DELETE endpoint + frontend)
- Add accept/decline friend request actions with WS real-time events
- Fix replyToId persistence in message creation
- Hydrate reactions and replyTo in message queries
- Add joinByCode to API client and serverStore
- Add friend_request_received/accepted WebSocket events

Phase 2 - Discord UI Overhaul:
- Remove stray borders between layout columns
- Replace shadow-sm with shadow-header on content headers
- Replace all bg-gray-*/text-gray-* with Discord color tokens
- Ensure flat color contrast (#1E1F22, #2B2D31, #313338)

Testing:
- Set up vitest + @testing-library/react + jsdom
- Add 17 tests across InviteModal, JoinServer, FriendsPage (all passing)
- Fix vite resolve.extensions to prefer .tsx over stale .js files
This commit is contained in:
Jannis Braun
2026-02-18 05:34:45 +01:00
parent 4fd17084a5
commit 5ef502f2e3
82 changed files with 4906 additions and 552 deletions
@@ -0,0 +1,79 @@
import React, { useState, useEffect } from 'react';
import { api } from '../../api/client';
interface EmbedProps {
url: string;
}
interface Metadata {
title?: string;
description?: string;
image?: string;
siteName?: string;
}
export function Embed({ url }: EmbedProps) {
const [metadata, setMetadata] = useState<Metadata | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
let isMounted = true;
// Simple fetch from our new API
fetch(`/api/utils/metadata?url=${encodeURIComponent(url)}`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('opencord_token')}`
}
})
.then(res => res.json())
.then(data => {
if (isMounted && data.title) {
setMetadata(data);
}
setIsLoading(false);
})
.catch(() => {
if (isMounted) setIsLoading(false);
});
return () => { isMounted = false; };
}, [url]);
if (isLoading || !metadata) return null;
return (
<div className="mt-2 max-w-[520px] bg-discord-bg-secondary rounded-[4px] border-l-4 border-discord-bg-tertiary flex overflow-hidden">
<div className="flex-1 p-3 min-w-0">
{metadata.siteName && (
<div className="text-[12px] text-discord-text-normal font-medium mb-1 truncate">
{metadata.siteName}
</div>
)}
{metadata.title && (
<a
href={url}
target="_blank"
rel="noopener noreferrer"
className="text-[16px] text-discord-text-link font-semibold hover:underline block mb-2"
>
{metadata.title}
</a>
)}
{metadata.description && (
<div className="text-[14px] text-discord-text-normal leading-[1.125rem]">
{metadata.description}
</div>
)}
</div>
{metadata.image && (
<div className="w-[80px] h-[80px] m-3 flex-shrink-0">
<img
src={metadata.image}
alt=""
className="w-full h-full object-cover rounded-[4px]"
/>
</div>
)}
</div>
);
}