big update: added reactions, groups, media share
This commit is contained in:
@@ -1,17 +1,26 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import Avatar from './Avatar'
|
||||
import { IconClose, IconSearch, IconUserPlus } from './icons'
|
||||
import { IconClose, IconSearch, IconUserPlus, IconUsers } from './icons'
|
||||
import { api } from '../lib/api'
|
||||
import { userTitle } from '../lib/format'
|
||||
import type { Contact, User } from '../lib/types'
|
||||
|
||||
type DialogMode = 'dm' | 'group'
|
||||
|
||||
interface NewChatDialogProps {
|
||||
onClose: () => void
|
||||
onStartChat: (user: User) => Promise<void>
|
||||
onCreateGroup: (name: string, memberIds: number[]) => Promise<void>
|
||||
onContactsChanged: () => void
|
||||
}
|
||||
|
||||
export default function NewChatDialog({ onClose, onStartChat, onContactsChanged }: NewChatDialogProps) {
|
||||
export default function NewChatDialog({
|
||||
onClose,
|
||||
onStartChat,
|
||||
onCreateGroup,
|
||||
onContactsChanged,
|
||||
}: NewChatDialogProps) {
|
||||
const [mode, setMode] = useState<DialogMode>('dm')
|
||||
const [query, setQuery] = useState('')
|
||||
const [results, setResults] = useState<User[]>([])
|
||||
const [resultQuery, setResultQuery] = useState('')
|
||||
@@ -19,6 +28,9 @@ export default function NewChatDialog({ onClose, onStartChat, onContactsChanged
|
||||
const [loadingContacts, setLoadingContacts] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [busyId, setBusyId] = useState<number | null>(null)
|
||||
const [groupName, setGroupName] = useState('')
|
||||
const [selectedIds, setSelectedIds] = useState<Set<number>>(() => new Set())
|
||||
const [creating, setCreating] = useState(false)
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -106,6 +118,32 @@ export default function NewChatDialog({ onClose, onStartChat, onContactsChanged
|
||||
onContactsChanged()
|
||||
})
|
||||
|
||||
const toggleMember = (userId: number) => {
|
||||
setSelectedIds((prev) => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(userId)) next.delete(userId)
|
||||
else next.add(userId)
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const handleCreateGroup = async () => {
|
||||
const name = groupName.trim()
|
||||
if (!name || selectedIds.size === 0) return
|
||||
setCreating(true)
|
||||
setError(null)
|
||||
try {
|
||||
await onCreateGroup(name, [...selectedIds])
|
||||
onClose()
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Не удалось создать группу')
|
||||
} finally {
|
||||
setCreating(false)
|
||||
}
|
||||
}
|
||||
|
||||
const canCreate = groupName.trim().length > 0 && selectedIds.size > 0 && !creating
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-end justify-center bg-black/60 p-0 backdrop-blur-sm sm:items-center sm:p-6">
|
||||
<button type="button" aria-label="Закрыть" onClick={onClose} className="absolute inset-0 cursor-default" />
|
||||
@@ -120,7 +158,9 @@ export default function NewChatDialog({ onClose, onStartChat, onContactsChanged
|
||||
<h2 id="new-chat-title" className="font-display text-base font-semibold text-fg">
|
||||
Новый чат
|
||||
</h2>
|
||||
<p className="text-xs text-muted">Найдите человека или выберите из контактов</p>
|
||||
<p className="text-xs text-muted">
|
||||
{mode === 'dm' ? 'Найдите человека или выберите из контактов' : 'Соберите группу и добавьте участников'}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@@ -133,6 +173,39 @@ export default function NewChatDialog({ onClose, onStartChat, onContactsChanged
|
||||
</div>
|
||||
|
||||
<div className="px-5 pt-4">
|
||||
<div className="mb-3 grid grid-cols-2 gap-1 rounded-xl border border-line bg-surface/60 p-1">
|
||||
{(['dm', 'group'] as const).map((value) => (
|
||||
<button
|
||||
key={value}
|
||||
type="button"
|
||||
onClick={() => setMode(value)}
|
||||
className={`rounded-lg px-3 py-1.5 text-xs font-semibold transition ${
|
||||
mode === value ? 'bg-accent-ghost text-accent ring-1 ring-accent/30' : 'text-muted hover:text-fg'
|
||||
}`}
|
||||
>
|
||||
{value === 'dm' ? 'Личный чат' : 'Группа'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{mode === 'group' && (
|
||||
<div className="relative mb-3">
|
||||
<IconUsers className="pointer-events-none absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-faint" />
|
||||
<label htmlFor="group-name" className="sr-only">
|
||||
Название группы
|
||||
</label>
|
||||
<input
|
||||
id="group-name"
|
||||
type="text"
|
||||
value={groupName}
|
||||
onChange={(event) => setGroupName(event.target.value)}
|
||||
placeholder="Название группы"
|
||||
maxLength={80}
|
||||
className="w-full rounded-xl border border-line bg-surface/70 py-2.5 pr-3 pl-9 text-sm text-fg placeholder:text-faint focus:border-accent/50 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/20"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="relative">
|
||||
<IconSearch className="pointer-events-none absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-faint" />
|
||||
<label htmlFor="new-chat-search" className="sr-only">
|
||||
@@ -170,38 +243,80 @@ export default function NewChatDialog({ onClose, onStartChat, onContactsChanged
|
||||
{list.map((user) => {
|
||||
const isContact = contactIds.has(user.id)
|
||||
const busy = busyId === user.id
|
||||
const selected = selectedIds.has(user.id)
|
||||
return (
|
||||
<li key={user.id} className="flex items-center gap-3 rounded-xl px-2.5 py-2 transition hover:bg-surface-2/70">
|
||||
<Avatar name={userTitle(user)} seed={user.id} size="md" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm font-semibold text-fg">{userTitle(user)}</p>
|
||||
<p className="truncate text-xs text-muted">@{user.username}</p>
|
||||
</div>
|
||||
{!isContact && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleAdd(user)}
|
||||
disabled={busy}
|
||||
className="inline-flex items-center gap-1 rounded-lg border border-line px-2.5 py-1.5 text-xs font-medium text-muted transition hover:border-accent/50 hover:text-accent disabled:opacity-50"
|
||||
>
|
||||
<IconUserPlus className="h-4 w-4" />
|
||||
Добавить
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleStart(user)}
|
||||
disabled={busy}
|
||||
className="rounded-lg bg-gradient-to-br from-accent to-accent-strong px-3 py-1.5 text-xs font-semibold text-white transition hover:brightness-110 disabled:opacity-50"
|
||||
<li key={user.id}>
|
||||
<div
|
||||
className={`flex items-center gap-3 rounded-xl px-2.5 py-2 transition ${
|
||||
mode === 'group' && selected ? 'bg-accent-ghost ring-1 ring-accent/30' : 'hover:bg-surface-2/70'
|
||||
}`}
|
||||
>
|
||||
{busy ? '…' : 'Написать'}
|
||||
</button>
|
||||
{mode === 'group' && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleMember(user.id)}
|
||||
aria-pressed={selected}
|
||||
aria-label={selected ? 'Убрать из группы' : 'Добавить в группу'}
|
||||
className={`inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-md border transition ${
|
||||
selected ? 'border-accent bg-accent text-white' : 'border-line bg-surface text-transparent'
|
||||
}`}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" aria-hidden="true">
|
||||
<path d="m5 12.5 4.5 4.5L19 7" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
<Avatar name={userTitle(user)} seed={user.id} size="md" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm font-semibold text-fg">{userTitle(user)}</p>
|
||||
<p className="truncate text-xs text-muted">@{user.username}</p>
|
||||
</div>
|
||||
{mode === 'dm' && (
|
||||
<>
|
||||
{!isContact && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleAdd(user)}
|
||||
disabled={busy}
|
||||
className="inline-flex items-center gap-1 rounded-lg border border-line px-2.5 py-1.5 text-xs font-medium text-muted transition hover:border-accent/50 hover:text-accent disabled:opacity-50"
|
||||
>
|
||||
<IconUserPlus className="h-4 w-4" />
|
||||
Добавить
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleStart(user)}
|
||||
disabled={busy}
|
||||
className="rounded-lg bg-gradient-to-br from-accent to-accent-strong px-3 py-1.5 text-xs font-semibold text-white transition hover:brightness-110 disabled:opacity-50"
|
||||
>
|
||||
{busy ? '…' : 'Написать'}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{mode === 'group' && (
|
||||
<div className="flex items-center justify-between gap-3 border-t border-line/80 px-5 py-3">
|
||||
<span className="text-xs text-muted">
|
||||
Выбрано: <span className="font-semibold text-fg">{selectedIds.size}</span>
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleCreateGroup()}
|
||||
disabled={!canCreate}
|
||||
className="rounded-xl bg-gradient-to-br from-accent to-accent-strong px-4 py-2 text-sm font-semibold text-white transition enabled:hover:brightness-110 disabled:cursor-not-allowed disabled:opacity-40"
|
||||
>
|
||||
{creating ? 'Создание…' : 'Создать группу'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user