SHA256
Start server-side channel read RPC handlers and simplify API spec
This commit is contained in:
@@ -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
@@ -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',
|
||||
|
||||
@@ -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,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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user