diff --git a/Makefile b/Makefile index e8b72ff..a0d3519 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,7 @@ clean: # DEVELOPMENT # --------------------------------------------------------------------------------------------------------------------- -dev: docker-build docker-console +dev: docker-build docker-start production: npm run build @@ -40,3 +40,9 @@ docker-console: docker run -it --rm -v ${PWD}:/work -w /work --name mind-history-extension -p 3000:3000 $(DOCKER_IMAGE_TAG) bash console: docker-console + +docker-start: + docker run -it --rm -v ${PWD}:/work -w /work --name mind-history-extension -p 3000:3000 $(DOCKER_IMAGE_TAG) yarn start + +attach-console: + docker exec -it mind-history-extension /bin/bash diff --git a/build/dev-server.js b/build/dev-server.js index 85e4e1a..56a32fb 100755 --- a/build/dev-server.js +++ b/build/dev-server.js @@ -41,6 +41,11 @@ var server = new WebpackDevServer(compiler, { 'Access-Control-Allow-Origin': '*', }, disableHostCheck: true, + stats: config.stats, + overlay: { + warnings: false, + errors: true, + }, }) server.listen(env.PORT) diff --git a/package.json b/package.json index a0c4337..6706e24 100755 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "scripts": { "build": "node build/production.js", "start": "node build/dev-server.js", + "dev": "DEBUG=true NODE_ENV=development npm run start", "lint": "yarn lintonly --fix", "lintonly": "eslint src/**/*.{ts,tsx}", "test": "jest", @@ -27,6 +28,7 @@ "react": "^17.0.1", "react-cytoscapejs": "^1.2.1", "react-dom": "^17.0.1", + "react-helmet": "^6.1.0", "react-hot-loader": "^4.13.0", "react-redux": "^7.2.2", "react-router-dom": "^5.2.0", @@ -52,6 +54,7 @@ "@types/jest": "^26.0.19", "@types/react": "^17.0.0", "@types/react-dom": "^17.0.0", + "@types/react-helmet": "^6.1.0", "@types/react-redux": "^7.1.14", "@types/react-router-dom": "^5.1.7", "@typescript-eslint/eslint-plugin": "^4.11.1", @@ -97,4 +100,4 @@ "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" } } -} +} \ No newline at end of file diff --git a/src/browser/index.ts b/src/browser/index.ts new file mode 100644 index 0000000..4e35559 --- /dev/null +++ b/src/browser/index.ts @@ -0,0 +1,5 @@ +import * as tabs from './tabs' + +export { + tabs +} \ No newline at end of file diff --git a/src/browser/tabs.ts b/src/browser/tabs.ts new file mode 100644 index 0000000..5012867 --- /dev/null +++ b/src/browser/tabs.ts @@ -0,0 +1,27 @@ + +/** Get tab which user currently see */ +export const getActive = (): Promise => new Promise((resolve, reject) => { + chrome.tabs.query({ active: true, windowId: chrome.windows.WINDOW_ID_CURRENT }, tabs => { + if (!tabs.length) { + reject(new Error("Cannot retrive active tab")) + return + } + + const [tab] = tabs + console.log('Active tab is', tab.url || tab.pendingUrl, 'and title', tab.title, 'with status', tab.status, tab) + resolve(tab) + }) +}) + +/** Gets the tab that this script call is being made from. On background will throw exception */ +export const getScriptTab = (): Promise => new Promise((resolve, reject) => { + chrome.tabs.getCurrent(tab => { + if (!tab) { + reject(new Error("Current script not have tab")) + return + } + + console.log('Current tab is', tab) + resolve(tab) + }) +}) \ No newline at end of file diff --git a/src/manifest.json b/src/manifest.json index 333504d..e7fdf5e 100755 --- a/src/manifest.json +++ b/src/manifest.json @@ -22,6 +22,9 @@ "permissions": [ "tabs", "history", - "webNavigation" + "webNavigation", + "", + "webRequest", + "webRequestBlocking" ] } \ No newline at end of file diff --git a/src/pages/Background/browser/tabs.ts b/src/pages/Background/browser/tabs.ts deleted file mode 100644 index 83faec9..0000000 --- a/src/pages/Background/browser/tabs.ts +++ /dev/null @@ -1,13 +0,0 @@ - -export const getCurrentTab = (): Promise => new Promise((resolve, reject) => { - chrome.tabs.query({ active: true, windowId: chrome.windows.WINDOW_ID_CURRENT }, tabs => { - if (!tabs.length) { - reject(new Error("Cannot retrive current tab")) - return - } - - const [tab] = tabs - console.log('Currnet tab is', tab.url || tab.pendingUrl, 'and title', tab.title, 'with status', tab.status, tab) - resolve(tab) - }) -}) \ No newline at end of file diff --git a/src/pages/Background/onOpenTab.ts b/src/pages/Background/onOpenTab.ts index 1931c2b..91b7879 100644 --- a/src/pages/Background/onOpenTab.ts +++ b/src/pages/Background/onOpenTab.ts @@ -8,13 +8,16 @@ export function registerOnTabOpenHook() { chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) { store.dispatch(actions.tryCloseOldPages()) + if (process.env.DEBUG) + console.debug("chrome.tabs.onUpdated", tabId, changeInfo, tab) + if (changeInfo.url) { store.dispatch(actions.savePageData({ url: changeInfo.url, page: { title: changeInfo.title, favIconUrl: changeInfo.favIconUrl } })) } - if (!isLoaded(changeInfo)) + if (!isLoaded(tab)) return const { url, title, favIconUrl } = getTabGrantedData(tab) @@ -29,6 +32,9 @@ export function registerOnTabOpenHook() { chrome.tabs.onActivated.addListener(({ tabId, windowId }) => { chrome.tabs.get(tabId, tab => { + if (process.env.DEBUG) + console.debug("chrome.tabs.onActivated", tabId, tab) + const url = tab.url || tab.pendingUrl console.log('onActivated Active page changed to', tab.title, 'with url', url, tab) diff --git a/src/pages/Background/onVisitPage.ts b/src/pages/Background/onVisitPage.ts index 419d627..5a5e60f 100644 --- a/src/pages/Background/onVisitPage.ts +++ b/src/pages/Background/onVisitPage.ts @@ -1,11 +1,11 @@ -import { getCurrentTab } from "./browser/tabs" +import { tabs } from "../../browser" import { NotHavePermissionError } from "./errors" import { store, actions } from "./store" export function registerOnVisitPageHook() { - if (!chrome.history) + if (!chrome.history) throw new NotHavePermissionError('history', 'access visited pages urls') - + chrome.history.onVisited.addListener(async event => { console.log('onVisited', event.url) @@ -41,13 +41,13 @@ export function registerOnVisitPageHook() { // On visited called allways, on new page loads // instead of onActivated, which not called when we load new page on same tab // so need change current tab - const tab = await getCurrentTab() + const tab = await tabs.getActive() if (tab.status === 'loading') { console.log('onVisited Active tab is changed to loading page, with url', tab.pendingUrl, tab) const url = tab.url || tab.pendingUrl - if (url) + if (url) store.dispatch(actions.setCurrentPage(url)) - + return } console.log('onVisited Active page changed to', tab.title, 'with url', tab.url, tab) diff --git a/src/pages/Background/store/reducer.ts b/src/pages/Background/store/reducer.ts index e68ac82..864d8f9 100644 --- a/src/pages/Background/store/reducer.ts +++ b/src/pages/Background/store/reducer.ts @@ -43,7 +43,7 @@ export const pagesReducer = createReducer(initialState, { lastAccessTime: getLastOrExistedTime(page.lastAccessTime, oldData.lastAccessTime) } if (process.env.DEBUG) - console.debug('Page data saved', JSON.stringify(state, null, 2)) + console.debug('Page data saved', JSON.parse(JSON.stringify(state, null, 2))) }, diff --git a/src/pages/Background/types/guards.ts b/src/pages/Background/types/guards.ts index b07d08d..6cdcc2a 100644 --- a/src/pages/Background/types/guards.ts +++ b/src/pages/Background/types/guards.ts @@ -2,8 +2,8 @@ // changeInfo.status // Tab can be not loaded and not in loading state, // changeInfo.status can not exists in that case. -export const isLoaded = (changeInfo: chrome.tabs.TabChangeInfo) => changeInfo.status === 'complete' -export const isLoading = (changeInfo: chrome.tabs.TabChangeInfo) => changeInfo.status === 'loading' +export const isLoaded = (tab: chrome.tabs.Tab) => tab.status === 'complete' +export const isLoading = (tab: chrome.tabs.Tab) => tab.status === 'loading' // Can be fired on search in google, but also firse "link" export const isOnFormSubmit = (transition: string) => diff --git a/src/pages/Background/willOpenPage.ts b/src/pages/Background/willOpenPage.ts index a549de0..ae3148a 100644 --- a/src/pages/Background/willOpenPage.ts +++ b/src/pages/Background/willOpenPage.ts @@ -1,16 +1,16 @@ import { NotHavePermissionError } from "./errors" import { isByLink } from "./types/guards" import { store, actions } from "./store" -import { getCurrentTab } from "./browser/tabs" +import { tabs } from "../../browser" export function registerOnWillOpenPageHook() { - if (!chrome.webNavigation) + if (!chrome.webNavigation) throw new NotHavePermissionError('webNavigation', 'see user page transtions') - + chrome.webNavigation.onBeforeNavigate.addListener(async event => { console.log('onBeforeNavigate to ' + event.url, 'in tab', event.tabId, 'at', new Date(event.timeStamp)) - const tab = await getCurrentTab() + const tab = await tabs.getActive() console.log('Was start opening page', event.url, 'from tab with url', tab.url || tab.pendingUrl, 'and title', tab.title, 'at', new Date(event.timeStamp), tab) }) diff --git a/src/pages/Graph/App.scss b/src/pages/Graph/App.scss index 3d03476..93002f2 100644 --- a/src/pages/Graph/App.scss +++ b/src/pages/Graph/App.scss @@ -1,18 +1,3 @@ -$myColor: rgb(17, 0, 255); - -h1, -h2, -h3, -h4, -h5, -h6 { - color: $myColor; - text-align: center; -} - -h1 { - margin-top: 3rem; -} .App { background-color: #282c34; diff --git a/src/pages/Graph/index.tsx b/src/pages/Graph/index.tsx index ec55ee2..bd5c264 100644 --- a/src/pages/Graph/index.tsx +++ b/src/pages/Graph/index.tsx @@ -2,16 +2,28 @@ import React from 'react' import { render } from 'react-dom' import { Provider } from 'react-redux' -import { store } from './store' +import { getFaviconsUrls, store } from './store' import App from './App' import './index.css' import { connectToDataBus } from './data-bus' +import { unblockResourcesLoading } from '../../resourcesLoading' -render( - - - , - window.document.querySelector('#app-container') -) +async function main() { -connectToDataBus() \ No newline at end of file + try { + await unblockResourcesLoading(getFaviconsUrls) + } catch (err) { + console.warn('Cannot set unblock for favicons', err) + } + + render( + + + , + window.document.querySelector('#app-container') + ) + + connectToDataBus() +} + +main() diff --git a/src/pages/Graph/store/index.ts b/src/pages/Graph/store/index.ts index e67c225..79e48f3 100644 --- a/src/pages/Graph/store/index.ts +++ b/src/pages/Graph/store/index.ts @@ -5,4 +5,11 @@ export * as actions from './actions' export const store = configureStore({ reducer: pagesReducer -}) \ No newline at end of file +}) + +export const getFaviconsUrls = (): Array => { + const { pages } = store.getState() + return Object.keys(pages) + .map(pageUrl => pages[pageUrl].favIconUrl) + .filter(url => !!url) as Array +} diff --git a/src/pages/Graph/views/MindGraph.tsx b/src/pages/Graph/views/MindGraph.tsx deleted file mode 100644 index 34eb72b..0000000 --- a/src/pages/Graph/views/MindGraph.tsx +++ /dev/null @@ -1,191 +0,0 @@ -import { EdgeDefinition, NodeDefinition } from 'cytoscape' -import React, { useEffect, useState } from 'react' -import CytoscapeComponent, { CytoscapeHook } from 'react-cytoscapejs' -import { PageVisit } from '../../../history' -import { PageDataDictanory } from '../../../types' -import { setupCyHooks, renderState } from '../graph' -import { useHistory } from 'react-router-dom' -import { AbstractNode, AbstractTreesGraph } from '../../../graph' -import { isTrackablePage } from '../../../history/filter' - -export interface MindGraphProps { - pages: PageDataDictanory - history: Array - nodeUrl?: string | null -} - -// TODO: calculate width and height on start -const MAX_WIDTH = 1440 -const MAX_HEIGHT = 720 - -export const MindGraph: React.FC = ({ pages, history, nodeUrl }) => { - useEffect(() => { - if (!nodeUrl) { - document.title = 'Mind History Graph' - return - } - const { title } = pages[nodeUrl] || {} - if (!title) - return - - document.title = `${title} | Mind History Graph` - - }, [pages, nodeUrl]) - - pages = filterPages(pages) - - let nodes = mapToNodes(pages) - - const edges = mapToEdges(history, Object.keys(pages)) - - nodes.forEach(node => { - node.data.score = countEdges(node, edges) - }) - nodes = nodes.filter(node => node.data.score !== 0) - - const elements = CytoscapeComponent.normalizeElements({ - nodes, - edges - }) - - console.log({ elements }) - - const g = new AbstractTreesGraph() - g.addNodes(nodes.map(({ data }) => data as AbstractNode)) - g.addEdges(edges.map(({ data }) => data)) - - const historyManager = useHistory() - - const [isCoreSetupComplete, setCoreSetupStatus] = useState(false) - const coreSetupComplete = () => setCoreSetupStatus(true) - - // prevent multiple setups on updates - const coreHookCallback: CytoscapeHook = core => { - renderState(core, g, nodeUrl) - if (isCoreSetupComplete) - return - - - setupCyHooks(core, g, historyManager) - coreSetupComplete() - } - - return ( -
-

