Skip to content

Commit 3e676e3

Browse files
author
ntwigg
committed
Progress
1 parent 3883abe commit 3e676e3

File tree

5 files changed

+399
-2
lines changed

5 files changed

+399
-2
lines changed
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { CommentType, CommentContext, TextareaHandler, TextareaInfo } from './textarea-handler';
2+
import { GitHubHandler } from '../handlers/github-handler';
3+
import { RedditHandler } from '../handlers/reddit-handler';
4+
5+
export class HandlerRegistry {
6+
private handlers = new Set<TextareaHandler<any>>();
7+
8+
constructor() {
9+
// Register all available handlers
10+
this.register(new GitHubHandler());
11+
this.register(new RedditHandler());
12+
}
13+
14+
private register<T extends CommentContext>(handler: TextareaHandler<T>): void {
15+
this.handlers.add(handler);
16+
}
17+
18+
getHandlerForType(type: CommentType): TextareaHandler<any> | null {
19+
for (const handler of this.handlers) {
20+
if (handler.forCommentTypes().includes(type)) {
21+
return handler;
22+
}
23+
}
24+
return null;
25+
}
26+
27+
identifyAll(): TextareaInfo<any>[] {
28+
const allTextareas: TextareaInfo<any>[] = [];
29+
30+
for (const handler of this.handlers) {
31+
try {
32+
const textareas = handler.identify();
33+
allTextareas.push(...textareas);
34+
} catch (error) {
35+
console.warn('Handler failed to identify textareas:', error);
36+
}
37+
}
38+
39+
return allTextareas;
40+
}
41+
42+
getAllHandlers(): TextareaHandler<any>[] {
43+
return Array.from(this.handlers);
44+
}
45+
46+
getCommentTypesForHandler(handler: TextareaHandler<any>): CommentType[] {
47+
return handler.forCommentTypes();
48+
}
49+
}

browser-extension/src/datamodel/textarea-handler.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ export interface TextareaInfo<T extends CommentContext = CommentContext> {
2929
}
3030

3131
export interface TextareaHandler<T extends CommentContext = CommentContext> {
32+
// Handler metadata
33+
forCommentTypes(): CommentType[];
34+
3235
// Content script functionality
3336
identify(): TextareaInfo<T>[];
3437
readContent(textarea: HTMLTextAreaElement): string;
@@ -52,6 +55,7 @@ export abstract class BaseTextareaHandler<T extends CommentContext = CommentCont
5255
this.domain = domain;
5356
}
5457

58+
abstract forCommentTypes(): CommentType[];
5559
abstract identify(): TextareaInfo<T>[];
5660
abstract extractContext(textarea: HTMLTextAreaElement): T | null;
5761
abstract determineType(textarea: HTMLTextAreaElement): CommentType | null;

browser-extension/src/entrypoints/content.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { CONFIG } from './content/config'
22
import { logger } from './content/logger'
33
import { injectStyles } from './content/styles'
4+
import { HandlerRegistry } from '../datamodel/handler-registry'
5+
6+
const registry = new HandlerRegistry()
47

