Чат: аватар собеседника в списке ЛС + переключатель комнат

- Аватар співрозмовника напроти особистого повідомлення (лівий список):
  chatBuildChannelRow отримав опцію avatar; id співрозмовника ЛС резолвиться
  в chatResolveDmLabel і підвантажує фото (chatApplyAvatar). Групові ЛС —
  без фото, з ініціалами.
- Верхній перемикач «кімнат» — команди Mattermost (Офіс / Аптеки / Склад):
  chatRenderTeamSwitcher/chatSelectTeam; chatRenderChannelList фільтрує канали
  за обраною командою; за замовчуванням — перша команда. ЛС показуються завжди.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-24 18:03:42 +03:00
parent 479fe3e0d7
commit 8a4577f165
2 changed files with 82 additions and 12 deletions
+79 -11
View File
@@ -23,6 +23,8 @@ var chatUploadedFileNames = {};
var chatFileBlobCache = {};
var chatDmPickerSelected = {};
var chatReactionPickerEl = null;
var chatCurrentTeamId = null; // обрана «кімната» (команда Mattermost)
var chatDmOtherUserId = {}; // channelId -> id співрозмовника (для аватара ЛС)
function chatPanelOpen(id) {
var panel = document.getElementById(id);
@@ -71,6 +73,8 @@ function chatInit() {
chatUploadedFileNames = {};
chatFileBlobCache = {};
chatDmPickerSelected = {};
chatCurrentTeamId = null;
chatDmOtherUserId = {};
chatLoadTeams().then(chatLoadChannels);
chatConnectSocket();
@@ -140,9 +144,43 @@ function chatResolveUsernames(posts) {
function chatLoadTeams() {
return chatApiFetch('/users/me/teams').then(function (teams) {
teams.forEach(function (team) { chatTeamsById[team.id] = team; });
var sorted = teams.slice().sort(function (a, b) { return a.display_name.localeCompare(b.display_name); });
if (!chatCurrentTeamId && sorted.length) chatCurrentTeamId = sorted[0].id;
chatRenderTeamSwitcher();
}).catch(function (e) { console.error(e); });
}
// Верхній перемикач «кімнат» — команди Mattermost (Офіс / Аптеки / Склад).
function chatRenderTeamSwitcher() {
var el = document.getElementById('chat-team-switcher');
if (!el) return;
var teams = Object.keys(chatTeamsById).map(function (id) { return chatTeamsById[id]; })
.sort(function (a, b) { return a.display_name.localeCompare(b.display_name); });
if (teams.length <= 1) { el.innerHTML = ''; return; } // перемикати нема що
el.innerHTML = '';
var group = document.createElement('div');
group.style.cssText = 'display:inline-flex; gap:0.25rem; background:var(--color-muted); border-radius:var(--radius); padding:0.25rem;';
teams.forEach(function (t) {
var active = t.id === chatCurrentTeamId;
var btn = document.createElement('button');
btn.type = 'button';
btn.textContent = t.display_name;
btn.style.cssText = 'padding:0.3125rem 0.875rem; font-size:0.8125rem; font-weight:500; border:none; cursor:pointer; border-radius:calc(var(--radius) - 2px); transition:all 150ms;' +
(active ? ' background:var(--color-card); color:var(--color-card-foreground); box-shadow:0 1px 2px rgb(0 0 0/0.08);'
: ' background:transparent; color:var(--color-muted-foreground);');
btn.onclick = function () { chatSelectTeam(t.id); };
group.appendChild(btn);
});
el.appendChild(group);
}
function chatSelectTeam(teamId) {
chatCurrentTeamId = teamId;
chatRenderTeamSwitcher();
chatRenderChannelList();
}
function chatLoadChannels() {
return chatApiFetch('/users/me/channels').then(function (channels) {
chatChannels = channels
@@ -161,8 +199,10 @@ function chatLoadChannels() {
function chatResolveDmLabel(channel) {
var promise;
var otherId = null;
if (channel.type === 'D') {
var otherId = channel.name.split('__').filter(function (id) { return id !== chatConfig.currentUserId; })[0];
otherId = channel.name.split('__').filter(function (id) { return id !== chatConfig.currentUserId; })[0];
chatDmOtherUserId[channel.id] = otherId;
promise = chatResolveUserIds([otherId]).then(function () {
return chatUserNames[otherId] || otherId;
});
@@ -178,6 +218,11 @@ function chatResolveDmLabel(channel) {
chatDmLabels[channel.id] = label;
var labelEl = document.getElementById('chat-dm-label-' + channel.id);
if (labelEl) labelEl.textContent = label;
var avEl = document.getElementById('chat-dm-avatar-' + channel.id);
if (avEl) {
avEl.textContent = (label || '').slice(0, 2).toUpperCase();
if (otherId) chatApplyAvatar(avEl, otherId); // фото — лише для особистих (не групових)
}
if (chatCurrentChannelId === channel.id) chatUpdateHeader(channel);
}).catch(function (e) { console.error(e); });
}
@@ -186,15 +231,19 @@ function chatRenderChannelList() {
var el = document.getElementById('chat-channel-list');
if (!el) return;
if (!chatChannels.length) {
var list = chatChannels.filter(function (ch) {
return !chatCurrentTeamId || ch.team_id === chatCurrentTeamId;
});
if (!list.length) {
el.innerHTML = '<div style="padding:1rem; text-align:center; font-size:0.8125rem; color:var(--color-muted-foreground);">Немає каналів</div>';
return;
}
el.innerHTML = '';
chatChannels.forEach(function (ch) {
var team = chatTeamsById[ch.team_id];
var row = chatBuildChannelRow(ch.id, '#' + ch.display_name, team ? team.display_name : '');
list.forEach(function (ch) {
// Команда обрана перемикачем зверху — назву команди у рядок не дублюємо.
var row = chatBuildChannelRow(ch.id, '#' + ch.display_name, '');
el.appendChild(row);
});
}
@@ -210,33 +259,52 @@ function chatRenderDmList() {
el.innerHTML = '';
chatDmChannels.forEach(function (ch) {
var row = chatBuildChannelRow(ch.id, chatDmLabels[ch.id] || '…', ch.type === 'G' ? 'Групова розмова' : '');
var row = chatBuildChannelRow(ch.id, chatDmLabels[ch.id] || '…',
ch.type === 'G' ? 'Групова розмова' : '', {avatar: true});
el.appendChild(row);
// Якщо співрозмовник ЛС уже відомий — одразу показати його аватар.
if (ch.type === 'D' && chatDmOtherUserId[ch.id]) {
chatApplyAvatar(document.getElementById('chat-dm-avatar-' + ch.id), chatDmOtherUserId[ch.id]);
}
});
}
function chatBuildChannelRow(channelId, label, subLabel) {
function chatBuildChannelRow(channelId, label, subLabel, opts) {
opts = opts || {};
var row = document.createElement('div');
row.id = 'chat-channel-row-' + channelId;
row.style.cssText = 'padding:0.5rem 0.625rem; border-bottom:1px solid var(--color-border); cursor:pointer;' +
row.style.cssText = 'display:flex; align-items:center; gap:0.5rem; padding:0.5rem 0.625rem; border-bottom:1px solid var(--color-border); cursor:pointer;' +
(channelId === chatCurrentChannelId ? ' background:hsl(0 72% 44% / 0.06);' : '');
row.onmouseover = function () { if (channelId !== chatCurrentChannelId) row.style.background = 'var(--color-muted)'; };
row.onmouseout = function () { if (channelId !== chatCurrentChannelId) row.style.background = ''; };
row.onclick = function () { chatSelectChannel(channelId); };
if (opts.avatar) {
// Кружок-аватар співрозмовника ЛС; фото підвантажиться в chatResolveDmLabel.
var av = document.createElement('div');
av.id = 'chat-dm-avatar-' + channelId;
av.style.cssText = 'width:1.75rem; height:1.75rem; border-radius:50%; background:hsl(205 80% 55% / 0.15); color:hsl(205 80% 45%); display:flex; align-items:center; justify-content:center; font-size:0.5625rem; font-weight:700; flex-shrink:0;';
av.textContent = (label && label !== '…') ? label.slice(0, 2).toUpperCase() : '';
row.appendChild(av);
}
var textCol = document.createElement('div');
textCol.style.cssText = 'min-width:0; flex:1;';
var name = document.createElement('div');
name.id = 'chat-dm-label-' + channelId;
name.style.cssText = 'font-size:0.8125rem; font-weight:500; color:var(--color-foreground);';
name.style.cssText = 'font-size:0.8125rem; font-weight:500; color:var(--color-foreground); overflow:hidden; text-overflow:ellipsis; white-space:nowrap;';
name.textContent = label;
row.appendChild(name);
textCol.appendChild(name);
if (subLabel) {
var sub = document.createElement('div');
sub.style.cssText = 'font-size:0.6875rem; color:var(--color-muted-foreground);';
sub.textContent = subLabel;
row.appendChild(sub);
textCol.appendChild(sub);
}
row.appendChild(textCol);
return row;
}
+3 -1
View File
@@ -8,7 +8,9 @@
{% if chat_state == 'ready' %}
{{ chat_config|json_script:"chat-config-data" }}
{{ chat_colleagues|json_script:"chat-colleagues-data" }}
<div id="chat-app" style="display:grid; grid-template-columns:260px 1fr; gap:1.25rem; height:calc(100vh - 12rem); max-height:640px;">
<!-- Перемикач «кімнат» (команди Mattermost: Офіс / Аптеки / Склад) -->
<div id="chat-team-switcher" style="margin-bottom:1rem;"></div>
<div id="chat-app" style="display:grid; grid-template-columns:260px 1fr; gap:1.25rem; height:calc(100vh - 15rem); max-height:640px;">
<div style="background:var(--color-card); border-radius:0.75rem; border:1px solid var(--color-border); box-shadow:0 1px 2px rgb(0 0 0/0.05); display:flex; flex-direction:column; overflow:hidden;">
<div style="padding:0.75rem; border-bottom:1px solid var(--color-border); display:flex; align-items:center; justify-content:space-between;">