You said:
ChatGPT said:
I’ll trace where Signal Desktop sets the Electron BrowserWindow title, identify the active conversation state, then make the smallest patch so it becomes Name - Signal with a safe fallback to Signal.
Done. I made a minimal patch that changes Signal Desktop’s title behavior to:
Person’s Name - Signal
with fallback to the existing title when no chat is selected. It also preserves the existing unread-count behavior, so you’ll get titles like:
Alice - Signal (3)
I changed two files:
ts/windows/main/phase1-ipc.preload.ts
Adds dynamic title generation based on the selected conversation.
ts/state/initializeRedux.preload.ts
Subscribes to Redux title-relevant changes so the window title updates when you switch conversations or a conversation title changes.
Patch file:
I also verified the patch applies cleanly with:
Bashpatch -p1 --dry-run < signal-window-title.patch
I did not run the full Signal lint/typecheck suite because the ZIP did not include node_modules, so the dependency garden is still unwatered.
You said:
ChatGPT said:
Got it, the unread count should attach to the person/conversation name, not trail after “Signal”. I’ll adjust the patch so the title formats as Alice (3) - Signal, while keeping the fallback as Signal (3) when no conversation is selected.
You said:
ChatGPT said:
Yes, that makes sense: the unread count is global-ish window state, so it belongs before the conversation name. I’ll revise the patch to target (3) Alice - Signal, with sensible fallbacks like (3) Signal when no conversation is selected.
Fixed. The patch now formats unread titles as:
(3) Alice - Signal
and falls back to:
(3) Signal
when no conversation is selected.
Updated patch:
I also re-ran:
Bashpatch -p1 --dry-run < signal-window-title.patch
and it applies cleanly.
You said:
ChatGPT said:
You said:
ChatGPT said:
I’ll paste the diff directly here. I’m going to read the patch file from the workspace first so I don’t accidentally reconstruct it from memory.
Sure — here’s the patch inline:
Diff--- a/ts/ConversationController.preload.ts
+++ b/ts/ConversationController.preload.ts
@@ -424,12 +424,12 @@
unreadStats.unreadCount + unreadStats.readChatsMarkedUnreadCount;
window.IPC.setBadge(total);
window.IPC.updateTrayIcon(total);
- window.document.title = `${window.getTitle()} (${total})`;
+ window.document.title = `(${total}) ${window.getTitle()}`;
} else if (unreadStats.readChatsMarkedUnreadCount > 0) {
const total = unreadStats.readChatsMarkedUnreadCount;
window.IPC.setBadge(total);
window.IPC.updateTrayIcon(total);
- window.document.title = `${window.getTitle()} (${total})`;
+ window.document.title = `(${total}) ${window.getTitle()}`;
} else {
window.IPC.setBadge(0);
window.IPC.updateTrayIcon(0);
--- a/ts/state/initializeRedux.preload.ts
+++ b/ts/state/initializeRedux.preload.ts
@@ -47,6 +47,20 @@
const store = createStore(initialState);
window.reduxStore = store;
+ let lastWindowTitle: string | undefined;
+ const updateWindowTitle = () => {
+ const nextWindowTitle = window.getTitle();
+ if (nextWindowTitle === lastWindowTitle) {
+ return;
+ }
+
+ lastWindowTitle = nextWindowTitle;
+ window.ConversationController.updateUnreadCount();
+ };
+
+ updateWindowTitle();
+ store.subscribe(updateWindowTitle);
+
// Binding these actions to our redux store and exposing them allows us to update
// redux when things change in the rest of the app.
window.reduxActions = {
--- a/ts/windows/main/phase1-ipc.preload.ts
+++ b/ts/windows/main/phase1-ipc.preload.ts
@@ -10,6 +10,7 @@
import { parseIntWithFallback } from '../../util/parseIntWithFallback.std.ts';
import { getSignalConnections } from '../../util/getSignalConnections.preload.ts';
import { ThemeType } from '../../types/Util.std.ts';
+import { NavTab } from '../../types/Nav.std.ts';
import { Environment } from '../../environment.std.ts';
import { SignalContext } from '../context.preload.ts';
import { createLogger } from '../../logging/log.std.ts';
@@ -60,7 +61,27 @@
};
window.ConversationController = new ConversationController();
window.platform = process.platform;
-window.getTitle = () => title;
+window.getTitle = () => {
+ const selectedLocation = window.reduxStore?.getState().nav.selectedLocation;
+ if (selectedLocation?.tab !== NavTab.Chats) {
+ return title;
+ }
+
+ const selectedConversationId = selectedLocation.details.conversationId;
+ if (!selectedConversationId) {
+ return title;
+ }
+
+ const conversationTitle = window.reduxStore
+ .getState()
+ .conversations.conversationLookup[selectedConversationId]?.title?.trim();
+
+ if (!conversationTitle) {
+ return title;
+ }
+
+ return `${conversationTitle} - ${title}`;
+};
window.getAppInstance = () => config.appInstance;
window.getVersion = () => config.version;
window.getBuildCreation = () => parseIntWithFallback(config.buildCreation, 0);
You said:
ChatGPT said:
I’ll check how Signal currently wires that unread-count event before giving a firm answer, because the better choice depends on whether the event does more than just call the method.
You’re right — window.Whisper.events.emit('updateUnreadCount') is the better choice here.
I used:
TypeScriptwindow.ConversationController.updateUnreadCount();
because the title update ultimately lives inside updateUnreadCount(), so it was the most direct way to force the document title to be recomputed after the selected conversation changes.
But in Signal’s codebase, the event is the established abstraction. The constructor wires it like this:
TypeScriptwindow.Whisper.events.on(
'updateUnreadCount',
this.#debouncedUpdateUnreadCount
);
So emitting the event has a few advantages:
-
It follows existing call-site style. Other places already do:
TypeScriptwindow.Whisper.events.emit('updateUnreadCount');
-
It preserves the debouncing behavior instead of bypassing it.
-
It avoids coupling
initializeRedux.preload.tsdirectly to a specificConversationControllermethod.
So the patch should use this instead:
Diff- window.ConversationController.updateUnreadCount();
+ window.Whisper.events.emit('updateUnreadCount');
The only minor reason to call the method directly would be if we needed a strictly synchronous title refresh. But since this title update is triggered from Redux subscription changes and the unread-count path is already event/debounce-based, the event is cleaner.