58
export default defineContentScript({
69
main() {
@@ -13,7 +16,7 @@ export default defineContentScript({
1316
childList: true,
1417
subtree: true,
1518
})
16-
logger.debug('Extension loaded')
19+
logger.debug('Extension loaded with', registry.getAllHandlers().length, 'handlers')
1720
},
1821
matches: ['<all_urls>'],
1922
runAt: 'document_end',
@@ -24,9 +27,16 @@ function handleMutations(mutations: MutationRecord[]): void {
2427
for (const node of mutation.addedNodes) {
2528
if (node.nodeType === Node.ELEMENT_NODE) {
2629
const element = node as Element
27-
if (element.tagName === 'textarea') {
30+
if (element.tagName === 'TEXTAREA') {
2831
initializeMaybe(element as HTMLTextAreaElement)
2932
}
33+
// Also check for textareas within added subtrees
34+
const textareas = element.querySelectorAll?.('textarea')
35+
if (textareas) {
36+
for (const textarea of textareas) {
37+
initializeMaybe(textarea)
38+
}
39+
}
3040
}
3141
}
3242
}
@@ -37,6 +47,13 @@ function initializeMaybe(textarea: HTMLTextAreaElement) {
3747
logger.debug('activating textarea {}', textarea)
3848
injectStyles()
3949
textarea.classList.add(CONFIG.ADDED_OVERTYPE_CLASS)
50+
51+
// Use registry to identify and handle this textarea
52+
const textareaInfos = registry.identifyAll().filter(info => info.element === textarea)
53+
for (const info of textareaInfos) {
54+
logger.debug('Identified textarea:', info.type, info.context.unique_key)
55+
// TODO: Set up textarea monitoring and draft saving
56+
}
4057
} else {
4158
logger.debug('already activated textarea {}', textarea)
4259
}
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import { CommentType, CommentContext, BaseTextareaHandler, TextareaInfo } from '../datamodel/textarea-handler';
2+
3+
export interface GitHubContext extends CommentContext {
4+
domain: string;
5+
slug: string; // owner/repo
6+
number?: number; // issue/PR number
7+
commentId?: string; // for editing existing comments
8+
}
9+
10+
export class GitHubHandler extends BaseTextareaHandler<GitHubContext> {
11+
constructor() {
12+
super('github.com');
13+
}
14+
15+
forCommentTypes(): CommentType[] {
16+
return [
17+
'GH_ISSUE_NEW',
18+
'GH_PR_NEW',
19+
'GH_ISSUE_ADD_COMMENT',
20+
'GH_ISSUE_EDIT_COMMENT',
21+
'GH_PR_ADD_COMMENT',
22+
'GH_PR_EDIT_COMMENT',
23+
'GH_PR_CODE_COMMENT'
24+
];
25+
}
26+
27+
identify(): TextareaInfo<GitHubContext>[] {
28+
const textareas = document.querySelectorAll<HTMLTextAreaElement>('textarea');
29+
const results: TextareaInfo<GitHubContext>[] = [];
30+
31+
for (const textarea of textareas) {
32+
const type = this.determineType(textarea);
33+
const context = this.extractContext(textarea);
34+
35+
if (type && context) {
36+
results.push({ element: textarea, type, context });
37+
}
38+
}
39+
40+
return results;
41+
}
42+
43+
extractContext(textarea: HTMLTextAreaElement): GitHubContext | null {
44+
const url = window.location.href;
45+
const pathname = window.location.pathname;
46+
47+
// Parse GitHub URL structure: /owner/repo/issues/123 or /owner/repo/pull/456
48+
const match = pathname.match(/^\/([^\/]+)\/([^\/]+)(?:\/(issues|pull)\/(\d+))?/);
49+
if (!match) return null;
50+
51+
const [, owner, repo, type, numberStr] = match;
52+
const slug = `${owner}/${repo}`;
53+
const number = numberStr ? parseInt(numberStr, 10) : undefined;
54+
55+
// Generate unique key based on context
56+
let unique_key = `github:${slug}`;
57+
if (number) {
58+
unique_key += `:${type}:${number}`;
59+
} else {
60+
unique_key += ':new';
61+
}
62+
63+
// Check if editing existing comment
64+
const commentId = this.getCommentId(textarea);
65+
if (commentId) {
66+
unique_key += `:edit:${commentId}`;
67+
}
68+
69+
return {
70+
unique_key,
71+
domain: window.location.hostname,
72+
slug,
73+
number,
74+
commentId
75+
};
76+
}
77+
78+
determineType(textarea: HTMLTextAreaElement): CommentType | null {
79+
const pathname = window.location.pathname;
80+
81+
// New issue
82+
if (pathname.includes('/issues/new')) {
83+
return 'GH_ISSUE_NEW';
84+
}
85+
86+
// New PR
87+
if (pathname.includes('/compare/') || pathname.endsWith('/compare')) {
88+
return 'GH_PR_NEW';
89+
}
90+
91+
// Check if we're on an issue or PR page
92+
const match = pathname.match(/\/(issues|pull)\/(\d+)/);
93+
if (!match) return null;
94+
95+
const [, type] = match;
96+
const isEditingComment = this.getCommentId(textarea) !== null;
97+
98+
if (type === 'issues') {
99+
return isEditingComment ? 'GH_ISSUE_EDIT_COMMENT' : 'GH_ISSUE_ADD_COMMENT';
100+
} else {
101+
// Check if it's a code comment (in Files Changed tab)
102+
const isCodeComment = textarea.closest('.js-inline-comment-form') !== null ||
103+
textarea.closest('[data-path]') !== null;
104+
105+
if (isCodeComment) {
106+
return 'GH_PR_CODE_COMMENT';
107+
}
108+
109+
return isEditingComment ? 'GH_PR_EDIT_COMMENT' : 'GH_PR_ADD_COMMENT';
110+
}
111+
}
112+
113+
generateDisplayTitle(context: GitHubContext): string {
114+
const { slug, number, commentId } = context;
115+
116+
if (commentId) {
117+
return `Edit comment in ${slug}${number ? ` #${number}` : ''}`;
118+
}
119+
120+
if (number) {
121+
return `Comment on ${slug} #${number}`;
122+
}
123+
124+
return `New ${window.location.pathname.includes('/issues/') ? 'issue' : 'PR'} in ${slug}`;
125+
}
126+
127+
generateIcon(type: CommentType): string {
128+
switch (type) {
129+
case 'GH_ISSUE_NEW':
130+
case 'GH_ISSUE_ADD_COMMENT':
131+
case 'GH_ISSUE_EDIT_COMMENT':
132+
return '🐛'; // Issue icon
133+
case 'GH_PR_NEW':
134+
case 'GH_PR_ADD_COMMENT':
135+
case 'GH_PR_EDIT_COMMENT':
136+
return '🔄'; // PR icon
137+
case 'GH_PR_CODE_COMMENT':
138+
return '💬'; // Code comment icon
139+
default:
140+
return '📝'; // Generic comment icon
141+
}
142+
}
143+
144+
buildUrl(context: GitHubContext, withDraft?: boolean): string {
145+
const baseUrl = `https://${context.domain}/${context.slug}`;
146+
147+
if (context.number) {
148+
const type = window.location.pathname.includes('/issues/') ? 'issues' : 'pull';
149+
return `${baseUrl}/${type}/${context.number}${context.commentId ? `#issuecomment-${context.commentId}` : ''}`;
150+
}
151+
152+
return baseUrl;
153+
}
154+
155+
private getCommentId(textarea: HTMLTextAreaElement): string | null {
156+
// Look for edit comment form indicators
157+
const commentForm = textarea.closest('[data-comment-id]');
158+
if (commentForm) {
159+
return commentForm.getAttribute('data-comment-id');
160+
}
161+
162+
const editForm = textarea.closest('.js-comment-edit-form');
163+
if (editForm) {
164+
const commentContainer = editForm.closest('.js-comment-container');
165+
if (commentContainer) {
166+
const id = commentContainer.getAttribute('data-gid') ||
167+
commentContainer.getAttribute('id');
168+
return id ? id.replace('issuecomment-', '') : null;
169+
}
170+
}
171+
172+
return null;
173+
}
174+
}

0 commit comments

Comments
 (0)