import React, { useState, useEffect, useRef } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { supabase } from '../supabaseClient';
import ApplicantLayout from '../components/ApplicantLayout';
import PlaceholderContent from '../components/PlaceholderContent';
import { ChatIcon, SearchIcon } from '../components/Icons';
// --- Inline UI Components for Chat ---
const SendIcon = () => (
);
const Avatar = ({ name }) => (
{name ? name[0].toUpperCase() : 'U'}
);
const glassStyle = { background: 'rgba(15, 23, 42, 0.6)', backdropFilter: 'blur(16px)', border: '1px solid rgba(255, 255, 255, 0.08)', borderRadius: '1.25rem', display: 'flex', flexDirection: 'column', overflow: 'hidden' };
const inputStyle = { width: '100%', padding: '0.8rem', background: 'transparent', border: 'none', color: 'white', outline: 'none', fontSize: '0.95rem' };
export default function ApplicantMessages({ onNavigate }) {
const [threads, setThreads] = useState([]);
const [selected, setSelected] = useState(null);
const [text, setText] = useState('');
const [search, setSearch] = useState('');
const [userId, setUserId] = useState(null);
const [loading, setLoading] = useState(true);
const scrollRef = useRef(null);
// Auto-scroll to bottom of conversation
useEffect(() => {
scrollRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [selected?.msgs]);
// ✅ FETCH MESSAGES AND NAMES
const fetchMsgs = async (uid) => {
try {
console.log("Fetching messages for user:", uid);
const { data: messages, error } = await supabase
.from('messages')
.select('*')
.or(`receiver_id.eq.${uid},sender_id.eq.${uid}`)
.order('created_at', { ascending: true });
if (error) throw error;
const threadMap = {};
messages.forEach(m => {
let isMe = m.sender_id === uid;
let otherId = isMe ? m.receiver_id : m.sender_id;
// --- FIX FOR AUTOMATED MESSAGES ---
// If the applicant sent it to themselves, it's a thread used for system messages.
if (m.sender_id === m.receiver_id && m.sender_id === uid) {
otherId = uid; // Keep a valid UUID so replies work
// Only force the automated welcome message to the left side
if (m.content && m.content.startsWith("Hello, Thank you for applying")) {
isMe = false;
} else {
isMe = true; // Applicant's manual replies stay on the right
}
}
if (!threadMap[otherId]) {
threadMap[otherId] = {
id: otherId,
name: 'Admin / HR',
subj: 'Application Update',
last: '',
unread: false,
time: m.created_at,
msgs: [],
companyName: '',
companyLogo: '',
companyId: null
};
}
threadMap[otherId].msgs.push({
id: m.id,
text: m.content,
time: new Date(m.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
rawTime: m.created_at,
isMe
});
threadMap[otherId].last = m.content;
threadMap[otherId].time = m.created_at;
if (!isMe && !m.is_read) threadMap[otherId].unread = true;
});
const threadList = Object.values(threadMap).sort((a, b) => new Date(b.time) - new Date(a.time));
// ✅ Fetch Admin Names and Company Info from 'user_roles' table
const otherUserIds = threadList.map(t => t.id);
if (otherUserIds.length > 0) {
const { data: rolesData } = await supabase
.from('user_roles')
.select('user_id, name, company_id')
.in('user_id', otherUserIds);
console.log("Roles Data:", rolesData);
if (rolesData && rolesData.length > 0) {
rolesData.forEach(role => {
const thread = threadList.find(t => t.id === role.user_id);
if (thread && role.name) thread.name = role.name;
if (thread) thread.companyId = role.company_id;
});
// ✅ Fetch Company Details including logo from storage
const companyIds = [...new Set(rolesData.map(r => r.company_id).filter(Boolean))];
console.log("Company IDs to fetch:", companyIds);
if (companyIds.length > 0) {
const { data: companiesData } = await supabase
.from('companies')
.select('id, name, logo_url')
.in('id', companyIds);
console.log("Companies Data:", companiesData);
if (companiesData) {
companiesData.forEach(company => {
threadList.forEach(thread => {
if (thread.companyId === company.id) {
thread.companyName = company.name;
thread.companyLogo = company.logo_url;
console.log(`Set company for thread ${thread.id}:`, company.name, company.logo_url);
}
});
});
}
}
} else {
console.log("No roles data found or rolesData is empty");
}
}
console.log("Final Thread List:", threadList);
setThreads(threadList);
// Refresh currently open chat window if data changed
if (selected) {
const updated = threadList.find(t => t.id === selected.id);
if (updated) setSelected(updated);
}
} catch (err) {
console.error("Message Fetch Error:", err);
} finally {
setLoading(false);
}
};
// ✅ REAL-TIME LISTENER
useEffect(() => {
let channel;
const init = async () => {
const { data: { user } } = await supabase.auth.getUser();
if (!user) return;
setUserId(user.id);
await fetchMsgs(user.id);
// Create a channel to listen for any new messages where THIS user is the receiver
channel = supabase.channel('applicant_inbox')
.on('postgres_changes', {
event: 'INSERT',
schema: 'public',
table: 'messages',
filter: `receiver_id=eq.${user.id}`
}, () => {
console.log("New message received from Admin!");
fetchMsgs(user.id);
})
.subscribe();
};
init();
return () => { if (channel) supabase.removeChannel(channel); };
}, []);
const markAsRead = async (t) => {
setSelected(t);
if (t.unread && userId) {
// Optimistic UI Update
setThreads(threads.map(x => x.id === t.id ? { ...x, unread: false } : x));
// Database Update
await supabase.from('messages')
.update({ is_read: true })
.eq('receiver_id', userId)
.eq('sender_id', t.id)
.eq('is_read', false);
}
};
const sendMsg = async () => {
if (!text.trim() || !selected || !userId) return;
const messageText = text.trim();
setText('');
// Send to Supabase
const { error } = await supabase.from('messages').insert([{
sender_id: userId,
receiver_id: selected.id,
content: messageText,
is_read: false
}]);
if (error) {
console.error("Send Error:", error);
} else {
fetchMsgs(userId); // Refresh to show my sent message
}
};
const unsendMsg = async (msgId) => {
if (!window.confirm("Unsend this message?")) return;
// 1. Try to delete the message
const { data, error } = await supabase.from('messages').delete().eq('id', msgId).select();
if (error) {
console.error("Unsend Error:", error);
alert("Failed to unsend message.");
return;
}
// 2. If RLS silently blocks deletion (data is empty), fallback to updating the content
if (!data || data.length === 0) {
const { error: updateError } = await supabase
.from('messages')
.update({ content: "🚫 This message was unsent" })
.eq('id', msgId);
if (updateError) {
alert("Database policies prevent unsending this message.");
return;
}
}
fetchMsgs(userId);
};
const canUnsend = (rawTime) => {
if (!rawTime) return false;
const msgTime = new Date(rawTime).getTime();
return (Date.now() - msgTime) < 5 * 60 * 1000; // 5 minutes
};
const filtered = threads.filter(t => t.name.toLowerCase().includes(search.toLowerCase()));
return (
{/* --- CONVERSATIONS LIST --- */}
{loading ? (
Loading...
) : filtered.length === 0 ? (
No messages found.
) : (
filtered.map(t => {
const isAct = selected?.id === t.id;
return (
markAsRead(t)}
whileHover={{ scale: 0.98 }}
style={{
padding: '1.25rem', marginBottom: '0.5rem', borderRadius: '1rem',
cursor: 'pointer', position: 'relative',
background: isAct ? 'rgba(251, 191, 36, 0.08)' : 'transparent',
border: isAct ? '1px solid rgba(251,191,36,0.3)' : '1px solid transparent',
display: 'flex', gap: '1rem'
}}
>
{t.unread && !isAct && }
{/* Company Logo */}
{t.companyLogo ? (
) : (
)}
{/* Message Info */}
{t.companyName || t.name}
HR
{new Date(t.time).toLocaleDateString([], { month: 'short', day: 'numeric' })}
{t.subj}
{t.last}
);
})
)}
{/* --- ACTIVE CONVERSATION --- */}
{selected ? (
<>
{selected.companyLogo ? (

) : (
)}
{selected.companyName || selected.name}
HR
Active Now
{selected.msgs.map(m => (
{m.text}
{m.time}
{m.isMe && canUnsend(m.rawTime) && (
)}
))}
>
) : (
)}
);
}