import React, { useState } from 'react'; import type { MessageWithUser } from '@backspace/shared'; import { MarkdownRenderer } from './MarkdownRenderer'; import { Avatar } from '../ui/Avatar'; import { ContextMenu } from '../ui/ContextMenu'; import { useAuthStore } from '../../stores/authStore'; import { useChatStore } from '../../stores/chatStore'; import { useServerStore } from '../../stores/serverStore'; import { useUIStore } from '../../stores/uiStore'; import { Embed } from './Embed'; import { hasPermissionBit, PermissionBits } from '../../utils/permissions'; interface MessageProps { message: MessageWithUser; isCompact: boolean; isFirstInGroup: boolean; } function formatTime(timestamp: number): string { const date = new Date(timestamp); const now = new Date(); const isToday = date.toDateString() === now.toDateString(); const yesterday = new Date(now); yesterday.setDate(yesterday.getDate() - 1); const isYesterday = date.toDateString() === yesterday.toDateString(); const time = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); if (isToday) return `Today at ${time}`; if (isYesterday) return `Yesterday at ${time}`; return `${date.toLocaleDateString()} ${time}`; } function formatHoverTime(timestamp: number): string { return new Date(timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } export function Message({ message, isCompact, isFirstInGroup }: MessageProps) { const [isEditing, setIsEditing] = useState(false); const [editContent, setEditContent] = useState(message.content ?? ''); const [isHovered, setIsHovered] = useState(false); const currentUser = useAuthStore((s) => s.user); const editMessage = useChatStore((s) => s.editMessage); const deleteMessage = useChatStore((s) => s.deleteMessage); const members = useServerStore((s) => s.members); const openImagePreview = useUIStore((s) => s.openImagePreview); const openUserProfile = useUIStore((s) => s.openUserProfile); const channelKey = message.channelId || (message as any).dmChannelId; const isAuthor = currentUser?.id === message.userId; const channelPermissions = useServerStore((s) => s.channelPermissions); const myChPerms = channelPermissions.get(message.channelId); const canManageMessages = hasPermissionBit(myChPerms, PermissionBits.MANAGE_MESSAGES); const canDelete = isAuthor || canManageMessages; const addReaction = useChatStore((s) => s.addReaction); const removeReaction = useChatStore((s) => s.removeReaction); const setReplyTo = useChatStore((s) => s.setReplyTo); const toggleReaction = (emoji: string) => { const hasReacted = message.reactions?.some(r => r.userId === currentUser?.id && r.emoji === emoji); if (hasReacted) { removeReaction(message.id, emoji); } else { addReaction(message.id, emoji); } }; const reactionGroups = (message.reactions || []).reduce((acc, r) => { const group = acc[r.emoji] || { count: 0, me: false }; group.count++; if (r.userId === currentUser?.id) { group.me = true; } acc[r.emoji] = group; return acc; }, {} as Record); const urlRegex = /(https?:\/\/[^\s]+)/g; const firstUrl = message.content?.match(urlRegex)?.[0]; const handleUsernameClick = (e: React.MouseEvent) => { if (!message.user) return; e.stopPropagation(); const rect = e.currentTarget.getBoundingClientRect(); openUserProfile(message.user, { top: Math.min(rect.top, window.innerHeight - 450), left: rect.right + 16, }); }; const contextMenuItems = []; if (isAuthor) { contextMenuItems.push({ label: 'Edit Message', onClick: () => { setEditContent(message.content ?? ''); setIsEditing(true); }, }); } if (canDelete) { contextMenuItems.push({ label: 'Delete Message', onClick: () => deleteMessage(message.id, channelKey), danger: true, }); } const handleEditSubmit = async (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); if (editContent.trim()) { await editMessage(message.id, editContent.trim(), channelKey); setIsEditing(false); } } if (e.key === 'Escape') { setIsEditing(false); setEditContent(message.content ?? ''); } }; const displayName = message.user.displayName ?? message.user.username; const servers = useServerStore((s) => s.servers); const currentServerId = useServerStore((s) => s.currentServerId); const ownerId = servers.find(s => s.id === currentServerId)?.ownerId; const getMemberDisplayColor = (userId: string) => { const member = members.find(m => m.userId === userId); if (member?.roles && member.roles.length > 0) { const sorted = [...member.roles].sort((a, b) => b.position - a.position); return { color: sorted[0]!.color }; } if (ownerId && userId === ownerId) return { color: '#fda4af' }; return { color: '#d8d8de' }; }; const roleColor = getMemberDisplayColor(message.userId); const replyRoleColor = (msg: { userId: string }) => getMemberDisplayColor(msg.userId); // Self-mention highlighting const isMentioned = currentUser && message.content?.includes('<@' + currentUser.id + '>'); const content = (
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} > {/* Reply Line */} {message.replyTo && (
)} {/* Avatar or timestamp column */}
{isFirstInGroup || message.replyTo ? (
) : ( {formatHoverTime(message.createdAt)} )}
{/* Content */}
{message.replyTo && (
{message.replyTo.user.displayName ?? message.replyTo.user.username} {message.replyTo.content}
)} {(isFirstInGroup || message.replyTo) && (
{displayName} {formatTime(message.createdAt)}
)} {isEditing ? (