Compare commits

..

3 Commits

7 changed files with 476 additions and 53 deletions
+75 -1
View File
@@ -1380,6 +1380,62 @@ function setupRendererConsoleLogging(win) {
});
}
function setupRendererLifecycleLogging(win) {
if (!debugEnabled()) return;
const logRendererLifecycle = (message) => {
logMain(`[renderer] ${message}`);
};
logRendererLifecycle(`window-created id=${win.id}`);
win.webContents.on("did-start-loading", () => {
logRendererLifecycle("did-start-loading");
});
win.webContents.on("dom-ready", () => {
logRendererLifecycle(`dom-ready url=${win.webContents.getURL()}`);
});
win.webContents.on("did-stop-loading", () => {
logRendererLifecycle("did-stop-loading");
});
win.webContents.on("did-finish-load", () => {
logRendererLifecycle(`did-finish-load url=${win.webContents.getURL()}`);
});
win.webContents.on("did-fail-load", (_event, errorCode, errorDescription, validatedURL, isMainFrame) => {
logRendererLifecycle(
`did-fail-load code=${errorCode} mainFrame=${!!isMainFrame} url=${validatedURL} error=${errorDescription}`
);
});
win.webContents.on("did-navigate", (_event, url, httpResponseCode, httpStatusText) => {
logRendererLifecycle(
`did-navigate url=${url} code=${httpResponseCode || 0} status=${httpStatusText || ""}`
);
});
win.webContents.on("did-navigate-in-page", (_event, url, isMainFrame) => {
logRendererLifecycle(`did-navigate-in-page mainFrame=${!!isMainFrame} url=${url}`);
});
win.webContents.on("render-process-gone", (_event, details) => {
logRendererLifecycle(
`render-process-gone reason=${details?.reason || ""} exitCode=${details?.exitCode ?? ""}`
);
});
win.on("unresponsive", () => {
logRendererLifecycle("window-unresponsive");
});
win.on("responsive", () => {
logRendererLifecycle("window-responsive");
});
}
function createMainWindow() {
const win = new BrowserWindow({
width: 1200,
@@ -1423,18 +1479,26 @@ function createMainWindow() {
});
setupRendererConsoleLogging(win);
setupRendererLifecycleLogging(win);
return win;
}
async function loadWithRetry(win, url) {
const startedAt = Date.now();
let attempt = 0;
// eslint-disable-next-line no-constant-condition
while (true) {
attempt += 1;
logMain(`[main] loadWithRetry attempt=${attempt} url=${url}`);
try {
await win.loadURL(url);
logMain(`[main] loadWithRetry success attempt=${attempt} elapsedMs=${Date.now() - startedAt} url=${url}`);
return;
} catch {
} catch (err) {
logMain(
`[main] loadWithRetry failure attempt=${attempt} elapsedMs=${Date.now() - startedAt} url=${url} error=${err?.message || err}`
);
if (Date.now() - startedAt > 60_000) throw new Error(`Failed to load URL in time: ${url}`);
await new Promise((r) => setTimeout(r, 500));
}
@@ -1502,6 +1566,15 @@ function registerWindowIpc() {
}
});
ipcMain.handle("app:isDebugEnabled", () => {
try {
return debugEnabled();
} catch (err) {
logMain(`[main] app:isDebugEnabled failed: ${err?.message || err}`);
return false;
}
});
ipcMain.handle("app:setCloseBehavior", (_event, behavior) => {
try {
const next = setCloseBehavior(behavior);
@@ -1727,6 +1800,7 @@ async function main() {
process.env.ELECTRON_START_URL ||
(app.isPackaged ? getBackendUiUrl() : "http://localhost:3000");
logMain(`[main] debugEnabled=${debugEnabled()} startUrl=${startUrl}`);
await loadWithRetry(win, startUrl);
// Auto-check updates after the UI has loaded (packaged builds only).
+1
View File
@@ -7,6 +7,7 @@ contextBridge.exposeInMainWorld("wechatDesktop", {
toggleMaximize: () => ipcRenderer.invoke("window:toggleMaximize"),
close: () => ipcRenderer.invoke("window:close"),
isMaximized: () => ipcRenderer.invoke("window:isMaximized"),
isDebugEnabled: () => ipcRenderer.invoke("app:isDebugEnabled"),
getAutoLaunch: () => ipcRenderer.invoke("app:getAutoLaunch"),
setAutoLaunch: (enabled) => ipcRenderer.invoke("app:setAutoLaunch", !!enabled),
+40 -40
View File
@@ -366,12 +366,12 @@
}
/* 统一特殊消息尾巴(红包 / 文件等) */
:deep(.wechat-special-card) {
.wechat-special-card {
position: relative;
overflow: visible;
}
:deep(.wechat-special-card)::after {
.wechat-special-card::after {
content: '';
position: absolute;
top: 12px;
@@ -383,7 +383,7 @@
border-radius: 2px;
}
:deep(.wechat-special-sent-side)::after {
.wechat-special-sent-side::after {
left: auto;
right: -4px;
}
@@ -754,7 +754,7 @@
}
/* 链接消息样式 - 微信风格 */
:deep(.wechat-link-card) {
.wechat-link-card {
width: 210px;
min-width: 210px;
max-width: 210px;
@@ -770,11 +770,11 @@
transition: background-color 0.15s ease;
}
:deep(.wechat-link-card:hover) {
.wechat-link-card:hover {
background: #f5f5f5;
}
:deep(.wechat-link-content) {
.wechat-link-content {
display: flex;
flex-direction: column;
gap: 8px;
@@ -783,14 +783,14 @@
flex: 1 1 auto;
}
:deep(.wechat-link-summary) {
.wechat-link-summary {
display: flex;
align-items: flex-start;
gap: 10px;
min-height: 42px;
}
:deep(.wechat-link-title) {
.wechat-link-title {
font-size: 14px;
color: #1a1a1a;
display: -webkit-box;
@@ -801,7 +801,7 @@
word-break: break-word;
}
:deep(.wechat-link-desc) {
.wechat-link-desc {
font-size: 12px;
color: #8c8c8c;
display: -webkit-box;
@@ -814,7 +814,7 @@
min-width: 0;
}
:deep(.wechat-link-thumb) {
.wechat-link-thumb {
width: 42px;
height: 42px;
flex: 0 0 auto;
@@ -824,19 +824,19 @@
align-self: flex-start;
}
:deep(.wechat-link-thumb-img) {
.wechat-link-thumb-img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
:deep(.wechat-link-card--mini-program) {
.wechat-link-card--mini-program {
max-height: 270px;
height: 270px;
}
:deep(.wechat-link-mini-body) {
.wechat-link-mini-body {
display: flex;
flex-direction: column;
gap: 10px;
@@ -846,14 +846,14 @@
min-height: 0;
}
:deep(.wechat-link-mini-header) {
.wechat-link-mini-header {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
:deep(.wechat-link-mini-header-avatar) {
.wechat-link-mini-header-avatar {
width: 20px;
height: 20px;
border-radius: 50%;
@@ -867,7 +867,7 @@
overflow: hidden;
}
:deep(.wechat-link-mini-header-avatar-img) {
.wechat-link-mini-header-avatar-img {
position: absolute;
inset: 0;
width: 100%;
@@ -876,7 +876,7 @@
display: block;
}
:deep(.wechat-link-mini-header-name) {
.wechat-link-mini-header-name {
font-size: 13px;
color: #7d7d7d;
overflow: hidden;
@@ -886,7 +886,7 @@
flex: 1 1 auto;
}
:deep(.wechat-link-mini-title) {
.wechat-link-mini-title {
font-size: 13px;
line-height: 1.45;
color: #1a1a1a;
@@ -897,7 +897,7 @@
word-break: break-word;
}
:deep(.wechat-link-mini-preview) {
.wechat-link-mini-preview {
width: 100%;
height: auto;
min-height: 0;
@@ -907,11 +907,11 @@
margin-top: auto;
}
:deep(.wechat-link-mini-preview--empty) {
.wechat-link-mini-preview--empty {
background: #f7f7f7;
}
:deep(.wechat-link-mini-preview-img) {
.wechat-link-mini-preview-img {
width: 100%;
height: 100%;
object-fit: contain;
@@ -919,7 +919,7 @@
display: block;
}
:deep(.wechat-link-mini-footer) {
.wechat-link-mini-footer {
height: 23px;
display: flex;
align-items: center;
@@ -930,7 +930,7 @@
flex-shrink: 0;
}
:deep(.wechat-link-mini-footer)::before {
.wechat-link-mini-footer::before {
content: '';
position: absolute;
top: 0;
@@ -940,19 +940,19 @@
background: #e8e8e8;
}
:deep(.wechat-link-mini-footer-icon) {
.wechat-link-mini-footer-icon {
width: 12px;
height: 12px;
object-fit: contain;
flex-shrink: 0;
}
:deep(.wechat-link-mini-footer-text) {
.wechat-link-mini-footer-text {
font-size: 10px;
color: #8c8c8c;
}
:deep(.wechat-link-from) {
.wechat-link-from {
height: 30px;
display: flex;
align-items: center;
@@ -962,7 +962,7 @@
flex-shrink: 0;
}
:deep(.wechat-link-from)::before {
.wechat-link-from::before {
content: '';
position: absolute;
top: 0;
@@ -972,7 +972,7 @@
background: #e8e8e8;
}
:deep(.wechat-link-from-avatar) {
.wechat-link-from-avatar {
width: 16px;
height: 16px;
border-radius: 50%;
@@ -986,7 +986,7 @@
overflow: hidden;
}
:deep(.wechat-link-from-avatar-img) {
.wechat-link-from-avatar-img {
position: absolute;
inset: 0;
width: 100%;
@@ -995,7 +995,7 @@
display: block;
}
:deep(.wechat-link-from-name) {
.wechat-link-from-name {
font-size: 12px;
color: #b2b2b2;
overflow: hidden;
@@ -1004,7 +1004,7 @@
}
/* 链接封面卡片(170x230 图 + 60 底栏) */
:deep(.wechat-link-card-cover) {
.wechat-link-card-cover {
width: 137px;
min-width: 137px;
max-width: 137px;
@@ -1020,11 +1020,11 @@
transition: background-color 0.15s ease;
}
:deep(.wechat-link-card-cover:hover) {
.wechat-link-card-cover:hover {
background: #f5f5f5;
}
:deep(.wechat-link-cover-image-wrap) {
.wechat-link-cover-image-wrap {
width: 137px;
height: 180px;
position: relative;
@@ -1034,7 +1034,7 @@
flex-shrink: 0;
}
:deep(.wechat-link-cover-image) {
.wechat-link-cover-image {
width: 100%;
height: 100%;
object-fit: cover;
@@ -1043,11 +1043,11 @@
}
/* 仅公众号封面卡片去掉菱形尖角,其它消息保持原样 */
:deep(.wechat-link-card-cover.wechat-special-card)::after {
.wechat-link-card-cover.wechat-special-card::after {
content: none !important;
}
:deep(.wechat-link-cover-from) {
.wechat-link-cover-from {
height: 30px;
display: flex;
align-items: center;
@@ -1062,7 +1062,7 @@
flex-shrink: 0;
}
:deep(.wechat-link-cover-from-avatar) {
.wechat-link-cover-from-avatar {
width: 18px;
height: 18px;
border-radius: 50%;
@@ -1076,7 +1076,7 @@
overflow: hidden;
}
:deep(.wechat-link-cover-from-avatar-img) {
.wechat-link-cover-from-avatar-img {
position: absolute;
inset: 0;
width: 100%;
@@ -1085,7 +1085,7 @@
display: block;
}
:deep(.wechat-link-cover-from-name) {
.wechat-link-cover-from-name {
font-size: 12px;
color: #f3f3f3;
overflow: hidden;
@@ -1093,7 +1093,7 @@
white-space: nowrap;
}
:deep(.wechat-link-cover-title) {
.wechat-link-cover-title {
height: 50px;
padding: 7px 10px 0;
box-sizing: border-box;
+2 -1
View File
@@ -1086,7 +1086,8 @@
<div
v-if="contextMenu.visible"
class="fixed z-[12000] bg-white border border-gray-200 rounded-md shadow-lg text-sm"
ref="contextMenuElement"
class="fixed z-[12000] max-h-[calc(100vh-16px)] overflow-y-auto bg-white border border-gray-200 rounded-md shadow-lg text-sm"
:style="{ left: contextMenu.x + 'px', top: contextMenu.y + 'px' }"
@click.stop
>
+48 -1
View File
@@ -1,4 +1,6 @@
import { ref, toRaw } from 'vue'
import { nextTick, ref, toRaw } from 'vue'
const CONTEXT_MENU_MARGIN = 8
const initialContextMenu = () => ({
visible: false,
@@ -45,6 +47,7 @@ export const useChatEditing = ({
locateMessageByServerId
}) => {
const contextMenu = ref(initialContextMenu())
const contextMenuElement = ref(null)
const messageEditModal = ref(initialMessageEditModal())
const messageFieldsModal = ref(initialMessageFieldsModal())
@@ -52,6 +55,44 @@ export const useChatEditing = ({
contextMenu.value = initialContextMenu()
}
const repositionContextMenu = () => {
if (!process.client || !contextMenu.value.visible) return
const menuEl = contextMenuElement.value
if (!menuEl) return
const rect = menuEl.getBoundingClientRect()
const viewportWidth = Math.max(window.innerWidth || 0, document.documentElement?.clientWidth || 0)
const viewportHeight = Math.max(window.innerHeight || 0, document.documentElement?.clientHeight || 0)
if (!viewportWidth || !viewportHeight) return
const maxX = Math.max(CONTEXT_MENU_MARGIN, viewportWidth - rect.width - CONTEXT_MENU_MARGIN)
const maxY = Math.max(CONTEXT_MENU_MARGIN, viewportHeight - rect.height - CONTEXT_MENU_MARGIN)
const currentX = Number(contextMenu.value.x || 0)
const currentY = Number(contextMenu.value.y || 0)
const nextX = Math.min(Math.max(currentX, CONTEXT_MENU_MARGIN), maxX)
const nextY = Math.min(Math.max(currentY, CONTEXT_MENU_MARGIN), maxY)
if (nextX !== currentX || nextY !== currentY) {
contextMenu.value = {
...contextMenu.value,
x: nextX,
y: nextY
}
}
}
const scheduleContextMenuReposition = () => {
if (!process.client) return
void nextTick(() => {
const run = () => repositionContextMenu()
if (typeof window.requestAnimationFrame === 'function') {
window.requestAnimationFrame(run)
} else {
run()
}
})
}
const loadContextMenuEditStatus = async (params) => {
if (!process.client) return
const account = String(params?.account || '').trim()
@@ -67,16 +108,19 @@ export const useChatEditing = ({
const current = String(contextMenu.value?.message?.id || '').trim()
if (contextMenu.value.visible && current === messageId) {
contextMenu.value.editStatus = response || { modified: false }
scheduleContextMenuReposition()
}
} catch {
const current = String(contextMenu.value?.message?.id || '').trim()
if (contextMenu.value.visible && current === messageId) {
contextMenu.value.editStatus = null
scheduleContextMenuReposition()
}
} finally {
const current = String(contextMenu.value?.message?.id || '').trim()
if (contextMenu.value.visible && current === messageId) {
contextMenu.value.editStatusLoading = false
scheduleContextMenuReposition()
}
}
}
@@ -126,6 +170,8 @@ export const useChatEditing = ({
void loadContextMenuEditStatus({ account, username, message_id: messageId })
}
} catch {}
scheduleContextMenuReposition()
}
const prettyJson = (value) => {
@@ -519,6 +565,7 @@ export const useChatEditing = ({
return {
contextMenu,
contextMenuElement,
messageEditModal,
messageFieldsModal,
closeContextMenu,
+106 -2
View File
@@ -27,6 +27,22 @@ export const useChatMessages = ({
const messageContainerRef = ref(null)
const activeMessagesFor = ref('')
const showJumpToBottom = ref(false)
let lastRenderMessagesFingerprint = ''
const isDesktopRenderer = () => {
if (!process.client || typeof window === 'undefined') return false
return !!window.wechatDesktop?.__brand
}
const logMessagePhase = (phase, details = {}) => {
if (!isDesktopRenderer()) return
console.info(`[chat-messages] ${phase}`, {
account: String(selectedAccount.value || '').trim(),
selectedUsername: String(selectedContact.value?.username || '').trim(),
activeMessagesFor: String(activeMessagesFor.value || '').trim(),
...details
})
}
const previewImageUrl = ref(null)
const previewVideoUrl = ref(null)
@@ -113,8 +129,16 @@ export const useChatMessages = ({
const renderMessages = computed(() => {
const list = messages.value || []
const reverseSides = !!reverseMessageSides.value
const fingerprint = `${String(selectedContact.value?.username || '').trim()}:${list.length}:${reverseSides ? '1' : '0'}`
const shouldLogRender = isDesktopRenderer() && fingerprint !== lastRenderMessagesFingerprint
if (shouldLogRender) {
logMessagePhase('renderMessages:start', {
count: list.length,
reverseSides
})
}
let previousTs = 0
return list.map((message) => {
const rendered = list.map((message) => {
const ts = Number(message.createTime || 0)
const show = !previousTs || (ts && Math.abs(ts - previousTs) >= 300)
if (ts) previousTs = ts
@@ -127,6 +151,14 @@ export const useChatMessages = ({
timeDivider: formatTimeDivider(ts)
}
})
if (shouldLogRender) {
lastRenderMessagesFingerprint = fingerprint
logMessagePhase('renderMessages:end', {
count: rendered.length,
reverseSides
})
}
return rendered
})
const updateJumpToBottomState = () => {
@@ -333,6 +365,10 @@ export const useChatMessages = ({
const loadMessages = async ({ username, reset }) => {
if (!username || !selectedAccount.value) return
logMessagePhase('loadMessages:enter', {
username,
reset
})
messagesError.value = ''
isLoadingMessages.value = true
activeMessagesFor.value = username
@@ -357,13 +393,47 @@ export const useChatMessages = ({
if (realtimeEnabled.value) {
params.source = 'realtime'
}
logMessagePhase('loadMessages:request:start', {
username,
reset,
offset,
existingCount: existing.length,
renderTypeFilter: messageTypeFilter.value,
realtime: !!realtimeEnabled.value
})
const response = await api.listChatMessages(params)
logMessagePhase('loadMessages:request:end', {
username,
reset,
rawCount: Array.isArray(response?.messages) ? response.messages.length : 0,
total: Number(response?.total || 0),
hasMore: response?.hasMore
})
const raw = response?.messages || []
logMessagePhase('loadMessages:normalize:start', {
username,
rawCount: raw.length
})
const mapped = dedupeMessagesById(raw.map(normalizeMessage))
logMessagePhase('loadMessages:normalize:end', {
username,
mappedCount: mapped.length
})
if (activeMessagesFor.value !== username) return
if (activeMessagesFor.value !== username) {
logMessagePhase('loadMessages:abort-stale', {
username,
activeMessagesFor: activeMessagesFor.value
})
return
}
logMessagePhase('loadMessages:state-commit:start', {
username,
reset,
mappedCount: mapped.length
})
if (reset) {
allMessages.value = { ...allMessages.value, [username]: mapped }
} else {
@@ -380,6 +450,10 @@ export const useChatMessages = ({
[username]: [...older, ...existing]
}
}
logMessagePhase('loadMessages:state-commit:end', {
username,
storedCount: (allMessages.value[username] || []).length
})
messagesMeta.value = {
...messagesMeta.value,
@@ -388,8 +462,20 @@ export const useChatMessages = ({
hasMore: response?.hasMore
}
}
logMessagePhase('loadMessages:meta-commit:end', {
username,
total: Number(response?.total || 0),
hasMore: response?.hasMore
})
logMessagePhase('loadMessages:nextTick:start', {
username
})
await nextTick()
logMessagePhase('loadMessages:nextTick:end', {
username,
renderedCount: (allMessages.value[username] || []).length
})
const nextContainer = messageContainerRef.value
if (nextContainer) {
if (reset) {
@@ -400,10 +486,28 @@ export const useChatMessages = ({
}
}
updateJumpToBottomState()
logMessagePhase('loadMessages:scroll:end', {
username,
hasContainer: !!nextContainer,
scrollTop: nextContainer ? nextContainer.scrollTop : null,
scrollHeight: nextContainer ? nextContainer.scrollHeight : null
})
} catch (error) {
console.error('[chat-messages] loadMessages:error', {
account: String(selectedAccount.value || '').trim(),
username: String(username || '').trim(),
reset: !!reset,
error
})
messagesError.value = error?.message || '加载聊天记录失败'
} finally {
isLoadingMessages.value = false
logMessagePhase('loadMessages:exit', {
username,
reset,
loading: isLoadingMessages.value,
error: messagesError.value
})
}
}
+204 -8
View File
@@ -58,6 +58,64 @@ const routeUsername = computed(() => {
return (Array.isArray(raw) ? raw[0] : raw) || ''
})
const isDesktopShell = () => {
if (!process.client || typeof window === 'undefined') return false
return !!window.wechatDesktop?.__brand
}
const desktopDebugEnabled = ref(false)
const chatBootstrapStartedAt = process.client && typeof performance !== 'undefined' ? performance.now() : 0
let messageLoadSequence = 0
let firstSelectContactLogged = false
let firstLoadMessagesLogged = false
const resolveDesktopDebugEnabled = async () => {
if (!isDesktopShell() || typeof window.wechatDesktop?.isDebugEnabled !== 'function') {
desktopDebugEnabled.value = false
return false
}
try {
desktopDebugEnabled.value = !!(await window.wechatDesktop.isDebugEnabled())
} catch {
desktopDebugEnabled.value = false
}
return desktopDebugEnabled.value
}
const chatBootstrapElapsedMs = () => {
if (!process.client || typeof performance === 'undefined') return null
const elapsed = performance.now() - chatBootstrapStartedAt
return Number.isFinite(elapsed) ? Number(elapsed.toFixed(1)) : null
}
const shouldLogChatBootstrap = () => isDesktopShell() || desktopDebugEnabled.value
const logChatBootstrap = (phase, details = {}) => {
if (!shouldLogChatBootstrap()) return
console.info(`[chat-bootstrap] ${phase}`, {
elapsedMs: chatBootstrapElapsedMs(),
route: route.fullPath,
...details
})
}
const waitForNextPaint = async () => {
await nextTick()
if (!process.client || typeof window === 'undefined') return
await new Promise((resolve) => {
window.requestAnimationFrame(() => {
window.setTimeout(resolve, 0)
})
})
}
const nextMessageLoadToken = () => {
messageLoadSequence += 1
return messageLoadSequence
}
const buildChatPath = (username) => {
return username ? `/chat/${encodeURIComponent(username)}` : '/chat'
}
@@ -184,17 +242,83 @@ const {
let exitSearchContext = async () => {}
const runMessageLoad = async ({ username, reset = true, deferUntilPaint = false, reason = '', token = nextMessageLoadToken() } = {}) => {
const nextUsername = String(username || '').trim()
if (!nextUsername) return false
if (deferUntilPaint) {
logChatBootstrap('loadMessages:scheduled', {
username: nextUsername,
reason,
token
})
await waitForNextPaint()
if (token !== messageLoadSequence) {
logChatBootstrap('loadMessages:skipped-stale', {
username: nextUsername,
reason,
token
})
return false
}
}
const isFirstLoad = !firstLoadMessagesLogged
if (isFirstLoad) {
firstLoadMessagesLogged = true
}
logChatBootstrap(isFirstLoad ? 'loadMessages:first:start' : 'loadMessages:start', {
username: nextUsername,
reason,
token,
reset
})
await loadMessages({ username: nextUsername, reset })
logChatBootstrap(isFirstLoad ? 'loadMessages:first:end' : 'loadMessages:end', {
username: nextUsername,
reason,
token,
renderedMessages: messages.value.length
})
return true
}
const selectContact = async (contact, options = {}) => {
if (!contact) return
const selectionReason = String(options.reason || 'manual-select').trim() || 'manual-select'
const loadToken = nextMessageLoadToken()
const nextUsername = contact?.username || ''
if (searchContext.value?.active && searchContext.value.username && searchContext.value.username !== nextUsername) {
await exitSearchContext()
}
const isFirstSelect = !firstSelectContactLogged
if (isFirstSelect) {
firstSelectContactLogged = true
}
logChatBootstrap(isFirstSelect ? 'selectContact:first' : 'selectContact', {
username: nextUsername,
reason: selectionReason,
deferLoadMessages: !!options.deferLoadMessages,
skipLoadMessages: !!options.skipLoadMessages,
syncRoute: options.syncRoute !== false
})
selectedContact.value = contact
if (!nextUsername) return
if (!options.skipLoadMessages) {
loadMessages({ username: nextUsername, reset: true })
void runMessageLoad({
username: nextUsername,
reset: true,
deferUntilPaint: !!options.deferLoadMessages,
reason: selectionReason,
token: loadToken
})
}
if (options.syncRoute !== false && nextUsername) {
@@ -205,24 +329,34 @@ const selectContact = async (contact, options = {}) => {
}
}
const applyRouteSelection = async () => {
const applyRouteSelection = async (options = {}) => {
if (!contacts.value || contacts.value.length === 0) {
selectedContact.value = null
return
}
const selectionReason = String(options.reason || 'route-selection').trim() || 'route-selection'
const requested = routeUsername.value || ''
if (requested) {
const matched = contacts.value.find((contact) => contact.username === requested)
if (matched) {
if (selectedContact.value?.username !== matched.username) {
await selectContact(matched, { syncRoute: false })
await selectContact(matched, {
syncRoute: false,
deferLoadMessages: !!options.deferLoadMessages,
reason: `${selectionReason}:matched-route`
})
}
return
}
}
await selectContact(contacts.value[0], { syncRoute: true, replaceRoute: true })
await selectContact(contacts.value[0], {
syncRoute: true,
replaceRoute: true,
deferLoadMessages: !!options.deferLoadMessages,
reason: `${selectionReason}:fallback-first-contact`
})
}
const searchState = useChatSearch({
@@ -363,6 +497,9 @@ const queueRealtimeSessionsRefresh = () => {
}
const onAccountChange = async () => {
logChatBootstrap('accountChange:start', {
selectedAccount: selectedAccount.value
})
try {
isLoadingContacts.value = true
contactsError.value = ''
@@ -374,7 +511,18 @@ const onAccountChange = async () => {
}
resetAccountScopedState()
await applyRouteSelection()
logChatBootstrap('accountChange:applyRouteSelection:start', {
selectedAccount: selectedAccount.value,
contactCount: contacts.value.length
})
await applyRouteSelection({
reason: 'account-change'
})
logChatBootstrap('accountChange:end', {
selectedAccount: selectedAccount.value,
selectedUsername: selectedContact.value?.username || '',
contactCount: contacts.value.length
})
}
const onGlobalClick = (event) => {
@@ -420,6 +568,13 @@ const onGlobalKeyDown = (event) => {
onMounted(async () => {
if (!process.client) return
await resolveDesktopDebugEnabled()
logChatBootstrap('route mount start', {
requestedUsername: routeUsername.value,
selectedAccount: selectedAccount.value,
desktopShell: isDesktopShell()
})
document.addEventListener('click', onGlobalClick)
document.addEventListener('keydown', onGlobalKeyDown)
document.addEventListener('mousemove', onFloatingWindowMouseMove)
@@ -428,9 +583,43 @@ onMounted(async () => {
document.addEventListener('touchend', onFloatingWindowMouseUp)
document.addEventListener('touchcancel', onFloatingWindowMouseUp)
logChatBootstrap('loadContacts:start', {
selectedAccount: selectedAccount.value
})
await loadContacts()
await applyRouteSelection()
logChatBootstrap('loadContacts:end', {
selectedAccount: selectedAccount.value,
contactCount: contacts.value.length
})
const deferInitialConversationBoot = isDesktopShell()
await waitForNextPaint()
logChatBootstrap('first render completion', {
contactCount: contacts.value.length,
deferInitialConversationBoot
})
logChatBootstrap('applyRouteSelection:start', {
requestedUsername: routeUsername.value,
deferLoadMessages: deferInitialConversationBoot
})
await applyRouteSelection({
deferLoadMessages: deferInitialConversationBoot,
reason: deferInitialConversationBoot ? 'initial-route-post-paint' : 'initial-route'
})
logChatBootstrap('applyRouteSelection:end', {
selectedUsername: selectedContact.value?.username || '',
requestedUsername: routeUsername.value
})
logChatBootstrap('tryEnableRealtimeAuto:start', {
selectedAccount: selectedAccount.value,
realtimeEnabled: realtimeEnabled.value
})
await tryEnableRealtimeAuto()
logChatBootstrap('tryEnableRealtimeAuto:end', {
realtimeEnabled: realtimeEnabled.value
})
})
onUnmounted(() => {
@@ -488,11 +677,17 @@ watch(messageTypeFilter, async (next, prev) => {
watch(
routeUsername,
async () => {
async (next, prev) => {
if (!process.client) return
if (isLoadingContacts.value) return
if (!contacts.value.length) return
await applyRouteSelection()
logChatBootstrap('routeUsername:change', {
previousUsername: prev || '',
nextUsername: next || ''
})
await applyRouteSelection({
reason: 'route-watch'
})
}
)
@@ -502,6 +697,7 @@ const chatState = {
availableAccounts,
contacts,
selectedContact,
searchContext,
filteredContacts,
searchQuery,
showSearchAccountSwitcher,