From 8303682bd740c47f01c6148df70fe7d7cae9112c613bfc1d2e6118673b09a662 Mon Sep 17 00:00:00 2001 From: AidarKC Date: Sat, 1 Aug 2026 02:19:29 +0400 Subject: [PATCH] =?UTF-8?q?=D0=9A=D0=B0=D0=BD=D0=B0=D0=BB=D1=8B:=20=D1=81?= =?UTF-8?q?=D0=BE=D0=B7=D0=B4=D0=B0=D0=B2=D0=B0=D1=82=D1=8C=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D1=84=D0=B8=D0=BB=D1=8C=20=D0=BE=D0=B4=D0=BD=D0=B8=D0=BC?= =?UTF-8?q?=20=D0=B1=D0=BB=D0=BE=D0=BA=D0=BE=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../blockchain/body/CreateChannelBody.java | 13 +-- .../blockchain/Net_AddBlock_Handler.java | 34 +++++- .../ChannelNamesStateBootstrapper.java | 75 +++++++++++- .../channels/ChannelsReadSupport.java | 22 +++- VERSION.properties | 4 +- .../01_Channel_Types_and_CreateChannel.md | 17 +++ docs/Blockchain/16_TEXT_Channel_Meta.md | 6 +- docs/Blockchain/CHANGELOG.md | 7 ++ shine-UI/js/pages/add-channel-view.js | 20 +--- shine-UI/js/services/auth-service.js | 108 ++++++------------ shine-UI/styles/components.css | 13 ++- 11 files changed, 201 insertions(+), 118 deletions(-) diff --git a/SHiNE-server/shine-server-blockchain/src/main/java/blockchain/body/CreateChannelBody.java b/SHiNE-server/shine-server-blockchain/src/main/java/blockchain/body/CreateChannelBody.java index 3bae2410..2ba5af48 100644 --- a/SHiNE-server/shine-server-blockchain/src/main/java/blockchain/body/CreateChannelBody.java +++ b/SHiNE-server/shine-server-blockchain/src/main/java/blockchain/body/CreateChannelBody.java @@ -19,7 +19,7 @@ import java.util.Objects; * [1] channelNameLen * [N] channelName UTF-8 * [2] channelDescriptionLen - * [M] channelDescription UTF-8 (0..200 bytes) + * [M] channelDescription UTF-8 (0..2048 bytes) * [2] channelTypeCode (uint16) * [2] channelTypeVersion (uint16) */ @@ -38,7 +38,7 @@ public final class CreateChannelBody implements BodyRecord, BodyHasLine { private static final byte[] ZERO32 = new byte[32]; private static final int MAX_NAME_LENGTH = 32; - private static final int MAX_DESCRIPTION_UTF8_LEN = 200; + public static final int MAX_DESCRIPTION_UTF8_LEN = 2048; public final short subType; public final short version; @@ -88,7 +88,7 @@ public final class CreateChannelBody implements BodyRecord, BodyHasLine { int descriptionLen = Short.toUnsignedInt(bb.getShort()); if (descriptionLen > MAX_DESCRIPTION_UTF8_LEN) { - throw new IllegalArgumentException("channelDescription utf8 len must be <=200"); + throw new IllegalArgumentException("channelDescription utf8 len must be <=2048"); } if (bb.remaining() != descriptionLen + 4) { throw new IllegalArgumentException("CreateChannelBody tail mismatch: remaining=" + bb.remaining() + " descriptionLen=" + descriptionLen); @@ -145,7 +145,7 @@ public final class CreateChannelBody implements BodyRecord, BodyHasLine { String normalizedDescription = normalizeDescription(channelDescription); byte[] descUtf8 = normalizedDescription.getBytes(StandardCharsets.UTF_8); if (descUtf8.length > MAX_DESCRIPTION_UTF8_LEN) { - throw new IllegalArgumentException("channelDescription utf8 len must be <=200"); + throw new IllegalArgumentException("channelDescription utf8 len must be <=2048"); } int typeCode = Short.toUnsignedInt(channelTypeCode); @@ -166,7 +166,7 @@ public final class CreateChannelBody implements BodyRecord, BodyHasLine { private static String normalizeDescription(String value) { if (value == null) return ""; - return value.trim().replaceAll("\\s+", " "); + return value.trim().replace("\r\n", "\n").replace('\r', '\n'); } @Override @@ -177,7 +177,7 @@ public final class CreateChannelBody implements BodyRecord, BodyHasLine { } byte[] descriptionUtf8 = normalizeDescription(channelDescription).getBytes(StandardCharsets.UTF_8); if (descriptionUtf8.length > MAX_DESCRIPTION_UTF8_LEN) { - throw new IllegalArgumentException("channelDescription utf8 len must be <=200"); + throw new IllegalArgumentException("channelDescription utf8 len must be <=2048"); } int cap = 4 + (4 + 32 + 4) + 1 + nameUtf8.length + 2 + descriptionUtf8.length + 4; @@ -204,4 +204,3 @@ public final class CreateChannelBody implements BodyRecord, BodyHasLine { @Override public int lineSeq() { return thisLineNumber; } } - diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler.java index c84c8572..5b3e6d3f 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler.java @@ -304,14 +304,36 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler { return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "internal_error", serverLastNum, serverLastHashHex); } + String displayName = normalizedName; + String channelDescription = createChannelBody.channelDescription == null + ? "" + : ChannelNameRules.normalizeDisplayName(createChannelBody.channelDescription); + String avaAr = ""; + String avaSha256 = ""; + long avaSize = 0L; + long metaUpdatedAtMs = 0L; + if (channelTypeCode == (CreateChannelBody.CHANNEL_TYPE_PUBLIC & 0xFFFF)) { + try { + ChannelMetaTextParser.Parsed createMeta = ChannelMetaTextParser.parseStrict(channelDescription, normalizedName); + displayName = createMeta.title(); + channelDescription = createMeta.description(); + avaAr = createMeta.avaAr(); + avaSha256 = createMeta.avaSha256(); + avaSize = createMeta.avaSize(); + metaUpdatedAtMs = block.timestamp * 1000L; + } catch (IllegalArgumentException e) { + return new AddBlockResult(WireCodes.Status.BAD_REQUEST, e.getMessage(), serverLastNum, serverLastHashHex); + } + } + channelNameStateEntry = new ChannelNameStateEntry(); channelNameStateEntry.setSlug(slug); - channelNameStateEntry.setDisplayName(normalizedName); - channelNameStateEntry.setChannelDescription( - createChannelBody.channelDescription == null - ? "" - : ChannelNameRules.normalizeDisplayName(createChannelBody.channelDescription) - ); + channelNameStateEntry.setDisplayName(displayName); + channelNameStateEntry.setChannelDescription(channelDescription); + channelNameStateEntry.setAvaAr(avaAr); + channelNameStateEntry.setAvaSha256(avaSha256); + channelNameStateEntry.setAvaSize(avaSize); + channelNameStateEntry.setMetaUpdatedAtMs(metaUpdatedAtMs); channelNameStateEntry.setOwnerLogin(login); channelNameStateEntry.setOwnerBlockchainName(blockchainName); channelNameStateEntry.setChannelTypeCode(channelTypeCode); diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/ChannelNamesStateBootstrapper.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/ChannelNamesStateBootstrapper.java index 4ff7d627..84576500 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/ChannelNamesStateBootstrapper.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/ChannelNamesStateBootstrapper.java @@ -1,7 +1,9 @@ package server.logic.ws_protocol.JSON.handlers.channels; import blockchain.BchBlockEntry; +import blockchain.MsgSubType; import blockchain.body.CreateChannelBody; +import blockchain.body.TextLineBody; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import shine.db.DbController; @@ -20,6 +22,7 @@ import java.util.Map; public final class ChannelNamesStateBootstrapper { private static final Logger log = LoggerFactory.getLogger(ChannelNamesStateBootstrapper.class); private static final int MSG_TYPE_TECH = 0; + private static final int MSG_TYPE_TEXT = 1; private static final int MSG_SUB_TYPE_CREATE_CHANNEL = 1; private static volatile boolean bootstrapped; @@ -74,17 +77,30 @@ public final class ChannelNamesStateBootstrapper { continue; } - final String displayName; + String displayName; final String slug; - final String channelDescription; + String channelDescription; final int channelTypeCode; final int channelTypeVersion; + String avaAr = ""; + String avaSha256 = ""; + long avaSize = 0L; + long metaUpdatedAtMs = 0L; try { displayName = ChannelNameRules.normalizeDisplayName(createChannelBody.channelName); slug = ChannelNameRules.toCanonicalSlug(displayName); channelDescription = ChannelNameRules.normalizeDisplayName(createChannelBody.channelDescription); channelTypeCode = Short.toUnsignedInt(createChannelBody.channelTypeCode); channelTypeVersion = Short.toUnsignedInt(createChannelBody.channelTypeVersion); + if (channelTypeCode == (CreateChannelBody.CHANNEL_TYPE_PUBLIC & 0xFFFF)) { + ChannelMetaTextParser.Parsed createMeta = ChannelMetaTextParser.parseStrict(channelDescription, displayName); + displayName = createMeta.title(); + channelDescription = createMeta.description(); + avaAr = createMeta.avaAr(); + avaSha256 = createMeta.avaSha256(); + avaSize = createMeta.avaSize(); + metaUpdatedAtMs = parsed.timestamp * 1000L; + } } catch (Exception badName) { skipped.add(ownerBch + "#" + blockNumber + " (invalid_name)"); continue; @@ -102,6 +118,10 @@ public final class ChannelNamesStateBootstrapper { entry.setSlug(slug); entry.setDisplayName(displayName); entry.setChannelDescription(channelDescription == null ? "" : channelDescription); + entry.setAvaAr(avaAr); + entry.setAvaSha256(avaSha256); + entry.setAvaSize(avaSize); + entry.setMetaUpdatedAtMs(metaUpdatedAtMs); entry.setOwnerLogin(ownerLogin); entry.setOwnerBlockchainName(ownerBch); entry.setChannelTypeCode(channelTypeCode); @@ -116,6 +136,8 @@ public final class ChannelNamesStateBootstrapper { dao.clearAll(c); dao.insertAll(c, entries); + + applyMetaUpdates(c, dao, entries, skipped); c.commit(); log.info("channel_names_state bootstrapped: {}", entries.size()); if (!conflicts.isEmpty()) { @@ -148,4 +170,53 @@ public final class ChannelNamesStateBootstrapper { throw new RuntimeException("Failed to bootstrap channel_names_state", e); } } + + private static void applyMetaUpdates(Connection c, + ChannelNameStateDAO dao, + List entries, + List skipped) throws Exception { + Map fallbackTitles = new LinkedHashMap<>(); + for (ChannelNameStateEntry entry : entries) { + fallbackTitles.put(entry.getOwnerBlockchainName() + "#" + entry.getChannelRootBlockNumber(), entry.getSlug()); + } + + String sql = """ + SELECT bch_name, block_number, block_bytes + FROM blocks + WHERE msg_type = ? AND msg_sub_type = ? + ORDER BY bch_name, block_number + """; + try (PreparedStatement ps = c.prepareStatement(sql)) { + ps.setInt(1, MSG_TYPE_TEXT); + ps.setInt(2, MsgSubType.TEXT_CHANNEL_META); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String ownerBch = rs.getString("bch_name"); + int blockNumber = rs.getInt("block_number"); + byte[] blockBytes = rs.getBytes("block_bytes"); + try { + BchBlockEntry parsed = new BchBlockEntry(blockBytes); + if (!(parsed.body instanceof TextLineBody metaBody)) continue; + int lineCode = metaBody.lineCode(); + String fallbackTitle = fallbackTitles.get(ownerBch + "#" + lineCode); + if (fallbackTitle == null) continue; + ChannelMetaTextParser.Parsed parsedMeta = ChannelMetaTextParser.parseStrict(metaBody.message, fallbackTitle); + + ChannelNameStateEntry update = new ChannelNameStateEntry(); + update.setOwnerBlockchainName(ownerBch); + update.setChannelRootBlockNumber(lineCode); + update.setDisplayName(parsedMeta.title()); + update.setChannelDescription(parsedMeta.description()); + update.setAvaAr(parsedMeta.avaAr()); + update.setAvaSha256(parsedMeta.avaSha256()); + update.setAvaSize(parsedMeta.avaSize()); + update.setMetaUpdatedAtMs(parsed.timestamp * 1000L); + dao.updateMeta(c, update); + } catch (Exception e) { + skipped.add(ownerBch + "#" + blockNumber + " (invalid_meta)"); + } + } + } + } + } } diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/ChannelsReadSupport.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/ChannelsReadSupport.java index 34136d71..80c34b84 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/ChannelsReadSupport.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/ChannelsReadSupport.java @@ -99,6 +99,15 @@ final class ChannelsReadSupport { meta.channelDescription = ccb.channelDescription == null ? "" : ccb.channelDescription; meta.channelTypeCode = Short.toUnsignedInt(ccb.channelTypeCode); meta.channelTypeVersion = Short.toUnsignedInt(ccb.channelTypeVersion); + if (meta.channelTypeCode == (CreateChannelBody.CHANNEL_TYPE_PUBLIC & 0xFFFF)) { + ChannelMetaTextParser.Parsed parsedMeta = ChannelMetaTextParser.parseStrict(meta.channelDescription, meta.channelName); + meta.displayName = parsedMeta.title(); + meta.channelDescription = parsedMeta.description(); + meta.avaAr = parsedMeta.avaAr(); + meta.avaSha256 = parsedMeta.avaSha256(); + meta.avaSize = parsedMeta.avaSize(); + meta.metaUpdatedAtMs = e.timestamp * 1000L; + } } else { meta.channelName = null; meta.displayName = null; @@ -415,6 +424,14 @@ final class ChannelsReadSupport { if (entry.body instanceof CreateChannelBody ccb) { event.title = ccb.channelName; event.description = ccb.channelDescription == null ? "" : ccb.channelDescription; + if (Short.toUnsignedInt(ccb.channelTypeCode) == (CreateChannelBody.CHANNEL_TYPE_PUBLIC & 0xFFFF)) { + ChannelMetaTextParser.Parsed parsedMeta = ChannelMetaTextParser.parseStrict(event.description, ccb.channelName); + event.title = parsedMeta.title(); + event.description = parsedMeta.description(); + event.avaAr = parsedMeta.avaAr(); + event.avaSha256 = parsedMeta.avaSha256(); + event.avaSize = parsedMeta.avaSize(); + } } } catch (Exception ignored) { event.createdAtMs = 0L; @@ -423,9 +440,8 @@ final class ChannelsReadSupport { event.title = base.displayName == null || base.displayName.isBlank() ? base.channelName : base.displayName; } if (event.description == null) event.description = ""; - event.avaAr = ""; - event.avaSha256 = ""; - event.avaSize = 0L; + if (event.avaAr == null) event.avaAr = ""; + if (event.avaSha256 == null) event.avaSha256 = ""; out.add(event); } } diff --git a/VERSION.properties b/VERSION.properties index ca219ccf..1a48d601 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.4.11 -server.version=1.4.2 +client.version=1.4.12 +server.version=1.4.3 diff --git a/docs/Blockchain/01_Channel_Types_and_CreateChannel.md b/docs/Blockchain/01_Channel_Types_and_CreateChannel.md index 0322cdaf..742a7948 100644 --- a/docs/Blockchain/01_Channel_Types_and_CreateChannel.md +++ b/docs/Blockchain/01_Channel_Types_and_CreateChannel.md @@ -9,6 +9,23 @@ Payload включает: 4. `channelType` (`uint16`, 2 байта); 5. `channelTypeVersion` (`uint16`, 2 байта). +### `channelDescription` + +`channelDescription` хранится как `uint16 length + UTF-8`, максимум `2048` байт. + +Для публичных каналов (`channelType=1`) поле является начальным снимком профиля канала и использует тот же текстовый meta-формат, что `TEXT_CHANNEL_META`: + +```text + + +Описание канала. +``` + +Если `title` отсутствует, человекочитаемым именем считается техническое `channelName`. +Если `avatar` отсутствует, канал создаётся без аватара. + +Новые UI-клиенты создают публичный канал одним блоком `TECH_CREATE_CHANNEL`; отдельный `TEXT_CHANNEL_META` при создании не пишется. `TEXT_CHANNEL_META` используется для последующих изменений профиля канала. + ## 2. Типы каналов - `0` — `stories` (root-канал пользователя). - `1` — публичный канал. diff --git a/docs/Blockchain/16_TEXT_Channel_Meta.md b/docs/Blockchain/16_TEXT_Channel_Meta.md index c5566831..51220e23 100644 --- a/docs/Blockchain/16_TEXT_Channel_Meta.md +++ b/docs/Blockchain/16_TEXT_Channel_Meta.md @@ -1,6 +1,6 @@ # TEXT_CHANNEL_META: профиль канала -`TEXT_CHANNEL_META` — скрытый технический `TEXT`-блок для полного снимка профиля канала. +`TEXT_CHANNEL_META` — скрытый технический `TEXT`-блок для полного снимка профиля канала после создания. ## Блок @@ -11,6 +11,8 @@ Блок пишется только в линию канала владельца. В обычной ленте сообщений он не отображается как пользовательский пост. +Первый снимок профиля публичного канала хранится прямо в `TECH_CREATE_CHANNEL.channelDescription` тем же текстовым форматом. Отдельный `TEXT_CHANNEL_META` при создании нового публичного канала UI не пишет. + ## Текстовый формат В начале текста могут идти технические теги, после них обычный текст описания: @@ -48,7 +50,7 @@ ## UI -- При создании канала UI может сразу после `TECH_CREATE_CHANNEL` записать первый `TEXT_CHANNEL_META` с человекочитаемым именем, аватаром и описанием. +- При создании публичного канала UI записывает человекочитаемое имя, аватар и описание в `TECH_CREATE_CHANNEL.channelDescription`. - При создании канала UI показывает системную карточку `Создан канал ...`. - При записи `TEXT_CHANNEL_META` UI показывает системную карточку `Изменён профиль канала`. - У системных карточек нет лайка, ответа, репоста, редактирования и других действий обычного сообщения. diff --git a/docs/Blockchain/CHANGELOG.md b/docs/Blockchain/CHANGELOG.md index 2b3ab6d7..f76eb71a 100644 --- a/docs/Blockchain/CHANGELOG.md +++ b/docs/Blockchain/CHANGELOG.md @@ -121,6 +121,13 @@ - Команда `/.desc` удалена из актуальной модели изменения описания канала; профиль канала меняется только через `TEXT_CHANNEL_META`. - API чтения каналов расширен полями `displayName`, `avaAr`, `avaSha256`, `avaSize`, `metaUpdatedAtMs` и списком `metaEvents`. +## 2026-08-01 02:18:00 +0400 +- Базовый коммит-ориентир: `8125d1a`. +- Увеличен лимит `CreateChannelBody.channelDescription` до `2048` UTF-8 байт. +- Для публичных каналов начальный профиль канала теперь хранится прямо в `TECH_CREATE_CHANNEL.channelDescription` тем же meta-форматом, что `TEXT_CHANNEL_META`. +- UI создания публичного канала больше не пишет отдельный `TEXT_CHANNEL_META` сразу после создания; отдельный meta-блок используется только для последующих изменений профиля. +- Bootstrap `channel_names_state` при старте сервера теперь применяет последующие `TEXT_CHANNEL_META` поверх начального профиля из `TECH_CREATE_CHANNEL`. + ## 2026-07-31 17:40:31 +0400 - Базовый коммит-ориентир: `d640dd6`. - Формат блокчейна не менялся. diff --git a/shine-UI/js/pages/add-channel-view.js b/shine-UI/js/pages/add-channel-view.js index 55795ee7..b6f3b4d7 100644 --- a/shine-UI/js/pages/add-channel-view.js +++ b/shine-UI/js/pages/add-channel-view.js @@ -36,7 +36,7 @@ function renderAvatarPreview(slot, avatar, title) { slot.innerHTML = ''; const wrap = document.createElement('div'); wrap.className = 'channel-profile-avatar'; - wrap.style.setProperty('--channel-avatar-size', '82px'); + wrap.style.setProperty('--channel-avatar-size', '104px'); if (avatar?.ar) { const img = document.createElement('img'); img.alt = 'Аватар канала'; @@ -180,25 +180,17 @@ export function render({ navigate }) { errorEl.textContent = ''; try { - const createResult = await authService.addBlockCreateChannel({ + await authService.addBlockCreateChannel({ login, storagePwd, channelName: normalizeChannelDisplayName(check.name), - channelDescription: '', + channelDescription: check.description, + channelProfileTitle: check.title, + channelAvatar: selectedAvatar, channelType: CHANNEL_TYPE_PUBLIC, channelTypeVersion: 1, }); - if (check.title || check.description || selectedAvatar?.ar) { - await authService.addBlockChannelMeta({ - login, - storagePwd, - channel: createResult.channel, - title: check.title, - description: check.description, - avatar: selectedAvatar, - }); - if (selectedAvatar?.ar) markArweaveAttachmentPlaced(login, selectedAvatar); - } + if (selectedAvatar?.ar) markArweaveAttachmentPlaced(login, selectedAvatar); persistCreateSuccessFlash(`Канал "${normalizeChannelDisplayName(check.name)}" создан.`); navigate('channels-list'); diff --git a/shine-UI/js/services/auth-service.js b/shine-UI/js/services/auth-service.js index b917d5db..eabc5c29 100644 --- a/shine-UI/js/services/auth-service.js +++ b/shine-UI/js/services/auth-service.js @@ -65,6 +65,7 @@ const CHANNEL_TYPE_PUBLIC = 1; const CHANNEL_TYPE_PERSONAL = 100; const CHANNEL_TYPE_GROUP = 200; const CHANNEL_TYPE_VERSION_DEFAULT = 1; +const CREATE_CHANNEL_DESCRIPTION_MAX_BYTES = 2048; const SESSION_TYPE_CLIENT = 1; const SESSION_TYPE_WALLET = 50; const SESSION_TYPE_HOMESERVER = 100; @@ -691,10 +692,10 @@ function makeConnectionBodyBytes({ } function normalizeChannelDescription(value) { - const text = String(value == null ? '' : value).trim().replace(/\s+/g, ' '); + const text = String(value == null ? '' : value).trim().replace(/\r\n/g, '\n').replace(/\r/g, '\n'); const bytes = utf8Bytes(text); - if (bytes.length > 200) { - throw new Error('Описание канала слишком длинное: максимум 200 символов.'); + if (bytes.length > CREATE_CHANNEL_DESCRIPTION_MAX_BYTES) { + throw new Error('Описание канала слишком длинное: максимум 2048 байт.'); } return text; } @@ -771,8 +772,8 @@ function makeCreateChannelBodyBytes({ } const descriptionBytes = utf8Bytes(cleanDescription); - if (descriptionBytes.length > 200) { - throw new Error('Описание канала слишком длинное: максимум 200 символов.'); + if (descriptionBytes.length > CREATE_CHANNEL_DESCRIPTION_MAX_BYTES) { + throw new Error('Описание канала слишком длинное: максимум 2048 байт.'); } const typeVer = Number(channelTypeVersion); if (!Number.isFinite(typeCode) || typeCode < 0 || typeCode > 65535) { @@ -796,31 +797,6 @@ function makeCreateChannelBodyBytes({ ); } -function makeCreateChannelBodyBytesLegacy({ - lineCode, - prevLineNumber, - prevLineHashHex, - thisLineNumber, - channelName, -}) { - const check = validatePersonalChannelName(channelName); - if (!check.ok) throw new Error(check.error); - 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 makeTextPostBodyBytes({ lineCode, prevLineNumber, prevLineHashHex, thisLineNumber, text }) { const message = String(text || '').trim(); if (!message) throw new Error('Message text is required'); @@ -2084,6 +2060,8 @@ export class AuthService { login, channelName, channelDescription = '', + channelProfileTitle = '', + channelAvatar = null, channelType = CHANNEL_TYPE_PUBLIC, channelTypeVersion = CHANNEL_TYPE_VERSION_DEFAULT, storagePwd, @@ -2099,7 +2077,13 @@ export class AuthService { throw new Error(typeCode === CHANNEL_TYPE_PERSONAL ? nameCheck.error : channelNameErrorText(nameCheck.code)); } const cleanChannelName = normalizeChannelDisplayName(nameCheck.normalized); - const cleanChannelDescription = normalizeChannelDescription(channelDescription); + const cleanChannelDescription = typeCode === CHANNEL_TYPE_PUBLIC + ? composeChannelMetaText({ + title: channelProfileTitle, + description: channelDescription, + avatar: channelAvatar, + }) + : normalizeChannelDescription(channelDescription); const channelSlug = toCanonicalChannelSlug(cleanChannelName); const typeVersion = Number(channelTypeVersion); const key = `create-channel:${cleanLogin}:${typeCode}:${channelSlug || cleanChannelName.toLowerCase()}`; @@ -2131,51 +2115,23 @@ export class AuthService { thisLineNumber = createdChannels.length + 1; } - let payload; - try { - payload = await this.addBlockSigned({ - login: cleanLogin, - storagePwd, - msgType: MSG_TYPE_TECH, - msgSubType: MSG_SUBTYPE_TECH_CREATE_CHANNEL, - msgVersion: CREATE_CHANNEL_BODY_VERSION, - bodyBytes: makeCreateChannelBodyBytes({ - lineCode: 0, - prevLineNumber, - prevLineHashHex, - thisLineNumber, - channelName: cleanChannelName, - channelDescription: cleanChannelDescription, - channelType: typeCode, - channelTypeVersion: typeVersion, - }), - }); - } catch (error) { - const rawCode = String(error?.code || '').toUpperCase(); - const rawText = String(error?.message || '').toLowerCase(); - const isLegacyFormatMismatch = ( - rawCode === 'BAD_BLOCK_FORMAT' || - rawText.includes('bad_block_format') || - rawText.includes('некорректный формат блока') - ); - if (!isLegacyFormatMismatch) throw error; - - // Совместимость со старыми серверами, где CreateChannel body без description/type. - payload = await this.addBlockSigned({ - login: cleanLogin, - storagePwd, - msgType: MSG_TYPE_TECH, - msgSubType: MSG_SUBTYPE_TECH_CREATE_CHANNEL, - msgVersion: 1, - bodyBytes: makeCreateChannelBodyBytesLegacy({ - lineCode: 0, - prevLineNumber, - prevLineHashHex, - thisLineNumber, - channelName: cleanChannelName, - }), - }); - } + const payload = await this.addBlockSigned({ + login: cleanLogin, + storagePwd, + msgType: MSG_TYPE_TECH, + msgSubType: MSG_SUBTYPE_TECH_CREATE_CHANNEL, + msgVersion: CREATE_CHANNEL_BODY_VERSION, + bodyBytes: makeCreateChannelBodyBytes({ + lineCode: 0, + prevLineNumber, + prevLineHashHex, + thisLineNumber, + channelName: cleanChannelName, + channelDescription: cleanChannelDescription, + channelType: typeCode, + channelTypeVersion: typeVersion, + }), + }); const selector = { ownerBlockchainName: blockchainName, diff --git a/shine-UI/styles/components.css b/shine-UI/styles/components.css index ebf6dc0a..3254a030 100644 --- a/shine-UI/styles/components.css +++ b/shine-UI/styles/components.css @@ -3649,10 +3649,11 @@ textarea.input { } .channel-row .avatar { - width: 42px; - height: 42px; - min-width: 42px; - min-height: 42px; + width: 58px; + height: 58px; + min-width: 58px; + min-height: 58px; + border-radius: 50%; background: linear-gradient(138deg, #efd193, #c18c42); color: #17233f; box-shadow: inset 0 1px 0 rgba(255, 244, 208, 0.58); @@ -3920,14 +3921,14 @@ textarea.input { } .channel-profile-avatar { - --channel-avatar-size: 72px; + --channel-avatar-size: 96px; width: var(--channel-avatar-size); height: var(--channel-avatar-size); display: inline-flex; align-items: center; justify-content: center; align-self: center; - border-radius: 22px; + border-radius: 50%; background: linear-gradient(145deg, rgba(255, 214, 122, 0.22), rgba(56, 41, 22, 0.65)); border: 1px solid rgba(255, 226, 155, 0.24); color: rgba(255, 236, 194, 0.95);