Mind Graph

-

Found nodes {nodes.length}

- - {elements.length && ( - )} -
- ) - -} - -function filterPages(pages: PageDataDictanory) { - const allowedPageUrls = Object.keys(pages) - .filter(url => isTrackablePage(url)) - - return copyKeys({}, pages, allowedPageUrls) -} - -function copyKeys(target: T, source: T, keys: string[]): T { - return keys.reduce((target, key) => { - // @ts-ignore - target[key] = source[key] - return target - }, target) -} - - -function mapToNodes(pages: PageDataDictanory): Array { - const result: Array = [] - - for (const url in pages) { - const page = pages[url] - - result.push({ - data: { - id: url, - label: page.title || url, - favIconUrl: page.favIconUrl, - }, - // position: { x: getRandomInt(0, MAX_WIDTH), y: getRandomInt(0, MAX_HEIGHT) } - }) - } - - return result -} - - -const mapToEdges = (history: Array, existingUrls: Array): Array => history - .filter(visit => !!visit.from) - .filter(visit => existingUrls.includes(visit.from!) && existingUrls.includes(visit.to)) - .map(visit => ({ - data: { - id: `${visit.from}->${visit.to}`, - source: visit.from!, - target: visit.to, - } - })) - -const countEdges = (node: NodeDefinition, edges: Array): number => { - let count = 0 - for (const edge of edges) { - if (edge.data.source === node.data.id || edge.data.target === node.data.id) - count++ - - } - - return count -} - -// function getRandomInt(min: number, max: number): number { -// min = Math.ceil(min) -// max = Math.floor(max) -// return Math.floor(Math.random() * (max - min + 1)) + min -// } - -const graphStyles = [{ - "selector": "core", - "style": { - "selection-box-color": "#AAD8FF", - "selection-box-border-color": "#8BB0D0", - "selection-box-opacity": "0.5" - } -}, { - "selector": "node", - "style": { - "width": "mapData(score, 1, 5, 30, 60)", - "height": "mapData(score, 1, 5, 30, 60)", - "label": "data(label)", - "font-size": "14px", - "text-valign": "bottom", - "text-halign": "center", - "background-color": "#777", - "background-image": 'data(favIconUrl)', - "color": "#fff", - "overlay-padding": "6px", - "z-index": "10" - } -}, { - "selector": "edge", - "style": { - 'width': 1, - "curve-style": "haystack", - "haystack-radius": "0.1", - "opacity": "0.4", - "line-color": "red", - "overlay-padding": "5px" - } -}, -] \ No newline at end of file diff --git a/src/pages/Graph/views/MindGraph/containers/NavBar/index.scss b/src/pages/Graph/views/MindGraph/containers/NavBar/index.scss new file mode 100644 index 0000000..e06b364 --- /dev/null +++ b/src/pages/Graph/views/MindGraph/containers/NavBar/index.scss @@ -0,0 +1,17 @@ +@import '../../../../../../styles.config.scss'; + +.nav-bar { + position: fixed; + top: 0; + left: 0; + right: 0; + + h1 { + margin-top: 1rem; + color: $primary-color; + text-align: left; + font-size: 1.5rem; + padding-left: 2rem; + } + +} diff --git a/src/pages/Graph/views/MindGraph/containers/NavBar/index.tsx b/src/pages/Graph/views/MindGraph/containers/NavBar/index.tsx new file mode 100644 index 0000000..87e71aa --- /dev/null +++ b/src/pages/Graph/views/MindGraph/containers/NavBar/index.tsx @@ -0,0 +1,8 @@ +import React from 'react' +import './index.scss' + +export const NavBar: React.FC = () => ( +
+

Mind Graph

+
+) \ No newline at end of file diff --git a/src/pages/Graph/views/MindGraph/containers/TreePagesGraph/components/CytoscapeWrapper.tsx b/src/pages/Graph/views/MindGraph/containers/TreePagesGraph/components/CytoscapeWrapper.tsx new file mode 100644 index 0000000..0f3c55d --- /dev/null +++ b/src/pages/Graph/views/MindGraph/containers/TreePagesGraph/components/CytoscapeWrapper.tsx @@ -0,0 +1,28 @@ +import React, { useState } from "react" +import CytoscapeComponent, { CytoscapeComponentProps, CytoscapeHook } from "react-cytoscapejs" + +export interface CytoscapeWrapperProps extends CytoscapeComponentProps { + onRerenderChange: CytoscapeHook + onSetup: CytoscapeHook +} + +export const CytoscapeWrapper: React.FC = ({ onRerenderChange, onSetup, ...props }) => { + const [isCoreSetupComplete, setCoreSetupStatus] = useState(false) + const coreSetupComplete = () => setCoreSetupStatus(true) + + // prevent multiple setups on updates + const coreHookCallback: CytoscapeHook = core => { + onRerenderChange(core) + if (isCoreSetupComplete) + return + + + onSetup(core) + coreSetupComplete() + } + + return +} \ No newline at end of file diff --git a/src/pages/Graph/views/MindGraph/containers/TreePagesGraph/components/FullScreenTreesGraph.tsx b/src/pages/Graph/views/MindGraph/containers/TreePagesGraph/components/FullScreenTreesGraph.tsx new file mode 100644 index 0000000..1536127 --- /dev/null +++ b/src/pages/Graph/views/MindGraph/containers/TreePagesGraph/components/FullScreenTreesGraph.tsx @@ -0,0 +1,36 @@ +import React from 'react' +import { TreesGraph, TreesGraphProps } from './TreesGraph' + +export interface FullScreenTreesGraphProps extends TreesGraphProps { + +} + +export const FullScreenTreesGraph: React.FC = (props) => { + const width = getWidth() + const height = getHeight() + + return +} + +function getWidth() { + return Math.max( + document.body.scrollWidth, + document.documentElement.scrollWidth, + document.body.offsetWidth, + document.documentElement.offsetWidth, + document.documentElement.clientWidth + ) +} + +function getHeight() { + return Math.max( + document.body.scrollHeight, + document.documentElement.scrollHeight, + document.body.offsetHeight, + document.documentElement.offsetHeight, + document.documentElement.clientHeight + ) +} \ No newline at end of file diff --git a/src/pages/Graph/views/MindGraph/containers/TreePagesGraph/components/TreesGraph.tsx b/src/pages/Graph/views/MindGraph/containers/TreePagesGraph/components/TreesGraph.tsx new file mode 100644 index 0000000..f41f41c --- /dev/null +++ b/src/pages/Graph/views/MindGraph/containers/TreePagesGraph/components/TreesGraph.tsx @@ -0,0 +1,59 @@ +import React from 'react' +import { CytoscapeWrapper, CytoscapeWrapperProps } from './CytoscapeWrapper' + +export interface TreesGraphProps extends CytoscapeWrapperProps { + +} + +export const TreesGraph: React.FC = (props) => + + +const graphStyles = [ + { + "selector": "core", + "style": { + "selection-box-color": "#AAD8FF", + "selection-box-border-color": "#8BB0D0", + "selection-box-opacity": "0.5" + } + }, + { + "selector": "node", + "style": { + "width": "mapData(score, 1, 5, 30, 60)", + "height": "mapData(score, 1, 5, 30, 60)", + "label": "data(label)", + "font-size": "14px", + "text-valign": "bottom", + "text-halign": "center", + "background-color": "#777", + "color": "#fff", + "overlay-padding": "6px", + "z-index": "10" + } + }, + { + "selector": "node[favIconUrl]", + "style": { + "background-image": 'data(favIconUrl)' + } + }, + { + "selector": "edge", + "style": { + 'width': 1, + "curve-style": "haystack", + "haystack-radius": "0.1", + "opacity": "0.4", + "line-color": "red", + "overlay-padding": "5px" + } + }, +] \ No newline at end of file diff --git a/src/pages/Graph/graph.ts b/src/pages/Graph/views/MindGraph/containers/TreePagesGraph/graph.ts similarity index 100% rename from src/pages/Graph/graph.ts rename to src/pages/Graph/views/MindGraph/containers/TreePagesGraph/graph.ts diff --git a/src/pages/Graph/views/MindGraph/containers/TreePagesGraph/index.tsx b/src/pages/Graph/views/MindGraph/containers/TreePagesGraph/index.tsx new file mode 100644 index 0000000..9364f27 --- /dev/null +++ b/src/pages/Graph/views/MindGraph/containers/TreePagesGraph/index.tsx @@ -0,0 +1,45 @@ +import React from "react" +import CytoscapeComponent from "react-cytoscapejs" +import { useHistory } from "react-router-dom" +import { AbstractTreesGraph, AbstractNode } from "../../../../../../graph" +import { PageVisit } from "../../../../../../history" +import { PageDataDictanory } from "../../../../../../types" +import { FullScreenTreesGraph } from "./components/FullScreenTreesGraph" +import { renderState, setupCyHooks } from "./graph" +import { filterPages, mapToNodes, mapToEdges, countEdges } from "./prepare" + +export interface TreePagesGraphProps { + pages: PageDataDictanory + history: Array + nodeUrl?: string | null +} + +export const TreePagesGraph: React.FC = ({ pages, history, nodeUrl }) => { + // TODO: reafactor + const historyManager = useHistory() + + pages = filterPages(pages) + + let nodes = mapToNodes(pages) + + const edges = mapToEdges(history, Object.keys(pages)) + + nodes.forEach(node => { + node.data.score = countEdges(node, edges) + }) + nodes = nodes.filter(node => node.data.score !== 0) + + const elements = CytoscapeComponent.normalizeElements({ nodes, edges }) + if (!elements.length) + return null + + const g = new AbstractTreesGraph() + g.addNodes(nodes.map(({ data }) => data as AbstractNode)) + g.addEdges(edges.map(({ data }) => data)) + + return renderState(core, g, nodeUrl)} + onSetup={core => setupCyHooks(core, g, historyManager)} + /> +} diff --git a/src/pages/Graph/views/MindGraph/containers/TreePagesGraph/labels.ts b/src/pages/Graph/views/MindGraph/containers/TreePagesGraph/labels.ts new file mode 100644 index 0000000..904ce3b --- /dev/null +++ b/src/pages/Graph/views/MindGraph/containers/TreePagesGraph/labels.ts @@ -0,0 +1,34 @@ + +export interface LabelableNode { + title?: string; + url: string; +} + +const MAX_LABEL_SYMBOLS = 50 + +export function computeLabel({ title, url }: LabelableNode): string { + if (title) + return truncate(title, { max: MAX_LABEL_SYMBOLS, atEnd: false }) + + return truncate(decodeURI(url), { max: MAX_LABEL_SYMBOLS }) +} + +export interface TruncateOptions { + max: number + atEnd?: boolean + separator?: string +} + +function truncate(input: string, { max, atEnd = true, separator = '...' }: TruncateOptions): string { + if (input.length < max) + return input + + if (atEnd) + return input.substring(0, max) + separator + + const charsToShow = max - separator.length + const leftLength = Math.ceil(charsToShow / 2) + const rightLenght = Math.floor(charsToShow / 2) + + return `${input.substr(0, leftLength)}${separator}${input.substr(input.length - rightLenght)}` +}; \ No newline at end of file diff --git a/src/pages/Graph/views/MindGraph/containers/TreePagesGraph/prepare.ts b/src/pages/Graph/views/MindGraph/containers/TreePagesGraph/prepare.ts new file mode 100644 index 0000000..df28cba --- /dev/null +++ b/src/pages/Graph/views/MindGraph/containers/TreePagesGraph/prepare.ts @@ -0,0 +1,62 @@ +import { NodeDefinition, EdgeDefinition } from "cytoscape" +import { isTrackablePage, PageVisit } from "../../../../../../history" +import { PageDataDictanory } from "../../../../../../types" +import { computeLabel } from "./labels" + +export function filterPages(pages: PageDataDictanory) { + const allowedPageUrls = Object.keys(pages) + .filter(url => isTrackablePage(url)) + + return copyKeys({}, pages, allowedPageUrls) +} + +function copyKeys(target: T, source: T, keys: string[]): T { + return keys.reduce((target, key) => { + // @ts-ignore + target[key] = source[key] + return target + }, target) +} + + +export function mapToNodes(pages: PageDataDictanory): Array { + const result: Array = [] + + for (const url in pages) { + const page = pages[url] + + result.push({ + data: { + id: url, + label: computeLabel({ ...page, url }), + favIconUrl: page.favIconUrl, + }, + // position: { x: getRandomInt(0, MAX_WIDTH), y: getRandomInt(0, MAX_HEIGHT) } + }) + } + + return result +} + + +export const mapToEdges = (history: Array, existingUrls: Array): Array => history + .filter(visit => !!visit.from) + .filter(visit => existingUrls.includes(visit.from!) && existingUrls.includes(visit.to)) + .map(visit => ({ + data: { + id: `${visit.from}->${visit.to}`, + source: visit.from!, + target: visit.to, + } + })) + +export const countEdges = (node: NodeDefinition, edges: Array): number => { + let count = 0 + for (const edge of edges) { + if (edge.data.source === node.data.id || edge.data.target === node.data.id) + count++ + + } + + return count +} \ No newline at end of file diff --git a/src/pages/Graph/views/MindGraph/index.tsx b/src/pages/Graph/views/MindGraph/index.tsx new file mode 100644 index 0000000..d7a27e0 --- /dev/null +++ b/src/pages/Graph/views/MindGraph/index.tsx @@ -0,0 +1,30 @@ +import React from 'react' +import { Helmet } from 'react-helmet' +import { NavBar } from './containers/NavBar' +import { TreePagesGraph, TreePagesGraphProps } from './containers/TreePagesGraph' + +export interface MindGraphProps extends TreePagesGraphProps { +} + + +export const MindGraph: React.FC = ({ pages, history, nodeUrl }) => { + const current = nodeUrl && pages[nodeUrl] + const { title } = current || {} + + return ( +
+ + {title ? `${title} | Mind History Graph` : 'Mind History Graph'} + + + + + +
+ ) + +} + + diff --git a/src/resourcesLoading/index.ts b/src/resourcesLoading/index.ts new file mode 100644 index 0000000..d2f7ec3 --- /dev/null +++ b/src/resourcesLoading/index.ts @@ -0,0 +1,15 @@ +import { tabs } from '../browser' +import { installUnblockers } from './install' +import { ResourcesUrlGetter } from './types' + +/** + * Favicons can be blocked by CORS polycies in chrome, + * if load them from from other sites in extension tab + * */ +export async function unblockResourcesLoading(getUrls: ResourcesUrlGetter) { + const { id } = await tabs.getScriptTab() + if (!id) + throw new Error("Cannot get id of current tab") + + installUnblockers(id, getUrls) +} \ No newline at end of file diff --git a/src/resourcesLoading/install.ts b/src/resourcesLoading/install.ts new file mode 100644 index 0000000..a05b7fb --- /dev/null +++ b/src/resourcesLoading/install.ts @@ -0,0 +1,23 @@ +import { onResponseListener } from "./onResponse" +import { ResourcesUrlGetter } from "./types" + +export function installUnblockers(tabId: number, getUrls: ResourcesUrlGetter) { + // TODO: check is uninstall can cause uninstall from other tabs + uninstallUnblockers(getUrls) + + const extra = ['blocking', 'responseHeaders'] + if (/Firefox/.test(navigator.userAgent) === false) + extra.push('extraHeaders') + + + chrome.webRequest.onHeadersReceived.addListener(onResponseListener.bind(null, getUrls), { + tabId, + urls: [''] + }, + extra + ) +} + +export function uninstallUnblockers(getUrls: ResourcesUrlGetter) { + chrome.webRequest.onHeadersReceived.removeListener(onResponseListener.bind(null, getUrls)) +} \ No newline at end of file diff --git a/src/resourcesLoading/onResponse.ts b/src/resourcesLoading/onResponse.ts new file mode 100644 index 0000000..0bdac18 --- /dev/null +++ b/src/resourcesLoading/onResponse.ts @@ -0,0 +1,115 @@ +// Based on https://github.com/balvin-perrie/Access-Control-Allow-Origin---Unblock/blob/master/src/background.js + +import { ResourcesUrlGetter } from "./types" + +// will be binded, not use context +export function onResponseListener(getUrls: ResourcesUrlGetter, details: chrome.webRequest.WebResponseHeadersDetails) { + const prefs = { + 'enabled': false, + 'overwrite-origin': true, + 'methods': ['GET', 'HEAD'], + 'remove-x-frame': true, + 'allow-credentials': true, + 'allow-headers-value': '*', + 'allow-origin-value': '*', + 'expose-headers-value': '*', + 'allow-headers': true, + 'unblock-initiator': true + } + + + if (details.type === 'main_frame') + return + + const { initiator, url, responseHeaders = [] } = details + let origin = '' + + const resources = getUrls() + if (!resources.includes(url)) { + // if resource not in need block, then ignor request + return + } + + if (prefs['unblock-initiator']) { + try { + const o = new URL(initiator || url) + origin = o.origin + } + catch (e) { + console.warn('cannot extract origin for initiator', initiator) + } + } + else + origin = '*' + + + if (prefs['overwrite-origin'] === true) { + const o = responseHeaders.find(({ name }) => name.toLowerCase() === 'access-control-allow-origin') + + if (o) + o.value = origin || prefs['allow-origin-value'] + + else { + responseHeaders.push({ + 'name': 'Access-Control-Allow-Origin', + 'value': origin || prefs['allow-origin-value'] + }) + } + } + if (prefs.methods.length > 3) { // GET, POST, HEAD are mandatory + const o = responseHeaders.find(({ name }) => name.toLowerCase() === 'access-control-allow-methods') + if (o) + o.value = prefs.methods.join(', ') + + else { + responseHeaders.push({ + 'name': 'Access-Control-Allow-Methods', + 'value': prefs.methods.join(', ') + }) + } + } + if (prefs['allow-credentials'] === true) { + const o = responseHeaders.find(({ name }) => name.toLowerCase() === 'access-control-allow-credentials') + if (o) + o.value = 'true' + + else { + responseHeaders.push({ + 'name': 'Access-Control-Allow-Credentials', + 'value': 'true' + }) + } + } + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers + if (prefs['allow-headers'] === true) { + const o = responseHeaders.find(({ name }) => name.toLowerCase() === 'access-control-allow-headers') + if (o) + o.value = prefs['allow-headers-value'] + + else { + responseHeaders.push({ + 'name': 'Access-Control-Allow-Headers', + 'value': prefs['allow-headers-value'] + }) + } + } + if (prefs['allow-headers'] === true) { + const o = responseHeaders.find(({ name }) => name.toLowerCase() === 'access-control-expose-headers') + if (o) + o.value = prefs['expose-headers-value'] + + else { + responseHeaders.push({ + 'name': 'Access-Control-Expose-Headers', + 'value': prefs['expose-headers-value'] + }) + } + } + if (prefs['remove-x-frame'] === true) { + const i = responseHeaders.findIndex(({ name }) => name.toLowerCase() === 'x-frame-options') + if (i !== -1) + responseHeaders.splice(i, 1) + + } + return { responseHeaders } +}; \ No newline at end of file diff --git a/src/resourcesLoading/types.ts b/src/resourcesLoading/types.ts new file mode 100644 index 0000000..8186df0 --- /dev/null +++ b/src/resourcesLoading/types.ts @@ -0,0 +1,3 @@ +export interface ResourcesUrlGetter { + (): Array; +} diff --git a/webpack.config.js b/webpack.config.js index 17f0d87..13161cb 100755 --- a/webpack.config.js +++ b/webpack.config.js @@ -105,7 +105,7 @@ var options = { new webpack.ProgressPlugin(), // clean the dist folder new CleanWebpackPlugin({ - verbose: true, + verbose: false, cleanStaleWebpackAssets: true, }), // expose and write the allowed env vars on the compiled bundle @@ -168,9 +168,50 @@ var options = { }, } -if (env.NODE_ENV === 'development') +if (env.NODE_ENV === 'development') { options.devtool = 'eval-cheap-module-source-map' -else { + + options.stats = { + // fallback value + all: false, + // Disable showing asset information + assets: false, + // Disable showing cached modules and cached assets + cached: false, + cachedAssets: false, + // Disable showing children + children: false, + // Add build date and time information + builtAt: true, + // Add minimal chunk information + chunks: true, + chunkGroups: false, + chunkModules: false, + chunkOrigins: false, + // Disable displaying the entry points with the corresponding bundles + entrypoints: false, + // Enable colored output + colors: true, + // Display errors + errors: true, + logging: 'error', + // Add details to errors (like resolving log) + errorDetails: true, + hash: false, + // Built modules information + modules: false, + // Disable showing performance hint when file size exceeds `performance.maxAssetSize` + performance: false, + // Add timing information + timings: true, + // Add webpack version information + version: true, + // Add warnings + warnings: true, + // Why modules included + reasons: false + } +} else { options.optimization = { minimize: true, minimizer: [ diff --git a/yarn.lock b/yarn.lock index 45d8fe0..0e118ea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2059,6 +2059,13 @@ dependencies: "@types/react" "*" +"@types/react-helmet@^6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@types/react-helmet/-/react-helmet-6.1.0.tgz#af586ed685f4905e2adc7462d1d65ace52beee7a" + integrity sha512-PYRoU1XJFOzQ3BHvWL1T8iDNbRjdMDJMT5hFmZKGbsq09kbSqJy61uwEpTrbTNWDopVphUT34zUSVLK9pjsgYQ== + dependencies: + "@types/react" "*" + "@types/react-redux@^7.1.14": version "7.1.15" resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.15.tgz#19075884db94101be762accef924d266a603fb1b" @@ -10261,6 +10268,21 @@ react-dom@^17.0.1: object-assign "^4.1.1" scheduler "^0.20.1" +react-fast-compare@^3.1.1: + version "3.2.0" + resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb" + integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA== + +react-helmet@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/react-helmet/-/react-helmet-6.1.0.tgz#a750d5165cb13cf213e44747502652e794468726" + integrity sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw== + dependencies: + object-assign "^4.1.1" + prop-types "^15.7.2" + react-fast-compare "^3.1.1" + react-side-effect "^2.1.0" + react-hot-loader@^4.13.0: version "4.13.0" resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.13.0.tgz#c27e9408581c2a678f5316e69c061b226dc6a202" @@ -10330,6 +10352,11 @@ react-router@5.2.0: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" +react-side-effect@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/react-side-effect/-/react-side-effect-2.1.1.tgz#66c5701c3e7560ab4822a4ee2742dee215d72eb3" + integrity sha512-2FoTQzRNTncBVtnzxFOk2mCpcfxQpenBMbk5kSVBg5UcPqV9fRbgY2zhb7GTWWOlpFmAxhClBDlIq8Rsubz1yQ== + react-transition-group@^4.4.0: version "4.4.1" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9"