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', + }; }