Skip to content

Commit 8991e15

Browse files
committed
feat(chat-list): add click-to-select functionality for chats
- Enable mouse click on chat list items to open conversations (using OpenTUI's onMouse callback) - Fix "two actives" bug where both selected and clicked chats showed highlights - Add updateSelectionAndActive() method to properly update all row styling when using cached list - Convert dynamic imports to static imports for cleaner code: - ChatListManager.ts: destroyConversationScrollBox - SessionCreate.ts: showQRCode - index.ts: setRenderer, showQRCode, chatListManager, getClient
1 parent debb32c commit 8991e15

3 files changed

Lines changed: 78 additions & 29 deletions

File tree

src/index.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ import { filterChats, isArchived } from "./utils/filterChats"
3232
import { pollingService } from "./services/PollingService"
3333
import { ConfigView } from "./views/ConfigView"
3434
import type { CliRenderer } from "@opentui/core"
35+
import { setRenderer } from "./state/RendererContext"
36+
import { showQRCode } from "./views/QRCodeView"
37+
import { chatListManager } from "./views/ChatListManager"
38+
import { getClient } from "./client"
3539

3640
/**
3741
* Run the configuration wizard using the TUI
@@ -131,7 +135,6 @@ async function main() {
131135
const renderer = await createCliRenderer({ exitOnCtrlC: true })
132136

133137
// Set renderer context for imperative API usage
134-
const { setRenderer } = await import("./state/RendererContext")
135138
setRenderer(renderer)
136139

137140
let config: WahaTuiConfig | null = null
@@ -193,7 +196,7 @@ async function main() {
193196

194197
// Fetch WAHA version and tier info
195198
try {
196-
const client = (await import("./client")).getClient()
199+
const client = getClient()
197200
const { data: versionInfo } = await client.observability.versionControllerGet()
198201
if (versionInfo?.tier) {
199202
appState.setState({ wahaTier: versionInfo.tier })
@@ -226,13 +229,9 @@ async function main() {
226229
appState.setCurrentSession(DEFAULT_SESSION)
227230
appState.setCurrentView("qr")
228231
// Trigger QR code loading
229-
const { showQRCode } = await import("./views/QRCodeView")
230232
await showQRCode(DEFAULT_SESSION)
231233
}
232234

233-
// Import ChatListManager for optimized rendering
234-
const { chatListManager } = await import("./views/ChatListManager")
235-
236235
// Set up reactive rendering
237236
function renderApp(forceRebuild: boolean = false) {
238237
const state = appState.getState()

src/views/ChatListManager.ts

Lines changed: 72 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
} from "../utils/formatters"
2323
import { debugLog } from "../utils/debug"
2424
import { appState } from "../state/AppState"
25-
import { loadMessages, loadContacts } from "./ConversationView"
25+
import { loadMessages, loadContacts, destroyConversationScrollBox } from "./ConversationView"
2626
import { ROW_HEIGHT } from "../utils/chatListScroll"
2727

2828
interface ChatRowData {
@@ -88,6 +88,9 @@ class ChatListManager {
8888
// CASE 1: exact same content (no changes)
8989
if (this.scrollBox && newContentHash === this.currentChatsHash) {
9090
debugLog("ChatListManager", "Using cached chat list (exact match)")
91+
// Still need to update selection/active styling as those may have changed
92+
const state = appState.getState()
93+
this.updateSelectionAndActive(state.selectedChatIndex, state.currentChatId, chats)
9194
return this.scrollBox
9295
}
9396

@@ -188,6 +191,10 @@ class ChatListManager {
188191
}
189192
}
190193

194+
// Capture index and chat reference for click handler closure
195+
const chatIndex = index
196+
const chatRef = chat
197+
191198
// Create chat row box
192199
// Use simple ID to enable in-place updates (no hash in ID)
193200
const rowId = `chat-row-${index}`
@@ -204,26 +211,32 @@ class ChatListManager {
204211
: isCurrentChat
205212
? WhatsAppTheme.activeBg
206213
: WhatsAppTheme.panelDark,
207-
})
208-
209-
// Handle click to open chat using the filtered chat
210-
const chatIndex = index // Capture for closure
211-
const chatRef = chat // Capture chat reference for closure
212-
chatRow.on("click", async () => {
213-
const currentState = appState.getState()
214-
appState.setSelectedChatIndex(chatIndex)
215-
if (chatRef && currentState.currentSession) {
216-
appState.setCurrentView("conversation")
217-
appState.setCurrentChat(chatRef.id)
218-
219-
// When switching chats, we want to start polling for messages immediately
220-
// BUT we need to be careful not to create circular dependency imports
221-
// PollingService imports ConversationView, so ConversationView/ChatListManager shouldn't import PollingService if possible
222-
// Ideally PollingService observes state changes.
223-
224-
await loadMessages(currentState.currentSession, chatRef.id)
225-
await loadContacts(currentState.currentSession)
226-
}
214+
// Handle click (mouse down) to open chat
215+
onMouse: (event) => {
216+
if (event.type === "down") {
217+
const currentState = appState.getState()
218+
if (chatRef && currentState.currentSession) {
219+
// Extract chat ID properly (handle _serialized property)
220+
const chatId =
221+
typeof chatRef.id === "string"
222+
? chatRef.id
223+
: (chatRef.id as { _serialized: string })._serialized
224+
225+
debugLog("ChatListManager", `Clicked chat: ${chatRef.name || chatId}`)
226+
227+
// Destroy old scroll box before loading new messages
228+
destroyConversationScrollBox()
229+
230+
// Set current chat first (this changes view to "conversation")
231+
appState.setCurrentChat(chatId)
232+
appState.setSelectedChatIndex(chatIndex)
233+
234+
// Load contacts and messages
235+
loadContacts(currentState.currentSession)
236+
loadMessages(currentState.currentSession, chatId)
237+
}
238+
}
239+
},
227240
})
228241

229242
// Avatar
@@ -375,6 +388,44 @@ class ChatListManager {
375388
}
376389
}
377390

391+
/**
392+
* Update selection and active (current chat) highlighting for all rows
393+
* This handles both selectedChatIndex and currentChatId changes
394+
*/
395+
private updateSelectionAndActive(
396+
newSelectedIndex: number,
397+
currentChatId: string | null,
398+
chats: ChatSummary[]
399+
): void {
400+
if (!this.scrollBox) return
401+
402+
// Update all rows to reflect current selection and active state
403+
for (let index = 0; index < chats.length; index++) {
404+
const rowData = this.chatRows.get(index)
405+
if (!rowData) continue
406+
407+
const chat = chats[index]
408+
const chatId =
409+
typeof chat.id === "string" ? chat.id : (chat.id as { _serialized: string })._serialized
410+
411+
const isSelected = index === newSelectedIndex
412+
const isCurrentChat = currentChatId === chatId
413+
414+
// Update background color
415+
rowData.box.backgroundColor = isSelected
416+
? WhatsAppTheme.selectedBg
417+
: isCurrentChat
418+
? WhatsAppTheme.activeBg
419+
: WhatsAppTheme.panelDark
420+
421+
// Update text attributes
422+
rowData.avatarText.attributes = isSelected ? TextAttributes.BOLD : TextAttributes.NONE
423+
rowData.nameText.attributes = isSelected ? TextAttributes.BOLD : TextAttributes.NONE
424+
}
425+
426+
this.currentSelectedIndex = newSelectedIndex
427+
}
428+
378429
/**
379430
* Update selection highlight - only changes styles, doesn't rebuild
380431
*/

src/views/SessionCreate.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { getClient } from "../client"
77
import { appState } from "../state/AppState"
88
import type { SessionCreateRequest } from "@muhammedaksam/waha-node"
99
import { debugLog } from "../utils/debug"
10+
import { showQRCode } from "./QRCodeView"
1011

1112
export async function createNewSession(sessionName: string = "default"): Promise<void> {
1213
try {
@@ -31,7 +32,6 @@ export async function createNewSession(sessionName: string = "default"): Promise
3132
console.log(`✅ Session exists and needs QR scan`)
3233

3334
// Show QR code in TUI
34-
const { showQRCode } = await import("../views/QRCodeView")
3535
await showQRCode(name)
3636
} else {
3737
console.log(`✅ Session already exists with status: ${existingSession.status}`)
@@ -57,7 +57,6 @@ export async function createNewSession(sessionName: string = "default"): Promise
5757

5858
if (session.status === "SCAN_QR_CODE") {
5959
// Show QR code in TUI
60-
const { showQRCode } = await import("../views/QRCodeView")
6160
await showQRCode(session.name)
6261
}
6362

0 commit comments

Comments
 (0)