SHA256
Исправить edit/delete сообщений, упростить вкладки каналов и улучшить автоскролл DM
This commit is contained in:
@@ -246,12 +246,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 '';
|
||||
}
|
||||
|
||||
@@ -333,15 +332,104 @@ function openReplyModal({ 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="thread-history-modal">
|
||||
<div class="modal-card stack">
|
||||
<h3 class="modal-title">${title}</h3>
|
||||
<div class="stack" id="thread-history-list"></div>
|
||||
<div class="form-actions-grid">
|
||||
<button class="secondary-btn" id="thread-history-close" type="button">Закрыть</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const list = root.querySelector('#thread-history-list');
|
||||
if (list) {
|
||||
rows.forEach((item, index) => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'card stack';
|
||||
const ts = Number(item?.createdAtMs || 0);
|
||||
const text = String(item?.text || '').trim() || 'удалено';
|
||||
row.innerHTML = `
|
||||
<strong>Версия ${index + 1}</strong>
|
||||
<div class="meta-muted">${ts > 0 ? new Date(ts).toLocaleString('ru-RU') : '—'}</div>
|
||||
<p class="channel-message-body">${text}</p>
|
||||
`;
|
||||
list.append(row);
|
||||
});
|
||||
}
|
||||
|
||||
root.querySelector('#thread-history-close')?.addEventListener('click', () => {
|
||||
root.innerHTML = '';
|
||||
});
|
||||
}
|
||||
|
||||
function openEditMessageModal({ initialText = '', onSave, onDelete }) {
|
||||
const root = document.getElementById('modal-root');
|
||||
root.innerHTML = `
|
||||
<div class="modal" id="thread-edit-modal">
|
||||
<div class="modal-card stack">
|
||||
<h3 class="modal-title">Редактировать сообщение</h3>
|
||||
<textarea id="thread-edit-text" class="input" rows="6" maxlength="2000"></textarea>
|
||||
<div class="meta-muted inline-error" id="thread-edit-error"></div>
|
||||
<div class="form-actions-grid">
|
||||
<button class="secondary-btn" id="thread-edit-cancel" type="button">Отмена</button>
|
||||
<button class="primary-btn" id="thread-edit-save" type="button">ОК</button>
|
||||
</div>
|
||||
<button class="destructive-btn modal-danger-action" id="thread-edit-delete" type="button">Удалить</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
const textEl = root.querySelector('#thread-edit-text');
|
||||
const errorEl = root.querySelector('#thread-edit-error');
|
||||
if (textEl) textEl.value = String(initialText || '');
|
||||
|
||||
const close = () => {
|
||||
root.innerHTML = '';
|
||||
};
|
||||
|
||||
root.querySelector('#thread-edit-cancel')?.addEventListener('click', close);
|
||||
root.querySelector('#thread-edit-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('#thread-edit-delete')?.addEventListener('click', async () => {
|
||||
try {
|
||||
await onDelete();
|
||||
close();
|
||||
} catch (error) {
|
||||
errorEl.textContent = toUserMessage(error, 'Не удалось удалить сообщение.');
|
||||
}
|
||||
});
|
||||
if (textEl) textEl.focus();
|
||||
}
|
||||
|
||||
function renderNodeCard(node, heading, handlers, localNumber) {
|
||||
const card = document.createElement('article');
|
||||
card.className = 'card stack thread-node-card channel-message-card';
|
||||
card.classList.add('is-counters-visible');
|
||||
|
||||
const author = node?.authorLogin || 'автор';
|
||||
const text = resolveNodeText(node) || '(пусто)';
|
||||
const versions = Array.isArray(node?.versions) ? node.versions : [];
|
||||
const versionsTotal = Number(node?.versionsTotal || versions.length || 1);
|
||||
const text = resolveNodeText(node) || (versionsTotal > 1 ? 'удалено' : '(пусто)');
|
||||
const likes = Number(node?.likesCount || 0);
|
||||
const replies = Number(node?.repliesCount || 0);
|
||||
const isOwnMessage = String(node?.authorLogin || '').trim().toLowerCase() === String(state.session.login || '').trim().toLowerCase();
|
||||
const isChannelPost = Number(node?.channelInfo?.channelRoot?.blockNumber) >= 0;
|
||||
|
||||
const headingText = String(heading || '').trim();
|
||||
if (headingText) {
|
||||
@@ -369,16 +457,33 @@ function renderNodeCard(node, heading, handlers, localNumber) {
|
||||
const numberEl = document.createElement('span');
|
||||
numberEl.className = 'author-line-num';
|
||||
numberEl.textContent = `· #${localNumber}`;
|
||||
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: `История #${localNumber}`,
|
||||
versions,
|
||||
});
|
||||
});
|
||||
title.append(editedMarker);
|
||||
}
|
||||
const timestamp = document.createElement('div');
|
||||
timestamp.className = 'channel-message-time';
|
||||
timestamp.textContent = node?.createdAtMs ? new Date(node.createdAtMs).toLocaleString() : '—';
|
||||
title.append(loginEl, numberEl);
|
||||
authorBlock.append(title, timestamp);
|
||||
authorTile.append(avatar, authorBlock);
|
||||
|
||||
const isDeletedMessage = String(text || '').trim().toLowerCase() === 'удалено';
|
||||
const body = document.createElement('p');
|
||||
body.className = 'channel-message-body';
|
||||
body.textContent = text;
|
||||
body.className = `channel-message-body${isDeletedMessage ? ' channel-message-body--deleted' : ''}`;
|
||||
body.textContent = isDeletedMessage ? 'Сообщение удалено' : text;
|
||||
|
||||
card.append(authorTile, body);
|
||||
|
||||
@@ -460,20 +565,27 @@ function renderNodeCard(node, heading, handlers, localNumber) {
|
||||
await handlers.onShare(target);
|
||||
});
|
||||
|
||||
const openThreadButton = document.createElement('button');
|
||||
openThreadButton.type = 'button';
|
||||
openThreadButton.className = 'channel-action-item thread-open-btn';
|
||||
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);
|
||||
handlers.onOpenThread(target);
|
||||
});
|
||||
|
||||
actions.append(likeButton, replyButton, openThreadButton, shareButton);
|
||||
actions.append(likeButton, replyButton, shareButton);
|
||||
if (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(text || '').trim() === 'удалено' ? '' : text,
|
||||
onSave: async (nextText) => handlers.onEdit(target, nextText, { isChannelPost }),
|
||||
onDelete: async () => handlers.onEdit(target, '', { isChannelPost, isDelete: true }),
|
||||
});
|
||||
});
|
||||
actions.append(editButton);
|
||||
}
|
||||
card.append(actions);
|
||||
authorTile.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
@@ -670,6 +782,21 @@ export function render({ navigate, route }) {
|
||||
: 'Не удалось поставить лайк.';
|
||||
showStatus(toUserMessage(error, fallback));
|
||||
},
|
||||
onEdit: async (target, textValue, meta = {}) => {
|
||||
const { login, storagePwd } = requireSigningSession();
|
||||
await authService.addBlockEditMessage({
|
||||
login,
|
||||
storagePwd,
|
||||
message: target,
|
||||
text: textValue,
|
||||
isChannelPost: meta?.isChannelPost === true,
|
||||
channel: selector?.channel || null,
|
||||
});
|
||||
softHaptic(12);
|
||||
showToast('Сообщение обновлено');
|
||||
showStatus('');
|
||||
rerender();
|
||||
},
|
||||
};
|
||||
|
||||
screen.append(header, statusBox);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -17,7 +17,7 @@ const CREATE_CHANNEL_FLASH_KEY = 'shine-channels-create-success';
|
||||
const MENU_OVERLAY_ID = 'channels-context-menu-overlay';
|
||||
const CHANNEL_TYPE_STORIES = 0;
|
||||
const CHANNEL_TYPE_PERSONAL = 100;
|
||||
const TAB_ORDER = ['dialogs', 'feed', 'my'];
|
||||
const TAB_ORDER = ['feed', 'my'];
|
||||
|
||||
function isChannelsDemoMode() {
|
||||
try {
|
||||
@@ -583,11 +583,9 @@ function mapMockGroups() {
|
||||
const mapRow = (channel) => ({
|
||||
...channel,
|
||||
route: `channel/${encodeRoutePart(String(channel.ownerName || 'channel'))}/${encodeRoutePart(String(channel.channelName || channel.title || channel.id))}`,
|
||||
tabCategory: channel.kind === 'own'
|
||||
tabCategory: channel.kind === 'own' || channel.kind === 'own-personal'
|
||||
? 'my'
|
||||
: channel.kind === 'own-personal'
|
||||
? 'dialogs'
|
||||
: 'feed',
|
||||
: 'feed',
|
||||
messagePreview: channel.lastMessage || 'Ждем ваших начинаний',
|
||||
isSubscribed: channel.kind !== 'own' && channel.kind !== 'own-personal',
|
||||
isOwnChannel: channel.kind === 'own' || channel.kind === 'own-personal',
|
||||
@@ -625,9 +623,7 @@ function mapApiChannelRow(summary, bucketKey, idx, index, notificationsState) {
|
||||
const channelTypeCode = Number(summary?.channel?.channelTypeCode ?? 1);
|
||||
const channelTypeVersion = Number(summary?.channel?.channelTypeVersion ?? 1);
|
||||
const isOwn = bucketKey === 'own';
|
||||
const tabCategory = isOwn
|
||||
? (channelTypeCode === CHANNEL_TYPE_PERSONAL ? 'dialogs' : 'my')
|
||||
: 'feed';
|
||||
const tabCategory = isOwn ? 'my' : 'feed';
|
||||
|
||||
const title = isOwn ? channelName : `${ownerLogin}/${channelName}`;
|
||||
|
||||
@@ -691,8 +687,6 @@ function renderEmptyState(activeTab, navigate) {
|
||||
text.className = 'meta-muted';
|
||||
if (activeTab === 'feed') {
|
||||
text.textContent = 'Нет подписок и найденных каналов.';
|
||||
} else if (activeTab === 'dialogs') {
|
||||
text.textContent = 'Чаты пока не работают.';
|
||||
} else if (activeTab === 'my') {
|
||||
text.textContent = 'У вас пока нет каналов.';
|
||||
} else {
|
||||
@@ -953,7 +947,7 @@ function renderChannelMain(channel, activeTab) {
|
||||
title.className = 'channel-row-title';
|
||||
title.textContent = activeTab === 'my' ? channel.channelName : channel.title;
|
||||
|
||||
if ((activeTab === 'my' || activeTab === 'dialogs') && channel.channelDescription) {
|
||||
if (activeTab === 'my' && channel.channelDescription) {
|
||||
const desc = document.createElement('p');
|
||||
desc.className = 'channel-row-description';
|
||||
desc.textContent = channel.channelDescription;
|
||||
@@ -1062,13 +1056,6 @@ function updateBottomCta({ button, listState, navigate, isTabEmpty = false }) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tab === 'dialogs') {
|
||||
button.textContent = 'Новый персональный публичный чат';
|
||||
button.className = baseClass;
|
||||
button.onclick = () => navigate('add-personal-public-chat-view');
|
||||
return;
|
||||
}
|
||||
|
||||
if (tab === 'my') {
|
||||
button.textContent = 'Создать канал';
|
||||
button.className = baseClass;
|
||||
@@ -1140,18 +1127,13 @@ export function render({ navigate, route }) {
|
||||
tabsEl.className = 'channels-tabs';
|
||||
const tabLabels = {
|
||||
feed: 'Каналы',
|
||||
dialogs: 'Чаты',
|
||||
my: 'Мои',
|
||||
my: 'Мои каналы',
|
||||
};
|
||||
TAB_ORDER.forEach((tabKey) => {
|
||||
const tabBtn = document.createElement('button');
|
||||
tabBtn.type = 'button';
|
||||
tabBtn.className = `channels-tab-btn${listState.activeTab === tabKey ? ' is-active' : ''}`;
|
||||
tabBtn.textContent = tabLabels[tabKey] || tabKey;
|
||||
if (tabKey === 'dialogs') {
|
||||
tabBtn.classList.add('is-disabled');
|
||||
tabBtn.title = 'Чаты пока не работают';
|
||||
}
|
||||
tabBtn.addEventListener('click', () => {
|
||||
if (listState.activeTab === tabKey) return;
|
||||
listState.activeTab = tabKey;
|
||||
|
||||
@@ -172,8 +172,11 @@ function scrollToLatestMessage(list) {
|
||||
};
|
||||
apply();
|
||||
window.requestAnimationFrame(apply);
|
||||
window.requestAnimationFrame(() => window.requestAnimationFrame(apply));
|
||||
window.setTimeout(apply, 0);
|
||||
window.setTimeout(apply, 60);
|
||||
window.setTimeout(apply, 120);
|
||||
window.setTimeout(apply, 260);
|
||||
}
|
||||
|
||||
function renderLog(list, chatId, { onOpenActions } = {}) {
|
||||
@@ -416,6 +419,9 @@ export function render({ navigate, route }) {
|
||||
const input = form.elements.message;
|
||||
autoResizeComposer(input);
|
||||
input?.addEventListener('input', () => autoResizeComposer(input));
|
||||
input?.addEventListener('focus', () => {
|
||||
scrollToLatestMessage(log);
|
||||
});
|
||||
input?.addEventListener('keydown', async (event) => {
|
||||
if (event.key !== 'Enter') return;
|
||||
if (event.ctrlKey) {
|
||||
@@ -480,6 +486,7 @@ export function render({ navigate, route }) {
|
||||
}),
|
||||
});
|
||||
window.requestAnimationFrame(() => scrollToLatestMessage(log));
|
||||
window.setTimeout(() => scrollToLatestMessage(log), 180);
|
||||
void sendReadReceiptsForVisible(chatId);
|
||||
return screen;
|
||||
}
|
||||
|
||||
@@ -37,7 +37,9 @@ const MSG_TYPE_CONNECTION = 3;
|
||||
|
||||
const MSG_SUBTYPE_TECH_CREATE_CHANNEL = 1;
|
||||
const MSG_SUBTYPE_TEXT_POST = 10;
|
||||
const MSG_SUBTYPE_TEXT_EDIT_POST = 11;
|
||||
const MSG_SUBTYPE_TEXT_REPLY = 20;
|
||||
const MSG_SUBTYPE_TEXT_EDIT_REPLY = 21;
|
||||
const MSG_SUBTYPE_REACTION_LIKE = 1;
|
||||
const MSG_SUBTYPE_REACTION_UNLIKE = 2;
|
||||
const MSG_SUBTYPE_CONNECTION_FOLLOW = 30;
|
||||
@@ -366,6 +368,56 @@ function makeTextReplyBodyBytes({ toBlockchainName, toBlockNumber, toBlockHashHe
|
||||
);
|
||||
}
|
||||
|
||||
function makeTextEditPostBodyBytes({
|
||||
lineCode,
|
||||
prevLineNumber,
|
||||
prevLineHashHex,
|
||||
thisLineNumber,
|
||||
toBlockNumber,
|
||||
toBlockHashHex,
|
||||
text,
|
||||
}) {
|
||||
const message = String(text || '').trim();
|
||||
const targetBlockNumber = Number(toBlockNumber);
|
||||
if (!Number.isFinite(targetBlockNumber) || targetBlockNumber < 0) {
|
||||
throw new Error('Invalid target block number for edit post');
|
||||
}
|
||||
const textBytes = utf8Bytes(message);
|
||||
if (textBytes.length > 65535) {
|
||||
throw new Error('Message text must be 0..65535 UTF-8 bytes');
|
||||
}
|
||||
|
||||
return concatBytes(
|
||||
int32Bytes(lineCode),
|
||||
int32Bytes(prevLineNumber),
|
||||
hexToBytes(normalizeHex32(prevLineHashHex)),
|
||||
int32Bytes(thisLineNumber),
|
||||
int32Bytes(targetBlockNumber),
|
||||
hexToBytes(normalizeHex32(toBlockHashHex)),
|
||||
int16Bytes(textBytes.length),
|
||||
textBytes,
|
||||
);
|
||||
}
|
||||
|
||||
function makeTextEditReplyBodyBytes({ toBlockNumber, toBlockHashHex, text }) {
|
||||
const message = String(text || '').trim();
|
||||
const targetBlockNumber = Number(toBlockNumber);
|
||||
if (!Number.isFinite(targetBlockNumber) || targetBlockNumber < 0) {
|
||||
throw new Error('Invalid target block number for edit reply');
|
||||
}
|
||||
const textBytes = utf8Bytes(message);
|
||||
if (textBytes.length > 65535) {
|
||||
throw new Error('Message text must be 0..65535 UTF-8 bytes');
|
||||
}
|
||||
|
||||
return concatBytes(
|
||||
int32Bytes(targetBlockNumber),
|
||||
hexToBytes(normalizeHex32(toBlockHashHex)),
|
||||
int16Bytes(textBytes.length),
|
||||
textBytes,
|
||||
);
|
||||
}
|
||||
|
||||
function makeConnectionBodyBytes({
|
||||
lineCode = 0,
|
||||
prevLineNumber = -1,
|
||||
@@ -955,6 +1007,98 @@ export class AuthService {
|
||||
});
|
||||
}
|
||||
|
||||
async addBlockEditMessage({
|
||||
login,
|
||||
message,
|
||||
text,
|
||||
storagePwd,
|
||||
isChannelPost = false,
|
||||
channel = null,
|
||||
}) {
|
||||
const cleanLogin = String(login || '').trim();
|
||||
const cleanText = String(text || '').trim();
|
||||
const target = normalizeMessageRefTarget(message, 'edit');
|
||||
const lockKey = `edit:${cleanLogin}:${target.blockchainName}:${target.blockNumber}:${target.blockHash}:${cleanText}:${isChannelPost ? 'post' : 'reply'}`;
|
||||
|
||||
return this.runWriteLocked(lockKey, async () => {
|
||||
if (isChannelPost) {
|
||||
const selector = channel || {};
|
||||
const ownerBlockchainName = String(selector?.ownerBlockchainName || target.blockchainName || '').trim();
|
||||
const lineCode = Number(selector?.channelRootBlockNumber);
|
||||
if (!ownerBlockchainName || !Number.isFinite(lineCode) || lineCode < 0) {
|
||||
throw new Error('Invalid channel selector for edit');
|
||||
}
|
||||
|
||||
let rootHashHex = normalizeHex32(selector?.channelRootBlockHash, ZERO64);
|
||||
if (rootHashHex === ZERO64) {
|
||||
const ownChannels = await this.listOwnChannelsForBlockchain(cleanLogin, ownerBlockchainName);
|
||||
const rootChannel = ownChannels.find((item) => item.rootBlockNumber === lineCode);
|
||||
if (rootChannel) rootHashHex = normalizeHex32(rootChannel.rootBlockHash, ZERO64);
|
||||
}
|
||||
|
||||
let prevLineNumber = lineCode;
|
||||
let prevLineHashHex = rootHashHex;
|
||||
let thisLineNumber = 1;
|
||||
try {
|
||||
const latestPayload = await this.getChannelMessages({
|
||||
ownerBlockchainName,
|
||||
channelRootBlockNumber: lineCode,
|
||||
channelRootBlockHash: rootHashHex,
|
||||
}, 1, 'desc', cleanLogin);
|
||||
const latestMessage = Array.isArray(latestPayload?.messages) ? latestPayload.messages[0] : null;
|
||||
const latestVersions = Array.isArray(latestMessage?.versions) ? latestMessage.versions : [];
|
||||
const latestVersion = latestVersions[latestVersions.length - 1] || null;
|
||||
const latestBlockNumber = Number(latestVersion?.blockNumber ?? latestMessage?.messageRef?.blockNumber);
|
||||
const latestBlockHash = normalizeHex32(latestVersion?.blockHash ?? latestMessage?.messageRef?.blockHash, '');
|
||||
const latestVersionsTotal = Number(latestMessage?.versionsTotal);
|
||||
if (Number.isFinite(latestBlockNumber) && latestBlockNumber >= 0 && latestBlockHash) {
|
||||
prevLineNumber = latestBlockNumber;
|
||||
prevLineHashHex = latestBlockHash;
|
||||
thisLineNumber = Number.isFinite(latestVersionsTotal) && latestVersionsTotal > 0
|
||||
? Math.max(1, latestVersionsTotal)
|
||||
: 1;
|
||||
}
|
||||
} catch {
|
||||
// fallback to root anchor
|
||||
}
|
||||
|
||||
const bodyBytes = makeTextEditPostBodyBytes({
|
||||
lineCode,
|
||||
prevLineNumber,
|
||||
prevLineHashHex,
|
||||
thisLineNumber,
|
||||
toBlockNumber: target.blockNumber,
|
||||
toBlockHashHex: target.blockHash,
|
||||
text: cleanText,
|
||||
});
|
||||
|
||||
return this.addBlockSigned({
|
||||
login: cleanLogin,
|
||||
storagePwd,
|
||||
msgType: MSG_TYPE_TEXT,
|
||||
msgSubType: MSG_SUBTYPE_TEXT_EDIT_POST,
|
||||
msgVersion: 1,
|
||||
bodyBytes,
|
||||
});
|
||||
}
|
||||
|
||||
const bodyBytes = makeTextEditReplyBodyBytes({
|
||||
toBlockNumber: target.blockNumber,
|
||||
toBlockHashHex: target.blockHash,
|
||||
text: cleanText,
|
||||
});
|
||||
|
||||
return this.addBlockSigned({
|
||||
login: cleanLogin,
|
||||
storagePwd,
|
||||
msgType: MSG_TYPE_TEXT,
|
||||
msgSubType: MSG_SUBTYPE_TEXT_EDIT_REPLY,
|
||||
msgVersion: 1,
|
||||
bodyBytes,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async addBlockFollowUser({ login, targetLogin, storagePwd, unfollow = false }) {
|
||||
const cleanTargetLogin = String(targetLogin || '').trim().replace(/^@+/, '');
|
||||
if (!cleanTargetLogin) throw new Error('Target login is required');
|
||||
|
||||
@@ -1204,6 +1204,10 @@ textarea.input {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.modal-danger-action {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.small-btn {
|
||||
padding: 6px 10px;
|
||||
font-size: 13px;
|
||||
@@ -2238,6 +2242,14 @@ textarea.input {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.channel-message-body--deleted {
|
||||
color: #ff9e9e;
|
||||
border: 1px solid rgba(255, 126, 126, 0.5);
|
||||
background: rgba(120, 18, 18, 0.28);
|
||||
border-radius: 10px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.channel-message-time {
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.48);
|
||||
@@ -2319,6 +2331,25 @@ textarea.input {
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.message-edited-marker {
|
||||
appearance: none;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: rgba(255, 220, 100, 0.85);
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
padding: 0;
|
||||
margin-left: 6px;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
.message-edited-marker:hover,
|
||||
.message-edited-marker:focus-visible {
|
||||
color: rgba(255, 232, 150, 0.95);
|
||||
}
|
||||
|
||||
.channel-action-counter {
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
|
||||
Reference in New Issue
Block a user