├── .husky
├── .gitignore
├── pre-commit
└── commit-msg
├── public
├── is.manifest.json
├── logo128.png
├── index.html
├── index.css
├── introduction.html
├── manifest.json
└── overwrite.css
├── .prettierignore
├── .eslintignore
├── assets
├── 01.png
├── 02.png
├── 03.png
├── 04.png
├── 05.png
├── 06.png
└── 07.png
├── .gitignore
├── src
├── lib
│ ├── __mocks__
│ │ └── styleMock.js
│ ├── flip
│ │ └── flip.types.d.ts
│ ├── overlay
│ │ ├── overlay.types.d.ts
│ │ ├── __snapshots__
│ │ │ └── index.spec.tsx.snap
│ │ ├── index.ts
│ │ └── index.spec.tsx
│ ├── augmentations
│ │ └── augmentations.types.d.ts
│ ├── features
│ │ ├── features.types.d.ts
│ │ └── index.tsx
│ ├── gutter
│ │ └── gutter.types.d.ts
│ ├── darkmode
│ │ ├── index.ts
│ │ └── index.spec.ts
│ ├── icon
│ │ ├── index.tsx
│ │ ├── __snapshots__
│ │ │ └── index.spec.tsx.snap
│ │ └── index.spec.tsx
│ ├── user
│ │ ├── index.spec.ts
│ │ └── index.ts
│ ├── keyboard
│ │ └── index.ts
│ ├── expand
│ │ ├── index.ts
│ │ └── index.test.ts
│ └── sidebar
│ │ └── index.spec.ts
├── modules
│ ├── shared
│ │ ├── index.ts
│ │ ├── ShareButton
│ │ │ ├── ShareButton.types.d.ts
│ │ │ └── ShareButton.scss
│ │ └── CustomSidebarIcon
│ │ │ └── CustomSidebarIcon.tsx
│ ├── onboarding
│ │ ├── PrivacyFrame
│ │ │ ├── PrivacyFrame.scss
│ │ │ └── PrivacyFrame.tsx
│ │ ├── index.ts
│ │ ├── WelcomeFrame
│ │ │ ├── WelcomeFrame.scss
│ │ │ └── WelcomeFrame.tsx
│ │ ├── IntroductionPage
│ │ │ ├── IntroductionPage.types.d.ts
│ │ │ ├── IntroductionPage.scss
│ │ │ └── IntroductionPage.tsx
│ │ ├── ToggleAnonymousQueries
│ │ │ └── ToggleAnonymousQueries.scss
│ │ └── QueriesFrame
│ │ │ ├── QueriesFrame.scss
│ │ │ └── QueriesFrame.tsx
│ ├── pages
│ │ ├── FeaturePage
│ │ │ ├── FeaturePage.types.d.ts
│ │ │ └── FeaturePage.tsx
│ │ ├── ActivePage
│ │ │ └── ActivePage.types.d.ts
│ │ ├── index.ts
│ │ ├── SettingsPage
│ │ │ ├── SettingsPage.types.d.ts
│ │ │ ├── SettingsPage.scss
│ │ │ └── SettingsPage.tsx
│ │ ├── GutterPage
│ │ │ ├── GutterPage.types.d.ts
│ │ │ └── GutterPage.scss
│ │ └── BuilderPage
│ │ │ ├── BuilderPage.types.d.ts
│ │ │ └── BuilderPage.scss
│ ├── gutter
│ │ ├── index.ts
│ │ ├── DomainStateCheckbox
│ │ │ ├── DomainStateCheckbox.types.d.ts
│ │ │ └── DomainStateCheckbox.tsx
│ │ ├── LeftActionBar
│ │ │ ├── LeftActionBar.types.d.ts
│ │ │ └── LeftActionBar.scss
│ │ ├── RightActionBar
│ │ │ ├── RightActionBar.types.d.ts
│ │ │ └── RightActionBar.scss
│ │ └── HoverOpenIcon
│ │ │ └── HoverOpenIcon.tsx
│ ├── sidebar
│ │ ├── SidebarTabMeta
│ │ │ ├── SidebarTabMeta.types.d.ts
│ │ │ └── SidebarTabMeta.scss
│ │ ├── SidebarHeader
│ │ │ ├── SidebarHeader.types.d.ts
│ │ │ └── SidebarHeader.scss
│ │ ├── SidebarToggleButton
│ │ │ ├── SidebarToggleButton.types.d.ts
│ │ │ └── SidebarToggleButton.scss
│ │ ├── ActionBar
│ │ │ ├── ActionBar.types.d.ts
│ │ │ └── ActionBar.scss
│ │ ├── SidebarTabContainer
│ │ │ ├── SidebarTabContainer.types.d.ts
│ │ │ └── SidebarTabContainer.tsx
│ │ ├── index.ts
│ │ ├── SidebarTabTitle
│ │ │ ├── SidebarTabTitle.types.d.ts
│ │ │ ├── SidebarTabTitle.scss
│ │ │ └── SidebarTabTitle.tsx
│ │ ├── Sidebar
│ │ │ ├── Sidebar.types.d.ts
│ │ │ └── Sidebar.scss
│ │ └── SidebarTabs
│ │ │ ├── SidebarTabs.types.d.ts
│ │ │ └── SidebarTabs.scss
│ └── builder
│ │ ├── NewActionDropdown
│ │ ├── NewActionDropdown.types.d.ts
│ │ └── NewActionDropdown.tsx
│ │ ├── SearchIntentDropdown
│ │ ├── SearchIntentDropdown.types.d.ts
│ │ └── SearchIntentDropdown.tsx
│ │ ├── AugmentationRow
│ │ └── AugmentationRow.types.d.ts
│ │ ├── DeleteAugmentationButton
│ │ ├── DeleteAugmentationButton.types.d.ts
│ │ └── DeleteAugmentationButton.tsx
│ │ ├── SearchEngineDropdown
│ │ ├── SearchEngineDropdown.types.d.ts
│ │ └── SearchEngineDropdown.tsx
│ │ ├── ActionInput
│ │ ├── ActionInput.types.d.ts
│ │ └── ActionInput.tsx
│ │ ├── NewSearchEngineModal
│ │ ├── NewSearchEngineModal.types.d.ts
│ │ └── NewSearchEngineModal.scss
│ │ ├── MultiValueInput
│ │ ├── MultiValueInput.types.d.ts
│ │ └── MultiValueInput.tsx
│ │ ├── ActionsSection
│ │ ├── ActionsSection.types.d.ts
│ │ └── ActionsSection.tsx
│ │ ├── ConditionInput
│ │ └── ConditionInput.types.d.ts
│ │ ├── EditAugmentationActions
│ │ └── EditAugmentationActions.types.d.ts
│ │ ├── NewConditionDropdown
│ │ └── NewConditionDropdown.types.d.ts
│ │ ├── MetaSection
│ │ ├── MetaSection.types.d.ts
│ │ ├── MetaSection.scss
│ │ └── MetaSection.tsx
│ │ ├── ConditionsSection
│ │ ├── ConditionsSection.types.d.ts
│ │ └── ConditionsSection.tsx
│ │ └── index.ts
├── constant
│ ├── keyboard.ts
│ ├── adblock.ts
│ ├── message.ts
│ └── application.ts
├── styles
│ ├── overwrites.scss
│ ├── mixins.scss
│ ├── animations.scss
│ ├── generics.scss
│ └── variables.scss
└── scripts
│ ├── background
│ └── hot.ts
│ └── content
│ ├── block.ts
│ ├── reorder.ts
│ └── index.tsx
├── .prettierrc
├── jest.setup.js
├── tasks
├── lib
│ └── path.ts
├── tsconfig.json
├── config
│ ├── webpack.prod.ts
│ ├── webpack.dev.ts
│ └── webpack.common.ts
├── README.md
└── scripts
│ ├── post_release.ts
│ ├── build.ts
│ └── watch.ts
├── .lintstagedrc.js
├── manifest
└── firefox.json
├── .gitattributes
├── .editorconfig
├── .github
├── PULL_REQUEST_TEMPLATE.md
└── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
├── jest.config.js
├── PRIVACY.md
├── .versionrc.json
├── tsconfig.json
├── .vscode
├── tasks.json
├── settings.json
└── docs.code-snippets
├── commitlint.config.js
├── .eslintrc
├── docs
└── ARCHITECTURE.md
├── Makefile
└── package.json
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
2 |
--------------------------------------------------------------------------------
/public/is.manifest.json:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | *.manifest.json
2 | src/constant
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | public/
2 | releases/
3 | *.manifest.json
--------------------------------------------------------------------------------
/assets/01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abhinavsharma/hypersearch/HEAD/assets/01.png
--------------------------------------------------------------------------------
/assets/02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abhinavsharma/hypersearch/HEAD/assets/02.png
--------------------------------------------------------------------------------
/assets/03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abhinavsharma/hypersearch/HEAD/assets/03.png
--------------------------------------------------------------------------------
/assets/04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abhinavsharma/hypersearch/HEAD/assets/04.png
--------------------------------------------------------------------------------
/assets/05.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abhinavsharma/hypersearch/HEAD/assets/05.png
--------------------------------------------------------------------------------
/assets/06.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abhinavsharma/hypersearch/HEAD/assets/06.png
--------------------------------------------------------------------------------
/assets/07.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abhinavsharma/hypersearch/HEAD/assets/07.png
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx lint-staged
5 |
--------------------------------------------------------------------------------
/public/logo128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abhinavsharma/hypersearch/HEAD/public/logo128.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | npm-debug.log
2 | node_modules/
3 | /dist*/
4 | /build*/
5 | tmp/
6 | .DS_Store
7 | coverage*
8 | releases*
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx --no-install commitlint --edit "$1"
5 |
--------------------------------------------------------------------------------
/src/lib/__mocks__/styleMock.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | sidebarMaxWidth: '480px',
3 | sidebarStretchedMaxWidth: '900px',
4 | };
5 |
--------------------------------------------------------------------------------
/src/modules/shared/index.ts:
--------------------------------------------------------------------------------
1 | export * from './CustomSidebarIcon/CustomSidebarIcon';
2 | export * from './ShareButton/ShareButton';
3 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": true,
3 | "trailingComma": "all",
4 | "singleQuote": true,
5 | "printWidth": 100,
6 | "tabWidth": 2
7 | }
8 |
--------------------------------------------------------------------------------
/jest.setup.js:
--------------------------------------------------------------------------------
1 | Object.assign(global, require('jest-chrome'));
2 | global.fetch = jest.fn(() => Promise.resolve({
3 | json: async () => {},
4 | }));
--------------------------------------------------------------------------------
/tasks/lib/path.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | src: '../../src',
3 | build: '../../build',
4 | dist: '../../dist',
5 | public: '../../public',
6 | };
7 |
--------------------------------------------------------------------------------
/.lintstagedrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "src/**/*.{ts,tsx}": [
3 | () => "tsc -p tsconfig.json --noEmit",
4 | "eslint src/**/*.ts* --fix"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/src/modules/onboarding/PrivacyFrame/PrivacyFrame.scss:
--------------------------------------------------------------------------------
1 | @use 'styles/variables' as *;
2 |
3 | #privacy-frame-container {
4 | padding: $intro_space_large 0;
5 | }
6 |
--------------------------------------------------------------------------------
/manifest/firefox.json:
--------------------------------------------------------------------------------
1 | {
2 | "applications": {
3 | "gecko": {
4 | "id": "hi@insightbrowser.com"
5 | }
6 | },
7 | "permissions": ["bookmarks"]
8 | }
9 |
--------------------------------------------------------------------------------
/src/modules/pages/FeaturePage/FeaturePage.types.d.ts:
--------------------------------------------------------------------------------
1 | import { FunctionComponent } from 'react';
2 |
3 | declare module './FeaturePage' {
4 | type FeaturePage = FunctionComponent;
5 | }
6 |
--------------------------------------------------------------------------------
/src/lib/flip/flip.types.d.ts:
--------------------------------------------------------------------------------
1 | declare type FlipSidebar = (
2 | document: Document,
3 | force: 'hide' | 'show',
4 | loader: TSidebarLoader,
5 | preventOverlay?: boolean,
6 | ) => void;
7 |
--------------------------------------------------------------------------------
/tasks/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "target": "es5",
6 | "esModuleInterop": true
7 | }
8 | }
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/lib/overlay/overlay.types.d.ts:
--------------------------------------------------------------------------------
1 | declare type CreateResultOverlay = (
2 | serpResult: HTMLElement,
3 | blockingAugmentations: Augmentation[],
4 | details: Record<'text' | 'header' | 'selectorString', string>,
5 | ) => void;
6 |
--------------------------------------------------------------------------------
/src/modules/pages/ActivePage/ActivePage.types.d.ts:
--------------------------------------------------------------------------------
1 | import { FunctionComponent } from 'react';
2 |
3 | declare module './ActivePage' {
4 | type ActivePageProps = any;
5 |
6 | type ActivePage = FunctionComponent;
7 | }
8 |
--------------------------------------------------------------------------------
/tasks/config/webpack.prod.ts:
--------------------------------------------------------------------------------
1 | import { merge } from 'webpack-merge';
2 | import common from './webpack.common';
3 |
4 | export default () =>
5 | merge(common({ mode: 'production', PROJECT: 'is' }), {
6 | mode: 'production',
7 | });
8 |
--------------------------------------------------------------------------------
/src/modules/gutter/index.ts:
--------------------------------------------------------------------------------
1 | export * from './DomainStateCheckbox/DomainStateCheckbox';
2 | export * from './HoverOpenIcon/HoverOpenIcon';
3 | export * from './LeftActionBar/LeftActionBar';
4 | export * from './RightActionBar/RightActionBar';
5 |
--------------------------------------------------------------------------------
/src/modules/pages/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ActivePage/ActivePage';
2 | export * from './BuilderPage/BuilderPage';
3 | export * from './FeaturePage/FeaturePage';
4 | export * from './GutterPage/GutterPage';
5 | export * from './SettingsPage/SettingsPage';
6 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | .vscode/* linguist-documentation
2 | .github/* linguist-documentation
3 | .husky/* linguist-documentation
4 | public/* linguist-documentation
5 | releases/* linguist-documentation
6 | docs/* linguist-documentation
7 | "Insight Extension/*" linguist-documentation
8 |
--------------------------------------------------------------------------------
/src/lib/augmentations/augmentations.types.d.ts:
--------------------------------------------------------------------------------
1 | declare type AugmentationData = {
2 | actions?: ActionObject[];
3 | conditions?: ConditionObject[];
4 | conditionEvaluation?: Condition['evaluate_with'];
5 | description?: string;
6 | name?: string;
7 | isActive?: boolean;
8 | };
9 |
--------------------------------------------------------------------------------
/src/modules/onboarding/index.ts:
--------------------------------------------------------------------------------
1 | export * from './IntroductionPage/IntroductionPage';
2 | export * from './PrivacyFrame/PrivacyFrame';
3 | export * from './QueriesFrame/QueriesFrame';
4 | export * from './ToggleAnonymousQueries/ToggleAnonymousQueries';
5 | export * from './WelcomeFrame/WelcomeFrame';
6 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | indent_style = space
2 | insert_final_newline = true
3 | max_line_length = 120
4 | trim_trailing_whitespace = true
5 |
6 | [*.md]
7 | max_line_length = 0
8 | indent_size = 2
9 | indent_style = space
10 | trim_trailing_whitespace = false
11 |
12 | [COMMIT_EDITMSG]
13 | max_line_length = 0
--------------------------------------------------------------------------------
/src/modules/sidebar/SidebarTabMeta/SidebarTabMeta.types.d.ts:
--------------------------------------------------------------------------------
1 | import { FunctionComponent } from 'react';
2 |
3 | declare module './SidebarTabMeta' {
4 | type SidebarTabMetaProps = {
5 | tab: SidebarTab;
6 | };
7 |
8 | type SidebarTabMeta = FunctionComponent;
9 | }
10 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | **Changes in this PR:**
2 |
3 |
4 | **Bug/Task URL**
5 |
6 |
7 | **Relevant Screenshots:**
8 | Add inline, use a GIF if change is not static
9 |
10 | **How I tested:**
11 |
12 | **Warnings / Notes / Deployment gotchas**
13 |
14 | FYI @archajain @abhinavsharma
15 |
--------------------------------------------------------------------------------
/src/modules/gutter/DomainStateCheckbox/DomainStateCheckbox.types.d.ts:
--------------------------------------------------------------------------------
1 | import { FunctionComponent } from 'react';
2 |
3 | declare module './DomainStateCheckbox' {
4 | type DomainStateCheckboxProps = {
5 | domain: string;
6 | };
7 |
8 | type DomainStateCheckbox = FunctionComponent;
9 | }
10 |
--------------------------------------------------------------------------------
/src/modules/sidebar/SidebarHeader/SidebarHeader.types.d.ts:
--------------------------------------------------------------------------------
1 | import { FunctionComponent } from 'react';
2 |
3 | declare module './SidebarHeader' {
4 | type SidebarTabDomainsSidebarHeaderProps = {
5 | tabs: SidebarTab[];
6 | };
7 |
8 | type SidebarHeader = FunctionComponent;
9 | }
10 |
--------------------------------------------------------------------------------
/src/modules/sidebar/SidebarToggleButton/SidebarToggleButton.types.d.ts:
--------------------------------------------------------------------------------
1 | import { FunctionComponent } from 'react';
2 |
3 | declare module './SidebarToggleButton' {
4 | type SidebarToggleButtonProps = {
5 | tabs: SidebarTab[];
6 | };
7 |
8 | type SidebarToggleButton = FunctionComponent;
9 | }
10 |
--------------------------------------------------------------------------------
/src/modules/onboarding/WelcomeFrame/WelcomeFrame.scss:
--------------------------------------------------------------------------------
1 | @use 'styles/variables' as *;
2 |
3 | #welcome-frame-container {
4 | padding: $intro_space_larger 0;
5 |
6 | #fade-section {
7 | opacity: 0;
8 | transition: opacity 800ms 0s ease-in;
9 |
10 | &.fade-section-visible {
11 | opacity: 1;
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/modules/shared/ShareButton/ShareButton.types.d.ts:
--------------------------------------------------------------------------------
1 | import { FunctionComponent } from 'react';
2 |
3 | declare module './ShareButton' {
4 | type ShareButtonProps = {
5 | augmentation: Augmentation;
6 | icon?: boolean;
7 | disabled?: boolean;
8 | };
9 |
10 | type ShareButton = FunctionComponent;
11 | }
12 |
--------------------------------------------------------------------------------
/src/modules/sidebar/ActionBar/ActionBar.types.d.ts:
--------------------------------------------------------------------------------
1 | import { Dispatch, FunctionComponent, SetStateAction } from 'react';
2 |
3 | declare module './ActionBar' {
4 | type ActionBarProps = {
5 | tab: SidebarTab;
6 | setActiveKey: Dispatch>;
7 | };
8 |
9 | type ActionBar = FunctionComponent;
10 | }
11 |
--------------------------------------------------------------------------------
/src/modules/builder/NewActionDropdown/NewActionDropdown.types.d.ts:
--------------------------------------------------------------------------------
1 | import { FunctionComponent } from 'react';
2 |
3 | declare module './NewActionDropdown' {
4 | type NewActionDropdownProps = {
5 | handleSaveLabel: (label: ActionLabel, key: ActionKey) => void;
6 | };
7 |
8 | type NewActionDropdown = FunctionComponent;
9 | }
10 |
--------------------------------------------------------------------------------
/src/modules/builder/SearchIntentDropdown/SearchIntentDropdown.types.d.ts:
--------------------------------------------------------------------------------
1 | import { FunctionComponent } from 'react';
2 |
3 | declare module './SearchIntentDropdown' {
4 | type SearchIntentDropdownProps = {
5 | newValue: any;
6 | handleSelect: (e: any) => void;
7 | };
8 |
9 | type SearchIntentDropdown = FunctionComponent;
10 | }
11 |
--------------------------------------------------------------------------------
/src/lib/features/features.types.d.ts:
--------------------------------------------------------------------------------
1 | type TFeatureGateProps = {
2 | feature: string;
3 | component: React.ReactNode;
4 | fallback?: React.ReactNode | null | undefined;
5 | };
6 |
7 | declare type Features = Record;
8 |
9 | declare type FeatureEntry = Record<'name' | 'enabled', any>;
10 |
11 | declare type FeatureGate = React.FunctionComponent;
12 |
--------------------------------------------------------------------------------
/src/modules/builder/AugmentationRow/AugmentationRow.types.d.ts:
--------------------------------------------------------------------------------
1 | import { FunctionComponent } from 'react';
2 |
3 | declare module './AugmentationRow' {
4 | type AugmentationRowProps = {
5 | augmentation: Augmentation;
6 | ignored?: boolean;
7 | other?: boolean;
8 | pinned?: boolean;
9 | };
10 |
11 | type AugmentationRow = FunctionComponent;
12 | }
13 |
--------------------------------------------------------------------------------
/src/modules/builder/DeleteAugmentationButton/DeleteAugmentationButton.types.d.ts:
--------------------------------------------------------------------------------
1 | import { FunctionComponent } from 'react';
2 |
3 | declare module './DeleteAugmentationButton' {
4 | type DeleteAugmentationButtonProps = {
5 | augmentation: Augmentation;
6 | disabled: boolean;
7 | };
8 |
9 | type DeleteAugmentationButton = FunctionComponent;
10 | }
11 |
--------------------------------------------------------------------------------
/src/lib/gutter/gutter.types.d.ts:
--------------------------------------------------------------------------------
1 | declare type ProcessSerpResults = (
2 | nodes: HTMLElement[],
3 | selector: string,
4 | details: Record<'text' | 'header' | 'selectorString', string>,
5 | augmentations: Record<'block' | 'search' | 'feature', Record> | null,
6 | createdUrls?: string[],
7 | processAsOpenPage?: boolean,
8 | processAsAdBlock?: boolean,
9 | ) => void;
10 |
--------------------------------------------------------------------------------
/src/modules/builder/SearchEngineDropdown/SearchEngineDropdown.types.d.ts:
--------------------------------------------------------------------------------
1 | import { FunctionComponent } from 'react';
2 |
3 | declare module './SearchEngineDropdown' {
4 | type SearchEngineDropdownProps = {
5 | newValue: any;
6 | handleSelect: (e: any) => void;
7 | placeholder?: string;
8 | };
9 |
10 | type SearchEngineDropdown = FunctionComponent;
11 | }
12 |
--------------------------------------------------------------------------------
/src/modules/sidebar/SidebarTabContainer/SidebarTabContainer.types.d.ts:
--------------------------------------------------------------------------------
1 | import { FunctionComponent } from 'react';
2 |
3 | declare module './SidebarTabContainer' {
4 | type SidebarTabDomainsSidebarTabContainerProps = {
5 | tab: SidebarTab;
6 | isSelected: boolean;
7 | index: string;
8 | };
9 |
10 | type SidebarTabContainer = FunctionComponent;
11 | }
12 |
--------------------------------------------------------------------------------
/src/modules/sidebar/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ActionBar/ActionBar';
2 | export * from './Sidebar/Sidebar';
3 | export * from './SidebarHeader/SidebarHeader';
4 | export * from './SidebarTabContainer/SidebarTabContainer';
5 | export * from './SidebarTabMeta/SidebarTabMeta';
6 | export * from './SidebarTabTitle/SidebarTabTitle';
7 | export * from './SidebarTabs/SidebarTabs';
8 | export * from './SidebarToggleButton/SidebarToggleButton';
9 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest',
3 | testEnvironment: 'jsdom',
4 | moduleDirectories: ['node_modules', 'src'],
5 | moduleNameMapper: {
6 | '\\.(css|less|scss)$': '/src/lib/__mocks__/styleMock.js'
7 | },
8 | setupFilesAfterEnv: ['./jest.setup.js'],
9 | globals: {
10 | 'ts-jest': {
11 | tsconfig: {
12 | sourceMap: true,
13 | },
14 | },
15 | },
16 | };
17 |
--------------------------------------------------------------------------------
/src/modules/builder/ActionInput/ActionInput.types.d.ts:
--------------------------------------------------------------------------------
1 | import { FunctionComponent } from 'react';
2 | import { CustomAction } from 'modules/pages';
3 |
4 | declare module './ActionInput' {
5 | type ActionInputProps = {
6 | action: CustomAction;
7 | saveAction: (e: CustomAction) => void;
8 | deleteAction: (e: CustomAction) => void;
9 | };
10 |
11 | type ActionInput = FunctionComponent;
12 | }
13 |
--------------------------------------------------------------------------------
/src/modules/sidebar/SidebarTabTitle/SidebarTabTitle.types.d.ts:
--------------------------------------------------------------------------------
1 | import { Dispatch, FunctionComponent, SetStateAction } from 'react';
2 |
3 | declare module './SidebarTabTitle' {
4 | type SidebarTabTitleProps = {
5 | tab: SidebarTab;
6 | index: number;
7 | activeKey: string;
8 | setActiveKey: Dispatch>;
9 | };
10 |
11 | type SidebarTabTitle = FunctionComponent;
12 | }
13 |
--------------------------------------------------------------------------------
/src/modules/builder/NewSearchEngineModal/NewSearchEngineModal.types.d.ts:
--------------------------------------------------------------------------------
1 | import { FunctionComponent } from 'react';
2 |
3 | declare module './NewSearchEngineModal' {
4 | type NewSearchEngineModalProps = {
5 | handleSelect: (e: any) => void;
6 | setIsModalVisible: Dispatch>;
7 | isModalVisible: boolean;
8 | };
9 |
10 | type NewSearchEngineModal = FunctionComponent;
11 | }
12 |
--------------------------------------------------------------------------------
/src/modules/builder/MultiValueInput/MultiValueInput.types.d.ts:
--------------------------------------------------------------------------------
1 | import { FunctionComponent } from 'react';
2 |
3 | declare module './MultiValueInput' {
4 | type MultiValueInputProps = {
5 | input: string | string[];
6 | add?: (e: string) => void;
7 | replace?: (e: string[]) => void;
8 | className?: string;
9 | placeholder?: string;
10 | };
11 |
12 | type MultiValueInput = FunctionComponent;
13 | }
14 |
--------------------------------------------------------------------------------
/src/constant/keyboard.ts:
--------------------------------------------------------------------------------
1 | export enum FULLSCREEN_KEY {
2 | KEY = 'F',
3 | CODE = 'KeyF',
4 | }
5 |
6 | export enum EXPAND_KEY {
7 | KEY = 'I',
8 | CODE = 'KeyI',
9 | }
10 |
11 | export enum SHRINK_KEY {
12 | KEY = 'Esc',
13 | CODE = 'Escape',
14 | }
15 |
16 | export enum SWITCH_RIGHT_TAB {
17 | KEY = 'Right',
18 | CODE = 'ArrowRight',
19 | }
20 |
21 | export enum SWITCH_LEFT_TAB {
22 | KEY = 'Left',
23 | CODE = 'ArrowLeft',
24 | }
25 |
--------------------------------------------------------------------------------
/src/modules/pages/SettingsPage/SettingsPage.types.d.ts:
--------------------------------------------------------------------------------
1 | import { FunctionComponent } from 'react';
2 |
3 | declare module './SettingsPage' {
4 | type SettingsContext = {
5 | useServerSuggestions: boolean | undefined;
6 | handlePrivacyChange: (value: boolean | undefined) => Promise;
7 | };
8 |
9 | type SettingsPageProps = {
10 | email?: string;
11 | };
12 |
13 | type SettingsPage = FunctionComponent;
14 | }
15 |
--------------------------------------------------------------------------------
/PRIVACY.md:
--------------------------------------------------------------------------------
1 | Hyperweb does not collect any data of any kind.
2 |
3 | - Hyperweb has analytic hooks in the code but they are disabled by default and only enabled for opt-in users who explicitly consent to sharing data.
4 |
5 | - The only time Hyperweb connects to a remote server is to update the filter lists and other related assets.
6 |
7 | The project is currently hosted on github.com, which is owned by GitHub, Inc., and thus is unrelated to Hyperweb.
8 |
9 | That is all.
--------------------------------------------------------------------------------
/src/modules/gutter/LeftActionBar/LeftActionBar.types.d.ts:
--------------------------------------------------------------------------------
1 | import { FunctionComponent } from 'react';
2 |
3 | declare module './LeftActionBar' {
4 | type LeftActionBarProps = {
5 | publication: string;
6 | container: string;
7 | searchingAugmentations: Augmentation[];
8 | blockingAugmentations: Augmentation[];
9 | featuringAugmentations: Augmentation[];
10 | };
11 |
12 | type LeftActionBar = FunctionComponent;
13 | }
14 |
--------------------------------------------------------------------------------
/src/modules/gutter/RightActionBar/RightActionBar.types.d.ts:
--------------------------------------------------------------------------------
1 | import { FunctionComponent } from 'react';
2 |
3 | declare module './RightActionBar' {
4 | type RightActionBarProps = {
5 | url: string;
6 | container: string;
7 | searchingAugmentations: Augmentation[];
8 | blockingAugmentations: Augmentation[];
9 | featuringAugmentations: Augmentation[];
10 | };
11 |
12 | type RightActionBar = FunctionComponent;
13 | }
14 |
--------------------------------------------------------------------------------
/src/lib/darkmode/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @module lib:darkmode
3 | * @license (C) Insight
4 | * @version 1.0.0
5 | */
6 |
7 | const Darkmode = require('./darkmode');
8 |
9 | class DarkMode {
10 |
11 | public enable = async (document: Document) => {
12 | const darkmode = Darkmode(document);
13 | darkmode.auto(undefined, undefined, true);
14 | };
15 |
16 | }
17 |
18 | /**
19 | * Static instance of the darkmode.
20 | */
21 | const instance = new DarkMode();
22 |
23 | export default instance;
24 |
--------------------------------------------------------------------------------
/src/modules/sidebar/Sidebar/Sidebar.types.d.ts:
--------------------------------------------------------------------------------
1 | import { FunctionComponent, Dispatch, SetStateAction } from 'react';
2 |
3 | declare module './Sidebar' {
4 | type SidebarProps = {
5 | tabs: SidebarTab[];
6 | };
7 |
8 | type Sidebar = FunctionComponent;
9 |
10 | type CloseIconProps = import('antd/lib/button').ButtonProps & {
11 | setForceTab: Dispatch>;
12 | numTabs: number;
13 | };
14 |
15 | type CloseIcon = FunctionComponent;
16 | }
17 |
--------------------------------------------------------------------------------
/src/modules/builder/NewSearchEngineModal/NewSearchEngineModal.scss:
--------------------------------------------------------------------------------
1 | @use 'styles/variables' as *;
2 |
3 | #new-cse-modal {
4 | .ant-modal-mask,
5 | .ant-modal-wrap {
6 | left: 30px;
7 | }
8 |
9 | .ant-modal {
10 | position: absolute;
11 | width: calc(100% - 60px) !important;
12 | left: 30px;
13 | right: 30px;
14 | }
15 |
16 | .modal-content-wrapper {
17 | display: flex;
18 | flex-direction: column;
19 |
20 | input {
21 | margin-top: $sidebar_space_medium;
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/modules/onboarding/IntroductionPage/IntroductionPage.types.d.ts:
--------------------------------------------------------------------------------
1 | import { FunctionComponent } from 'react';
2 |
3 | declare module './IntroductionPage' {
4 | type StepContext = {
5 | currentStep: number;
6 | setCurrentStep: React.Dispatch>;
7 | finished: boolean;
8 | license: string | undefined;
9 | setLicense: React.Dispatch>;
10 | setFinished: React.Dispatch>;
11 | };
12 |
13 | type IntroductionPage = FunctionComponent;
14 | }
15 |
--------------------------------------------------------------------------------
/src/modules/builder/ActionsSection/ActionsSection.types.d.ts:
--------------------------------------------------------------------------------
1 | import { FunctionComponent } from 'react';
2 | import { CustomAction } from 'modules/pages';
3 |
4 | declare module './ActionsSection' {
5 | type TCustomAction = CustomAction;
6 |
7 | type ActionsSectionProps = {
8 | actions: CustomAction[];
9 | onAdd: (action: CustomAction) => void;
10 | onSave: (action: CustomAction) => void;
11 | onDelete: (action: CustomAction) => void;
12 | };
13 |
14 | type ActionsSection = FunctionComponent;
15 | }
16 |
--------------------------------------------------------------------------------
/src/modules/builder/ConditionInput/ConditionInput.types.d.ts:
--------------------------------------------------------------------------------
1 | import { FunctionComponent } from 'react';
2 | import { CustomCondition } from 'modules/pages';
3 |
4 | declare module './ConditionInput' {
5 | type ConditionInputProps = {
6 | condition: CustomCondition;
7 | handleAnyUrl: () => void;
8 | handleAnySearchEngine: () => void;
9 | saveCondition: (e: CustomCondition) => void;
10 | deleteCondition: (e: CustomCondition) => void;
11 | };
12 |
13 | type ConditionInput = FunctionComponent;
14 | }
15 |
--------------------------------------------------------------------------------
/src/modules/pages/GutterPage/GutterPage.types.d.ts:
--------------------------------------------------------------------------------
1 | import { FunctionComponent } from 'react';
2 |
3 | declare module './GutterPage' {
4 | type Section = {
5 | type: 'block' | 'search' | 'local';
6 | augmentations: Array;
7 | title: string;
8 | subtitle: string;
9 | };
10 |
11 | type GutterPageProps = {
12 | hidingAugmentations: Augmentation[];
13 | domain: string;
14 | inline?: boolean;
15 | };
16 |
17 | type GutterPage = FunctionComponent;
18 | }
19 |
--------------------------------------------------------------------------------
/tasks/config/webpack.dev.ts:
--------------------------------------------------------------------------------
1 | import { Configuration } from 'webpack';
2 | import { merge } from 'webpack-merge';
3 | import common from './webpack.common';
4 |
5 | export default (env: { mode: string; PROJECT: 'is' | 'sc' }) =>
6 | merge(common(env), {
7 | mode: 'development',
8 | watch: true,
9 | watchOptions: {
10 | poll: 1000,
11 | followSymlinks: true,
12 | aggregateTimeout: 500,
13 | },
14 | devServer: {
15 | stats: 'errors-only',
16 | },
17 | devtool: 'cheap-module-source-map',
18 | } as Configuration);
19 |
--------------------------------------------------------------------------------
/public/index.css:
--------------------------------------------------------------------------------
1 | iframe {
2 | border: none;
3 | }
4 |
5 | #sidebar-root {
6 | transition-property: width;
7 | transition-duration: 0.5s;
8 | transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
9 | background: transparent;
10 | }
11 |
12 | #sidebar-root-iframe {
13 | position: fixed;
14 | display: block !important;
15 | right: 0;
16 | top: 0;
17 | bottom: 0;
18 | width: 475px;
19 | height: 100%;
20 | border-width: 0 !important;
21 | background: transparent;
22 | border: 0;
23 | }
24 |
25 | #sidebar-root-iframe.hidden {
26 | width: 0;
27 | }
28 |
--------------------------------------------------------------------------------
/src/lib/overlay/__snapshots__/index.spec.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Overlay tests createResultOverlay Should add children elements 1`] = `
4 | ",
8 | }
9 | }
10 | />
11 | `;
12 |
--------------------------------------------------------------------------------
/src/modules/builder/EditAugmentationActions/EditAugmentationActions.types.d.ts:
--------------------------------------------------------------------------------
1 | import { FunctionComponent } from 'react';
2 | import { CustomAction } from 'modules/pages';
3 |
4 | declare module './EditAugmentationActions' {
5 | type TCustomAction = CustomAction;
6 |
7 | type EditAugmentationActionsProps = {
8 | actions: CustomAction[];
9 | onAdd: (action: CustomAction) => void;
10 | onSave: (action: CustomAction) => void;
11 | onDelete: (action: CustomAction) => void;
12 | };
13 |
14 | type EditAugmentationActions = FunctionComponent;
15 | }
16 |
--------------------------------------------------------------------------------
/.versionrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "preMajor": true,
3 | "bumpFiles": [
4 | {
5 | "filename": "public/manifest.json",
6 | "type": "json"
7 | },
8 | {
9 | "filename": "package.json",
10 | "type": "json"
11 | }
12 | ],
13 | "types": [
14 | { "type": "feat", "section": "Features" },
15 | { "type": "fix", "section": "Bug Fixes" },
16 | { "type": "chore", "hidden": true },
17 | { "type": "docs", "hidden": true },
18 | { "type": "style", "hidden": true },
19 | { "type": "refactor", "hidden": true },
20 | { "type": "perf", "hidden": true },
21 | { "type": "test", "hidden": true }
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/src/modules/builder/NewConditionDropdown/NewConditionDropdown.types.d.ts:
--------------------------------------------------------------------------------
1 | import { FunctionComponent } from 'react';
2 |
3 | declare module './NewConditionDropdown' {
4 | type NewConditionDropdownProps = {
5 | newKey: ConditionObjectKey;
6 | handleSaveAnyCondition: (type: 'search' | 'url') => void;
7 | handleSaveNewLabel: (
8 | label: ConditionObjectLabel,
9 | key: ConditionObjectKey | ConditionObjectLegacyKey,
10 | unique_key: ConditionObjectKey,
11 | evaluation: ConditionObjectEvaluation | undefined,
12 | ) => void;
13 | };
14 |
15 | type NewConditionDropdown = FunctionComponent;
16 | }
17 |
--------------------------------------------------------------------------------
/src/lib/darkmode/index.spec.ts:
--------------------------------------------------------------------------------
1 | describe('Darkmode tests', () => {
2 |
3 | afterEach(() => {
4 | jest.resetModules();
5 | });
6 |
7 | test('Darkmode.enable', async () => {
8 | const spy = jest.fn(() => {});
9 |
10 | jest.doMock('./darkmode.js', () => {
11 | return (object: any) => {
12 | expect(object).toBeInstanceOf(Document);
13 | return {
14 | auto: spy,
15 | }
16 | };
17 | });
18 |
19 | const Darkmode = require('.').default;
20 |
21 | // When
22 | Darkmode.enable(document);
23 |
24 | // Then
25 | expect(spy).toHaveBeenCalledWith(undefined, undefined, true);
26 | });
27 |
28 | })
29 |
--------------------------------------------------------------------------------
/src/modules/builder/MetaSection/MetaSection.types.d.ts:
--------------------------------------------------------------------------------
1 | import { ChangeEventHandler, Dispatch, FunctionComponent, SetStateAction } from 'react';
2 | import { CustomAugmentation } from 'modules/pages';
3 |
4 | declare module './MetaSection' {
5 | type MetaSectionProps = {
6 | isNote: boolean;
7 | augmentation: CustomAugmentation;
8 | name: string;
9 | onNameChange: ChangeEventHandler;
10 | description: string;
11 | onDescriptionChange: ChangeEventHandler;
12 | enabled: boolean;
13 | setEnabled: Dispatch>;
14 | };
15 |
16 | type MetaSection = FunctionComponent;
17 | }
18 |
--------------------------------------------------------------------------------
/src/modules/pages/GutterPage/GutterPage.scss:
--------------------------------------------------------------------------------
1 | @use 'styles/variables' as *;
2 |
3 | #gutter-page.sidebar-page {
4 | &.inline-gutter-page-embedded {
5 | position: relative;
6 |
7 | .sidebar-page-wrapper {
8 | position: unset !important;
9 | }
10 | }
11 |
12 | .domain-state-checkbox-container {
13 | padding: $sidebar_space_medium;
14 | }
15 |
16 | .create-local-search-domain-augmentation-button {
17 | text-align: left;
18 | }
19 |
20 | .domain-text {
21 | display: block;
22 | color: $color_text_dark;
23 | font-weight: bold;
24 | margin-right: auto;
25 | font-size: $sidebar_font_large;
26 | margin: $sidebar_space_medium;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/src/modules/shared/CustomSidebarIcon/CustomSidebarIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { SVGAttributes } from 'react';
2 |
3 | export const CustomSidebarIcon = (props: SVGAttributes) => {
4 | return (
5 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/src/modules/sidebar/ActionBar/ActionBar.scss:
--------------------------------------------------------------------------------
1 | @use 'styles/variables' as *;
2 |
3 | #actionbar {
4 | .insight-suggested-tab-popup {
5 | display: flex;
6 | justify-content: space-around;
7 | padding: $sidebar_space_small;
8 | background: $color_background_white;
9 | border-bottom: 1px solid $color_border_medium;
10 |
11 | button,
12 | a {
13 | padding: 0;
14 |
15 | span {
16 | padding: $sidebar_space_small 0;
17 | color: $color_text_medium;
18 | }
19 | }
20 |
21 | .insight-suggested-text {
22 | padding-top: $sidebar_space_small;
23 | }
24 | }
25 |
26 | .tooltip-container {
27 | .ant-tooltip-inner {
28 | min-height: 0;
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/modules/sidebar/SidebarHeader/SidebarHeader.scss:
--------------------------------------------------------------------------------
1 | @use 'styles/variables' as *;
2 |
3 | #sidebar-header {
4 | display: flex;
5 | justify-content: space-between;
6 | height: $sidebar_header_height;
7 | background: $color_background_lighter;
8 |
9 | .app-name-and-feedback {
10 | display: block;
11 | padding: $sidebar_space_small $sidebar_space_medium;
12 |
13 | .app-name {
14 | margin: 0;
15 | }
16 |
17 | .app-feedback {
18 | color: $color_text_medium;
19 | font-size: $sidebar_font_small;
20 | text-decoration: none;
21 | }
22 | }
23 |
24 | #header-actions-container {
25 | display: flex;
26 | justify-content: flex-end;
27 | padding: $sidebar_space_medium $sidebar_space_large;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/modules/builder/ConditionsSection/ConditionsSection.types.d.ts:
--------------------------------------------------------------------------------
1 | import { Dispatch, FunctionComponent, SetStateAction } from 'react';
2 | import { CustomCondition } from 'modules/pages';
3 |
4 | declare module './ConditionsSection' {
5 | type TCustomCondition = CustomCondition;
6 |
7 | type ConditionsSectionProps = {
8 | conditions: CustomCondition[];
9 | setConditions: Dispatch>;
10 | evaluation: ConditionEvaluation;
11 | setEvaluation: Dispatch>;
12 | onAdd: (e: CustomCondition) => void;
13 | onSave: (e: CustomCondition) => void;
14 | onDelete: (e: CustomCondition) => void;
15 | };
16 |
17 | type ConditionsSection = FunctionComponent;
18 | }
19 |
--------------------------------------------------------------------------------
/src/modules/builder/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ActionInput/ActionInput';
2 | export * from './ActionsSection/ActionsSection';
3 | export * from './AugmentationRow/AugmentationRow';
4 | export * from './ConditionInput/ConditionInput';
5 | export * from './ConditionsSection/ConditionsSection';
6 | export * from './DeleteAugmentationButton/DeleteAugmentationButton';
7 | export * from './MetaSection/MetaSection';
8 | export * from './MultiValueInput/MultiValueInput';
9 | export * from './NewActionDropdown/NewActionDropdown';
10 | export * from './NewConditionDropdown/NewConditionDropdown';
11 | export * from './NewSearchEngineModal/NewSearchEngineModal';
12 | export * from './SearchEngineDropdown/SearchEngineDropdown';
13 | export * from './SearchIntentDropdown/SearchIntentDropdown';
14 |
--------------------------------------------------------------------------------
/src/modules/pages/BuilderPage/BuilderPage.types.d.ts:
--------------------------------------------------------------------------------
1 | import { FunctionComponent } from 'react';
2 |
3 | declare module './BuilderPage' {
4 | type CustomAugmentation = T & {
5 | installed?: boolean;
6 | };
7 |
8 | type BuilderPageProps = {
9 | augmentation: Augmentation;
10 | isAdding?: boolean;
11 | };
12 |
13 | type CustomAction = ActionObject & {
14 | id: string;
15 | };
16 |
17 | type CustomCondition = ConditionObject & {
18 | id: string;
19 | };
20 |
21 | type SectionHeaderProps = {
22 | title: string;
23 | tourTitle: string;
24 | tourText: string;
25 | };
26 |
27 | type SectionHeader = FunctionComponent;
28 |
29 | type BuilderPage = FunctionComponent;
30 |
31 | type Header = FunctionComponent;
32 | }
33 |
--------------------------------------------------------------------------------
/tasks/README.md:
--------------------------------------------------------------------------------
1 | # Tasks
2 |
3 | ### Preword
4 |
5 | The application is using [Webpack](https://webpack.js.org/concepts/) to build the source code. In this directory, there are build configurations and scripts providing the development and production processes.
6 |
7 | ### Scripts
8 |
9 | - `make dev` - Prompts for which project you want to work on.
10 |
11 | - `make ship` - Build the production version of **Insight** extension, then create a release and push to GitHub.
12 |
13 | Development configuration creates source-maps and loads the extension. Hot Module Replacement done by a custom background script which is only run when extension is installed via development mode. In production, we omit this functionality.
14 |
15 | Production configuration omits source-maps and HMR server from the build. Also, this will run with `production` flag to minify and optimize the source code.
--------------------------------------------------------------------------------
/src/modules/sidebar/SidebarTabs/SidebarTabs.types.d.ts:
--------------------------------------------------------------------------------
1 | import { FunctionComponent, ReactElement, MouseEvent, Dispatch, SetStateAction } from 'react';
2 |
3 | declare module './SidebarTabs' {
4 | type SidebarTabsProps = {
5 | tabs: SidebarTab[];
6 | activeKey: string;
7 | setActiveKey: Dispatch>;
8 | };
9 |
10 | type TabTitleProps = {
11 | tab: SidebarTab;
12 | active?: boolean;
13 | length: number;
14 | hide: boolean;
15 | onClick?: (e: MouseEvent) => void;
16 | };
17 |
18 | type SidebarTabs = FunctionComponent;
19 |
20 | type TabTitle = FunctionComponent;
21 |
22 | type _TabsProps = import('antd/lib/tabs').TabsProps;
23 |
24 | type TabBar = (
25 | props: _TabsProps,
26 | DefaultTabBar: any,
27 | ) => ReactElement>;
28 | }
29 |
--------------------------------------------------------------------------------
/public/introduction.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 |
12 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/modules/builder/ActionsSection/ActionsSection.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { v4 as uuid } from 'uuid';
3 | import { ActionInput } from 'modules/builder';
4 | import { LEGACY_ACTION_TYPE } from 'constant';
5 | import 'antd/lib/button/style/index.css';
6 | import 'antd/lib/grid/style/index.css';
7 |
8 | export const ActionsSection: ActionsSection = ({ actions, onAdd, onSave, onDelete }) => {
9 | const newAction: TCustomAction = {
10 | id: uuid(),
11 | type: LEGACY_ACTION_TYPE.LIST,
12 | value: [''],
13 | } as any;
14 | return (
15 | <>
16 | {actions.map((action) => (
17 |
18 | ))}
19 |
25 | >
26 | );
27 | };
28 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "./src",
4 | "module": "commonjs",
5 | "target": "es6",
6 | "sourceMap": false,
7 | "moduleResolution": "node",
8 | "jsx": "react",
9 | "skipLibCheck": true,
10 | "lib": [
11 | "esnext",
12 | "dom"
13 | ],
14 | "plugins": [
15 | {
16 | "name": "typescript-plugin-css-modules"
17 | }
18 | ],
19 | "alwaysStrict": true,
20 | "noEmitOnError": true,
21 | "noImplicitAny": true,
22 | "noImplicitThis": true,
23 | "noUnusedLocals": true,
24 | "noUnusedParameters": true,
25 | "esModuleInterop": true,
26 | "strictBindCallApply": true,
27 | "strictNullChecks": true,
28 | "strictFunctionTypes": true,
29 | "strictPropertyInitialization": true
30 | }
31 | }
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | // See https://go.microsoft.com/fwlink/?LinkId=733558
3 | // for the documentation about the tasks.json format
4 | "version": "2.0.0",
5 | "command": "npm",
6 | "tasks": [
7 | {
8 | "label": "install",
9 | "type": "shell",
10 | "command": "npm",
11 | "args": ["install"]
12 | },
13 | {
14 | "label": "update",
15 | "type": "shell",
16 | "command": "npm",
17 | "args": ["update"]
18 | },
19 | {
20 | "label": "test",
21 | "type": "shell",
22 | "command": "npm",
23 | "args": ["run", "test"]
24 | },
25 | {
26 | "label": "build",
27 | "type": "shell",
28 | "group": "build",
29 | "command": "npm",
30 | "args": ["run", "watch"]
31 | }
32 | ]
33 | }
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef */
2 | const Configuration = {
3 | /*
4 | * Resolve and load @commitlint/config-conventional from node_modules.
5 | * Referenced packages must be installed
6 | */
7 | extends: ['@commitlint/config-conventional'],
8 | /*
9 | * Functions that return true if commitlint should ignore the given message.
10 | */
11 | ignores: [(commit) => commit === ''],
12 | /*
13 | * Whether commitlint uses the default ignore rules.
14 | */
15 | defaultIgnores: true,
16 | /*
17 | * Custom URL to show upon failure
18 | */
19 | helpUrl: 'https://github.com/conventional-changelog/commitlint/#what-is-commitlint',
20 | /*
21 | * Custom prompt configs
22 | */
23 | prompt: {
24 | messages: {},
25 | questions: {
26 | type: {
27 | description: 'please input type:',
28 | },
29 | },
30 | },
31 | };
32 |
33 | module.exports = Configuration;
34 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "[javascript]": {
3 | "editor.formatOnSave": false
4 | },
5 | "[javascriptreact]": {
6 | "editor.formatOnSave": false
7 | },
8 | "[typescript]": {
9 | "editor.formatOnSave": false
10 | },
11 | "[typescriptreact]": {
12 | "editor.formatOnSave": false
13 | },
14 | "editor.codeActionsOnSave": {
15 | "source.fixAll.eslint": true
16 | },
17 | "eslint.validate": [
18 | "javascript",
19 | "javascriptreact",
20 | "typescript",
21 | "typescriptreact"
22 | ],
23 | "files.eol": "\n",
24 | "typescript.tsdk": "./node_modules/typescript/lib",
25 | "json.schemas": [
26 | {
27 | "fileMatch": [
28 | "/manifest.json"
29 | ],
30 | "url": "http://json.schemastore.org/chrome-manifest"
31 | }
32 | ],
33 | "cSpell.words": [
34 | "MAILCHIMP"
35 | ]
36 | }
--------------------------------------------------------------------------------
/tasks/scripts/post_release.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import chalk from 'chalk';
3 | import mv from 'mv';
4 | import rimraf from 'rimraf';
5 |
6 | (() => {
7 | const dateTime = new Date().toISOString().split('T');
8 | mv(
9 | 'dist/web-ext-artifacts',
10 | `releases/${dateTime[0]}-${dateTime[1].split(':').slice(0, 2).join('-')}`,
11 | { mkdirp: true },
12 | (err) => {
13 | if (err) {
14 | console.log(chalk.redBright.inverse.bold(`\n FAILED \n`));
15 | console.log(err);
16 | process.exit(1);
17 | }
18 | console.log(chalk.inverse.bold(' --- CREATED PACKAGE --- \n'));
19 | },
20 | );
21 | rimraf('dist/web-ext-artifacts', {}, (err) => {
22 | if (err) {
23 | console.log(chalk.redBright.inverse.bold(`\n FAILED \n`));
24 | console.log(err);
25 | process.exit(1);
26 | }
27 | console.log(chalk.inverse.bold(' --- REMOVED ARTIFACT --- \n'));
28 | });
29 | })();
30 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/src/modules/gutter/HoverOpenIcon/HoverOpenIcon.tsx:
--------------------------------------------------------------------------------
1 | import React, { SVGAttributes } from 'react';
2 |
3 | export const HoverOpenIcon = (props: SVGAttributes) => (
4 |
34 | );
35 |
--------------------------------------------------------------------------------
/src/modules/pages/SettingsPage/SettingsPage.scss:
--------------------------------------------------------------------------------
1 | @use 'styles/variables' as *;
2 |
3 | #settings-page.sidebar-page {
4 | .sidebar-page-wrapper {
5 | .settings-section-content {
6 | width: calc(100% - (2 * #{$sidebar_space_larger}));
7 | padding: $sidebar_space_medium;
8 |
9 | h2 {
10 | margin: $sidebar_space_medium 0;
11 | }
12 |
13 | button {
14 | &.ant-btn-link {
15 | padding-left: 0;
16 | }
17 |
18 | span {
19 | justify-content: center;
20 | }
21 | }
22 |
23 | .settings-logout-row {
24 | display: flex;
25 | width: 100%;
26 | align-items: center;
27 | justify-content: space-between;
28 | }
29 |
30 | .spin {
31 | animation: spin-animation 0.75s infinite;
32 | animation-timing-function: linear;
33 | display: inline-block;
34 | }
35 |
36 | @keyframes spin-animation {
37 | 0% {
38 | transform: rotate(0deg);
39 | }
40 | 100% {
41 | transform: rotate(360deg);
42 | }
43 | }
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/modules/pages/BuilderPage/BuilderPage.scss:
--------------------------------------------------------------------------------
1 | @use 'styles/variables' as *;
2 | @use 'sass:math';
3 |
4 | #builder-page.sidebar-page {
5 | .sidebar-page-wrapper {
6 | padding-top: 0 !important;
7 |
8 | .builder-page-collapse-panel {
9 | .builder-page-collapse-panel-title {
10 | display: inline;
11 | font-weight: bold;
12 | font-size: $sidebar_font_larger;
13 |
14 | .popover-container {
15 | position: absolute;
16 | top: 0px;
17 | left: 0;
18 | z-index: math.abs($sidebar_z_index + 1);
19 | }
20 | }
21 |
22 | .ant-collapse-content-box {
23 | padding: 0;
24 |
25 | .ant-btn-dangerous {
26 | font-size: $icon_large;
27 | padding: 0 $sidebar_space_small;
28 | }
29 |
30 | .augmentation-enabled-switch {
31 | display: flex;
32 | margin-left: auto;
33 | margin-right: $sidebar_space_large;
34 | }
35 | }
36 | }
37 |
38 | .no-condition-text {
39 | display: flex;
40 | justify-content: center;
41 | align-items: center;
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/modules/sidebar/SidebarTabTitle/SidebarTabTitle.scss:
--------------------------------------------------------------------------------
1 | @use 'styles/variables' as *;
2 |
3 | .insight-tab-bar {
4 | .insight-tab-pill {
5 | display: flex;
6 | height: 100%;
7 | width: 100%;
8 | justify-content: center;
9 |
10 | &.hidden {
11 | display: none;
12 | }
13 | }
14 |
15 | .ant-tabs-tab {
16 | display: flex;
17 | align-items: center;
18 | justify-content: center;
19 | width: auto;
20 | padding: 0;
21 | margin: 0;
22 | text-align: center;
23 | font-size: $sidebar_font_medium;
24 | color: $color_text_dark;
25 | background: $color_background_lighter;
26 | border-radius: 0;
27 | border-bottom: 0;
28 | cursor: pointer;
29 | }
30 |
31 | .ant-tabs-tab-active {
32 | color: $color_text_black;
33 | }
34 |
35 | .insight-tab-title {
36 | color: $color_text_medium;
37 | padding: $sidebar_space_small $sidebar_space_medium;
38 | -webkit-filter: grayscale(100%);
39 | filter: grayscale(100%);
40 |
41 | &.hidden {
42 | display: none;
43 | }
44 |
45 | &.active {
46 | color: $color_text_black;
47 | }
48 | }
49 |
50 | .ant-tabs-ink-bar {
51 | background: $color_icon_link;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/lib/icon/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { library } from '@fortawesome/fontawesome-svg-core';
3 | import { fas } from '@fortawesome/free-solid-svg-icons';
4 | import { far } from '@fortawesome/free-regular-svg-icons';
5 | import { fab } from '@fortawesome/free-brands-svg-icons';
6 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
7 | import '@fortawesome/fontawesome/styles.css';
8 |
9 | library.add(fas);
10 | library.add(far);
11 | library.add(fab);
12 |
13 | const faStyle = (style?: string) => {
14 | if (style === 'regular') {
15 | return 'far';
16 | } else if (style === 'brands') {
17 | return 'fab';
18 | }
19 |
20 | return 'fas';
21 | };
22 |
23 | const fontForIcon = (icon: AugmentationIcon) => {
24 | if (icon.font === 'font-awesome') {
25 | const type = faStyle(icon.style);
26 | return ;
27 | }
28 | }
29 |
30 | export const handleIcon = (icon?: AugmentationIcon, emoji?: string, fallbackElement?: any) => {
31 | if (icon) {
32 | return fontForIcon(icon);
33 | }
34 |
35 | if (fallbackElement) {
36 | const FallbackElement = fallbackElement;
37 | return ;
38 | } else {
39 | return emoji;
40 | }
41 | };
42 |
--------------------------------------------------------------------------------
/src/modules/onboarding/ToggleAnonymousQueries/ToggleAnonymousQueries.scss:
--------------------------------------------------------------------------------
1 | @use 'styles/variables' as *;
2 | @use 'sass:math' as *;
3 |
4 | #privacy-toggle-container,
5 | #privacy-frame-container {
6 | display: flex;
7 | flex-direction: column;
8 |
9 | a {
10 | text-decoration: underline;
11 | color: $color_text_white;
12 | }
13 |
14 | h3 {
15 | margin-top: $intro_space_tiny;
16 | font-size: $intro_font_large;
17 | font-weight: 200;
18 | }
19 |
20 | p {
21 | margin-top: 0;
22 | margin-bottom: div($intro_space_tiny, 2);
23 | font-size: $intro_font_smaller;
24 | }
25 |
26 | .privacy-explainer {
27 | max-width: $intro_max_privacy_explainer_width;
28 | }
29 |
30 | .privacy-toggle-button {
31 | margin-top: $intro_space_tiny;
32 | width: 100px;
33 | height: 45px;
34 | background-color: transparent;
35 | border: 1px solid $intro_color_border_opaque;
36 |
37 | .ant-switch-handle {
38 | width: 40px;
39 | height: 40px;
40 |
41 | &:before {
42 | border-radius: 40px;
43 | }
44 | }
45 |
46 | &.ant-switch-checked {
47 | background-color: transparent;
48 |
49 | .ant-switch-handle {
50 | left: calc(100% - 42px);
51 | }
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/modules/gutter/LeftActionBar/LeftActionBar.scss:
--------------------------------------------------------------------------------
1 | @use 'styles/variables' as *;
2 | @use 'styles/generics' as *;
3 | @use 'styles/animations' as *;
4 | @use 'sass:math';
5 |
6 | .insight-gutter-action-bar-left {
7 | position: absolute;
8 | display: block;
9 | margin-left: -100px;
10 | width: 100px;
11 | height: 150px;
12 |
13 | .gutter-icon-container {
14 | opacity: 0;
15 | display: flex;
16 | flex-direction: column;
17 | align-items: center;
18 | width: 100%;
19 | height: 100%;
20 | transition: 100ms all ease-in-out;
21 |
22 | .tooltip-container {
23 | position: absolute;
24 | left: -100px;
25 | right: -100px;
26 |
27 | .gutter-tooltip {
28 | width: 100%;
29 | z-index: math.abs($sidebar_z-index + 100) !important;
30 |
31 | .ant-tooltip-inner {
32 | min-height: 0;
33 | }
34 | }
35 | }
36 |
37 | .publication-time-tracker {
38 | padding: $sidebar_space_small;
39 | color: $color_text_medium;
40 | }
41 |
42 | .gutter-icon {
43 | width: 100px;
44 | height: 150px;
45 | font-size: $icon_larger;
46 | border: 0px;
47 | border-bottom: 0;
48 | box-shadow: none;
49 | pointer-events: all !important;
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/styles/overwrites.scss:
--------------------------------------------------------------------------------
1 | .ant-tooltip-inner {
2 | min-height: 0;
3 | }
4 |
5 | .ant-select-selector {
6 | width: auto !important;
7 | }
8 |
9 | .ant-select-item-option {
10 | pointer-events: auto !important;
11 | align-items: center !important;
12 | }
13 |
14 | .ant-collapse-content {
15 | height: auto !important;
16 |
17 | &.ant-collapse-content-inactive {
18 | height: 0 !important;
19 | }
20 | }
21 |
22 | .ant-tabs-dropdown {
23 | display: none;
24 | }
25 |
26 | .ant-tabs > .ant-tabs-nav .ant-tabs-nav-wrap::before,
27 | .ant-tabs > div > .ant-tabs-nav .ant-tabs-nav-wrap::before {
28 | left: -10px !important;
29 | content: ' ‹' !important;
30 | display: flex;
31 | align-items: center;
32 | justify-content: center;
33 | padding: 3px;
34 | background: rgb(2, 0, 36);
35 | background: linear-gradient(270deg, rgba(239, 239, 239, 0) 0%, rgba(239, 239, 239, 1) 30%);
36 | }
37 |
38 | .ant-tabs > .ant-tabs-nav .ant-tabs-nav-wrap::after,
39 | .ant-tabs > div > .ant-tabs-nav .ant-tabs-nav-wrap::after {
40 | content: '› ' !important;
41 | display: flex;
42 | align-items: center;
43 | justify-content: center;
44 | right: -10px !important;
45 | padding: 3px;
46 | background: rgb(2, 0, 36);
47 | background: linear-gradient(90deg, rgba(239, 239, 239, 0) 0%, rgba(239, 239, 239, 1) 30%);
48 | }
49 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "extends": [
4 | "eslint:recommended",
5 | "plugin:@typescript-eslint/recommended",
6 | "plugin:json/recommended",
7 | "plugin:react/recommended",
8 | "plugin:react-hooks/recommended",
9 | "plugin:prettier/recommended"
10 | ],
11 | "parserOptions": {
12 | "project": "./tsconfig.json",
13 | "tsconfigRootDir": "./",
14 | "ecmaVersion": 2020,
15 | "sourceType": "module",
16 | "ecmaFeatures": {
17 | "jsx": true
18 | }
19 | },
20 | "rules": {
21 | "@typescript-eslint/no-explicit-any": "off",
22 | "@typescript-eslint/no-empty-function": "off",
23 | "@typescript-eslint/explicit-function-return-type": "off",
24 | "@typescript-eslint/explicit-module-boundary-types": "off",
25 | "@typescript-eslint/interface-name-prefix": "off",
26 | "react/prop-types": "off",
27 | "no-console": "error",
28 | "no-else-return": "error",
29 | "no-empty": "error",
30 | "no-new-object": "error",
31 | "no-useless-escape": "off",
32 | "@typescript-eslint/no-unused-vars": [
33 | "error",
34 | {
35 | "argsIgnorePattern": "^_",
36 | "varsIgnorePattern": "^_"
37 | }
38 | ]
39 | },
40 | "settings": {
41 | "react": {
42 | "version": "detect"
43 | }
44 | }
45 | }
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "name": "Hyperweb",
4 | "description": "Hyperweb enhances search with fast previews, trusted sources, muting and filters with useful perspectives. 100% Open Source.",
5 | "version": "0.0.114",
6 | "web_accessible_resources": [
7 | "*.png",
8 | "*.svg",
9 | "*.html",
10 | "*.css"
11 | ],
12 | "icons": {
13 | "128": "logo128.png"
14 | },
15 | "content_scripts": [
16 | {
17 | "matches": [
18 | ""
19 | ],
20 | "js": [
21 | "js/insight_content.js"
22 | ],
23 | "run_at": "document_start",
24 | "all_frames": true
25 | },
26 | {
27 | "matches": [
28 | ""
29 | ],
30 | "css": [
31 | "index.css"
32 | ]
33 | },
34 | {
35 | "matches": [
36 | ""
37 | ],
38 | "css": [
39 | "overwrite.css",
40 | "bundle.css"
41 | ],
42 | "all_frames": true
43 | }
44 | ],
45 | "background": {
46 | "scripts": [
47 | "js/insight_background.js"
48 | ]
49 | },
50 | "browser_action": {
51 | "default_title": "Insight"
52 | },
53 | "permissions": [
54 | "storage",
55 | "",
56 | "webRequest",
57 | "webRequestBlocking",
58 | "webNavigation"
59 | ]
60 | }
61 |
--------------------------------------------------------------------------------
/src/modules/shared/ShareButton/ShareButton.scss:
--------------------------------------------------------------------------------
1 | @use 'styles/variables' as *;
2 | @use 'styles/mixins' as *;
3 |
4 | .share-button-container {
5 | .ant-input-affix-wrapper {
6 | width: 90%;
7 | }
8 |
9 | .tooltip-container {
10 | & > div {
11 | .ant-popover {
12 | display: block;
13 | pointer-events: auto !important;
14 |
15 | .copyable-text {
16 | border: 1px solid $color_border_medium;
17 | padding: $sidebar_space_small;
18 | width: 300px;
19 | }
20 |
21 | .popover-button-container {
22 | display: flex;
23 | flex-direction: column;
24 |
25 | button {
26 | width: 100%;
27 |
28 | span {
29 | padding: $sidebar_space_small !important;
30 | }
31 | }
32 | }
33 |
34 | .popover-title {
35 | @include fullWidthRow;
36 |
37 | .popover-close-button {
38 | display: flex;
39 | margin-left: auto;
40 | margin-right: $sidebar_space_medium;
41 |
42 | span {
43 | color: $color_text_link !important;
44 |
45 | &:hover {
46 | color: $color_text_hover !important;
47 | }
48 | }
49 | }
50 | }
51 | }
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/modules/onboarding/QueriesFrame/QueriesFrame.scss:
--------------------------------------------------------------------------------
1 | @use 'styles/variables' as *;
2 |
3 | #queries-frame-container {
4 | padding: $intro_space_larger 0;
5 |
6 | .ant-collapse-content {
7 | background-color: transparent;
8 | }
9 |
10 | .ant-collapse {
11 | margin-top: $intro_space_tiny;
12 | }
13 |
14 | .step-button {
15 | font-size: $intro_font_small;
16 | height: auto;
17 | margin-top: $intro_space_tiny;
18 | color: $color_text_white;
19 | border-color: $color_border_white;
20 | }
21 |
22 | .ant-list {
23 | a {
24 | font-size: $intro_font_smaller;
25 | text-decoration: underline;
26 | color: $intro_color_text_link;
27 |
28 | &:visited,
29 | &:link {
30 | color: $intro_color_text_link;
31 | }
32 |
33 | &:hover,
34 | &:focus,
35 | &:active {
36 | color: $intro_color_text_hover;
37 | }
38 | }
39 |
40 | .ant-list-header {
41 | display: flex;
42 |
43 | h3,
44 | button {
45 | display: inline;
46 | font-weight: 300;
47 | }
48 |
49 | button {
50 | margin-right: $intro_space_tiny;
51 | color: $intro_color_text_opaque;
52 |
53 | span {
54 | font-size: $intro_font_medium;
55 | }
56 |
57 | &:visited,
58 | &:link {
59 | color: $intro_color_text_opaque;
60 | }
61 |
62 | &:hover,
63 | &:focus,
64 | &:active {
65 | color: $intro_color_text_hover;
66 | }
67 | }
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/modules/builder/MetaSection/MetaSection.scss:
--------------------------------------------------------------------------------
1 | @use 'styles/variables' as *;
2 | @use 'styles/mixins' as *;
3 |
4 | #meta-button-row {
5 | @include fullWidthRow;
6 |
7 | .button-container {
8 | @include centerContent;
9 | width: 50%;
10 | padding: $sidebar_space_small 0 $sidebar_space_larger 0;
11 |
12 | &:first-of-type {
13 | border-right: 1px solid $color_border_light;
14 | }
15 |
16 | button {
17 | display: block;
18 |
19 | & .insight-augmentation-share-button-content,
20 | & .insight-augmentation-delete-button-content {
21 | display: flex;
22 | flex-direction: column;
23 | align-items: center;
24 | font-size: $sidebar_font_largest;
25 |
26 | & > span:last-child {
27 | color: $color_text_black !important;
28 | font-size: $sidebar_font_large;
29 | }
30 |
31 | &.insight-augmentation-delete-button-content {
32 | color: $color_icon_danger !important;
33 |
34 | & > span:last-child {
35 | color: $color_icon_danger !important;
36 | }
37 | }
38 |
39 | &.disabled {
40 | cursor: not-allowed;
41 | color: $color_text_lighter !important;
42 |
43 | & > span:last-child {
44 | color: $color_text_lighter !important;
45 | }
46 | }
47 | }
48 |
49 | &.insight-augmentation-tab-button {
50 | background: $color_background_white;
51 | text-align: left;
52 | font-weight: bold;
53 |
54 | &:hover {
55 | background: $color_background_lighter;
56 | }
57 | }
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/modules/gutter/RightActionBar/RightActionBar.scss:
--------------------------------------------------------------------------------
1 | @use 'styles/variables' as *;
2 | @use 'styles/generics' as *;
3 | @use 'styles/animations' as *;
4 | @use 'sass:math';
5 |
6 | .insight-gutter-action-bar-right {
7 | position: absolute;
8 | display: block;
9 | right: 0;
10 | margin-right: -10px;
11 | width: 60px;
12 |
13 | [insight-show-gutter-icon='true'] {
14 | opacity: 1;
15 |
16 | .gutter-icon-container {
17 | opacity: 1;
18 | cursor: pointer;
19 | }
20 | }
21 |
22 | .gutter-icon-container {
23 | opacity: 0;
24 | display: flex;
25 | flex-direction: column;
26 | justify-content: center;
27 | align-items: center;
28 | width: 100%;
29 | height: 100%;
30 | transition: 100ms all ease-in-out;
31 |
32 | &.has-overlay {
33 | background-color: $color_background_lightest;
34 |
35 | &.dark {
36 | background-color: $color_background_darker;
37 | }
38 | }
39 |
40 | .tooltip-container {
41 | position: absolute;
42 | left: -60px;
43 | right: -60px;
44 |
45 | .gutter-tooltip {
46 | width: 100%;
47 | z-index: math.abs($sidebar_z-index + 100) !important;
48 |
49 | .ant-tooltip-inner {
50 | min-height: 0;
51 | }
52 | }
53 | }
54 |
55 | .publication-time-tracker {
56 | padding: $sidebar_space_small;
57 | color: $color_text_medium;
58 | }
59 |
60 | .gutter-icon {
61 | width: 100px;
62 | height: 150px;
63 | font-size: $icon_larger;
64 | border: 0px;
65 | border-bottom: 0;
66 | box-shadow: none;
67 | pointer-events: all !important;
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/styles/mixins.scss:
--------------------------------------------------------------------------------
1 | @use 'styles/variables' as *;
2 | @use 'sass:math';
3 |
4 | @mixin reset {
5 | padding: 0;
6 | margin: 0;
7 | line-height: 1.3;
8 | font-weight: normal;
9 | list-style: none;
10 | text-align: left;
11 | background: transparent;
12 | }
13 |
14 | @mixin fullWidthRow {
15 | display: flex;
16 | width: 100%;
17 | align-items: center;
18 | }
19 |
20 | @mixin centerBox {
21 | position: absolute;
22 | left: 50%;
23 | transform: translate(-50%, 0);
24 | }
25 |
26 | @mixin centerContent {
27 | display: flex;
28 | align-items: center;
29 | justify-content: center;
30 | }
31 |
32 | @mixin dropdown($width) {
33 | width: #{width} !important;
34 |
35 | &.ant-slide-up-leave,
36 | &.ant-slide-up-leave-active {
37 | opacity: 0;
38 | z-index: -1;
39 | }
40 |
41 | &.ant-slide-up-enter,
42 | &.ant-slide-up-enter-prepare,
43 | &.ant-slide-up-enter-active,
44 | &.ant-slide-up-appear,
45 | &.ant-slide-up-appear-prepare,
46 | &.ant-slide-up-appear-active {
47 | pointer-events: auto !important;
48 | width: 100%;
49 | opacity: 1;
50 | z-index: math.abs($sidebar_z_index + 1);
51 |
52 | .ant-select-item-option {
53 | pointer-events: auto !important;
54 | align-items: center;
55 | }
56 | }
57 | }
58 |
59 | @mixin modal {
60 | .ant-fade-leave,
61 | .ant-fade-leave + .ant-modal-wrap {
62 | opacity: 0;
63 | z-index: -1;
64 | }
65 |
66 | .ant-fade-appear,
67 | .ant-fade-appear + .ant-modal-wrap {
68 | pointer-events: auto !important;
69 | opacity: 1;
70 | z-index: math.abs($sidebar_z_index + 100);
71 | }
72 | }
73 |
74 | @mixin fullWidthDropdown {
75 | @include dropdown(100%);
76 | }
77 |
--------------------------------------------------------------------------------
/src/scripts/background/hot.ts:
--------------------------------------------------------------------------------
1 | import { IN_DEBUG_MODE } from 'constant';
2 |
3 | (() => {
4 | if (!IN_DEBUG_MODE) {
5 | return;
6 | }
7 | const filesInDirectory = (dir: DirectoryEntry): Promise =>
8 | new Promise((resolve) =>
9 | dir.createReader().readEntries((entries) =>
10 | Promise.all(
11 | entries
12 | .filter((e) => e.name[0] !== '.')
13 | .map((e: Entry) =>
14 | e.isDirectory
15 | ? filesInDirectory(e as DirectoryEntry)
16 | : new Promise((resolve) => (e as FileEntry).file(resolve)),
17 | ),
18 | )
19 | .then((files) => Array(0).concat(...files))
20 | .then(resolve),
21 | ),
22 | );
23 |
24 | const timestampForFilesInDirectory = (dir: DirectoryEntry) =>
25 | filesInDirectory(dir).then((files: any[]) =>
26 | files.map((f) => f.name + f.lastModifiedDate).join(),
27 | );
28 |
29 | const watchChanges = (dir: DirectoryEntry, lastTimestamp = '') => {
30 | timestampForFilesInDirectory(dir).then((timestamp) => {
31 | if (!lastTimestamp || lastTimestamp === timestamp) {
32 | setTimeout(() => watchChanges(dir, timestamp), 1000); // retry after 1s
33 | } else {
34 | chrome.runtime.reload();
35 | }
36 | });
37 | };
38 |
39 | chrome.management.getSelf((self) => {
40 | if (self.installType === 'development') {
41 | chrome.runtime.getPackageDirectoryEntry((dir) => watchChanges(dir));
42 | chrome.tabs.query({ lastFocusedWindow: true }, (tabs) => {
43 | tabs.forEach((tab) => tab.id && chrome.tabs.reload(tab.id));
44 | });
45 | }
46 | });
47 | })();
48 |
--------------------------------------------------------------------------------
/docs/ARCHITECTURE.md:
--------------------------------------------------------------------------------
1 | # Lumos extension architecture
2 |
3 | ## User facing components
4 |
5 | ### Sidebar
6 |
7 | Collapsed state
8 |
9 |
10 | Expanded state
11 |
12 |
13 |
14 | What determines what tabs are the sidebar?
15 | 1. The extension calls the subtabs API with the url of the currently loaded page.
16 | 2. The subtabs API returns `suggested_augmentations` and `subtabs`. We ignore subtabs.
17 | 3. suggested_augmentations is the list of JSON blobs, each representing an extension
18 | 4. We only care about the extensions that are custom search engines, we parse them in `handleSubtabApiResponse`
19 |
20 | **What is the return of the subtabs API?**
21 |
22 |
23 | There are 2 main keys to care about
24 |
25 | 1. `suggested_augmentations`: this is a list of mobile extensions. We care about a subset of them, specifically ones with `id` beginning with `cse` representing custom search engines.
26 | 2. `subtabs`: these are URLs that we want the sidebar to render. This is currently NOT TO BE USED. We may bring it back soon though
27 |
28 | **When is the sidebar default expanded vs default collapsed?**
29 | Right now so long as there is at least 1 valid subtab we expand the extension.
30 |
31 | ## Lifecycle of a request
32 | 1. When a page starts to load, then `content_script.js` is injected
33 |
--------------------------------------------------------------------------------
/src/modules/sidebar/SidebarToggleButton/SidebarToggleButton.scss:
--------------------------------------------------------------------------------
1 | @use 'styles/variables' as *;
2 | @use 'styles/animations' as *;
3 |
4 | .insight-sidebar-toggle-button {
5 | @include shakeAnimation;
6 | display: none;
7 | right: 0;
8 | bottom: $sidebar_space_medium;
9 | width: $sidebar_toggle_width !important;
10 | color: $color_background_darker !important;
11 | background: $color_background_white;
12 | font-size: $sidebar_font_medium;
13 | border-right: none;
14 | border-radius: $sidebar_space_medium 0 0 $sidebar_space_medium !important;
15 | box-shadow: 0px 1px 2px 1px rgba(0, 0, 0, 0.2);
16 | cursor: pointer;
17 |
18 | h4 {
19 | font-weight: normal;
20 | }
21 |
22 | .ant-list {
23 | width: 100%;
24 |
25 | .ant-spin-container {
26 | width: $sidebar_toggle_rating_width;
27 | }
28 |
29 | .ant-list-items {
30 | list-style-type: none;
31 | width: 100%;
32 | padding: 0;
33 | margin-left: $sidebar_space_medium;
34 |
35 | .ant-list-item {
36 | padding: 0;
37 | border-bottom: 0;
38 |
39 | .ant-list-item-meta-title {
40 | margin: $sidebar_space_medium;
41 | }
42 | }
43 | }
44 | }
45 |
46 | .insight-sidebar-publication-rating-nub {
47 | font-size: $sidebar_font_medium;
48 | border-right: none;
49 | margin-left: $sidebar_space_large;
50 | }
51 |
52 | .insight-sidebar-toggle-appname {
53 | position: relative;
54 | text-align: center;
55 | font-size: $sidebar_font_medium;
56 | font-weight: bold;
57 | writing-mode: vertical-lr;
58 | padding: $sidebar_space_large;
59 | padding-bottom: $sidebar_space_medium;
60 | border-left: 1px solid $color_border_medium;
61 | transform: rotateZ(180deg);
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/modules/builder/NewActionDropdown/NewActionDropdown.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef } from 'react';
2 | import Select from 'antd/lib/select';
3 | import { ACTION_KEY, ACTION_LABEL } from 'constant';
4 | import 'antd/lib/select/style/index.css';
5 |
6 | /** MAGICS **/
7 | const NEW_ACTION_PLACEHOLDER = 'Add new action';
8 |
9 | const { Option } = Select;
10 |
11 | const ACTIONS: KeyEventMap = {
12 | [ACTION_LABEL.SEARCH_FEATURE]: ACTION_KEY.SEARCH_FEATURE,
13 | [ACTION_LABEL.SEARCH_DOMAINS]: ACTION_KEY.SEARCH_DOMAINS,
14 | [ACTION_LABEL.SEARCH_HIDE_DOMAIN]: ACTION_KEY.SEARCH_HIDE_DOMAIN,
15 | [ACTION_LABEL.SEARCH_APPEND]: ACTION_KEY.SEARCH_APPEND,
16 | [ACTION_LABEL.SEARCH_ALSO]: ACTION_KEY.SEARCH_ALSO,
17 | [ACTION_LABEL.OPEN_URL]: ACTION_KEY.OPEN_URL,
18 | [ACTION_LABEL.OPEN_LINK_CSS]: ACTION_KEY.OPEN_LINK_CSS,
19 | [ACTION_LABEL.NO_COOKIE]: ACTION_KEY.NO_COOKIE,
20 | };
21 |
22 | export const NewActionDropdown: NewActionDropdown = ({ handleSaveLabel }) => {
23 | const dropdownRef = useRef(null);
24 |
25 | const handleLabelChange = (label: ActionLabel) => {
26 | handleSaveLabel(label, ACTIONS[label]);
27 | };
28 |
29 | const getPopupContainer = () => dropdownRef.current as HTMLDivElement;
30 |
31 | return (
32 | <>
33 |
46 |
47 | >
48 | );
49 | };
50 |
--------------------------------------------------------------------------------
/src/modules/onboarding/WelcomeFrame/WelcomeFrame.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, useState } from 'react';
2 | import Typography from 'antd/lib/typography';
3 | import Button from 'antd/lib/button';
4 | import { Helmet } from 'react-helmet';
5 | import Typist from 'react-typist';
6 | import { StepContext } from 'modules/onboarding';
7 | import { APP_NAME } from 'constant';
8 | import 'antd/lib/typography/style/index.css';
9 | import 'antd/lib/button/style/index.css';
10 | import './WelcomeFrame.scss';
11 |
12 | const TAB_TITLE = `Welcome to ${APP_NAME}`;
13 | const NEXT_BUTTON_TEXT = 'Next';
14 | const TYPIST_TEXT = 'Hello';
15 | // ! See: https://www.npmjs.com/package/react-typist#typist-props
16 | const TYPIST_CONFIG = {
17 | cursor: { show: false },
18 | avgTypingDelay: 80,
19 | };
20 |
21 | const { Title } = Typography;
22 |
23 | export const WelcomeFrame = () => {
24 | const [titleLoaded, setTitleLoaded] = useState(false);
25 | const stepContext = useContext(StepContext);
26 | const handleTyped = () => setTitleLoaded(true);
27 | const handleNext = () => stepContext.setCurrentStep(1);
28 |
29 | return (
30 | <>
31 |
32 | {TAB_TITLE}
33 |
34 |
35 |
36 |
37 | {TYPIST_TEXT}
38 |
39 |
40 |
41 |
{TAB_TITLE}
42 |
51 |
52 |
53 | >
54 | );
55 | };
56 |
--------------------------------------------------------------------------------
/src/lib/user/index.spec.ts:
--------------------------------------------------------------------------------
1 | import { SYNC_LAST_USED_TAGS, SYNC_PRIVACY_KEY, SYNC_USER_TAGS } from 'constant';
2 | import { chrome } from 'jest-chrome';
3 | import UserManager from 'lib/user';
4 |
5 | describe('UserManager tests', () => {
6 |
7 | beforeEach(() => {
8 | chrome.storage.sync.get.mockImplementation((_: any, cb: any) => {
9 | cb({});
10 | });
11 | })
12 |
13 | test('addUserTag', async () => {
14 | // Given
15 | expect(UserManager.user.tags).toStrictEqual([]);
16 |
17 | const spy = jest.fn((items: any, cb: any) => {
18 | expect(items[SYNC_USER_TAGS]).toStrictEqual([ 'tag' ]);
19 | cb()
20 | });
21 | chrome.storage.sync.set.mockImplementation(spy);
22 |
23 | // When
24 | UserManager.addUserTag('tag');
25 |
26 | // Then
27 | expect(spy).toHaveBeenCalled();
28 | expect(UserManager.user.tags).toStrictEqual([ 'tag' ]);
29 | });
30 |
31 | test('updateUserPrivacy', async () => {
32 | // Given
33 | expect(UserManager.user.privacy).toBe(undefined);
34 |
35 | const spy = jest.fn((items: any) => {
36 | expect(items[SYNC_PRIVACY_KEY]).toBe(true);
37 | });
38 | chrome.storage.sync.set.mockImplementation(spy);
39 |
40 | // When
41 | UserManager.updateUserPrivacy(true);
42 |
43 | // Then
44 | expect(spy).toHaveBeenCalled();
45 | expect(UserManager.user.privacy).toBe(true);
46 | });
47 |
48 | test('changeLastUsedTags', async () => {
49 | // Given
50 | expect(UserManager.user.lastUsedTags).toStrictEqual([]);
51 |
52 | const spy = jest.fn((items: any) => {
53 | expect(items[SYNC_LAST_USED_TAGS]).toStrictEqual([ 'tag1', 'tag2' ]);
54 | });
55 | chrome.storage.sync.set.mockImplementation(spy);
56 |
57 | // When
58 | UserManager.changeLastUsedTags([ 'tag1', 'tag2' ]);
59 |
60 | // Then
61 | expect(spy).toHaveBeenCalled();
62 | expect(UserManager.user.lastUsedTags).toStrictEqual([ 'tag1', 'tag2' ]);
63 | });
64 |
65 | })
66 |
--------------------------------------------------------------------------------
/src/modules/builder/SearchIntentDropdown/SearchIntentDropdown.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useState } from 'react';
2 | import Select, { OptionProps } from 'antd/lib/select';
3 | import SearchEngineManager from 'lib/engines';
4 | import { SIDEBAR_Z_INDEX } from 'constant';
5 | import 'antd/lib/select/style/index.css';
6 |
7 | /** MAGICS **/
8 | const SEARCH_INTENT_DROPDOWN_LABEL = 'Search for intents...';
9 |
10 | const { Option } = Select;
11 |
12 | export const SearchIntentDropdown: SearchIntentDropdown = ({ newValue, handleSelect }) => {
13 | const [intents, setIntents] = useState();
14 |
15 | const dropdownRef = useRef(null);
16 |
17 | const handleFilter = (inputValue: string, options?: Omit) => {
18 | return (
19 | String(options?.key ?? '')
20 | .toLowerCase()
21 | .search(inputValue.toLowerCase()) > -1
22 | );
23 | };
24 |
25 | useEffect(() => {
26 | setIntents(SearchEngineManager.intents);
27 | // Singleton instance not reinitialized on rerender.
28 | // ! Be careful when updating the dependency list!
29 | // eslint-disable-next-line react-hooks/exhaustive-deps
30 | }, [SearchEngineManager.intents]);
31 |
32 | const getPopupContainer = () => dropdownRef.current as HTMLDivElement;
33 |
34 | return (
35 | <>
36 |
52 |
53 | >
54 | );
55 | };
56 |
--------------------------------------------------------------------------------
/src/modules/builder/MetaSection/MetaSection.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Row from 'antd/lib/row';
3 | import Col from 'antd/lib/col';
4 | import Input from 'antd/lib/input';
5 | import Switch from 'antd/lib/switch';
6 | import { DeleteAugmentationButton } from 'modules/builder';
7 | import { ShareButton } from 'modules/shared';
8 | import { NOTE_TAB_TITLE } from 'constant';
9 | import 'antd/lib/switch/style/index.css';
10 | import 'antd/lib/input/style/index.css';
11 | import 'antd/lib/grid/style/index.css';
12 | import './MetaSection.scss';
13 |
14 | /** MAGICS **/
15 | const NAME_SECTION_LABEL = 'Name';
16 | const DESCRIPTION_SECTION_LABEL = 'Description (optional)';
17 | const ENABLED_SECTION_LABEL = 'Enabled';
18 |
19 | export const MetaSection: MetaSection = ({
20 | isNote,
21 | augmentation,
22 | name,
23 | onNameChange,
24 | description,
25 | onDescriptionChange,
26 | enabled,
27 | setEnabled,
28 | }) => {
29 | return (
30 | <>
31 |
32 | {NAME_SECTION_LABEL}
33 |
34 |
35 |
36 |
37 |
38 | {DESCRIPTION_SECTION_LABEL}
39 |
40 |
41 |
42 |
43 |
44 | {ENABLED_SECTION_LABEL}
45 |
46 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | >
58 | );
59 | };
60 |
--------------------------------------------------------------------------------
/src/modules/sidebar/SidebarTabMeta/SidebarTabMeta.scss:
--------------------------------------------------------------------------------
1 | @use 'styles/variables' as *;
2 |
3 | #tab-meta-container {
4 | display: flex;
5 | border-bottom: 1px solid $color_border_medium;
6 | background: $color_background_lightest;
7 |
8 | #meta-info-icon-container {
9 | margin: auto $sidebar_space_medium;
10 | }
11 |
12 | #sidebar-tab-meta {
13 | display: inline-flex;
14 | flex-wrap: wrap;
15 | align-items: center;
16 | width: 100%;
17 | padding: $sidebar_space_medium 0;
18 |
19 | #domain-title-container {
20 | display: flex;
21 | align-items: center;
22 |
23 | .subtab-open-new-tab {
24 | padding-right: $sidebar_space_larger;
25 | border-right: 1px solid $color_border_medium;
26 | }
27 |
28 | .ant-btn {
29 | padding: 0;
30 | }
31 |
32 | #publication-meta {
33 | display: flex;
34 | flex-direction: column;
35 | align-items: flex-start;
36 | margin-left: $sidebar_space_larger;
37 |
38 | .domain-state-checkbox-container {
39 | padding: 0;
40 | }
41 |
42 | strong {
43 | font-family: $font_mono;
44 | }
45 | }
46 | }
47 |
48 | .meta-text {
49 | color: $color_text_dark;
50 | margin: 0;
51 | padding: 0;
52 | max-width: calc(100% - 60px);
53 |
54 | &.expanded {
55 | display: contents;
56 | }
57 |
58 | .space-right {
59 | margin-right: $sidebar_space_small;
60 | }
61 | }
62 |
63 | .meta-link {
64 | display: inline;
65 | color: $color_text_dark;
66 | text-decoration: underline;
67 | text-decoration-color: $color_border_medium;
68 | cursor: pointer;
69 |
70 | &:hover {
71 | color: $color_text_hover;
72 | text-decoration-color: $color_text_hover;
73 | }
74 | }
75 |
76 | #meta-toggle-button {
77 | display: inline;
78 | height: auto;
79 | padding: 0 $sidebar_space_medium;
80 | }
81 | }
82 | }
83 |
84 | #insight-sidebar-container.insight-expanded {
85 | #tab-meta-container {
86 | .meta-text {
87 | width: 90vw;
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/lib/features/index.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @module lib:features
3 | * @version 1.0.0
4 | * @license (C) Insight
5 | */
6 |
7 | import React, { useCallback, useEffect, useState } from 'react';
8 | import SidebarLoader from 'lib/sidebar';
9 | import { triggerSerpProcessing } from 'lib/helpers';
10 | import { DEV_FEATURE_FLAGS, FEATURE_FLAG_BLOB_URL, UPDATE_SIDEBAR_TABS_MESSAGE } from 'constant';
11 |
12 | /**
13 | * Hook for getting the current state of the specified feature.
14 | *
15 | * @param feature - The name of the feature
16 | * @returns [Status, Setter]
17 | */
18 | export const useFeature = (feature: string) => {
19 | const [localFlags, setLocalFlags] = useState(Object.create(null));
20 | const [features, setFeatures] = useState(Object.create(null));
21 |
22 | const getFeatures = useCallback(async () => {
23 | const remoteBlob = await fetch(FEATURE_FLAG_BLOB_URL, { mode: 'cors' });
24 | const raw = await remoteBlob.json();
25 | const locals = await new Promise>((resolve) =>
26 | chrome.storage.local.get(DEV_FEATURE_FLAGS, resolve),
27 | ).then((data) => data[DEV_FEATURE_FLAGS]);
28 | setLocalFlags(locals);
29 | setFeatures(
30 | Object.assign(
31 | raw['features'].reduce((record: Features, entry: FeatureEntry) => {
32 | record[entry.name] = entry.enabled;
33 | return record;
34 | }, Object.create(null)),
35 | locals,
36 | ),
37 | );
38 | }, []);
39 |
40 | useEffect(() => {
41 | getFeatures();
42 | }, [getFeatures]);
43 |
44 | return [
45 | features[feature],
46 | () => {
47 | chrome.storage.local.set({
48 | [DEV_FEATURE_FLAGS]: {
49 | ...localFlags,
50 | [feature]: !features[feature],
51 | },
52 | });
53 | setFeatures({ ...features, [feature]: !features[feature] });
54 | chrome.runtime.sendMessage({ type: UPDATE_SIDEBAR_TABS_MESSAGE });
55 | triggerSerpProcessing(SidebarLoader, false);
56 | },
57 | ] as [boolean, () => void];
58 | };
59 |
60 | export const FeatureGate: FeatureGate = ({ component, feature, fallback }) => {
61 | const [enabled] = useFeature(feature);
62 | return <>{enabled ? component : fallback ?? null}>;
63 | };
64 |
--------------------------------------------------------------------------------
/src/modules/builder/MultiValueInput/MultiValueInput.tsx:
--------------------------------------------------------------------------------
1 | import React, { Suspense, useState } from 'react';
2 | import Button from 'antd/lib/button';
3 | import Input from 'antd/lib/input';
4 | import 'antd/lib/input/style/index.css';
5 | import 'antd/lib/button/style/index.css';
6 |
7 | const MinusCircleOutlined = React.lazy(
8 | async () => await import('@ant-design/icons/MinusCircleOutlined').then((mod) => mod),
9 | );
10 |
11 | /** MAGICS **/
12 | const ADD_ACTION_VALUE_BUTTON_TEXT = 'Add';
13 |
14 | export const MultiValueInput: MultiValueInput = ({
15 | input,
16 | add,
17 | replace,
18 | className,
19 | placeholder,
20 | }) => {
21 | const [newValue, setNewValues] = useState('');
22 |
23 | const handleSaveValue = () => {
24 | const newValues = input;
25 | if (Array.isArray(newValues) && Array.isArray(input)) {
26 | newValues[input.length] = newValue;
27 | replace?.(newValues);
28 | }
29 | typeof input === 'string' && add?.(input);
30 | setNewValues('');
31 | };
32 |
33 | const handleValueDelete = (removeValue: string) => {
34 | if (Array.isArray(input)) {
35 | replace?.(input.filter((value) => value !== removeValue));
36 | }
37 | };
38 |
39 | const handleAddNewValue = (e: React.ChangeEvent) =>
40 | setNewValues(e.target.value);
41 |
42 | const valueStyle = { display: 'flex', alignItems: 'center' };
43 |
44 | return (
45 |
46 | {typeof input !== 'string' &&
47 | input.map((value, i) => {
48 | const handleDelete = () => handleValueDelete(value);
49 | return (
50 |
51 |
56 | {value}
57 |
58 | );
59 | })}
60 |
67 |
68 | );
69 | };
70 |
--------------------------------------------------------------------------------
/src/scripts/content/block.ts:
--------------------------------------------------------------------------------
1 | import { BLOCKED_ADS } from 'constant';
2 | import { runFunctionWhenDocumentReady } from 'lib/helpers';
3 | import { processSerpResults } from 'lib/gutter';
4 |
5 | declare type AdBlock = {
6 | site: string;
7 | adText: string;
8 | adElementSelector: string;
9 | delay?: number;
10 | }
11 |
12 | ((document) => {
13 | const originalHost = document.location.host.replace(/^www(\d)?\./, '');
14 | const host =
15 | originalHost.search(/google\.[\w]*/) > -1
16 | ? originalHost.replace(/\.[\w.]*$/, '.com')
17 | : originalHost;
18 |
19 | const adBlocks: AdBlock[] = BLOCKED_ADS.filter((adBlock) => {
20 | const site = adBlock.site.split(',');
21 | return site.includes(host);
22 | });
23 |
24 | const runAdBlock = () => {
25 | setTimeout(() => {
26 | adBlocks.forEach((adBlock) => {
27 | const adText = adBlock.adText.split(',');
28 | const adTextContainer = (adBlock as any).adTextContainer ?? 'span';
29 | const adElementSelector = adBlock.adElementSelector;
30 | let node: HTMLElement;
31 | const search = adText.map((adText) => "normalize-space()='" + adText + "'").join(' or ');
32 | const xpath = '//' + adTextContainer + '[' + search + ']';
33 |
34 | const delay = adBlock.delay ?? 0;
35 |
36 | setTimeout(() => {
37 | const matchingElements = document.evaluate(
38 | xpath,
39 | document,
40 | null,
41 | XPathResult.ANY_TYPE,
42 | null,
43 | );
44 | const blockedResults: HTMLElement[] = [];
45 | while ((node = matchingElements.iterateNext() as HTMLElement)) {
46 | node && blockedResults.push(node);
47 | }
48 |
49 | processSerpResults(
50 | blockedResults,
51 | adElementSelector,
52 | {
53 | header: 'Ad',
54 | text: 'Click to show likely ad.',
55 | selectorString: 'blocked-ad',
56 | },
57 | null,
58 | [],
59 | undefined,
60 | true,
61 | );
62 | }, delay);
63 | });
64 | }, 500);
65 | };
66 |
67 | runFunctionWhenDocumentReady(document, runAdBlock);
68 | })(document);
69 |
--------------------------------------------------------------------------------
/tasks/scripts/build.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import webpack, { Compiler, StatsChunkGroup, StatsCompilation } from 'webpack';
3 | import chalk from 'chalk';
4 | import config from '../config/webpack.prod';
5 |
6 | (async () => {
7 | (webpack(config()) as Compiler).run((err, stats) => {
8 | if (err?.message) {
9 | console.log(chalk.redBright('Unexpected error:', err));
10 | process.exit(1);
11 | }
12 |
13 | const errors = stats?.toJson().errors ?? [];
14 |
15 | const statData: StatsCompilation =
16 | stats?.toJson({
17 | colors: true,
18 | assets: true,
19 | builtAt: true,
20 | modules: true,
21 | chunksSort: '!size',
22 | groupAssetsByChunk: true,
23 | }) ?? Object.create(null);
24 |
25 | console.log(chalk.inverse.bold(' --- GENERATING ASSETS --- \n'));
26 | (Object.values(statData.namedChunkGroups ?? Object.create(null)) as StatsChunkGroup[]).forEach(
27 | (entry) => {
28 | entry.name &&
29 | console.log(
30 | chalk.greenBright.bold(entry.name),
31 | chalk[entry.isOverSizeLimit ? 'red' : 'black'].bold(
32 | `${(entry.assetsSize ?? 0) / 1000}KB`,
33 | ),
34 | );
35 | },
36 | );
37 |
38 | if (errors.length) {
39 | console.log(chalk.redBright.inverse.bold(`\n FAILED \n`));
40 | Object.values(
41 | errors.reduce((processed, error) => {
42 | if (error.loc && error.moduleName) {
43 | processed[error.moduleName] ??= [];
44 | processed[error.moduleName].push(error.message);
45 | } else {
46 | processed['unexpected'] ??= [];
47 | processed['unexpected'].push(error.message);
48 | }
49 | return processed;
50 | }, Object.create(null) as Record),
51 | ).forEach((messages) => {
52 | messages.forEach((message) => {
53 | console.log(message);
54 | });
55 | });
56 | } else {
57 | console.log(
58 | chalk.greenBright.inverse.bold(`\n COMPILED `),
59 | chalk.cyanBright.bold(
60 | new Date(statData.builtAt ?? 0).toLocaleTimeString(),
61 | `- Completed in ${(statData.time ?? 0) / 1000} seconds`,
62 | ),
63 | );
64 | }
65 | });
66 | })();
67 |
--------------------------------------------------------------------------------
/src/lib/icon/__snapshots__/index.spec.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Icon tests handleIcon With font-awesome fab 1`] = `
4 |
5 |
22 |
23 | `;
24 |
25 | exports[`Icon tests handleIcon With font-awesome far 1`] = `
26 |
27 |
44 |
45 | `;
46 |
47 | exports[`Icon tests handleIcon With font-awesome fas 1`] = `
48 |
49 |
66 |
67 | `;
68 |
--------------------------------------------------------------------------------
/src/lib/icon/index.spec.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { handleIcon } from 'lib/icon';
3 | import renderer from 'react-test-renderer';
4 |
5 | describe('Icon tests', () => {
6 |
7 | describe('handleIcon', () => {
8 |
9 | test('With font-awesome fas', () => {
10 | // Given
11 | const icon = handleIcon({
12 | name: 'circle',
13 | font: 'font-awesome',
14 | style: 'solid',
15 | });
16 |
17 | // When
18 | const component = renderer.create(
19 | { icon }
20 | );
21 |
22 | const tree = component.toJSON()!;
23 |
24 | // Then
25 | expect(tree).toMatchSnapshot();
26 | });
27 |
28 | test('With font-awesome fab', () => {
29 | // Given
30 | const icon = handleIcon({
31 | name: 'apple',
32 | font: 'font-awesome',
33 | style: 'brands',
34 | });
35 |
36 | // When
37 | const component = renderer.create(
38 | { icon }
39 | );
40 |
41 | const tree = component.toJSON()!;
42 |
43 | // Then
44 | expect(tree).toMatchSnapshot();
45 | });
46 |
47 | test('With font-awesome far', () => {
48 | // Given
49 | const icon = handleIcon({
50 | name: 'circle',
51 | font: 'font-awesome',
52 | style: 'regular',
53 | });
54 |
55 | // When
56 | const component = renderer.create(
57 | { icon }
58 | );
59 |
60 | const tree = component.toJSON()!;
61 |
62 | // Then
63 | expect(tree).toMatchSnapshot();
64 | });
65 |
66 | test('Should fallback to emoji if icon not informed', () => {
67 | // Given
68 | // Initial state
69 |
70 | // When
71 | const fallback = handleIcon(undefined, '😄');
72 |
73 | // Then
74 | expect(typeof fallback).toBe('string');
75 | expect(fallback).toBe('😄');
76 | });
77 |
78 | test('Should fallback to emoji if icon not informed', () => {
79 | // Given
80 | const Element = () => {
81 | return ;
82 | }
83 |
84 | // When
85 | const fallback = handleIcon(undefined, undefined, Element);
86 |
87 | // Then
88 | expect(typeof fallback).toBe('object');
89 | expect(fallback).toStrictEqual();
90 | });
91 |
92 | });
93 |
94 | })
95 |
--------------------------------------------------------------------------------
/src/styles/animations.scss:
--------------------------------------------------------------------------------
1 | #insight-sidebar {
2 | --animation-background-color: #ccc;
3 |
4 | &.dark {
5 | --animation-background-color: #212121;
6 | }
7 | }
8 |
9 | @keyframes shake {
10 | 0% {
11 | transform: translate(1px, 1px) rotate(0deg);
12 | }
13 | 3% {
14 | transform: translate(-1px, -4px) rotate(-1deg);
15 | }
16 | 6% {
17 | transform: translate(-6px, 0px) rotate(1deg);
18 | }
19 | 9% {
20 | transform: translate(6px, 4px) rotate(0deg);
21 | }
22 | 12% {
23 | transform: translate(1px, -1px) rotate(1deg);
24 | }
25 | 15% {
26 | transform: translate(-1px, 4px) rotate(-1deg);
27 | background: var(--animation-background-color);
28 | }
29 | 18% {
30 | transform: translate(-6px, 1px) rotate(0deg);
31 | }
32 | 21% {
33 | transform: translate(6px, 1px) rotate(-1deg);
34 | }
35 | 24% {
36 | transform: translate(-1px, -1px) rotate(1deg);
37 | }
38 | 27% {
39 | transform: translate(1px, 4px) rotate(0deg);
40 | }
41 | 30% {
42 | transform: translate(1px, -4px) rotate(-1deg);
43 | }
44 | 100% {
45 | transform: translate(0px, 0px) rotate(0deg);
46 | }
47 | }
48 |
49 | @keyframes bounceIn {
50 | from,
51 | 20%,
52 | 40%,
53 | 60%,
54 | 80%,
55 | to {
56 | animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
57 | }
58 |
59 | 0% {
60 | opacity: 0;
61 | transform: scale3d(0.3, 0.3, 0.3);
62 | }
63 |
64 | 20% {
65 | transform: scale3d(1.1, 1.1, 1.1);
66 | }
67 |
68 | 40% {
69 | transform: scale3d(0.9, 0.9, 0.9);
70 | }
71 |
72 | 60% {
73 | opacity: 1;
74 | transform: scale3d(1.03, 1.03, 1.03);
75 | }
76 |
77 | 80% {
78 | transform: scale3d(0.97, 0.97, 0.97);
79 | }
80 |
81 | to {
82 | opacity: 1;
83 | transform: scale3d(1, 1, 1);
84 | }
85 | }
86 |
87 | .bounceIn {
88 | animation: bounceIn 1s;
89 | animation-iteration-count: 1;
90 | }
91 |
92 | .insight-tour-shake {
93 | animation: shake 1s;
94 | animation-iteration-count: 2;
95 | animation-delay: 1000ms;
96 | }
97 |
98 | @mixin slideAnimation {
99 | transition-property: width;
100 | transition-duration: 0.5s;
101 | transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
102 | }
103 |
104 | @mixin shakeAnimation {
105 | animation: shake 1s;
106 | animation-iteration-count: 2;
107 | }
108 |
--------------------------------------------------------------------------------
/src/modules/sidebar/SidebarTabs/SidebarTabs.scss:
--------------------------------------------------------------------------------
1 | @use 'styles/variables' as *;
2 |
3 | .insight-tab-container {
4 | display: flex;
5 | flex-direction: column;
6 | min-height: 100vh;
7 | box-shadow: -1px -1px 6px 1px rgba(0, 0, 0, 0.2);
8 |
9 | .ant-tabs-tab + .ant-tabs-tab {
10 | margin: 0;
11 | }
12 |
13 | .insight-tab-bar {
14 | display: flex;
15 | }
16 |
17 | .sidebar-tab-panel {
18 | display: flex;
19 | flex-direction: column;
20 | min-height: 100vh;
21 | }
22 |
23 | .insight-tab-iframe-container {
24 | position: relative;
25 | display: flex;
26 | flex-grow: 1;
27 | justify-content: center;
28 |
29 | @keyframes insight-tab-loader-out {
30 | from {
31 | opacity: 1.0;
32 | }
33 |
34 | to {
35 | opacity: 0.0;
36 | pointer-events: none;
37 | }
38 | }
39 |
40 | .insight-tab-loader {
41 | position: absolute;
42 | width: 100%;
43 | height: 100%;
44 | display: flex;
45 | align-items: center;
46 | justify-content: center;
47 | background: $color_background_saturated;
48 |
49 | .ant-spin {
50 | padding-bottom: 20%;
51 | }
52 |
53 | &.insight-loader-hide {
54 | animation-duration: 0.15s;
55 | animation-delay: 0s;
56 | animation-name: insight-tab-loader-out;
57 | animation-timing-function: ease-out;
58 | animation-fill-mode: forwards;
59 | }
60 | }
61 |
62 | .insight-tab-iframe {
63 | display: flex;
64 | flex-grow: 1;
65 | border: none;
66 | padding: 0 0 $sidebar_space_largest 0;
67 |
68 | &.has-footer {
69 | margin-bottom: 95px;
70 | }
71 |
72 | &.insight-expanded {
73 | width: 100% !important;
74 | max-width: $sidebar_max_page_width !important;
75 | }
76 | }
77 |
78 | .insight-readable-content {
79 | position: absolute;
80 | right: 0;
81 | left: 2px;
82 | bottom: 0;
83 | height: auto;
84 | top: $sidebar_header_height;
85 | padding: $sidebar_space_medium $sidebar_space_large;
86 | background: $color_background_white;
87 | overflow-x: auto;
88 | }
89 | }
90 |
91 | .insight-tab-bar {
92 | margin-bottom: 0 !important;
93 | background: $color_background_lighter;
94 |
95 | .ant-tabs-nav-more {
96 | display: none;
97 | }
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/tasks/scripts/watch.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import webpack, { StatsChunkGroup, StatsCompilation } from 'webpack';
3 | import chalk from 'chalk';
4 | import config from '../config/webpack.dev';
5 |
6 | (async () => {
7 | let built = false;
8 | const project = 'is';
9 | webpack(config({ mode: 'development', PROJECT: project }), (err, stats) => {
10 | if (err?.message) {
11 | console.log(chalk.redBright('Unexpected error:', err));
12 | process.exit(1);
13 | }
14 |
15 | const errors = stats?.toJson().errors ?? [];
16 |
17 | const statData: StatsCompilation =
18 | stats?.toJson({
19 | colors: true,
20 | assets: true,
21 | builtAt: true,
22 | modules: true,
23 | chunksSort: '!size',
24 | groupAssetsByChunk: true,
25 | }) ?? Object.create(null);
26 |
27 | if (!built) {
28 | console.log(chalk.inverse.bold(' --- GENERATING ASSETS --- \n'));
29 | (
30 | Object.values(statData.namedChunkGroups ?? Object.create(null)) as StatsChunkGroup[]
31 | ).forEach((entry) => {
32 | entry.name &&
33 | console.log(
34 | chalk.greenBright.bold(entry.name),
35 | chalk[entry.isOverSizeLimit ? 'red' : 'black'].bold(
36 | `${(entry.assetsSize ?? 0) / 1000}KB`,
37 | ),
38 | );
39 | });
40 | }
41 |
42 | if (errors.length) {
43 | console.log(chalk.redBright.inverse.bold(`\n FAILED \n`));
44 | Object.values(
45 | errors.reduce((processed, error) => {
46 | if (error.loc && error.moduleName) {
47 | processed[error.moduleName] ??= [];
48 | processed[error.moduleName].push(error.message);
49 | } else {
50 | processed['unexpected'] ??= [];
51 | processed['unexpected'].push(error.message);
52 | }
53 | return processed;
54 | }, Object.create(null) as Record),
55 | ).forEach((messages) => {
56 | messages.forEach((message) => {
57 | console.log(message);
58 | });
59 | });
60 | } else {
61 | console.log(
62 | chalk.greenBright.inverse.bold(`\n COMPILED `),
63 | chalk.cyanBright.bold(
64 | new Date(statData.builtAt ?? 0).toLocaleTimeString(),
65 | `- Completed in ${(statData.time ?? 0) / 1000} seconds`,
66 | ),
67 | );
68 | }
69 |
70 | if (!built) {
71 | built = true;
72 | }
73 | });
74 | })();
75 |
--------------------------------------------------------------------------------
/.vscode/docs.code-snippets:
--------------------------------------------------------------------------------
1 | {
2 | //-----------------------------------------------------------------------------------------------
3 | // ! Project Snippets
4 | //-----------------------------------------------------------------------------------------------
5 | // Place your global snippets here. Each snippet is defined under a snippet name and has a scope,
6 | // prefix, body and description. Add comma separated ids of the languages where the snippet is
7 | // applicable in the scope field. If scopeis left empty or omitted, the snippet gets applied to all
8 | // languages. The prefix is what is used to trigger the snippet and the body will be expanded and
9 | // inserted. Possible variables are: $1, $2 for tab stops, $0 for the final cursor position, and
10 | // ${1:label}, ${2:another} for placeholders. Placeholders with the same ids are connected.
11 | // * @example
12 | // * |0| {
13 | // * |1| "description": "@insight/doc/{element}",
14 | // * |2| "prefix": "log",
15 | // * |3| "body": [
16 | // * |4| "console.log('$1');",
17 | // * |5| "$2"
18 | // * |6| ]
19 | // * |7| }
20 | //-----------------------------------------------------------------------------------------------
21 | "Module": {
22 | "description": "@insight/doc/module",
23 | "prefix": "idocm",
24 | "body": [
25 | "/**",
26 | " * @module $1",
27 | " * @version 1.0.0",
28 | " * @license (C) Insight",
29 | " */",
30 | "$2"
31 | ]
32 | },
33 | "Section": {
34 | "description": "@insight/doc/section",
35 | "prefix": "idocs",
36 | "body": [
37 | "//-----------------------------------------------------------------------------------------------",
38 | "// ! $1",
39 | "//-----------------------------------------------------------------------------------------------",
40 | ]
41 | },
42 | "Export": {
43 | "description": "@insight/doc/export",
44 | "prefix": "idoce",
45 | "body": [
46 | "/**",
47 | " * $1",
48 | " * --------------------------------------",
49 | " * $2",
50 | " */",
51 | ]
52 | },
53 | "Line": {
54 | "description": "@insight/doc/line",
55 | "prefix": "idocl",
56 | "body": [
57 | "//-----------------------------------------------------------------------------------------------"
58 | ]
59 | }
60 | }
--------------------------------------------------------------------------------
/src/styles/generics.scss:
--------------------------------------------------------------------------------
1 | @use 'styles/variables' as *;
2 | @use 'styles/mixins' as *;
3 |
4 | .insight-relative {
5 | position: relative;
6 | }
7 |
8 | .insight-list {
9 | display: flex !important;
10 | flex-direction: column !important;
11 | align-items: flex-start;
12 | }
13 |
14 | .insight-row {
15 | display: flex !important;
16 | flex-direction: row !important;
17 | align-items: center;
18 | }
19 |
20 | .insight-no-border {
21 | border: none !important;
22 | }
23 |
24 | .insight-inline-medium-text-button {
25 | margin-left: -$sidebar_space_small;
26 | margin-right: -$sidebar_space_medium;
27 | }
28 |
29 | .insight-full-size-fixed {
30 | position: fixed;
31 | top: 0;
32 | bottom: 0;
33 | right: 0;
34 | }
35 |
36 | .insight-large-input-row {
37 | display: flex;
38 | align-items: center;
39 | height: auto;
40 | min-height: 30px;
41 | padding: $sidebar_space_medium;
42 | border-bottom: 1px solid $color_border_light;
43 |
44 | .insight-large-input-row-content:not(.insight-list) {
45 | display: flex;
46 | align-items: center;
47 | font-weight: normal !important;
48 | }
49 | }
50 |
51 | .insight-full-width {
52 | width: 100%;
53 | }
54 |
55 | .insight-full-width-dropdown {
56 | @include fullWidthDropdown;
57 | }
58 |
59 | .insight-create-lens {
60 | width: 95%;
61 | margin: auto;
62 | }
63 |
64 | .insight-create-note {
65 | width: 95%;
66 | margin: auto;
67 | align-items: center;
68 | display: flex;
69 | justify-content: center;
70 | }
71 |
72 | .insight-augmentation-row {
73 | display: flex;
74 | align-items: center;
75 | margin: auto;
76 | min-height: 50px;
77 | width: 95%;
78 | background: $color_background_white;
79 | text-align: left;
80 |
81 | .insight-augmentation-row-name {
82 | display: flex;
83 | width: 320px;
84 | flex-direction: column;
85 |
86 | .insight-augmentation-row-extra {
87 | color: $color_text_medium;
88 | font-size: $sidebar_font_small;
89 | }
90 | }
91 |
92 | .insight-augmentation-row-button {
93 | cursor: pointer;
94 |
95 | &:first-of-type {
96 | margin-left: auto;
97 | }
98 |
99 | &:last-child {
100 | margin-right: $sidebar_space_medium;
101 | }
102 | }
103 | }
104 |
105 | .insight-frame-overlay {
106 | display: block;
107 | position: absolute;
108 | inset: unset;
109 | width: calc(100% - #{(2 * $sidebar_space_medium)});
110 | padding: $sidebar_space_medium;
111 | background: $color_background_lighter;
112 | }
113 |
--------------------------------------------------------------------------------
/src/lib/keyboard/index.ts:
--------------------------------------------------------------------------------
1 | import { getFirstValidTabIndex, getLastValidTabIndex, shouldPreventEventBubble } from 'lib/helpers';
2 | import { expandSidebar } from 'lib/expand';
3 | import { flipSidebar } from 'lib/flip';
4 | import {
5 | SWITCH_TO_TAB,
6 | UPDATE_SIDEBAR_TABS_MESSAGE,
7 | EXPAND_KEY,
8 | FULLSCREEN_KEY,
9 | SHRINK_KEY,
10 | SWITCH_LEFT_TAB,
11 | SWITCH_RIGHT_TAB,
12 | } from 'constant';
13 |
14 | let buffer: boolean[] = [];
15 |
16 | export const keyUpHandler = (event: KeyboardEvent) => {
17 | if (!!event.metaKey || !!event.ctrlKey) {
18 | buffer = [];
19 | }
20 | };
21 |
22 | export const keyboardHandler = (event: KeyboardEvent, loader: TSidebarLoader) => {
23 | if (!loader.isSerp || shouldPreventEventBubble(event)) return;
24 |
25 | const currentTabIndex = Number(loader.currentTab);
26 | const handleToggle = () => {
27 | loader.isExpanded = !loader.isExpanded;
28 | expandSidebar(loader);
29 | chrome.runtime.sendMessage({ type: UPDATE_SIDEBAR_TABS_MESSAGE });
30 | };
31 |
32 | if (buffer.indexOf(true) > -1) {
33 | buffer = [];
34 | return;
35 | }
36 |
37 | switch (event.code) {
38 | case FULLSCREEN_KEY.CODE:
39 | handleToggle();
40 | buffer = [];
41 | break;
42 | case EXPAND_KEY.CODE:
43 | loader.isPreview = true;
44 | loader.isExpanded && handleToggle();
45 | flipSidebar(document, 'show', loader);
46 | buffer = [];
47 | break;
48 | case SHRINK_KEY.CODE:
49 | loader.isExpanded && handleToggle();
50 | flipSidebar(document, 'hide', loader);
51 | buffer = [];
52 | break;
53 | case SWITCH_RIGHT_TAB.CODE:
54 | chrome.runtime.sendMessage({
55 | type: SWITCH_TO_TAB,
56 | index:
57 | currentTabIndex === loader.sidebarTabs.length
58 | ? getFirstValidTabIndex(loader.sidebarTabs)
59 | : (
60 | currentTabIndex +
61 | Number(getFirstValidTabIndex(loader.sidebarTabs.slice(currentTabIndex)))
62 | ).toString(),
63 | });
64 | buffer = [];
65 | break;
66 | case SWITCH_LEFT_TAB.CODE:
67 | {
68 | const lastIndex = getLastValidTabIndex(loader.sidebarTabs.slice(0, currentTabIndex - 1));
69 | chrome.runtime.sendMessage({
70 | type: SWITCH_TO_TAB,
71 | index: lastIndex === '0' ? getLastValidTabIndex(loader.sidebarTabs) : lastIndex,
72 | });
73 | buffer = [];
74 | }
75 | break;
76 | default:
77 | (!!event.metaKey || !!event.ctrlKey) && buffer.push(true);
78 | break;
79 | }
80 | };
81 |
--------------------------------------------------------------------------------
/src/constant/adblock.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @module constants:adblock
3 | * @version 1.0.0
4 | * @license (C) Insight
5 | */
6 |
7 | export const BLOCKED_ADS = [
8 | {
9 | site: 'twitter.com',
10 | adText: 'Promoted',
11 | adElementSelector: 'article',
12 | },
13 | {
14 | site: 'duckduckgo.com',
15 | adText: 'Ad',
16 | adElementSelector: '#ads',
17 | delay: 1000,
18 | },
19 | {
20 | site: 'bing.com',
21 | adText: 'Ad',
22 | adElementSelector: '.b_ad',
23 | },
24 | {
25 | site: 'facebook.com',
26 | adText: 'Sponsored',
27 | adElementSelector: 'article',
28 | },
29 | {
30 | site: 'reddit.com',
31 | adText: 'Promoted, Sponsored',
32 | adElementSelector: 'article',
33 | },
34 | {
35 | site: 'm.facebook.com',
36 | adText: 'Sponsored',
37 | adElementSelector: 'article',
38 | },
39 | {
40 | site: 'instagram.com',
41 | adText: 'Sponsored',
42 | adElementSelector: 'article',
43 | },
44 | {
45 | site: 'mobile.twitter.com',
46 | adText: 'Promoted',
47 | adElementSelector: 'article',
48 | },
49 | {
50 | site: 'amazon.com',
51 | adText: 'Sponsored',
52 | adElementSelector: 'div.s-result-item',
53 | },
54 | {
55 | site: 'm.youtube.com',
56 | adTextContainer: 'ytm-badge',
57 | adText: 'AD',
58 | adElementSelector: 'ytm-item-section-renderer',
59 | },
60 | {
61 | site: 'm.youtube.com',
62 | adTextContainer: 'ytm-badge',
63 | adText: 'AD',
64 | adElementSelector: 'ytm-rich-item-renderer',
65 | },
66 | {
67 | site: 'google.com',
68 | adText: 'Ad,Ad·',
69 | adElementSelector: '#tads',
70 | },
71 | {
72 | site: 'google.com',
73 | adText: 'Ad,Ad·',
74 | adElementSelector: '#tadsb',
75 | },
76 | {
77 | site: 'google.com',
78 | adText: 'Ads,Ads·',
79 | adElementSelector: '.cu-container',
80 | },
81 | {
82 | site: 'google.com',
83 | adText: 'Ads',
84 | adElementSelector: '.mnr-c',
85 | },
86 | {
87 | site: 'google.com',
88 | adText: 'Ads',
89 | adElementSelector: '._-is',
90 | },
91 | {
92 | site: 'google.com',
93 | adText: 'Ads',
94 | adElementSelector: '.commercial-unit-mobile-top',
95 | },
96 | {
97 | site: 'ecosia.org',
98 | adText: 'Ad',
99 | adTextContainer: 'a',
100 | adElementSelector: '.result-body',
101 | },
102 | {
103 | site: 'ecosia.org',
104 | adText: 'Ads',
105 | adTextContainer: 'a',
106 | adElementSelector: '.product-ad',
107 | },
108 | ] as const;
109 |
--------------------------------------------------------------------------------
/src/modules/builder/DeleteAugmentationButton/DeleteAugmentationButton.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @module modules:builder
3 | * @version 1.0.0
4 | * @license (C) Insight
5 | */
6 |
7 | import React, { Suspense } from 'react';
8 | import Button from 'antd/lib/button';
9 | import AugmentationManager from 'lib/augmentations';
10 | import { AUGMENTATION_ID, MESSAGE, PAGE, PROTECTED_AUGMENTATIONS } from 'constant';
11 | import 'antd/lib/button/style/index.css';
12 |
13 | const DeleteOutlined = React.lazy(
14 | async () => await import('@ant-design/icons/DeleteOutlined').then((mod) => mod),
15 | );
16 |
17 | //-----------------------------------------------------------------------------------------------
18 | // ! Magics
19 | //-----------------------------------------------------------------------------------------------
20 | const DELETE_TEXT = 'Delete Filter';
21 |
22 | //-----------------------------------------------------------------------------------------------
23 | // ! Component
24 | //-----------------------------------------------------------------------------------------------
25 | export const DeleteAugmentationButton: DeleteAugmentationButton = ({ augmentation, disabled }) => {
26 | //-----------------------------------------------------------------------------------------------
27 | // ! Handlers
28 | //-----------------------------------------------------------------------------------------------
29 | const handleDelete = (): void => {
30 | if (disabled || augmentation.id === AUGMENTATION_ID.BLOCKLIST) return;
31 | AugmentationManager.removeInstalledAugmentation(augmentation);
32 | chrome.runtime.sendMessage({
33 | type: MESSAGE.OPEN_PAGE,
34 | page: PAGE.ACTIVE,
35 | });
36 | };
37 |
38 | //-----------------------------------------------------------------------------------------------
39 | // ! Render
40 | //-----------------------------------------------------------------------------------------------
41 | return (
42 |
43 |
63 |
64 | );
65 | };
66 |
--------------------------------------------------------------------------------
/src/modules/onboarding/PrivacyFrame/PrivacyFrame.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @module module:onboarding
3 | * @version 1.0.0
4 | * @license (C) Insight
5 | */
6 |
7 | import React, { useContext } from 'react';
8 | import { Helmet } from 'react-helmet';
9 | import Button from 'antd/lib/button';
10 | import Typography from 'antd/lib/typography';
11 | import { StepContext } from 'modules/onboarding';
12 | import { APP_NAME } from 'constant';
13 | import 'antd/lib/button/style/index.css';
14 | import 'antd/lib/typography/style/index.css';
15 | import './PrivacyFrame.scss';
16 |
17 | //-----------------------------------------------------------------------------------------------
18 | // ! Magics
19 | //-----------------------------------------------------------------------------------------------
20 | const TAB_TITLE = `${APP_NAME} - Privacy Setting`;
21 | const NEXT_BUTTON_TEXT = 'Next';
22 | export const ACTIVE_LICENSE_MAIN_HEADER = 'Choose a Privacy Setting';
23 | export const INACTIVE_LICENSE_MAIN_HEADER = 'Maximum Privacy Enabled';
24 |
25 | const { Title } = Typography;
26 |
27 | //-----------------------------------------------------------------------------------------------
28 | // ! Component
29 | //-----------------------------------------------------------------------------------------------
30 | export const PrivacyFrame = () => {
31 | const { setCurrentStep } = useContext(StepContext);
32 | const handleNext = () => setCurrentStep(2);
33 |
34 | const OPEN_SOURCE_LINK = open source;
35 |
36 | const INACTIVE_LICENSE_TEXT_CONTENT = (
37 | <>
38 | { APP_NAME } never sends any information about the page you visit to our servers.
39 | { APP_NAME } is { OPEN_SOURCE_LINK } so you can verify this.
40 | >
41 | );
42 |
43 | //-----------------------------------------------------------------------------------------------
44 | // ! Render
45 | //-----------------------------------------------------------------------------------------------
46 | return (
47 | <>
48 |
49 | {TAB_TITLE}
50 |
51 |
52 |
{INACTIVE_LICENSE_MAIN_HEADER}
53 |
{INACTIVE_LICENSE_TEXT_CONTENT}
54 |
55 |
64 |
65 |
66 | >
67 | );
68 | };
69 |
--------------------------------------------------------------------------------
/tasks/config/webpack.common.ts:
--------------------------------------------------------------------------------
1 | import { Configuration, DefinePlugin, Module } from 'webpack';
2 | import path from 'path';
3 | import CopyPlugin from 'copy-webpack-plugin';
4 | import MiniCssExtractPlugin from 'mini-css-extract-plugin';
5 | import CssAutoprefixer from 'autoprefixer';
6 | import PATHS from '../lib/path';
7 |
8 | export default (env: { mode: string; PROJECT: 'is' | 'sc' }): Configuration => {
9 | return {
10 | entry: {
11 | insight_background: path.join(__dirname, PATHS.src + '/scripts/background/index.ts'),
12 | insight_content: path.join(__dirname, PATHS.src + '/scripts/content/index.tsx'),
13 | },
14 | output: {
15 | path: path.join(__dirname, `${env.mode === 'production' ? PATHS.dist : PATHS.build}/js`),
16 | filename: '[name].js',
17 | },
18 | optimization: {
19 | splitChunks: {
20 | cacheGroups: {
21 | styles: {
22 | name: 'bundle',
23 | test: (m: Module) => m.constructor.name === 'CssModule',
24 | chunks: 'all',
25 | reuseExistingChunk: true,
26 | enforce: true,
27 | },
28 | },
29 | },
30 | },
31 | module: {
32 | rules: [
33 | {
34 | test: /\.tsx?$/,
35 | use: 'ts-loader',
36 | exclude: /node_modules/,
37 | },
38 | {
39 | test: /\.s?[ac]ss$/i,
40 | use: [
41 | MiniCssExtractPlugin.loader,
42 | {
43 | loader: 'css-loader',
44 | options: {
45 | importLoaders: 1,
46 | modules: {
47 | compileType: 'icss',
48 | },
49 | },
50 | },
51 | {
52 | loader: 'postcss-loader',
53 | options: {
54 | postcssOptions: {
55 | plugins: [CssAutoprefixer],
56 | },
57 | },
58 | },
59 | 'sass-loader',
60 | ],
61 | },
62 | ],
63 | },
64 | resolve: {
65 | modules: ['node_modules', path.join(__dirname, PATHS.src)],
66 | extensions: ['.ts', '.tsx', '.js'],
67 | },
68 | plugins: [
69 | new MiniCssExtractPlugin({
70 | filename: '../[name].css',
71 | }),
72 | new CopyPlugin({
73 | patterns: [
74 | {
75 | from: `./*.(css|html|svg|json|png)`,
76 | to: '../',
77 | context: 'public',
78 | },
79 | ],
80 | }),
81 | new DefinePlugin({
82 | 'process.env.mode': JSON.stringify(env.mode),
83 | 'process.env.NODE_ENV': JSON.stringify(env.mode),
84 | 'process.env.PROJECT': JSON.stringify(env.PROJECT),
85 | }),
86 | ],
87 | };
88 | };
89 |
--------------------------------------------------------------------------------
/src/modules/pages/FeaturePage/FeaturePage.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Button from 'antd/lib/button';
3 | import Divider from 'antd/lib/divider';
4 | import Switch from 'antd/lib/switch';
5 | import { MESSAGE, PAGE } from 'constant';
6 | import { useFeature } from 'lib/features';
7 | import 'antd/lib/switch/style/index.css';
8 | import 'antd/lib/button/style/index.css';
9 |
10 | /** MAGICS **/
11 | const HEADER_TITLE = 'Settings';
12 | const HEADER_LEFT_BUTTON_TEXT = 'Close';
13 |
14 | export const FeaturePage: FeaturePage = () => {
15 | const [bookmarksFeature, toggleBookmarksFeature] = useFeature('desktop_bookmarks');
16 | const [loginFeature, toggleLoginFeature] = useFeature('desktop_login');
17 | const [ratingFeature, toggleRatingFeature] = useFeature('desktop_ratings');
18 |
19 | const handleToggleLogin = () => toggleLoginFeature();
20 | const handleToggleBookmarks = () => toggleBookmarksFeature();
21 |
22 | const handleClose = () => {
23 | chrome.runtime.sendMessage({
24 | type: MESSAGE.OPEN_PAGE,
25 | page: PAGE.SETTINGS,
26 | });
27 | };
28 |
29 | return (
30 |
31 |
32 |
35 | {HEADER_TITLE}
36 |
37 |
38 |
39 | Temporarily Enable/Disable Development Features
40 |
41 |
47 |
Toggle Publication Review Feature
48 |
49 |
50 |
56 |
Toggle Login Feature
57 |
58 |
59 |
65 |
Toggle Bookmarks Feature
66 |
67 |
68 |
69 |
70 |
71 | );
72 | };
73 |
--------------------------------------------------------------------------------
/src/lib/expand/index.ts:
--------------------------------------------------------------------------------
1 | import { flipSidebar } from 'lib/flip';
2 |
3 | export const expandSidebar = (loader: TSidebarLoader) => {
4 | const sidebarRoot = document.getElementById('sidebar-root') as HTMLDivElement;
5 | const sidebarRootIframe = document.getElementById('sidebar-root-iframe') as HTMLIFrameElement;
6 | const frameDocument = sidebarRootIframe?.contentWindow?.document;
7 |
8 | if (!frameDocument) return;
9 |
10 | const sidebarContainer = frameDocument.getElementById(
11 | 'insight-sidebar-container',
12 | ) as HTMLDivElement;
13 | const tabFrames = Array.from(frameDocument.getElementsByClassName('insight-tab-iframe'));
14 |
15 | if (!sidebarRoot.classList.contains('insight-expanded')) {
16 | flipSidebar(document, 'show', loader);
17 | document.documentElement.style.overflow = 'hidden';
18 | sidebarRoot.classList.add('insight-expanded');
19 | sidebarRoot.setAttribute(
20 | 'style',
21 | `
22 | position: fixed;
23 | border: 0;
24 | right: 0;
25 | top: 0;
26 | display: flex !important;
27 | width: 100% !important;
28 | outline: 0;
29 | transition-property: width;
30 | transition-duration: 0.5s;
31 | transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
32 | z-index: 9999;
33 | `,
34 | );
35 | sidebarRootIframe.classList.add('insight-expanded');
36 | sidebarRootIframe.setAttribute(
37 | 'style',
38 | `
39 | position: fixed;
40 | border: 0;
41 | display: flex;
42 | margin: 0;
43 | width: 100%;
44 | height: 100%;
45 | right: 0;
46 | top: 0;
47 | outline: 0;
48 | transition-property: width;
49 | transition-duration: 0.5s;
50 | transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
51 | `,
52 | );
53 | sidebarContainer.classList.add('insight-expanded');
54 | sidebarContainer.setAttribute(
55 | 'style',
56 | `
57 | position: fixed;
58 | border: 0;
59 | margin: 0;
60 | right: 0;
61 | width: 100%;
62 | outline: 0;
63 | transition-property: width;
64 | transition-duration: 0.5s;
65 | transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
66 | `,
67 | );
68 | tabFrames.forEach((frame) => frame.classList.add('insight-expanded'));
69 | } else {
70 | sidebarRoot.classList.remove('insight-expanded');
71 | sidebarRootIframe.classList.remove('insight-expanded');
72 | sidebarContainer.classList.remove('insight-expanded');
73 | document.documentElement.style.overflow = 'auto';
74 | sidebarContainer.setAttribute(
75 | 'style',
76 | `
77 | background: white;
78 | position: fixed;
79 | top: 0;
80 | bottom: 0;
81 | right: 0;
82 | width: 450px;
83 | `,
84 | );
85 | tabFrames.forEach((frame) => frame.classList.remove('insight-expanded'));
86 | flipSidebar(document, 'show', loader);
87 | }
88 | };
89 |
--------------------------------------------------------------------------------
/src/scripts/content/reorder.ts:
--------------------------------------------------------------------------------
1 | import { debug, extractUrlProperties } from 'lib/helpers';
2 | import {
3 | GOOGLE_SERP_RESULT_A_SELECTOR,
4 | GOOGLE_SERP_RESULT_CONTAINER,
5 | GOOGLE_SERP_RESULT_DIV_SELECTOR,
6 | } from 'constant';
7 |
8 | ((document, window) => {
9 | // First X results to replace with unique results.
10 | const MAXIMUM_MOVES = 3;
11 |
12 | // Currently we support only Google SERP reordering.
13 | if (window.location.href.search(/google\.[\w]*]/gi) === -1) return;
14 |
15 | // The list of individual search results. Google occasionally merges more results into one container
16 | // so we need to use a more specific selector to get the unique result blocks from the SERP.
17 | const results = Array.from(document.querySelectorAll(GOOGLE_SERP_RESULT_DIV_SELECTOR)).map((el) =>
18 | el.closest(GOOGLE_SERP_RESULT_CONTAINER),
19 | ) as HTMLElement[];
20 |
21 | // The list of elements that could be replaced by a higher ranked result.
22 | const topResults = results.slice(0, MAXIMUM_MOVES);
23 |
24 | // Reordered list of the available domains from SERP. Ordering is made by the unique domains
25 | // appearance count and their original position in the results page.
26 | //const rankedDomains = getRankedDomains(domains);
27 |
28 | // The list of elements that has been already moved.
29 | const movedDomains: string[] = [];
30 |
31 | /** DEV START **/
32 | const logData: any[] = [];
33 | /** DEV END **/
34 |
35 | results.forEach((node, index) => {
36 | const linkElement = node.querySelector(GOOGLE_SERP_RESULT_A_SELECTOR);
37 |
38 | if (!linkElement) return;
39 |
40 | const domain =
41 | extractUrlProperties(
42 | linkElement.getAttribute('href')?.replace(/.*https?:\/\//, 'https://') ?? '',
43 | ).hostname ?? '';
44 |
45 | const isMoved = !!movedDomains.find(
46 | (movedDomain) => domain.search(movedDomain) > -1 || movedDomain.search(domain) > -1,
47 | );
48 |
49 | if (
50 | !isMoved && // Ignore if already moved
51 | index > topResults.length - 1 && // Ignore the first three results
52 | movedDomains.length < MAXIMUM_MOVES // Only replace top results
53 | ) {
54 | // Due to Google's complex nesting, it's easier to replace the elements with
55 | // their clone, instead bothering the parent elements at all.
56 | const originalClone = topResults[movedDomains.length].cloneNode(true);
57 | const replaceClone = node.cloneNode(true);
58 | topResults[movedDomains.length].replaceWith(replaceClone);
59 | node.replaceWith(originalClone);
60 |
61 | movedDomains.push(domain);
62 |
63 | /** DEV START **/
64 | logData.push('\n\t', {
65 | Domain: domain,
66 | 'Move from index': index,
67 | 'Move to index': movedDomains.length,
68 | });
69 | /** DEV END **/
70 | }
71 | });
72 |
73 | /** DEV START **/
74 | !!logData.length && debug('Reordered SERP results\n---', ...logData, '\n---');
75 | /** DEV END **/
76 | })(document, window);
77 |
--------------------------------------------------------------------------------
/src/lib/overlay/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @module lib:overlay
3 | * @version 1.0.0
4 | * @license (C) Insight
5 | */
6 |
7 | import { INSIGHT_BLOCKED, INSIGHT_HIDDEN_RESULT_SELECTOR, SIDEBAR_Z_INDEX } from 'constant';
8 | import { isDark } from 'lib/helpers';
9 |
10 | /**
11 | * Create Result Overlay
12 | * --------------------------------------
13 | * Render hiding overlay above a blocked SERP result.
14 | */
15 | export const createResultOverlay: CreateResultOverlay = (result, blockers, details) => {
16 | if (!(result instanceof HTMLElement)) return;
17 |
18 | if (result.getAttribute(INSIGHT_HIDDEN_RESULT_SELECTOR) === 'true') {
19 | const existingOverlay = result.querySelector('.insight-hidden-domain-overlay');
20 | existingOverlay && existingOverlay.parentElement?.removeChild(existingOverlay);
21 | }
22 |
23 | if (!blockers?.length) {
24 | // Assume the overlay triggered by the ad-blocker when blocker list is empty
25 | result.setAttribute('insight-ad-block', 'true');
26 | }
27 |
28 | result.setAttribute(INSIGHT_HIDDEN_RESULT_SELECTOR, 'true');
29 | result.classList.add(INSIGHT_BLOCKED);
30 |
31 | if (
32 | !result.querySelectorAll('.insight-hidden').length &&
33 | result.getAttribute(`${INSIGHT_HIDDEN_RESULT_SELECTOR}-protected`) !== 'true'
34 | ) {
35 | const overlay = document.createElement('div');
36 | overlay.classList.add(`insight-${details.selectorString}-overlay`);
37 | overlay.classList.add('insight-hidden');
38 |
39 | if (isDark()) {
40 | overlay.classList.add('insight-dark');
41 | }
42 |
43 | // Z-Index must be one level below of other gutter units to properly show them
44 | overlay.setAttribute('style', `z-index: ${SIDEBAR_Z_INDEX - 3};`);
45 |
46 | const textWrapper = document.createElement('div');
47 | textWrapper.classList.add(`insight-${details.selectorString}-text-wrapper`);
48 | textWrapper.innerText = details.header;
49 |
50 | const innerText = document.createElement('div');
51 | innerText.classList.add(`insight-${details.selectorString}-inner-text`);
52 | innerText.innerText = details.text;
53 |
54 | overlay.appendChild(textWrapper);
55 | textWrapper.appendChild(innerText);
56 |
57 | overlay.addEventListener('click', (e) => {
58 | if (result.getAttribute(`${INSIGHT_HIDDEN_RESULT_SELECTOR}-protected`) !== 'true') {
59 | e.preventDefault();
60 | const root = (e.target as Element)?.closest('.insight-hidden');
61 | if (root?.parentElement) {
62 | root.parentElement.style.overflow = 'none';
63 | }
64 | if (root?.parentNode) {
65 | root.parentNode.removeChild(root);
66 | }
67 | result.setAttribute(`${INSIGHT_HIDDEN_RESULT_SELECTOR}-protected`, 'true');
68 | result.classList.remove(INSIGHT_BLOCKED);
69 | }
70 | });
71 |
72 | if (blockers?.length) {
73 | result.style.marginLeft = '-100px';
74 | result.style.paddingLeft = '100px';
75 | }
76 | result.insertBefore(overlay, result.firstChild);
77 | }
78 | };
79 |
--------------------------------------------------------------------------------
/src/constant/message.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @module constant:message
3 | * @version 1.0.0
4 | * @license (C) Insight
5 | */
6 |
7 | export const MESSAGE = {
8 |
9 | //-----------------------------------------------------------------------------------------------
10 | // ! Window
11 | //-----------------------------------------------------------------------------------------------
12 |
13 | ADD_EXTERNAL_AUGMENTATION: 'ADD_EXTERNAL_AUGMENTATION',
14 | PROCESS_SERP_OVERLAY: 'PROCESS_SERP_OVERLAY',
15 | REMOVE_AUGMENTATION_SUCCESS: 'EDIT_AUGMENTATION_SUCCESS',
16 | REMOVE_HIDE_DOMAIN_OVERLAY: 'REMOVE_HIDE_DOMAIN_OVERLAY',
17 | REMOVE_SEARCHED_DOMAIN: 'REMOVE_SEARCHED_DOMAIN',
18 | TOGGLE_BLOCKED_DOMAIN: 'TOGGLE_BLOCKED_DOMAIN',
19 | TOGGLE_TRUSTED_DOMAIN: 'TOGGLE_TRUSTED_DOMAIN',
20 | TRIGGER_FRAME_SCROLL_LOG: 'TRIGGER_FRAME_SCROLL_LOG',
21 | TRIGGER_GUTTER_HOVEROPEN: 'TRIGGER_GUTTER_HOVEROPEN',
22 | TRIGGER_PUBLICATION_TIMER: 'TRIGGER_PUBLICATION_TIMER',
23 | TRIGGER_START_TRACK_TIMER: 'TRIGGER_START_TRACK_TIMER',
24 | TRIGGER_STOP_TRACK_TIMER: 'TRIGGER_STOP_TRACK_TIMER',
25 | TRIGGER_ENGINE_SYNC: 'TRIGGER_ENGINE_SYNC',
26 |
27 | //-----------------------------------------------------------------------------------------------
28 | // ! Sidebar
29 | //-----------------------------------------------------------------------------------------------
30 |
31 | ADD_LOCAL_AUGMENTATION: 'ADD_LOCAL_AUGMENTATION',
32 | DISABLE_SUGGESTED_AUGMENTATION: 'DISABLE_SUGGESTED_AUGMENTATION',
33 | EXTENSION_SHORT_URL_RECEIVED: 'EXTENSION_SHORT_URL_RECEIVED',
34 | GET_TAB_DOMAINS: 'GET_TAB_DOMAINS',
35 | HIDE_FRAME_OVERLAY: 'HIDE_FRAME_OVERLAY;',
36 | OPEN_NEW_TAB: 'OPEN_NEW_TAB',
37 | OPEN_PAGE: 'OPEN_PAGE',
38 | OPEN_SETTINGS_PAGE: 'OPEN_SETTINGS_PAGE',
39 | SEND_FRAME_INFO: 'SEND_FRAME_INFO',
40 | SEND_LOG_MESSAGE: 'SEND_LOG_MESSAGE',
41 | SET_TAB_DOMAINS: 'SET_TAB_DOMAINS',
42 | SWITCH_TO_TAB: 'SWITCH_TO_TAB',
43 | UPDATE_MY_BLOCK_LIST: 'UPDATE_MY_BLOCK_LIST',
44 | UPDATE_SIDEBAR_TABS: 'UPDATE_SIDEBAR_TABS',
45 | URL_UPDATED: 'URL_UPDATED',
46 |
47 | //-----------------------------------------------------------------------------------------------
48 | // ! Logging
49 | //-----------------------------------------------------------------------------------------------
50 |
51 | EXTENSION_AUGMENTATION_SAVE: 'EXTENSION_AUGMENTATION_SAVE',
52 | EXTENSION_AUTO_EXPAND: 'EXTENSION_AUTO_EXPAND',
53 | EXTENSION_BLOCKLIST_ADD_DOMAIN: 'EXTENSION_BLOCKLIST_ADD_DOMAIN',
54 | EXTENSION_BLOCKLIST_REMOVE_DOMAIN: 'EXTENSION_BLOCKLIST_REMOVE_DOMAIN',
55 | EXTENSION_SERP_FILTER_LINK_CLICKED: 'EXTENSION_SERP_FILTER_LINK_CLICKED',
56 | EXTENSION_SERP_FILTER_LOADED: 'EXTENSION_SERP_FILTER_LOADED',
57 | EXTENSION_SERP_LINK_CLICKED: 'EXTENSION_SERP_LINK_CLICKED',
58 | EXTENSION_SERP_LINK_HOVEROPEN: 'EXTENSION_SERP_LINK_HOVEROPEN',
59 | EXTENSION_SERP_LOADED: 'EXTENSION_SERP_LOADED',
60 | EXTENSION_SERP_SUBTAB_CLICKED: 'EXTENSION_SERP_SUBTAB_CLICKED',
61 | EXTENSION_SUBTAB_SCROLL: 'EXTENSION_SUBTAB_SCROLL',
62 |
63 | } as const;
--------------------------------------------------------------------------------
/src/lib/overlay/index.spec.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import renderer from 'react-test-renderer';
3 |
4 | import { INSIGHT_BLOCKED, INSIGHT_HIDDEN_RESULT_SELECTOR } from "constant";
5 | import { createResultOverlay } from ".";
6 |
7 |
8 | describe('Overlay tests', () => {
9 |
10 | beforeEach(() => {
11 | (window.matchMedia as any) = () => {};
12 | });
13 |
14 | describe('createResultOverlay', () => {
15 |
16 | test('Should add basic attributes and classes', () => {
17 | // Given
18 | const element = document.createElement('div');
19 |
20 | expect(element.getAttribute(INSIGHT_HIDDEN_RESULT_SELECTOR)).toBeNull();
21 | expect(element.classList.length).toBe(0);
22 |
23 | // When
24 | createResultOverlay(element, [], {
25 | header: 'some-header',
26 | selectorString: 'some-selector',
27 | text: 'some-text',
28 | });
29 |
30 | // Then
31 | expect(element.getAttribute(INSIGHT_HIDDEN_RESULT_SELECTOR)).toBe('true');
32 | expect(element.classList.length).toBe(1);
33 | expect(element.classList.contains(INSIGHT_BLOCKED)).toBe(true);
34 | });
35 |
36 | test('Should add children elements', () => {
37 | // Given
38 | const element = document.createElement('div');
39 |
40 | createResultOverlay(element, [], {
41 | header: 'some-header',
42 | selectorString: 'some-selector',
43 | text: 'some-text',
44 | });
45 |
46 | const Element = () => {
47 | return
48 | };
49 |
50 | // When
51 | const component = renderer.create();
52 | const tree = component.toJSON();
53 |
54 | // Then
55 | expect(tree).toMatchSnapshot();
56 | });
57 |
58 | test('Should have texts', () => {
59 | // Given
60 | const element = document.createElement('div');
61 |
62 | // When
63 | createResultOverlay(element, [], {
64 | header: 'some-header',
65 | selectorString: 'some-selector',
66 | text: 'some-text',
67 | });
68 |
69 | // Then
70 | expect((element.querySelector('.insight-some-selector-text-wrapper') as any).innerText).toBe('some-header');
71 | expect((element.querySelector('.insight-some-selector-inner-text') as any).innerText).toBe('some-text');
72 | });
73 |
74 | test('Should remove overlay on click', async () => {
75 | // Given
76 | const element = document.createElement('div');
77 |
78 | createResultOverlay(element, [], {
79 | header: 'some-header',
80 | selectorString: 'some-selector',
81 | text: 'some-text',
82 | });
83 |
84 | expect(element.querySelectorAll('.insight-some-selector-overlay').length).toBe(1);
85 |
86 | // When
87 | (element.querySelectorAll('.insight-some-selector-overlay')[0] as any).click();
88 |
89 | // Then
90 | expect(element.querySelectorAll('.insight-some-selector-overlay').length).toBe(0);
91 | });
92 |
93 | });
94 |
95 | })
96 |
--------------------------------------------------------------------------------
/src/modules/sidebar/Sidebar/Sidebar.scss:
--------------------------------------------------------------------------------
1 | @use 'styles/variables' as *;
2 | @use 'styles/mixins' as *;
3 | @use 'styles/animations' as *;
4 | @use 'styles/generics' as *;
5 | @use 'styles/overwrites' as *;
6 | @use 'sass:math';
7 |
8 | body#insight-sidebar {
9 | @include reset;
10 | font-family: $font_sans;
11 | font-size: $sidebar_font_medium;
12 | color: $color_text_black;
13 |
14 | #insight-sidebar-container {
15 | @include slideAnimation;
16 | background: $color_background_white;
17 |
18 | .sidebar-tab-panel {
19 | background: $color_background_saturated;
20 | }
21 |
22 | .sidebar-page {
23 | background: $color_background_saturated;
24 |
25 | .sidebar-page-header {
26 | position: absolute;
27 | top: 0;
28 | left: 0;
29 | right: 0;
30 | display: flex;
31 | align-items: center;
32 | height: $sidebar_header_height;
33 | background: $color_background_lighter;
34 | border-bottom: 1px solid $color_border_medium;
35 | cursor: default;
36 |
37 | .left-button {
38 | margin-left: $sidebar_space_tiny;
39 | margin-right: auto;
40 | }
41 |
42 | .page-title {
43 | @include centerBox;
44 | color: $color_text_black;
45 | font-size: $sidebar_font_large;
46 | font-weight: bold;
47 | cursor: text;
48 | }
49 |
50 | .right-button {
51 | margin-left: auto;
52 | margin-right: $sidebar_space_tiny;
53 | }
54 | }
55 |
56 | .sidebar-page-wrapper {
57 | position: absolute;
58 | display: flex;
59 | flex-direction: column;
60 | top: math.abs($sidebar_header_height + 1px);
61 | left: 0;
62 | right: 0;
63 | bottom: 0;
64 | max-width: $sidebar_max_page_width;
65 | padding-top: $sidebar_space_medium;
66 | overflow-y: auto !important;
67 | background: $color_background_white;
68 |
69 | section {
70 | display: flex;
71 | flex-direction: column;
72 | align-items: flex-start;
73 | justify-content: center;
74 | width: 100%;
75 |
76 | .title {
77 | @include reset;
78 | font-size: $sidebar_font_large;
79 | font-weight: bold;
80 | margin-left: $sidebar_space_medium;
81 | margin-bottom: $sidebar_space_small;
82 | }
83 |
84 | .sub-title {
85 | @include reset;
86 | color: $color_text_light;
87 | font-size: $sidebar_font_small;
88 | font-weight: normal;
89 | margin-left: $sidebar_space_medium;
90 | margin-bottom: $sidebar_space_large;
91 | }
92 | }
93 | }
94 | }
95 |
96 | &.insight-expanded {
97 | .sidebar-page {
98 | .sidebar-page-wrapper {
99 | @include centerBox;
100 | }
101 | }
102 | }
103 | }
104 |
105 | .insight-modal-root {
106 | @include modal;
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/modules/onboarding/QueriesFrame/QueriesFrame.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, useEffect } from 'react';
2 | import { Helmet } from 'react-helmet';
3 | import Collapse from 'antd/lib/collapse';
4 | import List from 'antd/lib/list';
5 | import Typography from 'antd/lib/typography';
6 | import { StepContext } from 'modules/onboarding';
7 | import { APP_NAME, SYNC_FINISHED_KEY } from 'constant';
8 | import 'antd/lib/collapse/style/index.css';
9 | import 'antd/lib/list/style/index.css';
10 | import 'antd/lib/typography/style/index.css';
11 | import './QueriesFrame.scss';
12 |
13 | type QueryListItem = Record<'text', string>;
14 | type QueryList = Record;
15 |
16 | /** MAGICS **/
17 | const TAB_TITLE = `${APP_NAME} - Try Searching`;
18 | const PAGE_MAIN_HEADER_TEXT = 'Ready, Set, Search!';
19 | // * QUERIES
20 | const QUERIES_REFERENCE_URL = 'https://www.google.com/search?q=';
21 | // * QUERY LIST
22 | const LIST_DATA: QueryList = {
23 |
24 | "Shopping: see real people's perspectives and trusted review sites": [
25 | { text: 'best car insurance' },
26 | { text: 'best baby monitor' },
27 | { text: `best ev to buy ${ new Date().getFullYear() }` },
28 | ],
29 |
30 | 'Startups: sources trusted by top founders & investors': [
31 | { text: 'how to raise a seed round' },
32 | { text: 'how to hire engineers' },
33 | { text: 'how to hire marketers' },
34 | ],
35 |
36 | 'Dev: see sources trusted by engineers, designers, data scientists': [
37 | { text: 'best js framework' },
38 | { text: 'best machine learning books' },
39 | ],
40 |
41 | 'Misc: learn new things better & faster with insider trusted sources': [
42 | { text: 'how to build a bunker' },
43 | { text: 'best crypto books' },
44 | { text: 'best red wines for beginners' },
45 | ],
46 | };
47 |
48 | const { Title } = Typography;
49 | const { Panel } = Collapse;
50 |
51 | const entries = Object.entries(LIST_DATA);
52 |
53 | export const QueriesFrame = () => {
54 | const stepContext = useContext(StepContext);
55 |
56 | useEffect(() => {
57 | if (!stepContext.finished) {
58 | chrome.storage.sync.set({ [SYNC_FINISHED_KEY]: true });
59 | stepContext.setFinished(true);
60 | }
61 | }, [stepContext]);
62 |
63 | const item = (item: QueryListItem) => (
64 |
65 | ', item.text)}
68 | rel="noreferrer"
69 | >
70 | {item.text}
71 |
72 |
73 | );
74 |
75 | const panelItem = (panel: [ string, QueryListItem[] ], index: number) => {
76 | return (
77 |
78 |
79 |
80 | );
81 | };
82 |
83 | return (
84 | <>
85 |
86 | {TAB_TITLE}
87 |
88 |
89 |
{PAGE_MAIN_HEADER_TEXT}
90 |
91 | { entries.map(panelItem) }
92 |
93 |
94 | >
95 | );
96 | };
97 |
--------------------------------------------------------------------------------
/src/modules/onboarding/IntroductionPage/IntroductionPage.scss:
--------------------------------------------------------------------------------
1 | @use 'styles/variables' as *;
2 |
3 | body#insight-introduction-page {
4 | font-family: $font_sans !important;
5 | background-color: $intro_color_background !important;
6 |
7 | .horizontal-container {
8 | display: flex;
9 | align-items: center;
10 | }
11 |
12 | #insight-intro-container {
13 | display: flex;
14 | flex-direction: column;
15 | margin: $intro_space_medium auto;
16 | width: 100%;
17 | max-width: $intro_max_width;
18 |
19 | h1 {
20 | color: $color_text_white !important;
21 | font-family: $font_sans;
22 | font-size: $intro_font_largest;
23 | font-weight: 600;
24 | margin-bottom: 0px;
25 | }
26 |
27 | h2 {
28 | color: $color_text_white !important;
29 | font-size: $intro_font_larger;
30 | margin: 0;
31 | font-weight: 500;
32 | }
33 |
34 | button.step-button {
35 | display: flex;
36 | font-size: $intro_font_small;
37 | height: auto;
38 | margin-top: $sidebar_space_largest;
39 | border-color: $color_border_white;
40 |
41 | &:not([disabled]) {
42 | &:visited,
43 | &:link {
44 | color: $intro_color_text_link;
45 | border-color: $color_border_dark;
46 | }
47 |
48 | &:hover,
49 | &:focus,
50 | &:active {
51 | color: $intro_color_text_hover;
52 | }
53 | }
54 | }
55 |
56 | input {
57 | &:hover,
58 | &:focus,
59 | &:active {
60 | color: $intro_color_text_hover;
61 | border-color: $intro_color_text_hover;
62 | }
63 | }
64 |
65 | .ant-steps {
66 | .ant-steps-item {
67 | &:not(.ant-steps-item-active),
68 | &:not(.ant-steps-item-disabled) {
69 | .ant-steps-item-container:hover {
70 | .ant-steps-item-title {
71 | color: $color_text_white;
72 | }
73 | }
74 | }
75 |
76 | &.ant-steps-item-disabled {
77 | .ant-steps-item-container:hover {
78 | .ant-steps-item-title {
79 | color: $intro_color_text_opaque;
80 | }
81 | }
82 | }
83 | }
84 |
85 | .ant-steps-item-container {
86 | .ant-steps-item-icon {
87 | border-color: $color_border_white;
88 | display: inline-flex;
89 | justify-content: center;
90 | align-items: center;
91 |
92 | span {
93 | color: $color_text_white;
94 | }
95 | }
96 | }
97 |
98 | .ant-steps-item-process {
99 | .ant-steps-item-container {
100 | .ant-steps-item-icon {
101 | background: $color_background_white;
102 |
103 | span {
104 | color: $color_text_black;
105 | }
106 | }
107 | }
108 | }
109 |
110 | .ant-steps-item-finish {
111 | .ant-steps-item-container {
112 | .ant-steps-item-content {
113 | .ant-steps-item-title:after {
114 | background-color: $color_background_white;
115 | }
116 | }
117 | }
118 | }
119 | }
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/src/modules/builder/ConditionsSection/ConditionsSection.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { v4 as uuid } from 'uuid';
3 | import Row from 'antd/lib/row';
4 | import Col from 'antd/lib/col';
5 | import Button from 'antd/lib/button';
6 | import {
7 | CONDITION_KEY,
8 | CONDITION_LABEL,
9 | CONDITION_EVALUATION,
10 | LEGACY_CONDITION_TYPE,
11 | } from 'constant';
12 | import { ConditionInput } from 'modules/builder';
13 | import 'antd/lib/grid/style/index.css';
14 | import 'antd/lib/button/style/index.css';
15 |
16 | /** MAGICS **/
17 | const AND_CONDITION_EVALUATION_TEXT = 'All';
18 | const OR_CONDITION_EVALUATION_TEXT = 'Any';
19 |
20 | export const ConditionsSection: ConditionsSection = ({
21 | conditions,
22 | setConditions,
23 | evaluation,
24 | setEvaluation,
25 | onAdd,
26 | onSave,
27 | onDelete,
28 | }) => {
29 | const newCondition: TCustomCondition = {
30 | id: uuid(),
31 | type: LEGACY_CONDITION_TYPE.LIST,
32 | value: [],
33 | } as any;
34 |
35 | const handleMatchAnyPage = () =>
36 | setConditions([
37 | {
38 | id: '0',
39 | key: CONDITION_KEY.ANY_URL,
40 | unique_key: CONDITION_KEY.ANY_URL,
41 | label: CONDITION_LABEL.ANY_URL,
42 | type: LEGACY_CONDITION_TYPE.LIST,
43 | value: ['.*'],
44 | },
45 | ]);
46 |
47 | const handleMatchAnySearchEngine = () =>
48 | setConditions([
49 | {
50 | id: '0',
51 | key: CONDITION_KEY.ANY_SEARCH_ENGINE,
52 | unique_key: CONDITION_KEY.ANY_SEARCH_ENGINE,
53 | label: CONDITION_LABEL.ANY_SEARCH_ENGINE,
54 | type: LEGACY_CONDITION_TYPE.LIST,
55 | value: ['.*'],
56 | },
57 | ]);
58 |
59 | return (
60 | <>
61 |
62 |
63 |
64 |
81 | of these conditions are true
82 |
83 |
84 |
85 | {conditions.map((condition) => (
86 |
94 | ))}
95 |
103 | >
104 | );
105 | };
106 |
--------------------------------------------------------------------------------
/src/constant/application.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @module constant:application
3 | * @version 1.0.0
4 | * @license (C) Insight
5 | */
6 |
7 | export const APP_NAME = 'Hyperweb';
8 |
9 | //-----------------------------------------------------------------------------------------------
10 | // ! Engine
11 | //-----------------------------------------------------------------------------------------------
12 |
13 | /**
14 | * The search engine used when there is no matching engine to the current page
15 | */
16 | export const DEFAULT_FALLBACK_SEARCH_ENGINE_PREFIX = 'google.com/search';
17 |
18 | /**
19 | * The search engine used when the current browser is any version of Safari
20 | */
21 | export const SAFARI_FALLBACK_URL = 'https://www.ecosia.org/search';
22 |
23 | /**
24 | * The User Agent string appended to sidebar tab requests when {@link SPECIAL_URL_JUNK_STRING junk string} also present
25 | */
26 | export const CUSTOM_UA_STRING = 'Mozilla/5.0 (Linux; Android 7.0; SM-G930V Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.125 Mobile Safari/537.36';
27 |
28 | /**
29 | * The list of removable response header names
30 | */
31 | export const STRIPPED_RESPONSE_HEADERS = [
32 | 'x-frame-options',
33 | 'frame-options',
34 | 'content-security-policy',
35 | 'access-control-allow-origin',
36 | 'referer-policy',
37 | ];
38 |
39 | //-----------------------------------------------------------------------------------------------
40 | // ! Tabs
41 | //-----------------------------------------------------------------------------------------------
42 | export const SPECIAL_URL_JUNK_STRING = 'qhfabdyvaykdf';
43 |
44 | export const SIDEBAR_TAB_FAKE_URL = 'https://sidebar-fake-tab/';
45 |
46 | export const URL_PARAM_TAB_TITLE_KEY = 'insight-tab-title';
47 | export const URL_PARAM_NO_COOKIE_KEY = 'insight-no-cookie';
48 | export const URL_PARAM_POSSIBLE_SERP_RESULT = 'insight-possible-serp-result';
49 |
50 | //-----------------------------------------------------------------------------------------------
51 | // ! Options
52 | //-----------------------------------------------------------------------------------------------
53 |
54 | /**
55 | * The CSS layer of the sidebar frame
56 | */
57 | export const SIDEBAR_Z_INDEX = 9999;
58 |
59 | /**
60 | * The number of sidebar tabs that are pre-rendered
61 | */
62 | export const PRERENDER_TABS = 3;
63 |
64 | /**
65 | * The number of result domains considered top domains (augmentation matching)
66 | */
67 | export const NUM_DOMAINS_TO_CONSIDER = 5;
68 |
69 | /**
70 | * The maximum number of allowed result domain overlap
71 | */
72 | export const NUM_DOMAINS_TO_EXCLUDE = 3;
73 |
74 | /**
75 | * The minimum width of the browser window to trigger sidebar on page load finish
76 | */
77 | export const WINDOW_REQUIRED_MIN_WIDTH = 1200;
78 |
79 | /**
80 | * The minimum width of the browser window to trigger sidebar on gutter hover action
81 | */
82 | export const HOVER_EXPAND_REQUIRED_MIN_WIDTH = 1000;
83 |
84 | //-----------------------------------------------------------------------------------------------
85 | // ! Development
86 | //-----------------------------------------------------------------------------------------------
87 | export const ENV = process.env.mode === 'development' ? 'DEV' : 'PROD';
88 | export const IN_DEBUG_MODE = ENV === 'DEV' || window.top?.INSIGHT_FORCE_DEBUG;
89 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | setup:
2 | npm install --legacy-peer-deps
3 | dev:
4 | npm run watch
5 | prod:
6 | npm run build
7 | package:
8 | npm run package
9 | open releases
10 | ship:
11 | npm run release
12 | open releases
13 | manifest-firefox:
14 | jq -s ".[0].permissions = .[0].permissions + .[1].permissions | \
15 | .[0].applications = .[0].applications + .[1].applications | del(.[0].optional_permissions) | .[0]" \
16 | public/manifest.json manifest/firefox.json > tmp_manifest.json
17 | mv tmp_manifest.json public/manifest.json
18 | update-private:
19 | git fetch public
20 | git fetch origin
21 | LOCAL_EXISTS=`git show-ref refs/heads/master`; \
22 | if [ "$$LOCAL_EXISTS" = "" ]; then \
23 | git switch -c master origin/master; \
24 | else \
25 | git switch master; \
26 | fi;
27 | git pull origin master;
28 | STOP=0; \
29 | LIMIT=30; \
30 | SKIP=0; \
31 | while [ $$STOP -eq 0 ] && [ $$SKIP -le $$LIMIT ]; do \
32 | PRIVATE_COMMIT=`git log origin/master -n 1 --skip=$$SKIP --pretty="%s"`; \
33 | echo Looking for "$$PRIVATE_COMMIT" in public repo; \
34 | INNER_LIMIT=30; \
35 | INNER_SKIP=0; \
36 | FOUND=-1; \
37 | while [ $$INNER_SKIP -le $$LIMIT ]; do \
38 | LAST_PUBLIC_COMMIT=`git log public/main -n 1 --skip=$$INNER_SKIP --pretty="%s"`; \
39 | echo index: $$INNER_SKIP "$$LAST_PUBLIC_COMMIT"; \
40 | \
41 | if [ "$$PRIVATE_COMMIT" = "$$LAST_PUBLIC_COMMIT" ]; then \
42 | FOUND=$$INNER_SKIP; \
43 | STOP=1; \
44 | break; \
45 | fi; \
46 | \
47 | INNER_SKIP=`expr $$INNER_SKIP + 1`; \
48 | \
49 | done; \
50 | \
51 | if [ $$FOUND -ge 0 ]; then \
52 | HASH=`git log public/main -n 1 --skip=$$FOUND --pretty="%h"`; \
53 | echo Public hash starts in: $$HASH; \
54 | git cherry-pick public/main $$HASH..public/main; \
55 | fi; \
56 | \
57 | if [ $$STOP -eq 1 ]; then \
58 | break; \
59 | fi; \
60 | \
61 | SKIP=`expr $$SKIP + 1`; \
62 | done;
63 | update-public:
64 | git fetch origin
65 | git fetch public
66 | LOCAL_EXISTS=`git show-ref refs/heads/main`; \
67 | if [ "$$LOCAL_EXISTS" = "" ]; then \
68 | git switch -c main public/main; \
69 | else \
70 | git switch main; \
71 | fi;
72 | git pull public main;
73 | STOP=0; \
74 | LIMIT=30; \
75 | SKIP=0; \
76 | while [ $$STOP -eq 0 ] && [ $$SKIP -le $$LIMIT ]; do \
77 | PUBLIC_COMMIT=`git log public/main -n 1 --skip=$$SKIP --pretty="%s"`; \
78 | echo Looking for "$$PUBLIC_COMMIT" in public repo; \
79 | INNER_LIMIT=30; \
80 | INNER_SKIP=0; \
81 | FOUND=-1; \
82 | while [ $$INNER_SKIP -le $$LIMIT ]; do \
83 | PRIVATE_COMMIT=`git log origin/master -n 1 --skip=$$INNER_SKIP --pretty="%s"`; \
84 | echo index: $$INNER_SKIP "$$PRIVATE_COMMIT"; \
85 | \
86 | if [ "$$PUBLIC_COMMIT" = "$$PRIVATE_COMMIT" ]; then \
87 | FOUND=$$INNER_SKIP; \
88 | STOP=1; \
89 | break; \
90 | fi; \
91 | \
92 | INNER_SKIP=`expr $$INNER_SKIP + 1`; \
93 | \
94 | done; \
95 | \
96 | if [ $$FOUND -ge 0 ]; then \
97 | HASH=`git log origin/master -n 1 --skip=$$FOUND --pretty="%h"`; \
98 | echo Private hash starts in: $$HASH; \
99 | git cherry-pick origin/master $$HASH..origin/master; \
100 | fi; \
101 | \
102 | if [ $$STOP -eq 1 ]; then \
103 | break; \
104 | fi; \
105 | \
106 | SKIP=`expr $$SKIP + 1`; \
107 | done;
108 |
--------------------------------------------------------------------------------
/src/lib/sidebar/index.spec.ts:
--------------------------------------------------------------------------------
1 | import { ACTION_KEY, ACTION_LABEL, ANY_URL_CONDITION_TEMPLATE } from 'constant';
2 | import { chrome } from 'jest-chrome';
3 | import SidebarLoader from '.';
4 |
5 | const mockAugmentation = (actions: ActionObject[], conditions: ConditionObject[]): Augmentation => {
6 | return {
7 | id: '',
8 | name: '',
9 | description: '',
10 | actions: {
11 | action_list: actions,
12 | },
13 | conditions: {
14 | condition_list: conditions,
15 | evaluate_with: 'AND',
16 | },
17 | };
18 | };
19 |
20 | describe('Sidebar tests', () => {
21 |
22 | test('getDomains', () => {
23 | // Given
24 | const element = document.createElement('div');
25 | element.innerHTML = `
26 | Span
27 |
31 |
32 | Link 3
33 | `;
34 |
35 | document.body.appendChild(element);
36 |
37 | SidebarLoader.customSearchEngine = {
38 | querySelector: {
39 | desktop: '.target',
40 | container: '',
41 | featured: [],
42 | pad: '',
43 | phone: '',
44 | result_container_selector: '',
45 | },
46 | search_engine_json: {
47 | required_params: [],
48 | required_prefix: '',
49 | }
50 | }
51 |
52 | // When
53 | const domains = SidebarLoader.getDomains(document, false);
54 |
55 | // Then
56 | expect(domains).toStrictEqual([ 'google.com/', 'yahoo.com/' ]);
57 | });
58 |
59 | describe('getTabUrls', () => {
60 |
61 | beforeEach(() => {
62 | SidebarLoader.url = new URL('https://www.google.com');
63 | SidebarLoader.publicationSlices = {
64 | original: [],
65 | } as any;
66 | });
67 |
68 | test('', async () => {
69 | // Given
70 | chrome.storage.local.get.mockImplementation((_: any, cb: any) => cb?.({}))
71 |
72 | SidebarLoader.query = 'some-query';
73 | const se = {
74 | required_params: ['q'],
75 | required_prefix: 'https://www.url.com/search',
76 | };
77 | const augmentations: Augmentation[] = [
78 | mockAugmentation([
79 | {
80 | key: ACTION_KEY.OPEN_URL,
81 | label: ACTION_LABEL.OPEN_URL,
82 | type: 'string',
83 | value: [ 'https://www.apple.com/' ],
84 | },
85 | {
86 | key: ACTION_KEY.SEARCH_ALSO,
87 | label: ACTION_LABEL.SEARCH_ALSO,
88 | type: 'string',
89 | value: [ se ],
90 | },
91 | ], [ ANY_URL_CONDITION_TEMPLATE ])
92 | ];
93 |
94 | // When
95 | const tabs = await SidebarLoader.getTabsAndAugmentations(augmentations);
96 |
97 | // Then
98 | expect(tabs.length).toBe(2);
99 | expect(tabs[0].url.href).toBe('https://url.com/search?q=some-query&qhfabdyvaykdf=qhfabdyvaykdf');
100 | expect(tabs[1].url.href).toBe('https://apple.com/?insight-possible-serp-result=insight-possible-serp-result&qhfabdyvaykdf=qhfabdyvaykdf&insight-tab-title=apple.com');
101 | });
102 |
103 | });
104 |
105 | })
106 |
--------------------------------------------------------------------------------
/src/styles/variables.scss:
--------------------------------------------------------------------------------
1 | @use 'sass:math';
2 |
3 | /**
4 | * LAYERS
5 | */
6 | $sidebar_z_index: 9999;
7 |
8 | /**
9 | * DIMENSIONS
10 | */
11 | $sidebar_header_height: 50px;
12 | $sidebar_footer_height: 50px;
13 | $sidebar_max_width: 480px;
14 | $sidebar_stretched_max_width: 900px;
15 | $sidebar_max_page_width: 900px;
16 | $sidebar_toggle_width: 200px;
17 | $sidebar_toggle_rating_height: 70px;
18 | $sidebar_toggle_rating_width: 90px;
19 |
20 | $intro_max_width: 1200px;
21 | $intro_max_privacy_explainer_width: 800px;
22 |
23 | /**
24 | * SPACING
25 | */
26 | $sidebar_space_tiny: 2px;
27 | $sidebar_space_small: 5px;
28 | $sidebar_space_medium: 10px;
29 | $sidebar_space_large: 15px;
30 | $sidebar_space_larger: 20px;
31 | $sidebar_space_largest: 40px;
32 |
33 | $intro_space_tiny: 20px;
34 | $intro_space_small: 30px;
35 | $intro_space_medium: 50px;
36 | $intro_space_large: 100px;
37 | $intro_space_larger: 150px;
38 |
39 | /**
40 | * FONTS
41 | */
42 | $font_serif: 'DM Serif', 'Georgia', serif;
43 | $font_sans: 'Inter', sans-serif;
44 | $font_mono: 'Consolas', 'Incosolata', 'Courier New', monospace;
45 |
46 | $sidebar_font_small: 11px;
47 | $sidebar_font_medium: 14px;
48 | $sidebar_font_large: 16px;
49 | $sidebar_font_larger: 18px;
50 | $sidebar_font_largest: 24px;
51 |
52 | $intro_font_tiny: 14px;
53 | $intro_font_smaller: 18px;
54 | $intro_font_small: 24px;
55 | $intro_font_medium: 28px;
56 | $intro_font_large: 36px;
57 | $intro_font_larger: 64px;
58 | $intro_font_largest: 150px;
59 |
60 | /**
61 | * ICONS
62 | */
63 | $icon_small: 16px;
64 | $icon_medium: 18px;
65 | $icon_large: 20px;
66 | $icon_larger: 36px;
67 |
68 | /**
69 | * COLORS
70 | */
71 | $color_text_black: #000000;
72 | $color_text_dark: #333333;
73 | $color_text_medium: #666666;
74 | $color_text_light: #999999;
75 | $color_text_lighter: #cecece;
76 | $color_text_white: #ffffff;
77 | $color_text_link: #1890ff;
78 | $color_text_hover: #40a9ff;
79 |
80 | $color_border_black: #000000;
81 | $color_border_dark: #434343;
82 | $color_border_medium: #cccccc;
83 | $color_border_light: #eeeeee;
84 | $color_border_white: #ffffff;
85 |
86 | $color_background_black: #000000;
87 | $color_background_darkest: #222222;
88 | $color_background_darker: #444444;
89 | $color_background_dark: #cccccc;
90 | $color_background_medium: #eeeeee;
91 | $color_background_light: #f5f5f5;
92 | $color_background_lighter: #f9f9f9;
93 | $color_background_lightest: #fafafa;
94 | $color_background_white: #ffffff;
95 | $color_background_saturated: #f1f3f4;
96 |
97 | $color_icon_dark: #999999;
98 | $color_icon_medium: #cccccc;
99 | $color_icon_light: #ffffff;
100 | $color_icon_link: #1890ff;
101 | $color_icon_danger: #ff4d4f;
102 |
103 | $intro_color_text_opaque: rgba(255, 255, 255, 0.45);
104 | $intro_color_text_link: rgba(255, 255, 255, 0.85);
105 | $intro_color_text_hover: rgba(255, 255, 255, 1);
106 | $intro_color_border_opaque: rgba(255, 255, 255, 0.85);
107 | $intro_color_background: rgba(2, 19, 52, 1);
108 |
109 | /// Remove the unit of a length
110 | /// @param {Number} $number - Number to remove unit from
111 | /// @return {Number} - Unitless number
112 | @function strip-unit($number) {
113 | @if type-of($number) == 'number' and not unitless($number) {
114 | @return math.div($number, $number * 0 + 1);
115 | }
116 |
117 | @return $number;
118 | }
119 |
120 | :export {
121 | sidebarMaxWidth: $sidebar_max_width;
122 | sidebarStretchedMaxWidth: $sidebar_stretched_max_width;
123 | }
124 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@insight/browser-extension",
3 | "version": "0.0.114",
4 | "description": "Insight supercharges Google search with results from expert and community trusted sources for any field.",
5 | "main": "index.js",
6 | "scripts": {
7 | "watch": "rimraf ./build && TS_NODE_PROJECT=\"./tasks/tsconfig.json\" ts-node tasks/scripts/watch.ts",
8 | "build": "rimraf ./dist && TS_NODE_PROJECT=\"./tasks/tsconfig.json\" ts-node tasks/scripts/build.ts",
9 | "release": "standard-version && npm run package",
10 | "package": "npm run build && cd dist && web-ext build && cd .. && ts-node tasks/scripts/post_release.ts",
11 | "test": "npx jest",
12 | "test-coverage": "npx jest --coverage"
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url": "https://github.com/lumosbrowser/lumos-extension.git"
17 | },
18 | "dependencies": {
19 | "@commitlint/cli": "^12.1.4",
20 | "@commitlint/config-conventional": "^12.1.4",
21 | "@fortawesome/fontawesome": "^1.1.8",
22 | "@fortawesome/fontawesome-svg-core": "^1.3.0",
23 | "@fortawesome/free-brands-svg-icons": "^6.0.0",
24 | "@fortawesome/free-regular-svg-icons": "^6.0.0",
25 | "@fortawesome/free-solid-svg-icons": "^6.0.0",
26 | "@fortawesome/react-fontawesome": "^0.1.17",
27 | "@types/chrome": "^0.0.145",
28 | "@types/copy-webpack-plugin": "^8.0.0",
29 | "@types/md5": "^2.3.0",
30 | "@types/mini-css-extract-plugin": "^1.4.3",
31 | "@types/mv": "^2.1.1",
32 | "@types/node": "^15.3.1",
33 | "@types/react": "^17.0.2",
34 | "@types/react-dom": "^17.0.1",
35 | "@types/react-helmet": "^6.1.0",
36 | "@types/react-typist": "^2.0.1",
37 | "@types/rimraf": "^3.0.0",
38 | "@types/uuid": "^8.3.0",
39 | "@types/webpack": "^5.28.0",
40 | "@types/webpack-merge": "^5.0.0",
41 | "@typescript-eslint/eslint-plugin": "^4.15.1",
42 | "@typescript-eslint/parser": "^4.15.1",
43 | "antd": "^4.15.0",
44 | "autoprefixer": "^10.2.5",
45 | "axios": "^0.21.1",
46 | "beautiful-react-hooks": "^0.35.0",
47 | "chalk": "^4.1.0",
48 | "copy-webpack-plugin": "^9.0.0",
49 | "css-loader": "^5.0.2",
50 | "eslint": "^7.20.0",
51 | "eslint-config-prettier": "^8.3.0",
52 | "eslint-plugin-import": "^2.22.1",
53 | "eslint-plugin-json": "^3.0.0",
54 | "eslint-plugin-prettier": "^3.3.1",
55 | "eslint-plugin-react": "^7.23.2",
56 | "eslint-plugin-react-hooks": "^4.2.0",
57 | "husky": "^6.0.0",
58 | "lint-staged": "^11.0.0",
59 | "md5": "^2.3.0",
60 | "mini-css-extract-plugin": "^1.6.0",
61 | "mv": "^2.1.1",
62 | "postcss-loader": "^6.0.0",
63 | "prettier": "^2.2.1",
64 | "prompts": "^2.4.0",
65 | "react": "^17.0.1",
66 | "react-dom": "^17.0.1",
67 | "react-feather": "^2.0.9",
68 | "react-helmet": "^6.1.0",
69 | "react-typist": "^2.0.5",
70 | "rimraf": "^3.0.2 ",
71 | "sass": "^1.32.8",
72 | "sass-loader": "^12.0.0",
73 | "standard-version": "^9.3.0",
74 | "style-loader": "^2.0.0",
75 | "ts-loader": "^9.1.2",
76 | "ts-node": "^10.0.0",
77 | "tsconfig-paths": "^3.9.0",
78 | "typescript": "^4.1.5 ",
79 | "typescript-plugin-css-modules": "^3.2.0",
80 | "web-ext": "^6.1.0",
81 | "web-vitals": "^2.0.1",
82 | "webpack": "^5.23.0",
83 | "webpack-cli": "^4.5.0",
84 | "webpack-merge": "^5.7.3"
85 | },
86 | "devDependencies": {
87 | "@types/jest": "^26.0.24",
88 | "@types/react-test-renderer": "^17.0.1",
89 | "jest": "^27.0.6",
90 | "jest-chrome": "^0.7.1",
91 | "react-test-renderer": "^17.0.2",
92 | "ts-jest": "^27.0.3"
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/lib/user/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @module lib:user
3 | * @version 1.0.0
4 | * @license (C) Insight
5 | */
6 |
7 | import { v4 as uuid } from 'uuid';
8 | import { debug } from 'lib/helpers';
9 | import {
10 | SYNC_DISTINCT_KEY,
11 | SYNC_PRIVACY_KEY,
12 | SYNC_USER_TAGS,
13 | SYNC_LAST_USED_TAGS,
14 | } from 'constant';
15 |
16 | class User {
17 | private _id: string | undefined = undefined;
18 | private _privacy: boolean | undefined = undefined;
19 | private _tags: string[] = Array(0);
20 | private _lastUsedTags: string[] = Array(0);
21 |
22 | /**
23 | * Load locally stored user properties if not exists.
24 | */
25 | public async initialize() {
26 | await this.getLocalUserData();
27 | debug('User Initialized', this.user);
28 | }
29 |
30 | /**
31 | * Get the current user object.
32 | *
33 | * @readonly
34 | * @static
35 | */
36 | public get user() {
37 | return {
38 | id: this._id,
39 | privacy: this._privacy,
40 | tags: this._tags,
41 | lastUsedTags: this._lastUsedTags,
42 | };
43 | }
44 |
45 | public async addUserTag(tag: string) {
46 | this._tags.push(tag);
47 | return new Promise((resolve, reject) =>
48 | chrome.storage.sync.set({ [SYNC_USER_TAGS]: this._tags }, () => {
49 | const hasError = chrome.runtime.lastError;
50 | if (hasError) {
51 | debug('UserManager - Add User Tag - Error', hasError.message);
52 | reject(null);
53 | }
54 | debug('UserManager - Add User Tag - Success', tag);
55 | resolve(this._tags);
56 | }),
57 | );
58 | }
59 |
60 | public async updateUserPrivacy(privacy: boolean) {
61 | this._privacy = privacy;
62 | await new Promise((resolve) =>
63 | chrome.storage.sync.set({ [SYNC_PRIVACY_KEY]: privacy }, () => resolve(true)),
64 | );
65 | }
66 |
67 | public async changeLastUsedTags(tags: string[]) {
68 | this._lastUsedTags = tags;
69 | chrome.storage.sync.set({
70 | [SYNC_LAST_USED_TAGS]: tags,
71 | });
72 | }
73 |
74 | //-----------------------------------------------------------------------------------------------
75 | // ! Internal Implementation
76 | //-----------------------------------------------------------------------------------------------
77 |
78 | private async getLocalUserData() {
79 | const storedUserId = await new Promise | undefined>((resolve) =>
80 | chrome.storage.sync.get(SYNC_DISTINCT_KEY, resolve),
81 | );
82 | this._id = storedUserId?.[SYNC_DISTINCT_KEY] ?? '';
83 |
84 | const storedUserPrivacy = await new Promise | undefined>((resolve) =>
85 | chrome.storage.sync.get(SYNC_PRIVACY_KEY, resolve),
86 | );
87 | this._privacy = String(storedUserPrivacy?.[SYNC_PRIVACY_KEY]) === 'true';
88 |
89 | const storedUserTags = await new Promise | undefined>((resolve) => {
90 | chrome.storage.sync.get(SYNC_USER_TAGS, resolve);
91 | }).then((data) => data?.[SYNC_USER_TAGS]);
92 | this._tags = storedUserTags ?? [];
93 |
94 | const storedLastUserTags = await new Promise | undefined>(
95 | (resolve) => {
96 | chrome.storage.sync.get(SYNC_LAST_USED_TAGS, resolve);
97 | },
98 | ).then((data) => data?.[SYNC_LAST_USED_TAGS]);
99 | this._lastUsedTags = storedLastUserTags ?? [];
100 |
101 | if (!this._id) {
102 | this._id = uuid();
103 | await new Promise((resolve) =>
104 | chrome.storage.sync.set({ [SYNC_DISTINCT_KEY]: this._id }, () => resolve(true)),
105 | );
106 | }
107 | }
108 |
109 | }
110 |
111 | const instance = new User();
112 | export default instance;
113 |
--------------------------------------------------------------------------------
/src/modules/pages/SettingsPage/SettingsPage.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @module modules:pages
3 | * @version 1.0.0
4 | * @license (C) Insight
5 | */
6 |
7 | import React, { useCallback, useEffect, useState } from 'react';
8 | import Button from 'antd/lib/button';
9 | import Divider from 'antd/lib/divider';
10 | import Switch from 'antd/lib/switch';
11 | import UserManager from 'lib/user';
12 | import { MESSAGE, PAGE, SYNC_PRIVACY_KEY } from 'constant';
13 | import 'antd/lib/switch/style/index.css';
14 | import 'antd/lib/typography/style/index.css';
15 | import 'antd/lib/button/style/index.css';
16 | import './SettingsPage.scss';
17 |
18 | export const SettingsContext = React.createContext(Object.create(null));
19 |
20 | //-----------------------------------------------------------------------------------------------
21 | // ! Magics
22 | //-----------------------------------------------------------------------------------------------
23 | const HEADER_TITLE = 'Settings';
24 | const HEADER_LEFT_BUTTON_TEXT = 'Close';
25 |
26 | //-----------------------------------------------------------------------------------------------
27 | // ! Component
28 | //-----------------------------------------------------------------------------------------------
29 | export const SettingsPage: SettingsPage = () => {
30 | const [useServerSuggestions, setUseServerSuggestions] = useState(false);
31 |
32 | //-----------------------------------------------------------------------------------------------
33 | // ! Handlers
34 | //-----------------------------------------------------------------------------------------------
35 | const handleClose = () => {
36 | chrome.runtime.sendMessage({
37 | type: MESSAGE.OPEN_PAGE,
38 | page: PAGE.ACTIVE,
39 | create: true,
40 | });
41 | };
42 |
43 | const handlePrivacyChange = useCallback(async (value: boolean | undefined) => {
44 | setUseServerSuggestions(value);
45 | UserManager.updateUserPrivacy(value === false);
46 | }, []);
47 |
48 | const getPrivacy = useCallback(async () => {
49 | const isStrongPrivacy = await new Promise>((resolve) =>
50 | chrome.storage.sync.get(SYNC_PRIVACY_KEY, resolve),
51 | ).then((result) => result?.[SYNC_PRIVACY_KEY]);
52 | setUseServerSuggestions(!isStrongPrivacy);
53 | }, []);
54 |
55 | useEffect(() => {
56 | getPrivacy();
57 | }, [getPrivacy]);
58 |
59 | const context = {
60 | useServerSuggestions,
61 | handlePrivacyChange,
62 | };
63 |
64 | //-----------------------------------------------------------------------------------------------
65 | // ! Render
66 | //-----------------------------------------------------------------------------------------------
67 |
68 | const PrivacySection = () => (
69 |
79 | );
80 |
81 | const Header = () => (
82 |
83 |
86 | {HEADER_TITLE}
87 |
88 | );
89 |
90 | return (
91 |
92 |
98 |
99 | );
100 | };
101 |
--------------------------------------------------------------------------------
/src/scripts/content/index.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @module scripts:content
3 | * @version 1.0.0
4 | * @license (C) Insight
5 | */
6 |
7 | import React from 'react';
8 | import { render } from 'react-dom';
9 | import { keyboardHandler, keyUpHandler } from 'lib/keyboard';
10 | import {
11 | debug,
12 | extractPublication,
13 | isDark,
14 | replaceLocation,
15 | runFunctionWhenDocumentReady,
16 | } from 'lib/helpers';
17 | import { IS_TOP_WINDOW_DARK_MESSAGE, MESSAGE, PAGE, URL_UPDATED_MESSAGE } from 'constant';
18 | import darkmode from 'lib/darkmode';
19 |
20 | const enableDarkMode = () => {
21 | return new Promise(() => {
22 | chrome.runtime.sendMessage({ type: IS_TOP_WINDOW_DARK_MESSAGE }, (isDark) => {
23 | isDark && darkmode.enable(document);
24 | });
25 | });
26 | };
27 |
28 | (async (window: Window, document: Document, location: Location) => {
29 | let isTop = false;
30 |
31 | try {
32 | isTop = window.location.href === window.top?.location.href;
33 | } catch {}
34 |
35 | if (!isTop) {
36 | enableDarkMode();
37 | const LOAD_ASYNC_SCRIPTS_TO_SIDEBAR_ONLY = async () => {
38 | await import('./frame');
39 | await import('./reorder');
40 | await import('./block');
41 | await import('./results');
42 | };
43 | runFunctionWhenDocumentReady(document, LOAD_ASYNC_SCRIPTS_TO_SIDEBAR_ONLY);
44 | }
45 |
46 | if (isTop) {
47 | const LOCAL_URL = replaceLocation(location) ?? new URL(location.href);
48 |
49 | debug(
50 | 'Content Script - Entry\n---\n\tCurrent Location',
51 | extractPublication(LOCAL_URL.href) ?? LOCAL_URL.href,
52 | );
53 |
54 | const UserManager = (await import('lib/user')).default;
55 | await UserManager.initialize();
56 |
57 | if (!window.top?.location.href.includes('extension://')) {
58 | await import('./results');
59 | }
60 |
61 | const SidebarLoader = (await import('lib/sidebar')).default;
62 | window.location.href.includes('http') && SidebarLoader.loadOrUpdateSidebar(document, LOCAL_URL);
63 |
64 | if (window.top?.location.href.includes('extension://')) {
65 | const IntroductionPage = (await import('modules/onboarding')).IntroductionPage;
66 | const root = document.getElementById('root');
67 | render(, root);
68 | } else {
69 | const LOAD_ASYNC_SCRIPTS_TO_ROOT_PAGE = async () => {
70 | await import('./block');
71 | await import('./frame');
72 | };
73 |
74 | runFunctionWhenDocumentReady(document, LOAD_ASYNC_SCRIPTS_TO_ROOT_PAGE);
75 | }
76 |
77 | const handleKeyDown = (event: KeyboardEvent) => keyboardHandler(event, SidebarLoader);
78 | const handleKeyUp = (event: KeyboardEvent) => keyUpHandler(event);
79 | document.addEventListener('keydown', handleKeyDown, true);
80 | document.addEventListener('keyup', handleKeyUp, true);
81 |
82 | window.addEventListener('message', ({ data }) => {
83 | if (data.name === MESSAGE.ADD_EXTERNAL_AUGMENTATION) {
84 | chrome.runtime.sendMessage({
85 | type: MESSAGE.OPEN_PAGE,
86 | page: PAGE.BUILDER,
87 | augmentation: data.result,
88 | });
89 | }
90 | });
91 |
92 | chrome.runtime.onMessage.addListener(async (msg, _, sendResponse) => {
93 | switch (msg.type) {
94 | case URL_UPDATED_MESSAGE:
95 | window.location.href.includes('http') &&
96 | (await SidebarLoader.loadOrUpdateSidebar(document, LOCAL_URL));
97 | break;
98 | case IS_TOP_WINDOW_DARK_MESSAGE:
99 | try {
100 | if (window.location.href === window.top?.location.href) {
101 | sendResponse(isDark());
102 | } else {
103 | sendResponse(false);
104 | }
105 | } catch {
106 | sendResponse(false);
107 | }
108 |
109 | break;
110 | }
111 | });
112 | }
113 | })(window, document, location);
114 |
--------------------------------------------------------------------------------
/src/modules/sidebar/SidebarTabContainer/SidebarTabContainer.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useState } from 'react';
2 | import Spin from 'antd/lib/spin';
3 | import SidebarLoader from 'lib/sidebar';
4 | import Skeleton from 'antd/lib/skeleton';
5 | import { debug, decodeSpace, triggerSerpProcessing } from 'lib/helpers';
6 | import { keyboardHandler, keyUpHandler } from 'lib/keyboard';
7 | import {
8 | EXTENSION_SERP_FILTER_LOADED,
9 | SIDEBAR_TAB_FAKE_URL,
10 | URL_PARAM_TAB_TITLE_KEY,
11 | EXTERNAL_PDF_RENDERER_URL,
12 | } from 'constant';
13 | import 'antd/lib/skeleton/style/index.css';
14 | import 'antd/lib/spin/style/index.css';
15 |
16 | export const SidebarTabContainer: SidebarTabContainer = ({ tab, isSelected, index }) => {
17 | const [loadedTab, setLoadedTab] = useState(null);
18 | const [canLoad, setCanLoad] = useState(false);
19 | const [isLoaded, setIsLoaded] = useState(false);
20 | const containerRef = useRef(null);
21 | const frameRef = useRef(null);
22 | const overlayRef = useRef(null);
23 | const handleKeyDown = (event: KeyboardEvent) => keyboardHandler(event, SidebarLoader);
24 | const handleKeyUp = (event: KeyboardEvent) => keyUpHandler(event);
25 |
26 | useEffect(() => {
27 | isSelected && setCanLoad(true);
28 | }, [isSelected]);
29 |
30 | useEffect(() => {
31 | if (loadedTab === null || tab.augmentation.id !== loadedTab.augmentation.id) {
32 | setLoadedTab(loadedTab);
33 | setIsLoaded(false);
34 | }
35 | }, [tab]);
36 |
37 | useEffect(() => {
38 | const { current: frame } = frameRef;
39 |
40 | frame?.contentWindow?.addEventListener('keydown', handleKeyDown);
41 | frame?.contentWindow?.addEventListener('keyup', handleKeyUp);
42 | return () => {
43 | frame?.contentWindow?.removeEventListener('keydown', handleKeyDown);
44 | frame?.contentWindow?.removeEventListener('keyup', handleKeyUp);
45 | };
46 | }, []);
47 |
48 | const handleLoad = () => {
49 | debug('--> Test: tab loaded', 'index:', index, SidebarLoader.time())
50 |
51 | setTimeout(() => setIsLoaded(true), 100);
52 | triggerSerpProcessing(SidebarLoader, true);
53 | SidebarLoader.sendLogMessage(EXTENSION_SERP_FILTER_LOADED, {
54 | query: SidebarLoader.query,
55 | filter_name: tab.augmentation.name,
56 | domains_to_search: SidebarLoader.domainsToSearch[tab.augmentation.id],
57 | });
58 | };
59 |
60 | const handleError = () => setIsLoaded(true);
61 |
62 | const OVERLAY_STYLE = {
63 | height: containerRef?.current?.clientHeight
64 | ? `${containerRef?.current?.clientHeight}px`
65 | : '100%',
66 | };
67 |
68 | if (tab.url.href === SIDEBAR_TAB_FAKE_URL) {
69 | return null;
70 | }
71 |
72 | return (
73 |
74 |
75 |
76 |
77 | {canLoad &&
101 | );
102 | };
103 |
--------------------------------------------------------------------------------
/src/modules/builder/SearchEngineDropdown/SearchEngineDropdown.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useState } from 'react';
2 | import Select, { OptionProps } from 'antd/lib/select';
3 | import Button from 'antd/lib/button';
4 | import SearchEngineManager from 'lib/engines';
5 | import { NewSearchEngineModal } from 'modules/builder';
6 | import { SIDEBAR_Z_INDEX } from 'constant';
7 | import 'antd/lib/button/style/index.css';
8 | import 'antd/lib/select/style/index.css';
9 |
10 | /** MAGICS **/
11 | const SEARCH_ENGINE_DROPDOWN_LABEL = 'Select search engine...';
12 | const NEW_ENGINE_BUTTON_TEXT = '+ Add New Search Engine';
13 |
14 | const { Option } = Select;
15 |
16 | export const SearchEngineDropdown: SearchEngineDropdown = ({
17 | newValue,
18 | handleSelect,
19 | placeholder = SEARCH_ENGINE_DROPDOWN_LABEL,
20 | }) => {
21 | const [engines, setEngines] = useState>(Object.create(null));
22 | const [isModalVisible, setIsModalVisible] = useState(false);
23 |
24 | const showModal: React.MouseEventHandler = () => {
25 | setIsModalVisible(true);
26 | };
27 |
28 | const dropdownRef = useRef(null);
29 |
30 | const handleFilter = (inputValue: string, option?: Omit) => {
31 | return (
32 | String(option?.key ?? '')
33 | .toLowerCase()
34 | .search(inputValue.toLowerCase()) > -1
35 | );
36 | };
37 |
38 | const handleNewEngine = (e: Event) => {
39 | e.preventDefault();
40 | e.stopPropagation();
41 | };
42 |
43 | useEffect(() => {
44 | setEngines(SearchEngineManager.engines);
45 | // Singleton instance not reinitialized on rerender.
46 | // ! Be careful when updating the dependency list!
47 | // eslint-disable-next-line react-hooks/exhaustive-deps
48 | }, [SearchEngineManager.engines]);
49 |
50 | const getPopupContainer = () => dropdownRef.current as HTMLDivElement;
51 |
52 | return (
53 | <>
54 |
99 |
100 |
106 | >
107 | );
108 | };
109 |
--------------------------------------------------------------------------------
/src/modules/onboarding/IntroductionPage/IntroductionPage.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @module modules:introduction
3 | * @version 1.0.0
4 | * @license (C) Insight
5 | */
6 |
7 | import React, { useCallback, useEffect, useState } from 'react';
8 | import { Helmet } from 'react-helmet';
9 | import Steps from 'antd/lib/steps';
10 | import {
11 | WelcomeFrame,
12 | PrivacyFrame,
13 | QueriesFrame,
14 | } from 'modules/onboarding';
15 | import { APP_NAME, SYNC_FINISHED_KEY } from 'constant';
16 | import 'antd/lib/steps/style/index.css';
17 | import './IntroductionPage.scss';
18 |
19 | const { Step } = Steps;
20 |
21 | //-----------------------------------------------------------------------------------------------
22 | // ! Magics
23 | //-----------------------------------------------------------------------------------------------
24 | const TAB_TITLE = `Welcome to ${APP_NAME}`;
25 | const WELCOME_SECTION_TITLE = 'Welcome';
26 | const PRIVACY_SECTION_TITLE = 'Privacy';
27 | const FINISHED_SECTION_TITLE = 'Done';
28 |
29 | //-----------------------------------------------------------------------------------------------
30 | // ! Context
31 | //-----------------------------------------------------------------------------------------------
32 | type TContext = {
33 | currentStep: number;
34 | setCurrentStep: React.Dispatch>;
35 | finished: boolean;
36 | setFinished: React.Dispatch>;
37 | };
38 | export const StepContext = React.createContext(Object.create(null));
39 |
40 | //-----------------------------------------------------------------------------------------------
41 | // ! Component
42 | //-----------------------------------------------------------------------------------------------
43 | export const IntroductionPage: IntroductionPage = () => {
44 | const CONTEXT_VALUE: TContext = Object.create(null);
45 |
46 | const [currentStep, setCurrentStep] = useState(0);
47 | CONTEXT_VALUE.currentStep = currentStep;
48 | CONTEXT_VALUE.setCurrentStep = setCurrentStep;
49 |
50 | const [finished, setFinished] = useState(false);
51 | CONTEXT_VALUE.finished = finished;
52 | CONTEXT_VALUE.setFinished = setFinished;
53 |
54 | const STEPS = [
55 | {
56 | title: WELCOME_SECTION_TITLE,
57 | component: ,
58 | disabled: undefined,
59 | },
60 | {
61 | title: PRIVACY_SECTION_TITLE,
62 | component: ,
63 | disabled: undefined,
64 | },
65 | {
66 | title: FINISHED_SECTION_TITLE,
67 | component: ,
68 | disabled: currentStep !== 2 && !finished,
69 | },
70 | ];
71 |
72 | //-----------------------------------------------------------------------------------------------
73 | // ! Handlers
74 | //-----------------------------------------------------------------------------------------------
75 | const checkFinished = useCallback(async () => {
76 | const stored = await new Promise>((resolve) =>
77 | chrome.storage.sync.get(SYNC_FINISHED_KEY, resolve),
78 | ).then((result) => !!result?.[SYNC_FINISHED_KEY]);
79 | setFinished(stored);
80 | if (stored) {
81 | setCurrentStep(2);
82 | }
83 | }, []);
84 |
85 | useEffect(() => {
86 | checkFinished();
87 | }, [checkFinished]);
88 |
89 | const current = STEPS[currentStep];
90 |
91 | //-----------------------------------------------------------------------------------------------
92 | // ! Render
93 | //-----------------------------------------------------------------------------------------------
94 | return (
95 |
96 |
97 | {TAB_TITLE}
98 |
99 |
100 |
101 | {STEPS.map((step) => (
102 |
103 | ))}
104 |
105 | {current.component}
106 |
107 |
108 | );
109 | };
110 |
--------------------------------------------------------------------------------
/public/overwrite.css:
--------------------------------------------------------------------------------
1 | @keyframes MessageMoveOut {
2 | 0% {
3 | max-height: 150px;
4 | padding: 8px;
5 | opacity: 1;
6 | }
7 | 100% {
8 | max-height: 0;
9 | padding: 0;
10 | opacity: 0;
11 | }
12 | }
13 |
14 | html,
15 | body {
16 | width: 100% !important;
17 | max-width: 100% !important;
18 | }
19 |
20 | /* Google Searchbar */
21 | #searchform {
22 | z-index: 9998; /* ! MUST SET MANUALLY */
23 | }
24 |
25 | .insight-open-augmentation-builder-button {
26 | margin-top: 10px;
27 | }
28 |
29 | .insight-blocked-ad-overlay {
30 | font-family: -apple-system, BlinkMacSystemFont, sans-serif;
31 | position: absolute;
32 | left: 0;
33 | top: 0;
34 | right: 0;
35 | bottom: 0;
36 | background: linear-gradient(hsla(0, 0%, 100%, 0.9) 0%, #fff);
37 | }
38 |
39 | .insight-blocked {
40 | position: relative !important;
41 | max-height: 125px !important;
42 | overflow: hidden !important;
43 | }
44 |
45 | .insight-blocked-ad-overlay.insight-dark {
46 | background: linear-gradient(hsla(0, 0%, 10%, 0.95) 0%, #111);
47 | }
48 |
49 | .insight-blocked-ad-text-wrapper {
50 | position: absolute;
51 | left: 20px;
52 | top: 30px;
53 | font-weight: bold;
54 | font-size: 24px;
55 | color: #444;
56 | }
57 |
58 | .insight-dark .insight-blocked-ad-text-wrapper {
59 | color: #DDD;
60 | }
61 |
62 | .insight-blocked-ad-inner-text {
63 | font-weight: normal;
64 | margin-top: 10px;
65 | font-size: 16px;
66 | }
67 |
68 | .insight-hidden-domain-overlay {
69 | font-family: -apple-system, BlinkMacSystemFont, sans-serif;
70 | position: absolute;
71 | left: 0;
72 | top: 0;
73 | right: 0;
74 | bottom: 0;
75 | background: linear-gradient(rgb(255 255 255 / 95%) 0%, rgb(255 255 255));
76 | }
77 |
78 | .insight-hidden-domain-overlay.insight-dark {
79 | background: linear-gradient(hsla(0, 0%, 10%, 0.95) 0%, #222);
80 | }
81 |
82 | .insight-hidden-domain-text-wrapper {
83 | bottom: 50%;
84 | font-size: 14px;
85 | color: #777;
86 | }
87 |
88 | .insight-dark .insight-hidden-domain-text-wrapper {
89 | color: #DDD;
90 | }
91 |
92 | .insight-hidden-domain-inner-text {
93 | font-weight: normal;
94 | margin-top: 10px;
95 | font-size: 16px;
96 | }
97 |
98 | .insight-hidden-domain-button-root {
99 | display: inline;
100 | }
101 |
102 | .insight-hide-domain-action-button {
103 | padding: 0 2px !important;
104 | }
105 |
106 | .ant-message {
107 | box-sizing: border-box;
108 | margin: 0;
109 | padding: 0;
110 | color: rgba(0, 0, 0, 0.85);
111 | font-size: 14px;
112 | font-variant: tabular-nums;
113 | line-height: 1.5715;
114 | list-style: none;
115 | font-feature-settings: 'tnum';
116 | position: fixed;
117 | top: 8px;
118 | left: 0;
119 | z-index: 1010;
120 | width: 100%;
121 | pointer-events: none;
122 | }
123 |
124 | .ant-message-custom-content {
125 | display: flex;
126 | align-items: center;
127 | }
128 |
129 | .ant-message-notice {
130 | padding: 8px;
131 | text-align: center;
132 | }
133 | .ant-message-notice-content {
134 | display: inline-block;
135 | padding: 10px 16px;
136 | background: #fff;
137 | border-radius: 2px;
138 | box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08),
139 | 0 9px 28px 8px rgba(0, 0, 0, 0.05);
140 | pointer-events: all;
141 | }
142 |
143 | .ant-message-success .anticon {
144 | color: #52c41a;
145 | }
146 |
147 | .ant-message-error .anticon {
148 | color: #ff4d4f;
149 | }
150 |
151 | .ant-message-warning .anticon {
152 | color: #faad14;
153 | }
154 |
155 | .ant-message-info .anticon,
156 | .ant-message-loading .anticon {
157 | color: #1890ff;
158 | }
159 |
160 | .ant-message .anticon {
161 | position: relative;
162 | top: 1px;
163 | margin-right: 8px;
164 | font-size: 16px;
165 | }
166 |
167 | .ant-message-notice.move-up-leave.move-up-leave-active {
168 | -webkit-animation-name: MessageMoveOut;
169 | animation-name: MessageMoveOut;
170 | -webkit-animation-duration: 0.3s;
171 | animation-duration: 0.3s;
172 | }
173 |
174 | .ant-message-rtl {
175 | direction: rtl;
176 | }
177 |
178 | .ant-message-rtl span {
179 | direction: rtl;
180 | }
181 |
182 | .ant-message-rtl .anticon {
183 | margin-right: 0;
184 | margin-left: 8px;
185 | }
186 |
--------------------------------------------------------------------------------
/src/modules/builder/ActionInput/ActionInput.tsx:
--------------------------------------------------------------------------------
1 | import React, { Suspense, useEffect, useRef, useState } from 'react';
2 | import Row from 'antd/lib/row';
3 | import Col from 'antd/lib/col';
4 | import Button from 'antd/lib/button';
5 | import Input from 'antd/lib/input';
6 | import { NewActionDropdown, MultiValueInput, SearchEngineDropdown } from 'modules/builder';
7 | import SidebarLoader from 'lib/sidebar';
8 | import { ACTION_KEY, ACTION_LABEL } from 'constant';
9 | import 'antd/lib/input/style/index.css';
10 | import 'antd/lib/button/style/index.css';
11 | import 'antd/lib/grid/style/index.css';
12 |
13 | type TSelect = Record<'key' | 'value' | 'label', string>;
14 |
15 | const MinusCircleOutlined = React.lazy(
16 | async () => await import('@ant-design/icons/MinusCircleOutlined').then((mod) => mod),
17 | );
18 | export const ActionInput: ActionInput = ({ action, saveAction, deleteAction }) => {
19 | const [newValue, setNewValue] = useState(action?.value[0]);
20 | const [newKey, setNewKey] = useState>(action?.key);
21 | const [newLabel, setNewLabel] = useState(action?.label);
22 | const dropdownRef = useRef(null);
23 |
24 | const handleChange = (e: React.ChangeEvent | string[]) => {
25 | saveAction({
26 | ...action,
27 | label: newLabel,
28 | key: newKey,
29 | value: Array.isArray(e) ? e : [e.target.value],
30 | });
31 | };
32 |
33 | const handleSaveLabel = (label: ActionLabel, key: ActionKey) => {
34 | setNewLabel(label);
35 | setNewKey(key);
36 | saveAction({
37 | ...action,
38 | label,
39 | key,
40 | value: [],
41 | });
42 | };
43 |
44 | const handleDelete = () => deleteAction(action);
45 |
46 | const handleSelect = (e: TSelect) => {
47 | const updated = JSON.parse(e.value);
48 | setNewValue({ value: JSON.stringify(updated), key: e.label, label: e.label });
49 | handleChange([updated]);
50 | };
51 |
52 | const DEFAULT_INPUTS: ActionKey[] = [
53 | ACTION_KEY.OPEN_URL,
54 | ACTION_KEY.SEARCH_FEATURE,
55 | ACTION_KEY.SEARCH_APPEND,
56 | ACTION_KEY.SEARCH_HIDE_DOMAIN,
57 | ACTION_KEY.OPEN_LINK_CSS,
58 | ACTION_KEY.NO_COOKIE,
59 | ];
60 |
61 | const INPUTS: KeyEventMap = {
62 | [ACTION_KEY.SEARCH_DOMAINS]: (
63 |
64 | ),
65 | [ACTION_KEY.SEARCH_ALSO]: (
66 |
71 | ),
72 | [ACTION_KEY.URL_NOTE]: ,
73 | default: ,
74 | };
75 |
76 | useEffect(() => {
77 | newKey === ACTION_KEY.URL_NOTE &&
78 | saveAction({
79 | ...action,
80 | label: newLabel,
81 | key: newKey,
82 | value: [SidebarLoader.url.href],
83 | });
84 | // Singleton instance not reinitialized on rerender.
85 | // ! Be careful when updating the dependency list!
86 | // eslint-disable-next-line react-hooks/exhaustive-deps
87 | }, [newKey]);
88 |
89 | return (
90 |
91 |
92 | {!action.key ? (
93 |
94 | ) : (
95 | <>
96 |
101 | {action.label}
102 | >
103 | )}
104 |
105 |
106 | {DEFAULT_INPUTS.includes(newKey) || INPUTS[newKey]
107 | ? INPUTS[DEFAULT_INPUTS.includes(newKey) ? 'default' : newKey]
108 | : null}
109 |
110 |
111 |
112 | );
113 | };
114 |
--------------------------------------------------------------------------------
/src/modules/sidebar/SidebarTabTitle/SidebarTabTitle.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import md5 from 'md5';
3 | import { useDebouncedFn } from 'beautiful-react-hooks';
4 | import Tooltip from 'antd/lib/tooltip';
5 | import SidebarLoader from 'lib/sidebar';
6 | import UserManager from 'lib/user';
7 | import { decodeSpace, extractUrlProperties, removeEmoji, removeProtocol } from 'lib/helpers';
8 | import {
9 | APP_NAME,
10 | EXTENSION_SERP_LINK_HOVEROPEN,
11 | EXTENSION_SERP_SUBTAB_CLICKED,
12 | EXTENSION_SUBTAB_SCROLL,
13 | SIDEBAR_TAB_FAKE_URL,
14 | TRIGGER_FRAME_SCROLL_LOG_MESSAGE,
15 | TRIGGER_GUTTER_HOVEROPEN_MESSAGE,
16 | URL_PARAM_TAB_TITLE_KEY,
17 | } from 'constant';
18 | import 'antd/lib/tooltip/style/index.css';
19 | import './SidebarTabTitle.scss';
20 | import { handleIcon } from 'lib/icon';
21 |
22 | /** MAGICS **/
23 | const SUGGESTED_TOOLTIP_TEXT = `Filters suggested by ${APP_NAME}`;
24 | const INSTALLED_TOOLTIP_TEXT = 'Local Filters';
25 |
26 | export const SidebarTabTitle: SidebarTabTitle = ({ tab, index, activeKey, setActiveKey }) => {
27 | const handleHoverOpenLog = useDebouncedFn(
28 | (msg: any) => {
29 | SidebarLoader.sendLogMessage(EXTENSION_SERP_LINK_HOVEROPEN, {
30 | query: SidebarLoader.query,
31 | url: msg.url,
32 | position_in_serp:
33 | SidebarLoader.publicationSlices['original'].indexOf(
34 | extractUrlProperties(msg.url).full ?? '',
35 | ) + 1,
36 | });
37 | },
38 | 1000,
39 | undefined,
40 | [],
41 | );
42 |
43 | const handleClick = () => {
44 | setActiveKey((index + 1).toString());
45 | SidebarLoader.sendLogMessage(EXTENSION_SERP_SUBTAB_CLICKED, {
46 | originalUrl: UserManager.user.privacy ? md5(SidebarLoader.url.href) : SidebarLoader.url.href,
47 | originalQuery: UserManager.user.privacy ? md5(SidebarLoader.query) : SidebarLoader.query,
48 | subtabUrl: UserManager.user.privacy ? md5(tab.url.href) : tab.url.href,
49 | subtabName: tab.augmentation.name,
50 | });
51 | };
52 |
53 | useEffect(() => {
54 | chrome.runtime.onMessage.addListener((msg) => {
55 | const matching =
56 | msg.url &&
57 | (escape(removeProtocol(decodeSpace(tab.url.href))) === escape(removeProtocol(msg.url)) ||
58 | escape(removeProtocol(decodeSpace(tab.url.href))).includes(
59 | escape(removeProtocol(msg.url)),
60 | ));
61 | if (msg.type === TRIGGER_FRAME_SCROLL_LOG_MESSAGE && matching) {
62 | SidebarLoader.sendLogMessage(EXTENSION_SUBTAB_SCROLL, {
63 | originalUrl: UserManager.user.privacy
64 | ? md5(SidebarLoader.url.href)
65 | : SidebarLoader.url.href,
66 | originalQuery: UserManager.user.privacy ? md5(SidebarLoader.query) : SidebarLoader.query,
67 | subtabUrl: UserManager.user.privacy ? md5(tab.url.href) : tab.url.href,
68 | subtabName: tab.augmentation.name,
69 | });
70 | }
71 | if (msg.type === TRIGGER_GUTTER_HOVEROPEN_MESSAGE && matching) {
72 | handleHoverOpenLog(msg);
73 | }
74 | });
75 | }, [tab.url.href, tab.augmentation.name, handleHoverOpenLog]);
76 |
77 | const tabName = () => {
78 | const icon = tab.augmentation.icon ? handleIcon(tab.augmentation.icon) : null;
79 | const name = tab.url.searchParams?.get(URL_PARAM_TAB_TITLE_KEY) ?? icon ? removeEmoji(tab.augmentation.name) : tab.augmentation.name
80 | return (
81 |
82 | { icon }{ `${ icon ? ' ' : '' }${name.trim()}` }
83 |
84 | );
85 | };
86 |
87 | const keepParent = { keepParent: false };
88 |
89 | return (
90 |
94 |
99 |
100 | { tabName() }
101 |
102 |
103 |
104 | );
105 | };
106 |
--------------------------------------------------------------------------------
/src/modules/gutter/DomainStateCheckbox/DomainStateCheckbox.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @module modules:gutter
3 | * @version 1.0.0
4 | * @license (C) Insight
5 | */
6 |
7 | import React, { useEffect, useMemo, useState } from 'react';
8 | import Checkbox, { CheckboxChangeEvent } from 'antd/lib/checkbox';
9 | import SidebarLoader from 'lib/sidebar';
10 | import AugmentationManager from 'lib/augmentations';
11 | import { AUGMENTATION_ID } from 'constant';
12 | import 'antd/lib/checkbox/style/index.css';
13 |
14 | //-----------------------------------------------------------------------------------------------
15 | // ! Magics
16 | //-----------------------------------------------------------------------------------------------
17 | const TRUSTED_CHECKBOX_TEXT = 'Trusted';
18 | const BLOCKED_CHECKBOX_TEXT = 'Blocked';
19 |
20 | //-----------------------------------------------------------------------------------------------
21 | // ! Component
22 | //-----------------------------------------------------------------------------------------------
23 | export const DomainStateCheckbox: DomainStateCheckbox = ({ domain }) => {
24 | const augmentations = useMemo(
25 | () => [...SidebarLoader.installedAugmentations, ...SidebarLoader.otherAugmentations],
26 | // Singleton instance not reinitialized on rerender.
27 | // ! Be careful when updating the dependency list!
28 | // eslint-disable-next-line react-hooks/exhaustive-deps
29 | [SidebarLoader.installedAugmentations, SidebarLoader.otherAugmentations],
30 | );
31 |
32 | const trustList = useMemo(
33 | () => augmentations.find(({ id }) => id === AUGMENTATION_ID.TRUSTLIST),
34 | [augmentations],
35 | );
36 |
37 | const blockList = useMemo(
38 | () => augmentations.find(({ id }) => id === AUGMENTATION_ID.BLOCKLIST),
39 | [augmentations],
40 | );
41 |
42 | const [isBlocked, setIsBlocked] = useState(
43 | !!blockList?.actions?.action_list?.filter(
44 | (action) => !!action.value.find((value) => value === domain),
45 | ).length,
46 | );
47 |
48 | const [isTrusted, setIsTrusted] = useState(
49 | !!trustList?.actions?.action_list?.filter(
50 | (action) => !!action.value.find((value) => value === domain),
51 | ).length,
52 | );
53 |
54 | //-----------------------------------------------------------------------------------------------
55 | // ! Handlers
56 | //-----------------------------------------------------------------------------------------------
57 |
58 | const handleToggleBlocked = async (e: CheckboxChangeEvent) => {
59 | e.target.checked
60 | ? await AugmentationManager.updateBlockList(domain)
61 | : await AugmentationManager.deleteFromBlockList(domain);
62 | setIsBlocked(e.target.checked);
63 | };
64 |
65 | const handleToggleTrusted = (e: CheckboxChangeEvent) => {
66 | AugmentationManager.toggleTrustlist(domain);
67 | setIsTrusted(e.target.checked);
68 | };
69 |
70 | useEffect(() => {
71 | setIsBlocked(
72 | !!augmentations
73 | .find(({ id }) => id === AUGMENTATION_ID.BLOCKLIST)
74 | ?.actions?.action_list?.filter((action) => !!action.value.find((value) => value === domain))
75 | .length,
76 | );
77 |
78 | setIsTrusted(
79 | !!augmentations
80 | .find(({ id }) => id === AUGMENTATION_ID.TRUSTLIST)
81 | ?.actions?.action_list?.filter((action) => !!action.value.find((value) => value === domain))
82 | .length,
83 | );
84 | }, [domain, augmentations]);
85 |
86 | //-----------------------------------------------------------------------------------------------
87 | // ! Render
88 | //-----------------------------------------------------------------------------------------------
89 | return (
90 |
91 |
96 | {TRUSTED_CHECKBOX_TEXT}
97 |
98 |
103 | {BLOCKED_CHECKBOX_TEXT}
104 |
105 |
106 | );
107 | };
108 |
--------------------------------------------------------------------------------
/src/lib/expand/index.test.ts:
--------------------------------------------------------------------------------
1 | import React, { ReactElement } from "react";
2 | import { render } from 'react-dom';
3 | import { expandSidebar } from ".";
4 | import { Sidebar } from 'modules/sidebar';
5 | import SidebarLoader from 'lib/sidebar';
6 |
7 | function reactInjector(el: HTMLElement, reactEl: ReactElement, frameId: string) {
8 | const iframe = document.createElement('iframe');
9 | iframe.id = frameId;
10 | el.appendChild(iframe);
11 | const injector = () => {
12 | const doc = iframe.contentWindow?.document;
13 | if (!doc) {
14 | return null;
15 | }
16 | const link = doc.createElement('link');
17 | link.setAttribute('type', 'text/css');
18 | link.setAttribute('rel', 'stylesheet');
19 | link.setAttribute('href', chrome.runtime.getURL('bundle.css'));
20 | doc.head.appendChild(link);
21 | doc.body.id = 'insight-sidebar';
22 | doc.documentElement.setAttribute('style', 'overflow: hidden;');
23 | const div = document.createElement('div');
24 | const root = doc.body.appendChild(div);
25 | render(reactEl, root);
26 | };
27 | // Firefox is a special case, we need to set IFrame source to make it work.
28 | // Here we add an empty HTML file as source, so the browser won't complain.
29 | if (navigator.userAgent.search('Firefox') > -1) {
30 | iframe.src = chrome.runtime.getURL('index.html');
31 | iframe.onload = () => injector();
32 | } else {
33 | injector();
34 | }
35 | }
36 |
37 | describe('Expand tests', () => {
38 |
39 | beforeEach(() => {
40 | (global.matchMedia as any) = () => false;
41 |
42 | SidebarLoader.url = new URL('https://www.google.com/');
43 |
44 | const wrapper = document.createElement('div');
45 | wrapper.id = 'sidebar-root';
46 | wrapper.style.display = 'none';
47 | document.body.appendChild(wrapper);
48 | const sidebarInit = React.createElement(Sidebar);
49 | reactInjector(wrapper, sidebarInit, 'sidebar-root-iframe');
50 | });
51 |
52 | afterEach(() => {
53 | const sidebarRoot = document.getElementById('sidebar-root') as HTMLDivElement;
54 | sidebarRoot.remove();
55 | });
56 |
57 | test('expand should add expanded class', async () => {
58 | // Given
59 | const sidebarRoot = document.getElementById('sidebar-root') as HTMLDivElement;
60 | const sidebarRootIframe = document.getElementById('sidebar-root-iframe') as HTMLIFrameElement;
61 | const sidebarContainer = sidebarRootIframe!
62 | .contentWindow!.document.getElementById('insight-sidebar-container') as HTMLDivElement;
63 |
64 | document.documentElement.style.overflow = 'none';
65 |
66 | expect(sidebarRoot.classList.contains('insight-expanded')).toBe(false);
67 | expect(sidebarRootIframe.classList.contains('insight-expanded')).toBe(false);
68 | expect(sidebarContainer.classList.contains('insight-expanded')).toBe(false);
69 |
70 | // When
71 | expandSidebar(SidebarLoader);
72 |
73 | // Then
74 | expect(sidebarRoot.classList.contains('insight-expanded')).toBe(true);
75 | expect(sidebarRootIframe.classList.contains('insight-expanded')).toBe(true);
76 | expect(sidebarContainer.classList.contains('insight-expanded')).toBe(true);
77 | expect(document.documentElement.style.overflow).toBe('hidden');
78 | });
79 |
80 | test('expand again should hide it', async () => {
81 | // Given
82 | const sidebarRoot = document.getElementById('sidebar-root') as HTMLDivElement;
83 | const sidebarRootIframe = document.getElementById('sidebar-root-iframe') as HTMLIFrameElement;
84 | const sidebarContainer = sidebarRootIframe!
85 | .contentWindow!.document.getElementById('insight-sidebar-container') as HTMLDivElement;
86 |
87 | expandSidebar(SidebarLoader);
88 |
89 | expect(sidebarRoot.classList.contains('insight-expanded')).toBe(true);
90 | expect(sidebarRootIframe.classList.contains('insight-expanded')).toBe(true);
91 | expect(sidebarContainer.classList.contains('insight-expanded')).toBe(true);
92 |
93 | // When
94 | expandSidebar(SidebarLoader);
95 |
96 | // Then
97 | expect(sidebarRoot.classList.contains('insight-expanded')).toBe(false);
98 | expect(sidebarRootIframe.classList.contains('insight-expanded')).toBe(false);
99 | expect(sidebarContainer.classList.contains('insight-expanded')).toBe(false);
100 | expect(document.documentElement.style.overflow).toBe('auto');
101 | });
102 |
103 | })
104 |
--------------------------------------------------------------------------------