UI: исправить каналы и добавить MCP-док по чтению/дозаписи

This commit is contained in:
AidarKC
2026-05-14 17:35:54 +03:00
parent 0fdb5b245c
commit 01b38952e5
5 changed files with 298 additions and 14 deletions
+98 -11
View File
@@ -19,6 +19,7 @@ import {
import { openSpeechInputModal } from '../components/speech-input-modal.js';
export const pageMeta = { id: 'channel-view', title: 'Канал' };
const CHANNEL_TYPE_PERSONAL = 100;
const pendingReactionActions = new Set();
const pendingScrollByRoute = new Map();
@@ -408,18 +409,50 @@ function mapApiMessageToPost(message, selector, localNumber) {
}
async function loadFromApi(route, channelId) {
let cachedFeed = null;
const ensureFeed = async () => {
if (cachedFeed) return cachedFeed;
cachedFeed = await authService.listSubscriptionsFeed(state.session.login, 1000);
return cachedFeed;
};
const getAllRows = async () => {
const feed = await ensureFeed();
return [
...(Array.isArray(feed?.ownedChannels) ? feed.ownedChannels : []),
...(Array.isArray(feed?.followedUsersChannels) ? feed.followedUsersChannels : []),
...(Array.isArray(feed?.followedChannels) ? feed.followedChannels : []),
];
};
let selector = buildSelectorFromRoute(route, channelId);
if (selector?.ownerBlockchainName && selector?.channelName) {
const ownFeed = await authService.listSubscriptionsFeed(state.session.login, 1000);
const allRows = [
...(Array.isArray(ownFeed?.ownedChannels) ? ownFeed.ownedChannels : []),
...(Array.isArray(ownFeed?.followedUsersChannels) ? ownFeed.followedUsersChannels : []),
...(Array.isArray(ownFeed?.followedChannels) ? ownFeed.followedChannels : []),
];
const channel = allRows.find((item) => (
String(item?.channel?.ownerBlockchainName || '').trim() === selector.ownerBlockchainName
const routeOwnerRaw = String(selector.ownerBlockchainName || '').trim();
const routeOwnerNormalized = routeOwnerRaw.toLowerCase();
const allRows = await getAllRows();
let channel = allRows.find((item) => (
String(item?.channel?.ownerBlockchainName || '').trim().toLowerCase() === routeOwnerNormalized
&& String(item?.channel?.channelName || '').trim().toLowerCase() === selector.channelName.toLowerCase()
));
if (!channel) {
channel = allRows.find((item) => (
String(item?.channel?.ownerLogin || '').trim().toLowerCase() === routeOwnerNormalized
&& String(item?.channel?.channelName || '').trim().toLowerCase() === selector.channelName.toLowerCase()
));
}
if (!channel) {
try {
const ownerUser = await authService.getUser(routeOwnerRaw);
const ownerBch = String(ownerUser?.blockchainName || '').trim().toLowerCase();
if (ownerBch) {
channel = allRows.find((item) => (
String(item?.channel?.ownerBlockchainName || '').trim().toLowerCase() === ownerBch
&& String(item?.channel?.channelName || '').trim().toLowerCase() === selector.channelName.toLowerCase()
));
}
} catch {
// ignore fallback lookup failures
}
}
if (!channel?.channel?.ownerBlockchainName || channel?.channel?.channelRoot?.blockNumber == null) {
throw new Error('Канал не найден.');
}
@@ -437,8 +470,53 @@ async function loadFromApi(route, channelId) {
const payload = await authService.getChannelMessages(selector, 200, 'asc', state.session.login);
const messages = Array.isArray(payload.messages) ? payload.messages : [];
const posts = messages.map((message, index) => mapApiMessageToPost(message, selector, index + 1));
let reverseChannelMissingWarning = '';
let mergedMessages = [...messages];
const currentLogin = String(state.session.login || '').trim();
const ownerLogin = String(payload.channel?.ownerLogin || '').trim();
const channelName = String(payload.channel?.channelName || '').trim();
const channelTypeCode = Number(payload.channel?.channelTypeCode ?? 1);
const canResolveReverse = (
channelTypeCode === CHANNEL_TYPE_PERSONAL
&& !!currentLogin
&& !!ownerLogin
&& !!channelName
&& ownerLogin.toLowerCase() === currentLogin.toLowerCase()
);
if (canResolveReverse) {
const allRows = await getAllRows();
const reverseSummary = allRows.find((item) => (
Number(item?.channel?.channelTypeCode ?? 1) === CHANNEL_TYPE_PERSONAL
&& String(item?.channel?.ownerLogin || '').trim().toLowerCase() === channelName.toLowerCase()
&& String(item?.channel?.channelName || '').trim().toLowerCase() === currentLogin.toLowerCase()
));
if (reverseSummary?.channel?.ownerBlockchainName && reverseSummary?.channel?.channelRoot?.blockNumber != null) {
const reverseSelector = {
ownerBlockchainName: String(reverseSummary.channel.ownerBlockchainName),
channelRootBlockNumber: Number(reverseSummary.channel.channelRoot.blockNumber),
channelRootBlockHash: normalizeRouteHash(reverseSummary.channel.channelRoot.blockHash),
};
const reversePayload = await authService.getChannelMessages(reverseSelector, 200, 'asc', state.session.login);
const reverseMessages = Array.isArray(reversePayload?.messages) ? reversePayload.messages : [];
mergedMessages = mergedMessages.concat(reverseMessages);
} else {
reverseChannelMissingWarning = `У собеседника ${channelName} пока не создан ответный персональный чат.`;
}
}
const posts = mergedMessages
.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 }));
const isOwnChannel = ownerLogin.toLowerCase() === (state.session.login || '').toLowerCase();
const followedRows = Array.isArray(state.channelsFeed?.followedChannels) ? state.channelsFeed.followedChannels : [];
const isSubscribed = followedRows.some((row) => (
@@ -455,6 +533,7 @@ async function loadFromApi(route, channelId) {
ownerName: ownerLogin || 'неизвестно',
},
posts,
reverseChannelMissingWarning,
isOwnChannel,
isSubscribed,
selector,
@@ -685,6 +764,12 @@ function renderBody(screen, navigate, routeKey, channelData, handlers) {
head.append(title);
head.append(owner, headActions);
if (channelData.reverseChannelMissingWarning) {
const reverseWarning = document.createElement('p');
reverseWarning.className = 'channel-head-meta';
reverseWarning.textContent = channelData.reverseChannelMissingWarning;
head.append(reverseWarning);
}
const actionButton = document.createElement('button');
actionButton.className = channelData.isOwnChannel
@@ -787,6 +872,7 @@ export function render({ navigate, route }) {
const next = render({ navigate, route });
current.replaceWith(next);
};
let activeSelector = null;
const requireSigningSession = () => {
const login = state.session.login;
@@ -860,14 +946,14 @@ export function render({ navigate, route }) {
const onAddPost = async (bodyText) => {
const { login, storagePwd } = requireSigningSession();
if (!routeSelector?.ownerBlockchainName || routeSelector.channelRootBlockNumber == null) {
if (!activeSelector?.ownerBlockchainName || activeSelector.channelRootBlockNumber == null) {
throw new Error('Идентификатор канала не готов.');
}
await authService.addBlockTextPost({
login,
storagePwd,
channel: routeSelector,
channel: activeSelector,
text: bodyText,
});
@@ -892,6 +978,7 @@ export function render({ navigate, route }) {
(async () => {
try {
const apiData = await loadFromApi(route, channelId);
activeSelector = apiData?.selector || null;
skeleton.remove();
cleanupSeenTracking = renderBody(screen, navigate, routeKey, apiData, {
onToggleLike: async (messageRef, action) => {
+1 -1
View File
@@ -42,7 +42,7 @@ const MSG_SUBTYPE_REACTION_LIKE = 1;
const MSG_SUBTYPE_REACTION_UNLIKE = 2;
const MSG_SUBTYPE_CONNECTION_FOLLOW = 30;
const MSG_SUBTYPE_CONNECTION_UNFOLLOW = 31;
const CREATE_CHANNEL_BODY_VERSION = 3;
const CREATE_CHANNEL_BODY_VERSION = 1;
const CHANNEL_TYPE_STORIES = 0;
const CHANNEL_TYPE_PUBLIC = 1;
const CHANNEL_TYPE_PERSONAL = 100;