178 lines
7.6 KiB
JavaScript
178 lines
7.6 KiB
JavaScript
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
|
||
}
|