Skip to content

Commit ac51a24

Browse files
authored
Fixes for alpha (#13)
Closes #2
2 parents 67d3c91 + 0f08887 commit ac51a24

22 files changed

Lines changed: 392 additions & 194 deletions

doc/design.md

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ This is how user actions are handled:
1818
* ~~When the text is changed in the compose window, `compose` sends the update to `background`, which relays it to the server, which updates the text editor.~~ (Not until v2.0.0)
1919
1. The WebSocket connection remains open until one of the following happens:
2020
* a) When the server closes the WebSocket connection, which `background` detects and notifies `compose`.
21-
* b) When the compose window is closed, `compose` closes the `Port`, which `background` detects and closes the WebSocket connection.
21+
* b) When the shortcut key assigned to the close function is pressed, `compose` closes the `Port`, which `background` detects and closes the WebSocket connection.
22+
* c) When the compose window is closed, `compose` closes the `Port`, handled as the same as (b).
2223
1. The compose window returns to its normal state and the button is toggled off.
2324

2425
### The options page
@@ -81,11 +82,8 @@ loop
8182
B->>C: Close the port
8283
end
8384
else
84-
break When the Ghostbird button has been clicked again
85-
B->>B: User clicks the Ghostbird button
86-
B->>C: Request a graceful close
87-
C->>B: Send Last update
88-
B->>S: Send Last update
85+
break When the close shortcut key is pressed
86+
B->>B: User types the shortcut key
8987
B->>C: Close the port
9088
B->>S: Close the WebSocket
9189
end

src/app-background/background_event_router.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export class BackgroundEventRouter {
4848
return Promise.reject(Error("Event dropped"))
4949
}
5050

51-
return this.composeActionNotifier.toggle(composeTab)
51+
return this.composeActionNotifier.start(composeTab)
5252
}
5353

5454
/** handles one-off messages from content scripts */

