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 client.version=1.2.230
server.version=1.2.214 server.version=1.2.216

View File

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

View File

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

View File

@ -2,6 +2,7 @@ import { directMessages } from '../mock-data.js';
import { import {
getChatMessages, getChatMessages,
isSessionInvalidError, isSessionInvalidError,
normalizeDmChatId,
setContacts, setContacts,
state, state,
terminateCurrentSession, terminateCurrentSession,
@ -126,7 +127,7 @@ export function render({ navigate }) {
</div> </div>
`; `;
row.prepend(avatarWrap); row.prepend(avatarWrap);
row.addEventListener('click', () => navigate(`chat-view/${encodeURIComponent(item.id)}`)); row.addEventListener('click', () => navigate(`chat-view/${encodeURIComponent(normalizeDmChatId(item.id))}`));
return row; return row;
} }
@ -139,12 +140,13 @@ export function render({ navigate }) {
const contactRows = contacts.map((login) => { const contactRows = contacts.map((login) => {
const preview = directMessages.find((item) => item.id.toLowerCase() === login.toLowerCase()); 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 lastChat = chat[chat.length - 1];
const unread = chat.filter((m) => m?.from === 'in' && m?.unread).length; const unread = chat.filter((m) => m?.from === 'in' && m?.unread).length;
const lastTimeMs = Number(lastChat?.createdAtMs || 0); const lastTimeMs = Number(lastChat?.createdAtMs || 0);
return { return {
id: login, id: canonicalLogin,
name: preview?.name || login, name: preview?.name || login,
lastMessage: lastChat?.text || preview?.lastMessage || 'Диалог пока пуст.', lastMessage: lastChat?.text || preview?.lastMessage || 'Диалог пока пуст.',
time: formatChatRowTime(lastTimeMs), 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_CALL_PREFLIGHT_TIMEOUT_MS = 6000;
const DEFAULT_OPENAI_BASE_URL = 'https://api.openai.com/v1'; const DEFAULT_OPENAI_BASE_URL = 'https://api.openai.com/v1';
export function normalizeDmChatId(value) {
return String(value || '').trim().toLowerCase();
}
function normalizeToolsSettings(rawTools) { function normalizeToolsSettings(rawTools) {
const source = rawTools && typeof rawTools === 'object' ? rawTools : {}; const source = rawTools && typeof rawTools === 'object' ? rawTools : {};
const stt = source.speechToText && typeof source.speechToText === 'object' ? source.speechToText : {}; const stt = source.speechToText && typeof source.speechToText === 'object' ? source.speechToText : {};
@ -376,11 +380,12 @@ function sortChatMessagesInPlace(chatId) {
} }
function persistMessageRecord(chatId, row) { function persistMessageRecord(chatId, row) {
if (!chatId || !row?.messageKey) return; const normalizedChatId = normalizeDmChatId(chatId);
if (!normalizedChatId || !row?.messageKey) return;
const resolvedTs = resolveChatMessageTimeMs(row); const resolvedTs = resolveChatMessageTimeMs(row);
void putStoredMessage({ void putStoredMessage({
messageKey: row.messageKey, messageKey: row.messageKey,
chatId, chatId: normalizedChatId,
from: row.from || 'in', from: row.from || 'in',
text: String(row.text || ''), text: String(row.text || ''),
baseKey: String(row.baseKey || ''), baseKey: String(row.baseKey || ''),
@ -408,7 +413,7 @@ export async function hydrateMessagesFromStore() {
rows rows
.sort((a, b) => Number(a?.ts || 0) - Number(b?.ts || 0)) .sort((a, b) => Number(a?.ts || 0) - Number(b?.ts || 0))
.forEach((row) => { .forEach((row) => {
const chatId = String(row?.chatId || '').trim(); const chatId = normalizeDmChatId(row?.chatId);
const messageKey = String(row?.messageKey || '').trim(); const messageKey = String(row?.messageKey || '').trim();
if (!chatId || !messageKey) return; if (!chatId || !messageKey) return;
if (state.knownMessageKeys[messageKey]) return; if (state.knownMessageKeys[messageKey]) return;
@ -437,10 +442,12 @@ export async function hydrateMessagesFromStore() {
} }
export function getChatMessages(chatId) { export function getChatMessages(chatId) {
if (!state.chats[chatId]) { const normalizedChatId = normalizeDmChatId(chatId);
state.chats[chatId] = []; if (!normalizedChatId) return [];
if (!state.chats[normalizedChatId]) {
state.chats[normalizedChatId] = [];
} }
return state.chats[chatId]; return state.chats[normalizedChatId];
} }
export function addChatMessage(chatId, text) { export function addChatMessage(chatId, text) {
@ -583,9 +590,10 @@ export function addSignedMessageToChat({
revisionTimeMs = 0, revisionTimeMs = 0,
deleted = false, deleted = false,
} = {}) { } = {}) {
const normalizedChatId = normalizeDmChatId(chatId);
const id = String(messageKey || '').trim(); const id = String(messageKey || '').trim();
if (!chatId || !id) return false; if (!normalizedChatId || !id) return false;
const list = getChatMessages(chatId); const list = getChatMessages(normalizedChatId);
const existingIndex = list.findIndex((row) => String(row?.messageKey || '').trim() === id); const existingIndex = list.findIndex((row) => String(row?.messageKey || '').trim() === id);
const existing = existingIndex >= 0 ? list[existingIndex] : null; const existing = existingIndex >= 0 ? list[existingIndex] : null;
const nextRevision = Number(revisionTimeMs || 0); const nextRevision = Number(revisionTimeMs || 0);
@ -599,7 +607,7 @@ export function addSignedMessageToChat({
if (existingIndex >= 0) { if (existingIndex >= 0) {
list.splice(existingIndex, 1); list.splice(existingIndex, 1);
removeStoredMessageRecord(id); removeStoredMessageRecord(id);
sortChatMessagesInPlace(chatId); sortChatMessagesInPlace(normalizedChatId);
return true; return true;
} }
return false; return false;
@ -623,17 +631,18 @@ export function addSignedMessageToChat({
if (existingIndex < 0) { if (existingIndex < 0) {
list.push(row); list.push(row);
} }
sortChatMessagesInPlace(chatId); sortChatMessagesInPlace(normalizedChatId);
persistMessageRecord(chatId, row); persistMessageRecord(normalizedChatId, row);
return true; return true;
} }
export function markChatRead(chatId) { export function markChatRead(chatId) {
const list = getChatMessages(chatId); const normalizedChatId = normalizeDmChatId(chatId);
const list = getChatMessages(normalizedChatId);
list.forEach((row) => { list.forEach((row) => {
if (row?.from === 'in') { if (row?.from === 'in') {
row.unread = false; row.unread = false;
persistMessageRecord(chatId, row); persistMessageRecord(normalizedChatId, row);
} }
}); });
} }