Skip to content

Commit ff9d756

Browse files
authored
feat: tab-specific side panel (DAP-4892) (#60)
* feat: tab-specific side panel (DAP-4892) * feat: unsupported page popup * fix: the side panel is opened after second click
1 parent e921356 commit ff9d756

File tree

9 files changed

+181
-35
lines changed

9 files changed

+181
-35
lines changed

apps/extension/manifest.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@
1414
"icons": {
1515
"128": "icons/icon128.png"
1616
},
17-
"side_panel": {
18-
"default_title": "Mutable Web",
19-
"default_path": "sidepanel.html"
17+
"action": {
18+
"default_icon": {
19+
"128": "icons/icon128.png"
20+
}
2021
},
2122
"options_page": "options.html",
2223
"content_scripts": [

apps/extension/src/background/index.ts

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -72,22 +72,25 @@ const setDevServerUrl = async (devServerUrl: string | null): Promise<void> => {
7272
await browser.storage.local.set({ devServerUrl })
7373
}
7474

75-
const toggleSidePanel = async (req?: any) => {
76-
const windowId: number = req?.sender?.tab?.windowId
77-
78-
if (!windowId) return
79-
75+
const _toggleSidePanel = async (tabId: number) => {
8076
// !!! Workaround for user gesture error
8177
// We don't wait for the promise to resolve
8278
const isAlivePromise = SidePanel()
8379
.isAlive()
8480
.then(() => true)
8581
.catch(() => false)
8682

83+
// @ts-ignore
84+
browser.sidePanel.setOptions({
85+
tabId: tabId,
86+
path: 'sidepanel.html',
87+
enabled: true,
88+
})
89+
8790
// Open the side panel in any way
8891
// Don't wait for promise here too
8992
// @ts-ignore
90-
browser.sidePanel.open({ windowId })
93+
browser.sidePanel.open({ tabId })
9194

9295
const isAlive = await isAlivePromise
9396

@@ -97,6 +100,14 @@ const toggleSidePanel = async (req?: any) => {
97100
}
98101
}
99102

103+
const toggleSidePanel = async (req?: any) => {
104+
const tabId: number = req?.sender?.tab?.id
105+
106+
if (!tabId) return
107+
108+
await _toggleSidePanel(tabId)
109+
}
110+
100111
const bgFunctions = {
101112
near_signIn: near.signIn.bind(near),
102113
near_signOut: near.signOut.bind(near),
@@ -317,10 +328,6 @@ browser.runtime.onInstalled.addListener(async () => {
317328
browser.tabs.onActivated.addListener(({ tabId }) => mutationLinkListener(tabId))
318329
browser.tabs.onUpdated.addListener((tabId) => mutationLinkListener(tabId))
319330

320-
// Allows users to open the side panel by clicking on the action toolbar icon
321-
// @ts-ignore
322-
browser.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(console.error)
323-
324331
const portConnectListener = async (port: browser.Runtime.Port) => {
325332
if (port.name === 'port-from-page') {
326333
const signInListener = (params: any) => port.postMessage({ type: 'signedIn', params })
@@ -337,3 +344,37 @@ const portConnectListener = async (port: browser.Runtime.Port) => {
337344
}
338345

339346
browser.runtime.onConnect.addListener(portConnectListener)
347+
348+
async function handleActionClick(tab: browser.Tabs.Tab) {
349+
if (!tab.id) return
350+
await _toggleSidePanel(tab.id)
351+
}
352+
353+
const updateAction = async (tabId: number) => {
354+
const tab = await browser.tabs.get(tabId)
355+
// A normal site where the extension can work
356+
if (tab.id && (tab?.url?.startsWith('https://') || tab?.url?.startsWith('http://'))) {
357+
// The script may not be injected if the extension was just installed
358+
const isContentScriptInjected = await ContentScript(tabId)
359+
.isAlive()
360+
.then(() => true)
361+
.catch(() => false)
362+
363+
if (isContentScriptInjected) {
364+
await browser.action.setPopup({ tabId, popup: '' })
365+
browser.action.onClicked.addListener(handleActionClick)
366+
} else {
367+
const popupUrl = browser.runtime.getURL('popup.html?page=no-cs-injected')
368+
await browser.action.setPopup({ tabId, popup: popupUrl })
369+
browser.action.onClicked.removeListener(handleActionClick)
370+
}
371+
} else {
372+
// If it's a system tab where the extension doesn't work
373+
const popupUrl = browser.runtime.getURL('popup.html?page=unsupported-page')
374+
await browser.action.setPopup({ tabId, popup: popupUrl })
375+
browser.action.onClicked.removeListener(handleActionClick)
376+
}
377+
}
378+
379+
browser.tabs.onActivated.addListener(({ tabId }) => updateAction(tabId))
380+
browser.tabs.onUpdated.addListener((tabId) => updateAction(tabId))

apps/extension/src/popup/app.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import React from 'react'
2+
import { InfoIcon } from './components/info-icon'
3+
4+
export const App: React.FC = () => {
5+
const page = new URLSearchParams(window.location.search).get('page')
6+
switch (page) {
7+
case 'unsupported-page':
8+
return (
9+
<>
10+
<header>
11+
<InfoIcon />
12+
Unsupported page
13+
</header>
14+
<div>You are on an unsupported page</div>
15+
<div>
16+
Mutable Web Extension works on pages with context, for example on{' '}
17+
<a target="_blank" href="https://twitter.com/" rel="noreferrer">
18+
twitter.com
19+
</a>
20+
</div>
21+
</>
22+
)
23+
case 'no-cs-injected':
24+
return (
25+
<>
26+
<header>
27+
<InfoIcon />
28+
Reload page
29+
</header>
30+
<div>
31+
You have installed a new version of the Mutable Web extension. To start working with it,
32+
you need to reload the page
33+
</div>
34+
</>
35+
)
36+
default:
37+
return null
38+
}
39+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import React from 'react'
2+
export const InfoIcon: React.FC = () => (
3+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
4+
<g clipPath="url(#clip0_8_7)">
5+
<path
6+
d="M9.99996 18.3333C14.6023 18.3333 18.3333 14.6024 18.3333 10C18.3333 5.39762 14.6023 1.66666 9.99996 1.66666C5.39759 1.66666 1.66663 5.39762 1.66663 10C1.66663 14.6024 5.39759 18.3333 9.99996 18.3333Z"
7+
stroke="#5AB5E8"
8+
strokeWidth="2"
9+
strokeLinecap="round"
10+
strokeLinejoin="round"
11+
/>
12+
<path
13+
d="M10 13.3333V10"
14+
stroke="#5AB5E8"
15+
strokeWidth="2"
16+
strokeLinecap="round"
17+
strokeLinejoin="round"
18+
/>
19+
<path
20+
d="M10 6.66666H10.0083"
21+
stroke="#5AB5E8"
22+
strokeWidth="2"
23+
strokeLinecap="round"
24+
strokeLinejoin="round"
25+
/>
26+
</g>
27+
<defs>
28+
<clipPath id="clip0_8_7">
29+
<rect width="20" height="20" fill="white" />
30+
</clipPath>
31+
</defs>
32+
</svg>
33+
)

apps/extension/src/popup/index.css

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
body {
2+
display: flex;
3+
flex-direction: column;
4+
width: 305px;
5+
height: 80px;
6+
padding: 6px;
7+
}
8+
header {
9+
display: flex;
10+
align-items: center;
11+
margin-bottom: 6px;
12+
13+
font-size: 14px;
14+
font-weight: 400;
15+
line-height: 149%;
16+
color: #5AB5E8;
17+
}
18+
svg {
19+
margin-right: 5px;
20+
}
21+
div{
22+
font-size: 12px;
23+
font-weight: 400;
24+
font-style: normal;
25+
line-height: 149%;
26+
color: #747376;
27+
}
28+
a{
29+
font-size: 12px;
30+
font-weight: 400;
31+
font-style: normal;
32+
line-height: 149%;
33+
color: #588ca3;
34+
text-decoration-line: underline;
35+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<html>
2+
<body>
3+
<div id="app"></div>
4+
</body>
5+
<script src="./popup.js"></script>
6+
</html>

apps/extension/src/popup/index.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import * as React from 'react'
2+
import * as ReactDOM from 'react-dom'
3+
import { App } from './app'
4+
import './index.css'
5+
6+
const container = document.getElementById('app')
7+
ReactDOM.render(<App />, container)

apps/extension/src/sidepanel/use-current-tab.ts

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { IContextNode, PureContextNode, TransferableContextNode } from '@mweb/core'
2-
import { useCallback, useEffect, useRef, useState } from 'react'
2+
import { useEffect, useRef, useState } from 'react'
33
import browser from 'webextension-polyfill'
44
import ContentScript from '../common/content-script'
55
import { WildcardEventEmitter } from '../common/wildcard-event-emitter'
@@ -89,28 +89,7 @@ export const useCurrentTab = (windowId: number) => {
8989

9090
getCurrentTabId(windowId).then(connectToTab)
9191

92-
const handleTabUpdated = (
93-
tabId: number,
94-
changeInfo: browser.Tabs.OnUpdatedChangeInfoType,
95-
tab: browser.Tabs.Tab
96-
) => {
97-
if (tab.windowId !== windowId) return
98-
if (changeInfo.status && tab.active && tab.status === 'complete') {
99-
connectToTab(tabId)
100-
}
101-
}
102-
103-
const handleTabActivated = (activeInfo: browser.Tabs.OnActivatedActiveInfoType) => {
104-
if (activeInfo.windowId !== windowId) return
105-
connectToTab(activeInfo.tabId)
106-
}
107-
108-
browser.tabs.onActivated.addListener(handleTabActivated)
109-
browser.tabs.onUpdated.addListener(handleTabUpdated) // ToDo: add active check
110-
11192
return () => {
112-
browser.tabs.onActivated.removeListener(handleTabActivated)
113-
browser.tabs.onUpdated.removeListener(handleTabUpdated)
11493
portRef.current?.disconnect()
11594
portRef.current = null
11695
}

apps/extension/webpack.common.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ module.exports = {
5454
'service-worker': path.join(__dirname, 'src/background/index.ts'),
5555
contentscript: path.join(__dirname, 'src/contentscript/index.tsx'),
5656
options: path.join(__dirname, 'src/options/index.tsx'),
57+
popup: path.join(__dirname, 'src/popup/index.tsx'),
5758
},
5859
output: {
5960
path: path.join(__dirname, 'build'),
@@ -144,6 +145,10 @@ module.exports = {
144145
from: 'src/sidepanel/index.html',
145146
to: 'sidepanel.html',
146147
},
148+
{
149+
from: 'src/popup/index.html',
150+
to: 'popup.html',
151+
},
147152
{
148153
from: 'manifest.json',
149154
to: 'manifest.json',

0 commit comments

Comments
 (0)