SHA256
Добавить дневник действий и статусы материалов
This commit is contained in:
@@ -235,11 +235,11 @@ function resolveChannelHeadingFromNode(node) {
|
||||
function getChannelMessageTypeMeta(msgSubType) {
|
||||
switch (Number(msgSubType || 0)) {
|
||||
case MSG_SUBTYPE_TEXT_EXERCISE:
|
||||
return { label: 'Упражнение' };
|
||||
return { label: 'Упражнение', actionable: true };
|
||||
case MSG_SUBTYPE_TEXT_SERVICE:
|
||||
return { label: 'Процедура' };
|
||||
return { label: 'Услуга', actionable: true };
|
||||
case MSG_SUBTYPE_TEXT_COURSE:
|
||||
return { label: 'Курс' };
|
||||
return { label: 'Курс', actionable: true };
|
||||
case MSG_SUBTYPE_TEXT_ENTRYPOINT:
|
||||
return { label: 'Оглавление' };
|
||||
default:
|
||||
@@ -770,6 +770,8 @@ function renderNodeCard(node, heading, handlers, localNumber) {
|
||||
const authorTile = document.createElement('button');
|
||||
authorTile.type = 'button';
|
||||
authorTile.className = 'channel-message-author-tile';
|
||||
const headRow = document.createElement('div');
|
||||
headRow.className = 'channel-message-head-row';
|
||||
|
||||
const avatar = createThreadAvatar(author);
|
||||
|
||||
@@ -787,13 +789,6 @@ function renderNodeCard(node, heading, handlers, localNumber) {
|
||||
numberEl.textContent = `· #${localNumber}`;
|
||||
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';
|
||||
@@ -815,6 +810,25 @@ function renderNodeCard(node, heading, handlers, localNumber) {
|
||||
timestamp.textContent = node?.createdAtMs ? new Date(node.createdAtMs).toLocaleString() : '—';
|
||||
authorBlock.append(title, timestamp);
|
||||
authorTile.append(avatar, authorBlock);
|
||||
headRow.append(authorTile);
|
||||
const typeMeta = getChannelMessageTypeMeta(node?.msgSubType);
|
||||
if (typeMeta) {
|
||||
const typeButton = document.createElement('button');
|
||||
typeButton.type = 'button';
|
||||
typeButton.className = 'channel-message-type-button';
|
||||
typeButton.textContent = typeMeta.label;
|
||||
if (typeMeta.actionable && typeof handlers?.onStatusAction === 'function') {
|
||||
typeButton.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
animatePress(event.currentTarget);
|
||||
handlers.onStatusAction(node);
|
||||
});
|
||||
} else {
|
||||
typeButton.classList.add('is-static');
|
||||
typeButton.disabled = true;
|
||||
}
|
||||
headRow.append(typeButton);
|
||||
}
|
||||
|
||||
const isDeletedMessage = String(text || '').trim().toLowerCase() === 'удалено';
|
||||
|
||||
@@ -835,7 +849,7 @@ function renderNodeCard(node, heading, handlers, localNumber) {
|
||||
card.append(deleted);
|
||||
return card;
|
||||
} else {
|
||||
card.append(authorTile);
|
||||
card.append(headRow);
|
||||
if (parsedText.attachments.length > 0) {
|
||||
card.append(createAttachmentCarouselElement(parsedText.attachments, {
|
||||
gateway: state.entrySettings.arweaveServer,
|
||||
|
||||
@@ -43,6 +43,17 @@ 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_STATUS_DONE_ONCE = 10;
|
||||
const MSG_SUBTYPE_STATUS_LEARNED = 20;
|
||||
const MSG_SUBTYPE_STATUS_SERVICE_PASSED = 30;
|
||||
const MSG_SUBTYPE_STATUS_CONFIRMED = 100;
|
||||
const MSG_SUBTYPE_STATUS_INTERESTED = 110;
|
||||
const MSG_SUBTYPE_STATUS_STARTED = 120;
|
||||
const MSG_SUBTYPE_STATUS_IN_STUDY = 130;
|
||||
const MSG_SUBTYPE_STATUS_ABANDONED = 140;
|
||||
const MSG_SUBTYPE_STATUS_COMPLETED = 150;
|
||||
const DIARY_CHANNEL_NAME = 'diary';
|
||||
const DIARY_CHANNEL_DISPLAY_NAME = 'Личный дневник';
|
||||
|
||||
const pendingReactionActions = new Set();
|
||||
const pendingScrollByRoute = new Map();
|
||||
@@ -296,11 +307,11 @@ function resolveMessageTimestampMs(message) {
|
||||
function getChannelMessageTypeMeta(msgSubType) {
|
||||
switch (Number(msgSubType || 0)) {
|
||||
case MSG_SUBTYPE_TEXT_EXERCISE:
|
||||
return { label: 'Упражнение' };
|
||||
return { label: 'Упражнение', actionable: true };
|
||||
case MSG_SUBTYPE_TEXT_SERVICE:
|
||||
return { label: 'Процедура' };
|
||||
return { label: 'Услуга', actionable: true };
|
||||
case MSG_SUBTYPE_TEXT_COURSE:
|
||||
return { label: 'Курс' };
|
||||
return { label: 'Курс', actionable: true };
|
||||
case MSG_SUBTYPE_TEXT_ENTRYPOINT:
|
||||
return { label: 'Оглавление' };
|
||||
default:
|
||||
@@ -308,6 +319,63 @@ function getChannelMessageTypeMeta(msgSubType) {
|
||||
}
|
||||
}
|
||||
|
||||
function isDiarySelector(selector) {
|
||||
return String(selector?.channelName || '').trim().toLowerCase() === DIARY_CHANNEL_NAME;
|
||||
}
|
||||
|
||||
function isStatusActionSubType(msgSubType) {
|
||||
return new Set([
|
||||
MSG_SUBTYPE_STATUS_DONE_ONCE,
|
||||
MSG_SUBTYPE_STATUS_LEARNED,
|
||||
MSG_SUBTYPE_STATUS_SERVICE_PASSED,
|
||||
MSG_SUBTYPE_STATUS_CONFIRMED,
|
||||
MSG_SUBTYPE_STATUS_INTERESTED,
|
||||
MSG_SUBTYPE_STATUS_STARTED,
|
||||
MSG_SUBTYPE_STATUS_IN_STUDY,
|
||||
MSG_SUBTYPE_STATUS_ABANDONED,
|
||||
MSG_SUBTYPE_STATUS_COMPLETED,
|
||||
]).has(Number(msgSubType || 0));
|
||||
}
|
||||
|
||||
function getStatusActionTypeMeta(statusSubType, targetMsgSubType = 0) {
|
||||
const status = Number(statusSubType || 0);
|
||||
const target = Number(targetMsgSubType || 0);
|
||||
if (status === MSG_SUBTYPE_STATUS_DONE_ONCE && target === MSG_SUBTYPE_TEXT_EXERCISE) return { label: 'Выполнено' };
|
||||
if (status === MSG_SUBTYPE_STATUS_LEARNED && target === MSG_SUBTYPE_TEXT_EXERCISE) return { label: 'Изучено' };
|
||||
if (status === MSG_SUBTYPE_STATUS_SERVICE_PASSED && target === MSG_SUBTYPE_TEXT_SERVICE) return { label: 'Пройдено' };
|
||||
if (status === MSG_SUBTYPE_STATUS_INTERESTED) return { label: 'Заинтересовался' };
|
||||
if (status === MSG_SUBTYPE_STATUS_STARTED) return { label: 'Начал' };
|
||||
if (status === MSG_SUBTYPE_STATUS_IN_STUDY) return { label: 'Изучаю' };
|
||||
if (status === MSG_SUBTYPE_STATUS_ABANDONED) return { label: 'Бросил' };
|
||||
if (status === MSG_SUBTYPE_STATUS_COMPLETED) return { label: 'Завершил' };
|
||||
if (status === MSG_SUBTYPE_STATUS_CONFIRMED) return { label: 'Подтверждено' };
|
||||
return { label: 'Действие' };
|
||||
}
|
||||
|
||||
function getStatusActionOptionsForTarget(targetMsgSubType) {
|
||||
switch (Number(targetMsgSubType || 0)) {
|
||||
case MSG_SUBTYPE_TEXT_EXERCISE:
|
||||
return [
|
||||
{ subType: MSG_SUBTYPE_STATUS_DONE_ONCE, label: 'Выполнено', modalTitle: 'Упражнение выполнено' },
|
||||
{ subType: MSG_SUBTYPE_STATUS_LEARNED, label: 'Изучено', modalTitle: 'Упражнение изучено' },
|
||||
];
|
||||
case MSG_SUBTYPE_TEXT_SERVICE:
|
||||
return [
|
||||
{ subType: MSG_SUBTYPE_STATUS_SERVICE_PASSED, label: 'Пройдено', modalTitle: 'Услуга пройдена' },
|
||||
];
|
||||
case MSG_SUBTYPE_TEXT_COURSE:
|
||||
return [
|
||||
{ subType: MSG_SUBTYPE_STATUS_INTERESTED, label: 'Заинтересовался', modalTitle: 'Курс заинтересовал' },
|
||||
{ subType: MSG_SUBTYPE_STATUS_STARTED, label: 'Начал', modalTitle: 'Курс начат' },
|
||||
{ subType: MSG_SUBTYPE_STATUS_IN_STUDY, label: 'Изучаю', modalTitle: 'Курс изучается' },
|
||||
{ subType: MSG_SUBTYPE_STATUS_COMPLETED, label: 'Завершил', modalTitle: 'Курс завершён' },
|
||||
{ subType: MSG_SUBTYPE_STATUS_ABANDONED, label: 'Бросил', modalTitle: 'Курс брошен' },
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function createChannelAvatarElement(channel, size = 72) {
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'channel-profile-avatar';
|
||||
@@ -674,6 +742,97 @@ function openReplyModal({ onSubmit, navigate, mode = 'reply' }) {
|
||||
if (textEl) textEl.focus();
|
||||
}
|
||||
|
||||
function openStatusActionCommentModal({ title, submitLabel, onSubmit }) {
|
||||
const root = document.getElementById('modal-root');
|
||||
root.innerHTML = `
|
||||
<div class="modal" id="channel-status-action-modal">
|
||||
<div class="modal-card stack">
|
||||
<h3 class="modal-title">${escapeHtml(title || 'Новое действие')}</h3>
|
||||
<p class="meta-muted">Если хотите, можете добавить комментарий</p>
|
||||
<textarea id="channel-status-action-text" class="input" rows="5" maxlength="2000" placeholder="Комментарий"></textarea>
|
||||
<div class="meta-muted inline-error" id="channel-status-action-error"></div>
|
||||
<div class="form-actions-grid">
|
||||
<button class="secondary-btn" id="channel-status-action-cancel" type="button">Отмена</button>
|
||||
<button class="primary-btn" id="channel-status-action-submit" type="button">${escapeHtml(submitLabel || 'Сохранить')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const textEl = root.querySelector('#channel-status-action-text');
|
||||
const errorEl = root.querySelector('#channel-status-action-error');
|
||||
const submitEl = root.querySelector('#channel-status-action-submit');
|
||||
let inFlight = false;
|
||||
|
||||
const setBusy = (busy) => {
|
||||
inFlight = !!busy;
|
||||
if (textEl) textEl.disabled = inFlight;
|
||||
if (submitEl) {
|
||||
submitEl.disabled = inFlight;
|
||||
submitEl.textContent = inFlight ? 'Сохраняем...' : (submitLabel || 'Сохранить');
|
||||
}
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
root.innerHTML = '';
|
||||
};
|
||||
|
||||
root.querySelector('#channel-status-action-cancel')?.addEventListener('click', close);
|
||||
submitEl?.addEventListener('click', async () => {
|
||||
if (inFlight) return;
|
||||
setBusy(true);
|
||||
errorEl.textContent = '';
|
||||
try {
|
||||
await onSubmit(String(textEl?.value || '').trim());
|
||||
close();
|
||||
} catch (error) {
|
||||
setBusy(false);
|
||||
errorEl.textContent = toUserMessage(error, 'Не удалось сохранить действие.');
|
||||
}
|
||||
});
|
||||
|
||||
bindSubmitOnPlainEnter(textEl, () => submitEl?.click());
|
||||
if (textEl) textEl.focus();
|
||||
}
|
||||
|
||||
function openStatusActionMenuModal({ targetLabel, options = [], onSelect }) {
|
||||
const root = document.getElementById('modal-root');
|
||||
const rows = (Array.isArray(options) ? options : [])
|
||||
.map((item, index) => `
|
||||
<button class="channel-menu-item channel-status-action-item" data-status-index="${index}" type="button">
|
||||
${escapeHtml(item.label || 'Действие')}
|
||||
</button>
|
||||
`)
|
||||
.join('');
|
||||
|
||||
root.innerHTML = `
|
||||
<div class="modal" id="channel-status-menu-modal">
|
||||
<div class="modal-card stack">
|
||||
<h3 class="modal-title">${escapeHtml(targetLabel || 'Действия')}</h3>
|
||||
<div class="stack">${rows}</div>
|
||||
<div class="form-actions-grid">
|
||||
<button class="secondary-btn" id="channel-status-menu-cancel" type="button">Закрыть</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const close = () => {
|
||||
root.innerHTML = '';
|
||||
};
|
||||
|
||||
root.querySelector('#channel-status-menu-cancel')?.addEventListener('click', close);
|
||||
root.querySelectorAll('[data-status-index]').forEach((button) => {
|
||||
button.addEventListener('click', async (event) => {
|
||||
const idx = Number(event.currentTarget?.dataset?.statusIndex || -1);
|
||||
const option = options[idx];
|
||||
if (!option) return;
|
||||
close();
|
||||
await onSelect(option);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function openRepostModal({ navigate, channels = [], onSubmit }) {
|
||||
const root = document.getElementById('modal-root');
|
||||
const options = (Array.isArray(channels) ? channels : [])
|
||||
@@ -761,7 +920,7 @@ function openAddMessageModal({ channelName, onSubmit, navigate }) {
|
||||
<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_SERVICE}">Услуга</option>
|
||||
<option value="${MSG_SUBTYPE_TEXT_COURSE}">Курс</option>
|
||||
<option value="${MSG_SUBTYPE_TEXT_ENTRYPOINT}">Оглавление канала</option>
|
||||
</select>
|
||||
@@ -943,6 +1102,8 @@ function mapApiMessageToPost(message, selector, localNumber) {
|
||||
const hasRef = !!(messageBch && blockNumber != null && blockHash);
|
||||
|
||||
const resolvedText = resolveMessageText(message);
|
||||
const msgSubType = Number(message?.msgSubType || 0);
|
||||
const isStatusAction = isStatusActionSubType(msgSubType);
|
||||
const messageRef = hasRef
|
||||
? {
|
||||
blockchainName: messageBch,
|
||||
@@ -958,7 +1119,7 @@ function mapApiMessageToPost(message, selector, localNumber) {
|
||||
return {
|
||||
localNumber,
|
||||
authorLogin: message?.authorLogin || 'автор',
|
||||
body: resolvedText || (Number(message?.versionsTotal || 1) > 1 ? 'удалено' : '(пусто)'),
|
||||
body: resolvedText || (isStatusAction ? '' : (Number(message?.versionsTotal || 1) > 1 ? 'удалено' : '(пусто)')),
|
||||
versionsTotal: Number(message?.versionsTotal || 1),
|
||||
versions: Array.isArray(message?.versions) ? message.versions : [],
|
||||
likesCount: Number(message?.likesCount || 0),
|
||||
@@ -967,8 +1128,9 @@ function mapApiMessageToPost(message, selector, localNumber) {
|
||||
timestampMs: resolveMessageTimestampMs(message),
|
||||
messageRef,
|
||||
rawMessage: message,
|
||||
msgSubType: Number(message?.msgSubType || 0),
|
||||
isRating: Number(message?.msgSubType || 0) === MSG_SUBTYPE_TEXT_RATING,
|
||||
msgSubType,
|
||||
isRating: msgSubType === MSG_SUBTYPE_TEXT_RATING,
|
||||
isStatusAction,
|
||||
targetRef: message?.targetBlockchainName && Number.isFinite(Number(message?.targetBlockNumber))
|
||||
? {
|
||||
blockchainName: String(message.targetBlockchainName).trim(),
|
||||
@@ -976,6 +1138,11 @@ function mapApiMessageToPost(message, selector, localNumber) {
|
||||
blockHash: normalizeMessageHash(message?.targetBlockHash),
|
||||
}
|
||||
: null,
|
||||
targetMsgSubType: Number(message?.targetMsgSubType || 0),
|
||||
targetText: String(message?.targetText || '').trim(),
|
||||
targetAuthorLogin: String(message?.targetAuthorLogin || '').trim(),
|
||||
targetAuthorBlockchainName: String(message?.targetAuthorBlockchainName || '').trim(),
|
||||
targetCreatedAtMs: Number(message?.targetCreatedAtMs || 0),
|
||||
reactionState: messageRef ? getMessageReactionState(messageRef) : '',
|
||||
isOwnMessage: String(message?.authorLogin || '').trim().toLowerCase() === String(state.session.login || '').trim().toLowerCase(),
|
||||
};
|
||||
@@ -1004,6 +1171,45 @@ async function loadFromApi(route, channelId) {
|
||||
};
|
||||
|
||||
let selector = buildSelectorFromRoute(route, channelId);
|
||||
if (selector?.ownerBlockchainName && selector?.channelName && isDiarySelector(selector)) {
|
||||
if (!isAuthorized) {
|
||||
throw new Error('Личный дневник доступен только после входа.');
|
||||
}
|
||||
const diaryPayload = await authService.getPersonalDiary(currentSessionLogin, 400, 'asc');
|
||||
const diaryMessages = Array.isArray(diaryPayload?.messages) ? diaryPayload.messages : [];
|
||||
const posts = diaryMessages
|
||||
.map((message, index) => mapApiMessageToPost(message, selector, index + 1))
|
||||
.sort((a, b) => {
|
||||
const byTime = Number(a?.timestampMs || 0) - Number(b?.timestampMs || 0);
|
||||
if (byTime !== 0) return byTime;
|
||||
const aNum = Number(a?.messageRef?.blockNumber || 0);
|
||||
const bNum = Number(b?.messageRef?.blockNumber || 0);
|
||||
return aNum - bNum;
|
||||
})
|
||||
.map((post, index) => ({ ...post, localNumber: index + 1 }));
|
||||
|
||||
return {
|
||||
channel: {
|
||||
name: diaryPayload?.channel?.channelName || DIARY_CHANNEL_NAME,
|
||||
displayTitle: String(diaryPayload?.channel?.displayName || DIARY_CHANNEL_DISPLAY_NAME).trim(),
|
||||
displayName: DIARY_CHANNEL_DISPLAY_NAME,
|
||||
description: String(diaryPayload?.channel?.channelDescription || '').trim(),
|
||||
avaAr: '',
|
||||
avaSha256: '',
|
||||
avaSize: 0,
|
||||
metaUpdatedAtMs: 0,
|
||||
ownerName: currentSessionLogin,
|
||||
},
|
||||
posts,
|
||||
metaEvents: [],
|
||||
reverseChannelMissingWarning: '',
|
||||
isOwnChannel: true,
|
||||
isSubscribed: true,
|
||||
isDiary: true,
|
||||
selector,
|
||||
};
|
||||
}
|
||||
|
||||
if (selector?.ownerBlockchainName && selector?.channelName) {
|
||||
const routeOwnerRaw = String(selector.ownerBlockchainName || '').trim();
|
||||
const routeOwnerNormalized = routeOwnerRaw.toLowerCase();
|
||||
@@ -1275,6 +1481,7 @@ function renderPostCard(post, {
|
||||
onToggleLike,
|
||||
onReply,
|
||||
onRating,
|
||||
onStatusAction,
|
||||
onRepost,
|
||||
onShare,
|
||||
onEdit,
|
||||
@@ -1293,6 +1500,8 @@ function renderPostCard(post, {
|
||||
|
||||
const authorBlock = document.createElement('div');
|
||||
authorBlock.className = 'channel-message-author';
|
||||
const headRow = document.createElement('div');
|
||||
headRow.className = 'channel-message-head-row';
|
||||
|
||||
const title = document.createElement('div');
|
||||
title.className = 'channel-message-title author-line';
|
||||
@@ -1307,13 +1516,6 @@ function renderPostCard(post, {
|
||||
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';
|
||||
@@ -1336,6 +1538,25 @@ function renderPostCard(post, {
|
||||
}
|
||||
authorBlock.append(title, timestamp);
|
||||
authorTile.append(avatar, authorBlock);
|
||||
headRow.append(authorTile);
|
||||
const typeMeta = getChannelMessageTypeMeta(post.msgSubType);
|
||||
if (typeMeta) {
|
||||
const typeButton = document.createElement('button');
|
||||
typeButton.type = 'button';
|
||||
typeButton.className = 'channel-message-type-button';
|
||||
if (!typeMeta.actionable) typeButton.classList.add('is-static');
|
||||
typeButton.textContent = typeMeta.label;
|
||||
if (typeMeta.actionable && typeof onStatusAction === 'function') {
|
||||
typeButton.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
animatePress(event.currentTarget);
|
||||
onStatusAction(post);
|
||||
});
|
||||
} else {
|
||||
typeButton.disabled = true;
|
||||
}
|
||||
headRow.append(typeButton);
|
||||
}
|
||||
authorTile.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
const cleanLogin = String(post.authorLogin || '').trim();
|
||||
@@ -1363,7 +1584,7 @@ function renderPostCard(post, {
|
||||
card.append(deleted);
|
||||
return card;
|
||||
} else {
|
||||
card.append(authorTile);
|
||||
card.append(headRow);
|
||||
if (parsedBody.attachments.length > 0) {
|
||||
card.append(createAttachmentCarouselElement(parsedBody.attachments, {
|
||||
gateway: state.entrySettings.arweaveServer,
|
||||
@@ -1376,6 +1597,25 @@ function renderPostCard(post, {
|
||||
ratingBadge.textContent = 'Оценка';
|
||||
card.append(ratingBadge);
|
||||
}
|
||||
if (post.isStatusAction) {
|
||||
const statusBadge = document.createElement('span');
|
||||
statusBadge.className = 'channel-message-kind-badge channel-message-kind-badge--status';
|
||||
statusBadge.textContent = getStatusActionTypeMeta(post.msgSubType, post.targetMsgSubType).label;
|
||||
card.append(statusBadge);
|
||||
if (post.targetText || post.targetAuthorLogin) {
|
||||
const targetPreview = document.createElement('div');
|
||||
targetPreview.className = 'channel-message-target-preview';
|
||||
const targetType = getChannelMessageTypeMeta(post.targetMsgSubType)?.label || 'Материал';
|
||||
const targetText = String(post.targetText || '').trim();
|
||||
const targetAuthor = String(post.targetAuthorLogin || '').trim();
|
||||
targetPreview.innerHTML = `
|
||||
<strong>${escapeHtml(targetType)}</strong>
|
||||
<span>${escapeHtml(targetAuthor || 'автор')}</span>
|
||||
<p>${escapeHtml(targetText || 'Без текста')}</p>
|
||||
`;
|
||||
card.append(targetPreview);
|
||||
}
|
||||
}
|
||||
const body = document.createElement('p');
|
||||
body.className = 'channel-message-body';
|
||||
body.textContent = parsedBody.text;
|
||||
@@ -1607,6 +1847,7 @@ function renderBody(screen, navigate, routeKey, channelData, handlers) {
|
||||
onToggleLike: handlers.onToggleLike,
|
||||
onReply: handlers.onReply,
|
||||
onRating: handlers.onRating,
|
||||
onStatusAction: handlers.onStatusAction,
|
||||
onRepost: handlers.onRepost,
|
||||
onShare: handlers.onShare,
|
||||
onEdit: handlers.onEdit,
|
||||
@@ -1620,7 +1861,9 @@ function renderBody(screen, navigate, routeKey, channelData, handlers) {
|
||||
} else {
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'card meta-muted';
|
||||
empty.textContent = 'Ждем ваших начинаний';
|
||||
empty.textContent = channelData.isDiary
|
||||
? 'К сожалению, у вас пока еще ничего нет в личном дневнике.'
|
||||
: 'Ждем ваших начинаний';
|
||||
feed.append(empty);
|
||||
}
|
||||
|
||||
@@ -1634,7 +1877,9 @@ function renderBody(screen, navigate, routeKey, channelData, handlers) {
|
||||
backButton.textContent = 'Назад к каналам';
|
||||
backButton.addEventListener('click', () => navigate('channels-list'));
|
||||
|
||||
if (channelData.isOwnChannel) {
|
||||
if (channelData.isDiary) {
|
||||
screen.append(feed, backButton);
|
||||
} else if (channelData.isOwnChannel) {
|
||||
screen.append(feed, addMessageButton);
|
||||
} else if (!channelData.isSubscribed && !isStoriesChannel(channelData.channel)) {
|
||||
screen.append(actionButton, feed, backButton);
|
||||
@@ -1642,7 +1887,7 @@ function renderBody(screen, navigate, routeKey, channelData, handlers) {
|
||||
screen.append(feed, backButton);
|
||||
}
|
||||
|
||||
applyPendingScroll(screen, routeKey, channelData.isOwnChannel);
|
||||
applyPendingScroll(screen, routeKey, channelData.isOwnChannel || channelData.isDiary);
|
||||
return () => {
|
||||
// noop
|
||||
};
|
||||
@@ -1764,6 +2009,35 @@ export function render({ navigate, route }) {
|
||||
rerender();
|
||||
};
|
||||
|
||||
const onStatusAction = async (post) => {
|
||||
const options = getStatusActionOptionsForTarget(post?.msgSubType);
|
||||
const typeMeta = getChannelMessageTypeMeta(post?.msgSubType);
|
||||
if (!options.length || !typeMeta || !post?.messageRef) return;
|
||||
openStatusActionMenuModal({
|
||||
targetLabel: typeMeta.label,
|
||||
options,
|
||||
onSelect: async (option) => {
|
||||
openStatusActionCommentModal({
|
||||
title: option.modalTitle,
|
||||
submitLabel: option.label,
|
||||
onSubmit: async (text) => {
|
||||
const { login, storagePwd } = requireSigningSession();
|
||||
await authService.addBlockStatusAction({
|
||||
login,
|
||||
storagePwd,
|
||||
message: post.messageRef,
|
||||
text,
|
||||
statusSubType: option.subType,
|
||||
});
|
||||
softHaptic(14);
|
||||
showToast(`${option.label} сохранено`);
|
||||
rerender();
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const loadOwnedChannelsForRepost = async (login) => {
|
||||
const feed = await authService.listSubscriptionsFeed(login, 1000);
|
||||
const rows = Array.isArray(feed?.ownedChannels) ? feed.ownedChannels : [];
|
||||
@@ -1911,7 +2185,7 @@ export function render({ navigate, route }) {
|
||||
channelHeaderButton.onclick = (event) => {
|
||||
animatePress(event.currentTarget);
|
||||
openAboutChannelModal(apiData.channel, {
|
||||
canEdit: apiData?.isOwnChannel === true && !isStoriesChannel(apiData?.channel),
|
||||
canEdit: apiData?.isOwnChannel === true && !apiData?.isDiary && !isStoriesChannel(apiData?.channel),
|
||||
onEdit: () => openEditChannelModal({
|
||||
channel: apiData.channel,
|
||||
onSave: onEditChannelMeta,
|
||||
@@ -1959,6 +2233,14 @@ export function render({ navigate, route }) {
|
||||
throw new Error(toUserMessage(error, 'Не удалось отправить оценку.'));
|
||||
}
|
||||
},
|
||||
onStatusAction: async (post) => {
|
||||
try {
|
||||
await onStatusAction(post);
|
||||
showStatus('');
|
||||
} catch (error) {
|
||||
showStatus(toUserMessage(error, 'Не удалось записать действие.'));
|
||||
}
|
||||
},
|
||||
onRepost: async (messageRef) => {
|
||||
try {
|
||||
await onRepost(messageRef);
|
||||
|
||||
@@ -21,6 +21,8 @@ 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 DIARY_CHANNEL_NAME = 'diary';
|
||||
const DIARY_DISPLAY_NAME = 'Личный дневник';
|
||||
|
||||
function cleanChannelMessagePreview(text) {
|
||||
const parsed = parseMessageAttachments(text);
|
||||
@@ -698,11 +700,64 @@ function pullCreateSuccessFlash() {
|
||||
}
|
||||
}
|
||||
|
||||
function mapApiFeed(feed, notificationsState) {
|
||||
function buildDiaryChannelRow(diaryPayload, ownRows = [], notificationsState = {}, index = {}) {
|
||||
const messages = Array.isArray(diaryPayload?.messages) ? diaryPayload.messages : [];
|
||||
if (!messages.length) return null;
|
||||
|
||||
const ownerBlockchainName = String(
|
||||
diaryPayload?.channel?.ownerBlockchainName
|
||||
|| ownRows[0]?.channel?.ownerBlockchainName
|
||||
|| ''
|
||||
).trim();
|
||||
if (!ownerBlockchainName) return null;
|
||||
|
||||
const ownerLogin = String(diaryPayload?.channel?.ownerLogin || state.session.login || '').trim();
|
||||
const lastMessage = messages[messages.length - 1] || null;
|
||||
const rowId = 'own-diary';
|
||||
index[rowId] = { diary: true, payload: diaryPayload };
|
||||
|
||||
return {
|
||||
id: rowId,
|
||||
route: makeShineChannelRoute({
|
||||
ownerLogin,
|
||||
ownerBlockchainName,
|
||||
channelName: DIARY_CHANNEL_NAME,
|
||||
}),
|
||||
ownerName: ownerLogin || 'я',
|
||||
ownerBlockchainName,
|
||||
channelRootBlockNumber: 0,
|
||||
channelRootBlockHash: '0',
|
||||
avatar: 'Д',
|
||||
avaAr: '',
|
||||
title: DIARY_DISPLAY_NAME,
|
||||
technicalLabel: 'Виртуальная лента ваших действий',
|
||||
channelName: DIARY_CHANNEL_NAME,
|
||||
displayTitle: DIARY_DISPLAY_NAME,
|
||||
channelDescription: 'История действий по упражнениям, услугам и курсам',
|
||||
channelTypeCode: 900,
|
||||
channelTypeVersion: 1,
|
||||
messagePreview: String(lastMessage?.text || '').trim()
|
||||
? cleanChannelMessagePreview(lastMessage?.text)
|
||||
: 'Новая запись в личном дневнике',
|
||||
messagesCount: messages.length,
|
||||
unreadCount: 0,
|
||||
lastMessageAt: Number(lastMessage?.createdAtMs || 0),
|
||||
isOwnChannel: true,
|
||||
isSubscribed: false,
|
||||
notificationsEnabled: notificationsState[rowId] === true,
|
||||
pending: false,
|
||||
};
|
||||
}
|
||||
|
||||
function mapApiFeed(feed, notificationsState, diaryPayload = null) {
|
||||
const index = {};
|
||||
const ownChannels = (feed?.ownedChannels || [])
|
||||
.filter(isVisibleChannelSummary)
|
||||
.map((it, idx) => mapApiChannelRow(it, 'own', idx, index, notificationsState));
|
||||
const diaryChannel = diaryPayload
|
||||
? buildDiaryChannelRow(diaryPayload, feed?.ownedChannels || [], notificationsState, index)
|
||||
: null;
|
||||
if (diaryChannel) ownChannels.unshift(diaryChannel);
|
||||
const followedUserChannels = (feed?.followedUsersChannels || [])
|
||||
.filter(isVisibleChannelSummary)
|
||||
.map((it, idx) => mapApiChannelRow(it, 'followedUsers', idx, index, notificationsState));
|
||||
@@ -1126,7 +1181,13 @@ async function loadFeedAndRender({ screen, listState, contentEl, navigate }) {
|
||||
|
||||
try {
|
||||
const feed = await authService.listSubscriptionsFeed(state.session.login, 200);
|
||||
const groups = mapApiFeed(feed, listState.notificationsState);
|
||||
let diaryPayload = null;
|
||||
try {
|
||||
diaryPayload = await authService.getPersonalDiary(state.session.login, 200, 'asc');
|
||||
} catch {
|
||||
diaryPayload = null;
|
||||
}
|
||||
const groups = mapApiFeed(feed, listState.notificationsState, diaryPayload);
|
||||
|
||||
listState.channels = toListModel(groups);
|
||||
setChannelsFeed(feed, groups.index);
|
||||
|
||||
Reference in New Issue
Block a user