SHA256
UI: добавить вложения Arweave в каналы
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
import { buildArweaveDataUrl, validateArweaveTxId, validateSha256Hex } from './arweave-file-service.js';
|
||||
|
||||
const ATTACH_PREFIX = '<SHiNE:attach;';
|
||||
const ATTACH_BLOCK_RE = /^<SHiNE:attach;([^>]*)>\n?/u;
|
||||
|
||||
export function escapeHtml(text) {
|
||||
return String(text || '')
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''');
|
||||
}
|
||||
|
||||
export function formatBytes(bytes) {
|
||||
const value = Number(bytes || 0);
|
||||
if (!Number.isFinite(value) || value <= 0) return '0 B';
|
||||
if (value < 1024) return `${value} B`;
|
||||
if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`;
|
||||
return `${(value / (1024 * 1024)).toFixed(2)} MB`;
|
||||
}
|
||||
|
||||
function normalizeName(name) {
|
||||
const clean = String(name || 'file')
|
||||
.replace(/[\u0000-\u001f\u007f]/g, '')
|
||||
.replace(/[\\/]/g, '_')
|
||||
.trim();
|
||||
return clean || 'file';
|
||||
}
|
||||
|
||||
export function normalizeAttachment(input = {}) {
|
||||
const txId = String(input.ar || input.txId || '').trim();
|
||||
const sha256Hex = String(input.sha256 || input.sha256Hex || '').trim().toLowerCase();
|
||||
const size = Number(input.size || input.sizeBytes || 0);
|
||||
const name = normalizeName(input.name || input.fileName || 'file');
|
||||
if (!validateArweaveTxId(txId)) throw new Error('Некорректный Transaction ID Arweave.');
|
||||
if (!validateSha256Hex(sha256Hex)) throw new Error('Некорректный SHA-256 файла.');
|
||||
if (!Number.isInteger(size) || size <= 0) throw new Error('Некорректный размер файла.');
|
||||
return {
|
||||
v: 1,
|
||||
name,
|
||||
size,
|
||||
sha256: sha256Hex,
|
||||
ar: txId,
|
||||
uploadedAtMs: Number(input.uploadedAtMs || 0) || Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildAttachmentBlock(attachment) {
|
||||
const item = normalizeAttachment(attachment);
|
||||
const encodedName = encodeURIComponent(item.name);
|
||||
return `<SHiNE:attach;v=1;name=${encodedName};size=${item.size};sha256=${item.sha256};ar=${item.ar}>`;
|
||||
}
|
||||
|
||||
export function composeMessageWithAttachments(text, attachments = []) {
|
||||
const cleanText = String(text || '').trim();
|
||||
const rows = (Array.isArray(attachments) ? attachments : []).map(buildAttachmentBlock);
|
||||
if (!rows.length) return cleanText;
|
||||
return `${rows.join('\n')}${cleanText ? `\n${cleanText}` : ''}`;
|
||||
}
|
||||
|
||||
function parseFields(rawFields) {
|
||||
const out = {};
|
||||
String(rawFields || '').split(';').forEach((part) => {
|
||||
const eq = part.indexOf('=');
|
||||
if (eq <= 0) return;
|
||||
const key = part.slice(0, eq).trim();
|
||||
const value = part.slice(eq + 1).trim();
|
||||
if (key) out[key] = value;
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
export function parseMessageAttachments(rawText) {
|
||||
let rest = String(rawText || '');
|
||||
const attachments = [];
|
||||
|
||||
while (rest.startsWith(ATTACH_PREFIX)) {
|
||||
const match = rest.match(ATTACH_BLOCK_RE);
|
||||
if (!match) break;
|
||||
const fields = parseFields(match[1]);
|
||||
try {
|
||||
attachments.push(normalizeAttachment({
|
||||
name: decodeURIComponent(String(fields.name || 'file')),
|
||||
size: fields.size,
|
||||
sha256: fields.sha256,
|
||||
ar: fields.ar,
|
||||
}));
|
||||
} catch {
|
||||
// Битый attach-блок скрываем из UI, но не ломаем отображение текста.
|
||||
}
|
||||
rest = rest.slice(match[0].length);
|
||||
}
|
||||
|
||||
return {
|
||||
attachments,
|
||||
text: rest.replace(/^\n+/u, ''),
|
||||
};
|
||||
}
|
||||
|
||||
export function createAttachmentListElement(attachments = [], { gateway = '', compact = false } = {}) {
|
||||
const items = Array.isArray(attachments) ? attachments : [];
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = `message-attachments${compact ? ' message-attachments--compact' : ''}`;
|
||||
items.forEach((raw) => {
|
||||
let item;
|
||||
try {
|
||||
item = normalizeAttachment(raw);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const url = buildArweaveDataUrl({ gateway, txId: item.ar });
|
||||
const link = document.createElement('a');
|
||||
link.className = 'message-attachment-card';
|
||||
link.href = url;
|
||||
link.target = '_blank';
|
||||
link.rel = 'noopener';
|
||||
link.title = 'Открыть файл в Arweave';
|
||||
link.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
});
|
||||
link.innerHTML = `
|
||||
<span class="message-attachment-icon" aria-hidden="true">📎</span>
|
||||
<span class="message-attachment-main">
|
||||
<span class="message-attachment-name">${escapeHtml(item.name)}</span>
|
||||
<span class="message-attachment-meta">${escapeHtml(formatBytes(item.size))} · ${escapeHtml(item.ar)}</span>
|
||||
</span>
|
||||
`;
|
||||
wrap.append(link);
|
||||
});
|
||||
return wrap;
|
||||
}
|
||||
Reference in New Issue
Block a user