Skip to content

Commit

Permalink
feat: replace iframe with chrome sidepanel (#5197)
Browse files Browse the repository at this point in the history
fixes - #5201


https://github.com/twentyhq/twenty/assets/13139771/871019c6-6456-46b4-95dd-07ffb33eb4fd

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
  • Loading branch information
AdityaPimpalkar and lucasbordeau committed May 21, 2024
1 parent 4907ae5 commit eb78be6
Show file tree
Hide file tree
Showing 21 changed files with 456 additions and 309 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/options/index.tsx"></script>
<script type="module" src="/src/options/page-inaccessible-index.tsx"></script>
</body>
</html>
</html>
22 changes: 22 additions & 0 deletions packages/twenty-chrome-extension/sidepanel.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/icons/android/android-launchericon-48-48.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Twenty</title>
<style>
/* Reset margin and padding */
html, body {
margin: 0;
padding: 0;
height: 100%; /* Ensure body takes full viewport height */
overflow: hidden; /* Prevents scrollbars from appearing */
}
</style>

</head>
<body>
<div id="app"></div>
<script type="module" src="/src/options/index.tsx"></script>
</body>
</html>
80 changes: 53 additions & 27 deletions packages/twenty-chrome-extension/src/background/index.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,55 @@
import Crypto from 'crypto-js';

import { openOptionsPage } from '~/background/utils/openOptionsPage';
import { exchangeAuthorizationCode } from '~/db/auth.db';
import { isDefined } from '~/utils/isDefined';

// Open options page programmatically in a new tab.
chrome.runtime.onInstalled.addListener((details) => {
if (details.reason === 'install') {
openOptionsPage();
}
});
// chrome.runtime.onInstalled.addListener((details) => {
// if (details.reason === 'install') {
// openOptionsPage();
// }
// });

// Open options page when extension icon is clicked.
chrome.action.onClicked.addListener((tab) => {
chrome.tabs.sendMessage(tab.id ?? 0, { action: 'TOGGLE' });
});
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });

// This listens for an event from other parts of the extension, such as the content script, and performs the required tasks.
// The cases themselves are labelled such that their operations are reflected by their names.
chrome.runtime.onMessage.addListener((message, _, sendResponse) => {
switch (message.action) {
case 'getActiveTab': // e.g. "https://linkedin.com/company/twenty/"
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
if (isDefined(tabs) && isDefined(tabs[0])) {
sendResponse({ tab: tabs[0] });
case 'getActiveTab': {
// e.g. "https://linkedin.com/company/twenty/"
chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => {
if (isDefined(tab) && isDefined(tab.id)) {
sendResponse({ tab });
}
});
break;
case 'openOptionsPage':
openOptionsPage();
break;
case 'CONNECT':
}
case 'launchOAuth': {
launchOAuth(({ status, message }) => {
sendResponse({ status, message });
});
break;
}
case 'openSidepanel': {
chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => {
if (isDefined(tab) && isDefined(tab.id)) {
chrome.sidePanel.open({ tabId: tab.id });
}
});
break;
}
case 'changeSidepanelUrl': {
chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => {
if (isDefined(tab) && isDefined(tab.id)) {
chrome.tabs.sendMessage(tab.id, {
action: 'changeSidepanelUrl',
message,
});
}
});
break;
}
default:
break;
}
Expand Down Expand Up @@ -101,13 +116,16 @@ const launchOAuth = (

callback({ status: true, message: '' });

chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
if (isDefined(tabs) && isDefined(tabs[0])) {
chrome.tabs.sendMessage(tabs[0].id ?? 0, {
action: 'AUTHENTICATED',
});
}
});
chrome.tabs.query(
{ active: true, currentWindow: true },
([tab]) => {
if (isDefined(tab) && isDefined(tab.id)) {
chrome.tabs.sendMessage(tab.id, {
action: 'executeContentScript',
});
}
},
);
}
});
}
Expand All @@ -117,14 +135,22 @@ const launchOAuth = (
});
};

chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
chrome.tabs.onUpdated.addListener(async (tabId, _, tab) => {
const isDesiredRoute =
tab.url?.match(/^https?:\/\/(?:www\.)?linkedin\.com\/company(?:\/\S+)?/) ||
tab.url?.match(/^https?:\/\/(?:www\.)?linkedin\.com\/in(?:\/\S+)?/);

if (changeInfo.status === 'complete' && tab.active) {
if (tab.active === true) {
if (isDefined(isDesiredRoute)) {
chrome.tabs.sendMessage(tabId, { action: 'executeContentScript' });
}
}

await chrome.sidePanel.setOptions({
tabId,
path: tab.url?.match(/^https?:\/\/(?:www\.)?linkedin\.com/)
? 'sidepanel.html'
: 'page-inaccessible.html',
enabled: true,
});
});

This file was deleted.

34 changes: 18 additions & 16 deletions packages/twenty-chrome-extension/src/contentScript/createButton.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { isDefined } from '~/utils/isDefined';

interface CustomDiv extends HTMLDivElement {
onClickHandler: (newHandler: () => void) => void;
}

export const createDefaultButton = (
buttonId: string,
onClickHandler?: () => void,
buttonText = '',
) => {
const btn = document.getElementById(buttonId);
): CustomDiv => {
const btn = document.getElementById(buttonId) as CustomDiv;
if (isDefined(btn)) return btn;
const div = document.createElement('div');
const div = document.createElement('div') as CustomDiv;
const img = document.createElement('img');
const span = document.createElement('span');

Expand Down Expand Up @@ -52,19 +55,18 @@ export const createDefaultButton = (
Object.assign(div.style, divStyles);
});

// Handle the click event.
div.addEventListener('click', async (e) => {
e.preventDefault();
const store = await chrome.storage.local.get();

// If an api key is not set, the options page opens up to allow the user to configure an api key.
if (!store.accessToken) {
chrome.runtime.sendMessage({ action: 'openOptionsPage' });
return;
}
div.onClickHandler = (newHandler) => {
div.onclick = async () => {
const store = await chrome.storage.local.get();

onClickHandler?.();
});
// If an api key is not set, the options page opens up to allow the user to configure an api key.
if (!store.accessToken) {
chrome.runtime.sendMessage({ action: 'openSidepanel' });
return;
}
newHandler();
};
};

div.id = buttonId;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createDefaultButton } from '~/contentScript/createButton';
import changeSidePanelUrl from '~/contentScript/utils/changeSidepanelUrl';
import extractCompanyLinkedinLink from '~/contentScript/utils/extractCompanyLinkedinLink';
import extractDomain from '~/contentScript/utils/extractDomain';
import { createCompany, fetchCompany } from '~/db/company.db';
Expand Down Expand Up @@ -71,27 +72,19 @@ export const addCompany = async () => {
const companyURL = extractCompanyLinkedinLink(activeTab.url);
companyInputData.linkedinLink = { url: companyURL, label: companyURL };

const company = await createCompany(companyInputData);
return company;
const companyId = await createCompany(companyInputData);

if (isDefined(companyId)) {
await changeSidePanelUrl(
`${import.meta.env.VITE_FRONT_BASE_URL}/object/company/${companyId}`,
);
}

return companyId;
};

