krona-front/src/features/communications/components/ChannelsView.tsx

227 lines
11 KiB
TypeScript
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 { useState, type FormEvent } from 'react';
import { Hash, Lock, Users, Pin, Send, Plus } from 'lucide-react';
import { useCommunicationsStore } from '../services/communicationsService';
import { Button } from '@omnichannel/ui';
interface ChannelsViewProps {
onCreateChannelModal: () => void;
}
export function ChannelsView({ onCreateChannelModal }: ChannelsViewProps) {
const {
channels,
activeChannelId,
sendChannelMessage,
toggleChannelReaction,
} = useCommunicationsStore();
const [currentChannelId, setCurrentChannelId] = useState(activeChannelId);
const [inputText, setInputText] = useState('');
const [showMembers, setShowMembers] = useState(false);
const activeChannel = channels.find((c) => c.id === currentChannelId) || channels[0];
const handleSend = (e: FormEvent) => {
e.preventDefault();
if (!inputText.trim()) return;
sendChannelMessage(activeChannel.id, inputText);
setInputText('');
};
return (
<div className="flex-1 min-h-0 p-3 sm:p-4 lg:p-5 flex gap-4 bg-[var(--krona-bg-workspace)] overflow-hidden">
{/* Channels directory list */}
<aside className="w-64 lg:w-72 bg-[var(--krona-surface-raised)] rounded-2xl border border-[var(--krona-border-subtle)] shadow-[var(--krona-shadow-soft)] p-4 flex flex-col gap-4 shrink-0 overflow-y-auto">
<div className="flex items-center justify-between">
<span className="text-xs font-bold text-[var(--krona-text-secondary)] uppercase tracking-wider">
Внутренние каналы
</span>
<button
type="button"
onClick={onCreateChannelModal}
className="w-7 h-7 rounded-md border border-[var(--krona-border)] flex items-center justify-center hover:bg-[var(--krona-surface-hover)] text-[var(--krona-text-primary)] transition-colors"
title="Создать канал"
>
<Plus className="w-4 h-4" />
</button>
</div>
<nav className="space-y-1 overflow-y-auto">
{channels.map((ch) => {
const isActive = ch.id === activeChannel.id;
return (
<button
key={ch.id}
type="button"
onClick={() => setCurrentChannelId(ch.id)}
className={`w-full p-2.5 rounded-xl text-left flex items-center justify-between gap-2 transition-colors min-h-[44px] ${
isActive
? 'bg-[var(--krona-primary-soft)] text-[var(--krona-primary)] font-bold'
: 'text-[var(--krona-text-primary)] hover:bg-[var(--krona-surface-hover)] font-medium'
}`}
>
<div className="flex items-center gap-2 truncate">
{ch.isPrivate ? (
<Lock className="w-4 h-4 shrink-0 text-amber-600" />
) : (
<Hash className="w-4 h-4 shrink-0 text-[var(--krona-primary)]" />
)}
<span className="text-xs truncate">{ch.name}</span>
</div>
{ch.unreadCount > 0 && (
<span className="px-1.5 py-0.5 rounded-full bg-[var(--krona-primary)] text-white text-[10px] font-bold shrink-0">
{ch.unreadCount}
</span>
)}
</button>
);
})}
</nav>
</aside>
{/* Main channel conversation area */}
<section className="flex-1 min-h-0 bg-[var(--krona-surface-raised)] rounded-2xl border border-[var(--krona-border-subtle)] shadow-[var(--krona-shadow-soft)] flex flex-col overflow-hidden">
{/* Channel Header */}
<header className="h-16 px-6 bg-[var(--krona-surface)] border-b border-[var(--krona-border-subtle)] flex items-center justify-between gap-3 shrink-0">
<div className="flex items-center gap-3 min-w-0">
<div className="w-10 h-10 rounded-xl bg-[var(--krona-primary-soft)] text-[var(--krona-primary)] flex items-center justify-center font-bold">
{activeChannel.isPrivate ? <Lock className="w-5 h-5" /> : <Hash className="w-5 h-5" />}
</div>
<div className="min-w-0">
<div className="flex items-center gap-2">
<h2 className="font-bold text-sm sm:text-base text-[var(--krona-text-primary)] truncate">
{activeChannel.name}
</h2>
{activeChannel.isPrivate && (
<span className="px-2 py-0.5 rounded text-[10px] bg-amber-100 dark:bg-amber-950 text-amber-700 font-semibold">
Закрытый
</span>
)}
</div>
<p className="text-xs text-[var(--krona-text-secondary)] truncate">
{activeChannel.topic}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => setShowMembers(!showMembers)}
className="px-3 py-1.5 rounded-lg border border-[var(--krona-border)] text-xs font-semibold text-[var(--krona-text-primary)] hover:bg-[var(--krona-surface-hover)] flex items-center gap-1.5 transition-colors"
>
<Users className="w-3.5 h-3.5" />
<span>{activeChannel.membersCount} участников</span>
</button>
</div>
</header>
{/* Pinned announcement banner */}
{activeChannel.pinnedMessage && (
<div className="px-6 py-2.5 bg-amber-50/70 dark:bg-amber-950/30 border-b border-amber-200 dark:border-amber-900/50 flex items-center gap-2.5 text-xs text-amber-900 dark:text-amber-200">
<Pin className="w-4 h-4 text-amber-600 shrink-0" />
<span className="font-semibold shrink-0">Закреплено:</span>
<span className="truncate">{activeChannel.pinnedMessage}</span>
</div>
)}
{/* Message feed */}
<div className="flex-1 overflow-y-auto p-4 sm:p-6 space-y-4">
{activeChannel.messages.map((msg) => (
<div key={msg.id} className="flex items-start gap-3">
<div className="w-8 h-8 rounded-full bg-[var(--krona-surface-raised)] border border-[var(--krona-border)] flex items-center justify-center font-bold text-xs text-[var(--krona-primary)] shrink-0">
{msg.senderName[0]}
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="font-bold text-xs text-[var(--krona-text-primary)]">
{msg.senderName}
</span>
<span className="text-[10px] text-[var(--krona-text-muted)]">
{msg.timestamp}
</span>
</div>
<div className="mt-1 text-sm text-[var(--krona-text-primary)] bg-[var(--krona-surface)] p-3 rounded-xl border border-[var(--krona-border-subtle)] inline-block max-w-xl shadow-xs">
{msg.text}
</div>
{/* Reactions */}
<div className="mt-1.5 flex items-center gap-1.5">
{msg.reactions &&
Object.entries(msg.reactions).map(([emoji, count]) => (
<button
key={emoji}
type="button"
onClick={() => toggleChannelReaction(activeChannel.id, msg.id, emoji)}
className="px-2 py-0.5 rounded-full bg-[var(--krona-surface-raised)] border border-[var(--krona-border)] text-xs flex items-center gap-1 hover:bg-[var(--krona-surface-hover)] transition-colors"
>
<span>{emoji}</span>
<span className="font-bold text-[10px]">{count}</span>
</button>
))}
<button
type="button"
onClick={() => toggleChannelReaction(activeChannel.id, msg.id, '👍')}
className="w-6 h-6 rounded-full border border-dashed border-[var(--krona-border)] flex items-center justify-center text-xs text-[var(--krona-text-muted)] hover:text-[var(--krona-text-primary)]"
title="Добавить реакцию"
>
+
</button>
</div>
</div>
</div>
))}
</div>
{/* Input bar */}
<form onSubmit={handleSend} className="p-4 bg-[var(--krona-surface)] border-t border-[var(--krona-border-subtle)] flex items-center gap-2">
<input
type="text"
value={inputText}
onChange={(e) => setInputText(e.target.value)}
placeholder={`Сообщение в #${activeChannel.name}...`}
className="flex-1 h-11 px-4 rounded-xl bg-[var(--krona-bg-workspace)] border border-[var(--krona-border)] text-sm text-[var(--krona-text-primary)] placeholder-[var(--krona-text-muted)] focus:outline-none focus:border-[var(--krona-primary)]"
/>
<Button type="submit" variant="primary" size="md" disabled={!inputText.trim()} leftIcon={<Send className="w-4 h-4" />}>
Отправить
</Button>
</form>
</section>
{/* Members drawer/sidebar */}
{showMembers && (
<aside className="w-64 border-l border-[var(--krona-border-subtle)] bg-[var(--krona-surface)] p-4 flex flex-col gap-3 shrink-0 animate-slide-left">
<div className="flex items-center justify-between">
<h3 className="font-bold text-xs uppercase text-[var(--krona-text-secondary)]">
Участники канала
</h3>
<button
type="button"
onClick={() => setShowMembers(false)}
className="text-xs text-[var(--krona-text-muted)] hover:text-[var(--krona-text-primary)]"
>
✕
</button>
</div>
<div className="space-y-2 overflow-y-auto">
{activeChannel.members.map((m) => (
<div key={m.id} className="flex items-center gap-2 text-xs">
<div className="w-7 h-7 rounded-full bg-[var(--krona-primary-soft)] text-[var(--krona-primary)] flex items-center justify-center font-bold">
{m.name[0]}
</div>
<div className="min-w-0">
<span className="block font-semibold text-[var(--krona-text-primary)] truncate">
{m.name}
</span>
<span className="block text-[10px] text-[var(--krona-text-muted)] truncate">
{m.role}
</span>
</div>
</div>
))}
</div>
</aside>
)}
</div>
);
}