SHA256
Доработать оглавление канала и отображение дневника
This commit is contained in:
@@ -53,7 +53,7 @@ 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 DIARY_CHANNEL_DISPLAY_NAME = 'Дневник';
|
||||
|
||||
const pendingReactionActions = new Set();
|
||||
const pendingScrollByRoute = new Map();
|
||||
@@ -319,6 +319,10 @@ function getChannelMessageTypeMeta(msgSubType) {
|
||||
}
|
||||
}
|
||||
|
||||
function isEntrypointSubType(msgSubType) {
|
||||
return Number(msgSubType || 0) === MSG_SUBTYPE_TEXT_ENTRYPOINT;
|
||||
}
|
||||
|
||||
function isDiarySelector(selector) {
|
||||
return String(selector?.channelName || '').trim().toLowerCase() === DIARY_CHANNEL_NAME;
|
||||
}
|
||||
@@ -340,9 +344,9 @@ function isStatusActionSubType(msgSubType) {
|
||||
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_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: 'Изучаю' };
|
||||
@@ -833,6 +837,100 @@ function openStatusActionMenuModal({ targetLabel, options = [], onSelect }) {
|
||||
});
|
||||
}
|
||||
|
||||
function sortPostsByTimeDesc(posts = []) {
|
||||
return [...posts].sort((a, b) => {
|
||||
const byTime = Number(b?.timestampMs || 0) - Number(a?.timestampMs || 0);
|
||||
if (byTime !== 0) return byTime;
|
||||
return Number(b?.messageRef?.blockNumber || 0) - Number(a?.messageRef?.blockNumber || 0);
|
||||
});
|
||||
}
|
||||
|
||||
function getEntrypointPosts(posts = []) {
|
||||
return sortPostsByTimeDesc((Array.isArray(posts) ? posts : []).filter((post) => isEntrypointSubType(post?.msgSubType)));
|
||||
}
|
||||
|
||||
function flashAndScrollToMessage(messageRef) {
|
||||
const key = messageRefKey(messageRef);
|
||||
if (!key) return false;
|
||||
const cards = Array.from(document.querySelectorAll('.channel-message-card[data-message-key]'));
|
||||
const target = cards.find((card) => card.dataset.messageKey === key);
|
||||
if (!target) return false;
|
||||
target.classList.remove('is-focus-flash');
|
||||
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
window.setTimeout(() => target.classList.add('is-focus-flash'), 60);
|
||||
window.setTimeout(() => target.classList.remove('is-focus-flash'), 1800);
|
||||
return true;
|
||||
}
|
||||
|
||||
function openEntrypointMenuModal({ onShowHistory }) {
|
||||
const root = document.getElementById('modal-root');
|
||||
root.innerHTML = `
|
||||
<div class="modal" id="channel-entrypoint-menu-modal">
|
||||
<div class="modal-card stack">
|
||||
<h3 class="modal-title">Оглавление канала</h3>
|
||||
<div class="stack">
|
||||
<button class="channel-menu-item channel-status-action-item" id="channel-entrypoint-history" type="button">Просмотреть историю изменений оглавления</button>
|
||||
</div>
|
||||
<div class="form-actions-grid">
|
||||
<button class="secondary-btn" id="channel-entrypoint-close" type="button">Закрыть</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
root.querySelector('#channel-entrypoint-close')?.addEventListener('click', () => {
|
||||
root.innerHTML = '';
|
||||
});
|
||||
root.querySelector('#channel-entrypoint-history')?.addEventListener('click', () => {
|
||||
root.innerHTML = '';
|
||||
if (typeof onShowHistory === 'function') onShowHistory();
|
||||
});
|
||||
}
|
||||
|
||||
function openEntrypointHistoryModal({ channelTitle = '', posts = [], onSelect }) {
|
||||
const root = document.getElementById('modal-root');
|
||||
const items = getEntrypointPosts(posts);
|
||||
const cleanTitle = String(channelTitle || '').trim() || 'канала';
|
||||
root.innerHTML = `
|
||||
<div class="modal" id="channel-entrypoint-history-modal">
|
||||
<div class="modal-card stack">
|
||||
<h3 class="modal-title">История оглавления</h3>
|
||||
<p class="meta-muted">Показаны все версии оглавления канала ${escapeHtml(cleanTitle)}</p>
|
||||
<div class="entrypoint-history-list" id="entrypoint-history-list"></div>
|
||||
<button class="secondary-btn" id="entrypoint-history-close" type="button">Закрыть</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
const list = root.querySelector('#entrypoint-history-list');
|
||||
if (list) {
|
||||
if (!items.length) {
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'card meta-muted';
|
||||
empty.textContent = 'Оглавление в этом канале пока не добавлялось.';
|
||||
list.append(empty);
|
||||
} else {
|
||||
items.forEach((post) => {
|
||||
const parsed = parseMessageAttachments(post.body);
|
||||
const item = document.createElement('button');
|
||||
item.type = 'button';
|
||||
item.className = 'entrypoint-history-item';
|
||||
item.innerHTML = `
|
||||
<strong>${escapeHtml(post.timestampMs ? new Date(post.timestampMs).toLocaleString('ru-RU') : 'Без даты')}</strong>
|
||||
<span>#${escapeHtml(post.localNumber || '—')}</span>
|
||||
<p>${escapeHtml(String(parsed.text || '').trim() || 'Без текста')}</p>
|
||||
`;
|
||||
item.addEventListener('click', () => {
|
||||
root.innerHTML = '';
|
||||
if (typeof onSelect === 'function') onSelect(post);
|
||||
});
|
||||
list.append(item);
|
||||
});
|
||||
}
|
||||
}
|
||||
root.querySelector('#entrypoint-history-close')?.addEventListener('click', () => {
|
||||
root.innerHTML = '';
|
||||
});
|
||||
}
|
||||
|
||||
function openRepostModal({ navigate, channels = [], onSubmit }) {
|
||||
const root = document.getElementById('modal-root');
|
||||
const options = (Array.isArray(channels) ? channels : [])
|
||||
@@ -1129,7 +1227,7 @@ function mapApiMessageToPost(message, selector, localNumber) {
|
||||
messageRef,
|
||||
rawMessage: message,
|
||||
msgSubType,
|
||||
isRating: msgSubType === MSG_SUBTYPE_TEXT_RATING,
|
||||
isRating: msgSubType === MSG_SUBTYPE_TEXT_RATING && !isStatusAction,
|
||||
isStatusAction,
|
||||
targetRef: message?.targetBlockchainName && Number.isFinite(Number(message?.targetBlockNumber))
|
||||
? {
|
||||
@@ -1173,7 +1271,7 @@ async function loadFromApi(route, channelId) {
|
||||
let selector = buildSelectorFromRoute(route, channelId);
|
||||
if (selector?.ownerBlockchainName && selector?.channelName && isDiarySelector(selector)) {
|
||||
if (!isAuthorized) {
|
||||
throw new Error('Личный дневник доступен только после входа.');
|
||||
throw new Error('Дневник доступен только после входа.');
|
||||
}
|
||||
const diaryPayload = await authService.getPersonalDiary(currentSessionLogin, 400, 'asc');
|
||||
const diaryMessages = Array.isArray(diaryPayload?.messages) ? diaryPayload.messages : [];
|
||||
@@ -1192,7 +1290,7 @@ async function loadFromApi(route, channelId) {
|
||||
channel: {
|
||||
name: diaryPayload?.channel?.channelName || DIARY_CHANNEL_NAME,
|
||||
displayTitle: String(diaryPayload?.channel?.displayName || DIARY_CHANNEL_DISPLAY_NAME).trim(),
|
||||
displayName: DIARY_CHANNEL_DISPLAY_NAME,
|
||||
displayName: 'Дневника',
|
||||
description: String(diaryPayload?.channel?.channelDescription || '').trim(),
|
||||
avaAr: '',
|
||||
avaSha256: '',
|
||||
@@ -1482,6 +1580,7 @@ function renderPostCard(post, {
|
||||
onReply,
|
||||
onRating,
|
||||
onStatusAction,
|
||||
onOpenEntrypointMenu,
|
||||
onRepost,
|
||||
onShare,
|
||||
onEdit,
|
||||
@@ -1491,6 +1590,7 @@ function renderPostCard(post, {
|
||||
const card = document.createElement('article');
|
||||
card.className = 'card stack channel-message-card';
|
||||
if (post.isRating) card.classList.add('is-rating');
|
||||
if (selector && isDiarySelector(selector)) card.classList.add('is-diary-entry');
|
||||
|
||||
const authorTile = document.createElement('button');
|
||||
authorTile.type = 'button';
|
||||
@@ -1544,7 +1644,6 @@ function renderPostCard(post, {
|
||||
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) => {
|
||||
@@ -1552,7 +1651,14 @@ function renderPostCard(post, {
|
||||
animatePress(event.currentTarget);
|
||||
onStatusAction(post);
|
||||
});
|
||||
} else if (isEntrypointSubType(post.msgSubType) && typeof onOpenEntrypointMenu === 'function') {
|
||||
typeButton.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
animatePress(event.currentTarget);
|
||||
onOpenEntrypointMenu(post);
|
||||
});
|
||||
} else {
|
||||
typeButton.classList.add('is-static');
|
||||
typeButton.disabled = true;
|
||||
}
|
||||
headRow.append(typeButton);
|
||||
@@ -1597,7 +1703,7 @@ function renderPostCard(post, {
|
||||
ratingBadge.textContent = 'Оценка';
|
||||
card.append(ratingBadge);
|
||||
}
|
||||
if (post.isStatusAction) {
|
||||
if (post.isStatusAction && selector && isDiarySelector(selector)) {
|
||||
const statusBadge = document.createElement('span');
|
||||
statusBadge.className = 'channel-message-kind-badge channel-message-kind-badge--status';
|
||||
statusBadge.textContent = getStatusActionTypeMeta(post.msgSubType, post.targetMsgSubType).label;
|
||||
@@ -1848,6 +1954,7 @@ function renderBody(screen, navigate, routeKey, channelData, handlers) {
|
||||
onReply: handlers.onReply,
|
||||
onRating: handlers.onRating,
|
||||
onStatusAction: handlers.onStatusAction,
|
||||
onOpenEntrypointMenu: handlers.onOpenEntrypointMenu,
|
||||
onRepost: handlers.onRepost,
|
||||
onShare: handlers.onShare,
|
||||
onEdit: handlers.onEdit,
|
||||
@@ -1862,7 +1969,7 @@ function renderBody(screen, navigate, routeKey, channelData, handlers) {
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'card meta-muted';
|
||||
empty.textContent = channelData.isDiary
|
||||
? 'К сожалению, у вас пока еще ничего нет в личном дневнике.'
|
||||
? 'К сожалению, у вас пока еще ничего нет в Дневнике.'
|
||||
: 'Ждем ваших начинаний';
|
||||
feed.append(empty);
|
||||
}
|
||||
@@ -1928,13 +2035,20 @@ export function render({ navigate, route }) {
|
||||
const header = renderHeader({
|
||||
title: '',
|
||||
leftAction: { label: '<', onClick: () => navigateBack() },
|
||||
rightActions: [{ label: 'Канал: ...', onClick: () => {} }],
|
||||
rightActions: [
|
||||
{ label: 'Канал: ...', className: 'channel-header-route-btn', onClick: () => {} },
|
||||
{ label: 'Оглавление', className: 'channel-header-entrypoint-btn', onClick: () => {} },
|
||||
],
|
||||
});
|
||||
const channelHeaderButton = header.querySelector('.header-actions .icon-btn');
|
||||
const channelHeaderButton = header.querySelector('.header-actions .channel-header-route-btn');
|
||||
const channelEntrypointButton = header.querySelector('.header-actions .channel-header-entrypoint-btn');
|
||||
if (channelHeaderButton) {
|
||||
channelHeaderButton.classList.add('channel-header-route-btn');
|
||||
channelHeaderButton.disabled = true;
|
||||
}
|
||||
if (channelEntrypointButton) {
|
||||
channelEntrypointButton.disabled = true;
|
||||
channelEntrypointButton.hidden = true;
|
||||
}
|
||||
|
||||
const rerender = () => {
|
||||
const current = document.querySelector('section.channels-screen--channel');
|
||||
@@ -2179,6 +2293,18 @@ export function render({ navigate, route }) {
|
||||
const apiData = await loadFromApi(route, channelId);
|
||||
activeSelector = apiData?.selector || null;
|
||||
const titleLabel = apiData?.channel?.displayTitle || apiData?.channel?.name || 'channel';
|
||||
const entrypointPosts = getEntrypointPosts(apiData?.posts);
|
||||
const openEntrypointHistory = () => {
|
||||
openEntrypointHistoryModal({
|
||||
channelTitle: titleLabel,
|
||||
posts: apiData?.posts,
|
||||
onSelect: (post) => {
|
||||
if (!flashAndScrollToMessage(post?.messageRef)) {
|
||||
showToast('Не удалось найти запись оглавления в ленте');
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
if (channelHeaderButton) {
|
||||
channelHeaderButton.textContent = titleLabel;
|
||||
channelHeaderButton.disabled = false;
|
||||
@@ -2193,6 +2319,17 @@ export function render({ navigate, route }) {
|
||||
});
|
||||
};
|
||||
}
|
||||
if (channelEntrypointButton) {
|
||||
const canShowEntrypointButton = !apiData?.isDiary && entrypointPosts.length > 0;
|
||||
channelEntrypointButton.hidden = !canShowEntrypointButton;
|
||||
channelEntrypointButton.disabled = !canShowEntrypointButton;
|
||||
channelEntrypointButton.onclick = () => {
|
||||
const latestEntrypoint = entrypointPosts[0];
|
||||
if (!latestEntrypoint?.messageRef || !flashAndScrollToMessage(latestEntrypoint.messageRef)) {
|
||||
showToast('Не удалось найти актуальное оглавление');
|
||||
}
|
||||
};
|
||||
}
|
||||
skeleton.remove();
|
||||
cleanupSeenTracking = renderBody(screen, navigate, routeKey, apiData, {
|
||||
onAddMessage: () => {
|
||||
@@ -2241,6 +2378,11 @@ export function render({ navigate, route }) {
|
||||
showStatus(toUserMessage(error, 'Не удалось записать действие.'));
|
||||
}
|
||||
},
|
||||
onOpenEntrypointMenu: () => {
|
||||
openEntrypointMenuModal({
|
||||
onShowHistory: openEntrypointHistory,
|
||||
});
|
||||
},
|
||||
onRepost: async (messageRef) => {
|
||||
try {
|
||||
await onRepost(messageRef);
|
||||
|
||||
@@ -22,7 +22,7 @@ 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 = 'Личный дневник';
|
||||
const DIARY_DISPLAY_NAME = 'Дневник';
|
||||
|
||||
function cleanChannelMessagePreview(text) {
|
||||
const parsed = parseMessageAttachments(text);
|
||||
@@ -730,7 +730,7 @@ function buildDiaryChannelRow(diaryPayload, ownRows = [], notificationsState = {
|
||||
avatar: 'Д',
|
||||
avaAr: '',
|
||||
title: DIARY_DISPLAY_NAME,
|
||||
technicalLabel: 'Виртуальная лента ваших действий',
|
||||
technicalLabel: 'Виртуальная лента Дневника',
|
||||
channelName: DIARY_CHANNEL_NAME,
|
||||
displayTitle: DIARY_DISPLAY_NAME,
|
||||
channelDescription: 'История действий по упражнениям, услугам и курсам',
|
||||
@@ -738,7 +738,7 @@ function buildDiaryChannelRow(diaryPayload, ownRows = [], notificationsState = {
|
||||
channelTypeVersion: 1,
|
||||
messagePreview: String(lastMessage?.text || '').trim()
|
||||
? cleanChannelMessagePreview(lastMessage?.text)
|
||||
: 'Новая запись в личном дневнике',
|
||||
: 'Новая запись в Дневнике',
|
||||
messagesCount: messages.length,
|
||||
unreadCount: 0,
|
||||
lastMessageAt: Number(lastMessage?.createdAtMs || 0),
|
||||
|
||||
@@ -278,12 +278,25 @@ function createMediaSlide({ item, url, kind, messageTimestampMs, slide, previewU
|
||||
openAttachmentViewer({ item, url, kind });
|
||||
});
|
||||
|
||||
const bindImageLayout = (img) => {
|
||||
img.addEventListener('load', () => {
|
||||
const naturalWidth = Number(img.naturalWidth || 0);
|
||||
const naturalHeight = Number(img.naturalHeight || 0);
|
||||
const isLandscape = naturalWidth > 0 && naturalHeight > 0 && naturalWidth > naturalHeight;
|
||||
img.classList.toggle('is-landscape', isLandscape);
|
||||
frame.classList.toggle('is-landscape', isLandscape);
|
||||
slide.classList.toggle('has-landscape-media', isLandscape);
|
||||
slide.parentElement?.classList.toggle('has-landscape-media', isLandscape);
|
||||
}, { once: true });
|
||||
};
|
||||
|
||||
if (kind === 'image') {
|
||||
const img = document.createElement('img');
|
||||
img.className = 'message-attachment-media';
|
||||
img.src = url;
|
||||
img.alt = item.name;
|
||||
img.loading = 'lazy';
|
||||
bindImageLayout(img);
|
||||
img.addEventListener('error', () => replaceWithUnavailable(slide, messageTimestampMs), { once: true });
|
||||
frame.append(img);
|
||||
} else if (previewUrl) {
|
||||
@@ -292,6 +305,7 @@ function createMediaSlide({ item, url, kind, messageTimestampMs, slide, previewU
|
||||
img.src = previewUrl;
|
||||
img.alt = `${item.name} preview`;
|
||||
img.loading = 'lazy';
|
||||
bindImageLayout(img);
|
||||
img.addEventListener('error', () => replaceWithUnavailable(slide, messageTimestampMs), { once: true });
|
||||
const play = document.createElement('span');
|
||||
play.className = 'message-attachment-play';
|
||||
@@ -351,6 +365,7 @@ export function createAttachmentCarouselElement(attachments = [], { gateway = ''
|
||||
next.setAttribute('aria-label', 'Следующее вложение');
|
||||
const counter = document.createElement('div');
|
||||
counter.className = 'message-attachment-counter';
|
||||
wrap.classList.toggle('has-single-attachment', items.length <= 1);
|
||||
|
||||
const render = () => {
|
||||
stopVideos(wrap);
|
||||
@@ -358,6 +373,8 @@ export function createAttachmentCarouselElement(attachments = [], { gateway = ''
|
||||
const kind = getAttachmentKind(item);
|
||||
const url = buildArweaveDataUrl({ gateway, txId: item.ar });
|
||||
const previewUrl = item.preview?.ar ? buildArweaveDataUrl({ gateway, txId: item.preview.ar }) : '';
|
||||
viewport.classList.remove('has-landscape-media');
|
||||
slide.className = 'message-attachment-slide';
|
||||
slide.dataset.unavailable = '0';
|
||||
slide.replaceChildren();
|
||||
slide.append(kind === 'file'
|
||||
@@ -365,7 +382,8 @@ export function createAttachmentCarouselElement(attachments = [], { gateway = ''
|
||||
: createMediaSlide({ item, url, kind, messageTimestampMs, slide, previewUrl }));
|
||||
prev.hidden = items.length <= 1 || index <= 0;
|
||||
next.hidden = items.length <= 1 || index >= items.length - 1;
|
||||
counter.textContent = `${index + 1} из ${items.length}`;
|
||||
counter.hidden = items.length <= 1;
|
||||
counter.textContent = items.length > 1 ? `${index + 1} из ${items.length}` : '';
|
||||
};
|
||||
|
||||
const move = (delta) => {
|
||||
|
||||
Reference in New Issue
Block a user