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;
}