Чат: автооткрытие свежего диалога и поиск сообщений

- Списки каналов и ЛС сортируются по last_post_at (свежие сверху)
- При открытии чата автоматически выбирается самая свежая розмова — сразу
  видны последние сообщения, а не пустой экран
- Поиск сообщений через Mattermost /teams/{id}/posts/search: строка поиска
  над лентой, результаты списком (кто/канал/время/текст), клик открывает канал
- Выход из поиска при открытии канала и смене команды

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 15:48:28 +03:00
parent 04b1625fde
commit caa728158b
2 changed files with 139 additions and 2 deletions
+129 -2
View File
@@ -176,20 +176,30 @@ function chatRenderTeamSwitcher() {
}
function chatSelectTeam(teamId) {
chatCloseSearch(); // результати пошуку прив'язані до команди
chatCurrentTeamId = teamId;
chatRenderTeamSwitcher();
chatRenderChannelList();
}
function chatByRecent(a, b) {
// Спершу найсвіжіші за останнім повідомленням, потім за назвою.
var diff = (b.last_post_at || 0) - (a.last_post_at || 0);
return diff !== 0 ? diff : (a.display_name || '').localeCompare(b.display_name || '');
}
function chatLoadChannels() {
return chatApiFetch('/users/me/channels').then(function (channels) {
chatChannels = channels
.filter(function (ch) { return ch.type === 'O' || ch.type === 'P'; })
.sort(function (a, b) { return a.display_name.localeCompare(b.display_name); });
chatDmChannels = channels.filter(function (ch) { return ch.type === 'D' || ch.type === 'G'; });
.sort(chatByRecent);
chatDmChannels = channels
.filter(function (ch) { return ch.type === 'D' || ch.type === 'G'; })
.sort(chatByRecent);
chatRenderChannelList();
chatRenderDmList();
chatDmChannels.forEach(chatResolveDmLabel);
chatAutoSelectRecent();
}).catch(function (e) {
console.error(e);
var el = document.getElementById('chat-channel-list');
@@ -197,6 +207,20 @@ function chatLoadChannels() {
});
}
function chatAutoSelectRecent() {
// Відкриваємо найсвіжішу розмову за замовчуванням, щоб одразу бачити останні
// повідомлення (а не порожній екран). Лише якщо користувач ще нічого не обрав.
if (chatCurrentChannelId) return;
var candidates = chatChannels.filter(function (ch) {
return !chatCurrentTeamId || ch.team_id === chatCurrentTeamId;
}).concat(chatDmChannels);
if (!candidates.length) return;
var best = candidates.reduce(function (a, b) {
return (b.last_post_at || 0) > (a.last_post_at || 0) ? b : a;
});
if (best) chatSelectChannel(best.id);
}
function chatResolveDmLabel(channel) {
var promise;
var otherId = null;
@@ -324,6 +348,7 @@ function chatUpdateHeader(ch) {
}
function chatSelectChannel(channelId) {
chatCloseSearch(); // вихід із режиму пошуку при відкритті каналу
var prevRow = chatCurrentChannelId ? document.getElementById('chat-channel-row-' + chatCurrentChannelId) : null;
if (prevRow) prevRow.style.background = '';
@@ -394,6 +419,108 @@ function chatRenderThreadMessages(posts, replace) {
chatAppendPosts(el, posts, chatThreadRenderedPostIds, 'thread');
}
// ── Пошук повідомлень (Mattermost /teams/{id}/posts/search) ──────────────────
function chatCloseSearch() {
var resultsEl = document.getElementById('chat-search-results');
var messagesEl = document.getElementById('chat-messages');
if (resultsEl) { resultsEl.style.display = 'none'; resultsEl.innerHTML = ''; }
if (messagesEl) messagesEl.style.display = 'flex';
var input = document.getElementById('chat-search-input');
if (input) input.value = '';
}
function chatSearchChannelLabel(channelId) {
var ch = chatFindChannel(channelId);
if (!ch) return '';
if (ch.type === 'O' || ch.type === 'P') return '#' + ch.display_name;
return chatDmLabels[ch.id] || 'Особисте';
}
function chatSearchMessages(terms) {
terms = (terms || '').trim();
var resultsEl = document.getElementById('chat-search-results');
var messagesEl = document.getElementById('chat-messages');
if (!resultsEl) return;
if (!terms) { chatCloseSearch(); return; }
if (!chatCurrentTeamId) return;
resultsEl.style.display = 'flex';
if (messagesEl) messagesEl.style.display = 'none';
resultsEl.innerHTML = '<div style="text-align:center; font-size:0.8125rem; color:var(--color-muted-foreground); padding:1rem;">Пошук…</div>';
chatApiFetch('/teams/' + chatCurrentTeamId + '/posts/search', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({terms: terms, is_or_search: false, per_page: 30}),
}).then(function (data) {
var ids = data.order || [];
var posts = ids.map(function (id) { return data.posts[id]; }).filter(Boolean);
return chatResolveUsernames(posts).then(function () {
chatRenderSearchResults(posts);
});
}).catch(function (e) {
console.error(e);
resultsEl.innerHTML = '<div style="text-align:center; font-size:0.8125rem; color:var(--color-destructive); padding:1rem;">Не вдалося виконати пошук</div>';
});
}
function chatRenderSearchResults(posts) {
var el = document.getElementById('chat-search-results');
if (!el) return;
el.innerHTML = '';
var head = document.createElement('div');
head.style.cssText = 'display:flex; align-items:center; justify-content:space-between; margin-bottom:0.25rem;';
var title = document.createElement('span');
title.style.cssText = 'font-size:0.75rem; color:var(--color-muted-foreground);';
title.textContent = 'Знайдено: ' + posts.length;
var closeBtn = document.createElement('button');
closeBtn.type = 'button';
closeBtn.textContent = 'Закрити пошук';
closeBtn.style.cssText = 'background:none; border:none; cursor:pointer; font-size:0.75rem; color:var(--color-primary);';
closeBtn.onclick = chatCloseSearch;
head.appendChild(title);
head.appendChild(closeBtn);
el.appendChild(head);
if (!posts.length) {
var none = document.createElement('div');
none.style.cssText = 'text-align:center; font-size:0.8125rem; color:var(--color-muted-foreground); padding:1rem;';
none.textContent = 'Нічого не знайдено';
el.appendChild(none);
return;
}
posts.sort(function (a, b) { return (b.create_at || 0) - (a.create_at || 0); });
posts.forEach(function (post) {
var row = document.createElement('div');
row.style.cssText = 'padding:0.625rem 0.75rem; border:1px solid var(--color-border); border-radius:0.5rem; cursor:pointer;';
row.onmouseover = function () { row.style.background = 'var(--color-muted)'; };
row.onmouseout = function () { row.style.background = ''; };
row.onclick = function () { chatSelectChannel(post.channel_id); };
var meta = document.createElement('div');
meta.style.cssText = 'display:flex; justify-content:space-between; gap:0.5rem; margin-bottom:0.25rem;';
var who = document.createElement('span');
who.style.cssText = 'font-size:0.75rem; font-weight:600; color:var(--color-foreground); overflow:hidden; text-overflow:ellipsis; white-space:nowrap;';
who.textContent = (chatUserNames[post.user_id] || '') + ' · ' + chatSearchChannelLabel(post.channel_id);
var when = document.createElement('span');
when.style.cssText = 'font-size:0.6875rem; color:var(--color-muted-foreground); flex-shrink:0;';
when.textContent = post.create_at ? new Date(post.create_at).toLocaleString() : '';
meta.appendChild(who);
meta.appendChild(when);
var msg = document.createElement('div');
msg.style.cssText = 'font-size:0.8125rem; color:var(--color-foreground); white-space:pre-wrap; word-break:break-word;';
msg.textContent = post.message || '';
row.appendChild(meta);
row.appendChild(msg);
el.appendChild(row);
});
}
function chatBuildMessageEl(post, context) {
var isMine = post.user_id === chatConfig.currentUserId;
var wrap = document.createElement('div');
+10
View File
@@ -36,7 +36,17 @@
<span style="font-size:0.875rem; color:var(--color-muted-foreground);">Оберіть канал зліва</span>
</div>
<div style="padding:0.5rem 1.25rem; border-bottom:1px solid var(--color-border); display:flex; gap:0.5rem; align-items:center;">
<input type="text" id="chat-search-input" placeholder="Пошук повідомлень..." class="input" style="flex:1;"
onkeydown="if(event.key==='Enter'){event.preventDefault();chatSearchMessages(this.value);}">
<button type="button" class="btn btn-ghost btn-icon" aria-label="Шукати"
onclick="chatSearchMessages(document.getElementById('chat-search-input').value)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
</button>
</div>
<div id="chat-messages" style="flex:1; overflow-y:auto; padding:1.25rem; display:flex; flex-direction:column; gap:1rem;"></div>
<div id="chat-search-results" style="display:none; flex:1; overflow-y:auto; padding:0.875rem 1.25rem; flex-direction:column; gap:0.5rem;"></div>
<div id="chat-composer-files" style="display:none; padding:0 1.25rem; flex-wrap:wrap; gap:0.375rem;"></div>