diff --git a/electron.js b/electron.js index e1d7d344c9..38197281c2 100644 --- a/electron.js +++ b/electron.js @@ -41,7 +41,7 @@ let csp = [ "style-src 'unsafe-inline' http://localhost:* file://*", "font-src data: file://*", "connect-src http://localhost:* http://127.0.0.1:* ws://localhost:* https://sentry.anytype.io https://anytype.io https://api.amplitude.com/ devtools://devtools data:", - "script-src-elem file://* http://localhost:* https://sentry.io devtools://devtools 'unsafe-inline'", + "script-src-elem file: http://localhost:* https://sentry.io devtools://devtools 'unsafe-inline'", "frame-src chrome-extension://react-developer-tools" ]; let autoUpdate = false; @@ -501,6 +501,12 @@ function menuInit () { setConfig({ debugMW: !config.debugMW }); } }, + { + label: 'Threads', type: 'checkbox', checked: config.debugTH, + click: () => { + setConfig({ debugTH: !config.debugTH }); + } + }, { label: 'Analytics', type: 'checkbox', checked: config.debugAN, click: () => { diff --git a/electron/entitlements.mac.plist b/electron/entitlements.mac.plist index d6b93bc0b2..ad77a2a1ea 100644 --- a/electron/entitlements.mac.plist +++ b/electron/entitlements.mac.plist @@ -2,7 +2,13 @@ + com.apple.security.cs.allow-jit + com.apple.security.cs.allow-unsigned-executable-memory + com.apple.security.cs.allow-dyld-environment-variables + + com.apple.security.cs.disable-library-validation + - + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 70e9359112..842ee2994b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "anytype2", - "version": "0.16.8", + "version": "0.16.13-alpha", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "anytype2", - "version": "0.16.8", + "version": "0.16.13-alpha", "license": "ISC", "dependencies": { "@sentry/browser": "^5.27.6", diff --git a/package.deps.json b/package.deps.json index e03f0b1d26..24e0c16776 100644 --- a/package.deps.json +++ b/package.deps.json @@ -13,5 +13,6 @@ "dist/img/**/*", "dist/anytypeHelper.exe", "dist/anytypeHelper", + "dist/*.node", "!node_modules/**/*" ] diff --git a/package.json b/package.json index 7982353ed9..f68bc290f1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "anytype2", - "version": "0.16.8", + "version": "0.16.13-alpha", "description": "Anytype", "main": "electron.js", "scripts": { @@ -175,7 +175,9 @@ "dist/anytypeHelper", "dist/anytypeHelper.exe", "electron/icon*", - "build" + "build", + "dist/*.node", + "node_modules/keytar/build/Release/keytar.node" ], "extraResources": [], "files": [ @@ -193,6 +195,7 @@ "dist/img/**/*", "dist/anytypeHelper.exe", "dist/anytypeHelper", + "dist/*.node", "!node_modules/**/*", "node_modules/about-window", "node_modules/async", @@ -234,6 +237,7 @@ "node_modules/lockfile", "node_modules/lodash", "node_modules/lodash.isequal", + "node_modules/lru-cache", "node_modules/mime-db", "node_modules/minimatch", "node_modules/mkdirp", @@ -259,7 +263,8 @@ "node_modules/unused-filename", "node_modules/with-open-file", "node_modules/wrappy", - "node_modules/write-file-atomic" + "node_modules/write-file-atomic", + "node_modules/yallist" ], "dmg": { "sign": false diff --git a/src/img/icon/cafe.svg b/src/img/icon/cafe.svg new file mode 100644 index 0000000000..44d547cc7c --- /dev/null +++ b/src/img/icon/cafe.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/json/text.json b/src/json/text.json index ea3058b3ab..841d05d91b 100644 --- a/src/json/text.json +++ b/src/json/text.json @@ -52,8 +52,8 @@ "authInviteLogin": { "en": "Enter" }, "authInviteEmpty": { "en": "Invite code is empty" }, - "authSelectTitle": { - "en": "Organize everything", + "authSelectTitle": { + "en": "Organize everything", "ru": "Организуй все" }, "authSelectLabel": { "en": "With Anytype you can write notes and documents, manage tasks, share files and save important content from the web." }, @@ -294,6 +294,18 @@ "filterConditionEmpty": { "en": "is empty" }, "filterConditionNotEmpty": { "en": "is not empty" }, - "inputWithFileTextUrl": { "en": "Paste a link" } + "inputWithFileTextUrl": { "en": "Paste a link" }, -} \ No newline at end of file + "syncStatus0": { "en": "Preparing..." }, + "syncStatus1": { "en": "No connection" }, + "syncStatus2": { "en": "Syncing..." }, + "syncStatus3": { "en": "Synced" }, + "syncStatus4": { "en": "Not syncing" }, + + "tooltip0": { "en": "Initializing sync" }, + "tooltip1": { "en": "Can’t connect to Anytype node" }, + "tooltip2": { "en": "Downloading or uploading data to some node" }, + "tooltip3": { "en": "Backed up on one node at least" }, + "tooltip4": { "en": "Failed to sync, trying again..." } + +} diff --git a/src/scss/_vars.scss b/src/scss/_vars.scss index 8ce51c6951..0cffc67a4b 100644 --- a/src/scss/_vars.scss +++ b/src/scss/_vars.scss @@ -10,6 +10,7 @@ $colorBlue: #3e58eb; $colorIce: #2aa7ee; $colorTeal: #0fc8ba; $colorLime: #5dd400; +$colorGreen: #57c600; $transitionFast: all 0.2s ease-in-out; diff --git a/src/scss/component/header.scss b/src/scss/component/header.scss index ea75c2711c..9153336f5d 100644 --- a/src/scss/component/header.scss +++ b/src/scss/component/header.scss @@ -55,6 +55,7 @@ .side.right { right: 6px; z-index: 1; } .side.right { + .sync { margin-right: 8px; cursor: pointer; } .icon { margin: 0px; } } } @@ -99,6 +100,7 @@ } } } + } .popup { diff --git a/src/scss/component/icon.scss b/src/scss/component/icon.scss index dca27575d2..655de23f06 100644 --- a/src/scss/component/icon.scss +++ b/src/scss/component/icon.scss @@ -55,7 +55,7 @@ width: 64px; height: 64px; border-radius: 100%; font-weight: 500; position: relative; overflow: hidden; text-transform: uppercase; font-size: 22px; color: #fff; text-align: center; line-height: 62px; background-color: $colorIce; } -.icon.user.c18 { width: 18px; height: 18px; line-height: 18px; font-size: 11px; } +.icon.user.c18 { width: 18px !important; height: 18px !important; line-height: 18px; font-size: 12px; } .icon.user.c40 { width: 40px; height: 40px; line-height: 40px; font-size: 18px; } .icon.user.c96 { width: 96px; height: 96px; line-height: 96px; font-size: 44px; font-weight: 500; } diff --git a/src/scss/component/sync.scss b/src/scss/component/sync.scss new file mode 100644 index 0000000000..7ed1779b35 --- /dev/null +++ b/src/scss/component/sync.scss @@ -0,0 +1,15 @@ +@import "~scss/_vars"; + +.sync { + background: #f3f2ec; border-radius: 4px; @include text-small; padding: 0px 6px; display: inline-block; vertical-align: middle; + color: $colorDarkGrey; white-space: nowrap; +} +.sync { + .bullet { + width: 8px; height: 8px; border-radius: 100%; display: inline-block; vertical-align: middle; margin: -2px 4px 0px 0px; + background: $colorLightGrey; + } + .bullet.green { background: $colorGreen; } + .bullet.orange { background: $colorOrange; } + .bullet.red { background: $colorRed; } +} \ No newline at end of file diff --git a/src/scss/menu/help.scss b/src/scss/menu/help.scss index 8a1f557161..139f6f9c27 100644 --- a/src/scss/menu/help.scss +++ b/src/scss/menu/help.scss @@ -1,7 +1,7 @@ @import "~scss/_vars"; .menus { - .menu.menuHelp { width: 224px; } + .menu.menuHelp { width: 224px; position: fixed; top: auto !important; bottom: 38px !important; } .menu.menuHelp { .item:hover { background: #f3f2ec; } diff --git a/src/scss/menu/thread.scss b/src/scss/menu/thread.scss new file mode 100644 index 0000000000..199f5b65e7 --- /dev/null +++ b/src/scss/menu/thread.scss @@ -0,0 +1,41 @@ +@import "~scss/_vars"; + +.menus { + .menu.menuThreadList, + .menu.menuThreadStatus { width: 320px; position: fixed; } + + .menu.menuThreadList, + .menu.menuThreadStatus { + .description { color: $colorDarkGrey; display: flex; } + .item { line-height: 22px; } + .side { width: 50%; } + .side.right { text-align: right; white-space: nowrap; } + + .icon { margin-right: 9px; } + + .red { color: $colorRed; } + .orange { color: $colorOrange; } + .green { color: $colorGreen; } + } + + .menu.menuThreadStatus { + .content { max-height: 250px; } + .item { padding: 5px 16px; } + } + + .menu.menuThreadList { width: 272px; top: 42px !important; } + .menu.menuThreadList { + .content { padding: 0px; } + .item { padding: 11px 16px; border-bottom: 1px solid #eae9e0;} + .item:hover::before { opacity: 0.15; } + .item:last-child { border: 0px; } + + .item { + .icon { position: absolute; left: 16px; top: 12px; } + .icon.user { top: 13px; } + .icon.cafe { left: 15px; width: 20px; height: 20px; background-size: 18px 16px; background-image: url('~img/icon/cafe.svg'); } + .info { padding-left: 28px; } + } + } + +} diff --git a/src/ts/app.tsx b/src/ts/app.tsx index 85cd5e4095..3d63e8192a 100644 --- a/src/ts/app.tsx +++ b/src/ts/app.tsx @@ -38,6 +38,7 @@ import 'scss/component/linkPreview.scss'; import 'scss/component/drag.scss'; import 'scss/component/pager.scss'; import 'scss/component/pin.scss'; +import 'scss/component/sync.scss'; import 'scss/page/auth.scss'; import 'scss/page/main/index.scss'; @@ -84,6 +85,7 @@ import 'scss/menu/smile.scss'; import 'scss/menu/help.scss'; import 'scss/menu/select.scss'; import 'scss/menu/search.scss'; +import 'scss/menu/thread.scss'; import 'scss/menu/block/context.scss'; import 'scss/menu/block/common.scss'; @@ -310,14 +312,10 @@ class App extends React.Component { ipcRenderer.on('keytarGet', (e: any, key: string, value: string) => { if ((key == 'phrase') && accountId) { - if (!value) { - if (phrase) { - value = phrase; - ipcRenderer.send('keytarSet', 'phrase', phrase); - Storage.delete('phrase'); - } else { - return; - }; + if (phrase) { + value = phrase; + ipcRenderer.send('keytarSet', 'phrase', phrase); + Storage.delete('phrase'); }; authStore.phraseSet(value); diff --git a/src/ts/component/editor/page.tsx b/src/ts/component/editor/page.tsx index c37dd12f84..97e5984fe9 100644 --- a/src/ts/component/editor/page.tsx +++ b/src/ts/component/editor/page.tsx @@ -333,6 +333,7 @@ class EditorPage extends React.Component { C.BlockClose(id, (message: any) => { blockStore.blocksClear(id); dbStore.relationsRemove(id); + authStore.threadRemove(id); }); }; diff --git a/src/ts/component/header/main/edit.tsx b/src/ts/component/header/main/edit.tsx index fbe463b9eb..3e37e3c78a 100644 --- a/src/ts/component/header/main/edit.tsx +++ b/src/ts/component/header/main/edit.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { RouteComponentProps } from 'react-router'; -import { Icon, Smile } from 'ts/component'; +import { Icon, Smile, Sync } from 'ts/component'; import { I, Util, SmileUtil, DataUtil, crumbs, focus } from 'ts/lib'; import { commonStore, blockStore } from 'ts/store'; import { observer } from 'mobx-react'; @@ -26,6 +26,7 @@ class HeaderMainEdit extends React.Component { this.onNavigation = this.onNavigation.bind(this); this.onAdd = this.onAdd.bind(this); this.onRelation = this.onRelation.bind(this); + this.onSync = this.onSync.bind(this); this.onPathOver = this.onPathOver.bind(this); this.onPathOut = this.onPathOut.bind(this); @@ -69,8 +70,9 @@ class HeaderMainEdit extends React.Component {
+ - +
); @@ -140,6 +142,22 @@ class HeaderMainEdit extends React.Component { }); }; + onSync (e: any) { + const { rootId } = this.props; + + commonStore.menuOpen('threadList', { + type: I.MenuType.Vertical, + element: '#button-sync', + offsetX: 0, + offsetY: 0, + vertical: I.MenuDirection.Bottom, + horizontal: I.MenuDirection.Right, + data: { + rootId: rootId, + } + }); + }; + onNavigation (e: any, expanded: boolean) { e.preventDefault(); e.stopPropagation(); diff --git a/src/ts/component/index.tsx b/src/ts/component/index.tsx index 24aa9ad208..134af62b05 100644 --- a/src/ts/component/index.tsx +++ b/src/ts/component/index.tsx @@ -51,6 +51,7 @@ import Tooltip from './util/tooltip'; import Drag from './util/drag'; import Marker from './util/marker'; import Pin from './util/pin'; +import Sync from './util/sync'; import Icon from './util/icon'; import IconUser from './util/iconUser'; @@ -87,6 +88,7 @@ export { Input, InputWithFile, Pin, + Sync, Checkbox, Textarea, Button, diff --git a/src/ts/component/menu/index.tsx b/src/ts/component/menu/index.tsx index 43b9e1232f..8a592bf467 100644 --- a/src/ts/component/menu/index.tsx +++ b/src/ts/component/menu/index.tsx @@ -10,6 +10,9 @@ import MenuSmile from './smile'; import MenuSmileSkin from './smile/skin'; import MenuSearch from './search'; +import MenuThreadList from './thread/list'; +import MenuThreadStatus from './thread/status'; + import MenuBlockContext from './block/context'; import MenuBlockStyle from './block/style'; import MenuBlockAdd from './block/add'; @@ -73,6 +76,9 @@ class Menu extends React.Component { smile: MenuSmile, smileSkin: MenuSmileSkin, search: MenuSearch, + + threadList: MenuThreadList, + threadStatus: MenuThreadStatus, blockContext: MenuBlockContext, blockAction: MenuBlockAction, diff --git a/src/ts/component/menu/thread/list.tsx b/src/ts/component/menu/thread/list.tsx new file mode 100644 index 0000000000..8b94cfeda4 --- /dev/null +++ b/src/ts/component/menu/thread/list.tsx @@ -0,0 +1,152 @@ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import { Icon, IconUser } from 'ts/component'; +import { authStore, commonStore } from 'ts/store'; +import { observer } from 'mobx-react'; +import { I, DataUtil, translate, Util } from 'ts/lib'; + +interface Props extends I.Menu {}; + +const Constant = require('json/constant.json'); +const $ = require('jquery'); + +@observer +class MenuThreadList extends React.Component { + + timeoutMenu: number = 0; + timeoutClose: number = 0; + + constructor (props: any) { + super(props); + + this.onMouseEnter = this.onMouseEnter.bind(this); + }; + + render () { + const { param } = this.props; + const { data } = param; + const { rootId } = data; + const thread = authStore.threadGet(rootId); + const { accounts, cafe } = thread; + const { status } = cafe; + + const Item = (item: any) => ( +
{ this.onMouseEnter(item.id, false); }} + > + +
+
{item.name}
+
+
Last sync
+
+ {Util.timeAgo(Math.max(item.lastPulled, item.lastEdited))} +
+
+
+
+ ); + + return ( +
+
{ this.onMouseEnter('cafe', true); }} + > + +
+
Backup node
+
+ {translate('syncStatus' + status)} +
+
+
+ {accounts.map((item: I.ThreadAccount, i: number) => ( + + ))} +
+ ); + }; + + componentDidMount () { + const obj = $('#menuThreadList'); + const clear = () => { + window.clearTimeout(this.timeoutClose); + window.clearTimeout(this.timeoutMenu); + }; + const leave = () => { + clear(); + this.timeoutClose = window.setTimeout(() => { + clear(); + commonStore.menuClose(this.props.id); + commonStore.menuClose('threadStatus'); + }, 1000); + }; + + obj.unbind('mouseenter').on('mouseenter', () => { + clear(); + }); + obj.unbind('mouseleave').on('mouseleave', () => { + const status = $('#menuThreadStatus'); + if (status.length) { + status.unbind('mouseenter').on('mouseenter', () => { + clear(); + }); + status.unbind('mouseleave').on('mouseleave', leave); + }; + leave(); + }); + }; + + componentWillUnmount () { + window.clearTimeout(this.timeoutClose); + window.clearTimeout(this.timeoutMenu); + }; + + onMouseEnter (id: string, isCafe: boolean) { + if (!id) { + return; + }; + + const { param } = this.props; + const { data } = param; + const { menus } = commonStore; + const menu = menus.find((it: I.Menu) => { return it.id == 'threadStatus'; }); + + if (menu && (menu.param.data.accountId == id)) { + return; + }; + + const node = $(ReactDOM.findDOMNode(this)); + const item = node.find('#item-' + id); + if (!item.length) { + return; + }; + + const top = item.offset().top - $(window).scrollTop(); + + window.clearTimeout(this.timeoutMenu); + this.timeoutMenu = window.setTimeout(() => { + commonStore.menuOpen('threadStatus', { + element: '#item-' + id, + type: I.MenuType.Vertical, + vertical: I.MenuDirection.Bottom, + horizontal: I.MenuDirection.Right, + offsetX: 272, + offsetY: 0, + fixedY: top, + data: { + ...data, + accountId: id, + isCafe: isCafe, + }, + }); + }, Constant.delay.menu); + }; + +}; + +export default MenuThreadList; \ No newline at end of file diff --git a/src/ts/component/menu/thread/status.tsx b/src/ts/component/menu/thread/status.tsx new file mode 100644 index 0000000000..9a946f355b --- /dev/null +++ b/src/ts/component/menu/thread/status.tsx @@ -0,0 +1,101 @@ +import * as React from 'react'; +import { authStore } from 'ts/store'; +import { I, Util } from 'ts/lib'; + +interface Props extends I.Menu {}; + +class MenuThreadStatus extends React.Component { + + render () { + const { param } = this.props; + const { data } = param; + const { rootId, isCafe, accountId } = data; + const thread = authStore.threadGet(rootId); + const { cafe, accounts } = thread; + const { status, files } = cafe; + const account = accounts.find((it: I.ThreadAccount) => { return it.id == accountId; }); + + const Item = (item: any) => ( +
+
{item.name}
+ {item.fields.map((field: any, i: number) => { + if (!field.key) { + return null; + }; + + let content = null; + if (field.value.toString()) { + content = ( + +
{field.key}
+
{field.value}
+
+ ); + } else { + content = field.key; + }; + + return ( +
+ {content} +
+ ); + })} +
+ ); + + let cafeStatus = []; + if (cafe.lastPushSucceed) { + cafeStatus = [ + { key: 'This object is backed up', value: '' }, + { key: 'Updates requested', value: cafe.lastPulled ? Util.timeAgo(cafe.lastPulled) : 'No interaction' } + ]; + } else { + cafeStatus = [ + { key: 'Some changes are not backed up', value: '' }, + { key: 'Updates requested', value: cafe.lastPulled ? Util.timeAgo(cafe.lastPulled) : 'No interaction' } + ]; + }; + + const fileStatus = [ + { key: 'Uploading', value: files.pinning + files.failed }, + { key: 'Waiting for upload', value: files.failed }, + { key: 'Stored', value: files.pinned }, + ]; + + return isCafe ? ( +
+ + +
+ ) : ( + +
+
My devices direct interaction
+
+ {account.devices.map((item: any, i: number) => { + const fields = [ + // { + // key: 'Direct interation status:', + // value: (item.online ? 'Online' : 'Offline'), + // }, + { + key: 'Updates requested', + value: (item.lastPulled ? Util.timeAgo(item.lastPulled) : 'No interaction'), + }, + { + key: 'Last edits received', + value: (item.lastEdited ? Util.timeAgo(item.lastEdited) : 'No changes'), + }, + ]; + return ; + })} +
+
+
+ ); + }; + +}; + +export default MenuThreadStatus; diff --git a/src/ts/component/page/auth/account/select.tsx b/src/ts/component/page/auth/account/select.tsx index a6188d9a34..5ca27581a5 100644 --- a/src/ts/component/page/auth/account/select.tsx +++ b/src/ts/component/page/auth/account/select.tsx @@ -1,9 +1,9 @@ import * as React from 'react'; import { RouteComponentProps } from 'react-router'; -import { Frame, Icon, Cover, Error, Title, IconUser, HeaderAuth as Header, FooterAuth as Footer } from 'ts/component'; +import { Frame, Icon, Cover, Error, Title, IconUser, HeaderAuth as Header, FooterAuth as Footer, Loader } from 'ts/component'; import { commonStore, authStore } from 'ts/store'; import { observer } from 'mobx-react'; -import { Storage, I, C, Util, translate } from 'ts/lib'; +import { I, C, Util, translate } from 'ts/lib'; interface Props extends RouteComponentProps {}; @@ -29,7 +29,7 @@ class PageAccountSelect extends React.Component { const { cover } = commonStore; const { error } = this.state; const { accounts } = authStore; - + const Item = (item: any) => (
{ this.onSelect(item as I.Account); }}> @@ -44,18 +44,22 @@ class PageAccountSelect extends React.Component {