UI: добавить просмотр данных блокчейна сообщения

This commit is contained in:
AidarKC
2026-07-31 17:41:30 +04:00
parent d640dd66e2
commit d5f41d12e8
5 changed files with 388 additions and 25 deletions
+140 -12
View File
@@ -22,6 +22,7 @@ import { markArweaveAttachmentPlaced, openArweaveAttachmentManager } from '../co
import {
composeMessageWithAttachments,
createAttachmentCarouselElement,
escapeHtml,
MAX_MESSAGE_ATTACHMENTS,
parseMessageAttachments,
} from '../services/attachment-format.js';
@@ -313,6 +314,91 @@ function bindSubmitOnPlainEnter(textarea, submit) {
});
}
function setActionTitle(button, label) {
if (!button) return;
button.title = label;
button.setAttribute('aria-label', label);
const labelEl = button.querySelector('.channel-action-label');
if (labelEl) labelEl.textContent = label;
}
async function copyTextToClipboard(text) {
if (navigator?.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
return true;
}
const ta = document.createElement('textarea');
ta.value = text;
ta.setAttribute('readonly', '');
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.append(ta);
ta.select();
const ok = document.execCommand('copy');
ta.remove();
return !!ok;
}
function buildBlockchainDetails({ messageRef, authorLogin, timestampMs, text, raw, localNumber, msgSubType }) {
const source = raw && typeof raw === 'object' ? raw : {};
return {
authorLogin,
authorBlockchainName: messageRef?.blockchainName || source.authorBlockchainName || '',
blockNumber: messageRef?.blockNumber ?? source?.messageRef?.blockNumber ?? '',
blockHash: messageRef?.blockHash || source?.messageRef?.blockHash || '',
localNumber,
msgSubType: msgSubType ?? source.msgSubType ?? '',
createdAtMs: timestampMs || source.createdAtMs || '',
text: String(text || ''),
signature: source.signature || source.blockSignature || source.authorSignature || 'нет в ответе сервера',
publicKey: source.publicKey || source.authorPublicKey || 'нет в ответе сервера',
raw,
};
}
function openBlockchainDetailsModal(details) {
const root = document.getElementById('modal-root');
const rawText = JSON.stringify(details.raw || details, null, 2);
root.innerHTML = `
<div class="modal" id="blockchain-details-modal">
<div class="modal-card stack blockchain-details-card">
<h3 class="modal-title">Данные блокчейна сообщения</h3>
<p class="meta-muted">Это технические данные записи SHiNE. По ним можно увидеть цепочку автора, номер записи, хэш и подпись, если она есть в ответе сервера.</p>
<div class="blockchain-details-grid">
<span>Автор</span><strong>${escapeHtml(details.authorLogin)}</strong>
<span>Блокчейн</span><code>${escapeHtml(details.authorBlockchainName)}</code>
<span>Номер записи</span><code>${escapeHtml(details.blockNumber)}</code>
<span>Хэш</span><code>${escapeHtml(details.blockHash)}</code>
<span>Тип</span><code>${escapeHtml(details.msgSubType)}</code>
<span>Время</span><code>${escapeHtml(details.createdAtMs ? new Date(Number(details.createdAtMs)).toLocaleString('ru-RU') : '—')}</code>
<span>Public key</span><code>${escapeHtml(details.publicKey)}</code>
<span>Подпись</span><code>${escapeHtml(details.signature)}</code>
</div>
<label class="field-label" for="blockchain-details-text">Текст записи</label>
<textarea class="input" id="blockchain-details-text" rows="4" readonly>${escapeHtml(details.text)}</textarea>
<pre class="blockchain-raw-block" id="blockchain-raw-block" hidden>${escapeHtml(rawText)}</pre>
<div class="form-actions-grid">
<button class="secondary-btn" id="blockchain-details-copy" type="button">Скопировать</button>
<button class="secondary-btn" id="blockchain-details-raw" type="button">Показать сырой блок</button>
</div>
<button class="secondary-btn" id="blockchain-details-close" type="button">Закрыть</button>
</div>
</div>
`;
root.querySelector('#blockchain-details-close')?.addEventListener('click', () => {
root.innerHTML = '';
});
root.querySelector('#blockchain-details-copy')?.addEventListener('click', async () => {
await copyTextToClipboard(rawText);
showToast('Данные блокчейна скопированы');
});
root.querySelector('#blockchain-details-raw')?.addEventListener('click', () => {
const rawEl = root.querySelector('#blockchain-raw-block');
if (!rawEl) return;
rawEl.hidden = !rawEl.hidden;
});
}
function renderDraftAttachments(container, attachments) {
if (!container) return;
container.innerHTML = '';
@@ -693,6 +779,7 @@ function mapApiMessageToPost(message, selector, localNumber) {
repliesCount: Number(message?.repliesCount || 0),
timestampMs: resolveMessageTimestampMs(message),
messageRef,
rawMessage: message,
msgSubType: Number(message?.msgSubType || 0),
targetRef: message?.targetBlockchainName && Number.isFinite(Number(message?.targetBlockNumber))
? {
@@ -1011,18 +1098,35 @@ function renderPostCard(post, {
const isDeletedMessage = String(post.body || '').trim().toLowerCase() === 'удалено';
const parsedBody = parseMessageAttachments(post.body);
const body = document.createElement('p');
body.className = `channel-message-body${isDeletedMessage ? ' channel-message-body--deleted' : ''}`;
body.textContent = isDeletedMessage ? 'Сообщение удалено' : parsedBody.text;
card.append(authorTile);
if (!isDeletedMessage && parsedBody.attachments.length > 0) {
card.append(createAttachmentCarouselElement(parsedBody.attachments, {
gateway: state.entrySettings.arweaveServer,
messageTimestampMs: post.timestampMs,
}));
if (isDeletedMessage) {
card.classList.add('channel-message-card--deleted-compact');
const deleted = document.createElement('button');
deleted.type = 'button';
deleted.className = 'deleted-message-pill';
deleted.textContent = `Удалённое сообщение от ${post.authorLogin}`;
deleted.title = 'Открыть историю изменений';
deleted.addEventListener('click', (event) => {
event.stopPropagation();
openMessageHistoryModal({
title: `История #${post.localNumber}`,
versions: post.versions,
});
});
card.append(deleted);
} else {
card.append(authorTile);
if (parsedBody.attachments.length > 0) {
card.append(createAttachmentCarouselElement(parsedBody.attachments, {
gateway: state.entrySettings.arweaveServer,
messageTimestampMs: post.timestampMs,
}));
}
const body = document.createElement('p');
body.className = 'channel-message-body';
body.textContent = parsedBody.text;
card.append(body);
}
card.append(body);
const refKey = messageRefKey(post.messageRef);
if (refKey) {
@@ -1048,6 +1152,7 @@ function renderPostCard(post, {
<span class="channel-action-label">${isPending ? 'Лайк...' : 'Лайк'}</span>
<span class="channel-action-counter">${post.likesCount || 0}</span>
`;
setActionTitle(likeButton, isPending ? 'Лайк...' : 'Лайк');
likeButton.disabled = isPending;
likeButton.addEventListener('click', async (event) => {
event.stopPropagation();
@@ -1059,8 +1164,7 @@ function renderPostCard(post, {
}
await longPressFeel(event.currentTarget, 130);
likeButton.disabled = true;
const labelEl = likeButton.querySelector('.channel-action-label');
if (labelEl) labelEl.textContent = 'Лайк...';
setActionTitle(likeButton, 'Лайк...');
await onToggleLike(post.messageRef, isLiked ? 'unlike' : 'like', { likeButton });
});
@@ -1072,6 +1176,7 @@ function renderPostCard(post, {
<span class="channel-action-label">Ответить</span>
<span class="channel-action-counter">${post.repliesCount || 0}</span>
`;
setActionTitle(replyButton, 'Ответить');
replyButton.addEventListener('click', (event) => {
event.stopPropagation();
animatePress(event.currentTarget);
@@ -1091,6 +1196,7 @@ function renderPostCard(post, {
<span class="channel-action-icon" aria-hidden="true">↗</span>
<span class="channel-action-label">Отправить</span>
`;
setActionTitle(shareButton, 'Отправить');
shareButton.addEventListener('click', async (event) => {
event.stopPropagation();
animatePress(event.currentTarget);
@@ -1107,6 +1213,7 @@ function renderPostCard(post, {
<span class="channel-action-icon" aria-hidden="true">↪</span>
<span class="channel-action-label">Оригинал</span>
`;
setActionTitle(originalBtn, 'Оригинал');
originalBtn.addEventListener('click', (event) => {
event.stopPropagation();
const ownerLogin = extractLoginFromBlockchainName(post.targetRef.blockchainName);
@@ -1121,6 +1228,27 @@ function renderPostCard(post, {
});
actions.append(originalBtn);
}
const detailsButton = document.createElement('button');
detailsButton.type = 'button';
detailsButton.className = 'channel-action-item';
detailsButton.innerHTML = `
<span class="channel-action-icon" aria-hidden="true">⛓</span>
<span class="channel-action-label">Данные блокчейна</span>
`;
setActionTitle(detailsButton, 'Данные блокчейна');
detailsButton.addEventListener('click', (event) => {
event.stopPropagation();
openBlockchainDetailsModal(buildBlockchainDetails({
messageRef: post.messageRef,
authorLogin: post.authorLogin,
timestampMs: post.timestampMs,
text: post.body,
raw: post.rawMessage,
localNumber: post.localNumber,
msgSubType: post.msgSubType,
}));
});
actions.append(detailsButton);
if (post.isOwnMessage) {
const editButton = document.createElement('button');
editButton.type = 'button';