Skip to content

Commit 18a896d

Browse files
AdityaPimpalkarlucasbordeau
authored andcommitted
feat: replace iframe with chrome sidepanel (#5197)
fixes - #5201 https://github.com/twentyhq/twenty/assets/13139771/871019c6-6456-46b4-95dd-07ffb33eb4fd --------- Co-authored-by: Lucas Bordeau <[email protected]>
1 parent 50cda93 commit 18a896d

21 files changed

+456
-309
lines changed

packages/twenty-chrome-extension/options.html renamed to packages/twenty-chrome-extension/page-inaccessible.html

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@
77
</head>
88
<body>
99
<div id="app"></div>
10-
<script type="module" src="/src/options/index.tsx"></script>
10+
<script type="module" src="/src/options/page-inaccessible-index.tsx"></script>
1111
</body>
12-
</html>
12+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<html lang="en">
2+
<head>
3+
<meta charset="UTF-8" />
4+
<link rel="icon" href="/icons/android/android-launchericon-48-48.png" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Twenty</title>
7+
<style>
8+
/* Reset margin and padding */
9+
html, body {
10+
margin: 0;
11+
padding: 0;
12+
height: 100%; /* Ensure body takes full viewport height */
13+
overflow: hidden; /* Prevents scrollbars from appearing */
14+
}
15+
</style>
16+
17+
</head>
18+
<body>
19+
<div id="app"></div>
20+
<script type="module" src="/src/options/index.tsx"></script>
21+
</body>
22+
</html>

packages/twenty-chrome-extension/src/background/index.ts

+53-27
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,55 @@
11
import Crypto from 'crypto-js';
22

3-
import { openOptionsPage } from '~/background/utils/openOptionsPage';
43
import { exchangeAuthorizationCode } from '~/db/auth.db';
54
import { isDefined } from '~/utils/isDefined';
65

76
// Open options page programmatically in a new tab.
8-
chrome.runtime.onInstalled.addListener((details) => {
9-
if (details.reason === 'install') {
10-
openOptionsPage();
11-
}
12-
});
7+
// chrome.runtime.onInstalled.addListener((details) => {
8+
// if (details.reason === 'install') {
9+
// openOptionsPage();
10+
// }
11+
// });
1312

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

1915
// This listens for an event from other parts of the extension, such as the content script, and performs the required tasks.
2016
// The cases themselves are labelled such that their operations are reflected by their names.
2117
chrome.runtime.onMessage.addListener((message, _, sendResponse) => {
2218
switch (message.action) {
23-
case 'getActiveTab': // e.g. "https://linkedin.com/company/twenty/"
24-
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
25-
if (isDefined(tabs) && isDefined(tabs[0])) {
26-
sendResponse({ tab: tabs[0] });
19+
case 'getActiveTab': {
20+
// e.g. "https://linkedin.com/company/twenty/"
21+
chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => {
22+
if (isDefined(tab) && isDefined(tab.id)) {
23+
sendResponse({ tab });
2724
}
2825
});
2926
break;
30-
case 'openOptionsPage':
31-
openOptionsPage();
32-
break;
33-
case 'CONNECT':
27+
}
28+
case 'launchOAuth': {
3429
launchOAuth(({ status, message }) => {
3530
sendResponse({ status, message });
3631
});
3732
break;
33+
}
34+
case 'openSidepanel': {
35+
chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => {
36+
if (isDefined(tab) && isDefined(tab.id)) {
37+
chrome.sidePanel.open({ tabId: tab.id });
38+
}
39+
});
40+
break;
41+
}
42+
case 'changeSidepanelUrl': {
43+
chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => {
44+
if (isDefined(tab) && isDefined(tab.id)) {
45+
chrome.tabs.sendMessage(tab.id, {
46+
action: 'changeSidepanelUrl',
47+
message,
48+
});
49+
}
50+
});
51+
break;
52+
}
3853
default:
3954
break;
4055
}
@@ -101,13 +116,16 @@ const launchOAuth = (
101116

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

104-
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
105-
if (isDefined(tabs) && isDefined(tabs[0])) {
106-
chrome.tabs.sendMessage(tabs[0].id ?? 0, {
107-
action: 'AUTHENTICATED',
108-
});
109-
}
110-
});
119+
chrome.tabs.query(
120+
{ active: true, currentWindow: true },
121+
([tab]) => {
122+
if (isDefined(tab) && isDefined(tab.id)) {
123+
chrome.tabs.sendMessage(tab.id, {
124+
action: 'executeContentScript',
125+
});
126+
}
127+
},
128+
);
111129
}
112130
});
113131
}
@@ -117,14 +135,22 @@ const launchOAuth = (
117135
});
118136
};
119137

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

