diff --git a/package.json b/package.json index de81f894..90a22ff0 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "scripts": { "build": "preact build --no-prerender", "serve": "sirv build --port 8080 --cors --single", - "dev": "preact watch", + "dev": "echo 'dev server will start at http://localhost:8080';preact watch", "lint": "eslint src", "test": "jest" }, diff --git a/src/js/PeerManager.js b/src/js/PeerManager.js index ae7e172d..8307b619 100644 --- a/src/js/PeerManager.js +++ b/src/js/PeerManager.js @@ -9,7 +9,7 @@ const ELECTRON_GUN_URL = 'http://localhost:8767/gun'; let maxConnectedPeers = Helpers.isElectron ? 2 : 1; const DEFAULT_PEERS = {}; -if (window.location.hostname.endsWith('herokuapp.com') || window.location.host === 'localhost:5000') { +if (window.location.hostname.endsWith('herokuapp.com') || window.location.host === 'localhost:4944') { DEFAULT_PEERS[window.location.origin + '/gun'] = {}; } else { DEFAULT_PEERS['https://gun-rs.iris.to/gun'] = {}; diff --git a/src/js/components/ExplorerNode.js b/src/js/components/ExplorerNode.js new file mode 100644 index 00000000..1bf7cca7 --- /dev/null +++ b/src/js/components/ExplorerNode.js @@ -0,0 +1,251 @@ +import BaseComponent from "../BaseComponent"; +import Session from "../Session"; +import Gun from "gun"; +import {html} from "htm/preact"; + +const hashRegex = /^(?:[A-Za-z0-9+/]{4}){10}(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)+$/; +const pubKeyRegex = /^[A-Za-z0-9\-\_]{40,50}\.[A-Za-z0-9\_\-]{40,50}$/; +const SHOW_CHILDREN_COUNT = 50; + +const chevronDown = html` + + + +`; + +const chevronRight = html` + + + +`; + +class ExplorerNode extends BaseComponent { + constructor() { + super(); + this.children = {}; + this.state = {children: {}, shownChildrenCount: SHOW_CHILDREN_COUNT}; + } + + getNode() { + if (this.props.path.length > 1) { + const path = this.props.path.split('/'); + return path.slice(1).reduce((sum, current) => (current && sum.get(decodeURIComponent(current))) || sum, this.props.gun); + } + return this.props.gun; + } + + shouldComponentUpdate() { + return true; + } + + componentDidMount() { + this.isMine = this.props.path.indexOf(`public/~${ Session.getPubKey()}`) === 0; + this.isGroup = this.props.path.indexOf('group') === 0; + + this.children = {}; + if (this.props.children && typeof this.props.children === "object") { + this.children = Object.assign(this.children, this.props.children); + } + this.setState({children: {}, shownChildrenCount: SHOW_CHILDREN_COUNT}); + + const cb = this.sub( + async (v, k, c, e, from) => { + if (k === '_') { return; } + let encryption; + if (typeof v === 'string' && v.indexOf('SEA{') === 0) { + try { + const myKey = Session.getKey(); + let dec = await Gun.SEA.decrypt(v, myKey); + if (dec === undefined) { + if (!this.mySecret) { + this.mySecret = await Gun.SEA.secret(myKey.epub, myKey); + dec = await Gun.SEA.decrypt(v, this.mySecret); + } + } + if (dec !== undefined) { + v = dec; + encryption = 'Decrypted'; + } else { + encryption = 'Encrypted'; + } + } catch(e) { + null; + } + } + const prev = this.children[k] || {}; + this.children[k] = Object.assign(prev, { value: v, encryption, from }); + this.setState({children: this.children}); + } + ); + + if (this.isGroup) { + const path = this.props.path.split('/').slice(2).join('/'); + this.props.gun.map(path, cb); // TODO: make State.group() provide the normal gun api + } else { + this.getNode().map().on(cb); + } + } + + onChildObjectClick(e, k) { + e.preventDefault(); + this.children[k].open = !this.children[k].open; + this.setState({children: this.children}); + } + + onShowMoreClick(e, k) { + e.preventDefault(); + this.children[k].showMore = !this.children[k].showMore; + this.setState({children: this.children}); + } + + renderChildObject(k) { + const path = `${this.props.path }/${ encodeURIComponent(k)}`; + return html` +
+ this.onChildObjectClick(e, k)}>${this.state.children[k].open ? chevronDown : chevronRight} + + + ${typeof k === 'string' && k.substr(1).match(pubKeyRegex) ? html`` : k} + + +
+ ${this.state.children[k].open ? html`<${ExplorerNode} gun=${this.props.gun} indent=${this.props.indent + 1} key=${path} path=${path} isGroup=${this.props.isGroup}/>` : ''} + `; + } + + renderChildValue(k, v) { + let s; + const encryption = this.children[k].encryption; + const from = this.children[k].from; + const decrypted = encryption === 'Decrypted'; + const lnk = (href, text, cls) => html`${text}`; + const keyLinks = html` + ${typeof k === 'string' && k.match(hashRegex) ? lnk(`/post/${encodeURIComponent(k)}`, '#') : ''} + ${typeof k === 'string' && k.match(pubKeyRegex) ? lnk(`/explorer/public%2F~${encodeURIComponent(encodeURIComponent(k))}`, html``) : ''} + `; + if (encryption) { + if (!decrypted) { + s = html`Encrypted value`; + } else { + s = JSON.stringify(v); + } + } else { + const pub = Session.getPubKey(); + const isMine = this.props.path.indexOf(`public/~${ pub}`) === 0; + const path = isMine && (`${this.props.path }/${ encodeURIComponent(k)}`).replace(`public/~${ pub }/`, ''); + if (typeof v === 'string' && v.indexOf('data:image') === 0) { + s = isMine ? html`` : html``; + } else { + let stringified = JSON.stringify(v); + let showToggle; + if (stringified.length > 100) { + showToggle = true; + if (!this.state.children[k].showMore) { + stringified = stringified.slice(0, 100); + } + } + + const valueLinks = html` + ${typeof v === 'string' && v.match(hashRegex) ? lnk(`/post/${encodeURIComponent(v)}`, '#') : ''} + ${typeof v === 'string' && v.match(pubKeyRegex) ? lnk(`/explorer/public%2F~${encodeURIComponent(encodeURIComponent(v))}`, html``) : ''} + ${typeof from === 'string' ? html` from ${lnk(`/explorer/public%2F~${encodeURIComponent(encodeURIComponent(from))}`, html``, '')}` : ''} + `; + + s = isMine ? html` + + ${valueLinks} + ` : + html` + + ${stringified} + ${showToggle ? html` + this.onShowMoreClick(e, k)} href="">${this.state.children[k].showMore ? 'less' : 'more'} + ` : ''} + ${valueLinks} + + `; + } + } + return html` +
+ ${k} ${keyLinks}: + ${encryption ? html` + ${encryption} value + ${decrypted ? '🔓' : ''} + + ` : ''} ${s} +
+ `; + } + + onExpandClicked() { + const expandAll = !this.state.expandAll; + Object.keys(this.children).forEach(k => { + this.children[k].open = expandAll; + }); + this.setState({expandAll, children: this.children}); + } + + onNewItemSubmit(e) { + e.preventDefault(); + if (this.state.newItemName) { + this.getNode().get(this.state.newItemName.trim()).put(this.state.showNewItem === 'object' ? {a:null} : ''); + this.setState({showNewItem: false, newItemName: ''}); + } + } + + onNewItemNameInput(e) { + this.setState({newItemName: e.target.value.trimStart().replace(' ', ' ')}); + } + + showNewItemClicked(type) { + this.setState({showNewItem:type}); + setTimeout(() => document.querySelector('#newItemNameInput').focus(), 0); + } + + render() { + const children = Object.keys(this.state.children).sort(); + + const renderChildren = children => { + return children.map(k => { + const v = this.state.children[k].value; + if (typeof v === 'object' && v && v['_']) { + return this.renderChildObject(k, v); + } + return this.renderChildValue(k, v); + + }); + } + + const showMoreBtn = children.length > this.state.shownChildrenCount; + return html` + ${this.props.indent === 0 ? html` +
+ ${this.props.showTools ? html` +

+ this.onExpandClicked()}>${this.state.expandAll ? 'Close all' : 'Expand all'} + this.showNewItemClicked('object')}>New object + this.showNewItemClicked('value')}>New value + ${children.length} items +

+ `: ''} + ${this.state.showNewItem ? html` +

+

this.onNewItemSubmit(e)}> + this.onNewItemNameInput(e)} value=${this.state.newItemName} placeholder="New ${this.state.showNewItem} name"/> + + +
+

