SHA256
UI: добавить просмотр данных блокчейна сообщения
This commit is contained in:
@@ -16,6 +16,7 @@ import { markArweaveAttachmentPlaced, openArweaveAttachmentManager } from '../co
|
||||
import {
|
||||
composeMessageWithAttachments,
|
||||
createAttachmentCarouselElement,
|
||||
escapeHtml,
|
||||
MAX_MESSAGE_ATTACHMENTS,
|
||||
parseMessageAttachments,
|
||||
} from '../services/attachment-format.js';
|
||||
@@ -329,6 +330,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({ target, authorLogin, timestampMs, text, raw, localNumber, msgSubType }) {
|
||||
const source = raw && typeof raw === 'object' ? raw : {};
|
||||
return {
|
||||
authorLogin,
|
||||
authorBlockchainName: target?.blockchainName || source.authorBlockchainName || '',
|
||||
blockNumber: target?.blockNumber ?? source?.messageRef?.blockNumber ?? '',
|
||||
blockHash: target?.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="thread-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="thread-blockchain-details-text">Текст записи</label>
|
||||
<textarea class="input" id="thread-blockchain-details-text" rows="4" readonly>${escapeHtml(details.text)}</textarea>
|
||||
<pre class="blockchain-raw-block" id="thread-blockchain-raw-block" hidden>${escapeHtml(rawText)}</pre>
|
||||
<div class="form-actions-grid">
|
||||
<button class="secondary-btn" id="thread-blockchain-details-copy" type="button">Скопировать</button>
|
||||
<button class="secondary-btn" id="thread-blockchain-details-raw" type="button">Показать сырой блок</button>
|
||||
</div>
|
||||
<button class="secondary-btn" id="thread-blockchain-details-close" type="button">Закрыть</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
root.querySelector('#thread-blockchain-details-close')?.addEventListener('click', () => {
|
||||
root.innerHTML = '';
|
||||
});
|
||||
root.querySelector('#thread-blockchain-details-copy')?.addEventListener('click', async () => {
|
||||
await copyTextToClipboard(rawText);
|
||||
showToast('Данные блокчейна скопированы');
|
||||
});
|
||||
root.querySelector('#thread-blockchain-details-raw')?.addEventListener('click', () => {
|
||||
const rawEl = root.querySelector('#thread-blockchain-raw-block');
|
||||
if (!rawEl) return;
|
||||
rawEl.hidden = !rawEl.hidden;
|
||||
});
|
||||
}
|
||||
|
||||
function resolveNodeText(node) {
|
||||
return firstNonEmptyText(
|
||||
node?.text,
|
||||
@@ -671,18 +757,35 @@ function renderNodeCard(node, heading, handlers, localNumber) {
|
||||
authorTile.append(avatar, authorBlock);
|
||||
|
||||
const isDeletedMessage = String(text || '').trim().toLowerCase() === 'удалено';
|
||||
const body = document.createElement('p');
|
||||
body.className = `channel-message-body${isDeletedMessage ? ' channel-message-body--deleted' : ''}`;
|
||||
body.textContent = isDeletedMessage ? 'Сообщение удалено' : parsedText.text;
|
||||
|
||||
card.append(authorTile);
|
||||
if (!isDeletedMessage && parsedText.attachments.length > 0) {
|
||||
card.append(createAttachmentCarouselElement(parsedText.attachments, {
|
||||
gateway: state.entrySettings.arweaveServer,
|
||||
messageTimestampMs: node?.createdAtMs,
|
||||
}));
|
||||
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 = `Удалённое сообщение от ${author}`;
|
||||
deleted.title = 'Открыть историю изменений';
|
||||
deleted.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
openMessageHistoryModal({
|
||||
title: `История #${localNumber}`,
|
||||
versions,
|
||||
});
|
||||
});
|
||||
card.append(deleted);
|
||||
} else {
|
||||
card.append(authorTile);
|
||||
if (parsedText.attachments.length > 0) {
|
||||
card.append(createAttachmentCarouselElement(parsedText.attachments, {
|
||||
gateway: state.entrySettings.arweaveServer,
|
||||
messageTimestampMs: node?.createdAtMs,
|
||||
}));
|
||||
}
|
||||
const body = document.createElement('p');
|
||||
body.className = 'channel-message-body';
|
||||
body.textContent = parsedText.text;
|
||||
card.append(body);
|
||||
}
|
||||
card.append(body);
|
||||
|
||||
const target = buildTargetFromNode(node);
|
||||
const refKey = messageRefKey(target);
|
||||
@@ -709,6 +812,7 @@ function renderNodeCard(node, heading, handlers, localNumber) {
|
||||
<span class="channel-action-label">${isPending ? 'Лайк...' : 'Лайк'}</span>
|
||||
<span class="channel-action-counter">${likes}</span>
|
||||
`;
|
||||
setActionTitle(likeButton, isPending ? 'Лайк...' : 'Лайк');
|
||||
likeButton.disabled = isPending;
|
||||
likeButton.addEventListener('click', async (event) => {
|
||||
event.stopPropagation();
|
||||
@@ -720,6 +824,7 @@ function renderNodeCard(node, heading, handlers, localNumber) {
|
||||
}
|
||||
await longPressFeel(event.currentTarget, 130);
|
||||
likeButton.disabled = true;
|
||||
setActionTitle(likeButton, 'Лайк...');
|
||||
try {
|
||||
await handlers.onToggleLike(target, isLiked ? 'unlike' : 'like');
|
||||
} catch (error) {
|
||||
@@ -740,6 +845,7 @@ function renderNodeCard(node, heading, handlers, localNumber) {
|
||||
<span class="channel-action-label">Ответить</span>
|
||||
<span class="channel-action-counter">${replies}</span>
|
||||
`;
|
||||
setActionTitle(replyButton, 'Ответить');
|
||||
replyButton.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
animatePress(event.currentTarget);
|
||||
@@ -756,6 +862,7 @@ function renderNodeCard(node, heading, handlers, localNumber) {
|
||||
<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);
|
||||
@@ -773,6 +880,7 @@ function renderNodeCard(node, heading, handlers, localNumber) {
|
||||
<span class="channel-action-icon" aria-hidden="true">↪</span>
|
||||
<span class="channel-action-label">Оригинал</span>
|
||||
`;
|
||||
setActionTitle(originalButton, 'Оригинал');
|
||||
originalButton.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
const ok = window.confirm('Перейти к оригинальному сообщению?');
|
||||
@@ -787,6 +895,27 @@ function renderNodeCard(node, heading, handlers, localNumber) {
|
||||
});
|
||||
actions.append(originalButton);
|
||||
}
|
||||
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({
|
||||
target,
|
||||
authorLogin: author,
|
||||
timestampMs: node?.createdAtMs,
|
||||
text,
|
||||
raw: node,
|
||||
localNumber,
|
||||
msgSubType,
|
||||
}));
|
||||
});
|
||||
actions.append(detailsButton);
|
||||
if (isOwnMessage) {
|
||||
const editButton = document.createElement('button');
|
||||
editButton.type = 'button';
|
||||
|
||||
Reference in New Issue
Block a user