perf(vpn-bot): фоновая подготовка подписки, оптимизация запросов, анимация кнопок
- validateTelemostRoom: таймаут 3 сек + кэш 10 мин - createVpnTask: транзакция + batch insert field values - prepareVpnSubscription: валидация и создание подписки в фоне при получении ссылки - sendVpnBotMessage: кэш botId, Promise.all для enrich/members/orgMap - processPlatformSelection: одно сообщение с файлом и подпиской вместо двух - Chat.tsx: нажатая кнопка становится зелёной галочкой, остальные disabled
This commit is contained in:
@@ -146,7 +146,7 @@ function groupButtonsByRow(buttons: BotButton[]): BotButton[][] {
|
||||
|
||||
function MessageBubble({
|
||||
msg, isOwn, onReply, onEdit, onDelete, reactions, poll, onReactionsChange, onPollChange,
|
||||
onOpenUserChat, onOpenBotChat, onButtonClick,
|
||||
onOpenUserChat, onOpenBotChat, onButtonClick, pressedButtons,
|
||||
}: {
|
||||
msg: ConvMessage;
|
||||
isOwn: boolean;
|
||||
@@ -160,6 +160,7 @@ function MessageBubble({
|
||||
onOpenUserChat?: (userId: number) => void;
|
||||
onOpenBotChat?: (botId: number) => void;
|
||||
onButtonClick?: (m: ConvMessage, button: BotButton) => void;
|
||||
pressedButtons?: Set<string>;
|
||||
}) {
|
||||
const [pickerOpen, setPickerOpen] = useState(false);
|
||||
const isEdited = msg.updatedAt && msg.updatedAt !== msg.createdAt;
|
||||
@@ -313,17 +314,26 @@ function MessageBubble({
|
||||
<div className={cn("flex flex-col gap-1.5 mt-1.5 max-w-full", isOwn ? "items-end" : "items-start")}>
|
||||
{groupButtonsByRow(msg.botButtons).map((row, rowIdx) => (
|
||||
<div key={rowIdx} className="flex flex-wrap gap-1.5">
|
||||
{row.map((btn, btnIdx) => (
|
||||
<Button
|
||||
key={btnIdx}
|
||||
variant={btn.style === 'primary' ? 'default' : btn.style === 'destructive' ? 'destructive' : btn.style === 'outline' ? 'outline' : 'secondary'}
|
||||
size="sm"
|
||||
className="h-7 text-xs px-3 py-1 rounded-full"
|
||||
onClick={() => onButtonClick(msg, btn)}
|
||||
>
|
||||
{btn.text}
|
||||
</Button>
|
||||
))}
|
||||
{row.map((btn, btnIdx) => {
|
||||
const btnKey = `${msg.id}:${btn.callbackData}`;
|
||||
const isPressed = pressedButtons?.has(btnKey) ?? false;
|
||||
return (
|
||||
<Button
|
||||
key={btnIdx}
|
||||
variant={isPressed ? 'default' : btn.style === 'primary' ? 'default' : btn.style === 'destructive' ? 'destructive' : btn.style === 'outline' ? 'outline' : 'secondary'}
|
||||
size="sm"
|
||||
className={cn(
|
||||
"h-7 text-xs px-3 py-1 rounded-full",
|
||||
isPressed && "bg-green-600 hover:bg-green-600 text-white"
|
||||
)}
|
||||
disabled={isPressed}
|
||||
onClick={() => onButtonClick(msg, btn)}
|
||||
>
|
||||
{isPressed && <Check className="w-3 h-3 mr-1" />}
|
||||
{btn.text}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -1000,6 +1010,8 @@ export default function ChatPage() {
|
||||
// Typing indicator for bots
|
||||
const [typingBots, setTypingBots] = useState<Set<string>>(new Set());
|
||||
const typingTimersRef = useRef<Map<string, NodeJS.Timeout>>(new Map());
|
||||
// Pressed bot buttons (messageId:callbackData)
|
||||
const [pressedButtons, setPressedButtons] = useState<Set<string>>(new Set());
|
||||
// File attachments
|
||||
const [pendingFiles, setPendingFiles] = useState<File[]>([]);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -1406,10 +1418,11 @@ export default function ChatPage() {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Clear all typing indicators when switching conversations
|
||||
// Clear all typing indicators and pressed buttons when switching conversations
|
||||
typingTimersRef.current.forEach(timer => clearTimeout(timer));
|
||||
typingTimersRef.current.clear();
|
||||
setTypingBots(new Set());
|
||||
setPressedButtons(new Set());
|
||||
}, [activeConvId]);
|
||||
|
||||
// ── SSE ───────────────────────────────────────────────────────────────────────
|
||||
@@ -2402,8 +2415,16 @@ export default function ChatPage() {
|
||||
openBotDirectMutation.mutate(botId);
|
||||
}}
|
||||
onButtonClick={(m, btn) => {
|
||||
const btnKey = `${m.id}:${btn.callbackData}`;
|
||||
setPressedButtons(prev => {
|
||||
if (prev.has(btnKey)) return prev;
|
||||
const next = new Set(prev);
|
||||
next.add(btnKey);
|
||||
return next;
|
||||
});
|
||||
botButtonCallbackMutation.mutate({ messageId: m.id, callbackData: btn.callbackData });
|
||||
}}
|
||||
pressedButtons={pressedButtons}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user