SHiNE-server/shine-UI/js/pages/add-personal-public-chat-view.js

224 lines
8.4 KiB
JavaScript
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 { 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 = `
<strong class="channel-head-title">Создание персонального публичного чата</strong>
<p class="channel-head-meta">Тип канала фиксирован: персональный (100).</p>
<label for="chat-login">Введите логин другого пользователя</label>
<input id="chat-login" class="input" maxlength="20" placeholder="Например: aidar" autocomplete="off" required />
<div id="chat-login-suggest" class="channels-search-suggest" style="display:none"></div>
<div id="chat-login-error" class="meta-muted inline-error"></div>
<label for="chat-description">Добавить описание этому публичному чату (необязательно)</label>
<textarea id="chat-description" class="input" rows="4" maxlength="400" placeholder="Короткое описание"></textarea>
<div class="meta-muted" id="chat-description-counter">0 / 200 байт</div>
<div id="chat-description-error" class="meta-muted inline-error"></div>
<div class="card meta-muted">
Публичные чаты могут просматривать любые пользователи, и сообщения сохраняются в блокчейне.
Для личной приватной переписки используйте вкладку «Личные сообщения».
</div>
<div id="chat-create-error" class="meta-muted inline-error"></div>
<div class="form-actions-grid">
<button type="button" class="secondary-btn" id="cancel-create-chat">Отмена</button>
<button type="submit" class="primary-btn" id="submit-create-chat">Создать чат</button>
</div>
`;
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;
}