├── .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 | 14 | 19 | 20 | 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 | 12 | 19 | 26 | 33 | 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 | <Typist {...TYPIST_CONFIG} onTypingDone={handleTyped}> 37 | {TYPIST_TEXT} 38 | </Typist> 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 |
28 | Link 1 29 | Link 2 30 |
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 |
70 | 71 |
72 | 77 |
78 |
79 | ); 80 | 81 | const Header = () => ( 82 |
83 | 86 | {HEADER_TITLE} 87 |
88 | ); 89 | 90 | return ( 91 | 92 |
93 |
94 |
95 | 96 |
97 |
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 &&