Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions extension/scripts/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ document.addEventListener('turbo:render', init);
function init() {
injectSingleIssueUI();
injectIssueListUI();
injectConversationUI();
}

/**
Expand Down Expand Up @@ -712,3 +713,106 @@ function getLinkedIssues() {
/** @type {<T>(arg: T) => arg is NonNullable<T>} */ ((a) => Boolean(a))
);
}

/**
* Inject Linear buttons into GitHub conversation threads
*/
async function injectConversationUI() {
const conversations = document.querySelectorAll('.review-thread-component');

for (const conversation of conversations) {
// Skip if we already added any Linear button in this conversation
if (conversation.querySelectorAll('.gh2l-conversation-button').length > 0) continue;

const resolveButton = conversation.querySelector('.review-thread-reply-button');
if (!resolveButton) continue;

// Get the comment link from the form action URL
const commentForm = conversation.querySelector('form.js-comment-update');
const commentId = commentForm?.action.match(/review_comment\/(\d+)/)?.[1];
const commentLink = commentId ? `${window.location.origin}${window.location.pathname}#r${commentId}` : null;
if (!commentLink) continue;

// Get issue metadata for creating Linear issue
const issueMetaData = parseGitHubUrl(location);
if (!issueMetaData) continue;

const identifier = makeGitHubIdentifier(issueMetaData);
const title = `${identifier} - Code Review Comment`;
const description = `GitHub Comment: ${commentLink}`;
const newIssueUrl = await getNewIssueUrl(title, description);

// Create Linear button
const linearButton = h(
'button',
{
class: 'gh2l-conversation-button btn btn-sm ml-1',
style: 'white-space: nowrap;',
},
h(
'span',
{ class: 'gh2l-icon-text-lockup' },
LinearLogo(),
'Add to Linear'
)
);

// Add click handler
linearButton.addEventListener('click', (e) => {
e.preventDefault();
window.open(newIssueUrl, '_blank');
});

// Insert button next to resolve button
resolveButton.insertAdjacentElement('afterend', linearButton);
}
}

// Add observer to handle dynamically loaded conversations
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.addedNodes.length) {
// When new nodes are added, check for new comments
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
// If the added node is a comment
if (node.matches('.timeline-comment')) {
addLinearButtonToComment(node);
}
// If the added node contains comments
const comments = node.querySelectorAll('.timeline-comment');
comments.forEach(comment => {
addLinearButtonToComment(comment);
});
}
});
}
}
});

// Start observing after initial injection
observer.observe(document.body, {
childList: true,
subtree: true,
});

function createLinearButton() {
const button = document.createElement('button');
button.className = 'add-to-linear-button btn-link timeline-comment-action';
// ... rest of button creation code ...
return button;
}

function addLinearButtonToComment(comment) {
// Check if button already exists in this comment
if (comment.querySelector('.add-to-linear-button')) {
return;
}

const actionsContainer = comment.querySelector('.timeline-comment-actions');
if (!actionsContainer) return;

// Create and add the button
const linearButton = createLinearButton();
actionsContainer.insertBefore(linearButton, actionsContainer.firstChild);
}