125-
if (changeInfo.status === 'complete' && tab.active) {
143+
if (tab.active === true) {
126144
if (isDefined(isDesiredRoute)) {
127145
chrome.tabs.sendMessage(tabId, { action: 'executeContentScript' });
128146
}
129147
}
148+
149+
await chrome.sidePanel.setOptions({
150+
tabId,
151+
path: tab.url?.match(/^https?:\/\/(?:www\.)?linkedin\.com/)
152+
? 'sidepanel.html'
153+
: 'page-inaccessible.html',
154+
enabled: true,
155+
});
130156
});

packages/twenty-chrome-extension/src/background/utils/openOptionsPage.ts

-5
This file was deleted.

packages/twenty-chrome-extension/src/contentScript/createButton.ts

+18-16
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import { isDefined } from '~/utils/isDefined';
22

3+
interface CustomDiv extends HTMLDivElement {
4+
onClickHandler: (newHandler: () => void) => void;
5+
}
6+
37
export const createDefaultButton = (
48
buttonId: string,
5-
onClickHandler?: () => void,
69
buttonText = '',
7-
) => {
8-
const btn = document.getElementById(buttonId);
10+
): CustomDiv => {
11+
const btn = document.getElementById(buttonId) as CustomDiv;
912
if (isDefined(btn)) return btn;
10-
const div = document.createElement('div');
13+
const div = document.createElement('div') as CustomDiv;
1114
const img = document.createElement('img');
1215
const span = document.createElement('span');
1316

@@ -52,19 +55,18 @@ export const createDefaultButton = (
5255
Object.assign(div.style, divStyles);
5356
});
5457

55-
// Handle the click event.
56-
div.addEventListener('click', async (e) => {
57-
e.preventDefault();
58-
const store = await chrome.storage.local.get();
59-
60-
// If an api key is not set, the options page opens up to allow the user to configure an api key.
61-
if (!store.accessToken) {
62-
chrome.runtime.sendMessage({ action: 'openOptionsPage' });
63-
return;
64-
}
58+
div.onClickHandler = (newHandler) => {
59+
div.onclick = async () => {
60+
const store = await chrome.storage.local.get();
6561

66-
onClickHandler?.();
67-
});
62+
// If an api key is not set, the options page opens up to allow the user to configure an api key.
63+
if (!store.accessToken) {
64+
chrome.runtime.sendMessage({ action: 'openSidepanel' });
65+
return;
66+
}
67+
newHandler();
68+
};
69+
};
6870

6971
div.id = buttonId;
7072

packages/twenty-chrome-extension/src/contentScript/extractCompanyProfile.ts

+37-22
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { createDefaultButton } from '~/contentScript/createButton';
2+
import changeSidePanelUrl from '~/contentScript/utils/changeSidepanelUrl';
23
import extractCompanyLinkedinLink from '~/contentScript/utils/extractCompanyLinkedinLink';
34
import extractDomain from '~/contentScript/utils/extractDomain';
45
import { createCompany, fetchCompany } from '~/db/company.db';
@@ -71,27 +72,19 @@ export const addCompany = async () => {
7172
const companyURL = extractCompanyLinkedinLink(activeTab.url);
7273
companyInputData.linkedinLink = { url: companyURL, label: companyURL };
7374

74-
const company = await createCompany(companyInputData);
75-
return company;
75+
const companyId = await createCompany(companyInputData);
76+
77+
if (isDefined(companyId)) {
78+
await changeSidePanelUrl(
79+
`${import.meta.env.VITE_FRONT_BASE_URL}/object/company/${companyId}`,
80+
);
81+
}
82+
83+
return companyId;
7684
};
7785

7886
export const insertButtonForCompany = async () => {
79-
const companyButtonDiv = createDefaultButton(
80-
'twenty-company-btn',
81-
async () => {
82-
if (isDefined(companyButtonDiv)) {
83-
const companyBtnSpan = companyButtonDiv.getElementsByTagName('span')[0];
84-
companyBtnSpan.textContent = 'Saving...';
85-
const company = await addCompany();
86-
if (isDefined(company)) {
87-
companyBtnSpan.textContent = 'Saved';
88-
Object.assign(companyButtonDiv.style, { pointerEvents: 'none' });
89-
} else {
90-
companyBtnSpan.textContent = 'Try again';
91-
}
92-
}
93-
},
94-
);
87+
const companyButtonDiv = createDefaultButton('twenty-company-btn');
9588

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

108-
const companyBtnSpan = companyButtonDiv.getElementsByTagName('span')[0];
101+
const companyButtonSpan = companyButtonDiv.getElementsByTagName('span')[0];
109102
const company = await checkIfCompanyExists();
110103

104+
const openCompanyOnSidePanel = (companyId: string) => {
105+
companyButtonSpan.textContent = 'View in Twenty';
106+
companyButtonDiv.onClickHandler(async () => {
107+
await changeSidePanelUrl(
108+
`${import.meta.env.VITE_FRONT_BASE_URL}/object/company/${companyId}`,
109+
);
110+
chrome.runtime.sendMessage({ action: 'openSidepanel' });
111+
});
112+
};
113+
111114
if (isDefined(company)) {
112-
companyBtnSpan.textContent = 'Saved';
113-
Object.assign(companyButtonDiv.style, { pointerEvents: 'none' });
115+
await changeSidePanelUrl(
116+
`${import.meta.env.VITE_FRONT_BASE_URL}/object/company/${company.id}`,
117+
);
118+
if (isDefined(company.id)) openCompanyOnSidePanel(company.id);
114119
} else {
115-
companyBtnSpan.textContent = 'Add to Twenty';
120+
companyButtonSpan.textContent = 'Add to Twenty';
121+
122+
companyButtonDiv.onClickHandler(async () => {
123+
companyButtonSpan.textContent = 'Saving...';
124+
const companyId = await addCompany();
125+
if (isDefined(companyId)) {
126+
openCompanyOnSidePanel(companyId);
127+
} else {
128+
companyButtonSpan.textContent = 'Try again';
129+
}
130+
});
116131
}
117132
};

