UI: добавить вложения Arweave в каналы

This commit is contained in:
AidarKC
2026-07-30 11:04:50 +04:00
parent c7684d6fe1
commit e9e6628b21
10 changed files with 974 additions and 29 deletions
+58 -8
View File
@@ -12,6 +12,12 @@ import {
} from '../services/channels-ux.js';
import { navigateBack } from '../router.js';
import { renderUserAvatar } from '../components/avatar-image.js';
import { openArweaveAttachmentManager } from '../components/arweave-attachment-manager.js';
import {
composeMessageWithAttachments,
createAttachmentListElement,
parseMessageAttachments,
} from '../services/attachment-format.js';
import { loadProfileSnapshot } from '../services/user-profile-params.js';
import { extractLoginFromBlockchainName, makeProfileRoute, makeShineChannelRoute, makeShineMessageRoute } from '../services/shine-routes.js';
@@ -331,6 +337,25 @@ function resolveNodeText(node) {
);
}
function renderDraftAttachments(container, attachments) {
if (!container) return;
container.innerHTML = '';
(Array.isArray(attachments) ? attachments : []).forEach((item, index) => {
const button = document.createElement('button');
button.type = 'button';
button.className = 'draft-attachment-chip';
button.textContent = `${item.name} · ${item.ar}`;
button.title = 'Нажмите, чтобы убрать вложение';
button.addEventListener('click', () => {
const ok = window.confirm('Отменить вложение?');
if (!ok) return;
attachments.splice(index, 1);
renderDraftAttachments(container, attachments);
});
container.append(button);
});
}
function openReplyModal({ onSubmit, navigate }) {
const root = document.getElementById('modal-root');
root.innerHTML = `
@@ -338,6 +363,8 @@ function openReplyModal({ onSubmit, navigate }) {
<div class="modal-card stack">
<h3 class="modal-title">Ответ</h3>
<textarea id="thread-reply-text" class="input" rows="5" maxlength="2000" placeholder="Текст ответа"></textarea>
<div class="draft-attachments" id="thread-reply-attachments"></div>
<button class="secondary-btn attachment-trigger-btn" id="thread-reply-attach" type="button" aria-label="Добавить вложение" title="Добавить вложение">▣ 📎</button>
<div class="meta-muted inline-error" id="thread-reply-error"></div>
<div class="form-actions-grid">
<button class="secondary-btn" id="thread-reply-cancel" type="button">Отмена</button>
@@ -348,14 +375,17 @@ function openReplyModal({ onSubmit, navigate }) {
`;
const textEl = root.querySelector('#thread-reply-text');
const attachmentsEl = root.querySelector('#thread-reply-attachments');
const errorEl = root.querySelector('#thread-reply-error');
const submitEl = root.querySelector('#thread-reply-submit');
const attachments = [];
let inFlight = false;
const setBusy = (busy) => {
inFlight = !!busy;
submitEl.disabled = inFlight;
if (textEl) textEl.disabled = inFlight;
root.querySelector('#thread-reply-attach')?.toggleAttribute('disabled', inFlight);
submitEl.textContent = inFlight ? 'Отправляем...' : 'Отправить';
};
@@ -368,8 +398,8 @@ function openReplyModal({ onSubmit, navigate }) {
if (inFlight) return;
const text = String(textEl?.value || '').trim();
if (!text) {
errorEl.textContent = 'Введите текст ответа.';
if (!text && attachments.length === 0) {
errorEl.textContent = 'Введите текст ответа или добавьте вложение.';
return;
}
@@ -377,7 +407,7 @@ function openReplyModal({ onSubmit, navigate }) {
errorEl.textContent = '';
try {
await onSubmit(text);
await onSubmit(composeMessageWithAttachments(text, attachments));
close();
} catch (error) {
setBusy(false);
@@ -385,6 +415,21 @@ function openReplyModal({ onSubmit, navigate }) {
}
});
root.querySelector('#thread-reply-attach')?.addEventListener('click', async () => {
try {
const item = await openArweaveAttachmentManager({
login: state.session.login,
storagePwd: state.session.storagePwdInMemory,
gateway: state.entrySettings.arweaveServer,
});
if (!item) return;
attachments.push(item);
renderDraftAttachments(attachmentsEl, attachments);
} catch (error) {
errorEl.textContent = toUserMessage(error, 'Не удалось добавить вложение.');
}
});
bindSubmitOnPlainEnter(textEl, () => root.querySelector('#thread-reply-submit')?.click());
if (textEl) textEl.focus();
@@ -504,7 +549,7 @@ function openMessageHistoryModal({ versions = [], title = 'История изм
});
}
function openEditMessageModal({ initialText = '', onSave, onDelete }) {
function openEditMessageModal({ initialText = '', allowEmptyText = false, onSave, onDelete }) {
const root = document.getElementById('modal-root');
root.innerHTML = `
<div class="modal" id="thread-edit-modal">
@@ -531,7 +576,7 @@ function openEditMessageModal({ initialText = '', onSave, onDelete }) {
root.querySelector('#thread-edit-cancel')?.addEventListener('click', close);
root.querySelector('#thread-edit-save')?.addEventListener('click', async () => {
const value = String(textEl?.value || '').trim();
if (!value) {
if (!value && !allowEmptyText) {
errorEl.textContent = 'Введите текст сообщения.';
return;
}
@@ -569,6 +614,7 @@ function renderNodeCard(node, heading, handlers, localNumber) {
const isChannelPost = Number(node?.channelInfo?.channelRoot?.blockNumber) >= 0;
const msgSubType = Number(node?.msgSubType || 0);
const repostTarget = msgSubType === 30 ? buildRepostTargetFromNode(node) : null;
const parsedText = parseMessageAttachments(text);
const headingText = String(heading || '').trim();
if (headingText) {
@@ -620,9 +666,12 @@ function renderNodeCard(node, heading, handlers, localNumber) {
const isDeletedMessage = String(text || '').trim().toLowerCase() === 'удалено';
const body = document.createElement('p');
body.className = `channel-message-body${isDeletedMessage ? ' channel-message-body--deleted' : ''}`;
body.textContent = isDeletedMessage ? 'Сообщение удалено' : text;
body.textContent = isDeletedMessage ? 'Сообщение удалено' : parsedText.text;
card.append(authorTile, body);
if (!isDeletedMessage && parsedText.attachments.length > 0) {
card.append(createAttachmentListElement(parsedText.attachments, { gateway: state.entrySettings.arweaveServer }));
}
const target = buildTargetFromNode(node);
const refKey = messageRefKey(target);
@@ -740,8 +789,9 @@ function renderNodeCard(node, heading, handlers, localNumber) {
event.stopPropagation();
animatePress(event.currentTarget);
openEditMessageModal({
initialText: String(text || '').trim() === 'удалено' ? '' : text,
onSave: async (nextText) => handlers.onEdit(target, nextText, { isChannelPost }),
initialText: String(text || '').trim() === 'удалено' ? '' : parsedText.text,
allowEmptyText: parsedText.attachments.length > 0,
onSave: async (nextText) => handlers.onEdit(target, composeMessageWithAttachments(nextText, parsedText.attachments), { isChannelPost }),
onDelete: async () => handlers.onEdit(target, '', { isChannelPost, isDelete: true }),
});
});