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
+90 -3
View File
@@ -30,6 +30,7 @@ export interface Server {
export interface ServerWithChannelsAndMembers extends Server {
channels: Channel[];
members: MemberWithUser[];
roles: Role[];
}
// ─── Member Types ───────────────────────────────────────────────────────────
@@ -46,6 +47,30 @@ export interface Member {
export interface MemberWithUser extends Member {
user: User;
roles: Role[];
}
// ─── Role Types ─────────────────────────────────────────────────────────────
export interface Role {
id: string;
serverId: string;
name: string;
color: string;
position: number;
permissions?: string[];
createdAt: number;
}
// ─── Folder Types ───────────────────────────────────────────────────────────
export interface ServerFolder {
id: string;
userId: string;
name: string | null;
color: string | null;
position: number;
serverIds: string[];
}
// ─── Channel Types ──────────────────────────────────────────────────────────
@@ -68,6 +93,7 @@ export interface Message {
id: string;
channelId: string;
userId: string;
replyToId: string | null;
content: string | null;
editedAt: number | null;
createdAt: number;
@@ -76,6 +102,19 @@ export interface Message {
export interface MessageWithUser extends Message {
user: User;
attachments: Attachment[];
reactions: Reaction[];
replyTo?: MessageWithUser | null;
}
// ─── Reaction Types ────────────────────────────────────────────────────────
export interface Reaction {
id: string;
messageId: string;
userId: string;
emoji: string;
createdAt: number;
user?: User;
}
// ─── Attachment Types ───────────────────────────────────────────────────────
@@ -105,10 +144,17 @@ export interface DmMessage {
userId: string;
content: string | null;
createdAt: number;
// Compatibility fields
channelId?: string;
replyToId?: string | null;
editedAt?: number | null;
}
export interface DmMessageWithUser extends DmMessage {
user: User;
attachments?: Attachment[];
reactions?: Reaction[];
replyTo?: MessageWithUser | null;
}
// ─── WebSocket Event Types ──────────────────────────────────────────────────
@@ -116,18 +162,20 @@ export interface DmMessageWithUser extends DmMessage {
// Client → Server Events
export type ClientEvent =
| { type: 'auth'; token: string }
| { type: 'message_create'; channelId: string; content: string }
| { type: 'message_create'; channelId: string; content: string; replyToId?: string }
| { type: 'message_edit'; messageId: string; content: string }
| { type: 'message_delete'; messageId: string }
| { type: 'typing_start'; channelId: string }
| { type: 'presence_update'; status: 'online' | 'idle' | 'dnd' }
| { type: 'voice_join'; channelId: string }
| { type: 'voice_leave' }
| { type: 'dm_message_create'; dmChannelId: string; content: string };
| { type: 'dm_message_create'; dmChannelId: string; content: string }
| { type: 'reaction_add'; messageId: string; emoji: string }
| { type: 'reaction_remove'; messageId: string; emoji: string };
// Server → Client Events
export type ServerEvent =
| { type: 'ready'; user: User; servers: ServerWithChannelsAndMembers[]; dmChannels: DmChannel[] }
| { type: 'ready'; user: User; servers: ServerWithChannelsAndMembers[]; dmChannels: DmChannel[]; folders?: ServerFolder[] }
| { type: 'message_created'; message: MessageWithUser }
| { type: 'message_updated'; message: MessageWithUser }
| { type: 'message_deleted'; messageId: string; channelId: string }
@@ -137,6 +185,10 @@ export type ServerEvent =
| { type: 'member_joined'; serverId: string; member: MemberWithUser }
| { type: 'member_left'; serverId: string; userId: string }
| { type: 'dm_message_created'; message: DmMessageWithUser }
| { type: 'reaction_added'; messageId: string; reaction: Reaction }
| { type: 'reaction_removed'; messageId: string; userId: string; emoji: string }
| { type: 'friend_request_received'; request: FriendRequest }
| { type: 'friend_request_accepted'; friend: Friend; requestId: string }
| { type: 'error'; message: string };
// ─── API Request/Response Types ─────────────────────────────────────────────
@@ -183,6 +235,7 @@ export interface UpdateUserRequest {
displayName?: string;
avatar?: string;
customStatus?: string;
status?: UserStatus;
}
export interface UpdateMemberRequest {
@@ -192,6 +245,7 @@ export interface UpdateMemberRequest {
export interface CreateMessageRequest {
content: string;
attachments?: string[];
replyToId?: string;
}
export interface UpdateMessageRequest {
@@ -208,6 +262,7 @@ export interface LiveKitTokenRequest {
export interface LiveKitTokenResponse {
token: string;
url: string;
}
export interface CreateDmRequest {
@@ -227,3 +282,35 @@ export interface ApiError {
error: string;
statusCode: number;
}
// ─── Social Types ────────────────────────────────────────────────────────────
export interface Friend {
id: string;
username: string;
displayName: string | null;
avatar: string | null;
status: UserStatus;
customStatus: string | null;
createdAt: number;
addedAt: number;
}
export type FriendRequestStatus = 'pending' | 'accepted' | 'declined';
export interface FriendRequest {
id: string;
fromId: string;
toId: string;
status: FriendRequestStatus;
createdAt: number;
user?: User; // The other user (if it's an incoming request, the sender; if outgoing, the recipient)
}
export interface SendFriendRequest {
username: string;
}
export interface UpdateFriendRequest {
status: 'accepted' | 'declined';
}