Start server-side channel read RPC handlers and simplify API spec

This commit is contained in:
ai5590
2026-03-30 14:32:15 +03:00
parent eb5593c7be
commit 9723696b2c
20 changed files with 1604 additions and 45 deletions
+2
View File
@@ -39,6 +39,7 @@ import * as contactSearchView from './pages/contact-search-view.js?v=20260330001
import * as chatView from './pages/chat-view.js?v=20260330001044';
import * as channelsList from './pages/channels-list.js?v=20260330001044';
import * as channelView from './pages/channel-view.js?v=20260330001044';
import * as addChannelView from './pages/add-channel-view.js?v=20260330001044';
import * as networkView from './pages/network-view.js?v=20260330001044';
import * as notificationsView from './pages/notifications-view.js?v=20260330001044';
@@ -69,6 +70,7 @@ const routes = {
'chat-view': chatView,
'channels-list': channelsList,
'channel-view': channelView,
'add-channel-view': addChannelView,
'network-view': networkView,
'notifications-view': notificationsView,
};
+61 -16
View File
@@ -147,35 +147,80 @@ export const chatMessages = {
export const channels = [
{
id: 'ch1',
name: 'Новости продукта',
initials: 'НП',
description: 'Официальный канал команды Shine с релизами и обновлениями.',
lastMessage: 'Опубликовали обзор нового демо-прототипа мобильного интерфейса.',
id: 'ch0',
name: 'Личный канал',
initials: 'ЛК',
ownerLogin: '@shine.alex',
ownerName: 'Вы',
description: 'Ваш основной канал (нулевой).',
lastMessage: 'Добро пожаловать в личный канал.',
time: '16:05',
unread: 14,
messagesCount: 14,
kind: 'own-personal',
},
{
id: 'ch1',
name: 'Команда продукта',
initials: 'КП',
ownerLogin: '@shine.alex',
ownerName: 'Вы',
description: 'Канал команды, который вы создали.',
lastMessage: 'Обновили roadmap на апрель.',
time: '15:42',
messagesCount: 8,
kind: 'own',
},
{
id: 'ch2',
name: 'Анекдоты дня',
initials: 'АД',
description: 'Лёгкий развлекательный канал с короткими шутками и мемами.',
lastMessage: 'Новый пост: как дизайнер, разработчик и дедлайн зашли в бар.',
name: 'Новости Bob',
initials: 'NB',
ownerLogin: '@bob',
ownerName: 'Bob',
description: 'Основной канал пользователя Bob.',
lastMessage: 'Вышел новый дайджест разработчика.',
time: '15:20',
unread: 3,
messagesCount: 5,
kind: 'followed-user-channel',
},
{
id: 'ch3',
name: 'Новости рынка',
initials: 'НР',
description: 'Короткие ежедневные сводки по рынку, технологиям и сообществам.',
lastMessage: 'В ленте свежая подборка новостей и главных событий дня.',
name: 'Стендап команды Bob',
initials: 'SB',
ownerLogin: '@bob',
ownerName: 'Bob',
description: 'Второй канал пользователя Bob.',
lastMessage: 'Перенесли созвон на 19:30.',
time: 'вчера',
unread: 0,
messagesCount: 11,
kind: 'followed-user-channel',
},
{
id: 'ch4',
name: 'Анекдоты дня',
initials: 'АД',
ownerLogin: '@fun.club',
ownerName: 'Fun Club',
description: 'Публичный развлекательный канал по подписке.',
lastMessage: 'Сегодня в выпуске 5 новых шуток.',
time: 'вчера',
messagesCount: 33,
kind: 'subscribed',
},
];
export const channelPosts = {
ch0: [
{
id: 'p0-1',
title: 'Первый личный пост',
body: 'Этот канал всегда ваш и стоит в списке первым.',
},
{
id: 'p0-2',
title: 'Планы',
body: 'Сюда удобно сохранять личные заметки и объявления.',
},
],
ch1: [
{
id: 'p1',
+38
View File
@@ -0,0 +1,38 @@
import { renderHeader } from '../components/header.js?v=20260330001044';
export const pageMeta = { id: 'add-channel-view', title: 'Добавить канал' };
export function render({ navigate }) {
const screen = document.createElement('section');
screen.className = 'stack';
screen.append(
renderHeader({
title: 'Добавить канал',
leftAction: { label: '←', onClick: () => navigate('channels-list') },
})
);
const form = document.createElement('form');
form.className = 'card stack';
form.innerHTML = `
<label for="channel-name">Имя канала</label>
<input id="channel-name" class="input" maxlength="64" placeholder="Например: Новости команды" required />
<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px;">
<button type="button" class="secondary-btn" id="cancel-create-channel">Отмена</button>
<button type="submit" class="primary-btn">Создать</button>
</div>
`;
form.addEventListener('submit', (event) => {
event.preventDefault();
navigate('channels-list');
});
form.querySelector('#cancel-create-channel').addEventListener('click', () => {
navigate('channels-list');
});
screen.append(form);
return screen;
}
+7 -2
View File
@@ -7,6 +7,7 @@ export function render({ navigate, route }) {
const channelId = route.params.channelId || 'ch1';
const channel = channels.find((c) => c.id === channelId) || channels[0];
const posts = channelPosts[channelId] || [];
const isOwnChannel = channel.ownerLogin === '@shine.alex';
const screen = document.createElement('section');
screen.className = 'stack';
@@ -23,9 +24,13 @@ export function render({ navigate, route }) {
head.innerHTML = `
<strong># ${channel.name}</strong>
<p class="meta-muted" style="margin-top:4px;">${channel.description}</p>
<p class="meta-muted" style="margin-top:8px;">Публичный канал, режим только чтение</p>
<p class="meta-muted" style="margin-top:8px;">Владелец: ${channel.ownerName}</p>
`;
const actionButton = document.createElement('button');
actionButton.className = isOwnChannel ? 'primary-btn' : 'secondary-btn';
actionButton.textContent = isOwnChannel ? 'Добавить сообщение в канал' : 'Отписаться от канала';
const feed = document.createElement('div');
feed.className = 'stack';
@@ -36,6 +41,6 @@ export function render({ navigate, route }) {
feed.append(card);
});
screen.append(head, feed);
screen.append(head, actionButton, feed);
return screen;
}
+110 -26
View File
@@ -3,40 +3,124 @@ import { channels } from '../mock-data.js?v=20260330001044';
export const pageMeta = { id: 'channels-list', title: 'Каналы' };
function openSimpleSubscribeModal(kindLabel) {
const root = document.getElementById('modal-root');
root.innerHTML = `
<div class="modal" id="channels-subscribe-modal">
<div class="modal-card stack">
<h3 style="font-size:18px;">${kindLabel}</h3>
<label class="meta-muted" for="subscribe-input">Введите идентификатор</label>
<input id="subscribe-input" class="input" placeholder="@login или #канал" />
<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px;">
<button class="secondary-btn" id="sub-cancel">Отмена</button>
<button class="primary-btn" id="sub-submit">Подписаться</button>
</div>
</div>
</div>
`;
const close = () => {
root.innerHTML = '';
};
root.querySelector('#sub-cancel').addEventListener('click', close);
root.querySelector('#sub-submit').addEventListener('click', close);
}
function renderChannelRow(channel, navigate) {
const row = document.createElement('article');
row.className = 'list-item';
row.innerHTML = `
<div class="avatar">${channel.initials}</div>
<div>
<strong># ${channel.name}</strong>
<p class="meta-muted" style="margin-top:4px;">${channel.description}</p>
<p class="meta-muted" style="margin-top:6px; color:#d8e3ff;">${channel.lastMessage}</p>
<p class="meta-muted" style="margin-top:6px;">Владелец: ${channel.ownerName}</p>
</div>
<div style="display:grid; justify-items:end; gap:6px;">
<span class="badge alt" style="padding:4px 8px; font-size:10px;">Канал</span>
<span class="meta-muted">${channel.time}</span>
<span class="unread">${channel.messagesCount}</span>
</div>
`;
row.addEventListener('click', () => navigate(`channel-view/${channel.id}`));
return row;
}
function renderSection(title, items, navigate) {
const wrap = document.createElement('section');
wrap.className = 'stack';
const header = document.createElement('h3');
header.className = 'section-title';
header.textContent = title;
wrap.append(header);
items.forEach((channel) => {
wrap.append(renderChannelRow(channel, navigate));
});
return wrap;
}
export function render({ navigate }) {
const screen = document.createElement('section');
screen.className = 'stack';
screen.append(renderHeader({ title: 'Каналы' }));
screen.append(
renderHeader({
title: 'Каналы',
rightActions: [
{ label: 'Подписаться на человека', onClick: () => openSimpleSubscribeModal('Подписка на человека') },
{ label: 'Подписаться на канал', onClick: () => openSimpleSubscribeModal('Подписка на канал') },
],
})
);
const search = document.createElement('div');
search.className = 'card';
search.textContent = 'Найти канал';
search.style.color = 'var(--text-muted)';
const ownChannels = channels
.filter((channel) => channel.kind === 'own-personal' || channel.kind === 'own')
.sort((a, b) => {
if (a.kind === 'own-personal') return -1;
if (b.kind === 'own-personal') return 1;
return a.name.localeCompare(b.name, 'ru');
});
const followedUserChannels = channels.filter((channel) => channel.kind === 'followed-user-channel');
const subscribedChannels = channels.filter((channel) => channel.kind === 'subscribed');
const listWrap = document.createElement('div');
listWrap.className = 'channels-scroll-wrap';
const list = document.createElement('div');
list.className = 'stack';
list.className = 'stack channels-groups';
channels.forEach((channel) => {
const row = document.createElement('article');
row.className = 'list-item';
row.innerHTML = `
<div class="avatar">${channel.initials}</div>
<div>
<strong># ${channel.name}</strong>
<p class="meta-muted" style="margin-top:4px;">${channel.description}</p>
<p class="meta-muted" style="margin-top:6px; color:#d8e3ff;">${channel.lastMessage}</p>
</div>
<div style="display:grid; justify-items:end; gap:6px;">
<span class="badge alt" style="padding:4px 8px; font-size:10px;">Канал</span>
<span class="meta-muted">${channel.time}</span>
${channel.unread ? `<span class="unread">${channel.unread}</span>` : '<span></span>'}
</div>
`;
row.addEventListener('click', () => navigate(`channel-view/${channel.id}`));
list.append(row);
});
list.append(renderSection('Мои каналы', ownChannels, navigate));
screen.append(search, list);
const dividerOne = document.createElement('hr');
dividerOne.className = 'channels-divider';
list.append(dividerOne);
list.append(renderSection('Каналы пользователей, на кого вы подписаны', followedUserChannels, navigate));
const dividerTwo = document.createElement('hr');
dividerTwo.className = 'channels-divider';
list.append(dividerTwo);
list.append(renderSection('Каналы, на которые вы подписаны', subscribedChannels, navigate));
const addChannelButton = document.createElement('button');
addChannelButton.className = 'primary-btn';
addChannelButton.textContent = 'Добавить канал';
addChannelButton.addEventListener('click', () => navigate('add-channel-view'));
list.append(addChannelButton);
const scrollHint = document.createElement('div');
scrollHint.className = 'channels-scroll-hint';
listWrap.append(list, scrollHint);
screen.append(listWrap);
return screen;
}
+1 -1
View File
@@ -57,6 +57,6 @@ export function resolveToolbarActive(pageId) {
return 'profile-view';
}
if (pageId === 'chat-view' || pageId === 'contact-search-view') return 'messages-list';
if (pageId === 'channel-view') return 'channels-list';
if (pageId === 'channel-view' || pageId === 'add-channel-view') return 'channels-list';
return 'profile-view';
}
+35
View File
@@ -728,3 +728,38 @@
border-radius: 16px;
padding: 14px;
}
.section-title {
font-size: 14px;
font-weight: 700;
color: #dbe7ff;
margin: 4px 2px;
}
.channels-scroll-wrap {
position: relative;
max-height: 58vh;
overflow-y: auto;
padding-right: 12px;
}
.channels-groups {
min-height: min-content;
}
.channels-divider {
border: 0;
border-top: 1px solid rgba(255, 255, 255, 0.14);
margin: 6px 0 8px;
}
.channels-scroll-hint {
position: absolute;
top: 4px;
right: 0;
width: 4px;
height: calc(100% - 8px);
border-radius: 999px;
background: linear-gradient(180deg, rgba(83, 216, 251, 0.55), rgba(83, 216, 251, 0.15));
pointer-events: none;
}