export const insertButtonForCompany = async () => {
const companyButtonDiv = createDefaultButton(
'twenty-company-btn',
async () => {
if (isDefined(companyButtonDiv)) {
const companyBtnSpan = companyButtonDiv.getElementsByTagName('span')[0];
companyBtnSpan.textContent = 'Saving...';
const company = await addCompany();
if (isDefined(company)) {
companyBtnSpan.textContent = 'Saved';
Object.assign(companyButtonDiv.style, { pointerEvents: 'none' });
} else {
companyBtnSpan.textContent = 'Try again';
}
}
},
);
const companyButtonDiv = createDefaultButton('twenty-company-btn');

const parentDiv: HTMLDivElement | null = document.querySelector(
'.org-top-card-primary-actions__inner',
Expand All @@ -105,13 +98,35 @@ export const insertButtonForCompany = async () => {
parentDiv.prepend(companyButtonDiv);
}

const companyBtnSpan = companyButtonDiv.getElementsByTagName('span')[0];
const companyButtonSpan = companyButtonDiv.getElementsByTagName('span')[0];
const company = await checkIfCompanyExists();

const openCompanyOnSidePanel = (companyId: string) => {
companyButtonSpan.textContent = 'View in Twenty';
companyButtonDiv.onClickHandler(async () => {
await changeSidePanelUrl(
`${import.meta.env.VITE_FRONT_BASE_URL}/object/company/${companyId}`,
);
chrome.runtime.sendMessage({ action: 'openSidepanel' });
});
};

if (isDefined(company)) {
companyBtnSpan.textContent = 'Saved';
Object.assign(companyButtonDiv.style, { pointerEvents: 'none' });
await changeSidePanelUrl(
`${import.meta.env.VITE_FRONT_BASE_URL}/object/company/${company.id}`,
);
if (isDefined(company.id)) openCompanyOnSidePanel(company.id);
} else {
companyBtnSpan.textContent = 'Add to Twenty';
companyButtonSpan.textContent = 'Add to Twenty';

companyButtonDiv.onClickHandler(async () => {
companyButtonSpan.textContent = 'Saving...';
const companyId = await addCompany();
if (isDefined(companyId)) {
openCompanyOnSidePanel(companyId);
} else {
companyButtonSpan.textContent = 'Try again';
}
});
}
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createDefaultButton } from '~/contentScript/createButton';
import changeSidePanelUrl from '~/contentScript/utils/changeSidepanelUrl';
import extractFirstAndLastName from '~/contentScript/utils/extractFirstAndLastName';
import { createPerson, fetchPerson } from '~/db/person.db';
import { PersonInput } from '~/db/types/person.types';
Expand Down Expand Up @@ -82,44 +83,58 @@ export const addPerson = async () => {
}

personData.linkedinLink = { url: activeTabUrl, label: activeTabUrl };
const person = await createPerson(personData);
return person;
const personId = await createPerson(personData);

if (isDefined(personId)) {
await changeSidePanelUrl(
`${import.meta.env.VITE_FRONT_BASE_URL}/object/person/${personId}`,
);
}

return personId;
};

export const insertButtonForPerson = async () => {
const personButtonDiv = createDefaultButton('twenty-person-btn', async () => {
if (isDefined(personButtonDiv)) {
const personBtnSpan = personButtonDiv.getElementsByTagName('span')[0];
personBtnSpan.textContent = 'Saving...';
const person = await addPerson();
if (isDefined(person)) {
personBtnSpan.textContent = 'Saved';
Object.assign(personButtonDiv.style, { pointerEvents: 'none' });
} else {
personBtnSpan.textContent = 'Try again';
}
}
});
const personButtonDiv = createDefaultButton('twenty-person-btn');

if (isDefined(personButtonDiv)) {
const parentDiv: HTMLDivElement | null = document.querySelector(
'.pv-top-card-v2-ctas',
const addedProfileDiv: HTMLDivElement | null = document.querySelector(
'.pv-top-card-v2-ctas__custom',
);

if (isDefined(parentDiv)) {
if (isDefined(addedProfileDiv)) {
Object.assign(personButtonDiv.style, {
marginRight: '.8rem',
});
parentDiv.prepend(personButtonDiv);
addedProfileDiv.prepend(personButtonDiv);
}

const personBtnSpan = personButtonDiv.getElementsByTagName('span')[0];
const personButtonSpan = personButtonDiv.getElementsByTagName('span')[0];
const person = await checkIfPersonExists();

const openPersonOnSidePanel = (personId: string) => {
personButtonSpan.textContent = 'View in Twenty';
personButtonDiv.onClickHandler(async () => {
await changeSidePanelUrl(
`${import.meta.env.VITE_FRONT_BASE_URL}/object/person/${personId}`,
);
chrome.runtime.sendMessage({ action: 'openSidepanel' });
});
};

if (isDefined(person)) {
personBtnSpan.textContent = 'Saved';
Object.assign(personButtonDiv.style, { pointerEvents: 'none' });
await changeSidePanelUrl(
`${import.meta.env.VITE_FRONT_BASE_URL}/object/person/${person.id}`,
);
if (isDefined(person.id)) openPersonOnSidePanel(person.id);
} else {
personBtnSpan.textContent = 'Add to Twenty';
personButtonSpan.textContent = 'Add to Twenty';
personButtonDiv.onClickHandler(async () => {
personButtonSpan.textContent = 'Saving...';
const personId = await addPerson();
if (isDefined(personId)) openPersonOnSidePanel(personId);
else personButtonSpan.textContent = 'Try again';
});
}
}
};

0 comments on commit eb78be6

Please sign in to comment.