Files
SkinbaseNova/resources/js/components/messaging/NewConversationModal.jsx
2026-02-26 21:12:32 +01:00

178 lines
7.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useCallback } from 'react'
/**
* Modal for creating a new direct or group conversation.
*/
export default function NewConversationModal({ currentUserId, apiFetch, onCreated, onClose }) {
const [type, setType] = useState('direct')
const [recipientInput, setRecipient] = useState('')
const [groupTitle, setGroupTitle] = useState('')
const [participantInputs, setParticipantInputs] = useState(['', ''])
const [body, setBody] = useState('')
const [sending, setSending] = useState(false)
const [error, setError] = useState(null)
const addParticipant = () => setParticipantInputs(p => [...p, ''])
const updateParticipant = (i, val) =>
setParticipantInputs(p => p.map((v, idx) => idx === i ? val : v))
const removeParticipant = (i) =>
setParticipantInputs(p => p.filter((_, idx) => idx !== i))
const handleSubmit = useCallback(async (e) => {
e.preventDefault()
setError(null)
setSending(true)
try {
// Resolve usernames to IDs via the search API
let payload = { type, body }
if (type === 'direct') {
const user = await resolveUsername(recipientInput.trim(), apiFetch)
payload.recipient_id = user.id
} else {
const resolved = await Promise.all(
participantInputs
.map(p => p.trim())
.filter(Boolean)
.map(u => resolveUsername(u, apiFetch))
)
payload.participant_ids = resolved.map(u => u.id)
payload.title = groupTitle.trim()
}
const conv = await apiFetch('/api/messages/conversation', {
method: 'POST',
body: JSON.stringify(payload),
})
onCreated(conv)
} catch (e) {
setError(e.message)
} finally {
setSending(false)
}
}, [type, body, recipientInput, groupTitle, participantInputs, apiFetch, onCreated])
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/40 backdrop-blur-sm">
<div className="bg-white dark:bg-gray-900 rounded-2xl shadow-xl w-full max-w-md p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">New Message</h2>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" />
</svg>
</button>
</div>
{/* Type toggle */}
<div className="flex rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden mb-4">
{['direct', 'group'].map(t => (
<button
key={t}
type="button"
onClick={() => setType(t)}
className={`flex-1 py-1.5 text-sm font-medium transition-colors ${
type === t
? 'bg-blue-500 text-white'
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800'
}`}
>
{t === 'direct' ? '1:1 Message' : 'Group'}
</button>
))}
</div>
<form onSubmit={handleSubmit} className="space-y-3">
{type === 'direct' ? (
<div>
<label className="block text-xs font-medium text-gray-500 mb-1">Recipient username</label>
<input
type="text"
value={recipientInput}
onChange={e => setRecipient(e.target.value)}
placeholder="username"
required
className="w-full rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 px-3 py-2 text-sm text-gray-900 dark:text-gray-100 outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
) : (
<>
<div>
<label className="block text-xs font-medium text-gray-500 mb-1">Group name</label>
<input
type="text"
value={groupTitle}
onChange={e => setGroupTitle(e.target.value)}
placeholder="Group name"
required
maxLength={120}
className="w-full rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 px-3 py-2 text-sm text-gray-900 dark:text-gray-100 outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-500 mb-1">Participants (usernames)</label>
{participantInputs.map((val, i) => (
<div key={i} className="flex gap-2 mb-1">
<input
type="text"
value={val}
onChange={e => updateParticipant(i, e.target.value)}
placeholder={`Username ${i + 1}`}
required
className="flex-1 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 px-3 py-1.5 text-sm text-gray-900 dark:text-gray-100 outline-none focus:ring-2 focus:ring-blue-500"
/>
{participantInputs.length > 2 && (
<button type="button" onClick={() => removeParticipant(i)} className="text-gray-400 hover:text-red-500">×</button>
)}
</div>
))}
<button type="button" onClick={addParticipant} className="text-xs text-blue-500 hover:text-blue-700 mt-1">
+ Add participant
</button>
</div>
</>
)}
<div>
<label className="block text-xs font-medium text-gray-500 mb-1">Message</label>
<textarea
value={body}
onChange={e => setBody(e.target.value)}
placeholder="Write your message…"
required
rows={3}
maxLength={5000}
className="w-full resize-none rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 px-3 py-2 text-sm text-gray-900 dark:text-gray-100 outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
{error && (
<p className="text-xs text-red-500 bg-red-50 dark:bg-red-900/30 rounded px-2 py-1">{error}</p>
)}
<div className="flex gap-2 justify-end pt-1">
<button type="button" onClick={onClose} className="px-4 py-2 text-sm text-gray-600 hover:text-gray-900">Cancel</button>
<button
type="submit"
disabled={sending}
className="px-4 py-2 rounded-xl bg-blue-500 hover:bg-blue-600 disabled:opacity-50 text-white text-sm font-medium transition-colors"
>
{sending ? 'Sending…' : 'Send'}
</button>
</div>
</form>
</div>
</div>
)
}
// ── Resolve username to user object via search API ───────────────────────────
async function resolveUsername(username, apiFetch) {
const data = await apiFetch(`/api/search/users?q=${encodeURIComponent(username)}&limit=1`)
const user = data?.data?.[0] ?? data?.[0]
if (!user) throw new Error(`User "${username}" not found.`)
return user
}