import { renderHeader } from '../components/header.js'; import { authService, state } from '../state.js'; import { toUserMessage } from '../services/ui-error-texts.js'; import { normalizeChannelDescription } from '../services/channel-name-rules.js'; export const pageMeta = { id: 'add-personal-public-chat-view', title: 'Новый персональный публичный чат' }; const CREATE_CHANNEL_FLASH_KEY = 'shine-channels-create-success'; const CHANNEL_TYPE_PERSONAL = 100; function persistCreateSuccessFlash(message) { try { sessionStorage.setItem(CREATE_CHANNEL_FLASH_KEY, String(message || '').trim()); } catch { // ignore storage errors } } function normalizeLoginInput(value) { return String(value || '').trim().replace(/^@+/, ''); } function isValidLogin(value) { const clean = normalizeLoginInput(value); if (!clean) return false; if (clean.length < 1 || clean.length > 20) return false; return /^[A-Za-z0-9_]+$/.test(clean); } function validateDescription(value) { const normalized = normalizeChannelDescription(value); const bytes = new TextEncoder().encode(normalized).length; if (bytes > 200) { return { ok: false, normalized, bytes, error: 'Описание слишком длинное: максимум 200 байт UTF-8.' }; } return { ok: true, normalized, bytes, error: '' }; } function createDebounced(fn, delayMs = 240) { let timer = null; return (...args) => { if (timer) clearTimeout(timer); timer = setTimeout(() => { timer = null; fn(...args); }, delayMs); }; } export function render({ navigate }) { const screen = document.createElement('section'); screen.className = 'stack channels-screen channels-screen--add'; screen.append( renderHeader({ title: 'Новый персональный публичный чат', leftAction: { label: '<', onClick: () => navigate('channels-list/dialogs') }, }), ); const form = document.createElement('form'); form.className = 'card stack'; form.innerHTML = ` Создание персонального публичного чата

Тип канала фиксирован: персональный (100).

0 / 200 байт
Публичные чаты могут просматривать любые пользователи, и сообщения сохраняются в блокчейне. Для личной приватной переписки используйте вкладку «Личные сообщения».
`; const loginEl = form.querySelector('#chat-login'); const suggestEl = form.querySelector('#chat-login-suggest'); const loginErrorEl = form.querySelector('#chat-login-error'); const descriptionEl = form.querySelector('#chat-description'); const descriptionErrorEl = form.querySelector('#chat-description-error'); const descriptionCounterEl = form.querySelector('#chat-description-counter'); const errorEl = form.querySelector('#chat-create-error'); const submitEl = form.querySelector('#submit-create-chat'); const cancelEl = form.querySelector('#cancel-create-chat'); let submitInFlight = false; let selectedCanonicalLogin = ''; const setBusy = (busy) => { submitInFlight = !!busy; submitEl.disabled = submitInFlight; cancelEl.disabled = submitInFlight; loginEl.disabled = submitInFlight; descriptionEl.disabled = submitInFlight; submitEl.textContent = submitInFlight ? 'Создаём...' : 'Создать чат'; }; const renderLoginSuggestions = (logins) => { suggestEl.innerHTML = ''; const rows = Array.isArray(logins) ? logins.filter(Boolean) : []; if (!rows.length) { suggestEl.style.display = 'none'; return; } suggestEl.style.display = ''; rows.slice(0, 8).forEach((login) => { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'channel-search-item'; btn.textContent = String(login); btn.addEventListener('click', () => { selectedCanonicalLogin = String(login); loginEl.value = selectedCanonicalLogin; suggestEl.style.display = 'none'; }); suggestEl.append(btn); }); }; const updateValidation = () => { const loginRaw = String(loginEl.value || '').trim(); const loginOk = isValidLogin(loginRaw); const descriptionCheck = validateDescription(descriptionEl.value); loginErrorEl.textContent = loginOk ? '' : 'Логин: 1-20 символов, латиница/цифры/_.'; descriptionErrorEl.textContent = descriptionCheck.error; descriptionCounterEl.textContent = `${Number(descriptionCheck.bytes || 0)} / 200 байт`; const ok = loginOk && descriptionCheck.ok; submitEl.disabled = submitInFlight || !ok; return { ok, description: descriptionCheck.normalized }; }; const refreshSuggestions = createDebounced(async () => { if (submitInFlight) return; const loginRaw = normalizeLoginInput(loginEl.value); if (loginRaw.length < 1) { suggestEl.style.display = 'none'; suggestEl.innerHTML = ''; return; } try { const logins = await authService.searchUsers(loginRaw); renderLoginSuggestions(logins); } catch { suggestEl.style.display = 'none'; suggestEl.innerHTML = ''; } }, 220); loginEl.addEventListener('input', () => { selectedCanonicalLogin = ''; errorEl.textContent = ''; updateValidation(); refreshSuggestions(); }); descriptionEl.addEventListener('input', updateValidation); form.addEventListener('submit', async (event) => { event.preventDefault(); if (submitInFlight) return; const login = state.session.login; const storagePwd = state.session.storagePwdInMemory; if (!login || !storagePwd) { errorEl.textContent = 'Сессия недействительна. Выполните вход заново.'; return; } const validation = updateValidation(); if (!validation.ok) return; setBusy(true); errorEl.textContent = ''; loginErrorEl.textContent = ''; try { const inputLogin = normalizeLoginInput(loginEl.value); const foundUser = await authService.getUser(inputLogin); if (!foundUser?.exists) { throw new Error('Пользователь с таким логином не найден.'); } const canonicalLogin = String(foundUser?.login || inputLogin).trim(); if (!canonicalLogin) throw new Error('Не удалось определить логин пользователя.'); await authService.addBlockCreateChannel({ login, storagePwd, channelName: canonicalLogin, channelDescription: validation.description, channelType: CHANNEL_TYPE_PERSONAL, channelTypeVersion: 1, }); persistCreateSuccessFlash(`Публичный чат с "${canonicalLogin}" создан.`); navigate('channels-list/dialogs'); } catch (error) { errorEl.textContent = toUserMessage(error, 'Не удалось создать персональный публичный чат.'); setBusy(false); updateValidation(); } }); cancelEl.addEventListener('click', () => navigate('channels-list/dialogs')); screen.append(form); loginEl.focus(); updateValidation(); return screen; }