Исправить edit/delete сообщений, упростить вкладки каналов и улучшить автоскролл DM

This commit is contained in:
AidarKC
2026-05-19 21:00:29 +03:00
parent 7986184111
commit f3262c2d64
18 changed files with 845 additions and 104 deletions
+165 -24
View File
@@ -165,12 +165,11 @@ function firstNonEmptyText(...candidates) {
}
function latestVersionText(versions) {
if (!Array.isArray(versions)) return '';
for (let i = versions.length - 1; i >= 0; i -= 1) {
const version = versions[i];
const value = firstNonEmptyText(version?.text, version?.message, version?.body);
if (value) return value;
}
if (!Array.isArray(versions) || !versions.length) return '';
const version = versions[versions.length - 1];
if (typeof version?.text === 'string') return version.text;
if (typeof version?.message === 'string') return version.message;
if (typeof version?.body === 'string') return version.body;
return '';
}
@@ -377,6 +376,92 @@ function openAddMessageModal({ channelName, onSubmit, navigate }) {
if (textEl) textEl.focus();
}
function openMessageHistoryModal({ versions = [], title = 'История изменений' }) {
const root = document.getElementById('modal-root');
const rows = Array.isArray(versions) ? versions : [];
root.innerHTML = `
<div class="modal" id="message-history-modal">
<div class="modal-card stack">
<h3 class="modal-title">${title}</h3>
<div class="stack" id="message-history-list"></div>
<div class="form-actions-grid">
<button class="secondary-btn" id="message-history-close" type="button">Закрыть</button>
</div>
</div>
</div>
`;
const list = root.querySelector('#message-history-list');
if (list) {
rows.forEach((item, index) => {
const row = document.createElement('div');
row.className = 'card stack';
const ts = toTimestampMs(item?.createdAtMs);
const text = String(item?.text || '').trim() || 'удалено';
row.innerHTML = `
<strong>Версия ${index + 1}</strong>
<div class="meta-muted">${ts > 0 ? formatRelativeTime(ts) : '—'}</div>
<p class="channel-message-body">${text}</p>
`;
list.append(row);
});
}
root.querySelector('#message-history-close')?.addEventListener('click', () => {
root.innerHTML = '';
});
}
function openEditMessageModal({ initialText = '', onSave, onDelete }) {
const root = document.getElementById('modal-root');
root.innerHTML = `
<div class="modal" id="edit-message-modal">
<div class="modal-card stack">
<h3 class="modal-title">Редактировать сообщение</h3>
<textarea id="edit-message-text" class="input" rows="6" maxlength="2000"></textarea>
<div class="meta-muted inline-error" id="edit-message-error"></div>
<div class="form-actions-grid">
<button class="secondary-btn" id="edit-message-cancel" type="button">Отмена</button>
<button class="primary-btn" id="edit-message-save" type="button">ОК</button>
</div>
<button class="destructive-btn modal-danger-action" id="edit-message-delete" type="button">Удалить</button>
</div>
</div>
`;
const textEl = root.querySelector('#edit-message-text');
const errorEl = root.querySelector('#edit-message-error');
if (textEl) textEl.value = String(initialText || '');
const close = () => {
root.innerHTML = '';
};
root.querySelector('#edit-message-cancel')?.addEventListener('click', close);
root.querySelector('#edit-message-save')?.addEventListener('click', async () => {
const value = String(textEl?.value || '').trim();
if (!value) {
errorEl.textContent = 'Введите текст сообщения.';
return;
}
try {
await onSave(value);
close();
} catch (error) {
errorEl.textContent = toUserMessage(error, 'Не удалось изменить сообщение.');
}
});
root.querySelector('#edit-message-delete')?.addEventListener('click', async () => {
try {
await onDelete();
close();
} catch (error) {
errorEl.textContent = toUserMessage(error, 'Не удалось удалить сообщение.');
}
});
if (textEl) textEl.focus();
}
function mapApiMessageToPost(message, selector, localNumber) {
const blockNumber = toSafeInt(message?.messageRef?.blockNumber);
const blockHash = normalizeMessageHash(message?.messageRef?.blockHash);
@@ -399,12 +484,15 @@ function mapApiMessageToPost(message, selector, localNumber) {
return {
localNumber,
authorLogin: message?.authorLogin || 'автор',
body: resolvedText || '(пусто)',
body: resolvedText || (Number(message?.versionsTotal || 1) > 1 ? 'удалено' : '(пусто)'),
versionsTotal: Number(message?.versionsTotal || 1),
versions: Array.isArray(message?.versions) ? message.versions : [],
likesCount: Number(message?.likesCount || 0),
repliesCount: Number(message?.repliesCount || 0),
timestampMs: resolveMessageTimestampMs(message),
messageRef,
reactionState: messageRef ? getMessageReactionState(messageRef) : '',
isOwnMessage: String(message?.authorLogin || '').trim().toLowerCase() === String(state.session.login || '').trim().toLowerCase(),
};
}
@@ -625,7 +713,10 @@ function renderPostCard(post, {
onToggleLike,
onReply,
onShare,
onEdit,
}) {
const versionsTotal = Number(post?.versionsTotal || 1);
const card = document.createElement('article');
card.className = 'card stack channel-message-card';
@@ -655,6 +746,22 @@ function renderPostCard(post, {
timestamp.textContent = post.timestampMs ? formatRelativeTime(post.timestampMs) : '—';
title.append(loginEl, numberEl);
if (versionsTotal > 1) {
const editedMarker = document.createElement('button');
editedMarker.type = 'button';
editedMarker.className = 'message-edited-marker';
editedMarker.textContent = `изменено ${Math.max(1, versionsTotal - 1)}`;
editedMarker.title = 'Открыть историю редактирования';
editedMarker.addEventListener('click', (event) => {
event.stopPropagation();
animatePress(event.currentTarget);
openMessageHistoryModal({
title: `История #${post.localNumber}`,
versions: post.versions,
});
});
title.append(editedMarker);
}
authorBlock.append(title, timestamp);
authorTile.append(avatar, authorBlock);
authorTile.addEventListener('click', (event) => {
@@ -664,9 +771,10 @@ function renderPostCard(post, {
navigate(`user/${encodeRoutePart(cleanLogin)}`);
});
const isDeletedMessage = String(post.body || '').trim().toLowerCase() === 'удалено';
const body = document.createElement('p');
body.className = 'channel-message-body';
body.textContent = post.body;
body.className = `channel-message-body${isDeletedMessage ? ' channel-message-body--deleted' : ''}`;
body.textContent = isDeletedMessage ? 'Сообщение удалено' : post.body;
card.append(authorTile, body);
@@ -728,20 +836,6 @@ function renderPostCard(post, {
});
actions.append(likeButton, replyButton);
const openThreadButton = document.createElement('button');
openThreadButton.type = 'button';
openThreadButton.className = 'channel-action-item channel-action-thread';
openThreadButton.innerHTML = `
<span class="channel-action-icon" aria-hidden="true">#</span>
<span class="channel-action-label">Тред</span>
`;
openThreadButton.addEventListener('click', (event) => {
event.stopPropagation();
animatePress(event.currentTarget);
const route = buildThreadRoute(post.messageRef, selector);
if (route) navigate(route);
});
const shareButton = document.createElement('button');
shareButton.type = 'button';
shareButton.className = 'channel-action-item channel-action-share';
@@ -756,7 +850,27 @@ function renderPostCard(post, {
await onShare(route);
});
actions.append(openThreadButton, shareButton);
actions.append(shareButton);
if (post.isOwnMessage) {
const editButton = document.createElement('button');
editButton.type = 'button';
editButton.className = 'channel-action-item';
editButton.setAttribute('aria-label', 'Редактировать');
editButton.title = 'Редактировать';
editButton.innerHTML = `
<span class="channel-action-icon" aria-hidden="true">✏️</span>
`;
editButton.addEventListener('click', (event) => {
event.stopPropagation();
animatePress(event.currentTarget);
openEditMessageModal({
initialText: String(post.body || '').trim() === 'удалено' ? '' : post.body,
onSave: async (nextText) => onEdit(post.messageRef, nextText, { isDelete: false }),
onDelete: async () => onEdit(post.messageRef, '', { isDelete: true }),
});
});
actions.append(editButton);
}
card.append(actions);
card.addEventListener('click', () => {
const route = buildThreadRoute(post.messageRef, selector);
@@ -791,6 +905,7 @@ function renderBody(screen, navigate, routeKey, channelData, handlers) {
onToggleLike: handlers.onToggleLike,
onReply: handlers.onReply,
onShare: handlers.onShare,
onEdit: handlers.onEdit,
});
const key = messageRefKey(post.messageRef);
if (key) {
@@ -976,6 +1091,24 @@ export function render({ navigate, route }) {
rerender();
};
const onEditPost = async (messageRef, text) => {
const { login, storagePwd } = requireSigningSession();
if (!activeSelector?.ownerBlockchainName || activeSelector.channelRootBlockNumber == null) {
throw new Error('Идентификатор канала не готов.');
}
await authService.addBlockEditMessage({
login,
storagePwd,
message: messageRef,
text,
isChannelPost: true,
channel: activeSelector,
});
softHaptic(12);
showToast('Сообщение обновлено');
rerender();
};
screen.append(header);
screen.append(statusBox);
@@ -1023,6 +1156,14 @@ export function render({ navigate, route }) {
}
},
onShare: onShare,
onEdit: async (messageRef, text) => {
try {
await onEditPost(messageRef, text);
showStatus('');
} catch (error) {
throw new Error(toUserMessage(error, 'Не удалось изменить сообщение.'));
}
},
onSubscribeChannel: async (event) => {
animatePress(event?.currentTarget);
try {