Support chat mobile UX: fix keyboard overlap and improve composer.
Hide bottom navigation while typing and in support chat mode, adapt chat layout to visual viewport/keyboard insets, and enlarge the message composer so input remains visible and comfortable in TG/MAX mobile webviews.
This commit is contained in:
@@ -256,6 +256,21 @@ class Settings(BaseSettings):
|
||||
n8n_max_auth_webhook: str = "" # Webhook n8n: max_user_id → unified_id, contact_id, has_drafts
|
||||
n8n_auth_webhook: str = "" # Универсальный auth: channel + channel_user_id + init_data → unified_id, phone, contact_id, has_drafts
|
||||
|
||||
# ============================================
|
||||
# ПОДДЕРЖКА (чат, треды, n8n webhook)
|
||||
# ============================================
|
||||
n8n_support_webhook: str = "" # N8N_SUPPORT_WEBHOOK — URL webhook n8n (multipart). Обязателен для отправки сообщений.
|
||||
support_attachments_max_count: int = 0 # 0 = без ограничений
|
||||
support_attachments_max_size_mb: int = 0 # 0 = без ограничений
|
||||
support_attachments_allowed_types: str = "" # пусто = любые (например: .pdf,.jpg,image/*)
|
||||
support_incoming_secret: str = "" # Секрет для POST /api/v1/support/incoming (n8n → backend)
|
||||
|
||||
@property
|
||||
def support_attachments_max_size_bytes(self) -> int:
|
||||
if self.support_attachments_max_size_mb <= 0:
|
||||
return 0
|
||||
return self.support_attachments_max_size_mb * 1024 * 1024
|
||||
|
||||
# ============================================
|
||||
# LOGGING
|
||||
# ============================================
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<title>Clientright — защита прав потребителей</title>
|
||||
<!-- Подключаем только скрипт текущей платформы, иначе в MAX приходят события Telegram → UnsupportedEvent -->
|
||||
<script>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-x: hidden;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
@@ -27,8 +29,10 @@
|
||||
flex: 1;
|
||||
max-width: 1200px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.card {
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
max-width: 100vw;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
min-height: 64px;
|
||||
height: calc(64px + env(safe-area-inset-bottom, 0));
|
||||
@@ -19,8 +18,14 @@
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
z-index: 100;
|
||||
transition: transform 0.2s ease, opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.app-bottom-bar--hidden {
|
||||
transform: translateY(120%);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.app-bar-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -27,6 +27,9 @@ export default function BottomBar({ currentPath, avatarUrl, profileNeedsAttentio
|
||||
const isSupport = currentPath === '/support';
|
||||
const [backEnabled, setBackEnabled] = useState(false);
|
||||
const [supportUnreadCount, setSupportUnreadCount] = useState(0);
|
||||
const [keyboardOpen, setKeyboardOpen] = useState(false);
|
||||
const [inputFocused, setInputFocused] = useState(false);
|
||||
const [supportChatMode, setSupportChatMode] = useState(false);
|
||||
|
||||
// Непрочитанные в поддержке — для бейджа на иконке
|
||||
useEffect(() => {
|
||||
@@ -55,6 +58,61 @@ export default function BottomBar({ currentPath, avatarUrl, profileNeedsAttentio
|
||||
return () => window.clearTimeout(t);
|
||||
}, [isHome, isProfile, currentPath]);
|
||||
|
||||
// Если открыта клавиатура — прячем нижний бар, чтобы он не перекрывал поле ввода
|
||||
useEffect(() => {
|
||||
const vv = window.visualViewport;
|
||||
if (!vv) return;
|
||||
const update = () => {
|
||||
const inset = Math.max(0, window.innerHeight - vv.height - vv.offsetTop);
|
||||
setKeyboardOpen(inset > 80);
|
||||
};
|
||||
update();
|
||||
vv.addEventListener('resize', update);
|
||||
vv.addEventListener('scroll', update);
|
||||
return () => {
|
||||
vv.removeEventListener('resize', update);
|
||||
vv.removeEventListener('scroll', update);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Универсально для любых WebView: если в фокусе поле ввода, нижний бар скрываем.
|
||||
useEffect(() => {
|
||||
const isEditable = (el: EventTarget | null): boolean => {
|
||||
if (!(el instanceof HTMLElement)) return false;
|
||||
const tag = el.tagName.toLowerCase();
|
||||
return tag === 'input' || tag === 'textarea' || el.isContentEditable;
|
||||
};
|
||||
|
||||
const handleFocusIn = (e: FocusEvent) => {
|
||||
if (isEditable(e.target)) setInputFocused(true);
|
||||
};
|
||||
|
||||
const handleFocusOut = () => {
|
||||
window.setTimeout(() => {
|
||||
const active = document.activeElement;
|
||||
setInputFocused(isEditable(active));
|
||||
}, 30);
|
||||
};
|
||||
|
||||
window.addEventListener('focusin', handleFocusIn);
|
||||
window.addEventListener('focusout', handleFocusOut);
|
||||
return () => {
|
||||
window.removeEventListener('focusin', handleFocusIn);
|
||||
window.removeEventListener('focusout', handleFocusOut);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const onSupportChatMode = (e: Event) => {
|
||||
const detail = (e as CustomEvent<{ active?: boolean }>).detail;
|
||||
setSupportChatMode(!!detail?.active);
|
||||
};
|
||||
window.addEventListener('miniapp:supportChatMode', onSupportChatMode as EventListener);
|
||||
return () => {
|
||||
window.removeEventListener('miniapp:supportChatMode', onSupportChatMode as EventListener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleBack = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -148,7 +206,10 @@ export default function BottomBar({ currentPath, avatarUrl, profileNeedsAttentio
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="app-bottom-bar" aria-label="Навигация">
|
||||
<nav
|
||||
className={`app-bottom-bar${keyboardOpen || inputFocused || supportChatMode ? ' app-bottom-bar--hidden' : ''}`}
|
||||
aria-label="Навигация"
|
||||
>
|
||||
{!isHome && !isProfile && (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Button, Form, Input, Spin, Typography } from 'antd';
|
||||
import { Button, Form, Input, message, Spin, Typography } from 'antd';
|
||||
import { Paperclip, X } from 'lucide-react';
|
||||
|
||||
const { TextArea } = Input;
|
||||
@@ -73,11 +73,30 @@ export default function SupportChat({
|
||||
const [form] = Form.useForm();
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
const [fileInputKey, setFileInputKey] = useState(0);
|
||||
const [keyboardInset, setKeyboardInset] = useState(0);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const inputBarRef = useRef<HTMLDivElement>(null);
|
||||
const eventSourceRef = useRef<EventSource | null>(null);
|
||||
const threadIdRef = useRef<string | null>(null);
|
||||
threadIdRef.current = threadId;
|
||||
|
||||
// При фокусе: в TG/MAX запрашиваем expand(); затем прокручиваем поле ввода в видимую зону (над клавиатурой)
|
||||
const scrollInputIntoView = useCallback(() => {
|
||||
const win = typeof window !== 'undefined' ? window : null;
|
||||
const tg = (win as unknown as { Telegram?: { WebApp?: { expand?: () => void } } })?.Telegram?.WebApp;
|
||||
const max = (win as unknown as { WebApp?: { expand?: () => void } })?.WebApp;
|
||||
if (tg?.expand) tg.expand();
|
||||
if (max?.expand) max.expand();
|
||||
|
||||
const scroll = () => inputBarRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
const t1 = window.setTimeout(scroll, 350);
|
||||
const t2 = window.setTimeout(scroll, 700);
|
||||
return () => {
|
||||
window.clearTimeout(t1);
|
||||
window.clearTimeout(t2);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const markRead = useCallback((tid: string) => {
|
||||
const token = getSessionToken();
|
||||
if (!token) return;
|
||||
@@ -146,6 +165,22 @@ export default function SupportChat({
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}, [messages]);
|
||||
|
||||
useEffect(() => {
|
||||
const vv = window.visualViewport;
|
||||
if (!vv) return;
|
||||
const update = () => {
|
||||
const inset = Math.max(0, window.innerHeight - vv.height - vv.offsetTop);
|
||||
setKeyboardInset(inset);
|
||||
};
|
||||
update();
|
||||
vv.addEventListener('resize', update);
|
||||
vv.addEventListener('scroll', update);
|
||||
return () => {
|
||||
vv.removeEventListener('resize', update);
|
||||
vv.removeEventListener('scroll', update);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleSend = async () => {
|
||||
const values = await form.getFieldsValue();
|
||||
const text = (values.message || '').trim();
|
||||
@@ -169,7 +204,13 @@ export default function SupportChat({
|
||||
const res = await fetch('/api/v1/support', { method: 'POST', body: fd });
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
throw new Error(err.detail || res.statusText);
|
||||
const detail = err.detail || res.statusText;
|
||||
if (res.status === 503) {
|
||||
message.error('Сервис поддержки временно недоступен. Попробуйте позже.');
|
||||
} else {
|
||||
message.error(typeof detail === 'string' ? detail : 'Не удалось отправить сообщение.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
const data = await res.json();
|
||||
if (data.thread_id) setThreadId(data.thread_id);
|
||||
@@ -179,6 +220,7 @@ export default function SupportChat({
|
||||
setFileInputKey((k) => k + 1);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
message.error('Ошибка соединения. Попробуйте ещё раз.');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
@@ -206,7 +248,13 @@ export default function SupportChat({
|
||||
const res = await fetch('/api/v1/support', { method: 'POST', body: fd });
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
throw new Error(err.detail || res.statusText);
|
||||
const detail = err.detail || res.statusText;
|
||||
if (res.status === 503) {
|
||||
message.error('Сервис поддержки временно недоступен. Попробуйте позже.');
|
||||
} else {
|
||||
message.error(typeof detail === 'string' ? detail : 'Не удалось отправить обращение.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
const data = await res.json();
|
||||
if (data.thread_id) setThreadId(data.thread_id);
|
||||
@@ -217,6 +265,7 @@ export default function SupportChat({
|
||||
onSuccess?.();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
message.error('Ошибка соединения. Попробуйте ещё раз.');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
@@ -241,7 +290,10 @@ export default function SupportChat({
|
||||
|
||||
if (!showChat) {
|
||||
return (
|
||||
<div className={compact ? 'support-chat support-chat--compact' : 'support-chat'}>
|
||||
<div
|
||||
className={compact ? 'support-chat support-chat--compact' : 'support-chat'}
|
||||
style={{ paddingBottom: keyboardInset ? keyboardInset + 8 : 8 }}
|
||||
>
|
||||
{claimId && !hideClaimLabel && (
|
||||
<p style={{ marginBottom: 12, color: '#666', fontSize: 13 }}>По обращению №{claimId}</p>
|
||||
)}
|
||||
@@ -251,7 +303,7 @@ export default function SupportChat({
|
||||
label="Сообщение"
|
||||
rules={[{ required: true, message: 'Введите текст' }]}
|
||||
>
|
||||
<TextArea rows={compact ? 3 : 5} placeholder="Опишите вопрос..." maxLength={5000} showCount />
|
||||
<TextArea rows={compact ? 3 : 5} placeholder="Опишите вопрос..." maxLength={5000} showCount onFocus={scrollInputIntoView} />
|
||||
</Form.Item>
|
||||
<Form.Item name="subject" label="Тема (необязательно)">
|
||||
<Input placeholder="Краткая тема" maxLength={200} />
|
||||
@@ -313,6 +365,7 @@ export default function SupportChat({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 12,
|
||||
paddingBottom: keyboardInset ? keyboardInset + 8 : 8,
|
||||
}}
|
||||
>
|
||||
{messages.map((msg) => (
|
||||
@@ -345,13 +398,26 @@ export default function SupportChat({
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
<Form form={form} onFinish={handleSend} style={{ flexShrink: 0 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<div
|
||||
ref={inputBarRef}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 8,
|
||||
paddingTop: 8,
|
||||
borderTop: '1px solid #f0f0f0',
|
||||
boxShadow: '0 -2px 8px rgba(0,0,0,0.04)',
|
||||
paddingBottom: keyboardInset ? keyboardInset : 0,
|
||||
background: '#fff',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'flex-end' }}>
|
||||
<Form.Item name="message" style={{ flex: 1, marginBottom: 0 }}>
|
||||
<TextArea
|
||||
placeholder="Сообщение..."
|
||||
autoSize={{ minRows: 1, maxRows: 4 }}
|
||||
autoSize={{ minRows: 2, maxRows: 6 }}
|
||||
maxLength={5000}
|
||||
onFocus={scrollInputIntoView}
|
||||
onPressEnter={(e) => {
|
||||
if (!e.shiftKey) {
|
||||
e.preventDefault();
|
||||
@@ -371,9 +437,10 @@ export default function SupportChat({
|
||||
<Button
|
||||
type="button"
|
||||
icon={<Paperclip size={18} />}
|
||||
size="large"
|
||||
onClick={() => document.getElementById('support-chat-files-chat')?.click()}
|
||||
/>
|
||||
<Button type="primary" htmlType="submit" loading={submitting}>
|
||||
<Button type="primary" htmlType="submit" loading={submitting} size="large">
|
||||
Отправить
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -4,15 +4,26 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
overflow-x: hidden;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background: #ffffff;
|
||||
overflow-x: hidden;
|
||||
max-width: 100%;
|
||||
position: relative;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
#root {
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Button, List, Spin, Typography } from 'antd';
|
||||
import { Button, List, Spin, Typography, message } from 'antd';
|
||||
import { ArrowLeft, MessageCirclePlus } from 'lucide-react';
|
||||
import SupportChat from '../components/SupportChat';
|
||||
|
||||
@@ -37,11 +37,18 @@ interface SupportProps {
|
||||
onNavigate?: (path: string) => void;
|
||||
}
|
||||
|
||||
function getViewportHeight(): number {
|
||||
if (typeof window === 'undefined') return 600;
|
||||
const vv = window.visualViewport;
|
||||
return (vv?.height ?? window.innerHeight) || 600;
|
||||
}
|
||||
|
||||
export default function Support({ onNavigate }: SupportProps) {
|
||||
const [view, setView] = useState<'list' | 'chat'>('list');
|
||||
const [threads, setThreads] = useState<SupportThreadItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedClaimId, setSelectedClaimId] = useState<string | null | undefined>(undefined);
|
||||
const [viewportHeight, setViewportHeight] = useState(getViewportHeight);
|
||||
|
||||
useEffect(() => {
|
||||
if (view !== 'list') return;
|
||||
@@ -53,11 +60,24 @@ export default function Support({ onNavigate }: SupportProps) {
|
||||
const params = new URLSearchParams();
|
||||
params.set('session_token', token);
|
||||
fetch(`/api/v1/support/threads?${params.toString()}`)
|
||||
.then((res) => (res.ok ? res.json() : { threads: [] }))
|
||||
.then((res) => {
|
||||
if (res.status === 401) {
|
||||
message.error('Сессия истекла. Обновите страницу или войдите снова.');
|
||||
return { threads: [] };
|
||||
}
|
||||
if (!res.ok) {
|
||||
message.error('Не удалось загрузить список обращений. Попробуйте позже.');
|
||||
return { threads: [] };
|
||||
}
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => {
|
||||
setThreads(data.threads || []);
|
||||
})
|
||||
.catch(() => setThreads([]))
|
||||
.catch(() => {
|
||||
message.error('Ошибка соединения. Проверьте интернет и попробуйте снова.');
|
||||
setThreads([]);
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}, [view]);
|
||||
|
||||
@@ -84,29 +104,70 @@ export default function Support({ onNavigate }: SupportProps) {
|
||||
return () => window.removeEventListener('miniapp:goBack', onGoBack);
|
||||
}, [view, onNavigate]);
|
||||
|
||||
useEffect(() => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('miniapp:supportChatMode', {
|
||||
detail: { active: view === 'chat' },
|
||||
}),
|
||||
);
|
||||
return () => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('miniapp:supportChatMode', {
|
||||
detail: { active: false },
|
||||
}),
|
||||
);
|
||||
};
|
||||
}, [view]);
|
||||
|
||||
useEffect(() => {
|
||||
if (view !== 'chat') return;
|
||||
const vv = window.visualViewport;
|
||||
if (!vv) return;
|
||||
const update = () => setViewportHeight(getViewportHeight());
|
||||
update();
|
||||
vv.addEventListener('resize', update);
|
||||
vv.addEventListener('scroll', update);
|
||||
return () => {
|
||||
vv.removeEventListener('resize', update);
|
||||
vv.removeEventListener('scroll', update);
|
||||
};
|
||||
}, [view]);
|
||||
|
||||
if (view === 'chat') {
|
||||
return (
|
||||
<div style={{ padding: 24, maxWidth: 560, margin: '0 auto' }}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ArrowLeft size={18} />}
|
||||
onClick={handleBack}
|
||||
style={{ marginBottom: 16, paddingLeft: 0 }}
|
||||
>
|
||||
К списку обращений
|
||||
</Button>
|
||||
<SupportChat
|
||||
claimId={selectedClaimId === null ? undefined : selectedClaimId ?? undefined}
|
||||
source="bar"
|
||||
onSuccess={() => {
|
||||
handleBack();
|
||||
if (onNavigate) onNavigate('/hello');
|
||||
else {
|
||||
window.history.pushState({}, '', '/hello');
|
||||
window.dispatchEvent(new PopStateEvent('popstate'));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
height: viewportHeight,
|
||||
overflow: 'auto',
|
||||
WebkitOverflowScrolling: 'touch',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<div style={{ padding: 24, maxWidth: 560, margin: '0 auto', flex: '1 1 auto', minHeight: 0, display: 'flex', flexDirection: 'column' }}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ArrowLeft size={18} />}
|
||||
onClick={handleBack}
|
||||
style={{ marginBottom: 16, paddingLeft: 0, flexShrink: 0 }}
|
||||
>
|
||||
К списку обращений
|
||||
</Button>
|
||||
<div style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
|
||||
<SupportChat
|
||||
claimId={selectedClaimId === null ? undefined : selectedClaimId ?? undefined}
|
||||
source="bar"
|
||||
onSuccess={() => {
|
||||
handleBack();
|
||||
if (onNavigate) onNavigate('/hello');
|
||||
else {
|
||||
window.history.pushState({}, '', '/hello');
|
||||
window.dispatchEvent(new PopStateEvent('popstate'));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -120,19 +181,23 @@ export default function Support({ onNavigate }: SupportProps) {
|
||||
Ваши обращения и переписка с поддержкой.
|
||||
</Text>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<MessageCirclePlus size={18} style={{ marginRight: 6 }} />}
|
||||
onClick={() => handleOpenThread(null)}
|
||||
style={{ marginBottom: 24, width: '100%' }}
|
||||
>
|
||||
Новое обращение
|
||||
</Button>
|
||||
{getSessionToken() && (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<MessageCirclePlus size={18} style={{ marginRight: 6 }} />}
|
||||
onClick={() => handleOpenThread(null)}
|
||||
style={{ marginBottom: 24, width: '100%' }}
|
||||
>
|
||||
Новое обращение
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<div style={{ textAlign: 'center', padding: 48 }}>
|
||||
<Spin />
|
||||
</div>
|
||||
) : !getSessionToken() ? (
|
||||
<Text type="secondary">Войдите в аккаунт, чтобы видеть обращения и писать в поддержку.</Text>
|
||||
) : threads.length === 0 ? (
|
||||
<Text type="secondary">Пока нет обращений. Нажмите «Новое обращение», чтобы написать.</Text>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user