fix ui dm chatid lowercase normalization

This commit is contained in:
AidarKC 2026-06-21 12:27:41 +04:00
parent c8ffb6cf29
commit 2a834f1b14
6 changed files with 56 additions and 23 deletions

View File

@ -0,0 +1,19 @@
# Исправление chatId личных сообщений через lowercase
- краткое описание фичи:
- В клиентском UI SHiNE для личных сообщений технический `chatId` теперь канонизируется через `trim().toLowerCase()` при приёме DM, открытии чата и восстановлении сообщений из IndexedDB.
- Цель: исключить рассинхрон, когда unread-индикатор есть, а входящие сообщения конкретного собеседника не видны из-за разного регистра логина.
- что именно проверять:
- Отправить личные сообщения между двумя пользователями, у одного из которых логин отображается с заглавными буквами.
- Убедиться, что входящие сообщения показываются внутри открытого чата, а не только в общем unread-индикаторе.
- Перезагрузить страницу и проверить, что история чата после гидрации из IndexedDB остаётся в одном диалоге.
- Проверить, что переход в чат из списка диалогов и из графа связей открывает тот же диалог без дублирования.
- ожидаемый результат:
- Все сообщения одного собеседника попадают в один и тот же DM-чат независимо от регистра логина.
- Общий unread, список диалогов и содержимое открытого чата совпадают между собой.
- После перезагрузки UI не появляется отдельный дубль диалога с тем же логином в другом регистре.
- статус:
- pending

View File

@ -1,2 +1,2 @@
client.version=1.2.228
server.version=1.2.214
client.version=1.2.230
server.version=1.2.216

View File

