${turboMode ? '' : ''}
@@ -609,7 +628,8 @@ export function openArweaveAttachmentManager({
const walletEl = root.querySelector('#ar-attach-wallet');
const turboKeyEl = root.querySelector('#turbo-key-source');
const fileEl = root.querySelector('#ar-attach-file');
- const metaEl = root.querySelector('[data-meta="true"]');
+ const mainMetaEl = root.querySelector('[data-meta-main="true"]');
+ const previewMetaEl = root.querySelector('[data-meta-preview="true"]');
const errorEl = root.querySelector('[data-error="true"]');
const uploadBtn = root.querySelector('[data-action="upload"]');
const previewOptionEl = root.querySelector('[data-preview-option="true"]');
@@ -648,10 +668,10 @@ export function openArweaveAttachmentManager({
turboKeyEl.addEventListener('change', async () => {
selectedTurboKeySource = String(turboKeyEl.value || 'client');
writeLastTurboKeySource(cleanLogin, selectedTurboKeySource);
- await refreshTurboStateForCurrentFile(metaEl, errorEl, uploadBtn);
+ await refreshTurboStateForCurrentFile(mainMetaEl, previewMetaEl, errorEl, uploadBtn);
});
}
- await refreshTurboStateForCurrentFile(metaEl, errorEl, uploadBtn);
+ await refreshTurboStateForCurrentFile(mainMetaEl, previewMetaEl, errorEl, uploadBtn);
} else {
await loadWallets(errorEl);
if (walletEl) {
@@ -667,16 +687,17 @@ export function openArweaveAttachmentManager({
priceInfo = null;
balanceInfo = null;
if (fileEl) fileEl.value = '';
- setText(metaEl, '');
+ setText(mainMetaEl, '');
+ setText(previewMetaEl, '');
});
}
}
root.querySelector('[data-action="cancel"]')?.addEventListener('click', () => close(resolve, null));
root.querySelector('[data-action="topup"]')?.addEventListener('click', async () => {
- await promptTurboTopUp(metaEl, errorEl);
+ await promptTurboTopUp(mainMetaEl, previewMetaEl, errorEl);
if (turboMode && selectedFile && selectedSha256) {
- await refreshTurboStateForCurrentFile(metaEl, errorEl, uploadBtn);
+ await refreshTurboStateForCurrentFile(mainMetaEl, previewMetaEl, errorEl, uploadBtn);
}
});
if (!turboMode) {
@@ -691,7 +712,8 @@ export function openArweaveAttachmentManager({
balanceInfo = null;
uploadBtn.disabled = true;
setText(errorEl, '');
- setText(metaEl, '');
+ setText(mainMetaEl, '');
+ setText(previewMetaEl, '');
if (previewToggleEl instanceof HTMLInputElement) previewToggleEl.checked = false;
if (previewOptionEl) previewOptionEl.hidden = true;
if (!selectedFile) return;
@@ -709,9 +731,9 @@ export function openArweaveAttachmentManager({
previewOptionEl.hidden = false;
}
if (turboMode) {
- await refreshTurboStateForCurrentFile(metaEl, errorEl, uploadBtn);
+ await refreshTurboStateForCurrentFile(mainMetaEl, previewMetaEl, errorEl, uploadBtn);
} else {
- await recalculateArweaveState(metaEl, errorEl, uploadBtn);
+ await recalculateArweaveState(mainMetaEl, previewMetaEl, errorEl, uploadBtn);
}
} catch (error) {
setText(errorEl, error?.message || 'Не удалось подготовить файл.');
@@ -735,9 +757,9 @@ export function openArweaveAttachmentManager({
selectedPreviewSha256 = await sha256HexFromArrayBuffer(await selectedPreviewFile.arrayBuffer());
}
if (turboMode) {
- await refreshTurboStateForCurrentFile(metaEl, errorEl, uploadBtn);
+ await refreshTurboStateForCurrentFile(mainMetaEl, previewMetaEl, errorEl, uploadBtn);
} else {
- await recalculateArweaveState(metaEl, errorEl, uploadBtn);
+ await recalculateArweaveState(mainMetaEl, previewMetaEl, errorEl, uploadBtn);
}
} catch (error) {
resetPreviewState();
diff --git a/shine-UI/js/services/attachment-format.js b/shine-UI/js/services/attachment-format.js
index 57172cbd..4167e332 100644
--- a/shine-UI/js/services/attachment-format.js
+++ b/shine-UI/js/services/attachment-format.js
@@ -286,11 +286,21 @@ function createMediaSlide({ item, url, kind, messageTimestampMs, slide, previewU
img.loading = 'lazy';
img.addEventListener('error', () => replaceWithUnavailable(slide, messageTimestampMs), { once: true });
frame.append(img);
+ } else if (previewUrl) {
+ const img = document.createElement('img');
+ img.className = 'message-attachment-media';
+ img.src = previewUrl;
+ img.alt = `${item.name} preview`;
+ img.loading = 'lazy';
+ img.addEventListener('error', () => replaceWithUnavailable(slide, messageTimestampMs), { once: true });
+ const play = document.createElement('span');
+ play.className = 'message-attachment-play';
+ play.textContent = '▶';
+ frame.append(img, play);
} else {
const video = document.createElement('video');
video.className = 'message-attachment-media';
video.src = url;
- if (previewUrl) video.poster = previewUrl;
video.preload = 'metadata';
video.muted = true;
video.playsInline = true;
diff --git a/Черновик_TEXT_CHANNEL_META_для_каналов.md b/Черновик_TEXT_CHANNEL_META_для_каналов.md
new file mode 100644
index 00000000..081d0ef9
--- /dev/null
+++ b/Черновик_TEXT_CHANNEL_META_для_каналов.md
@@ -0,0 +1,297 @@
+# Черновик `TEXT_CHANNEL_META` для каналов SHiNE
+
+## Статус документа
+
+Этот файл является рабочим черновиком и не считается финальной спецификацией.
+
+Важно:
+
+- это не окончательный формат;
+- точная версия должна быть зафиксирована отдельно при начале реализации;
+- до реализации допустимы изменения синтаксиса, ограничений и полей БД;
+- документ временно лежит в корне проекта как согласованный draft.
+
+## Назначение
+
+Нужен специальный скрытый тип текстового сообщения канала, который:
+
+- технически хранится в блокчейне как `TEXT`-блок;
+- не показывается пользователям как обычное сообщение ленты;
+- может писать только владелец канала;
+- не редактируется;
+- задаёт полное текущее состояние метаданных канала.
+
+Через этот блок должны обновляться:
+
+- человекочитаемое заглавие канала;
+- аватар канала;
+- описание канала.
+
+## Место в формате блокчейна
+
+- `msg_type = 1`
+- `subType = 70`
+- имя подтипа: `TEXT_CHANNEL_META`
+
+Этот подтип относится к текстовым блокам, но трактуется как скрытое техническое сообщение канала.
+
+## Общая модель применения
+
+`TEXT_CHANNEL_META`:
+
+- пишется в линию конкретного канала;
+- задаёт полный снимок метаданных;
+- при чтении канала сервер берёт последний валидный `TEXT_CHANNEL_META` в линии;
+- более старые `TEXT_CHANNEL_META` остаются в истории блокчейна, но не участвуют в вычислении текущего состояния;
+- если meta-блок не найден, используются базовые данные канала без override-полей.
+
+## Общий формат текста
+
+Внутри `TEXT_CHANNEL_META` текст строится так:
+
+1. в начале идут технические теги ``;
+2. после тегов идёт обычный UTF-8 текст;
+3. этот хвостовой текст считается описанием канала.
+
+Пример:
+
+```text
+
+
+Тут описание канала.
+```
+
+## Поддерживаемые теги
+
+### 1. Тег заглавия
+
+Формат:
+
+```text
+
+```
+
+Смысл:
+
+- задаёт красивое отображаемое имя канала;
+- хранится как обычный UTF-8 текст;
+- это не технический slug и не системное имя.
+
+### 2. Тег аватара
+
+Формат:
+
+```text
+
+```
+
+Смысл:
+
+- задаёт аватар канала;
+- `ar` хранит только `txId`, без полного URL;
+- конкретный gateway/URL уже выбирает клиент или сервер.
+
+## Правила порядка
+
+- теги должны идти только в начале текста;
+- после начала обычного описания новые теги больше не допускаются;
+- рекомендуемый порядок:
+ 1. `title`
+ 2. `avatar`
+ 3. текст описания
+- каждый тег допустим не более одного раза.
+
+## Правила отсутствующих полей
+
+- если `title` отсутствует, используется обычное техническое имя канала;
+- если `avatar` отсутствует, аватар у канала отсутствует;
+- если текста после тегов нет, описание считается пустым;
+- если тегов нет вообще, но текст есть, это означает пустые `title` и `avatar`, а текст используется как описание.
+
+## Полный снимок состояния
+
+Каждый новый `TEXT_CHANNEL_META` задаёт полное состояние канала целиком.
+
+Это значит:
+
+- новый блок не дозаполняет старые поля;
+- новый блок не наследует значения из прошлого meta-блока;
+- если в новом блоке нет `title`, то красивое имя сбрасывается к обычному имени канала;
+- если в новом блоке нет `avatar`, аватар сбрасывается;
+- если в новом блоке нет текста описания, описание становится пустым.
+
+## Ограничения для тега `title`
+
+`title` хранится как UTF-8 строка, но для упрощения формата запрещаются следующие символы:
+
+- `<`
+- `>`
+- `;`
+- табуляция
+- перевод строки
+- `\0`
+
+Дополнительно:
+
+- максимальная длина `title` — `50` символов;
+- допускаются обычные Unicode-символы, включая кириллицу, латиницу, эмодзи и декоративные символы, если они не нарушают запреты выше.
+
+## Ограничения для описания
+
+- описание хранится как обычный UTF-8 текст;
+- максимальная длина — `250` символов.
+
+Если описание длиннее лимита, meta-блок должен считаться невалидным.
+
+## Ограничения для тега `avatar`
+
+Обязательные поля тега:
+
+- `v`
+- `size`
+- `sha256`
+- `ar`
+
+Правила:
+
+- `size` — размер файла в байтах;
+- `sha256` — SHA-256 файла в hex;
+- `sha256` должен иметь длину ровно `64` hex-символа;
+- `ar` — только `txId` Arweave;
+- длина `ar` должна соответствовать длине идентификатора Arweave-транзакции;
+- имя файла в meta-теге аватара не хранится.
+
+## Валидация блока
+
+Блок `TEXT_CHANNEL_META` считается валидным только если одновременно соблюдены все условия:
+
+- теги находятся строго в начале текста;
+- каждый тип тега встречается не более одного раза;
+- `title` удовлетворяет ограничениям;
+- `avatar`-тег имеет все обязательные поля и корректные значения;
+- длина описания не превышает лимит.
+
+Если блок невалиден:
+
+- весь meta-блок не применяется целиком;
+- сервер не должен частично брать из него только отдельные поля.
+
+## Что именно хранится в текущем состоянии канала
+
+Текущее вычисленное состояние канала должно включать:
+
+- техническое имя канала;
+- красивое отображаемое имя;
+- описание;
+- `ava_ar`;
+- `ava_sha256`;
+- `ava_size`;
+- время последнего meta-обновления.
+
+## База данных
+
+Базовую таблицу каналов предлагается не менять концептуально, а расширить существующую модель хранения каналов.
+
+Текущая основа уже есть в таблице:
+
+- `channel_names_state`
+
+В неё предлагается добавить поля:
+
+- `ava_ar`
+- `ava_sha256`
+- `ava_size`
+- `meta_updated_at_ms`
+
+При этом:
+
+- техническое имя канала продолжает храниться отдельно;
+- красивое имя хранится в `display_name`;
+- описание хранится в `channel_description`.
+
+## Поведение API и чтения каналов
+
+Обычным пользователям `TEXT_CHANNEL_META` не должен приходить как обычное сообщение ленты.
+
+Это означает:
+
+- в `GetChannelMessages` такие блоки не показываются как сообщения;
+- в обычных списках каналов клиент получает уже вычисленное текущее состояние канала;
+- в debug/raw-чтении сам блок может оставаться доступным как часть блокчейна.
+
+## UI-заметки
+
+### 1. В обычной ленте
+
+`TEXT_CHANNEL_META` не показывается как обычное текстовое сообщение.
+
+### 2. Системная карточка
+
+UI может показывать специальную системную карточку:
+
+- для текущего состояния канала;
+- для стартового состояния при создании канала.
+
+### 3. Что показывать по нажатию
+
+По нажатию на системную карточку нужно показывать полную информацию:
+
+- дата и время;
+- заглавие;
+- аватар;
+- описание.
+
+### 4. История UI
+
+На первом этапе UI показывает только текущую системную карточку состояния.
+
+Полная история всех прошлых meta-обновлений в UI на этом этапе не требуется.
+
+## Поведение при создании канала
+
+Для UI полезно также иметь отдельный визуальный системный блок в начале канала:
+
+- «канал создан»;
+- по нажатию можно показать стартовые данные канала;
+- если на момент создания уже есть заглавие/ава/описание, UI показывает именно их.
+
+Это UI-заметка и не требует отдельного нового блокчейн-формата поверх `TEXT_CHANNEL_META`.
+
+## Краткое резюме согласованной схемы
+
+- `msg_type=1`
+- `subType=70`
+- имя: `TEXT_CHANNEL_META`
+- это скрытый технический текстовый блок канала
+- порядок: теги в начале, потом описание
+- теги:
+ - ``
+ - ``
+- `title`:
+ - UTF-8
+ - максимум `50` символов
+ - нельзя `<`, `>`, `;`, таб, перевод строки, `\0`
+- описание:
+ - UTF-8
+ - максимум `250` символов
+- `avatar`:
+ - `ar` только `txId`
+ - хранится без имени файла
+- новый meta-блок задаёт полный снимок состояния
+- сервер применяет только последний валидный meta-блок
+- невалидный meta-блок не применяется целиком
+- БД расширяется в той же модели каналов
+- в UI обычным сообщением блок не показывается
+
+## Что ещё потребуется зафиксировать позже
+
+При переводе черновика в официальную спецификацию нужно будет дополнительно утвердить:
+
+- точный номер версии формата;
+- точный способ подсчёта длины `title` и описания:
+ - по Unicode code points;
+ - по Java `String.length()`;
+ - или по UTF-8 байтам;
+- точную проверку длины `ar`;
+- список API-ответов, куда должны быть добавлены новые поля;
+- правила миграции старых каналов без meta-блоков.
diff --git a/Черновик_формата_вложений_в_сообщениях_блокчейна.md b/Черновик_формата_вложений_в_сообщениях_блокчейна.md
new file mode 100644
index 00000000..5c5c86db
--- /dev/null
+++ b/Черновик_формата_вложений_в_сообщениях_блокчейна.md
@@ -0,0 +1,173 @@
+# Черновик формата вложений в сообщениях блокчейна SHiNE
+
+## Статус документа
+
+Этот файл пока является черновиком и рабочим описанием идеи.
+
+Важно:
+
+- это не финальная спецификация;
+- точная версия формата будет зафиксирована после начала реальной реализации;
+- до момента реализации допустимы изменения полей, синтаксиса и правил отображения;
+- документ специально лежит в корне проекта как временная рабочая заметка.
+
+## Цель
+
+Нужен минимальный формат, позволяющий прикладывать файлы к текстовым сообщениям в каналах и тредах, не превращая само сообщение в отдельный файловый контейнер.
+
+Основная идея:
+
+- само сообщение остаётся обычным текстом;
+- вложения описываются специальными техническими вставками в начале текста;
+- сами файлы хранятся отдельно в архиве/Arweave;
+- в сообщении хранятся только метаданные, достаточные для отображения и скачивания.
+
+## Общий принцип
+
+Если в начале текста сообщения стоит один или несколько блоков:
+
+```text
+
+```
+
+клиент трактует их как описания вложений.
+
+После этих блоков идёт обычный пользовательский текст сообщения.
+
+Если технических блоков нет, сообщение считается обычным текстовым сообщением без вложений.
+
+## Формат одного блока вложения
+
+Общий вид:
+
+```text
+
+```
+
+Где:
+
+- `attach` — тип технической вставки;
+- `v=1` — текущая рабочая версия чернового формата;
+- `name` — имя файла;
+- `size` — размер файла в байтах;
+- `sha256` — SHA-256 файла в hex;
+- `ar` — идентификатор файла в архиве или адрес файла в Arweave.
+
+## Несколько вложений
+
+Если к одному сообщению приложено несколько файлов, блоки просто идут подряд в начале текста:
+
+```text
+
+
+Текст сообщения
+```
+
+## Обязательные поля
+
+Для чернового варианта обязательными считаются:
+
+- `v`
+- `name`
+- `size`
+- `sha256`
+- `ar`
+
+Если какого-то из этих полей нет, клиент может:
+
+- игнорировать конкретный битый блок;
+- не считать его валидным вложением;
+- при этом продолжать обрабатывать остальные корректные блоки.
+
+## Почему без `mime` и без `kind`
+
+В минимальном варианте тип файла отдельно не хранится.
+
+Причины:
+
+- это экономит место;
+- тип всё равно обычно определяется клиентом;
+- для первого варианта достаточно имени файла, адреса, хэша и размера;
+- превью и способ отображения клиент может решать сам по расширению имени файла или по попытке открыть файл.
+
+То есть в сообщении не хранится:
+
+- `mime`;
+- `kind`;
+- `width`;
+- `height`;
+- `duration`;
+- `thumbnail`.
+
+Все эти поля можно добавить позже отдельной версией, если они реально понадобятся.
+
+## Логика клиента
+
+Клиент получает:
+
+- имя файла;
+- размер;
+- хэш;
+- адрес файла.
+
+После этого клиент сам решает, как рендерить вложение:
+
+- если расширение похоже на изображение, можно пробовать показать preview;
+- если расширение похоже на видео, можно пробовать встроенный видеоплеер;
+- если расширение похоже на аудио, можно пробовать аудиоплеер;
+- иначе показывать обычную кнопку скачивания.
+
+Если браузер или клиент не смог показать preview, должен быть fallback на скачивание файла.
+
+## Правила совместимости
+
+- Сообщение без `` остаётся обычным текстом.
+- Старые клиенты, которые не умеют распознавать этот формат, могут показывать сообщение как обычный текст целиком.
+- Новые клиенты могут скрывать технические блоки и показывать вместо них UI вложений.
+
+## Ограничения чернового варианта
+
+В текущем виде значения полей не предполагают сложного экранирования.
+
+Значит нужно отдельно договориться, как безопасно хранить `name`, если там встретятся символы:
+
+- `;`
+- `=`
+- `>`
+
+На этапе реальной реализации нужно выбрать один из вариантов:
+
+- жёстко ограничить допустимые символы в `name`;
+- хранить имя файла в безопасно закодированном виде;
+- добавить отдельное правило escape/encoding.
+
+Пока этот вопрос считается открытым.
+
+## Примеры
+
+### Одно вложение
+
+```text
+
+Привет, вот фото
+```
+
+### Два вложения
+
+```text
+
+
+Смотри, прикрепил фото и документ
+```
+
+## Что надо зафиксировать позже
+
+Перед переводом этого черновика в официальную спецификацию нужно отдельно утвердить:
+
+- точное место этого формата в общей документации блокчейна;
+- где именно используются такие вложения: каналы, треды, комментарии или иные текстовые блоки;
+- финальные правила кодирования `name`;
+- допустим ли полный URL в `ar` или только короткий `txId`;
+- нужно ли ограничение на количество вложений в одном сообщении;
+- нужна ли отдельная серверная или клиентская проверка расширения файла;
+- как именно это будет отображаться в web UI и других клиентах.