190 lines
5.6 KiB
JavaScript
190 lines
5.6 KiB
JavaScript
import { renderHeader } from '../components/header.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: 'small',
|
||
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: 'small',
|
||
className: 'avatar',
|
||
title,
|
||
});
|
||
avatarEl.replaceWith(upgraded);
|
||
});
|
||
return avatarEl;
|
||
}
|
||
|
||
export function render({ navigate }) {
|
||
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 = 'input dm-input contact-search-input';
|
||
input.type = 'text';
|
||
input.name = 'contact';
|
||
input.placeholder = 'Введите начало логина';
|
||
input.autocomplete = 'off';
|
||
input.maxLength = 80;
|
||
|
||
const resultsCard = document.createElement('section');
|
||
resultsCard.className = 'card stack contact-search-results-card';
|
||
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();
|
||
}, 2000);
|
||
};
|
||
|
||
const searchButton = document.createElement('button');
|
||
searchButton.className = 'primary-btn dm-send-btn';
|
||
searchButton.type = 'button';
|
||
searchButton.textContent = 'Поиск';
|
||
searchButton.addEventListener('click', async () => {
|
||
if (searchTimer) {
|
||
window.clearTimeout(searchTimer);
|
||
searchTimer = 0;
|
||
}
|
||
await runSearch();
|
||
});
|
||
|
||
input.addEventListener('input', () => {
|
||
scheduleSearch();
|
||
});
|
||
|
||
const controls = document.createElement('div');
|
||
controls.className = 'contact-search-actions';
|
||
controls.append(searchButton);
|
||
|
||
const formCard = document.createElement('section');
|
||
formCard.className = 'card stack contact-search-form-card';
|
||
formCard.append(input, controls);
|
||
|
||
resultsCard.append(status, resultsList);
|
||
|
||
screen.append(
|
||
renderHeader({
|
||
title: 'Поиск контактов',
|
||
leftAction: { label: '←', onClick: () => navigate('messages-list') },
|
||
}),
|
||
formCard,
|
||
resultsCard,
|
||
);
|
||
|
||
screen.cleanup = () => {
|
||
if (searchTimer) window.clearTimeout(searchTimer);
|
||
};
|
||
|
||
return screen;
|
||
}
|