@ -29,6 +29,7 @@ import {
addSignedMessageToChat,
markIncomingReadByBaseKey,
markOutgoingReadByBaseKey,
normalizeDmChatId,
setContacts,
} from './state.js';
@ -912,7 +913,7 @@ async function init() {
const fromLogin = parsed.fromLogin || '';
const toLogin = parsed.toLogin || '';
const messageType = Number(parsed.messageType || 0);
const chatId = messageType === 2 ? toLogin : fromLogin;
const chatId = normalizeDmChatId(messageType === 2 ? toLogin : fromLogin);
const text = (messageType === 1 || messageType === 2)
? String(parsed.text || '')
: '';

View File

@ -10,6 +10,7 @@ import {
markChatRead,
markOutgoingSent,
markReadReceiptSentByBaseKey,
normalizeDmChatId,
authService,
setContacts,
state,
@ -334,11 +335,12 @@ function renderLog(list, chatId, { onOpenActions } = {}) {
}
export function render({ navigate, route }) {
const chatId = route.params.chatId || 'u1';
const contact = directMessages.find((d) => d.id === chatId) || {
const routeChatId = route.params.chatId || 'u1';
const chatId = normalizeDmChatId(routeChatId) || 'u1';
const contact = directMessages.find((d) => normalizeDmChatId(d.id) === chatId) || {
id: chatId,
name: chatId,
initials: (chatId[0] || '?').toUpperCase(),
name: String(routeChatId || chatId),
initials: (String(routeChatId || chatId)[0] || '?').toUpperCase(),
};
const screen = document.createElement('section');

View File

@ -2,6 +2,7 @@ import { directMessages } from '../mock-data.js';
import {
getChatMessages,
isSessionInvalidError,
normalizeDmChatId,
setContacts,
state,
terminateCurrentSession,
@ -126,7 +127,7 @@ export function render({ navigate }) {
</div>
`;
row.prepend(avatarWrap);
row.addEventListener('click', () => navigate(`chat-view/${encodeURIComponent(item.id)}`));
row.addEventListener('click', () => navigate(`chat-view/${encodeURIComponent(normalizeDmChatId(item.id))}`));
return row;
}
@ -139,12 +140,13 @@ export function render({ navigate }) {
const contactRows = contacts.map((login) => {
const preview = directMessages.find((item) => item.id.toLowerCase() === login.toLowerCase());
const chat = getChatMessages(login);
const canonicalLogin = normalizeDmChatId(login);
const chat = getChatMessages(canonicalLogin);
const lastChat = chat[chat.length - 1];
const unread = chat.filter((m) => m?.from === 'in' && m?.unread).length;
const lastTimeMs = Number(lastChat?.createdAtMs || 0);
return {
id: login,
id: canonicalLogin,
name: preview?.name || login,
lastMessage: lastChat?.text || preview?.lastMessage || 'Диалог пока пуст.',
time: formatChatRowTime(lastTimeMs),

View File

@ -92,6 +92,10 @@ const DEFAULT_ARWEAVE_SERVER = 'https://arweave.net';
const DEFAULT_CALL_PREFLIGHT_TIMEOUT_MS = 6000;
const DEFAULT_OPENAI_BASE_URL = 'https://api.openai.com/v1';
export function normalizeDmChatId(value) {
return String(value || '').trim().toLowerCase();
}
function normalizeToolsSettings(rawTools) {
const source = rawTools && typeof rawTools === 'object' ? rawTools : {};
const stt = source.speechToText && typeof source.speechToText === 'object' ? source.speechToText : {};
@ -376,11 +380,12 @@ function sortChatMessagesInPlace(chatId) {
}
function persistMessageRecord(chatId, row) {
if (!chatId || !row?.messageKey) return;
const normalizedChatId = normalizeDmChatId(chatId);
if (!normalizedChatId || !row?.messageKey) return;
const resolvedTs = resolveChatMessageTimeMs(row);
void putStoredMessage({
messageKey: row.messageKey,
chatId,
chatId: normalizedChatId,
from: row.from || 'in',
text: String(row.text || ''),
baseKey: String(row.baseKey || ''),
@ -408,7 +413,7 @@ export async function hydrateMessagesFromStore() {
rows
.sort((a, b) => Number(a?.ts || 0) - Number(b?.ts || 0))
.forEach((row) => {
const chatId = String(row?.chatId || '').trim();
const chatId = normalizeDmChatId(row?.chatId);
const messageKey = String(row?.messageKey || '').trim();
if (!chatId || !messageKey) return;
if (state.knownMessageKeys[messageKey]) return;
@ -437,10 +442,12 @@ export async function hydrateMessagesFromStore() {
}
export function getChatMessages(chatId) {
if (!state.chats[chatId]) {
state.chats[chatId] = [];
const normalizedChatId = normalizeDmChatId(chatId);
if (!normalizedChatId) return [];
if (!state.chats[normalizedChatId]) {
state.chats[normalizedChatId] = [];
}
return state.chats[chatId];
return state.chats[normalizedChatId];
}
export function addChatMessage(chatId, text) {
@ -583,9 +590,10 @@ export function addSignedMessageToChat({
revisionTimeMs = 0,
deleted = false,
} = {}) {
const normalizedChatId = normalizeDmChatId(chatId);
const id = String(messageKey || '').trim();
if (!chatId || !id) return false;
const list = getChatMessages(chatId);
if (!normalizedChatId || !id) return false;
const list = getChatMessages(normalizedChatId);
const existingIndex = list.findIndex((row) => String(row?.messageKey || '').trim() === id);
const existing = existingIndex >= 0 ? list[existingIndex] : null;
const nextRevision = Number(revisionTimeMs || 0);
@ -599,7 +607,7 @@ export function addSignedMessageToChat({
if (existingIndex >= 0) {
list.splice(existingIndex, 1);
removeStoredMessageRecord(id);
sortChatMessagesInPlace(chatId);
sortChatMessagesInPlace(normalizedChatId);
return true;
}
return false;
@ -623,17 +631,18 @@ export function addSignedMessageToChat({
if (existingIndex < 0) {
list.push(row);
}
sortChatMessagesInPlace(chatId);
persistMessageRecord(chatId, row);
sortChatMessagesInPlace(normalizedChatId);
persistMessageRecord(normalizedChatId, row);
return true;
}
export function markChatRead(chatId) {
const list = getChatMessages(chatId);
const normalizedChatId = normalizeDmChatId(chatId);
const list = getChatMessages(normalizedChatId);
list.forEach((row) => {
if (row?.from === 'in') {
row.unread = false;
persistMessageRecord(chatId, row);
persistMessageRecord(normalizedChatId, row);
}
});
}