packages/twenty-chrome-extension/src/contentScript/extractPersonProfile.ts

+38-23
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { createDefaultButton } from '~/contentScript/createButton';
2+
import changeSidePanelUrl from '~/contentScript/utils/changeSidepanelUrl';
23
import extractFirstAndLastName from '~/contentScript/utils/extractFirstAndLastName';
34
import { createPerson, fetchPerson } from '~/db/person.db';
45
import { PersonInput } from '~/db/types/person.types';
@@ -82,44 +83,58 @@ export const addPerson = async () => {
8283
}
8384

8485
personData.linkedinLink = { url: activeTabUrl, label: activeTabUrl };
85-
const person = await createPerson(personData);
86-
return person;
86+
const personId = await createPerson(personData);
87+
88+
if (isDefined(personId)) {
89+
await changeSidePanelUrl(
90+
`${import.meta.env.VITE_FRONT_BASE_URL}/object/person/${personId}`,
91+
);
92+
}
93+
94+
return personId;
8795
};
8896

8997
export const insertButtonForPerson = async () => {
90-
const personButtonDiv = createDefaultButton('twenty-person-btn', async () => {
91-
if (isDefined(personButtonDiv)) {
92-
const personBtnSpan = personButtonDiv.getElementsByTagName('span')[0];
93-
personBtnSpan.textContent = 'Saving...';
94-
const person = await addPerson();
95-
if (isDefined(person)) {
96-
personBtnSpan.textContent = 'Saved';
97-
Object.assign(personButtonDiv.style, { pointerEvents: 'none' });
98-
} else {
99-
personBtnSpan.textContent = 'Try again';
100-
}
101-
}
102-
});
98+
const personButtonDiv = createDefaultButton('twenty-person-btn');
10399

104100
if (isDefined(personButtonDiv)) {
105-
const parentDiv: HTMLDivElement | null = document.querySelector(
106-
'.pv-top-card-v2-ctas',
101+
const addedProfileDiv: HTMLDivElement | null = document.querySelector(
102+
'.pv-top-card-v2-ctas__custom',
107103
);
108104

109-
if (isDefined(parentDiv)) {
105+
if (isDefined(addedProfileDiv)) {
110106
Object.assign(personButtonDiv.style, {
111107
marginRight: '.8rem',
112108
});
113-
parentDiv.prepend(personButtonDiv);
109+
addedProfileDiv.prepend(personButtonDiv);
114110
}
115111

116-
const personBtnSpan = personButtonDiv.getElementsByTagName('span')[0];
112+
const personButtonSpan = personButtonDiv.getElementsByTagName('span')[0];
117113
const person = await checkIfPersonExists();
114+
115+
const openPersonOnSidePanel = (personId: string) => {
116+
personButtonSpan.textContent = 'View in Twenty';
117+
personButtonDiv.onClickHandler(async () => {
118+
await changeSidePanelUrl(
119+
`${import.meta.env.VITE_FRONT_BASE_URL}/object/person/${personId}`,
120+
);
121+
chrome.runtime.sendMessage({ action: 'openSidepanel' });
122+
});
123+
};
124+
118125
if (isDefined(person)) {
119-
personBtnSpan.textContent = 'Saved';
120-
Object.assign(personButtonDiv.style, { pointerEvents: 'none' });
126+
await changeSidePanelUrl(
127+
`${import.meta.env.VITE_FRONT_BASE_URL}/object/person/${person.id}`,
128+
);
129+
if (isDefined(person.id)) openPersonOnSidePanel(person.id);
121130
} else {
122-
personBtnSpan.textContent = 'Add to Twenty';
131+
personButtonSpan.textContent = 'Add to Twenty';
132+
personButtonDiv.onClickHandler(async () => {
133+
personButtonSpan.textContent = 'Saving...';
134+
const personId = await addPerson();
135+
if (isDefined(personId)) openPersonOnSidePanel(personId);
136+
else personButtonSpan.textContent = 'Try again';
137+
});
123138
}
124139
}
125140
};

0 commit comments

Comments
 (0)