SHA256
Добавить рейтинг и типы материалов в каналах
This commit is contained in:
@@ -37,7 +37,12 @@ import { buildArweaveDataUrl } from '../services/arweave-file-service.js';
|
||||
export const pageMeta = { id: 'channel-view', title: 'Канал' };
|
||||
const CHANNEL_TYPE_STORIES = 0;
|
||||
const CHANNEL_TYPE_PERSONAL = 100;
|
||||
const MSG_SUBTYPE_TEXT_RATING = 30;
|
||||
const MSG_SUBTYPE_TEXT_REPOST = 50;
|
||||
const MSG_SUBTYPE_TEXT_ENTRYPOINT = 100;
|
||||
const MSG_SUBTYPE_TEXT_EXERCISE = 110;
|
||||
const MSG_SUBTYPE_TEXT_SERVICE = 120;
|
||||
const MSG_SUBTYPE_TEXT_COURSE = 130;
|
||||
|
||||
const pendingReactionActions = new Set();
|
||||
const pendingScrollByRoute = new Map();
|
||||
@@ -288,6 +293,21 @@ function resolveMessageTimestampMs(message) {
|
||||
);
|
||||
}
|
||||
|
||||
function getChannelMessageTypeMeta(msgSubType) {
|
||||
switch (Number(msgSubType || 0)) {
|
||||
case MSG_SUBTYPE_TEXT_EXERCISE:
|
||||
return { label: 'Упражнение' };
|
||||
case MSG_SUBTYPE_TEXT_SERVICE:
|
||||
return { label: 'Процедура' };
|
||||
case MSG_SUBTYPE_TEXT_COURSE:
|
||||
return { label: 'Курс' };
|
||||
case MSG_SUBTYPE_TEXT_ENTRYPOINT:
|
||||
return { label: 'Оглавление' };
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function createChannelAvatarElement(channel, size = 72) {
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'channel-profile-avatar';
|
||||
@@ -560,13 +580,22 @@ function renderDraftAttachments(container, attachments) {
|
||||
});
|
||||
}
|
||||
|
||||
function openReplyModal({ onSubmit, navigate }) {
|
||||
function openReplyModal({ onSubmit, navigate, mode = 'reply' }) {
|
||||
const isRating = mode === 'rating';
|
||||
const title = isRating ? 'Оценка' : 'Ответ';
|
||||
const placeholder = isRating ? 'Текст оценки' : 'Текст ответа';
|
||||
const emptyError = isRating
|
||||
? 'Введите текст оценки или добавьте вложение.'
|
||||
: 'Введите текст ответа или добавьте вложение.';
|
||||
const submitError = isRating
|
||||
? 'Не удалось отправить оценку.'
|
||||
: 'Не удалось отправить ответ.';
|
||||
const root = document.getElementById('modal-root');
|
||||
root.innerHTML = `
|
||||
<div class="modal" id="reply-modal">
|
||||
<div class="modal-card stack">
|
||||
<h3 class="modal-title">Ответ</h3>
|
||||
<textarea id="reply-text" class="input" rows="5" maxlength="2000" placeholder="Текст ответа"></textarea>
|
||||
<h3 class="modal-title">${title}</h3>
|
||||
<textarea id="reply-text" class="input" rows="5" maxlength="2000" placeholder="${placeholder}"></textarea>
|
||||
<div class="draft-attachments" id="reply-attachments"></div>
|
||||
<button class="secondary-btn attachment-trigger-btn" id="reply-attach" type="button" aria-label="Добавить вложение" title="Добавить вложение">▣ 📎</button>
|
||||
<div class="meta-muted inline-error" id="reply-error"></div>
|
||||
@@ -603,7 +632,7 @@ function openReplyModal({ onSubmit, navigate }) {
|
||||
|
||||
const text = String(textEl?.value || '').trim();
|
||||
if (!text && attachments.length === 0) {
|
||||
errorEl.textContent = 'Введите текст ответа или добавьте вложение.';
|
||||
errorEl.textContent = emptyError;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -616,7 +645,7 @@ function openReplyModal({ onSubmit, navigate }) {
|
||||
close();
|
||||
} catch (error) {
|
||||
setBusy(false);
|
||||
errorEl.textContent = toUserMessage(error, 'Не удалось отправить ответ.');
|
||||
errorEl.textContent = toUserMessage(error, submitError);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -728,7 +757,16 @@ function openAddMessageModal({ channelName, onSubmit, navigate }) {
|
||||
<p class="meta-muted">${channelName}</p>
|
||||
<textarea id="channel-message-text" class="input" rows="6" maxlength="2000" placeholder="Текст сообщения"></textarea>
|
||||
<div class="draft-attachments" id="channel-message-attachments"></div>
|
||||
<button class="secondary-btn attachment-trigger-btn" id="channel-message-attach" type="button" aria-label="Добавить вложение" title="Добавить вложение">▣ 📎</button>
|
||||
<div class="channel-message-tools">
|
||||
<select id="channel-message-type" class="input channel-message-type-select">
|
||||
<option value="${10}">Пост</option>
|
||||
<option value="${MSG_SUBTYPE_TEXT_EXERCISE}">Упражнение</option>
|
||||
<option value="${MSG_SUBTYPE_TEXT_SERVICE}">Процедура</option>
|
||||
<option value="${MSG_SUBTYPE_TEXT_COURSE}">Курс</option>
|
||||
<option value="${MSG_SUBTYPE_TEXT_ENTRYPOINT}">Оглавление канала</option>
|
||||
</select>
|
||||
<button class="secondary-btn attachment-trigger-btn" id="channel-message-attach" type="button" aria-label="Добавить вложение" title="Добавить вложение">▣ 📎</button>
|
||||
</div>
|
||||
<div class="meta-muted inline-error" id="channel-message-error"></div>
|
||||
<div class="form-actions-grid">
|
||||
<button class="secondary-btn" id="channel-message-cancel" type="button">Отмена</button>
|
||||
@@ -739,6 +777,7 @@ function openAddMessageModal({ channelName, onSubmit, navigate }) {
|
||||
`;
|
||||
|
||||
const textEl = root.querySelector('#channel-message-text');
|
||||
const typeEl = root.querySelector('#channel-message-type');
|
||||
const attachmentsEl = root.querySelector('#channel-message-attachments');
|
||||
const errorEl = root.querySelector('#channel-message-error');
|
||||
const submitEl = root.querySelector('#channel-message-submit');
|
||||
@@ -749,6 +788,7 @@ function openAddMessageModal({ channelName, onSubmit, navigate }) {
|
||||
inFlight = !!busy;
|
||||
submitEl.disabled = inFlight;
|
||||
if (textEl) textEl.disabled = inFlight;
|
||||
if (typeEl) typeEl.disabled = inFlight;
|
||||
root.querySelector('#channel-message-attach')?.toggleAttribute('disabled', inFlight);
|
||||
submitEl.textContent = inFlight ? 'Отправляем...' : 'Отправить';
|
||||
};
|
||||
@@ -762,6 +802,7 @@ function openAddMessageModal({ channelName, onSubmit, navigate }) {
|
||||
if (inFlight) return;
|
||||
|
||||
const body = String(textEl?.value || '').trim();
|
||||
const msgSubType = Number(typeEl?.value || 10);
|
||||
if (!body && attachments.length === 0) {
|
||||
errorEl.textContent = 'Введите текст сообщения или добавьте вложение.';
|
||||
return;
|
||||
@@ -771,7 +812,10 @@ function openAddMessageModal({ channelName, onSubmit, navigate }) {
|
||||
errorEl.textContent = '';
|
||||
|
||||
try {
|
||||
await onSubmit(composeMessageWithAttachments(body, attachments));
|
||||
await onSubmit({
|
||||
text: composeMessageWithAttachments(body, attachments),
|
||||
msgSubType,
|
||||
});
|
||||
attachments.forEach((attachment) => markArweaveAttachmentPlaced(state.session.login, attachment));
|
||||
close();
|
||||
} catch (error) {
|
||||
@@ -919,10 +963,12 @@ function mapApiMessageToPost(message, selector, localNumber) {
|
||||
versions: Array.isArray(message?.versions) ? message.versions : [],
|
||||
likesCount: Number(message?.likesCount || 0),
|
||||
repliesCount: Number(message?.repliesCount || 0),
|
||||
ratingsCount: Number(message?.ratingsCount || 0),
|
||||
timestampMs: resolveMessageTimestampMs(message),
|
||||
messageRef,
|
||||
rawMessage: message,
|
||||
msgSubType: Number(message?.msgSubType || 0),
|
||||
isRating: Number(message?.msgSubType || 0) === MSG_SUBTYPE_TEXT_RATING,
|
||||
targetRef: message?.targetBlockchainName && Number.isFinite(Number(message?.targetBlockNumber))
|
||||
? {
|
||||
blockchainName: String(message.targetBlockchainName).trim(),
|
||||
@@ -1228,6 +1274,7 @@ function renderPostCard(post, {
|
||||
selector,
|
||||
onToggleLike,
|
||||
onReply,
|
||||
onRating,
|
||||
onRepost,
|
||||
onShare,
|
||||
onEdit,
|
||||
@@ -1236,6 +1283,7 @@ function renderPostCard(post, {
|
||||
|
||||
const card = document.createElement('article');
|
||||
card.className = 'card stack channel-message-card';
|
||||
if (post.isRating) card.classList.add('is-rating');
|
||||
|
||||
const authorTile = document.createElement('button');
|
||||
authorTile.type = 'button';
|
||||
@@ -1248,6 +1296,8 @@ function renderPostCard(post, {
|
||||
|
||||
const title = document.createElement('div');
|
||||
title.className = 'channel-message-title author-line';
|
||||
const titleMain = document.createElement('div');
|
||||
titleMain.className = 'author-line-main';
|
||||
const loginEl = document.createElement('span');
|
||||
loginEl.className = 'author-line-login';
|
||||
loginEl.textContent = post.authorLogin;
|
||||
@@ -1255,12 +1305,19 @@ function renderPostCard(post, {
|
||||
const numberEl = document.createElement('span');
|
||||
numberEl.className = 'author-line-num';
|
||||
numberEl.textContent = `· #${post.localNumber}`;
|
||||
titleMain.append(loginEl, numberEl);
|
||||
title.append(titleMain);
|
||||
const typeMeta = getChannelMessageTypeMeta(post.msgSubType);
|
||||
if (typeMeta) {
|
||||
const typeChip = document.createElement('span');
|
||||
typeChip.className = 'channel-message-type-chip';
|
||||
typeChip.textContent = typeMeta.label;
|
||||
title.append(typeChip);
|
||||
}
|
||||
|
||||
const timestamp = document.createElement('div');
|
||||
timestamp.className = 'channel-message-time';
|
||||
timestamp.textContent = post.timestampMs ? formatRelativeTime(post.timestampMs) : '—';
|
||||
|
||||
title.append(loginEl, numberEl);
|
||||
if (versionsTotal > 1) {
|
||||
const editedMarker = document.createElement('button');
|
||||
editedMarker.type = 'button';
|
||||
@@ -1313,6 +1370,12 @@ function renderPostCard(post, {
|
||||
messageTimestampMs: post.timestampMs,
|
||||
}));
|
||||
}
|
||||
if (post.isRating) {
|
||||
const ratingBadge = document.createElement('span');
|
||||
ratingBadge.className = 'channel-message-kind-badge channel-message-kind-badge--rating';
|
||||
ratingBadge.textContent = 'Оценка';
|
||||
card.append(ratingBadge);
|
||||
}
|
||||
const body = document.createElement('p');
|
||||
body.className = 'channel-message-body';
|
||||
body.textContent = parsedBody.text;
|
||||
@@ -1376,9 +1439,27 @@ function renderPostCard(post, {
|
||||
onSubmit: async (text) => onReply(post.messageRef, text),
|
||||
});
|
||||
});
|
||||
const ratingButton = document.createElement('button');
|
||||
ratingButton.type = 'button';
|
||||
ratingButton.className = 'channel-action-item channel-action-rating';
|
||||
ratingButton.innerHTML = `
|
||||
<span class="channel-action-icon" aria-hidden="true">★</span>
|
||||
<span class="channel-action-label">Оценка</span>
|
||||
<span class="channel-action-counter">${post.ratingsCount || 0}</span>
|
||||
`;
|
||||
setActionTitle(ratingButton, 'Оценка');
|
||||
ratingButton.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
animatePress(event.currentTarget);
|
||||
openReplyModal({
|
||||
navigate,
|
||||
mode: 'rating',
|
||||
onSubmit: async (text) => onRating(post.messageRef, text),
|
||||
});
|
||||
});
|
||||
// Репосты временно отключены до будущей реализации.
|
||||
// Точка возврата: docs/Future_Features/2026-05-24_1140_репосты_в_каналах_и_тредах.md
|
||||
actions.append(likeButton, replyButton);
|
||||
actions.append(likeButton, replyButton, ratingButton);
|
||||
|
||||
const shareButton = document.createElement('button');
|
||||
shareButton.type = 'button';
|
||||
@@ -1525,6 +1606,7 @@ function renderBody(screen, navigate, routeKey, channelData, handlers) {
|
||||
selector: channelData.selector,
|
||||
onToggleLike: handlers.onToggleLike,
|
||||
onReply: handlers.onReply,
|
||||
onRating: handlers.onRating,
|
||||
onRepost: handlers.onRepost,
|
||||
onShare: handlers.onShare,
|
||||
onEdit: handlers.onEdit,
|
||||
@@ -1670,6 +1752,18 @@ export function render({ navigate, route }) {
|
||||
rerender();
|
||||
};
|
||||
|
||||
const onRating = async (messageRef, text) => {
|
||||
const { login, storagePwd } = requireSigningSession();
|
||||
await authService.addBlockRating({ login, storagePwd, message: messageRef, text });
|
||||
|
||||
const scrollTarget = messageRefKey(messageRef);
|
||||
if (scrollTarget) pendingScrollByRoute.set(routeKey, scrollTarget);
|
||||
|
||||
softHaptic(15);
|
||||
showToast('Оценка отправлена');
|
||||
rerender();
|
||||
};
|
||||
|
||||
const loadOwnedChannelsForRepost = async (login) => {
|
||||
const feed = await authService.listSubscriptionsFeed(login, 1000);
|
||||
const rows = Array.isArray(feed?.ownedChannels) ? feed.ownedChannels : [];
|
||||
@@ -1742,7 +1836,7 @@ export function render({ navigate, route }) {
|
||||
}
|
||||
};
|
||||
|
||||
const onAddPost = async (bodyText) => {
|
||||
const onAddPost = async (bodyText, msgSubType = 10) => {
|
||||
const { login, storagePwd } = requireSigningSession();
|
||||
if (!activeSelector?.ownerBlockchainName || activeSelector.channelRootBlockNumber == null) {
|
||||
throw new Error('Идентификатор канала не готов.');
|
||||
@@ -1753,6 +1847,7 @@ export function render({ navigate, route }) {
|
||||
storagePwd,
|
||||
channel: activeSelector,
|
||||
text: bodyText,
|
||||
msgSubType,
|
||||
});
|
||||
|
||||
pendingScrollByRoute.set(routeKey, '__LAST__');
|
||||
@@ -1830,9 +1925,9 @@ export function render({ navigate, route }) {
|
||||
openAddMessageModal({
|
||||
channelName: apiData?.channel?.name || '',
|
||||
navigate,
|
||||
onSubmit: async (bodyText) => {
|
||||
onSubmit: async ({ text: bodyText, msgSubType }) => {
|
||||
try {
|
||||
await onAddPost(bodyText);
|
||||
await onAddPost(bodyText, msgSubType);
|
||||
showStatus('');
|
||||
} catch (error) {
|
||||
throw new Error(toUserMessage(error, 'Не удалось добавить сообщение.'));
|
||||
@@ -1856,6 +1951,14 @@ export function render({ navigate, route }) {
|
||||
throw new Error(toUserMessage(error, 'Не удалось отправить ответ.'));
|
||||
}
|
||||
},
|
||||
onRating: async (messageRef, text) => {
|
||||
try {
|
||||
await onRating(messageRef, text);
|
||||
showStatus('');
|
||||
} catch (error) {
|
||||
throw new Error(toUserMessage(error, 'Не удалось отправить оценку.'));
|
||||
}
|
||||
},
|
||||
onRepost: async (messageRef) => {
|
||||
try {
|
||||
await onRepost(messageRef);
|
||||
|
||||
Reference in New Issue
Block a user