Compose Window Integration
Relevant source files
The following files were used as context for generating this wiki page:
The Compose Window Integration is a core part of the Ghostbird add-on, enabling the live editing of email content in an external text editor. This is achieved by injecting a content script, compose.js, into Thunderbird's mail compose window. This script is responsible for communicating with the add-on's background script, reading the content from the compose editor, sending it to the external editor (via the background script), and applying updates received from the external editor back to the compose window's DOM.
This system is designed to be a client in the GhostText protocol, relaying information through the background script which manages the WebSocket connection to the GhostText server running in the user's text editor. The integration handles the lifecycle of an editing session, from initiation by the user to termination when the window is closed or the connection is lost.
Sources: doc/design.md, README.md
Initialization and Entry Point
The integration begins when the background script injects compose.js into the compose window tab. This happens when a user initiates a Ghostbird session for that specific compose window.
Sources: src/thunderbird/background_util/compose_tab.ts:18-26
Script Loading and Startup
The ComposeTab class in the background script is responsible for injecting compose.js if it's not already present.
// src/thunderbird/background_util/compose_tab.ts:18-26
export class ComposeTab implements IComposeWindow {
// ...
async prepareContentScript(): Promise<boolean> {
// ...
const result = await this.scripting.executeScript({
target: { tabId: this.tabId },
injectImmediately: true,
files: ["js/compose.js"],
})
// ...
}
}
Once injected, the src/root/compose.ts script serves as the entry point. It performs the following actions:
- Calls
prepareRouter()to initialize the necessary components. prepareRouter()usesstartupCompose()to instantiate theComposeEventRouterand its dependencies, providing it with necessary browser globals likemessenger,document.body, andwindow.getSelection().- It then sets up listeners for
messenger.runtime.onMessageandmessenger.runtime.onConnectto handle communication from the background script.
Sources: src/root/compose.ts:10-38
The diagram below illustrates the initialization flow for the compose script.
Core Components
The compose window script is built around a few key components that manage state, communication, and DOM manipulation. The architecture follows the Dependency Inversion Principle, with components instantiated in a central location (startupCompose).
Sources: doc/design.md, src/root/compose.ts:10-19
| Component | Description | Source File |
|---|---|---|
ComposeEventRouter | The central component in compose.js. It routes incoming messages and connection events from the background script to the appropriate handlers. | src/app-compose |
ComposeEditor | A component responsible for interacting with the compose window's DOM, specifically reading and writing the email body. (Conceptual component from design doc). | doc/design.md |
PortHandler | Handles the lifecycle of the runtime.Port connection to the background script, relaying data between the ComposeEditor and the background. (Conceptual component from design doc). | doc/design.md |
PromisifiedPort | A utility wrapper around the standard runtime.Port to allow for async/await style communication. | src/root/compose.ts:32-38 |
Sources: doc/design.md, src/root/compose.ts, src/test/startup.test.ts
The following diagram shows the relationship between the main conceptual classes within the compose script.
This diagram is a conceptual representation based on the architecture described in doc/design.md.
Communication and Data Flow
Communication between the compose script and the background script is fundamental to the feature's operation.
Connection to Background Script
A persistent connection is established using messenger.tabs.connect() from the background and messenger.runtime.onConnect in the compose script. This creates a runtime.Port which is used for bidirectional communication throughout the editing session. The PromisifiedPort class wraps this port to simplify asynchronous message handling.
Sources: src/root/compose.ts:30-38, src/thunderbird/background_util/compose_tab.ts:46-52
Data Exchange Sequence
The sequence diagram below, adapted from the design document, shows the flow of information when an editing session is active.
Sources: doc/design.md
Data Structures
The data exchanged follows the GhostText protocol. The compose script is responsible for providing the initial state and applying incoming changes.
EmailState
This structure represents the state of the compose window, which is sent to the GhostText server.
| Field | Type | Description |
|---|---|---|
subject | string | The subject of the email. |
url | string | The extension ID, used as the document's host URL. |
isPlainText | boolean | Whether the email is in plain text or HTML format. |
body | string | The email body content. |
selections | ReadonlyArray<SelectionObject> | The user's cursor selections. Currently not implemented (always empty). |
Sources: src/ghosttext-session/types.ts:42-55
EditorChangeResponse
This is the message received from the server (via the background script) when the text is modified in the external editor. The compose script uses the text property to update the email body.
// src/ghosttext-session/types.ts:35-40
export type EditorChangeResponse = {
/** The temporary file content */
text?: string
/** An array of selection objects that describe the user's current cursor selections in the editor */
selections?: ReadonlyArray<SelectionObject>
}
DOM Interaction
The compose script directly interacts with the DOM of the mail compose window to read and write the email content.
- Reading Content: On session start, the
ComposeTab.getDetails()method is called by the background script, which in turn retrieves theplainTextBodyorbodyfrom the compose details provided by the Thunderbird API. This initial text is then sent to the compose script. - Writing Content: When an
EditorChangeResponseis received, the script updates the content of the email body. The test implementation inghostbird.test.tsshows this is done by updating thetextContentof the<body>element.
Sources: src/thunderbird/background_util/compose_tab.ts:28-36, src/test/ghostbird.test.ts:100-103
Conclusion
The Compose Window Integration is the user-facing part of Ghostbird's core functionality. Through the compose.js content script, it bridges Thunderbird's UI with the GhostText protocol, enabling a seamless "edit in external editor" experience. Its architecture, centered around the ComposeEventRouter and a persistent port connection to the background script, provides a robust mechanism for synchronizing email content in real-time.