src/app-background/compose_action_notifier.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export class ComposeActionNotifier {
1010

1111
async start(tab: IComposeWindow): Promise<void> {
1212
if (this.findOpenPort(tab)) {
13-
console.log("Port is open; skipping")
13+
console.info("Port is already open; skipping")
1414
return
1515
}
1616
await tab.prepareContentScript()
@@ -19,11 +19,11 @@ export class ComposeActionNotifier {
1919
this.runners.set(tab.tabId, port)
2020

2121
try {
22-
console.log("starting session")
22+
console.info("starting session")
2323
let editor = new EmailEditor(tab, port)
2424
await this.ghostTextRunner.run(editor, editor)
2525
} finally {
26-
console.log("session closed")
26+
console.info("session closed")
2727
this.close(tab, port)
2828
}
2929
}
@@ -35,10 +35,10 @@ export class ComposeActionNotifier {
3535
async toggle(tab: IComposeWindow): Promise<void> {
3636
let port = this.findOpenPort(tab)
3737
if (port) {
38-
console.log("toggle: closing")
38+
console.info("toggle: closing")
3939
this.close(tab, port)
4040
} else {
41-
console.log("toggle: starting")
41+
console.info("toggle: starting")
4242
await this.start(tab)
4343
}
4444
}

src/app-compose/api.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
import type { IMessagePort, IMessenger } from "../ghosttext-runner/message"
2-
import type { EditorChangeResponse, IEditorState } from "../ghosttext-session"
1+
import type { IMessenger } from "../ghosttext-runner/message"
32

4-
/** Connection to the background script, and the GhostText server behind it */
5-
export type IGhostClientPort = IMessagePort<Partial<IEditorState>, EditorChangeResponse>
3+
export type { IGhostClientPort } from "../ghosttext-adaptor/api"
64

75
/** Can send one-off messages to background */
86
export type IBackgroundMessenger = IMessenger<{ ping: "pong" }>

src/app-compose/compose_event_router.ts

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,36 @@
1-
import type { MessagesFromBackground } from "../ghosttext-runner/message"
1+
import type { ExternalEdit, MessagesFromBackground } from "../ghosttext-runner"
2+
import type { BodyState } from "../ghosttext-session/types"
23
import type { IBackgroundMessenger, IGhostClientPort } from "./api"
34

45
export class ComposeEventRouter {
56
static isSingleton = true
67

7-
constructor(readonly backgroundMessenger: IBackgroundMessenger) {}
8+
constructor(
9+
readonly body: HTMLBodyElement,
10+
readonly backgroundMessenger: IBackgroundMessenger,
11+
) {}
812

913
async handleConnect(port: IGhostClientPort): Promise<void> {
10-
const body = document.body
11-
const clearReadOnly = makeReadOnly(body)
14+
const clearReadOnly = makeReadOnly(this.body)
1215
try {
1316
console.info({ connected: Date.now(), date: new Date() })
17+
18+
port.send({ html: this.body.innerHTML, plainText: this.body.textContent } satisfies BodyState)
19+
1420
for (;;) {
15-
let ready = await port.waitReady().then(
16-
() => true,
17-
() => false,
18-
)
19-
if (!ready) {
20-
break
21-
}
21+
await port.waitReady()
22+
2223
let got = port.clearReceived()
23-
console.debug({ got })
24-
if (!got?.text) {
24+
25+
if (!got || !this.applyEdit(got)) {
2526
break
2627
}
28+
2729
// TODO sync cursors
28-
body.textContent = got.text
2930
// TODO send changes
30-
// TODO reconnect
3131
}
32+
} catch (thrown) {
33+
console.info({ thrown })
3234
} finally {
3335
console.info({ disconnected: Date.now(), date: new Date() })
3436
clearReadOnly()
@@ -40,16 +42,26 @@ export class ComposeEventRouter {
4042

4143
if (message === "ping") {
4244
return response(message)
43-
} else if (message === "start") {
44-
return "ok"
45-
} else if (message === "stop") {
46-
return "ok"
47-
} else if (message === "toggle") {
48-
return "ok"
4945
} else {
5046
return "ok"
5147
}
5248
}
49+
50+
private applyEdit(got: ExternalEdit): boolean {
51+
console.debug({ got })
52+
53+
if (typeof got.plainText === "string") {
54+
this.body.textContent = got.plainText
55+
return true
56+
}
57+
58+
if (typeof got.html === "string") {
59+
this.body.innerHTML = got.html
60+
return true
61+
}
62+
63+
return false
64+
}
5365
}
5466

5567
function response(_message: "ping"): MessagesFromBackground["ping"] {
@@ -67,15 +79,18 @@ function makeReadOnly(body: HTMLElement): () => void {
6779
const controller = new AbortController()
6880
const option = { signal: controller.signal }
6981

70-
body.style.background = "slategray"
71-
7282
body.addEventListener("beforeinput", disable, option)
7383
body.addEventListener("paste", disable, option)
7484
body.addEventListener("cut", disable, option)
7585
body.addEventListener("drop", disable, option)
7686
body.addEventListener("dragover", disable, option)
7787

88+
body.style.background = "lightgray"
89+
body.style.pointerEvents = "none"
90+
body.blur()
91+
7892
return () => {
93+
body.style.removeProperty("pointerEvents")
7994
body.style.removeProperty("background")
8095
controller.abort()
8196
}

src/app-compose/tsconfig.json

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,7 @@
88
"path": "../util"
99
},
1010
{
11-
"path": "../ghosttext-session"
12-
},
13-
{
14-
"path": "../ghosttext-runner"
11+
"path": "../ghosttext-adaptor"
1512
}
1613
]
1714
}

src/ghosttext-adaptor/api.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import type { IMessagePort } from "../ghosttext-runner"
2-
import type { EditorChangeResponse, IEditorState } from "../ghosttext-session"
1+
import type { ExternalEdit, IMessagePort, InternalEdit } from "../ghosttext-runner"
32

43
/** Wrapper for `fetch` and `WebSocket` */
54
export interface IWebClient {
@@ -29,9 +28,7 @@ export type ComposeDetails = {
2928
}
3029

3130
/** Updatable fields of a compose window */
32-
export type SettableComposeDetails =
33-
| { subject: string; body?: string | undefined }
34-
| { subject?: string | undefined; body: string }
31+
export type SettableComposeDetails = { subject: string }
3532

3633
/** an utility to interact with a mail compose window */
3734
export interface IComposeWindow {
@@ -67,4 +64,7 @@ export interface IComposeWindow {
6764
setIcon(imageFilePath: string): Promise<void>
6865
}
6966

70-
export type IGhostServerPort = IMessagePort<EditorChangeResponse, Partial<IEditorState>>
67+
/** Connection to the background script, and the GhostText server behind it */
68+
export type IGhostClientPort = IMessagePort<InternalEdit, ExternalEdit>
69+
/** Connection to the content script */
70+
export type IGhostServerPort = IMessagePort<ExternalEdit, InternalEdit>

src/ghosttext-adaptor/email_editor.ts

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { ClientStatus, IClientEditor, IStatusIndicator } from "../ghosttext-runner"
2-
import type { EditorChangeResponse, IEditorState } from "../ghosttext-session"
1+
import type { ClientStatus, ExternalEdit, IClientEditor, InternalEdit, IStatusIndicator } from "../ghosttext-runner"
2+
import type { EmailState } from "../ghosttext-session"
33
import type { IComposeWindow, IGhostServerPort } from "./api"
44

55
export class EmailEditor implements IClientEditor, IStatusIndicator {
@@ -12,30 +12,38 @@ export class EmailEditor implements IClientEditor, IStatusIndicator {
1212
return this.composeWindow.setIcon(imageForStatus(status))
1313
}
1414

15-
async getState(): Promise<IEditorState> {
16-
this.port.clearReceived()
15+
async getState(): Promise<EmailState> {
16+
let { body, subject, isPlainText } = await this.composeWindow.getDetails()
1717

18-
let { body, subject } = await this.composeWindow.getDetails()
18+
// Expect body from the compose window
19+
await this.waitEdit()
20+
let r = this.port.clearReceived()
21+
if (isPlainText && r && "plainText" in r && r.plainText != null) {
22+
body = r.plainText
23+
} else if (!isPlainText && r && "html" in r && r.html != null) {
24+
body = r.html
25+
}
1926

2027
// TODO Add an option to strip HTML tags on edit
2128
// TODO pass a better url that identify the email
2229

23-
return { selections: [{ start: 0, end: 0 }], subject, text: body, url: import.meta.url }
30+
let { host } = new URL(import.meta.url)
31+
32+
return { selections: [{ start: 0, end: 0 }], subject, isPlainText, body, url: host }
2433
}
2534

26-
async applyChange(change: EditorChangeResponse): Promise<void> {
35+
async applyChange(change: ExternalEdit): Promise<void> {
2736
// TODO Should re-add HTML tags like `<p></p>` if stripped
28-
if (change.text) {
29-
this.port.send(change)
30-
}
37+
this.port.send(change)
3138
}
3239

3340
waitEdit(): Promise<void> {
3441
return this.port.waitReady()
3542
}
3643

37-
popLastEdit(): Partial<IEditorState> | undefined {
38-
return this.port.clearReceived()
44+
popLastEdit(): InternalEdit | undefined {
45+
let edits = this.port.clearReceived()
46+
return edits
3947
}
4048
}
4149

src/ghosttext-adaptor/tsconfig.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,6 @@
77
{
88
"path": "../util"
99
},
10-
{
11-
"path": "../ghosttext-session"
12-
},
1310
{
1411
"path": "../ghosttext-runner"
1512
}

src/ghosttext-runner/api.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import type {
22
EditorChangeResponse,
3-
IEditorState,
3+
EmailState,
4+
ExternalEdit,
5+
InternalEdit,
46
ServerInitialResponse,
57
UpdateRequest,
68
} from "../ghosttext-session/types"
@@ -60,14 +62,14 @@ export interface ISession {
6062
*/
6163
export interface IClientEditor {
6264
/** @returns complete current state of the editor */
63-
getState(): Promise<IEditorState>
65+
getState(): Promise<EmailState>
6466

6567
/**
6668
* Applies changes received from the GhostText server to the compose window
6769
* @param change The change from the server
6870
* @returns resolves when updated, or rejects if the editor has been closed
6971
*/
70-
applyChange(change: EditorChangeResponse): Promise<void>
72+
applyChange(change: ExternalEdit): Promise<void>
7173

7274
/**
7375
* Waits for an edit to occur in the local compose window.
@@ -79,9 +81,11 @@ export interface IClientEditor {
7981
* Get the most recent edit and drop the rest
8082
* @returns recent edit or `undefined` if no new edits have occurred
8183
*/
82-
popLastEdit(): Partial<IEditorState> | undefined
84+
popLastEdit(): InternalEdit | undefined
8385
}
8486

87+
export type { InternalEdit, ExternalEdit }
88+
8589
/**
8690
* Status of the connection
8791
* - `inactive`: Not connected or starting

0 commit comments

Comments
 (0)