diff --git a/shine-UI/js/pages/chat-view.js b/shine-UI/js/pages/chat-view.js
index 71dd9512..ef940ba6 100644
--- a/shine-UI/js/pages/chat-view.js
+++ b/shine-UI/js/pages/chat-view.js
@@ -128,7 +128,7 @@ function createChatHeaderParts(login, navigate) {
loginEl.setAttribute('role', 'heading');
loginEl.setAttribute('aria-level', '1');
loginEl.setAttribute('aria-label', `Чат с ${cleanLogin}`);
- loginEl.innerHTML = ``;
+ loginEl.innerHTML = ``;
void loadProfileSnapshot(cleanLogin)
.then((snapshot) => {
@@ -153,7 +153,10 @@ function createChatHeaderParts(login, navigate) {
const nameNode = loginEl.querySelector('.chat-header-display-name');
const loginNode = loginEl.querySelector('.chat-header-user-login');
if (nameNode) nameNode.textContent = display;
- if (loginNode) loginNode.textContent = cleanLogin;
+ if (loginNode) {
+ loginNode.textContent = cleanLogin;
+ loginNode.hidden = display === cleanLogin;
+ }
}
})
.catch(() => {});
@@ -967,7 +970,6 @@ export function render({ navigate, route, chrome }) {
const screen = document.createElement('section');
screen.className = 'stack dm-screen dm-chat-screen';
const isTextToSpeechReady = isTextToSpeechConfigured(state.entrySettings);
- const isKnownContact = (state.contacts || []).some((x) => String(x || '').toLowerCase() === String(chatId || '').toLowerCase());
const hasUnreadIncoming = getChatMessages(chatId).some((msg) => msg?.from === 'in' && msg?.unread);
const UNREAD_SEPARATOR_AUTO_HIDE_MS = 5000;
let historyHasMore = true;
@@ -1151,44 +1153,59 @@ export function render({ navigate, route, chrome }) {
chatHeaderLeft?.append(chatHeaderParts.avatarButton);
chrome?.setTopbar(chatHeader);
- if (!isKnownContact) {
- const card = document.createElement('div');
- card.className = 'card';
- const btn = document.createElement('button');
- btn.className = 'secondary-btn';
- btn.type = 'button';
- btn.textContent = 'Добавить собеседника в контакты';
- btn.addEventListener('click', async () => {
- try {
- const approved = await openConfirmContactModal(chatId);
- if (!approved) return;
- await authService.setUserRelation({
- login: state.session.login,
- toLogin: chatId,
- kind: 'contact',
- enabled: true,
- storagePwd: state.session.storagePwdInMemory,
- });
- const contactsPayload = await authService.listContacts();
- setContacts(contactsPayload?.contacts || []);
- addAppLogEntry({
- level: 'info',
- source: 'contacts',
- message: `Пользователь ${chatId} добавлен в контакты`,
- });
- card.remove();
- } catch (e) {
- addAppLogEntry({
- level: 'warn',
- source: 'contacts',
- message: 'Не удалось добавить пользователя в контакты',
- details: { login: chatId, error: e?.message || 'unknown' },
- });
- }
+ const addContactCard = document.createElement('div');
+ addContactCard.className = 'card dm-add-contact-card';
+ addContactCard.hidden = true;
+ const addContactBtn = document.createElement('button');
+ addContactBtn.className = 'secondary-btn';
+ addContactBtn.type = 'button';
+ addContactBtn.textContent = 'Добавить собеседника в контакты';
+ addContactBtn.addEventListener('click', async () => {
+ try {
+ const approved = await openConfirmContactModal(chatId);
+ if (!approved) return;
+ await authService.setUserRelation({
+ login: state.session.login,
+ toLogin: chatId,
+ kind: 'contact',
+ enabled: true,
+ storagePwd: state.session.storagePwdInMemory,
+ });
+ const contactsPayload = await authService.listContacts();
+ setContacts(contactsPayload?.contacts || []);
+ addAppLogEntry({
+ level: 'info',
+ source: 'contacts',
+ message: `Пользователь ${chatId} добавлен в контакты`,
+ });
+ addContactCard.remove();
+ } catch (e) {
+ addAppLogEntry({
+ level: 'warn',
+ source: 'contacts',
+ message: 'Не удалось добавить пользователя в контакты',
+ details: { login: chatId, error: e?.message || 'unknown' },
+ });
+ }
+ });
+ addContactCard.append(addContactBtn);
+ log.prepend(addContactCard);
+
+ // The button is valid only when there is no social relation at all.
+ // listContacts already returns relationFlag for every dialog/peer, so no extra server API is needed.
+ void authService.listContacts()
+ .then((contactsPayload) => {
+ setContacts(contactsPayload?.contacts || []);
+ const peerKey = String(chatId || '').trim().toLowerCase();
+ const peerDialog = (Array.isArray(contactsPayload?.dialogs) ? contactsPayload.dialogs : [])
+ .find((dialog) => String(dialog?.peerLogin || '').trim().toLowerCase() === peerKey);
+ const relationFlag = String(peerDialog?.relationFlag || 'none').trim().toLowerCase();
+ addContactCard.hidden = relationFlag !== 'none';
+ })
+ .catch(() => {
+ // On an API failure do not offer a potentially invalid relation action.
+ addContactCard.hidden = true;
});
- card.append(btn);
- screen.append(card);
- }
const form = document.createElement('form');
form.className = 'chat-input dm-chat-input';
diff --git a/shine-UI/js/pages/messages-list.js b/shine-UI/js/pages/messages-list.js
index d92fb8d1..d9fcce62 100644
--- a/shine-UI/js/pages/messages-list.js
+++ b/shine-UI/js/pages/messages-list.js
@@ -319,7 +319,7 @@ function renderRow(item) {
const titleEl = row.querySelector('.dm-row-title');
const previewEl = row.querySelector('.dm-row-last-message');
const timeEl = row.querySelector('.dm-row-time');
- if (titleEl) titleEl.textContent = userDisplayName(item);
+ if (titleEl) titleEl.textContent = userDisplayName({ login: item.peerLogin, firstName: item.firstName, lastName: item.lastName });
if (previewEl) previewEl.textContent = 'Загрузка…';
if (timeEl) timeEl.textContent = formatChatRowTime(item.lastMessageTimeMs);
row.prepend(avatarWrap);
diff --git a/shine-UI/js/services/user-profile-params.js b/shine-UI/js/services/user-profile-params.js
index 33dd57b1..58408d2b 100644
--- a/shine-UI/js/services/user-profile-params.js
+++ b/shine-UI/js/services/user-profile-params.js
@@ -116,6 +116,9 @@ export async function loadProfileSnapshot(login) {
});
}
+ const firstName = String(fields.find((field) => field.key === 'first_name')?.value || '').trim();
+ const lastName = String(fields.find((field) => field.key === 'last_name')?.value || '').trim();
+
const latestAccountRole = loadLatestByAliasesFromItems(items, ['account_role']);
const rawAccountRole = String(latestAccountRole?.value || '').trim().toLowerCase();
const accountRole = rawAccountRole === PROFILE_ACCOUNT_ROLE_PRIMARY || rawAccountRole === PROFILE_ACCOUNT_ROLE_NON_VOTING
@@ -157,6 +160,8 @@ export async function loadProfileSnapshot(login) {
return {
fields,
+ firstName,
+ lastName,
toggles,
accountRole,
accountRoleTimeMs: latestAccountRole?.timeMs || 0,
diff --git a/shine-UI/styles/components.css b/shine-UI/styles/components.css
index 4e4d3f7c..81f456f2 100644
--- a/shine-UI/styles/components.css
+++ b/shine-UI/styles/components.css
@@ -6468,6 +6468,14 @@ html, body { overflow-x: hidden; }
gap: 10px;
}
+.dm-add-contact-card {
+ margin: 0 14px 2px;
+}
+
+.dm-add-contact-card .secondary-btn {
+ width: 100%;
+}
+
.dm-history-loader {
position: sticky;
top: 0;
@@ -11378,3 +11386,17 @@ body.chat-topbar-overlay .composer-slot {
font-size: 9px;
}
}
+
+/* ===== Channel list preview/time flex sizing fix (2026-09-02) =====
+ * The global .channel-row-time rule still has width:100%, which makes the
+ * timestamp consume the entire flex row and collapses the message preview.
+ * Keep the timestamp content-sized and reserve a little more room at the
+ * right edge so the final digit is never clipped by the widened list tile. */
+.channels-screen--list .channel-row {
+ padding-right: 14px !important;
+}
+
+.channels-screen--list .channel-row-preview-line .channel-row-time {
+ width: auto !important;
+ max-width: none !important;
+}