Добавить рейтинг и типы материалов в каналах

This commit is contained in:
AidarKC
2026-08-10 01:16:10 +04:00
parent 3552e05e4c
commit ee185cf20a
11 changed files with 409 additions and 40 deletions
+86 -11
View File
@@ -24,6 +24,11 @@ import { loadProfileSnapshot } from '../services/user-profile-params.js';
import { extractLoginFromBlockchainName, makeProfileRoute, makeShineChannelRoute, makeShineMessageRoute } from '../services/shine-routes.js';
export const pageMeta = { id: 'channel-thread-view', title: 'Тред' };
const MSG_SUBTYPE_TEXT_RATING = 30;
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 pendingThreadScroll = new Map();
@@ -227,6 +232,21 @@ function resolveChannelHeadingFromNode(node) {
return `Сообщение в канале ${channelName}`;
}
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 extractChannelContextFromThreadPayload(payload) {
const focusInfo = payload?.focus?.channelInfo;
if (focusInfo?.ownerBlockchainName && focusInfo?.channelRoot?.blockNumber != null) {
@@ -461,13 +481,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="thread-reply-modal">
<div class="modal-card stack">
<h3 class="modal-title">Ответ</h3>
<textarea id="thread-reply-text" class="input" rows="5" maxlength="2000" placeholder="Текст ответа"></textarea>
<h3 class="modal-title">${title}</h3>
<textarea id="thread-reply-text" class="input" rows="5" maxlength="2000" placeholder="${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>
@@ -504,7 +533,7 @@ function openReplyModal({ onSubmit, navigate }) {
const text = String(textEl?.value || '').trim();
if (!text && attachments.length === 0) {
errorEl.textContent = 'Введите текст ответа или добавьте вложение.';
errorEl.textContent = emptyError;
return;
}
@@ -517,7 +546,7 @@ function openReplyModal({ onSubmit, navigate }) {
close();
} catch (error) {
setBusy(false);
errorEl.textContent = toUserMessage(error, 'Не удалось отправить ответ.');
errorEl.textContent = toUserMessage(error, submitError);
}
});
@@ -713,7 +742,6 @@ function openEditMessageModal({ initialText = '', allowEmptyText = false, onSave
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 versions = Array.isArray(node?.versions) ? node.versions : [];
@@ -721,11 +749,15 @@ function renderNodeCard(node, heading, handlers, localNumber) {
const text = resolveNodeText(node) || (versionsTotal > 1 ? 'удалено' : '(пусто)');
const likes = Number(node?.likesCount || 0);
const replies = Number(node?.repliesCount || 0);
const ratings = Number(node?.ratingsCount || 0);
const isOwnMessage = String(node?.authorLogin || '').trim().toLowerCase() === String(state.session.login || '').trim().toLowerCase();
const isChannelPost = Number(node?.channelInfo?.channelRoot?.blockNumber) >= 0;
const msgSubType = Number(node?.msgSubType || 0);
const isRating = msgSubType === MSG_SUBTYPE_TEXT_RATING;
const repostTarget = msgSubType === 50 ? buildRepostTargetFromNode(node) : null;
const parsedText = parseMessageAttachments(text);
if (isRating) card.classList.add('is-rating');
card.classList.add('is-counters-visible');
const headingText = String(heading || '').trim();
if (headingText) {
@@ -745,13 +777,23 @@ function renderNodeCard(node, heading, handlers, localNumber) {
authorBlock.className = 'channel-message-author';
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 = author;
const numberEl = document.createElement('span');
numberEl.className = 'author-line-num';
numberEl.textContent = `· #${localNumber}`;
title.append(loginEl, numberEl);
titleMain.append(loginEl, numberEl);
title.append(titleMain);
const typeMeta = getChannelMessageTypeMeta(node?.msgSubType);
if (typeMeta) {
const typeChip = document.createElement('span');
typeChip.className = 'channel-message-type-chip';
typeChip.textContent = typeMeta.label;
title.append(typeChip);
}
if (versionsTotal > 1) {
const editedMarker = document.createElement('button');
editedMarker.type = 'button';
@@ -800,6 +842,12 @@ function renderNodeCard(node, heading, handlers, localNumber) {
messageTimestampMs: node?.createdAtMs,
}));
}
if (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 = parsedText.text;
@@ -873,6 +921,24 @@ function renderNodeCard(node, heading, handlers, localNumber) {
onSubmit: async (textValue) => handlers.onReply(target, textValue),
});
});
const ratingButton = document.createElement('button');
ratingButton.type = 'button';
ratingButton.className = 'channel-action-item thread-rating-btn';
ratingButton.innerHTML = `
<span class="channel-action-icon" aria-hidden="true">★</span>
<span class="channel-action-label">Оценка</span>
<span class="channel-action-counter">${ratings}</span>
`;
setActionTitle(ratingButton, 'Оценка');
ratingButton.addEventListener('click', (event) => {
event.stopPropagation();
animatePress(event.currentTarget);
openReplyModal({
navigate: handlers.navigate,
mode: 'rating',
onSubmit: async (textValue) => handlers.onRating(target, textValue),
});
});
const shareButton = document.createElement('button');
shareButton.type = 'button';
@@ -890,7 +956,7 @@ function renderNodeCard(node, heading, handlers, localNumber) {
// Репосты временно отключены до будущей реализации.
// Точка возврата: docs/Future_Features/2026-05-24_1140_репосты_в_каналах_и_тредах.md
actions.append(likeButton, replyButton, shareButton);
actions.append(likeButton, replyButton, ratingButton, shareButton);
if (repostTarget) {
const originalButton = document.createElement('button');
originalButton.type = 'button';
@@ -977,7 +1043,7 @@ function renderDescendants(items, handlers, nextNumber, depth = 0) {
normalized.forEach((branch, index) => {
try {
const nodeNumber = nextNumber();
const row = renderNodeCard(branch?.node, `Ответ ${index + 1}`, handlers, nodeNumber);
const row = renderNodeCard(branch?.node, '', handlers, nodeNumber);
row.classList.add('thread-node-level');
row.style.setProperty('--depth', String(Math.min(depth, 4)));
wrap.append(row);
@@ -1122,6 +1188,15 @@ export function render({ navigate, route }) {
showStatus('');
rerender();
},
onRating: async (target, textValue) => {
const { login, storagePwd } = requireSigningSession();
await authService.addBlockRating({ login, storagePwd, message: target, text: textValue });
pendingThreadScroll.set(routeKey, '__LAST_REPLY__');
softHaptic(15);
showToast('Оценка отправлена');
showStatus('');
rerender();
},
onRepost: async (target) => {
const { login, storagePwd } = requireSigningSession();
const feed = await authService.listSubscriptionsFeed(login, 1000);
@@ -1365,7 +1440,7 @@ export function render({ navigate, route }) {
descendantsWrap.className = 'stack thread-block thread-block--replies';
const descendantsTitle = document.createElement('h3');
descendantsTitle.className = 'section-title';
descendantsTitle.textContent = 'Ответы';
descendantsTitle.textContent = 'Ответы и оценки';
descendantsWrap.append(descendantsTitle);
if (descendants.length) {
@@ -1373,7 +1448,7 @@ export function render({ navigate, route }) {
} else {
const empty = document.createElement('div');
empty.className = 'card meta-muted';
empty.textContent = 'Ответов пока нет.';
empty.textContent = 'Ответов и оценок пока нет.';
descendantsWrap.append(empty);
}
+116 -13
View File
@@ -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);
+75 -3
View File
@@ -56,6 +56,10 @@ const MSG_SUBTYPE_TEXT_EDIT_REPLY = 21;
const MSG_SUBTYPE_TEXT_RATING = 30;
const MSG_SUBTYPE_TEXT_REPOST = 50;
const MSG_SUBTYPE_TEXT_CHANNEL_META = 90;
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 MSG_SUBTYPE_REACTION_LIKE = 1;
const MSG_SUBTYPE_REACTION_UNLIKE = 2;
const MSG_SUBTYPE_CONNECTION_FOLLOW = 30;
@@ -564,6 +568,38 @@ function makeTextReplyBodyBytes({ toBlockchainName, toBlockNumber, toBlockHashHe
);
}
function makeTextRatingBodyBytes({ toBlockchainName, toBlockNumber, toBlockHashHex, text }) {
const cleanBch = String(toBlockchainName || '').trim();
if (!cleanBch) throw new Error('toBlockchainName is required for rating');
const blockNumber = Number(toBlockNumber);
if (!Number.isFinite(blockNumber) || blockNumber < 0) {
throw new Error('Invalid toBlockNumber for rating');
}
const message = String(text || '').trim();
if (!message) throw new Error('Rating text is required');
const bchBytes = utf8Bytes(cleanBch);
if (bchBytes.length < 1 || bchBytes.length > 255) {
throw new Error('toBlockchainName must be 1..255 bytes');
}
const textBytes = utf8Bytes(message);
if (textBytes.length < 1 || textBytes.length > 65535) {
throw new Error('Rating text must be 1..65535 UTF-8 bytes');
}
return concatBytes(
int8Byte(bchBytes.length),
bchBytes,
int32Bytes(blockNumber),
hexToBytes(normalizeHex32(toBlockHashHex)),
int16Bytes(textBytes.length),
textBytes
);
}
function makeTextRepostBodyBytes({
lineCode,
prevLineNumber,
@@ -1769,6 +1805,31 @@ export class AuthService {
});
}
async addBlockRating({ login, message, text, storagePwd }) {
const cleanLogin = String(login || '').trim();
const cleanText = String(text || '').trim();
const target = normalizeMessageRefTarget(message, 'rating');
const key = `rating:${cleanLogin}:${target.blockchainName}:${target.blockNumber}:${target.blockHash}:${cleanText}`;
return this.runWriteLocked(key, async () => {
const bodyBytes = makeTextRatingBodyBytes({
toBlockchainName: target.blockchainName,
toBlockNumber: target.blockNumber,
toBlockHashHex: target.blockHash,
text: cleanText,
});
return this.addBlockSigned({
login: cleanLogin,
storagePwd,
msgType: MSG_TYPE_TEXT,
msgSubType: MSG_SUBTYPE_TEXT_RATING,
msgVersion: 1,
bodyBytes,
});
});
}
async addBlockRepost({ login, channel, message, text, storagePwd }) {
const cleanLogin = String(login || '').trim();
if (!cleanLogin) throw new Error('Missing login');
@@ -2212,14 +2273,25 @@ export class AuthService {
};
}
async addBlockTextPost({ login, channel, text, storagePwd }) {
async addBlockTextPost({ login, channel, text, storagePwd, msgSubType = MSG_SUBTYPE_TEXT_POST }) {
const cleanLogin = (login || '').trim();
if (!cleanLogin) throw new Error('Missing login');
const cleanText = String(text || '').trim();
const cleanSubType = Number(msgSubType || MSG_SUBTYPE_TEXT_POST);
const allowedTextSubTypes = new Set([
MSG_SUBTYPE_TEXT_POST,
MSG_SUBTYPE_TEXT_ENTRYPOINT,
MSG_SUBTYPE_TEXT_EXERCISE,
MSG_SUBTYPE_TEXT_SERVICE,
MSG_SUBTYPE_TEXT_COURSE,
]);
if (!allowedTextSubTypes.has(cleanSubType)) {
throw new Error('Unsupported channel text subtype');
}
const selector = channel || {};
const owner = String(selector?.ownerBlockchainName || '').trim();
const root = Number(selector?.channelRootBlockNumber);
const key = `text-post:${cleanLogin}:${owner}:${root}:${cleanText}`;
const key = `text-post:${cleanLogin}:${owner}:${root}:${cleanSubType}:${cleanText}`;
return this.runWriteLocked(key, async () => {
const user = await this.ensureChainInitializedForLineOps(cleanLogin, storagePwd);
@@ -2239,7 +2311,7 @@ export class AuthService {
login: cleanLogin,
storagePwd,
msgType: MSG_TYPE_TEXT,
msgSubType: MSG_SUBTYPE_TEXT_POST,
msgSubType: cleanSubType,
msgVersion: 1,
bodyBytes,
});