Skip to content

Commit 2373c23

Browse files
authored
wired-up proof-of-concept on the popup (#33)
2 parents 4082e4d + a8fffaa commit 2373c23

File tree

12 files changed

+499
-45
lines changed

12 files changed

+499
-45
lines changed

browser-extension/README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,32 @@ This is a [WXT](https://wxt.dev/)-based browser extension that
3434
### Entry points
3535

3636
- `src/entrypoints/content.ts` - injected into every webpage
37+
- `src/entrypoints/background.ts` - service worker that manages state and handles messages
3738
- `src/entrypoints/popup` - html/css/ts which opens when the extension's button gets clicked
3839

40+
```mermaid
41+
graph TD
42+
Content[Content Script<br/>content.ts]
43+
Background[Background Script<br/>background.ts]
44+
Popup[Popup Script<br/>popup/main.ts]
45+
46+
Content -->|ENHANCED/DESTROYED<br/>CommentEvent| Background
47+
Popup -->|GET_OPEN_SPOTS<br/>SWITCH_TO_TAB| Background
48+
Background -->|GetOpenSpotsResponse<br/>spots array| Popup
49+
50+
Background -.->|manages| Storage[Comment State Storage<br/>openSpots JsonMap]
51+
Content -.->|enhances| TextArea[textarea elements<br/>on web pages]
52+
Popup -.->|displays| UI[Extension UI<br/>list of comment spots]
53+
54+
classDef entrypoint fill:#e1f5fe
55+
classDef storage fill:#f3e5f5
56+
classDef ui fill:#e8f5e8
57+
58+
class Content,Background,Popup entrypoint
59+
class Storage storage
60+
class TextArea,UI ui
61+
```
62+
3963
### Architecture
4064

4165
Every time a `textarea` shows up on a page, on initial load or later on, it gets passed to a list of `CommentEnhancer`s. Each one gets a turn to say "I can enhance this box!". They show that they can enhance it by returning a [`CommentSpot`, `Overtype`].

browser-extension/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"build": "wxt build",
4242
"build:dev": "wxt build --mode development",
4343
"build:firefox": "wxt build -b firefox",
44+
"precommit": "npm run biome:fix && npm run typecheck && npm run test",
4445
"typecheck": "tsc --noEmit",
4546
"dev": "wxt",
4647
"dev:firefox": "wxt -b firefox",
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import type { CommentDraft, CommentEvent, CommentSpot } from '../lib/enhancer'
2+
import type { GetOpenSpotsResponse, ToBackgroundMessage } from '../lib/messages'
3+
import {
4+
CLOSE_MESSAGE_PORT,
5+
isContentToBackgroundMessage,
6+
isGetOpenSpotsMessage,
7+
isSwitchToTabMessage,
8+
KEEP_PORT_OPEN,
9+
} from '../lib/messages'
10+
11+
export interface Tab {
12+
tabId: number
13+
windowId: number
14+
}
15+
export interface CommentState {
16+
tab: Tab
17+
spot: CommentSpot
18+
drafts: [number, CommentDraft][]
19+
}
20+
21+
export const openSpots = new Map<string, CommentState>()
22+
23+
export function handleCommentEvent(message: CommentEvent, sender: any): boolean {
24+
if (
25+
(message.type === 'ENHANCED' || message.type === 'DESTROYED') &&
26+
sender.tab?.id &&
27+
sender.tab?.windowId
28+
) {
29+
if (message.type === 'ENHANCED') {
30+
const tab: Tab = {
31+
tabId: sender.tab.id,
32+
windowId: sender.tab.windowId,
33+
}
34+
const commentState: CommentState = {
35+
drafts: [],
36+
spot: message.spot,
37+
tab,
38+
}
39+
openSpots.set(message.spot.unique_key, commentState)
40+
} else if (message.type === 'DESTROYED') {
41+
openSpots.delete(message.spot.unique_key)
42+
} else {
43+
throw new Error(`Unhandled comment event type: ${message.type}`)
44+
}
45+
}
46+
return CLOSE_MESSAGE_PORT
47+
}
48+
49+
export function handlePopupMessage(
50+
message: any,
51+
_sender: any,
52+
sendResponse: (response: any) => void,
53+
): typeof CLOSE_MESSAGE_PORT | typeof KEEP_PORT_OPEN {
54+
if (isGetOpenSpotsMessage(message)) {
55+
const spots: CommentState[] = []
56+
for (const [, commentState] of openSpots) {
57+
spots.push(commentState)
58+
}
59+
const response: GetOpenSpotsResponse = { spots }
60+
sendResponse(response)
61+
return KEEP_PORT_OPEN
62+
} else if (isSwitchToTabMessage(message)) {
63+
browser.windows
64+
.update(message.windowId, { focused: true })
65+
.then(() => {
66+
return browser.tabs.update(message.tabId, { active: true })
67+
})
68+
.catch((error) => {
69+
console.error('Error switching to tab:', error)
70+
})
71+
return CLOSE_MESSAGE_PORT
72+
} else {
73+
throw new Error(`Unhandled popup message type: ${message?.type || 'unknown'}`)
74+
}
75+
}
76+
77+
export default defineBackground(() => {
78+
browser.runtime.onMessage.addListener((message: ToBackgroundMessage, sender, sendResponse) => {
79+
if (isContentToBackgroundMessage(message)) {
80+
return handleCommentEvent(message, sender)
81+
} else {
82+
return handlePopupMessage(message, sender, sendResponse)
83+
}
84+
})
85+
})

browser-extension/src/entrypoints/content.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,27 @@
11
import { CONFIG, type ModeType } from '../lib/config'
2+
import type { CommentEvent, CommentSpot } from '../lib/enhancer'
23
import { logger } from '../lib/logger'
34
import { EnhancerRegistry, TextareaRegistry } from '../lib/registries'
45
import { githubPrNewCommentContentScript } from '../playgrounds/github-playground'
56

67
const enhancers = new EnhancerRegistry()
78
const enhancedTextareas = new TextareaRegistry()
89

10+
function sendEventToBackground(type: 'ENHANCED' | 'DESTROYED', spot: CommentSpot): void {
11+
const message: CommentEvent = {
12+
spot,
13+
type,
14+
}
15+
browser.runtime.sendMessage(message).catch((error) => {
16+
logger.debug('Failed to send event to background:', error)
17+
})
18+
}
19+
20+
enhancedTextareas.setEventHandlers(
21+
(spot) => sendEventToBackground('ENHANCED', spot),
22+
(spot) => sendEventToBackground('DESTROYED', spot),
23+
)
24+
925
export default defineContentScript({
1026
main() {
1127
if ((CONFIG.MODE as ModeType) === 'PLAYGROUNDS_PR') {

browser-extension/src/entrypoints/popup/index.html

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,10 @@
33
<head>
44
<meta charset="utf-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6-
<title>Gitcasso Markdown Assistant</title>
6+
<title>Gitcasso</title>
77
</head>
88
<body>
99
<div id="app">
10-
<div class="header">
11-
<div class="logo">Gitcasso Markdown Assistant</div>
12-
<div class="subtitle">Syntax highlighting and autosave for comments on GitHub (and other other markdown-friendly websites).</div>
13-
</div>
14-
<div id="scan-results">
15-
<p>Loading drafts from local storage...</p>
16-
</p>
1710
</div>
1811
<script type="module" src="main.ts"></script>
1912
</body>
Lines changed: 90 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,97 @@
11
import './style.css'
2+
import { logger } from '../../lib/logger'
3+
import type {
4+
GetOpenSpotsMessage,
5+
GetOpenSpotsResponse,
6+
SwitchToTabMessage,
7+
} from '../../lib/messages'
8+
import { EnhancerRegistry } from '../../lib/registries'
9+
import type { CommentState } from '../background'
210

3-
document.addEventListener('DOMContentLoaded', async () => {
4-
const statusDiv = document.getElementById('scan-results') as HTMLElement
11+
// Test basic DOM access
12+
try {
13+
const app = document.getElementById('app')!
14+
logger.debug('Found app element:', app)
15+
app.innerHTML = '<div>Script is running...</div>'
16+
} catch (error) {
17+
logger.error('Error accessing DOM:', error)
18+
}
519

20+
const enhancers = new EnhancerRegistry()
21+
22+
async function getOpenSpots(): Promise<CommentState[]> {
23+
logger.debug('Sending message to background script...')
624
try {
7-
// get current active tab
8-
const tabs = await browser.tabs.query({ active: true, currentWindow: true })
9-
const tab = tabs[0]
10-
11-
if (!tab?.id) {
12-
statusDiv.textContent = 'Cannot access current tab'
13-
return
14-
}
15-
16-
// send message to content script to get scan results
17-
const results = await browser.tabs.sendMessage(tab.id, {
18-
action: 'getScanResults',
19-
})
20-
if (results) {
21-
// TODO: statusDiv.textContent = {{show drafts}}
22-
}
25+
const message: GetOpenSpotsMessage = { type: 'GET_OPEN_SPOTS' }
26+
const response = (await browser.runtime.sendMessage(message)) as GetOpenSpotsResponse
27+
logger.debug('Received response:', response)
28+
return response.spots || []
2329
} catch (error) {
24-
console.error('Popup error:', error)
25-
statusDiv.textContent = 'Unable to load saved drafts.'
30+
logger.error('Error sending message to background:', error)
31+
return []
32+
}
33+
}
34+
35+
function switchToTab(tabId: number, windowId: number): void {
36+
// Send message to background script to handle tab switching
37+
// This avoids the popup context being destroyed before completion
38+
const message: SwitchToTabMessage = {
39+
tabId,
40+
type: 'SWITCH_TO_TAB',
41+
windowId,
42+
}
43+
browser.runtime.sendMessage(message)
44+
window.close()
45+
}
46+
47+
function createSpotElement(commentState: CommentState): HTMLElement {
48+
const item = document.createElement('div')
49+
item.className = 'spot-item'
50+
51+
logger.debug('Creating spot element for:', commentState.spot)
52+
const enhancer = enhancers.enhancerFor(commentState.spot)
53+
if (!enhancer) {
54+
logger.error('No enhancer found for:', commentState.spot)
55+
logger.error('Only have enhancers for:', enhancers.byType)
2656
}
57+
58+
const title = document.createElement('div')
59+
title.className = 'spot-title'
60+
title.textContent = enhancer.tableTitle(commentState.spot)
61+
item.appendChild(title)
62+
item.addEventListener('click', () => {
63+
switchToTab(commentState.tab.tabId, commentState.tab.windowId)
64+
})
65+
return item
66+
}
67+
68+
async function renderOpenSpots(): Promise<void> {
69+
logger.debug('renderOpenSpots called')
70+
const app = document.getElementById('app')!
71+
const spots = await getOpenSpots()
72+
logger.debug('Got spots:', spots)
73+
74+
if (spots.length === 0) {
75+
app.innerHTML = '<div class="no-spots">No open comment spots</div>'
76+
return
77+
}
78+
79+
const header = document.createElement('h2')
80+
header.textContent = 'Open Comment Spots'
81+
app.appendChild(header)
82+
83+
const list = document.createElement('div')
84+
list.className = 'spots-list'
85+
86+
spots.forEach((spot) => {
87+
list.appendChild(createSpotElement(spot))
88+
})
89+
90+
app.appendChild(list)
91+
}
92+
93+
renderOpenSpots().catch((error) => {
94+
logger.error('Error in renderOpenSpots:', error)
95+
const app = document.getElementById('app')!
96+
app.innerHTML = `<div class="no-spots">Error loading spots: ${error.message}</div>`
2797
})

browser-extension/src/entrypoints/popup/style.css

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,47 @@ body {
77
margin: 0;
88
}
99

10-
.header {
11-
text-align: center;
12-
margin-bottom: 15px;
10+
h2 {
11+
margin: 0 0 15px 0;
12+
font-size: 16px;
13+
font-weight: 600;
14+
color: #333;
15+
}
16+
17+
.spots-list {
18+
display: flex;
19+
flex-direction: column;
20+
gap: 8px;
21+
}
22+
23+
.spot-item {
24+
padding: 10px;
25+
border: 1px solid #e0e0e0;
26+
border-radius: 6px;
27+
cursor: pointer;
28+
transition: all 0.2s ease;
29+
background: white;
30+
}
31+
32+
.spot-item:hover {
33+
background: #f5f5f5;
34+
border-color: #d0d0d0;
35+
transform: translateY(-1px);
36+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
1337
}
1438

15-
.logo {
16-
font-size: 18px;
17-
font-weight: bold;
18-
color: #0066cc;
19-
margin-bottom: 8px;
39+
.spot-title {
40+
font-weight: 500;
41+
color: #333;
42+
margin-bottom: 4px;
43+
overflow: hidden;
44+
text-overflow: ellipsis;
45+
white-space: nowrap;
2046
}
2147

22-
.subtitle {
48+
.no-spots {
49+
text-align: center;
2350
color: #666;
24-
font-size: 12px;
51+
padding: 40px 20px;
52+
font-style: italic;
2553
}

browser-extension/src/lib/enhancer.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,19 @@ export interface CommentSpot {
1010
type: string
1111
}
1212

13+
export interface CommentDraft {
14+
title?: string
15+
body: string
16+
}
17+
18+
export type CommentEventType = 'ENHANCED' | 'LOST_FOCUS' | 'DESTROYED'
19+
20+
export interface CommentEvent {
21+
type: CommentEventType
22+
spot: CommentSpot
23+
draft?: CommentDraft
24+
}
25+
1326
/** Wraps the textareas of a given platform with Gitcasso's enhancements. */
1427
export interface CommentEnhancer<Spot extends CommentSpot = CommentSpot> {
1528
/** Guarantees to only return a type within this list. */
@@ -30,5 +43,4 @@ export interface CommentEnhancer<Spot extends CommentSpot = CommentSpot> {
3043

3144
tableIcon(spot: Spot): string
3245
tableTitle(spot: Spot): string
33-
buildUrl(spot: Spot): string
3446
}

0 commit comments

Comments
 (0)