SHA256
UI вложений и черновики форматов каналов
This commit is contained in:
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
client.version=1.4.21
|
client.version=1.4.22
|
||||||
server.version=1.4.5
|
server.version=1.4.5
|
||||||
|
|||||||
@@ -309,6 +309,7 @@ export function openArweaveAttachmentManager({
|
|||||||
let selectedPreviewEnabled = false;
|
let selectedPreviewEnabled = false;
|
||||||
let selectedPreviewFile = null;
|
let selectedPreviewFile = null;
|
||||||
let selectedPreviewSha256 = '';
|
let selectedPreviewSha256 = '';
|
||||||
|
let selectedPreviewPriceInfo = null;
|
||||||
let priceInfo = null;
|
let priceInfo = null;
|
||||||
let balanceInfo = null;
|
let balanceInfo = null;
|
||||||
let autoOpenedFileDialog = false;
|
let autoOpenedFileDialog = false;
|
||||||
@@ -391,41 +392,48 @@ export function openArweaveAttachmentManager({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderTurboMeta = (metaEl) => {
|
const renderTurboMeta = (mainMetaEl, previewMetaEl) => {
|
||||||
if (!metaEl) return;
|
if (!mainMetaEl || !previewMetaEl) return;
|
||||||
const turboAddress = String(balanceInfo?.address || priceInfo?.address || '').trim();
|
const turboAddress = String(balanceInfo?.address || priceInfo?.address || '').trim();
|
||||||
const turboCredits = balanceInfo ? formatTurboCredits(balanceInfo.effectiveCredits, 6) : '—';
|
const turboCredits = balanceInfo ? formatTurboCredits(balanceInfo.effectiveCredits, 6) : '—';
|
||||||
const solBalance = balanceInfo ? `${escapeHtml(String(balanceInfo.solBalance ?? 0))} SOL` : '—';
|
const solBalance = balanceInfo ? `${escapeHtml(String(balanceInfo.solBalance ?? 0))} SOL` : '—';
|
||||||
const uploadPrice = priceInfo ? `${formatTurboCredits(priceInfo.credits, 6)} credits` : '—';
|
const uploadPrice = priceInfo ? `${formatTurboCredits(priceInfo.credits, 6)} credits` : '—';
|
||||||
const fileSize = selectedFile ? formatBytes(selectedFile.size) : '—';
|
const fileSize = selectedFile ? formatBytes(selectedFile.size) : '—';
|
||||||
const previewSize = selectedPreviewEnabled && selectedPreviewFile ? formatBytes(selectedPreviewFile.size) : '';
|
|
||||||
const freeHint = selectedFile
|
const freeHint = selectedFile
|
||||||
? (combinedUploadSize() <= getFreeTurboUploadBytesLimit() ? 'Да, бесплатно через Turbo' : 'Нет, нужен Turbo balance')
|
? (combinedUploadSize() <= getFreeTurboUploadBytesLimit() ? 'Да, бесплатно через Turbo' : 'Нет, нужен Turbo balance')
|
||||||
: `До ${formatBytes(getFreeTurboUploadBytesLimit())} бесплатно`;
|
: `До ${formatBytes(getFreeTurboUploadBytesLimit())} бесплатно`;
|
||||||
const freeBadge = priceInfo?.isFree
|
const freeBadge = priceInfo?.isFree
|
||||||
? ' <span style="color:#15803d;">(пока файлы до 100 KB бесплатно)</span>'
|
? ' <span style="color:#15803d;">(пока файлы до 100 KB бесплатно)</span>'
|
||||||
: '';
|
: '';
|
||||||
metaEl.innerHTML = `
|
mainMetaEl.innerHTML = `
|
||||||
<div>Turbo-адрес: ${escapeHtml(shortAddress(turboAddress || '—'))}</div>
|
<div>Turbo-адрес: ${escapeHtml(shortAddress(turboAddress || '—'))}</div>
|
||||||
<div>Turbo balance: ${escapeHtml(turboCredits)}</div>
|
<div>Turbo balance: ${escapeHtml(turboCredits)}</div>
|
||||||
<div>SOL на этом ключе: ${solBalance}</div>
|
<div>SOL на этом ключе: ${solBalance}</div>
|
||||||
<div>Размер файла: ${escapeHtml(fileSize)}</div>
|
<div>Размер файла: ${escapeHtml(fileSize)}</div>
|
||||||
${previewSize ? `<div>Размер превью: ${escapeHtml(previewSize)}</div>` : ''}
|
|
||||||
<div>SHA-256: ${escapeHtml(selectedSha256 || '—')}</div>
|
<div>SHA-256: ${escapeHtml(selectedSha256 || '—')}</div>
|
||||||
${selectedPreviewEnabled && selectedPreviewSha256 ? `<div>SHA-256 превью: ${escapeHtml(selectedPreviewSha256)}</div>` : ''}
|
|
||||||
<div>Цена Turbo: ${escapeHtml(uploadPrice)}${freeBadge}</div>
|
<div>Цена Turbo: ${escapeHtml(uploadPrice)}${freeBadge}</div>
|
||||||
<div>До ${escapeHtml(formatBytes(getFreeTurboUploadBytesLimit()))} бесплатно: ${escapeHtml(freeHint)}</div>
|
<div>До ${escapeHtml(formatBytes(getFreeTurboUploadBytesLimit()))} бесплатно: ${escapeHtml(freeHint)}</div>
|
||||||
`;
|
`;
|
||||||
|
previewMetaEl.innerHTML = '';
|
||||||
|
if (selectedPreviewEnabled && selectedPreviewFile) {
|
||||||
|
previewMetaEl.innerHTML = `
|
||||||
|
<div><strong>Файл превью</strong></div>
|
||||||
|
<div>Имя: ${escapeHtml(selectedPreviewFile.name)}</div>
|
||||||
|
<div>Размер превью: ${escapeHtml(formatBytes(selectedPreviewFile.size))}</div>
|
||||||
|
<div>SHA-256 превью: ${escapeHtml(selectedPreviewSha256 || '—')}</div>
|
||||||
|
<div>Цена превью: ${escapeHtml(selectedPreviewPriceInfo ? `${formatTurboCredits(selectedPreviewPriceInfo.credits, 6)} credits` : '—')}</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshTurboBalance = async (metaEl, errorEl) => {
|
const refreshTurboBalance = async (mainMetaEl, previewMetaEl, errorEl) => {
|
||||||
try {
|
try {
|
||||||
balanceInfo = await getTurboBalanceForStoredSolanaKey({
|
balanceInfo = await getTurboBalanceForStoredSolanaKey({
|
||||||
login: cleanLogin,
|
login: cleanLogin,
|
||||||
storagePwd: cleanStoragePwd,
|
storagePwd: cleanStoragePwd,
|
||||||
keySource: selectedTurboKeySource,
|
keySource: selectedTurboKeySource,
|
||||||
});
|
});
|
||||||
renderTurboMeta(metaEl);
|
renderTurboMeta(mainMetaEl, previewMetaEl);
|
||||||
if (errorEl && String(errorEl.textContent || '').includes('Turbo balance')) {
|
if (errorEl && String(errorEl.textContent || '').includes('Turbo balance')) {
|
||||||
setText(errorEl, '');
|
setText(errorEl, '');
|
||||||
}
|
}
|
||||||
@@ -434,7 +442,7 @@ export function openArweaveAttachmentManager({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const promptTurboTopUp = async (metaEl, errorEl) => {
|
const promptTurboTopUp = async (mainMetaEl, previewMetaEl, errorEl) => {
|
||||||
const value = window.prompt('Сколько SOL перевести в Turbo?', '0.01');
|
const value = window.prompt('Сколько SOL перевести в Turbo?', '0.01');
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
setText(errorEl, 'Пополняем Turbo...');
|
setText(errorEl, 'Пополняем Turbo...');
|
||||||
@@ -445,7 +453,7 @@ export function openArweaveAttachmentManager({
|
|||||||
keySource: selectedTurboKeySource,
|
keySource: selectedTurboKeySource,
|
||||||
amountSol: value,
|
amountSol: value,
|
||||||
});
|
});
|
||||||
await refreshTurboBalance(metaEl, errorEl);
|
await refreshTurboBalance(mainMetaEl, previewMetaEl, errorEl);
|
||||||
setText(errorEl, `Turbo пополнен. Tx: ${result.txId || 'отправлено'}`);
|
setText(errorEl, `Turbo пополнен. Tx: ${result.txId || 'отправлено'}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setText(errorEl, error?.message || 'Не удалось пополнить Turbo.');
|
setText(errorEl, error?.message || 'Не удалось пополнить Turbo.');
|
||||||
@@ -456,6 +464,7 @@ export function openArweaveAttachmentManager({
|
|||||||
selectedPreviewEnabled = false;
|
selectedPreviewEnabled = false;
|
||||||
selectedPreviewFile = null;
|
selectedPreviewFile = null;
|
||||||
selectedPreviewSha256 = '';
|
selectedPreviewSha256 = '';
|
||||||
|
selectedPreviewPriceInfo = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isPreviewEligibleForCurrentFile = () => (
|
const isPreviewEligibleForCurrentFile = () => (
|
||||||
@@ -468,30 +477,36 @@ export function openArweaveAttachmentManager({
|
|||||||
return Number(selectedFile?.size || 0) + Number(selectedPreviewEnabled ? (selectedPreviewFile?.size || 0) : 0);
|
return Number(selectedFile?.size || 0) + Number(selectedPreviewEnabled ? (selectedPreviewFile?.size || 0) : 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderArweaveMeta = (metaEl) => {
|
const renderArweaveMeta = (mainMetaEl, previewMetaEl) => {
|
||||||
if (!metaEl) return;
|
if (!mainMetaEl || !previewMetaEl) return;
|
||||||
metaEl.innerHTML = `
|
mainMetaEl.innerHTML = `
|
||||||
${isAvatarMode ? '' : `<div>Имя: ${escapeHtml(selectedFile?.name || 'file')}</div>`}
|
${isAvatarMode ? '' : `<div>Имя: ${escapeHtml(selectedFile?.name || 'file')}</div>`}
|
||||||
<div>Размер: ${escapeHtml(formatBytes(selectedFile?.size || 0))}</div>
|
<div>Размер: ${escapeHtml(formatBytes(selectedFile?.size || 0))}</div>
|
||||||
<div>SHA-256: ${escapeHtml(selectedSha256 || '—')}</div>
|
<div>SHA-256: ${escapeHtml(selectedSha256 || '—')}</div>
|
||||||
${selectedPreviewEnabled && selectedPreviewFile
|
|
||||||
? `<div>Превью: ${escapeHtml(selectedPreviewFile.name)} · ${escapeHtml(formatBytes(selectedPreviewFile.size))}</div>`
|
|
||||||
: ''}
|
|
||||||
${selectedPreviewEnabled && selectedPreviewSha256
|
|
||||||
? `<div>SHA-256 превью: ${escapeHtml(selectedPreviewSha256)}</div>`
|
|
||||||
: ''}
|
|
||||||
<div>Цена: ${escapeHtml(Number(priceInfo?.ar || 0).toLocaleString('ru-RU', { maximumFractionDigits: 6 }))} AR</div>
|
<div>Цена: ${escapeHtml(Number(priceInfo?.ar || 0).toLocaleString('ru-RU', { maximumFractionDigits: 6 }))} AR</div>
|
||||||
<div>Баланс кошелька: ${escapeHtml(Number(balanceInfo?.ar || 0).toLocaleString('ru-RU', { maximumFractionDigits: 6 }))} AR</div>
|
<div>Баланс кошелька: ${escapeHtml(Number(balanceInfo?.ar || 0).toLocaleString('ru-RU', { maximumFractionDigits: 6 }))} AR</div>
|
||||||
`;
|
`;
|
||||||
|
previewMetaEl.innerHTML = '';
|
||||||
|
if (selectedPreviewEnabled && selectedPreviewFile) {
|
||||||
|
previewMetaEl.innerHTML = `
|
||||||
|
<div><strong>Файл превью</strong></div>
|
||||||
|
<div>Имя: ${escapeHtml(selectedPreviewFile.name)}</div>
|
||||||
|
<div>Размер превью: ${escapeHtml(formatBytes(selectedPreviewFile.size))}</div>
|
||||||
|
<div>SHA-256 превью: ${escapeHtml(selectedPreviewSha256 || '—')}</div>
|
||||||
|
<div>Цена превью: ${escapeHtml(selectedPreviewPriceInfo ? `${Number(selectedPreviewPriceInfo.ar || 0).toLocaleString('ru-RU', { maximumFractionDigits: 6 })} AR` : '—')}</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const recalculateArweaveState = async (metaEl, errorEl, uploadBtn) => {
|
const recalculateArweaveState = async (mainMetaEl, previewMetaEl, errorEl, uploadBtn) => {
|
||||||
uploadBtn.disabled = true;
|
uploadBtn.disabled = true;
|
||||||
priceInfo = null;
|
priceInfo = null;
|
||||||
balanceInfo = null;
|
balanceInfo = null;
|
||||||
|
selectedPreviewPriceInfo = null;
|
||||||
setText(errorEl, '');
|
setText(errorEl, '');
|
||||||
if (!selectedFile || !selectedSha256) {
|
if (!selectedFile || !selectedSha256) {
|
||||||
setText(metaEl, '');
|
setText(mainMetaEl, '');
|
||||||
|
setText(previewMetaEl, '');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const wallet = selectedWallet();
|
const wallet = selectedWallet();
|
||||||
@@ -500,6 +515,7 @@ export function openArweaveAttachmentManager({
|
|||||||
let totalWinston = BigInt(mainPrice.winston);
|
let totalWinston = BigInt(mainPrice.winston);
|
||||||
if (selectedPreviewEnabled && selectedPreviewFile) {
|
if (selectedPreviewEnabled && selectedPreviewFile) {
|
||||||
const previewPrice = await getArweaveUploadPrice({ gateway: cleanGateway, byteLength: selectedPreviewFile.size });
|
const previewPrice = await getArweaveUploadPrice({ gateway: cleanGateway, byteLength: selectedPreviewFile.size });
|
||||||
|
selectedPreviewPriceInfo = previewPrice;
|
||||||
totalWinston += BigInt(previewPrice.winston);
|
totalWinston += BigInt(previewPrice.winston);
|
||||||
}
|
}
|
||||||
balanceInfo = await getArweaveBalance({ gateway: cleanGateway, address: wallet.address });
|
balanceInfo = await getArweaveBalance({ gateway: cleanGateway, address: wallet.address });
|
||||||
@@ -507,7 +523,7 @@ export function openArweaveAttachmentManager({
|
|||||||
ar: Number(totalWinston) / 1e12,
|
ar: Number(totalWinston) / 1e12,
|
||||||
winston: totalWinston.toString(),
|
winston: totalWinston.toString(),
|
||||||
};
|
};
|
||||||
renderArweaveMeta(metaEl);
|
renderArweaveMeta(mainMetaEl, previewMetaEl);
|
||||||
const hasFunds = BigInt(balanceInfo.winston) >= totalWinston;
|
const hasFunds = BigInt(balanceInfo.winston) >= totalWinston;
|
||||||
if (!hasFunds) {
|
if (!hasFunds) {
|
||||||
setText(errorEl, 'Недостаточно AR на выбранном кошельке.');
|
setText(errorEl, 'Недостаточно AR на выбранном кошельке.');
|
||||||
@@ -516,13 +532,14 @@ export function openArweaveAttachmentManager({
|
|||||||
uploadBtn.disabled = false;
|
uploadBtn.disabled = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshTurboStateForCurrentFile = async (metaEl, errorEl, uploadBtn) => {
|
const refreshTurboStateForCurrentFile = async (mainMetaEl, previewMetaEl, errorEl, uploadBtn) => {
|
||||||
uploadBtn.disabled = true;
|
uploadBtn.disabled = true;
|
||||||
priceInfo = null;
|
priceInfo = null;
|
||||||
balanceInfo = null;
|
balanceInfo = null;
|
||||||
|
selectedPreviewPriceInfo = null;
|
||||||
setText(errorEl, '');
|
setText(errorEl, '');
|
||||||
if (!selectedFile || !selectedSha256) {
|
if (!selectedFile || !selectedSha256) {
|
||||||
renderTurboMeta(metaEl);
|
renderTurboMeta(mainMetaEl, previewMetaEl);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -540,6 +557,7 @@ export function openArweaveAttachmentManager({
|
|||||||
keySource: selectedTurboKeySource,
|
keySource: selectedTurboKeySource,
|
||||||
byteLength: selectedPreviewFile.size,
|
byteLength: selectedPreviewFile.size,
|
||||||
});
|
});
|
||||||
|
selectedPreviewPriceInfo = previewPrice;
|
||||||
totalWinc += BigInt(previewPrice.winc);
|
totalWinc += BigInt(previewPrice.winc);
|
||||||
}
|
}
|
||||||
priceInfo = {
|
priceInfo = {
|
||||||
@@ -554,7 +572,7 @@ export function openArweaveAttachmentManager({
|
|||||||
storagePwd: cleanStoragePwd,
|
storagePwd: cleanStoragePwd,
|
||||||
keySource: selectedTurboKeySource,
|
keySource: selectedTurboKeySource,
|
||||||
});
|
});
|
||||||
renderTurboMeta(metaEl);
|
renderTurboMeta(mainMetaEl, previewMetaEl);
|
||||||
if (!priceInfo.isFree && BigInt(balanceInfo.effectiveWinc) < BigInt(priceInfo.winc)) {
|
if (!priceInfo.isFree && BigInt(balanceInfo.effectiveWinc) < BigInt(priceInfo.winc)) {
|
||||||
setText(errorEl, 'Turbo balance недостаточен. Нажмите «Пополнить Turbo».');
|
setText(errorEl, 'Turbo balance недостаточен. Нажмите «Пополнить Turbo».');
|
||||||
return;
|
return;
|
||||||
@@ -591,11 +609,12 @@ export function openArweaveAttachmentManager({
|
|||||||
`}
|
`}
|
||||||
<label class="meta-muted" for="ar-attach-file">Файл для загрузки</label>
|
<label class="meta-muted" for="ar-attach-file">Файл для загрузки</label>
|
||||||
<input class="input" id="ar-attach-file" type="file" ${isAvatarMode ? 'accept="image/*"' : ''} />
|
<input class="input" id="ar-attach-file" type="file" ${isAvatarMode ? 'accept="image/*"' : ''} />
|
||||||
|
<div class="ar-attachment-meta" data-meta-main="true"></div>
|
||||||
<label class="meta-muted" data-preview-option="true" hidden>
|
<label class="meta-muted" data-preview-option="true" hidden>
|
||||||
<input type="checkbox" data-preview-toggle="true" />
|
<input type="checkbox" data-preview-toggle="true" />
|
||||||
Добавить превью
|
Добавить превью
|
||||||
</label>
|
</label>
|
||||||
<div class="ar-attachment-meta" data-meta="true"></div>
|
<div class="ar-attachment-meta" data-meta-preview="true"></div>
|
||||||
<p class="meta-muted inline-error" data-error="true"></p>
|
<p class="meta-muted inline-error" data-error="true"></p>
|
||||||
<div class="form-actions-grid">
|
<div class="form-actions-grid">
|
||||||
${turboMode ? '<button class="secondary-btn" type="button" data-action="topup">Пополнить Turbo</button>' : ''}
|
${turboMode ? '<button class="secondary-btn" type="button" data-action="topup">Пополнить Turbo</button>' : ''}
|
||||||
@@ -609,7 +628,8 @@ export function openArweaveAttachmentManager({
|
|||||||
const walletEl = root.querySelector('#ar-attach-wallet');
|
const walletEl = root.querySelector('#ar-attach-wallet');
|
||||||
const turboKeyEl = root.querySelector('#turbo-key-source');
|
const turboKeyEl = root.querySelector('#turbo-key-source');
|
||||||
const fileEl = root.querySelector('#ar-attach-file');
|
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 errorEl = root.querySelector('[data-error="true"]');
|
||||||
const uploadBtn = root.querySelector('[data-action="upload"]');
|
const uploadBtn = root.querySelector('[data-action="upload"]');
|
||||||
const previewOptionEl = root.querySelector('[data-preview-option="true"]');
|
const previewOptionEl = root.querySelector('[data-preview-option="true"]');
|
||||||
@@ -648,10 +668,10 @@ export function openArweaveAttachmentManager({
|
|||||||
turboKeyEl.addEventListener('change', async () => {
|
turboKeyEl.addEventListener('change', async () => {
|
||||||
selectedTurboKeySource = String(turboKeyEl.value || 'client');
|
selectedTurboKeySource = String(turboKeyEl.value || 'client');
|
||||||
writeLastTurboKeySource(cleanLogin, selectedTurboKeySource);
|
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 {
|
} else {
|
||||||
await loadWallets(errorEl);
|
await loadWallets(errorEl);
|
||||||
if (walletEl) {
|
if (walletEl) {
|
||||||
@@ -667,16 +687,17 @@ export function openArweaveAttachmentManager({
|
|||||||
priceInfo = null;
|
priceInfo = null;
|
||||||
balanceInfo = null;
|
balanceInfo = null;
|
||||||
if (fileEl) fileEl.value = '';
|
if (fileEl) fileEl.value = '';
|
||||||
setText(metaEl, '');
|
setText(mainMetaEl, '');
|
||||||
|
setText(previewMetaEl, '');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
root.querySelector('[data-action="cancel"]')?.addEventListener('click', () => close(resolve, null));
|
root.querySelector('[data-action="cancel"]')?.addEventListener('click', () => close(resolve, null));
|
||||||
root.querySelector('[data-action="topup"]')?.addEventListener('click', async () => {
|
root.querySelector('[data-action="topup"]')?.addEventListener('click', async () => {
|
||||||
await promptTurboTopUp(metaEl, errorEl);
|
await promptTurboTopUp(mainMetaEl, previewMetaEl, errorEl);
|
||||||
if (turboMode && selectedFile && selectedSha256) {
|
if (turboMode && selectedFile && selectedSha256) {
|
||||||
await refreshTurboStateForCurrentFile(metaEl, errorEl, uploadBtn);
|
await refreshTurboStateForCurrentFile(mainMetaEl, previewMetaEl, errorEl, uploadBtn);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (!turboMode) {
|
if (!turboMode) {
|
||||||
@@ -691,7 +712,8 @@ export function openArweaveAttachmentManager({
|
|||||||
balanceInfo = null;
|
balanceInfo = null;
|
||||||
uploadBtn.disabled = true;
|
uploadBtn.disabled = true;
|
||||||
setText(errorEl, '');
|
setText(errorEl, '');
|
||||||
setText(metaEl, '');
|
setText(mainMetaEl, '');
|
||||||
|
setText(previewMetaEl, '');
|
||||||
if (previewToggleEl instanceof HTMLInputElement) previewToggleEl.checked = false;
|
if (previewToggleEl instanceof HTMLInputElement) previewToggleEl.checked = false;
|
||||||
if (previewOptionEl) previewOptionEl.hidden = true;
|
if (previewOptionEl) previewOptionEl.hidden = true;
|
||||||
if (!selectedFile) return;
|
if (!selectedFile) return;
|
||||||
@@ -709,9 +731,9 @@ export function openArweaveAttachmentManager({
|
|||||||
previewOptionEl.hidden = false;
|
previewOptionEl.hidden = false;
|
||||||
}
|
}
|
||||||
if (turboMode) {
|
if (turboMode) {
|
||||||
await refreshTurboStateForCurrentFile(metaEl, errorEl, uploadBtn);
|
await refreshTurboStateForCurrentFile(mainMetaEl, previewMetaEl, errorEl, uploadBtn);
|
||||||
} else {
|
} else {
|
||||||
await recalculateArweaveState(metaEl, errorEl, uploadBtn);
|
await recalculateArweaveState(mainMetaEl, previewMetaEl, errorEl, uploadBtn);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setText(errorEl, error?.message || 'Не удалось подготовить файл.');
|
setText(errorEl, error?.message || 'Не удалось подготовить файл.');
|
||||||
@@ -735,9 +757,9 @@ export function openArweaveAttachmentManager({
|
|||||||
selectedPreviewSha256 = await sha256HexFromArrayBuffer(await selectedPreviewFile.arrayBuffer());
|
selectedPreviewSha256 = await sha256HexFromArrayBuffer(await selectedPreviewFile.arrayBuffer());
|
||||||
}
|
}
|
||||||
if (turboMode) {
|
if (turboMode) {
|
||||||
await refreshTurboStateForCurrentFile(metaEl, errorEl, uploadBtn);
|
await refreshTurboStateForCurrentFile(mainMetaEl, previewMetaEl, errorEl, uploadBtn);
|
||||||
} else {
|
} else {
|
||||||
await recalculateArweaveState(metaEl, errorEl, uploadBtn);
|
await recalculateArweaveState(mainMetaEl, previewMetaEl, errorEl, uploadBtn);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
resetPreviewState();
|
resetPreviewState();
|
||||||
|
|||||||
@@ -286,11 +286,21 @@ function createMediaSlide({ item, url, kind, messageTimestampMs, slide, previewU
|
|||||||
img.loading = 'lazy';
|
img.loading = 'lazy';
|
||||||
img.addEventListener('error', () => replaceWithUnavailable(slide, messageTimestampMs), { once: true });
|
img.addEventListener('error', () => replaceWithUnavailable(slide, messageTimestampMs), { once: true });
|
||||||
frame.append(img);
|
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 {
|
} else {
|
||||||
const video = document.createElement('video');
|
const video = document.createElement('video');
|
||||||
video.className = 'message-attachment-media';
|
video.className = 'message-attachment-media';
|
||||||
video.src = url;
|
video.src = url;
|
||||||
if (previewUrl) video.poster = previewUrl;
|
|
||||||
video.preload = 'metadata';
|
video.preload = 'metadata';
|
||||||
video.muted = true;
|
video.muted = true;
|
||||||
video.playsInline = true;
|
video.playsInline = true;
|
||||||
|
|||||||
@@ -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. в начале идут технические теги `<SHiNE:...>`;
|
||||||
|
2. после тегов идёт обычный UTF-8 текст;
|
||||||
|
3. этот хвостовой текст считается описанием канала.
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
|
||||||
|
```text
|
||||||
|
<SHiNE:title;v=1;Моё имя канала>
|
||||||
|
<SHiNE:avatar;v=1;size=248193;sha256=3f2c8a...;ar=AbCdEf...>
|
||||||
|
Тут описание канала.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Поддерживаемые теги
|
||||||
|
|
||||||
|
### 1. Тег заглавия
|
||||||
|
|
||||||
|
Формат:
|
||||||
|
|
||||||
|
```text
|
||||||
|
<SHiNE:title;v=1;Моё имя канала>
|
||||||
|
```
|
||||||
|
|
||||||
|
Смысл:
|
||||||
|
|
||||||
|
- задаёт красивое отображаемое имя канала;
|
||||||
|
- хранится как обычный UTF-8 текст;
|
||||||
|
- это не технический slug и не системное имя.
|
||||||
|
|
||||||
|
### 2. Тег аватара
|
||||||
|
|
||||||
|
Формат:
|
||||||
|
|
||||||
|
```text
|
||||||
|
<SHiNE:avatar;v=1;size=248193;sha256=3f2c8a...;ar=AbCdEf...>
|
||||||
|
```
|
||||||
|
|
||||||
|
Смысл:
|
||||||
|
|
||||||
|
- задаёт аватар канала;
|
||||||
|
- `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`
|
||||||
|
- это скрытый технический текстовый блок канала
|
||||||
|
- порядок: теги в начале, потом описание
|
||||||
|
- теги:
|
||||||
|
- `<SHiNE:title;v=1;...>`
|
||||||
|
- `<SHiNE:avatar;v=1;size=...;sha256=...;ar=...>`
|
||||||
|
- `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-блоков.
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
# Черновик формата вложений в сообщениях блокчейна SHiNE
|
||||||
|
|
||||||
|
## Статус документа
|
||||||
|
|
||||||
|
Этот файл пока является черновиком и рабочим описанием идеи.
|
||||||
|
|
||||||
|
Важно:
|
||||||
|
|
||||||
|
- это не финальная спецификация;
|
||||||
|
- точная версия формата будет зафиксирована после начала реальной реализации;
|
||||||
|
- до момента реализации допустимы изменения полей, синтаксиса и правил отображения;
|
||||||
|
- документ специально лежит в корне проекта как временная рабочая заметка.
|
||||||
|
|
||||||
|
## Цель
|
||||||
|
|
||||||
|
Нужен минимальный формат, позволяющий прикладывать файлы к текстовым сообщениям в каналах и тредах, не превращая само сообщение в отдельный файловый контейнер.
|
||||||
|
|
||||||
|
Основная идея:
|
||||||
|
|
||||||
|
- само сообщение остаётся обычным текстом;
|
||||||
|
- вложения описываются специальными техническими вставками в начале текста;
|
||||||
|
- сами файлы хранятся отдельно в архиве/Arweave;
|
||||||
|
- в сообщении хранятся только метаданные, достаточные для отображения и скачивания.
|
||||||
|
|
||||||
|
## Общий принцип
|
||||||
|
|
||||||
|
Если в начале текста сообщения стоит один или несколько блоков:
|
||||||
|
|
||||||
|
```text
|
||||||
|
<SHiNE:attach;...>
|
||||||
|
```
|
||||||
|
|
||||||
|
клиент трактует их как описания вложений.
|
||||||
|
|
||||||
|
После этих блоков идёт обычный пользовательский текст сообщения.
|
||||||
|
|
||||||
|
Если технических блоков нет, сообщение считается обычным текстовым сообщением без вложений.
|
||||||
|
|
||||||
|
## Формат одного блока вложения
|
||||||
|
|
||||||
|
Общий вид:
|
||||||
|
|
||||||
|
```text
|
||||||
|
<SHiNE:attach;v=1;name=photo.jpg;size=248193;sha256=abcdef...;ar=tx_or_url>
|
||||||
|
```
|
||||||
|
|
||||||
|
Где:
|
||||||
|
|
||||||
|
- `attach` — тип технической вставки;
|
||||||
|
- `v=1` — текущая рабочая версия чернового формата;
|
||||||
|
- `name` — имя файла;
|
||||||
|
- `size` — размер файла в байтах;
|
||||||
|
- `sha256` — SHA-256 файла в hex;
|
||||||
|
- `ar` — идентификатор файла в архиве или адрес файла в Arweave.
|
||||||
|
|
||||||
|
## Несколько вложений
|
||||||
|
|
||||||
|
Если к одному сообщению приложено несколько файлов, блоки просто идут подряд в начале текста:
|
||||||
|
|
||||||
|
```text
|
||||||
|
<SHiNE:attach;v=1;name=photo.jpg;size=248193;sha256=aaa...;ar=...>
|
||||||
|
<SHiNE:attach;v=1;name=video.mp4;size=5820193;sha256=bbb...;ar=...>
|
||||||
|
Текст сообщения
|
||||||
|
```
|
||||||
|
|
||||||
|
## Обязательные поля
|
||||||
|
|
||||||
|
Для чернового варианта обязательными считаются:
|
||||||
|
|
||||||
|
- `v`
|
||||||
|
- `name`
|
||||||
|
- `size`
|
||||||
|
- `sha256`
|
||||||
|
- `ar`
|
||||||
|
|
||||||
|
Если какого-то из этих полей нет, клиент может:
|
||||||
|
|
||||||
|
- игнорировать конкретный битый блок;
|
||||||
|
- не считать его валидным вложением;
|
||||||
|
- при этом продолжать обрабатывать остальные корректные блоки.
|
||||||
|
|
||||||
|
## Почему без `mime` и без `kind`
|
||||||
|
|
||||||
|
В минимальном варианте тип файла отдельно не хранится.
|
||||||
|
|
||||||
|
Причины:
|
||||||
|
|
||||||
|
- это экономит место;
|
||||||
|
- тип всё равно обычно определяется клиентом;
|
||||||
|
- для первого варианта достаточно имени файла, адреса, хэша и размера;
|
||||||
|
- превью и способ отображения клиент может решать сам по расширению имени файла или по попытке открыть файл.
|
||||||
|
|
||||||
|
То есть в сообщении не хранится:
|
||||||
|
|
||||||
|
- `mime`;
|
||||||
|
- `kind`;
|
||||||
|
- `width`;
|
||||||
|
- `height`;
|
||||||
|
- `duration`;
|
||||||
|
- `thumbnail`.
|
||||||
|
|
||||||
|
Все эти поля можно добавить позже отдельной версией, если они реально понадобятся.
|
||||||
|
|
||||||
|
## Логика клиента
|
||||||
|
|
||||||
|
Клиент получает:
|
||||||
|
|
||||||
|
- имя файла;
|
||||||
|
- размер;
|
||||||
|
- хэш;
|
||||||
|
- адрес файла.
|
||||||
|
|
||||||
|
После этого клиент сам решает, как рендерить вложение:
|
||||||
|
|
||||||
|
- если расширение похоже на изображение, можно пробовать показать preview;
|
||||||
|
- если расширение похоже на видео, можно пробовать встроенный видеоплеер;
|
||||||
|
- если расширение похоже на аудио, можно пробовать аудиоплеер;
|
||||||
|
- иначе показывать обычную кнопку скачивания.
|
||||||
|
|
||||||
|
Если браузер или клиент не смог показать preview, должен быть fallback на скачивание файла.
|
||||||
|
|
||||||
|
## Правила совместимости
|
||||||
|
|
||||||
|
- Сообщение без `<SHiNE:attach;...>` остаётся обычным текстом.
|
||||||
|
- Старые клиенты, которые не умеют распознавать этот формат, могут показывать сообщение как обычный текст целиком.
|
||||||
|
- Новые клиенты могут скрывать технические блоки и показывать вместо них UI вложений.
|
||||||
|
|
||||||
|
## Ограничения чернового варианта
|
||||||
|
|
||||||
|
В текущем виде значения полей не предполагают сложного экранирования.
|
||||||
|
|
||||||
|
Значит нужно отдельно договориться, как безопасно хранить `name`, если там встретятся символы:
|
||||||
|
|
||||||
|
- `;`
|
||||||
|
- `=`
|
||||||
|
- `>`
|
||||||
|
|
||||||
|
На этапе реальной реализации нужно выбрать один из вариантов:
|
||||||
|
|
||||||
|
- жёстко ограничить допустимые символы в `name`;
|
||||||
|
- хранить имя файла в безопасно закодированном виде;
|
||||||
|
- добавить отдельное правило escape/encoding.
|
||||||
|
|
||||||
|
Пока этот вопрос считается открытым.
|
||||||
|
|
||||||
|
## Примеры
|
||||||
|
|
||||||
|
### Одно вложение
|
||||||
|
|
||||||
|
```text
|
||||||
|
<SHiNE:attach;v=1;name=photo.jpg;size=248193;sha256=3f2c8a...;ar=6sYk...>
|
||||||
|
Привет, вот фото
|
||||||
|
```
|
||||||
|
|
||||||
|
### Два вложения
|
||||||
|
|
||||||
|
```text
|
||||||
|
<SHiNE:attach;v=1;name=photo.jpg;size=248193;sha256=3f2c8a...;ar=6sYk...>
|
||||||
|
<SHiNE:attach;v=1;name=report.pdf;size=845221;sha256=9ab01e...;ar=K9Lp...>
|
||||||
|
Смотри, прикрепил фото и документ
|
||||||
|
```
|
||||||
|
|
||||||
|
## Что надо зафиксировать позже
|
||||||
|
|
||||||
|
Перед переводом этого черновика в официальную спецификацию нужно отдельно утвердить:
|
||||||
|
|
||||||
|
- точное место этого формата в общей документации блокчейна;
|
||||||
|
- где именно используются такие вложения: каналы, треды, комментарии или иные текстовые блоки;
|
||||||
|
- финальные правила кодирования `name`;
|
||||||
|
- допустим ли полный URL в `ar` или только короткий `txId`;
|
||||||
|
- нужно ли ограничение на количество вложений в одном сообщении;
|
||||||
|
- нужна ли отдельная серверная или клиентская проверка расширения файла;
|
||||||
|
- как именно это будет отображаться в web UI и других клиентах.
|
||||||
Reference in New Issue
Block a user