diff --git a/VERSION.properties b/VERSION.properties
index a33be7a..54add94 100644
--- a/VERSION.properties
+++ b/VERSION.properties
@@ -1,2 +1,2 @@
-client.version=1.2.42
-server.version=1.2.36
+client.version=1.2.43
+server.version=1.2.37
diff --git a/shine-UI/js/pages/add-channel-view.js b/shine-UI/js/pages/add-channel-view.js
index 3cd4e7b..475f40a 100644
--- a/shine-UI/js/pages/add-channel-view.js
+++ b/shine-UI/js/pages/add-channel-view.js
@@ -44,11 +44,11 @@ export function render({ navigate }) {
form.className = 'card stack';
form.innerHTML = `
Создание канала
-
Можно использовать кириллицу, латиницу, цифры, пробел, _ и -.
+ Можно использовать только латиницу, цифры, _ и -.
Длина названия: от 3 до 32 символов. Название уникально во всей системе.
-
+
@@ -124,18 +124,14 @@ export function render({ navigate }) {
errorEl.textContent = '';
try {
- const created = await authService.addBlockCreateChannel({
+ await authService.addBlockCreateChannel({
login,
storagePwd,
channelName: normalizeChannelDisplayName(check.name),
channelDescription: normalizeChannelDescription(check.description),
});
- const baseMessage = `Канал "${normalizeChannelDisplayName(check.name)}" создан.`;
- const successMessage = created?.usedLegacyDescriptionFallback && created?.savedDescriptionViaUserParam
- ? `${baseMessage} Описание сохранено через блок параметра.`
- : baseMessage;
- persistCreateSuccessFlash(successMessage);
+ persistCreateSuccessFlash(`Канал "${normalizeChannelDisplayName(check.name)}" создан.`);
navigate('channels-list');
} catch (error) {
errorEl.textContent = toUserMessage(error, 'Не удалось создать канал.');
diff --git a/shine-UI/js/pages/channel-view.js b/shine-UI/js/pages/channel-view.js
index df91883..c2cc1ee 100644
--- a/shine-UI/js/pages/channel-view.js
+++ b/shine-UI/js/pages/channel-view.js
@@ -123,41 +123,6 @@ function buildAbsoluteRouteUrl(routePath = '') {
return url.toString();
}
-function channelDescriptionParamKey(selector) {
- const owner = String(selector?.ownerBlockchainName || '').trim();
- const rootNo = Number(selector?.channelRootBlockNumber);
- const rootHash = normalizeRouteHash(selector?.channelRootBlockHash);
- if (!owner || !Number.isFinite(rootNo)) return '';
- return `channel_desc:${owner}:${rootNo}:${rootHash}`;
-}
-
-function parseDescriptionOverride(payload) {
- if (!payload || typeof payload !== 'object') {
- return { hasOverride: false, description: '' };
- }
-
- const rawValue = String(payload?.value ?? payload?.param_value ?? '').trim();
- if (!rawValue && !Number(payload?.time_ms || payload?.timeMs || 0)) {
- return { hasOverride: false, description: '' };
- }
-
- if (!rawValue) {
- return { hasOverride: true, description: '' };
- }
-
- try {
- const parsed = JSON.parse(rawValue);
- if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
- const value = typeof parsed.v === 'string' ? parsed.v : '';
- return { hasOverride: true, description: value.trim() };
- }
- } catch {
- // legacy raw string value
- }
-
- return { hasOverride: true, description: rawValue };
-}
-
function buildSelectorFromRoute(route, channelId) {
const params = route?.params || {};
@@ -410,88 +375,6 @@ function openAddMessageModal({ channelName, onSubmit }) {
if (textEl) textEl.focus();
}
-function openEditDescriptionModal({ initialValue = '', onSubmit }) {
- const root = document.getElementById('modal-root');
- root.innerHTML = `
-
-
-
Описание канала
-
-
0 / 200 байт
-
-
-
-
-
-
-
- `;
-
- const textEl = root.querySelector('#channel-description-text');
- const counterEl = root.querySelector('#channel-description-counter');
- const errorEl = root.querySelector('#channel-description-error');
- const submitEl = root.querySelector('#channel-description-submit');
- const cancelEl = root.querySelector('#channel-description-cancel');
-
- let inFlight = false;
-
- const compute = () => {
- const value = String(textEl?.value || '').replace(/\s+/g, ' ').trim();
- const bytes = new TextEncoder().encode(value).length;
- const ok = bytes <= 200;
- return {
- value,
- bytes,
- ok,
- error: ok ? '' : 'Описание слишком длинное: максимум 200 байт UTF-8.',
- };
- };
-
- const setBusy = (busy) => {
- inFlight = !!busy;
- submitEl.disabled = inFlight;
- cancelEl.disabled = inFlight;
- if (textEl) textEl.disabled = inFlight;
- submitEl.textContent = inFlight ? 'Сохраняем...' : 'Сохранить';
- };
-
- const close = () => {
- root.innerHTML = '';
- };
-
- const updateValidation = () => {
- const check = compute();
- counterEl.textContent = `${check.bytes} / 200 байт`;
- errorEl.textContent = check.error;
- submitEl.disabled = inFlight || !check.ok;
- return check;
- };
-
- cancelEl?.addEventListener('click', close);
- textEl?.addEventListener('input', updateValidation);
- submitEl?.addEventListener('click', async () => {
- if (inFlight) return;
- const check = updateValidation();
- if (!check.ok) return;
-
- setBusy(true);
- errorEl.textContent = '';
- try {
- await onSubmit(check.value);
- close();
- } catch (error) {
- setBusy(false);
- errorEl.textContent = toUserMessage(error, 'Не удалось сохранить описание.');
- }
- });
-
- if (textEl) {
- textEl.value = String(initialValue || '');
- textEl.focus();
- }
- updateValidation();
-}
-
function mapApiMessageToPost(message, selector, localNumber) {
const blockNumber = toSafeInt(message?.messageRef?.blockNumber);
const blockHash = normalizeMessageHash(message?.messageRef?.blockHash);
@@ -552,27 +435,11 @@ async function loadFromApi(route, channelId) {
const posts = messages.map((message, index) => mapApiMessageToPost(message, selector, index + 1));
const ownerLogin = String(payload.channel?.ownerLogin || '').trim();
- const readDescription = async () => {
- const sourceDescription = String(payload.channel?.channelDescription || '').trim();
- const paramKey = channelDescriptionParamKey(selector);
- if (!ownerLogin || !paramKey) return sourceDescription;
-
- try {
- const paramPayload = await authService.getUserParam(ownerLogin, paramKey);
- const override = parseDescriptionOverride(paramPayload);
- return override.hasOverride ? override.description : sourceDescription;
- } catch {
- return sourceDescription;
- }
- };
-
- const resolvedDescription = await readDescription();
-
return {
channel: {
name: payload.channel?.channelName || 'неизвестный канал',
displayName: `${ownerLogin || 'неизвестно'}/${payload.channel?.channelName || 'неизвестный канал'}`,
- description: resolvedDescription,
+ description: String(payload.channel?.channelDescription || '').trim(),
ownerName: ownerLogin || 'неизвестно',
},
posts,
@@ -818,22 +685,6 @@ function renderBody(screen, navigate, routeKey, channelData, handlers) {
});
headActions.append(aboutButton);
- if (channelData.isOwnChannel) {
- const editButton = document.createElement('button');
- editButton.type = 'button';
- editButton.className = 'secondary-btn small-btn';
- editButton.textContent = '✎';
- editButton.title = 'Редактировать описание';
- editButton.addEventListener('click', (event) => {
- animatePress(event.currentTarget);
- openEditDescriptionModal({
- initialValue: channelData.channel.description || '',
- onSubmit: async (nextValue) => handlers.onEditDescription(nextValue),
- });
- });
- headActions.append(editButton);
- }
-
head.append(title);
head.append(owner, headActions);
@@ -1024,25 +875,6 @@ export function render({ navigate, route }) {
rerender();
};
- const onEditDescription = async (descriptionText) => {
- const { login, storagePwd } = requireSigningSession();
- const selector = routeSelector;
- const param = channelDescriptionParamKey(selector);
- if (!param) throw new Error('Идентификатор канала не готов для обновления описания.');
-
- const value = JSON.stringify({ v: String(descriptionText || '').trim() });
- await authService.addBlockUserParam({
- login,
- storagePwd,
- param,
- value,
- });
-
- softHaptic(10);
- showToast('Описание канала сохранено');
- rerender();
- };
-
screen.append(
renderHeader({
title: '',
@@ -1085,14 +917,6 @@ export function render({ navigate, route }) {
}
},
onShare: onShare,
- onEditDescription: async (descriptionText) => {
- try {
- await onEditDescription(descriptionText);
- showStatus('');
- } catch (error) {
- throw new Error(toUserMessage(error, 'Не удалось сохранить описание.'));
- }
- },
onSubscribeChannel: async (event) => {
animatePress(event?.currentTarget);
try {
diff --git a/shine-UI/js/services/auth-service.js b/shine-UI/js/services/auth-service.js
index e789dec..ea54d9a 100644
--- a/shine-UI/js/services/auth-service.js
+++ b/shine-UI/js/services/auth-service.js
@@ -92,27 +92,6 @@ function opError(op, response) {
return error;
}
-function isLegacyCreateChannelFormatError(error) {
- const code = String(error?.code || '').trim().toUpperCase();
- const text = String(error?.message || '').toLowerCase();
- if (code === 'BAD_BLOCK_FORMAT') return true;
- return (
- text.includes('unknown body type/version') ||
- text.includes('unknown tech body type/version/subtype') ||
- text.includes('bad_block_format')
- );
-}
-
-function channelDescriptionParamKeyFromSelector(selector) {
- const owner = String(selector?.ownerBlockchainName || '').trim();
- const rootNo = Number(selector?.channelRootBlockNumber);
- const rootHash = String(selector?.channelRootBlockHash || '').trim().toLowerCase();
- if (!owner || !Number.isFinite(rootNo) || rootNo < 0 || !/^[0-9a-f]{64}$/.test(rootHash)) {
- return '';
- }
- return `channel_desc:${owner}:${rootNo}:${rootHash}`;
-}
-
function makeClientInfo() {
const ua = navigator.userAgent || 'unknown';
return ua.slice(0, 50);
@@ -416,26 +395,6 @@ function makeConnectionBodyBytes({
);
}
-function makeCreateChannelBodyBytes({ lineCode, prevLineNumber, prevLineHashHex, thisLineNumber, channelName }) {
- const check = validateChannelDisplayName(channelName);
- if (!check.ok) throw new Error(channelNameErrorText(check.code));
- const cleanName = check.normalized;
-
- const nameBytes = utf8Bytes(cleanName);
- if (nameBytes.length < 1 || nameBytes.length > 255) {
- throw new Error('Channel name must be 1..255 bytes');
- }
-
- return concatBytes(
- int32Bytes(lineCode),
- int32Bytes(prevLineNumber),
- hexToBytes(normalizeHex32(prevLineHashHex)),
- int32Bytes(thisLineNumber),
- int8Byte(nameBytes.length),
- nameBytes
- );
-}
-
function normalizeChannelDescription(value) {
const text = String(value == null ? '' : value).trim().replace(/\s+/g, ' ');
const bytes = utf8Bytes(text);
@@ -1088,44 +1047,21 @@ export class AuthService {
thisLineNumber = createdChannels.length + 1;
}
- const submitCreate = async (useV2) => {
- const bodyBytes = useV2
- ? makeCreateChannelBodyV2Bytes({
- lineCode: 0,
- prevLineNumber,
- prevLineHashHex,
- thisLineNumber,
- channelName: cleanChannelName,
- channelDescription: cleanChannelDescription,
- })
- : makeCreateChannelBodyBytes({
- lineCode: 0,
- prevLineNumber,
- prevLineHashHex,
- thisLineNumber,
- channelName: cleanChannelName,
- });
-
- return this.addBlockSigned({
- login: cleanLogin,
- storagePwd,
- msgType: MSG_TYPE_TECH,
- msgSubType: MSG_SUBTYPE_TECH_CREATE_CHANNEL,
- msgVersion: useV2 ? CREATE_CHANNEL_BODY_VERSION : 1,
- bodyBytes,
- });
- };
-
- let payload;
- let usedLegacyDescriptionFallback = false;
- let savedDescriptionViaUserParam = false;
- try {
- payload = await submitCreate(true);
- } catch (error) {
- if (!isLegacyCreateChannelFormatError(error)) throw error;
- payload = await submitCreate(false);
- usedLegacyDescriptionFallback = true;
- }
+ const payload = await this.addBlockSigned({
+ login: cleanLogin,
+ storagePwd,
+ msgType: MSG_TYPE_TECH,
+ msgSubType: MSG_SUBTYPE_TECH_CREATE_CHANNEL,
+ msgVersion: CREATE_CHANNEL_BODY_VERSION,
+ bodyBytes: makeCreateChannelBodyV2Bytes({
+ lineCode: 0,
+ prevLineNumber,
+ prevLineHashHex,
+ thisLineNumber,
+ channelName: cleanChannelName,
+ channelDescription: cleanChannelDescription,
+ }),
+ });
const selector = {
ownerBlockchainName: blockchainName,
@@ -1133,24 +1069,8 @@ export class AuthService {
channelRootBlockHash: normalizeHex32(payload?.serverLastGlobalHash, ZERO64),
};
- if (usedLegacyDescriptionFallback && cleanChannelDescription) {
- const param = channelDescriptionParamKeyFromSelector(selector);
- if (!param) {
- throw new Error('Не удалось сохранить описание канала: некорректный идентификатор канала.');
- }
- await this.addBlockUserParam({
- login: cleanLogin,
- storagePwd,
- param,
- value: JSON.stringify({ v: cleanChannelDescription }),
- });
- savedDescriptionViaUserParam = true;
- }
-
return {
...payload,
- usedLegacyDescriptionFallback,
- savedDescriptionViaUserParam,
channel: {
...selector,
},
diff --git a/shine-UI/js/services/channel-name-rules.js b/shine-UI/js/services/channel-name-rules.js
index de45ea6..80ba197 100644
--- a/shine-UI/js/services/channel-name-rules.js
+++ b/shine-UI/js/services/channel-name-rules.js
@@ -1,10 +1,10 @@
const MIN_LEN = 3;
const MAX_LEN = 32;
-const ALLOWED_CHARS_RE = /^[\p{Script=Latin}\p{Script=Cyrillic}0-9 _-]+$/u;
+const ALLOWED_CHARS_RE = /^[A-Za-z0-9_-]+$/;
export function normalizeChannelDisplayName(value) {
if (value == null) return '';
- return String(value).trim().replace(/\s+/g, ' ');
+ return String(value).trim();
}
export function normalizeChannelDescription(value) {
@@ -16,24 +16,9 @@ export function toCanonicalChannelSlug(value) {
const normalized = normalizeChannelDisplayName(value);
if (!normalized) return '';
- const lowered = normalized.toLowerCase().replace(/\u0451/g, '\u0435');
- let out = '';
- let pendingSeparator = false;
-
- for (const ch of lowered) {
- if (ch === ' ' || ch === '_' || ch === '-') {
- pendingSeparator = out.length > 0;
- continue;
- }
- if (!/[\p{Script=Latin}\p{Script=Cyrillic}0-9]/u.test(ch)) {
- return '';
- }
- if (pendingSeparator && out.length > 0) out += '-';
- out += ch;
- pendingSeparator = false;
- }
-
- return out.replace(/-+$/g, '');
+ const lowered = normalized.toLowerCase();
+ if (!ALLOWED_CHARS_RE.test(lowered)) return '';
+ return lowered;
}
export function validateChannelDisplayName(value) {
@@ -73,7 +58,7 @@ export function channelNameErrorText(code) {
case 'too_long':
return 'Название слишком длинное: максимум 32 символа.';
case 'bad_chars':
- return 'Разрешены кириллица, латиница, цифры, пробел, _ и -.';
+ return 'Разрешены только латиница, цифры, _ и -.';
case 'reserved':
return 'Название "0" зарезервировано.';
default:
diff --git a/shine-server-db/src/main/java/shine/db/channels/ChannelNameRules.java b/shine-server-db/src/main/java/shine/db/channels/ChannelNameRules.java
index 139f8d2..ee5aa2e 100644
--- a/shine-server-db/src/main/java/shine/db/channels/ChannelNameRules.java
+++ b/shine-server-db/src/main/java/shine/db/channels/ChannelNameRules.java
@@ -7,13 +7,13 @@ public final class ChannelNameRules {
private static final int MIN_DISPLAY_NAME_LENGTH = 3;
private static final int MAX_DISPLAY_NAME_LENGTH = 32;
private static final Pattern DISPLAY_ALLOWED_PATTERN =
- Pattern.compile("^[\\p{IsLatin}\\p{IsCyrillic}0-9 _-]+$");
+ Pattern.compile("^[A-Za-z0-9_-]+$");
private ChannelNameRules() {}
public static String normalizeDisplayName(String value) {
if (value == null) return "";
- return value.trim().replaceAll("\\s+", " ");
+ return value.trim();
}
public static String requireValidDisplayNameForCreate(String rawName) {
@@ -40,45 +40,10 @@ public final class ChannelNameRules {
throw new IllegalArgumentException("channelName is blank");
}
- String lowered = normalized.toLowerCase(Locale.ROOT).replace('\u0451', '\u0435');
- StringBuilder slug = new StringBuilder(lowered.length());
- boolean pendingSeparator = false;
-
- for (int i = 0; i < lowered.length(); ) {
- int cp = lowered.codePointAt(i);
- i += Character.charCount(cp);
-
- if (cp == ' ' || cp == '_' || cp == '-') {
- pendingSeparator = slug.length() > 0;
- continue;
- }
-
- if (!isLatinOrCyrillicOrDigit(cp)) {
- throw new IllegalArgumentException("channelName contains unsupported characters");
- }
-
- if (pendingSeparator && slug.length() > 0) {
- slug.append('-');
- }
- pendingSeparator = false;
- slug.appendCodePoint(cp);
+ String lowered = normalized.toLowerCase(Locale.ROOT);
+ if (!DISPLAY_ALLOWED_PATTERN.matcher(lowered).matches()) {
+ throw new IllegalArgumentException("channelName contains unsupported characters");
}
-
- int len = slug.length();
- if (len > 0 && slug.charAt(len - 1) == '-') {
- slug.deleteCharAt(len - 1);
- }
-
- if (slug.length() == 0) {
- throw new IllegalArgumentException("channelName canonical slug is empty");
- }
-
- return slug.toString();
- }
-
- private static boolean isLatinOrCyrillicOrDigit(int cp) {
- if (Character.isDigit(cp)) return true;
- Character.UnicodeScript script = Character.UnicodeScript.of(cp);
- return script == Character.UnicodeScript.LATIN || script == Character.UnicodeScript.CYRILLIC;
+ return lowered;
}
}