From 3f402fbde7f5ed753c8ec0f489bed87f1d32cef5362e0898df54300590f83936 Mon Sep 17 00:00:00 2001
From: AidarKC
Date: Fri, 7 Aug 2026 14:38:27 +0400
Subject: [PATCH] =?UTF-8?q?UI:=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8?=
=?UTF-8?q?=D1=82=D1=8C=20=D1=81=D1=81=D1=8B=D0=BB=D0=BA=D0=B8=20SHiNE=20?=
=?UTF-8?q?=D0=B8=20=D1=84=D0=BE=D1=80=D0=BC=D1=83=20=D0=BA=D0=B0=D0=BD?=
=?UTF-8?q?=D0=B0=D0=BB=D0=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
VERSION.properties | 2 +-
shine-UI/js/pages/add-channel-view.js | 81 +++++++++++++++++++++------
shine-UI/js/router.js | 22 ++++----
shine-UI/js/services/shine-routes.js | 40 ++++++++-----
4 files changed, 105 insertions(+), 40 deletions(-)
diff --git a/VERSION.properties b/VERSION.properties
index 531b2876..a79938b7 100644
--- a/VERSION.properties
+++ b/VERSION.properties
@@ -1,2 +1,2 @@
-client.version=1.5.2
+client.version=1.5.3
server.version=1.4.5
diff --git a/shine-UI/js/pages/add-channel-view.js b/shine-UI/js/pages/add-channel-view.js
index 0ec3b82a..200e1974 100644
--- a/shine-UI/js/pages/add-channel-view.js
+++ b/shine-UI/js/pages/add-channel-view.js
@@ -6,7 +6,11 @@ import {
normalizeChannelDisplayName,
validateChannelDisplayName,
} from '../services/channel-name-rules.js';
-import { openArweaveAttachmentManager, markArweaveAttachmentPlaced } from '../components/arweave-attachment-manager.js';
+import {
+ getArweaveAttachmentAvailability,
+ openArweaveAttachmentManager,
+ markArweaveAttachmentPlaced,
+} from '../components/arweave-attachment-manager.js';
import { buildArweaveDataUrl } from '../services/arweave-file-service.js';
import { makeShineChannelRoute } from '../services/shine-routes.js';
@@ -14,6 +18,7 @@ export const pageMeta = { id: 'add-channel-view', title: 'Создание ка
const CREATE_CHANNEL_FLASH_KEY = 'shine-channels-create-success';
const CHANNEL_TYPE_PUBLIC = 1;
+const CHANNEL_LOGIN_HINT = 'Разрешены латинские буквы, цифры, _ и -. Длина: от 3 до 32 символов. Название не должно состоять только из цифр.';
function persistCreateSuccessFlash(message) {
try {
@@ -59,6 +64,21 @@ function buildAbsoluteChannelUrl({ ownerBlockchainName = '', channelName = '' }
}
}
+function sanitizeChannelLoginInput(value) {
+ const source = String(value || '');
+ let result = '';
+ for (const ch of source) {
+ if (/[A-Za-z0-9_-]/.test(ch)) result += ch;
+ }
+ return result.slice(0, 32);
+}
+
+function shortAvatarBlockchainAddress(value) {
+ const raw = String(value || '').trim();
+ if (raw.length <= 24) return raw;
+ return raw.slice(-24);
+}
+
export function render({ navigate }) {
const screen = document.createElement('section');
screen.className = 'stack channels-screen channels-screen--add';
@@ -73,9 +93,6 @@ export function render({ navigate }) {
const form = document.createElement('form');
form.className = 'card stack';
form.innerHTML = `
- Создание канала
-
-
${isPending ? 'Аватар выбран и ещё не сохранён.' : 'Аватар установлен.'}
- -${txId}
- `; + const shortTxId = shortAvatarBlockchainAddress(txId); + const isPending = avatarAvailabilityLabel === 'pending' || avatarAvailabilityTxId !== txId; + avatarStatusEl.innerHTML = `Адрес аватара в блокчейне Arweave ${shortTxId}${isPending ? ', ещё не обновился в блокчейне.' : '.'}
`; }; const renderChannelLinkPreview = (nameValue) => { @@ -161,6 +177,26 @@ export function render({ navigate }) { linkPreviewEl.innerHTML = `Прямая ссылка: ${directUrl}`; }; + const refreshAvatarAvailability = async () => { + const txId = String(selectedAvatar?.ar || '').trim(); + avatarAvailabilityTxId = txId; + avatarAvailabilityLabel = txId ? 'pending' : ''; + renderAvatarStatus(nameEl.value, titleEl.value); + if (!txId) return; + try { + const availability = await getArweaveAttachmentAvailability({ + gateway: state.entrySettings.arweaveServer, + attachment: selectedAvatar, + }); + if (avatarAvailabilityTxId !== txId) return; + avatarAvailabilityLabel = String(availability?.status || '').trim().toLowerCase(); + } catch { + if (avatarAvailabilityTxId !== txId) return; + avatarAvailabilityLabel = 'pending'; + } + renderAvatarStatus(nameEl.value, titleEl.value); + }; + const setBusy = (busy) => { submitInFlight = !!busy; submitEl.disabled = submitInFlight; @@ -177,7 +213,9 @@ export function render({ navigate }) { const titleCheck = normalizeMetaText(titleEl.value, 50, 'Название'); const descriptionCheck = normalizeMetaText(descriptionEl.value, 250, 'Описание'); - nameErrorEl.textContent = nameCheck.ok ? '' : channelNameErrorText(nameCheck.code); + nameErrorEl.textContent = invalidChannelLoginTyped + ? CHANNEL_LOGIN_HINT + : (nameCheck.ok ? '' : channelNameErrorText(nameCheck.code)); titleErrorEl.textContent = titleCheck.error; descriptionErrorEl.textContent = descriptionCheck.error; @@ -197,7 +235,13 @@ export function render({ navigate }) { }; }; - nameEl.addEventListener('input', updateValidation); + nameEl.addEventListener('input', () => { + const raw = String(nameEl.value || ''); + const sanitized = sanitizeChannelLoginInput(raw); + invalidChannelLoginTyped = raw !== sanitized; + if (raw !== sanitized) nameEl.value = sanitized; + updateValidation(); + }); titleEl.addEventListener('input', updateValidation); descriptionEl.addEventListener('input', updateValidation); avatarBtn.addEventListener('click', async () => { @@ -209,7 +253,10 @@ export function render({ navigate }) { mode: 'avatar', historyPurpose: 'avatar', }); - if (nextAvatar?.ar) selectedAvatar = nextAvatar; + if (nextAvatar?.ar) { + selectedAvatar = nextAvatar; + void refreshAvatarAvailability(); + } updateValidation(); } catch (error) { errorEl.textContent = toUserMessage(error, 'Не удалось выбрать аватар.'); @@ -221,6 +268,8 @@ export function render({ navigate }) { const confirmed = window.confirm('Убрать аватар и сделать канал без аватара?'); if (!confirmed) return; selectedAvatar = null; + avatarAvailabilityLabel = ''; + avatarAvailabilityTxId = ''; updateValidation(); }); diff --git a/shine-UI/js/router.js b/shine-UI/js/router.js index 457d1c89..e251792b 100644 --- a/shine-UI/js/router.js +++ b/shine-UI/js/router.js @@ -1,4 +1,4 @@ -import { parseShineRootSegment } from './services/shine-routes.js'; +import { parseShineRouteParts } from './services/shine-routes.js'; const ROOT_PAGES = ['messages-list', 'channels-list', 'network-view', 'notifications-view', 'profile-view']; const PRETTY_PATHS = new Map([ @@ -90,9 +90,11 @@ export function getRoute() { }; const isLikelyBlockNumber = (value) => /^[0-9]+$/.test(String(value || '').trim()); - const shineLogin = parseShineRootSegment(pageId); - if (shineLogin) { - const section = decodePart(segments[1] || '').toLowerCase(); + const shineRoute = parseShineRouteParts(segments); + if (shineRoute?.entity) { + const shineLogin = shineRoute.entity; + const baseOffset = Number(shineRoute.offset || 0); + const section = decodePart(segments[baseOffset] || '').toLowerCase(); if (!section) { return { pageId: 'user', params: { login: shineLogin, fromPage: 'messages-list', section: 'profile' } }; } @@ -100,7 +102,7 @@ export function getRoute() { return { pageId: 'network-view', params: { mode: 'keep-history', login: shineLogin } }; } if (section === 'channels') { - const sub = decodePart(segments[2] || '').toLowerCase(); + const sub = decodePart(segments[baseOffset + 1] || '').toLowerCase(); if (sub === 'owned') { return { pageId: 'channels-list', @@ -118,12 +120,12 @@ export function getRoute() { params: { mode: 'feed', login: shineLogin, scope: 'all' }, }; } - if (segments.length === 2 && isLikelyBlockNumber(segments[1])) { + if (segments.length === baseOffset + 1 && isLikelyBlockNumber(segments[baseOffset])) { return { pageId: 'channel-thread-view', params: { messageBlockchainName: shineLogin, - messageBlockNumber: segments[1] || '', + messageBlockNumber: segments[baseOffset] || '', messageBlockHash: '', channelOwnerBlockchainName: '', channelRootBlockNumber: '', @@ -131,10 +133,10 @@ export function getRoute() { }, }; } - if (segments.length >= 2) { + if (segments.length >= baseOffset + 1) { const ownerBlockchainName = shineLogin; - const channelName = decodePart(segments[1] || ''); - const messageBlockNumber = segments[2] || ''; + const channelName = decodePart(segments[baseOffset] || ''); + const messageBlockNumber = segments[baseOffset + 1] || ''; if (ownerBlockchainName && channelName && messageBlockNumber) { return { pageId: 'channel-thread-view', diff --git a/shine-UI/js/services/shine-routes.js b/shine-UI/js/services/shine-routes.js index f11af593..7cc605cb 100644 --- a/shine-UI/js/services/shine-routes.js +++ b/shine-UI/js/services/shine-routes.js @@ -10,7 +10,7 @@ function decodeRoutePart(value = '') { } } -const SHINE_ROUTE_PREFIX = 'SHiNE.'; +const SHINE_ROUTE_SEGMENT = 'SHiNE'; export function normalizeLogin(value = '') { return String(value || '').trim().replace(/^@+/, ''); @@ -25,21 +25,21 @@ export function extractLoginFromBlockchainName(value = '') { export function makeProfileRoute(login = '') { const clean = normalizeLogin(login); - return clean ? `${SHINE_ROUTE_PREFIX}${encodeRoutePart(clean)}` : 'profile'; + return clean ? `${SHINE_ROUTE_SEGMENT}/${encodeRoutePart(clean)}` : 'profile'; } export function makeProfileLinksRoute(login = '') { const clean = normalizeLogin(login); - return clean ? `${SHINE_ROUTE_PREFIX}${encodeRoutePart(clean)}/links` : 'network/keep-history'; + return clean ? `${SHINE_ROUTE_SEGMENT}/${encodeRoutePart(clean)}/links` : 'network/keep-history'; } export function makeProfileChannelsRoute(login = '', scope = 'all') { const clean = normalizeLogin(login); if (!clean) return 'channels/feed'; const normalizedScope = String(scope || '').trim().toLowerCase(); - if (normalizedScope === 'owned') return `${SHINE_ROUTE_PREFIX}${encodeRoutePart(clean)}/channels/owned`; - if (normalizedScope === 'following') return `${SHINE_ROUTE_PREFIX}${encodeRoutePart(clean)}/channels/following`; - return `${SHINE_ROUTE_PREFIX}${encodeRoutePart(clean)}/channels`; + if (normalizedScope === 'owned') return `${SHINE_ROUTE_SEGMENT}/${encodeRoutePart(clean)}/channels/owned`; + if (normalizedScope === 'following') return `${SHINE_ROUTE_SEGMENT}/${encodeRoutePart(clean)}/channels/following`; + return `${SHINE_ROUTE_SEGMENT}/${encodeRoutePart(clean)}/channels`; } export function makeShineChannelRoute({ ownerLogin = '', ownerBlockchainName = '', channelName = '', messageBlockNumber = '' }) { @@ -47,19 +47,33 @@ export function makeShineChannelRoute({ ownerLogin = '', ownerBlockchainName = ' const chName = String(channelName || '').trim(); const msgNo = String(messageBlockNumber || '').trim(); if (!ownerBch || !chName) return ''; - if (msgNo) return `${SHINE_ROUTE_PREFIX}${encodeRoutePart(ownerBch)}/${encodeRoutePart(chName)}/${encodeRoutePart(msgNo)}`; - return `${SHINE_ROUTE_PREFIX}${encodeRoutePart(ownerBch)}/${encodeRoutePart(chName)}`; + if (msgNo) return `${SHINE_ROUTE_SEGMENT}/${encodeRoutePart(ownerBch)}/${encodeRoutePart(chName)}/${encodeRoutePart(msgNo)}`; + return `${SHINE_ROUTE_SEGMENT}/${encodeRoutePart(ownerBch)}/${encodeRoutePart(chName)}`; } export function makeShineMessageRoute({ ownerLogin = '', messageBlockchainName = '', messageBlockNumber = '' }) { const msgBch = String(messageBlockchainName || '').trim(); const msgNo = String(messageBlockNumber || '').trim(); if (!msgBch || !msgNo) return ''; - return `${SHINE_ROUTE_PREFIX}${encodeRoutePart(msgBch)}/${encodeRoutePart(msgNo)}`; + return `${SHINE_ROUTE_SEGMENT}/${encodeRoutePart(msgBch)}/${encodeRoutePart(msgNo)}`; } -export function parseShineRootSegment(segment = '') { - const raw = String(segment || '').trim(); - if (!raw.toLowerCase().startsWith(SHINE_ROUTE_PREFIX.toLowerCase())) return ''; - return normalizeLogin(decodeRoutePart(raw.slice(SHINE_ROUTE_PREFIX.length))); +export function parseShineRouteParts(segments = []) { + const parts = Array.isArray(segments) ? segments.map((value) => String(value || '').trim()) : []; + if (!parts.length) return null; + + if (String(parts[0] || '').toLowerCase() === SHINE_ROUTE_SEGMENT.toLowerCase()) { + const entity = normalizeLogin(decodeRoutePart(parts[1] || '')); + if (!entity) return null; + return { entity, offset: 2, format: 'slash' }; + } + + const raw = String(parts[0] || '').trim(); + const legacyPrefix = `${SHINE_ROUTE_SEGMENT}.`; + if (!raw.toLowerCase().startsWith(legacyPrefix.toLowerCase())) return null; + return { + entity: normalizeLogin(decodeRoutePart(raw.slice(legacyPrefix.length))), + offset: 1, + format: 'dot', + }; }