From 98140c1f71b45e04c74f6a3f0b999dc90128a6573183180fc2d6fcbdc39a85af Mon Sep 17 00:00:00 2001 From: AidarKC Date: Fri, 7 Aug 2026 14:20:18 +0400 Subject: [PATCH 1/3] =?UTF-8?q?UI:=20=D1=81=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=BA=D0=B0=D0=BD=D0=B0=D0=BB=D0=B0=20=D0=B8=20?= =?UTF-8?q?=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B8=20Solana?= 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 | 93 ++++++++++++++++--- shine-UI/js/pages/register-view.js | 5 + .../js/pages/registration-payment-view.js | 24 ++++- .../js/services/solana-register-service.js | 13 +++ shine-UI/styles/components.css | 67 +++++++++++++ 6 files changed, 188 insertions(+), 16 deletions(-) diff --git a/VERSION.properties b/VERSION.properties index 0452f76c..531b2876 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.5.1 +client.version=1.5.2 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 b6f3b4d7..0ec3b82a 100644 --- a/shine-UI/js/pages/add-channel-view.js +++ b/shine-UI/js/pages/add-channel-view.js @@ -8,8 +8,9 @@ import { } from '../services/channel-name-rules.js'; import { openArweaveAttachmentManager, markArweaveAttachmentPlaced } from '../components/arweave-attachment-manager.js'; import { buildArweaveDataUrl } from '../services/arweave-file-service.js'; +import { makeShineChannelRoute } from '../services/shine-routes.js'; -export const pageMeta = { id: 'add-channel-view', title: 'Создать канал' }; +export const pageMeta = { id: 'add-channel-view', title: 'Создание канала' }; const CREATE_CHANNEL_FLASH_KEY = 'shine-channels-create-success'; const CHANNEL_TYPE_PUBLIC = 1; @@ -48,13 +49,23 @@ function renderAvatarPreview(slot, avatar, title) { slot.append(wrap); } +function buildAbsoluteChannelUrl({ ownerBlockchainName = '', channelName = '' } = {}) { + const route = makeShineChannelRoute({ ownerBlockchainName, channelName }); + if (!route) return ''; + try { + return new URL(`/${route}`, window.location.origin).toString(); + } catch { + return `${window.location.origin}/${route}`; + } +} + export function render({ navigate }) { const screen = document.createElement('section'); screen.className = 'stack channels-screen channels-screen--add'; screen.append( renderHeader({ - title: 'Создать канал', + title: 'Создание канала', leftAction: { label: '<', onClick: () => navigate('channels-list') }, }), ); @@ -63,23 +74,31 @@ export function render({ navigate }) { form.className = 'card stack'; form.innerHTML = ` Создание канала -
- - +${isPending ? 'Аватар выбран и ещё не сохранён.' : 'Аватар установлен.'}
+ +${txId}
+ `; + }; + + const renderChannelLinkPreview = (nameValue) => { + const normalizedChannelName = normalizeChannelDisplayName(nameValue); + if (!ownerBlockchainName || !normalizedChannelName) { + linkPreviewEl.textContent = 'Прямая ссылка появится после ввода технического имени канала.'; + return; + } + const directUrl = buildAbsoluteChannelUrl({ + ownerBlockchainName, + channelName: normalizedChannelName, + }); + linkPreviewEl.innerHTML = `Прямая ссылка: ${directUrl}`; + }; const setBusy = (busy) => { submitInFlight = !!busy; @@ -134,7 +186,8 @@ export function render({ navigate }) { const ok = nameCheck.ok && titleCheck.ok && descriptionCheck.ok; submitEl.disabled = submitInFlight || !ok; - renderAvatarPreview(avatarPreviewEl, selectedAvatar, titleCheck.normalized || nameCheck.normalized); + renderAvatarStatus(nameCheck.normalized, titleCheck.normalized); + renderChannelLinkPreview(nameCheck.normalized); return { ok, @@ -149,19 +202,28 @@ export function render({ navigate }) { descriptionEl.addEventListener('input', updateValidation); avatarBtn.addEventListener('click', async () => { try { - selectedAvatar = await openArweaveAttachmentManager({ + const nextAvatar = await openArweaveAttachmentManager({ login: state.session.login, storagePwd: state.session.storagePwdInMemory, gateway: state.entrySettings.arweaveServer, mode: 'avatar', historyPurpose: 'avatar', }); + if (nextAvatar?.ar) selectedAvatar = nextAvatar; updateValidation(); } catch (error) { errorEl.textContent = toUserMessage(error, 'Не удалось выбрать аватар.'); } }); + avatarRemoveBtn.addEventListener('click', () => { + if (!selectedAvatar?.ar) return; + const confirmed = window.confirm('Убрать аватар и сделать канал без аватара?'); + if (!confirmed) return; + selectedAvatar = null; + updateValidation(); + }); + form.addEventListener('submit', async (event) => { event.preventDefault(); if (submitInFlight) return; @@ -206,5 +268,14 @@ export function render({ navigate }) { screen.append(form); nameEl.focus(); updateValidation(); + void authService.getUser(String(state.session.login || '').trim()) + .then((user) => { + ownerBlockchainName = String(user?.blockchainName || '').trim(); + renderChannelLinkPreview(nameEl.value); + }) + .catch(() => { + ownerBlockchainName = ''; + renderChannelLinkPreview(nameEl.value); + }); return screen; } diff --git a/shine-UI/js/pages/register-view.js b/shine-UI/js/pages/register-view.js index ff17e99f..edf8dd3f 100644 --- a/shine-UI/js/pages/register-view.js +++ b/shine-UI/js/pages/register-view.js @@ -4,6 +4,7 @@ import { toUserMessage } from '../services/ui-error-texts.js'; import { checkLoginExistsOnSolana, formatSolanaErrorDetails, + isSolanaRpcUnavailableError, precheckLoginClassOnSolana, } from '../services/solana-register-service.js'; import { @@ -251,6 +252,10 @@ export function render({ navigate }) { return isFree && className === 'free'; } catch (error) { if (runId !== loginCheckRunId) return false; + if (isSolanaRpcUnavailableError(error)) { + setStatusMessage('Нет связи с сервером Solana. Попробуйте ещё раз позже.', 'is-unavailable'); + return false; + } const base = toUserMessage(error, 'Не удалось проверить логин'); const details = formatSolanaErrorDetails(error); setStatusMessage(`${base}. Детали: ${details}`, 'is-unavailable'); diff --git a/shine-UI/js/pages/registration-payment-view.js b/shine-UI/js/pages/registration-payment-view.js index e7976e1f..5740749d 100644 --- a/shine-UI/js/pages/registration-payment-view.js +++ b/shine-UI/js/pages/registration-payment-view.js @@ -18,6 +18,7 @@ import { loadSolanaWeb3 } from '../vendor/solana-web3-loader.js'; import { checkLoginExistsOnSolana, formatSolanaErrorDetails, + isSolanaRpcUnavailableError, isUserAlreadyExistsSolanaError, registerUserOnSolana, } from '../services/solana-register-service.js'; @@ -311,6 +312,16 @@ export function render({ navigate }) { const keyBundle = state.registrationDraft.preGeneratedKeyBundle; if (!keyBundle) throw new Error('Ключи не найдены. Вернитесь на предыдущий шаг.'); + // Перед on-chain регистрацией перепроверяем логин ещё раз: + // между экраном выбора логина и оплатой его мог занять кто-то другой. + const loginStateBeforeRegister = await checkLoginExistsOnSolana({ + login: state.registrationDraft.login, + solanaEndpoint: state.entrySettings.solanaServer, + }); + if (loginStateBeforeRegister?.exists) { + throw new Error('Этот логин уже зарегистрирован. Войдите в существующий аккаунт или выберите другой логин.'); + } + // Регистрация на Solana (смарт контракт) submitButton.textContent = 'Регистрация в Solana...'; let registrationTxId = ''; @@ -325,15 +336,20 @@ export function render({ navigate }) { registrationTxId = String(registrationResult?.signature || '').trim(); } catch (solanaError) { const solanaMsg = formatSolanaErrorDetails(solanaError); - // Пользователь уже зарегистрирован в Solana — продолжаем - if (!solanaMsg.includes('already') && !isUserAlreadyExistsSolanaError(solanaError)) { - throw new Error(`Ошибка регистрации в Solana: ${solanaMsg}`); + if (solanaMsg.includes('already') || isUserAlreadyExistsSolanaError(solanaError)) { + throw new Error('Этот логин уже зарегистрирован. Войдите в существующий аккаунт или выберите другой логин.'); } + if (isSolanaRpcUnavailableError(solanaError)) { + throw new Error('Нет связи с сервером Solana. Попробуйте ещё раз позже.'); + } + throw new Error(`Ошибка регистрации в Solana: ${solanaMsg}`); } renderSolanaRegistrationStage({ navigate, status, keyBundle, registrationTxId }); } catch (error) { - const message = toUserMessage(error, 'Не удалось завершить регистрацию.'); + const message = isSolanaRpcUnavailableError(error) + ? 'Нет связи с сервером Solana. Попробуйте ещё раз позже.' + : toUserMessage(error, 'Не удалось завершить регистрацию.'); setAuthError(message); status.className = 'status-line is-unavailable'; status.textContent = message; diff --git a/shine-UI/js/services/solana-register-service.js b/shine-UI/js/services/solana-register-service.js index f9717a8f..f19306e6 100644 --- a/shine-UI/js/services/solana-register-service.js +++ b/shine-UI/js/services/solana-register-service.js @@ -72,6 +72,19 @@ export function formatSolanaErrorDetails(error) { return parts.join(' :: '); } +export function isSolanaRpcUnavailableError(error) { + const details = formatSolanaErrorDetails(error).toLowerCase(); + return details.includes('service unavailable') + || details.includes('503') + || details.includes('failed to fetch') + || details.includes('fetch failed') + || details.includes('networkerror') + || details.includes('network error') + || details.includes('rpc unavailable') + || details.includes('rpc error') + || details.includes('failed to get info about account'); +} + export function isUserAlreadyExistsSolanaError(error) { const details = formatSolanaErrorDetails(error); return details.includes('UserAlreadyExists') diff --git a/shine-UI/styles/components.css b/shine-UI/styles/components.css index 594907b3..17ab1625 100644 --- a/shine-UI/styles/components.css +++ b/shine-UI/styles/components.css @@ -6557,6 +6557,73 @@ html, body { overflow-x: hidden; } box-shadow: 0 0 22px rgba(212, 175, 55, 0.2), inset 0 0 8px rgba(212, 175, 55, 0.1); } +.channel-create-avatar-head { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + gap: 14px; + align-items: start; +} + +.channel-create-avatar-side { + min-width: 0; + display: grid; + gap: 10px; + align-content: start; +} + +.channel-create-avatar-status-row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 10px; + align-items: start; +} + +.channel-create-avatar-status { + min-width: 0; +} + +.channel-avatar-status-text { + margin: 0; + color: rgba(255, 236, 194, 0.95); + font-weight: 600; +} + +.channel-avatar-txid { + margin: 4px 0 0; + word-break: break-all; + color: rgba(255, 255, 255, 0.86); + font-size: 12px; +} + +.channel-avatar-remove-btn { + width: 26px; + min-width: 26px; + height: 26px; + padding: 0; + border: 1px solid rgba(255, 107, 107, 0.75); + border-radius: 8px; + background: rgba(150, 18, 18, 0.3); + color: #ff8b8b; + font-weight: 700; + line-height: 1; +} + +.channel-avatar-remove-btn:hover, +.channel-avatar-remove-btn:focus-visible { + background: rgba(180, 22, 22, 0.42); + box-shadow: 0 0 0 2px rgba(255, 107, 107, 0.16); +} + +.channel-avatar-remove-btn[hidden] { + display: none; +} + +@media (max-width: 420px) { + .channel-create-avatar-head { + grid-template-columns: 1fr; + } +} + /* ===== Final microinteractions: breathe cards + static energy buttons ===== */ @keyframes breatheCard { 0%, 100% { transform: translateY(0px); } From 3f402fbde7f5ed753c8ec0f489bed87f1d32cef5362e0898df54300590f83936 Mon Sep 17 00:00:00 2001 From: AidarKC Date: Fri, 7 Aug 2026 14:38:27 +0400 Subject: [PATCH 2/3] =?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', + }; } From 57a99b478a8524d96d82f500f05a3e00dfe791a24a42ab4da4c7295a144efc8a Mon Sep 17 00:00:00 2001 From: AidarKC Date: Fri, 7 Aug 2026 15:15:56 +0400 Subject: [PATCH 3/3] =?UTF-8?q?UI:=20=D1=83=D0=BB=D1=83=D1=87=D1=88=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D1=81=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8=D0=B5?= =?UTF-8?q?=20=D0=BA=D0=B0=D0=BD=D0=B0=D0=BB=D0=B0=20=D0=B8=20=D0=BE=D0=BA?= =?UTF-8?q?=D0=BD=D0=BE=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VERSION.properties | 2 +- .../components/arweave-attachment-manager.js | 20 +++++++---- shine-UI/js/pages/add-channel-view.js | 33 +++++++++++-------- shine-UI/styles/components.css | 32 +++++++++++++++--- 4 files changed, 61 insertions(+), 26 deletions(-) diff --git a/VERSION.properties b/VERSION.properties index a79938b7..64d6dd3f 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.5.3 +client.version=1.5.4 server.version=1.4.5 diff --git a/shine-UI/js/components/arweave-attachment-manager.js b/shine-UI/js/components/arweave-attachment-manager.js index 244601c2..c22207ed 100644 --- a/shine-UI/js/components/arweave-attachment-manager.js +++ b/shine-UI/js/components/arweave-attachment-manager.js @@ -314,6 +314,9 @@ export function openArweaveAttachmentManager({ let balanceInfo = null; let autoOpenedFileDialog = false; const isAvatarMode = String(mode || '') === 'avatar'; + if (isAvatarMode && !String(uploadTransport || '').trim()) { + selectedUploadTransport = 'turbo'; + } const historyPurposeMode = String(historyPurpose || '').trim(); const purposeFilter = isAvatarMode || historyPurposeMode === 'avatar' ? 'avatar' : 'attachment'; const selectedTxIdSet = new Set((Array.isArray(selectedTxIds) ? selectedTxIds : []).map((item) => String(item || '').trim()).filter(Boolean)); @@ -592,8 +595,8 @@ export function openArweaveAttachmentManager({