Files
SHiNE-server/shine-UI/js/pages/contact-search-view.js
T
qandClaude Opus 5.5 f8900e531a UI: новый общий стиль, тёмная/светлая тема, настраиваемая палитра
- styles/main.css: роли цветов (по умолчанию «Индиго»), шкала отступов/скруглений/шрифтов,
  цвета отношений для обеих тем; жёсткие цвета в стилях заменены на роли.
- Палитра: пресеты и личные правки, «Оформление» Авто/День/Ночь, долгое нажатие —
  редактор цветов с экспортом/импортом.
- Каналы: пузыри постов автора, плашки дней, строка «Написать в канал…», «О канале»,
  создание канала с адресом из названия; лента открывается на свежих постах.
- Чаты: плоский список, чипы-фильтры, пузыри, плашки дней; нижняя панель скрыта в переписке.
- Корневые разделы — единая шапка; профиль и чужой профиль — общая карточка;
  настройки, кошелёк, сеансы — меню-списки.
- Нижняя панель: иконки без подписей, бейджи на иконках.
- confirmDialog вместо window.confirm/alert; на телефоне диалоги — шторки снизу.
- docs/UI-Design/ISSUES-for-dev.md — найденные ошибки сервера/UI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 10:05:26 +03:00

195 lines
5.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { iconHtml } from '../components/ui-icon.js';
import { createTopBar } from '../components/topbar.js';
import { authService } from '../state.js';
import { renderUserAvatar } from '../components/avatar-image.js';
import { loadProfileSnapshot } from '../services/user-profile-params.js';
import { makeProfileRoute } from '../services/shine-routes.js';
export const pageMeta = { id: 'contact-search-view', title: 'Поиск контактов' };
const searchAvatarSnapshotCache = new Map();
const searchAvatarPendingByLogin = new Map();
async function loadSearchAvatarSnapshot(login) {
const cleanLogin = String(login || '').trim();
if (!cleanLogin) return null;
const key = cleanLogin.toLowerCase();
if (searchAvatarSnapshotCache.has(key)) return searchAvatarSnapshotCache.get(key);
if (searchAvatarPendingByLogin.has(key)) return searchAvatarPendingByLogin.get(key);
const pending = loadProfileSnapshot(cleanLogin)
.then((snapshot) => {
searchAvatarSnapshotCache.set(key, snapshot || null);
searchAvatarPendingByLogin.delete(key);
return snapshot || null;
})
.catch(() => {
searchAvatarSnapshotCache.set(key, null);
searchAvatarPendingByLogin.delete(key);
return null;
});
searchAvatarPendingByLogin.set(key, pending);
return pending;
}
function createSearchAvatar(login) {
const cleanLogin = String(login || '').trim();
const title = cleanLogin ? `Профиль ${cleanLogin}` : '';
const avatarEl = renderUserAvatar({
login: cleanLogin || 'unknown',
size: 'md',
className: 'avatar',
title,
});
if (!cleanLogin) return avatarEl;
void loadSearchAvatarSnapshot(cleanLogin).then((snapshot) => {
if (!avatarEl.isConnected) return;
const upgraded = renderUserAvatar({
login: cleanLogin,
avatar: snapshot?.avatar?.txId
? {
ar: String(snapshot.avatar.txId || '').trim(),
sha256Hex: String(snapshot?.avatar?.sha256Hex || '').trim().toLowerCase(),
}
: null,
size: 'md',
className: 'avatar',
title,
});
avatarEl.replaceWith(upgraded);
});
return avatarEl;
}
export function render({navigate, chrome}) {
const screen = document.createElement('section');
screen.className = 'stack dm-screen dm-search-screen';
let searchTimer = 0;
let searchSeq = 0;
const input = document.createElement('input');
input.className = 'search-field__input contact-search-input';
input.type = 'text';
input.name = 'contact';
input.placeholder = 'Логин или его начало';
input.setAttribute('aria-label', 'Логин для поиска');
input.enterKeyHint = 'search';
input.autocomplete = 'off';
input.maxLength = 80;
const resultsCard = document.createElement('section');
resultsCard.className = 'stack contact-search-results';
resultsCard.hidden = true;
const status = document.createElement('p');
status.className = 'contact-search-results-title';
const resultsList = document.createElement('div');
resultsList.className = 'stack dm-list';
const renderResults = (matches, query) => {
resultsList.innerHTML = '';
if (!query.trim()) {
status.textContent = '';
resultsCard.hidden = true;
return;
}
resultsCard.hidden = false;
if (!matches.length) {
status.textContent = 'Найдено пользователей: 0';
return;
}
status.textContent = `Найдено пользователей: ${matches.length}`;
matches.forEach((login) => {
const row = document.createElement('article');
row.className = 'list-item dm-dialog-card';
const avatarEl = createSearchAvatar(login);
row.innerHTML = `
<div class="contact-search-result-main">
<strong class="dm-row-title">${login}</strong>
</div>
<span class="dm-chevron" aria-hidden="true">›</span>
`;
row.prepend(avatarEl);
row.addEventListener('click', () => {
navigate(makeProfileRoute(login));
});
resultsList.append(row);
});
};
const runSearch = async () => {
const query = input.value.trim();
const seq = ++searchSeq;
if (!query) {
renderResults([], '');
return;
}
try {
const logins = await authService.searchUsers(query);
if (seq !== searchSeq) return;
renderResults((logins || []).slice(0, 5), query);
} catch (e) {
if (seq !== searchSeq) return;
status.textContent = `Ошибка поиска: ${e.message || 'unknown'}`;
resultsCard.hidden = false;
resultsList.innerHTML = '';
}
};
const scheduleSearch = () => {
if (searchTimer) window.clearTimeout(searchTimer);
searchTimer = window.setTimeout(() => {
searchTimer = 0;
void runSearch();
}, 500);
};
const searchButton = document.createElement('button');
searchButton.className = 'search-field__action';
searchButton.type = 'button';
searchButton.textContent = 'Найти';
searchButton.addEventListener('click', async () => {
if (searchTimer) {
window.clearTimeout(searchTimer);
searchTimer = 0;
}
await runSearch();
});
input.addEventListener('input', () => {
scheduleSearch();
});
input.addEventListener('keydown', (event) => {
if (event.key !== 'Enter') return;
event.preventDefault();
searchButton.click();
});
const formCard = document.createElement('label');
formCard.className = 'search-field contact-search-field';
formCard.innerHTML = `<span class="search-field__icon" aria-hidden="true">${iconHtml('search')}</span>`;
formCard.append(input, searchButton);
resultsCard.append(status, resultsList);
chrome?.setTopbar(createTopBar({
title: 'Найти человека',
back: { label: '←', onClick: () => navigate('messages-list') },
}));
screen.append(
formCard,
resultsCard,
);
screen.cleanup = () => {
if (searchTimer) window.clearTimeout(searchTimer);
};
return screen;
}