+ ` : ''} +
+ `: ''} + ${renderChildren(children.slice(0, this.state.shownChildrenCount))} + ${showMoreBtn ? html` + {e.preventDefault();this.setState({shownChildrenCount: this.state.shownChildrenCount + SHOW_CHILDREN_COUNT})}}>More (${children.length - this.state.shownChildrenCount}) + ` : ''} + `; + } +} + +export default ExplorerNode; \ No newline at end of file diff --git a/src/js/views/Explorer.js b/src/js/views/Explorer.js index 67c35ef4..f1089644 100644 --- a/src/js/views/Explorer.js +++ b/src/js/views/Explorer.js @@ -1,14 +1,7 @@ import { html } from 'htm/preact'; import State from '../State.js'; -import Session from '../Session.js'; import View from './View.js'; -import Gun from 'gun'; -import BaseComponent from "../BaseComponent"; - -const hashRegex = /^(?:[A-Za-z0-9+/]{4}){10}(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)+$/; -const pubKeyRegex = /^[A-Za-z0-9\-\_]{40,50}\.[A-Za-z0-9\_\-]{40,50}$/; - -const SHOW_CHILDREN_COUNT = 50; +import ExplorerNode from '../components/ExplorerNode'; const chevronDown = html` @@ -67,16 +60,7 @@ class Explorer extends View {
${chevronDown} Public (synced with peers)
-
- ${chevronDown} Users -
-
- ${chevronDown} ${Session.getPubKey()} -
- <${ExplorerNode} indent=${3} gun=${State.public} key='public/~${Session.getPubKey()}' path='public/~${Session.getPubKey()}'/> -
- ${chevronRight} # (content-addressed values, such as public posts) -
+ <${ExplorerNode} indent=${1} gun=${State.public} key='public' path='public' children=${{'#':{value:{_:1}}}}/>

${chevronDown} Local (only stored on your device) @@ -96,225 +80,4 @@ class Explorer extends View { } } -class ExplorerNode extends BaseComponent { - constructor() { - super(); - this.children = {}; - this.state = {children: {}, shownChildrenCount: SHOW_CHILDREN_COUNT}; - } - - getNode() { - if (this.props.path.length > 1) { - const path = this.props.path.split('/'); - return path.slice(1).reduce((sum, current) => (current && sum.get(decodeURIComponent(current))) || sum, this.props.gun); - } - return this.props.gun; - } - - shouldComponentUpdate() { - return true; - } - - componentDidMount() { - this.isMine = this.props.path.indexOf(`public/~${ Session.getPubKey()}`) === 0; - this.isGroup = this.props.path.indexOf('group') === 0; - - this.children = {}; - this.setState({children: {}, shownChildrenCount: SHOW_CHILDREN_COUNT}); - - const cb = this.sub( - async (v, k, c, e, from) => { - if (k === '_') { return; } - let encryption; - if (typeof v === 'string' && v.indexOf('SEA{') === 0) { - try { - const myKey = Session.getKey(); - let dec = await Gun.SEA.decrypt(v, myKey); - if (dec === undefined) { - if (!this.mySecret) { - this.mySecret = await Gun.SEA.secret(myKey.epub, myKey); - dec = await Gun.SEA.decrypt(v, this.mySecret); - } - } - if (dec !== undefined) { - v = dec; - encryption = 'Decrypted'; - } else { - encryption = 'Encrypted'; - } - } catch(e) { - null; - } - } - const prev = this.children[k] || {}; - this.children[k] = Object.assign(prev, { value: v, encryption, from }); - this.setState({children: this.children}); - } - ); - - if (this.isGroup) { - const path = this.props.path.split('/').slice(2).join('/'); - this.props.gun.map(path, cb); // TODO: make State.group() provide the normal gun api - } else { - this.getNode().map().on(cb); - } - } - - onChildObjectClick(e, k) { - e.preventDefault(); - this.children[k].open = !this.children[k].open; - this.setState({children: this.children}); - } - - onShowMoreClick(e, k) { - e.preventDefault(); - this.children[k].showMore = !this.children[k].showMore; - this.setState({children: this.children}); - } - - renderChildObject(k) { - const path = `${this.props.path }/${ encodeURIComponent(k)}`; - return html` -
- this.onChildObjectClick(e, k)}>${this.state.children[k].open ? chevronDown : chevronRight} - ${k} -
- ${this.state.children[k].open ? html`<${ExplorerNode} gun=${this.props.gun} indent=${this.props.indent + 1} key=${path} path=${path} isGroup=${this.props.isGroup}/>` : ''} - `; - } - - renderChildValue(k, v) { - let s; - const encryption = this.children[k].encryption; - const from = this.children[k].from; - const decrypted = encryption === 'Decrypted'; - const lnk = (href, text, cls) => html`${text}`; - const keyLinks = html` - ${typeof k === 'string' && k.match(hashRegex) ? lnk(`/post/${encodeURIComponent(k)}`, '#') : ''} - ${typeof k === 'string' && k.match(pubKeyRegex) ? lnk(`/explorer/public%2F~${encodeURIComponent(encodeURIComponent(k))}`, html``) : ''} - `; - if (encryption) { - if (!decrypted) { - s = html`Encrypted value`; - } else { - s = JSON.stringify(v); - } - } else { - const pub = Session.getPubKey(); - const isMine = this.props.path.indexOf(`public/~${ pub}`) === 0; - const path = isMine && (`${this.props.path }/${ encodeURIComponent(k)}`).replace(`public/~${ pub }/`, ''); - if (typeof v === 'string' && v.indexOf('data:image') === 0) { - s = isMine ? html`` : html``; - } else { - let stringified = JSON.stringify(v); - let showToggle; - if (stringified.length > 100) { - showToggle = true; - if (!this.state.children[k].showMore) { - stringified = stringified.slice(0, 100); - } - } - - const valueLinks = html` - ${typeof v === 'string' && v.match(hashRegex) ? lnk(`/post/${encodeURIComponent(v)}`, '#') : ''} - ${typeof v === 'string' && v.match(pubKeyRegex) ? lnk(`/explorer/public%2F~${encodeURIComponent(encodeURIComponent(v))}`, html``) : ''} - ${typeof from === 'string' ? html` from ${lnk(`/explorer/public%2F~${encodeURIComponent(encodeURIComponent(from))}`, html``, '')}` : ''} - `; - - s = isMine ? html` - - ${valueLinks} - ` : - html` - - ${stringified} - ${showToggle ? html` - this.onShowMoreClick(e, k)} href="">${this.state.children[k].showMore ? 'less' : 'more'} - ` : ''} - ${valueLinks} - - `; - } - } - return html` -
- ${k} ${keyLinks}: - ${encryption ? html` - ${encryption} value - ${decrypted ? '🔓' : ''} - - ` : ''} ${s} -
- `; - } - - onExpandClicked() { - const expandAll = !this.state.expandAll; - Object.keys(this.children).forEach(k => { - this.children[k].open = expandAll; - }); - this.setState({expandAll, children: this.children}); - } - - onNewItemSubmit(e) { - e.preventDefault(); - if (this.state.newItemName) { - this.getNode().get(this.state.newItemName.trim()).put(this.state.showNewItem === 'object' ? {a:null} : ''); - this.setState({showNewItem: false, newItemName: ''}); - } - } - - onNewItemNameInput(e) { - this.setState({newItemName: e.target.value.trimStart().replace(' ', ' ')}); - } - - showNewItemClicked(type) { - this.setState({showNewItem:type}); - setTimeout(() => document.querySelector('#newItemNameInput').focus(), 0); - } - - render() { - const children = Object.keys(this.state.children).sort(); - - const renderChildren = children => { - return children.map(k => { - const v = this.state.children[k].value; - if (typeof v === 'object' && v && v['_']) { - return this.renderChildObject(k, v); - } - return this.renderChildValue(k, v); - - }); - } - - const showMoreBtn = children.length > this.state.shownChildrenCount; - return html` - ${this.props.indent === 0 ? html` -
- ${this.props.showTools ? html` -

- this.onExpandClicked()}>${this.state.expandAll ? 'Close all' : 'Expand all'} - this.showNewItemClicked('object')}>New object - this.showNewItemClicked('value')}>New value -

- `: ''} - ${this.state.showNewItem ? html` -

-

this.onNewItemSubmit(e)}> - this.onNewItemNameInput(e)} value=${this.state.newItemName} placeholder="New ${this.state.showNewItem} name"/> - - -
-

- ` : ''} -
- `: ''} - ${renderChildren(children.slice(0, this.state.shownChildrenCount))} - ${showMoreBtn ? html` - {e.preventDefault();this.setState({shownChildrenCount: this.state.shownChildrenCount + SHOW_CHILDREN_COUNT})}}>More (${children.length - this.state.shownChildrenCount}) - ` : ''} - `; - } -} - export default Explorer;