mirror of
https://github.com/urbit/shrub.git
synced 2024-12-22 10:21:31 +03:00
Merge branch 'master' into pre-release/next-userspace
This commit is contained in:
commit
15c1c2146a
24
.github/workflows/build.yml
vendored
24
.github/workflows/build.yml
vendored
@ -32,7 +32,29 @@
|
|||||||
|
|
||||||
name: build
|
name: build
|
||||||
|
|
||||||
on: [push, pull_request]
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- 'pkg/arvo/**'
|
||||||
|
- 'pkg/docker-image/**'
|
||||||
|
- 'pkg/ent/**'
|
||||||
|
- 'pkg/ge-additions/**'
|
||||||
|
- 'pkg/hs/**'
|
||||||
|
- 'pkg/libaes_siv/**'
|
||||||
|
- 'pkg/urbit/**'
|
||||||
|
- 'bin/**'
|
||||||
|
- 'nix/**'
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'pkg/arvo/**'
|
||||||
|
- 'pkg/docker-image/**'
|
||||||
|
- 'pkg/ent/**'
|
||||||
|
- 'pkg/ge-additions/**'
|
||||||
|
- 'pkg/hs/**'
|
||||||
|
- 'pkg/libaes_siv/**'
|
||||||
|
- 'pkg/urbit/**'
|
||||||
|
- 'bin/**'
|
||||||
|
- 'nix/**'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
urbit:
|
urbit:
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:a8b19cbe89f770f8d6c1e05972be7a3a01545b93b0f2d4523809e7df18635f3c
|
oid sha256:59285407abdc63642ff71384d922f63f4b2c82b3a0daa3673a861c97c59e292f
|
||||||
size 9462938
|
size 9729397
|
||||||
|
@ -45,17 +45,16 @@ Most parts of Arvo have dedicated maintainers.
|
|||||||
* `/sys/vane/ames`: @belisarius222 (~rovnys-ricfer) & @philipcmonk (~wicdev-wisryt)
|
* `/sys/vane/ames`: @belisarius222 (~rovnys-ricfer) & @philipcmonk (~wicdev-wisryt)
|
||||||
* `/sys/vane/behn`: @belisarius222 (~rovnys-ricfer)
|
* `/sys/vane/behn`: @belisarius222 (~rovnys-ricfer)
|
||||||
* `/sys/vane/clay`: @philipcmonk (~wicdev-wisryt) & @belisarius222 (~rovnys-ricfer)
|
* `/sys/vane/clay`: @philipcmonk (~wicdev-wisryt) & @belisarius222 (~rovnys-ricfer)
|
||||||
* `/sys/vane/dill`: @joemfb (~master-morzod)
|
* `/sys/vane/dill`: @fang- (~palfun-foslup)
|
||||||
* `/sys/vane/eyre`: @eglaysher (~littel-ponnys)
|
* `/sys/vane/eyre`: @fang- (~palfun-foslup)
|
||||||
* `/sys/vane/gall`: @philipcmonk (~wicdev-wisryt)
|
* `/sys/vane/gall`: @philipcmonk (~wicdev-wisryt)
|
||||||
* `/sys/vane/jael`: @fang- (~palfun-foslup) & @philipcmonk (~wicdev-wisryt)
|
* `/sys/vane/jael`: @fang- (~palfun-foslup) & @philipcmonk (~wicdev-wisryt)
|
||||||
* `/app/acme`: @joemfb (~master-morzod)
|
* `/app/acme`: @joemfb (~master-morzod)
|
||||||
* `/app/dns`: @joemfb (~master-morzod)
|
* `/app/dns`: @joemfb (~master-morzod)
|
||||||
* `/app/aqua`: @philipcmonk (~wicdev-wisryt)
|
* `/app/aqua`: @philipcmonk (~wicdev-wisryt)
|
||||||
* `/app/hood`: @belisarius222 (~rovnys-ricfer)
|
* `/app/hood`: @belisarius222 (~rovnys-ricfer)
|
||||||
* `/lib/hood/drum`: @philipcmonk (~wicdev-wisryt)
|
* `/lib/hood/drum`: @fang- (~palfun-foslup)
|
||||||
* `/lib/hood/kiln`: @philipcmonk (~wicdev-wisryt)
|
* `/lib/hood/kiln`: @philipcmonk (~wicdev-wisryt)
|
||||||
* `/lib/test`: @eglaysher (~littel-ponnys)
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
/- glob
|
/- glob
|
||||||
/+ default-agent, verb, dbug
|
/+ default-agent, verb, dbug
|
||||||
|%
|
|%
|
||||||
++ hash 0v6.3olcs.d6chc.eidm2.1pft8.6k264
|
++ hash 0v1.4ujsp.698kt.ojftv.7jual.4hhu5
|
||||||
+$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))]
|
+$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))]
|
||||||
+$ all-states
|
+$ all-states
|
||||||
$% state-0
|
$% state-0
|
||||||
|
@ -293,7 +293,7 @@
|
|||||||
~(tap by unreads-count)
|
~(tap by unreads-count)
|
||||||
|= [=stats-index:store count=@ud]
|
|= [=stats-index:store count=@ud]
|
||||||
:* stats-index
|
:* stats-index
|
||||||
~(wyt in (~(gut by by-index) stats-index ~))
|
(~(gut by by-index) stats-index ~)
|
||||||
[%count count]
|
[%count count]
|
||||||
(~(gut by last-seen) stats-index *time)
|
(~(gut by last-seen) stats-index *time)
|
||||||
==
|
==
|
||||||
@ -304,7 +304,7 @@
|
|||||||
~(tap by unreads-each)
|
~(tap by unreads-each)
|
||||||
|= [=stats-index:store indices=(set index:graph-store)]
|
|= [=stats-index:store indices=(set index:graph-store)]
|
||||||
:* stats-index
|
:* stats-index
|
||||||
~(wyt in (~(gut by by-index) stats-index ~))
|
(~(gut by by-index) stats-index ~)
|
||||||
[%each indices]
|
[%each indices]
|
||||||
(~(gut by last-seen) stats-index *time)
|
(~(gut by last-seen) stats-index *time)
|
||||||
==
|
==
|
||||||
@ -317,7 +317,7 @@
|
|||||||
~
|
~
|
||||||
:- ~
|
:- ~
|
||||||
:* stats-index
|
:* stats-index
|
||||||
~(wyt in nots)
|
nots
|
||||||
[%count 0]
|
[%count 0]
|
||||||
*time
|
*time
|
||||||
==
|
==
|
||||||
|
@ -24,6 +24,6 @@
|
|||||||
<div id="portal-root"></div>
|
<div id="portal-root"></div>
|
||||||
<script src="/~landscape/js/channel.js"></script>
|
<script src="/~landscape/js/channel.js"></script>
|
||||||
<script src="/~landscape/js/session.js"></script>
|
<script src="/~landscape/js/session.js"></script>
|
||||||
<script src="/~landscape/js/bundle/index.2825fbc0a1f2fb69e6cf.js"></script>
|
<script src="/~landscape/js/bundle/index.e821c1b85987caabfb1f.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -151,7 +151,7 @@
|
|||||||
^- json
|
^- json
|
||||||
%- pairs
|
%- pairs
|
||||||
:~ unreads+(unread unreads.s)
|
:~ unreads+(unread unreads.s)
|
||||||
notifications+(numb notifications.s)
|
notifications+a+(turn ~(tap in notifications.s) notif-ref)
|
||||||
last+(time last-seen.s)
|
last+(time last-seen.s)
|
||||||
==
|
==
|
||||||
++ added
|
++ added
|
||||||
|
@ -150,7 +150,7 @@
|
|||||||
[index notification]
|
[index notification]
|
||||||
::
|
::
|
||||||
+$ stats
|
+$ stats
|
||||||
[notifications=@ud =unreads last-seen=@da]
|
[notifications=(set [time index]) =unreads last-seen=@da]
|
||||||
::
|
::
|
||||||
+$ unreads
|
+$ unreads
|
||||||
$% [%count num=@ud]
|
$% [%count num=@ud]
|
||||||
|
@ -94,7 +94,11 @@ module.exports = {
|
|||||||
use: {
|
use: {
|
||||||
loader: 'babel-loader',
|
loader: 'babel-loader',
|
||||||
options: {
|
options: {
|
||||||
presets: ['@babel/preset-env', '@babel/typescript', '@babel/preset-react'],
|
presets: ['@babel/preset-env', '@babel/typescript', ['@babel/preset-react', {
|
||||||
|
runtime: 'automatic',
|
||||||
|
development: true,
|
||||||
|
importSource: '@welldone-software/why-did-you-render',
|
||||||
|
}]],
|
||||||
plugins: [
|
plugins: [
|
||||||
'@babel/transform-runtime',
|
'@babel/transform-runtime',
|
||||||
'@babel/plugin-proposal-object-rest-spread',
|
'@babel/plugin-proposal-object-rest-spread',
|
||||||
|
81
pkg/interface/package-lock.json
generated
81
pkg/interface/package-lock.json
generated
@ -1783,30 +1783,36 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": {
|
"@babel/runtime": {
|
||||||
"version": "7.12.5",
|
"version": "7.12.5",
|
||||||
"bundled": true,
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz",
|
||||||
|
"integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"regenerator-runtime": "^0.13.4"
|
"regenerator-runtime": "^0.13.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/lodash": {
|
"@types/lodash": {
|
||||||
"version": "4.14.168",
|
"version": "4.14.168",
|
||||||
"bundled": true
|
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.168.tgz",
|
||||||
|
"integrity": "sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q=="
|
||||||
},
|
},
|
||||||
"@urbit/eslint-config": {
|
"@urbit/eslint-config": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"bundled": true
|
"resolved": "https://registry.npmjs.org/@urbit/eslint-config/-/eslint-config-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-Xmzb6MvM7KorlPJEq/hURZZ4BHSVy/7CoQXWogsBSTv5MOZnMqwNKw6yt24k2AO/2UpHwjGptimaNLqFfesJbw=="
|
||||||
},
|
},
|
||||||
"big-integer": {
|
"big-integer": {
|
||||||
"version": "1.6.48",
|
"version": "1.6.48",
|
||||||
"bundled": true
|
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz",
|
||||||
|
"integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w=="
|
||||||
},
|
},
|
||||||
"lodash": {
|
"lodash": {
|
||||||
"version": "4.17.20",
|
"version": "4.17.20",
|
||||||
"bundled": true
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
|
||||||
|
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
|
||||||
},
|
},
|
||||||
"regenerator-runtime": {
|
"regenerator-runtime": {
|
||||||
"version": "0.13.7",
|
"version": "0.13.7",
|
||||||
"bundled": true
|
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
|
||||||
|
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1989,6 +1995,15 @@
|
|||||||
"@xtuc/long": "4.2.2"
|
"@xtuc/long": "4.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@welldone-software/why-did-you-render": {
|
||||||
|
"version": "6.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@welldone-software/why-did-you-render/-/why-did-you-render-6.1.0.tgz",
|
||||||
|
"integrity": "sha512-0s+PuKQ4v9VV1SZSM6iS7d2T7X288T3DF+K8yfkFAhI31HhJGGH1SY1ssVm+LqjSMyrVWT60ZF5r0qUsO0Z9Lw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"lodash": "^4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@xtuc/ieee754": {
|
"@xtuc/ieee754": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
|
||||||
@ -2071,6 +2086,11 @@
|
|||||||
"color-convert": "^1.9.0"
|
"color-convert": "^1.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"any-ascii": {
|
||||||
|
"version": "0.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/any-ascii/-/any-ascii-0.1.7.tgz",
|
||||||
|
"integrity": "sha512-9zc8XIPeG9lDGtjiQGQtRF2+ow97/eTtZJR7K4UvciSC5GSOySYoytXeA2fSaY8pLhpRMcAsiZDEEkuU20HD8g=="
|
||||||
|
},
|
||||||
"anymatch": {
|
"anymatch": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
|
||||||
@ -3974,6 +3994,14 @@
|
|||||||
"prr": "~1.0.1"
|
"prr": "~1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"error-stack-parser": {
|
||||||
|
"version": "2.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.0.6.tgz",
|
||||||
|
"integrity": "sha512-d51brTeqC+BHlwF0BhPtcYgF5nlzf9ZZ0ZIUQNZpc9ZB9qw5IJ2diTrBY9jlCJkTLITYPjmiX6OWCwH+fuyNgQ==",
|
||||||
|
"requires": {
|
||||||
|
"stackframe": "^1.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"es-abstract": {
|
"es-abstract": {
|
||||||
"version": "1.18.0-next.2",
|
"version": "1.18.0-next.2",
|
||||||
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.2.tgz",
|
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.2.tgz",
|
||||||
@ -8743,6 +8771,45 @@
|
|||||||
"figgy-pudding": "^3.5.1"
|
"figgy-pudding": "^3.5.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"stack-generator": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-/t1ebrbHkrLrDuNMdeAcsvynWgoH/i4o8EGGfX7dEYDoTXOYVAkEpFdtshlvabzc6JlJ8Kf9YdFEoz7JkzGN9Q==",
|
||||||
|
"requires": {
|
||||||
|
"stackframe": "^1.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"stackframe": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-GrdeshiRmS1YLMYgzF16olf2jJ/IzxXY9lhKOskuVziubpTYcYqyOwYeJKzQkwy7uN0fYSsbsC4RQaXf9LCrYA=="
|
||||||
|
},
|
||||||
|
"stacktrace-gps": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-qIr8x41yZVSldqdqe6jciXEaSCKw1U8XTXpjDuy0ki/apyTn/r3w9hDAAQOhZdxvsC93H+WwwEu5cq5VemzYeg==",
|
||||||
|
"requires": {
|
||||||
|
"source-map": "0.5.6",
|
||||||
|
"stackframe": "^1.1.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"source-map": {
|
||||||
|
"version": "0.5.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz",
|
||||||
|
"integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"stacktrace-js": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==",
|
||||||
|
"requires": {
|
||||||
|
"error-stack-parser": "^2.0.6",
|
||||||
|
"stack-generator": "^2.0.5",
|
||||||
|
"stacktrace-gps": "^3.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"state-toggle": {
|
"state-toggle": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.3.tgz",
|
||||||
@ -9855,6 +9922,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
|
||||||
"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
|
"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"is-extendable": "^0.1.0"
|
"is-extendable": "^0.1.0"
|
||||||
}
|
}
|
||||||
@ -9921,6 +9989,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
|
||||||
"integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
|
"integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"kind-of": "^3.0.2"
|
"kind-of": "^3.0.2"
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
"@tlon/indigo-react": "^1.2.19",
|
"@tlon/indigo-react": "^1.2.19",
|
||||||
"@tlon/sigil-js": "^1.4.3",
|
"@tlon/sigil-js": "^1.4.3",
|
||||||
"@urbit/api": "file:../npm/api",
|
"@urbit/api": "file:../npm/api",
|
||||||
|
"any-ascii": "^0.1.7",
|
||||||
"aws-sdk": "^2.830.0",
|
"aws-sdk": "^2.830.0",
|
||||||
"big-integer": "^1.6.48",
|
"big-integer": "^1.6.48",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
@ -41,6 +42,7 @@
|
|||||||
"react-visibility-sensor": "^5.1.1",
|
"react-visibility-sensor": "^5.1.1",
|
||||||
"remark-breaks": "^2.0.1",
|
"remark-breaks": "^2.0.1",
|
||||||
"remark-disable-tokenizers": "^1.0.24",
|
"remark-disable-tokenizers": "^1.0.24",
|
||||||
|
"stacktrace-js": "^2.0.2",
|
||||||
"style-loader": "^1.3.0",
|
"style-loader": "^1.3.0",
|
||||||
"styled-components": "^5.1.1",
|
"styled-components": "^5.1.1",
|
||||||
"styled-system": "^5.1.5",
|
"styled-system": "^5.1.5",
|
||||||
@ -71,6 +73,7 @@
|
|||||||
"@types/yup": "^0.29.11",
|
"@types/yup": "^0.29.11",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.15.0",
|
"@typescript-eslint/eslint-plugin": "^4.15.0",
|
||||||
"@urbit/eslint-config": "file:../npm/eslint-config",
|
"@urbit/eslint-config": "file:../npm/eslint-config",
|
||||||
|
"@welldone-software/why-did-you-render": "^6.1.0",
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
"babel-loader": "^8.2.2",
|
"babel-loader": "^8.2.2",
|
||||||
"babel-plugin-lodash": "^3.3.4",
|
"babel-plugin-lodash": "^3.3.4",
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import './wdyr';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import * as ReactDOM from 'react-dom';
|
import * as ReactDOM from 'react-dom';
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import { StoreState } from '../store/type';
|
|||||||
export default class LocalApi extends BaseApi<StoreState> {
|
export default class LocalApi extends BaseApi<StoreState> {
|
||||||
getBaseHash() {
|
getBaseHash() {
|
||||||
this.scry<string>('file-server', '/clay/base/hash').then((baseHash) => {
|
this.scry<string>('file-server', '/clay/base/hash').then((baseHash) => {
|
||||||
this.store.handleEvent({ data: { local: { baseHash } } });
|
this.store.handleEvent({ data: { baseHash } });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,7 +77,6 @@ export default class MetadataApi extends BaseApi<StoreState> {
|
|||||||
tempChannel.delete();
|
tempChannel.delete();
|
||||||
},
|
},
|
||||||
(ev: any) => {
|
(ev: any) => {
|
||||||
console.log(ev);
|
|
||||||
if ('metadata-hook-update' in ev) {
|
if ('metadata-hook-update' in ev) {
|
||||||
done = true;
|
done = true;
|
||||||
tempChannel.delete();
|
tempChannel.delete();
|
||||||
|
@ -14,16 +14,14 @@
|
|||||||
//
|
//
|
||||||
//
|
//
|
||||||
import GlobalApi from '../api/global';
|
import GlobalApi from '../api/global';
|
||||||
import GlobalStore from '../store/store';
|
import useStorageState from '../state/storage';
|
||||||
|
|
||||||
|
|
||||||
class GcpManager {
|
class GcpManager {
|
||||||
#api: GlobalApi | null = null;
|
#api: GlobalApi | null = null;
|
||||||
#store: GlobalStore | null = null;
|
|
||||||
|
|
||||||
configure(api: GlobalApi, store: GlobalStore) {
|
configure(api: GlobalApi) {
|
||||||
this.#api = api;
|
this.#api = api;
|
||||||
this.#store = store;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#running = false;
|
#running = false;
|
||||||
@ -34,8 +32,8 @@ class GcpManager {
|
|||||||
console.warn('GcpManager already running');
|
console.warn('GcpManager already running');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this.#api || !this.#store) {
|
if (!this.#api) {
|
||||||
console.error('GcpManager must have api and store set');
|
console.error('GcpManager must have api set');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.#running = true;
|
this.#running = true;
|
||||||
@ -65,7 +63,7 @@ class GcpManager {
|
|||||||
#consecutiveFailures: number = 0;
|
#consecutiveFailures: number = 0;
|
||||||
|
|
||||||
private isConfigured() {
|
private isConfigured() {
|
||||||
return this.#store.state.storage.gcp.configured;
|
return useStorageState.getState().gcp.configured;
|
||||||
}
|
}
|
||||||
|
|
||||||
private refreshLoop() {
|
private refreshLoop() {
|
||||||
@ -78,7 +76,8 @@ class GcpManager {
|
|||||||
if (this.isConfigured()) {
|
if (this.isConfigured()) {
|
||||||
this.refreshLoop();
|
this.refreshLoop();
|
||||||
} else {
|
} else {
|
||||||
this.refreshAfter(10_000);
|
console.log('GcpManager: GCP storage not configured; stopping.');
|
||||||
|
this.stop();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((reason) => {
|
.catch((reason) => {
|
||||||
@ -89,7 +88,7 @@ class GcpManager {
|
|||||||
}
|
}
|
||||||
this.#api.gcp.getToken()
|
this.#api.gcp.getToken()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const token = this.#store.state.storage.gcp?.token;
|
const token = useStorageState.getState().gcp.token;
|
||||||
if (token) {
|
if (token) {
|
||||||
this.#consecutiveFailures = 0;
|
this.#consecutiveFailures = 0;
|
||||||
const interval = this.refreshInterval(token.expiresIn);
|
const interval = this.refreshInterval(token.expiresIn);
|
||||||
|
@ -18,10 +18,6 @@ export function useMigrateSettings(api: GlobalApi) {
|
|||||||
const { display, remoteContentPolicy, calm } = useSettingsState();
|
const { display, remoteContentPolicy, calm } = useSettingsState();
|
||||||
|
|
||||||
return async () => {
|
return async () => {
|
||||||
if (!localStorage?.has("localReducer")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let promises: Promise<any>[] = [];
|
let promises: Promise<any>[] = [];
|
||||||
|
|
||||||
if (local.hideAvatars !== calm.hideAvatars) {
|
if (local.hideAvatars !== calm.hideAvatars) {
|
||||||
|
@ -107,7 +107,9 @@ export function getComments(node: GraphNode): GraphNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getSnippet(body: string) {
|
export function getSnippet(body: string) {
|
||||||
const start = body.slice(0, body.indexOf('\n', 2));
|
const newlineIdx = body.indexOf('\n', 2);
|
||||||
|
const end = newlineIdx > -1 ? newlineIdx : body.length;
|
||||||
|
const start = body.substr(0, end);
|
||||||
|
|
||||||
return (start === body || start.startsWith('![')) ? start : `${start}...`;
|
return (start === body || start.startsWith('![')) ? start : `${start}...`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,9 +23,10 @@ export const Sigil = memo(
|
|||||||
size,
|
size,
|
||||||
svgClass = '',
|
svgClass = '',
|
||||||
icon = false,
|
icon = false,
|
||||||
padding = 0
|
padding = 0,
|
||||||
|
display = 'inline-block'
|
||||||
}) => {
|
}) => {
|
||||||
const innerSize = Number(size) - 2*padding;
|
const innerSize = Number(size) - 2 * padding;
|
||||||
const paddingPx = `${padding}px`;
|
const paddingPx = `${padding}px`;
|
||||||
const foregroundColor = foreground
|
const foregroundColor = foreground
|
||||||
? foreground
|
? foreground
|
||||||
@ -34,14 +35,14 @@ export const Sigil = memo(
|
|||||||
<Box
|
<Box
|
||||||
backgroundColor={color}
|
backgroundColor={color}
|
||||||
borderRadius={icon ? '1' : '0'}
|
borderRadius={icon ? '1' : '0'}
|
||||||
display='inline-block'
|
display={display}
|
||||||
height={size}
|
height={size}
|
||||||
width={size}
|
width={size}
|
||||||
className={classes}
|
className={classes}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Box
|
<Box
|
||||||
display='inline-block'
|
display={display}
|
||||||
borderRadius={icon ? '1' : '0'}
|
borderRadius={icon ? '1' : '0'}
|
||||||
flexBasis={size}
|
flexBasis={size}
|
||||||
backgroundColor={color}
|
backgroundColor={color}
|
||||||
|
@ -8,6 +8,7 @@ import S3 from 'aws-sdk/clients/s3';
|
|||||||
import GcpClient from './GcpClient';
|
import GcpClient from './GcpClient';
|
||||||
import { StorageClient, StorageAcl } from './StorageClient';
|
import { StorageClient, StorageAcl } from './StorageClient';
|
||||||
import { dateToDa, deSig } from './util';
|
import { dateToDa, deSig } from './util';
|
||||||
|
import useStorageState from '../state/storage';
|
||||||
|
|
||||||
|
|
||||||
export interface IuseStorage {
|
export interface IuseStorage {
|
||||||
@ -18,9 +19,10 @@ export interface IuseStorage {
|
|||||||
promptUpload: () => Promise<string | undefined>;
|
promptUpload: () => Promise<string | undefined>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStorage = ({gcp, s3}: StorageState,
|
const useStorage = ({ accept = '*' } = { accept: '*' }): IuseStorage => {
|
||||||
{ accept = '*' } = { accept: '*' }): IuseStorage => {
|
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
|
const gcp = useStorageState(state => state.gcp);
|
||||||
|
const s3 = useStorageState(state => state.s3);
|
||||||
|
|
||||||
const client = useRef<StorageClient | null>(null);
|
const client = useRef<StorageClient | null>(null);
|
||||||
|
|
||||||
|
@ -1,9 +1,17 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import _ from "lodash";
|
import _ from 'lodash';
|
||||||
import f, { memoize } from "lodash/fp";
|
import f, { compose, memoize } from 'lodash/fp';
|
||||||
import bigInt, { BigInteger } from "big-integer";
|
import bigInt, { BigInteger } from 'big-integer';
|
||||||
import { Contact } from '~/types';
|
import { Association, Contact } from '@urbit/api';
|
||||||
|
import useLocalState from '../state/local';
|
||||||
|
import produce, { enableMapSet } from 'immer';
|
||||||
import useSettingsState from '../state/settings';
|
import useSettingsState from '../state/settings';
|
||||||
|
import { State, UseStore } from 'zustand';
|
||||||
|
import { Cage } from '~/types/cage';
|
||||||
|
import { BaseState } from '../state/base';
|
||||||
|
import anyAscii from 'any-ascii';
|
||||||
|
|
||||||
|
enableMapSet();
|
||||||
|
|
||||||
export const MOBILE_BROWSER_REGEX = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i;
|
export const MOBILE_BROWSER_REGEX = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i;
|
||||||
|
|
||||||
@ -17,7 +25,7 @@ export const MOMENT_CALENDAR_DATE = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getModuleIcon = (mod: string) => {
|
export const getModuleIcon = (mod: string) => {
|
||||||
if (mod === 'link') {
|
if (mod === 'link') {
|
||||||
return 'Collection';
|
return 'Collection';
|
||||||
}
|
}
|
||||||
return _.capitalize(mod);
|
return _.capitalize(mod);
|
||||||
@ -50,7 +58,7 @@ export function daToUnix(da: BigInteger) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function unixToDa(unix: number) {
|
export function unixToDa(unix: number) {
|
||||||
const timeSinceEpoch = bigInt(unix).multiply(DA_SECOND).divide(bigInt(1000));
|
const timeSinceEpoch = bigInt(unix).multiply(DA_SECOND).divide(bigInt(1000));
|
||||||
return DA_UNIX_EPOCH.add(timeSinceEpoch);
|
return DA_UNIX_EPOCH.add(timeSinceEpoch);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -300,7 +308,7 @@ export function stringToTa(str: string) {
|
|||||||
|
|
||||||
export function amOwnerOfGroup(groupPath: string) {
|
export function amOwnerOfGroup(groupPath: string) {
|
||||||
if (!groupPath)
|
if (!groupPath)
|
||||||
return false;
|
return false;
|
||||||
const groupOwner = /(\/~)?\/~([a-z-]{3,})\/.*/.exec(groupPath)?.[2];
|
const groupOwner = /(\/~)?\/~([a-z-]{3,})\/.*/.exec(groupPath)?.[2];
|
||||||
return window.ship === groupOwner;
|
return window.ship === groupOwner;
|
||||||
}
|
}
|
||||||
@ -319,11 +327,12 @@ export function getContactDetails(contact: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function stringToSymbol(str: string) {
|
export function stringToSymbol(str: string) {
|
||||||
|
const ascii = anyAscii(str);
|
||||||
let result = '';
|
let result = '';
|
||||||
for (let i = 0; i < str.length; i++) {
|
for (let i = 0; i < ascii.length; i++) {
|
||||||
const n = str.charCodeAt(i);
|
const n = ascii.charCodeAt(i);
|
||||||
if ((n >= 97 && n <= 122) || (n >= 48 && n <= 57)) {
|
if ((n >= 97 && n <= 122) || (n >= 48 && n <= 57)) {
|
||||||
result += str[i];
|
result += ascii[i];
|
||||||
} else if (n >= 65 && n <= 90) {
|
} else if (n >= 65 && n <= 90) {
|
||||||
result += String.fromCharCode(n + 32);
|
result += String.fromCharCode(n + 32);
|
||||||
} else {
|
} else {
|
||||||
@ -337,7 +346,6 @@ export function stringToSymbol(str: string) {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formats a numbers as a `@ud` inserting dot where needed
|
* Formats a numbers as a `@ud` inserting dot where needed
|
||||||
*/
|
*/
|
||||||
@ -355,7 +363,7 @@ export function numToUd(num: number) {
|
|||||||
export function usePreventWindowUnload(shouldPreventDefault: boolean, message = 'You have unsaved changes. Are you sure you want to exit?') {
|
export function usePreventWindowUnload(shouldPreventDefault: boolean, message = 'You have unsaved changes. Are you sure you want to exit?') {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!shouldPreventDefault)
|
if (!shouldPreventDefault)
|
||||||
return;
|
return;
|
||||||
const handleBeforeUnload = (event) => {
|
const handleBeforeUnload = (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
return message;
|
return message;
|
||||||
@ -371,12 +379,13 @@ return;
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function pluralize(text: string, isPlural = false, vowel = false) {
|
export function pluralize(text: string, isPlural = false, vowel = false) {
|
||||||
return isPlural ? `${text}s`: `${vowel ? 'an' : 'a'} ${text}`;
|
return isPlural ? `${text}s` : `${vowel ? 'an' : 'a'} ${text}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide is an optional second parameter for when this function is used in class components
|
// Hide is an optional second parameter for when this function is used in class components
|
||||||
export function useShowNickname(contact: Contact | null, hide?: boolean): boolean {
|
export function useShowNickname(contact: Contact | null, hide?: boolean): boolean {
|
||||||
const hideNicknames = typeof hide !== 'undefined' ? hide : useSettingsState(state => state.calm.hideNicknames);
|
const hideState = useSettingsState(state => state.calm.hideNicknames);
|
||||||
|
const hideNicknames = typeof hide !== 'undefined' ? hide : hideState;
|
||||||
return !!(contact && contact.nickname && !hideNicknames);
|
return !!(contact && contact.nickname && !hideNicknames);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -399,12 +408,13 @@ export const useHovering = (): useHoveringInterface => {
|
|||||||
|
|
||||||
const DM_REGEX = /ship\/~([a-z]|-)*\/dm--/;
|
const DM_REGEX = /ship\/~([a-z]|-)*\/dm--/;
|
||||||
export function getItemTitle(association: Association) {
|
export function getItemTitle(association: Association) {
|
||||||
if(DM_REGEX.test(association.resource)) {
|
if (DM_REGEX.test(association.resource)) {
|
||||||
const [,,ship,name] = association.resource.split('/');
|
const [, , ship, name] = association.resource.split('/');
|
||||||
if(ship.slice(1) === window.ship) {
|
if (ship.slice(1) === window.ship) {
|
||||||
return cite(`~${name.slice(4)}`);
|
return cite(`~${name.slice(4)}`);
|
||||||
}
|
}
|
||||||
return cite(ship);
|
return cite(ship);
|
||||||
}
|
}
|
||||||
return association.metadata.title || association.resource;
|
return association.metadata.title || association.resource;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
44
pkg/interface/src/logic/lib/withState.tsx
Normal file
44
pkg/interface/src/logic/lib/withState.tsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { ReactElement } from "react";
|
||||||
|
import { UseStore } from "zustand";
|
||||||
|
import { BaseState } from "../state/base";
|
||||||
|
|
||||||
|
const withStateo = <
|
||||||
|
StateType extends BaseState<any>
|
||||||
|
>(
|
||||||
|
useState: UseStore<StateType>,
|
||||||
|
Component: any,
|
||||||
|
stateMemberKeys?: (keyof StateType)[]
|
||||||
|
) => {
|
||||||
|
return React.forwardRef((props, ref) => {
|
||||||
|
const state = stateMemberKeys ? useState(
|
||||||
|
state => stateMemberKeys.reduce(
|
||||||
|
(object, key) => ({ ...object, [key]: state[key] }), {}
|
||||||
|
)
|
||||||
|
) : useState();
|
||||||
|
return <Component ref={ref} {...state} {...props} />
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
const withState = <
|
||||||
|
StateType extends BaseState<StateType>,
|
||||||
|
stateKey extends keyof StateType
|
||||||
|
>(
|
||||||
|
Component: any,
|
||||||
|
stores: ([UseStore<StateType>, stateKey[]])[],
|
||||||
|
) => {
|
||||||
|
return React.forwardRef((props, ref) => {
|
||||||
|
let stateProps: unknown = {};
|
||||||
|
stores.forEach(([store, keys]) => {
|
||||||
|
const storeProps = Array.isArray(keys)
|
||||||
|
? store(state => keys.reduce(
|
||||||
|
(object, key) => ({ ...object, [key]: state[key] }), {}
|
||||||
|
))
|
||||||
|
: store();
|
||||||
|
Object.assign(stateProps, storeProps);
|
||||||
|
});
|
||||||
|
return <Component ref={ref} {...stateProps} {...props} />
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withState;
|
@ -1,44 +1,51 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { StoreState } from '../../store/type';
|
import { compose } from 'lodash/fp';
|
||||||
import { Cage } from '~/types/cage';
|
|
||||||
import { ContactUpdate } from '@urbit/api/contacts';
|
|
||||||
import { resourceAsPath } from '../lib/util';
|
|
||||||
|
|
||||||
type ContactState = Pick<StoreState, 'contacts'>;
|
import { ContactUpdate } from '@urbit/api';
|
||||||
|
|
||||||
export const ContactReducer = (json, state) => {
|
import useContactState, { ContactState } from '../state/contact';
|
||||||
const data = _.get(json, 'contact-update', false);
|
import { reduceState } from '../state/base';
|
||||||
|
|
||||||
|
|
||||||
|
export const ContactReducer = (json) => {
|
||||||
|
const data: ContactUpdate = _.get(json, 'contact-update', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
initial(data, state);
|
reduceState<ContactState, ContactUpdate>(useContactState, data, [
|
||||||
add(data, state);
|
initial,
|
||||||
remove(data, state);
|
add,
|
||||||
edit(data, state);
|
remove,
|
||||||
setPublic(data, state);
|
edit,
|
||||||
|
setPublic
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: better isolation
|
// TODO: better isolation
|
||||||
const res = _.get(json, 'resource', false);
|
const res = _.get(json, 'resource', false);
|
||||||
if(res) {
|
if (res) {
|
||||||
state.nackedContacts = state.nackedContacts.add(`~${res.ship}`);
|
useContactState.setState({
|
||||||
|
nackedContacts: useContactState.getState().nackedContacts.add(`~${res.ship}`)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const initial = (json: ContactUpdate, state: S) => {
|
const initial = (json: ContactUpdate, state: ContactState): ContactState => {
|
||||||
const data = _.get(json, 'initial', false);
|
const data = _.get(json, 'initial', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
state.contacts = data.rolodex;
|
state.contacts = data.rolodex;
|
||||||
state.isContactPublic = data['is-public'];
|
state.isContactPublic = data['is-public'];
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
};
|
};
|
||||||
|
|
||||||
const add = (json: ContactUpdate, state: S) => {
|
const add = (json: ContactUpdate, state: ContactState): ContactState => {
|
||||||
const data = _.get(json, 'add', false);
|
const data = _.get(json, 'add', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
state.contacts[data.ship] = data.contact;
|
state.contacts[data.ship] = data.contact;
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
};
|
};
|
||||||
|
|
||||||
const remove = (json: ContactUpdate, state: S) => {
|
const remove = (json: ContactUpdate, state: ContactState): ContactState => {
|
||||||
const data = _.get(json, 'remove', false);
|
const data = _.get(json, 'remove', false);
|
||||||
if (
|
if (
|
||||||
data &&
|
data &&
|
||||||
@ -46,9 +53,10 @@ const remove = (json: ContactUpdate, state: S) => {
|
|||||||
) {
|
) {
|
||||||
delete state.contacts[data.ship];
|
delete state.contacts[data.ship];
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
};
|
};
|
||||||
|
|
||||||
const edit = (json: ContactUpdate, state: S) => {
|
const edit = (json: ContactUpdate, state: ContactState): ContactState => {
|
||||||
const data = _.get(json, 'edit', false);
|
const data = _.get(json, 'edit', false);
|
||||||
const ship = `~${data.ship}`;
|
const ship = `~${data.ship}`;
|
||||||
if (
|
if (
|
||||||
@ -57,7 +65,7 @@ const edit = (json: ContactUpdate, state: S) => {
|
|||||||
) {
|
) {
|
||||||
const [field] = Object.keys(data['edit-field']);
|
const [field] = Object.keys(data['edit-field']);
|
||||||
if (!field) {
|
if (!field) {
|
||||||
return;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = data['edit-field'][field];
|
const value = data['edit-field'][field];
|
||||||
@ -71,10 +79,12 @@ const edit = (json: ContactUpdate, state: S) => {
|
|||||||
state.contacts[ship][field] = value;
|
state.contacts[ship][field] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
};
|
};
|
||||||
|
|
||||||
const setPublic = (json: ContactUpdate, state: S) => {
|
const setPublic = (json: ContactUpdate, state: ContactState): ContactState => {
|
||||||
const data = _.get(json, 'set-public', state.isContactPublic);
|
const data = _.get(json, 'set-public', state.isContactPublic);
|
||||||
state.isContactPublic = data;
|
state.isContactPublic = data;
|
||||||
|
return state;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,37 +1,43 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import {StoreState} from '../store/type';
|
import {StoreState} from '../store/type';
|
||||||
import {GcpToken} from '../../types/gcp-state';
|
import {GcpToken} from '../../types/gcp-state';
|
||||||
|
import { Cage } from '~/types/cage';
|
||||||
|
import useStorageState, { StorageState } from '../state/storage';
|
||||||
|
import { reduceState } from '../state/base';
|
||||||
|
|
||||||
type GcpState = Pick<StoreState, 'gcp'>;
|
export default class GcpReducer {
|
||||||
|
reduce(json: Cage) {
|
||||||
export default class GcpReducer<S extends GcpState>{
|
reduceState<StorageState, any>(useStorageState, json, [
|
||||||
reduce(json: Cage, state: S) {
|
reduceConfigured,
|
||||||
this.reduceConfigured(json, state);
|
reduceToken
|
||||||
this.reduceToken(json, state);
|
]);
|
||||||
}
|
|
||||||
|
|
||||||
reduceConfigured(json, state) {
|
|
||||||
let data = json['gcp-configured'];
|
|
||||||
if (data !== undefined) {
|
|
||||||
state.storage.gcp.configured = data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
reduceToken(json: Cage, state: S) {
|
|
||||||
let data = json['gcp-token'];
|
|
||||||
if (data) {
|
|
||||||
this.setToken(data, state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setToken(data: any, state: S) {
|
|
||||||
if (this.isToken(data)) {
|
|
||||||
state.storage.gcp.token = data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isToken(token: any): token is GcpToken {
|
|
||||||
return (typeof(token.accessKey) === 'string' &&
|
|
||||||
typeof(token.expiresIn) === 'number');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const reduceConfigured = (json, state: StorageState): StorageState => {
|
||||||
|
let data = json['gcp-configured'];
|
||||||
|
if (data !== undefined) {
|
||||||
|
state.gcp.configured = data;
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reduceToken = (json: Cage, state: StorageState): StorageState => {
|
||||||
|
let data = json['gcp-token'];
|
||||||
|
if (data) {
|
||||||
|
state = setToken(data, state);
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const setToken = (data: any, state: StorageState): StorageState => {
|
||||||
|
if (isToken(data)) {
|
||||||
|
state.gcp.token = data;
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isToken = (token: any): boolean => {
|
||||||
|
return (typeof(token.accessKey) === 'string' &&
|
||||||
|
typeof(token.expiresIn) === 'number');
|
||||||
|
}
|
||||||
|
@ -1,20 +1,24 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { BigIntOrderedMap } from "~/logic/lib/BigIntOrderedMap";
|
import { BigIntOrderedMap } from "~/logic/lib/BigIntOrderedMap";
|
||||||
import bigInt, { BigInteger } from "big-integer";
|
import bigInt, { BigInteger } from "big-integer";
|
||||||
|
import useGraphState, { GraphState } from '../state/graph';
|
||||||
|
import { reduceState } from '../state/base';
|
||||||
|
|
||||||
export const GraphReducer = (json, state) => {
|
export const GraphReducer = (json) => {
|
||||||
const data = _.get(json, 'graph-update', false);
|
const data = _.get(json, 'graph-update', false);
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
keys(data, state);
|
reduceState<GraphState, any>(useGraphState, data, [
|
||||||
addGraph(data, state);
|
keys,
|
||||||
removeGraph(data, state);
|
addGraph,
|
||||||
addNodes(data, state);
|
removeGraph,
|
||||||
removeNodes(data, state);
|
addNodes,
|
||||||
|
removeNodes
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const keys = (json, state) => {
|
const keys = (json, state: GraphState): GraphState => {
|
||||||
const data = _.get(json, 'keys', false);
|
const data = _.get(json, 'keys', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
state.graphKeys = new Set(data.map((res) => {
|
state.graphKeys = new Set(data.map((res) => {
|
||||||
@ -22,9 +26,10 @@ const keys = (json, state) => {
|
|||||||
return resource;
|
return resource;
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
};
|
};
|
||||||
|
|
||||||
const addGraph = (json, state) => {
|
const addGraph = (json, state: GraphState): GraphState => {
|
||||||
|
|
||||||
const _processNode = (node) => {
|
const _processNode = (node) => {
|
||||||
// is empty
|
// is empty
|
||||||
@ -72,10 +77,10 @@ const addGraph = (json, state) => {
|
|||||||
}
|
}
|
||||||
state.graphKeys.add(resource);
|
state.graphKeys.add(resource);
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeGraph = (json, state) => {
|
const removeGraph = (json, state: GraphState): GraphState => {
|
||||||
const data = _.get(json, 'remove-graph', false);
|
const data = _.get(json, 'remove-graph', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
|
|
||||||
@ -86,6 +91,7 @@ const removeGraph = (json, state) => {
|
|||||||
state.graphKeys.delete(resource);
|
state.graphKeys.delete(resource);
|
||||||
delete state.graphs[resource];
|
delete state.graphs[resource];
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapifyChildren = (children) => {
|
const mapifyChildren = (children) => {
|
||||||
@ -98,7 +104,7 @@ const mapifyChildren = (children) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const addNodes = (json, state) => {
|
const addNodes = (json, state) => {
|
||||||
const _addNode = (graph, index, node, resource) => {
|
const _addNode = (graph, index, node) => {
|
||||||
// set child of graph
|
// set child of graph
|
||||||
if (index.length === 1) {
|
if (index.length === 1) {
|
||||||
graph.set(index[0], node);
|
graph.set(index[0], node);
|
||||||
@ -160,7 +166,7 @@ const addNodes = (json, state) => {
|
|||||||
|
|
||||||
const data = _.get(json, 'add-nodes', false);
|
const data = _.get(json, 'add-nodes', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
if (!('graphs' in state)) { return; }
|
if (!('graphs' in state)) { return state; }
|
||||||
|
|
||||||
let resource = data.resource.ship + '/' + data.resource.name;
|
let resource = data.resource.ship + '/' + data.resource.name;
|
||||||
if (!(resource in state.graphs)) {
|
if (!(resource in state.graphs)) {
|
||||||
@ -192,7 +198,7 @@ const addNodes = (json, state) => {
|
|||||||
return bigInt(ind);
|
return bigInt(ind);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (indexArr.length === 0) { return; }
|
if (indexArr.length === 0) { return state; }
|
||||||
|
|
||||||
if (node.post.pending) {
|
if (node.post.pending) {
|
||||||
state.graphTimesentMap[resource][node.post['time-sent']] = index;
|
state.graphTimesentMap[resource][node.post['time-sent']] = index;
|
||||||
@ -210,10 +216,10 @@ const addNodes = (json, state) => {
|
|||||||
|
|
||||||
state.graphs[resource] = graph;
|
state.graphs[resource] = graph;
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const removeNodes = (json, state: GraphState): GraphState => {
|
||||||
const removeNodes = (json, state) => {
|
|
||||||
const _remove = (graph, index) => {
|
const _remove = (graph, index) => {
|
||||||
if (index.length === 1) {
|
if (index.length === 1) {
|
||||||
graph.delete(index[0]);
|
graph.delete(index[0]);
|
||||||
@ -230,7 +236,7 @@ const removeNodes = (json, state) => {
|
|||||||
if (data) {
|
if (data) {
|
||||||
const { ship, name } = data.resource;
|
const { ship, name } = data.resource;
|
||||||
const res = `${ship}/${name}`;
|
const res = `${ship}/${name}`;
|
||||||
if (!(res in state.graphs)) { return; }
|
if (!(res in state.graphs)) { return state; }
|
||||||
|
|
||||||
data.indices.forEach((index) => {
|
data.indices.forEach((index) => {
|
||||||
if (index.split('/').length === 0) { return; }
|
if (index.split('/').length === 0) { return; }
|
||||||
@ -240,4 +246,5 @@ const removeNodes = (json, state) => {
|
|||||||
_remove(state.graphs[res], indexArr);
|
_remove(state.graphs[res], indexArr);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
};
|
};
|
@ -1,5 +1,4 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { StoreState } from '../../store/type';
|
|
||||||
import { Cage } from '~/types/cage';
|
import { Cage } from '~/types/cage';
|
||||||
import {
|
import {
|
||||||
GroupUpdate,
|
GroupUpdate,
|
||||||
@ -14,8 +13,9 @@ import {
|
|||||||
} from '@urbit/api/groups';
|
} from '@urbit/api/groups';
|
||||||
import { Enc, PatpNoSig } from '@urbit/api';
|
import { Enc, PatpNoSig } from '@urbit/api';
|
||||||
import { resourceAsPath } from '../lib/util';
|
import { resourceAsPath } from '../lib/util';
|
||||||
|
import useGroupState, { GroupState } from '../state/group';
|
||||||
type GroupState = Pick<StoreState, 'groups' | 'groupKeys'>;
|
import { compose } from 'lodash/fp';
|
||||||
|
import { reduceState } from '../state/base';
|
||||||
|
|
||||||
function decodeGroup(group: Enc<Group>): Group {
|
function decodeGroup(group: Enc<Group>): Group {
|
||||||
const members = new Set(group.members);
|
const members = new Set(group.members);
|
||||||
@ -57,186 +57,195 @@ function decodeTags(tags: Enc<Tags>): Tags {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class GroupReducer<S extends GroupState> {
|
export default class GroupReducer {
|
||||||
reduce(json: Cage, state: S) {
|
reduce(json: Cage) {
|
||||||
const data = json.groupUpdate;
|
const data = json.groupUpdate;
|
||||||
if (data) {
|
if (data) {
|
||||||
console.log(data);
|
reduceState<GroupState, GroupUpdate>(useGroupState, data, [
|
||||||
this.initial(data, state);
|
initial,
|
||||||
this.addMembers(data, state);
|
addMembers,
|
||||||
this.addTag(data, state);
|
addTag,
|
||||||
this.removeMembers(data, state);
|
removeMembers,
|
||||||
this.initialGroup(data, state);
|
initialGroup,
|
||||||
this.removeTag(data, state);
|
removeTag,
|
||||||
this.initial(data, state);
|
addGroup,
|
||||||
this.addGroup(data, state);
|
removeGroup,
|
||||||
this.removeGroup(data, state);
|
changePolicy,
|
||||||
this.changePolicy(data, state);
|
expose,
|
||||||
this.expose(data, state);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
initial(json: GroupUpdate, state: S) {
|
}
|
||||||
const data = json['initial'];
|
const initial = (json: GroupUpdate, state: GroupState): GroupState => {
|
||||||
if (data) {
|
const data = json['initial'];
|
||||||
state.groups = _.mapValues(data, decodeGroup);
|
if (data) {
|
||||||
|
state.groups = _.mapValues(data, decodeGroup);
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialGroup = (json: GroupUpdate, state: GroupState): GroupState => {
|
||||||
|
if ('initialGroup' in json) {
|
||||||
|
const { resource, group } = json.initialGroup;
|
||||||
|
const path = resourceAsPath(resource);
|
||||||
|
state.groups[path] = decodeGroup(group);
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const addGroup = (json: GroupUpdate, state: GroupState): GroupState => {
|
||||||
|
if ('addGroup' in json) {
|
||||||
|
const { resource, policy, hidden } = json.addGroup;
|
||||||
|
const resourcePath = resourceAsPath(resource);
|
||||||
|
state.groups[resourcePath] = {
|
||||||
|
members: new Set(),
|
||||||
|
tags: { role: { admin: new Set([window.ship]) } },
|
||||||
|
policy: decodePolicy(policy),
|
||||||
|
hidden
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeGroup = (json: GroupUpdate, state: GroupState): GroupState => {
|
||||||
|
if('removeGroup' in json) {
|
||||||
|
const { resource } = json.removeGroup;
|
||||||
|
const resourcePath = resourceAsPath(resource);
|
||||||
|
delete state.groups[resourcePath];
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const addMembers = (json: GroupUpdate, state: GroupState): GroupState => {
|
||||||
|
if ('addMembers' in json) {
|
||||||
|
const { resource, ships } = json.addMembers;
|
||||||
|
const resourcePath = resourceAsPath(resource);
|
||||||
|
for (const member of ships) {
|
||||||
|
state.groups[resourcePath].members.add(member);
|
||||||
|
if (
|
||||||
|
'invite' in state.groups[resourcePath].policy &&
|
||||||
|
state.groups[resourcePath].policy.invite.pending.has(member)
|
||||||
|
) {
|
||||||
|
state.groups[resourcePath].policy.invite.pending.delete(member)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
initialGroup(json: GroupUpdate, state: S) {
|
const removeMembers = (json: GroupUpdate, state: GroupState): GroupState => {
|
||||||
if ('initialGroup' in json) {
|
if ('removeMembers' in json) {
|
||||||
const { resource, group } = json.initialGroup;
|
const { resource, ships } = json.removeMembers;
|
||||||
const path = resourceAsPath(resource);
|
const resourcePath = resourceAsPath(resource);
|
||||||
state.groups[path] = decodeGroup(group);
|
for (const member of ships) {
|
||||||
|
state.groups[resourcePath].members.delete(member);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
addGroup(json: GroupUpdate, state: S) {
|
const addTag = (json: GroupUpdate, state: GroupState): GroupState => {
|
||||||
if ('addGroup' in json) {
|
if ('addTag' in json) {
|
||||||
const { resource, policy, hidden } = json.addGroup;
|
const { resource, tag, ships } = json.addTag;
|
||||||
const resourcePath = resourceAsPath(resource);
|
const resourcePath = resourceAsPath(resource);
|
||||||
state.groups[resourcePath] = {
|
const tags = state.groups[resourcePath].tags;
|
||||||
members: new Set(),
|
const tagAccessors =
|
||||||
tags: { role: { admin: new Set([window.ship]) } },
|
'app' in tag ? [tag.app,tag.resource, tag.tag] : ['role', tag.tag];
|
||||||
policy: decodePolicy(policy),
|
const tagged = _.get(tags, tagAccessors, new Set());
|
||||||
hidden
|
for (const ship of ships) {
|
||||||
};
|
tagged.add(ship);
|
||||||
}
|
}
|
||||||
|
_.set(tags, tagAccessors, tagged);
|
||||||
}
|
}
|
||||||
removeGroup(json: GroupUpdate, state: S) {
|
return state;
|
||||||
if('removeGroup' in json) {
|
}
|
||||||
const { resource } = json.removeGroup;
|
|
||||||
const resourcePath = resourceAsPath(resource);
|
const removeTag = (json: GroupUpdate, state: GroupState): GroupState => {
|
||||||
delete state.groups[resourcePath];
|
if ('removeTag' in json) {
|
||||||
|
const { resource, tag, ships } = json.removeTag;
|
||||||
|
const resourcePath = resourceAsPath(resource);
|
||||||
|
const tags = state.groups[resourcePath].tags;
|
||||||
|
const tagAccessors =
|
||||||
|
'app' in tag ? [tag.app, tag.resource, tag.tag] : ['role', tag.tag];
|
||||||
|
const tagged = _.get(tags, tagAccessors, new Set());
|
||||||
|
|
||||||
|
if (!tagged) {
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
}
|
for (const ship of ships) {
|
||||||
|
tagged.delete(ship);
|
||||||
addMembers(json: GroupUpdate, state: S) {
|
|
||||||
if ('addMembers' in json) {
|
|
||||||
const { resource, ships } = json.addMembers;
|
|
||||||
const resourcePath = resourceAsPath(resource);
|
|
||||||
for (const member of ships) {
|
|
||||||
state.groups[resourcePath].members.add(member);
|
|
||||||
if (
|
|
||||||
'invite' in state.groups[resourcePath].policy &&
|
|
||||||
state.groups[resourcePath].policy.invite.pending.has(member)
|
|
||||||
) {
|
|
||||||
state.groups[resourcePath].policy.invite.pending.delete(member)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
_.set(tags, tagAccessors, tagged);
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
removeMembers(json: GroupUpdate, state: S) {
|
const changePolicy = (json: GroupUpdate, state: GroupState): GroupState => {
|
||||||
if ('removeMembers' in json) {
|
if ('changePolicy' in json && state) {
|
||||||
const { resource, ships } = json.removeMembers;
|
const { resource, diff } = json.changePolicy;
|
||||||
const resourcePath = resourceAsPath(resource);
|
const resourcePath = resourceAsPath(resource);
|
||||||
for (const member of ships) {
|
const policy = state.groups[resourcePath].policy;
|
||||||
state.groups[resourcePath].members.delete(member);
|
if ('open' in policy && 'open' in diff) {
|
||||||
}
|
openChangePolicy(diff.open, policy);
|
||||||
}
|
} else if ('invite' in policy && 'invite' in diff) {
|
||||||
}
|
inviteChangePolicy(diff.invite, policy);
|
||||||
|
} else if ('replace' in diff) {
|
||||||
addTag(json: GroupUpdate, state: S) {
|
state.groups[resourcePath].policy = diff.replace;
|
||||||
if ('addTag' in json) {
|
|
||||||
const { resource, tag, ships } = json.addTag;
|
|
||||||
const resourcePath = resourceAsPath(resource);
|
|
||||||
const tags = state.groups[resourcePath].tags;
|
|
||||||
const tagAccessors =
|
|
||||||
'app' in tag ? [tag.app,tag.resource, tag.tag] : ['role', tag.tag];
|
|
||||||
const tagged = _.get(tags, tagAccessors, new Set());
|
|
||||||
for (const ship of ships) {
|
|
||||||
tagged.add(ship);
|
|
||||||
}
|
|
||||||
_.set(tags, tagAccessors, tagged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
removeTag(json: GroupUpdate, state: S) {
|
|
||||||
if ('removeTag' in json) {
|
|
||||||
const { resource, tag, ships } = json.removeTag;
|
|
||||||
const resourcePath = resourceAsPath(resource);
|
|
||||||
const tags = state.groups[resourcePath].tags;
|
|
||||||
const tagAccessors =
|
|
||||||
'app' in tag ? [tag.app, tag.resource, tag.tag] : ['role', tag.tag];
|
|
||||||
const tagged = _.get(tags, tagAccessors, new Set());
|
|
||||||
|
|
||||||
if (!tagged) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const ship of ships) {
|
|
||||||
tagged.delete(ship);
|
|
||||||
}
|
|
||||||
_.set(tags, tagAccessors, tagged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
changePolicy(json: GroupUpdate, state: S) {
|
|
||||||
if ('changePolicy' in json && state) {
|
|
||||||
const { resource, diff } = json.changePolicy;
|
|
||||||
const resourcePath = resourceAsPath(resource);
|
|
||||||
const policy = state.groups[resourcePath].policy;
|
|
||||||
if ('open' in policy && 'open' in diff) {
|
|
||||||
this.openChangePolicy(diff.open, policy);
|
|
||||||
} else if ('invite' in policy && 'invite' in diff) {
|
|
||||||
this.inviteChangePolicy(diff.invite, policy);
|
|
||||||
} else if ('replace' in diff) {
|
|
||||||
state.groups[resourcePath].policy = diff.replace;
|
|
||||||
} else {
|
|
||||||
console.log('bad policy diff');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
expose(json: GroupUpdate, state: S) {
|
|
||||||
if( 'expose' in json && state) {
|
|
||||||
const { resource } = json.expose;
|
|
||||||
const resourcePath = resourceAsPath(resource);
|
|
||||||
state.groups[resourcePath].hidden = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private inviteChangePolicy(diff: InvitePolicyDiff, policy: InvitePolicy) {
|
|
||||||
if ('addInvites' in diff) {
|
|
||||||
const { addInvites } = diff;
|
|
||||||
for (const ship of addInvites) {
|
|
||||||
policy.invite.pending.add(ship);
|
|
||||||
}
|
|
||||||
} else if ('removeInvites' in diff) {
|
|
||||||
const { removeInvites } = diff;
|
|
||||||
for (const ship of removeInvites) {
|
|
||||||
policy.invite.pending.delete(ship);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
console.log('bad policy change');
|
console.log('bad policy diff');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
private openChangePolicy(diff: OpenPolicyDiff, policy: OpenPolicy) {
|
const expose = (json: GroupUpdate, state: GroupState): GroupState => {
|
||||||
if ('allowRanks' in diff) {
|
if( 'expose' in json && state) {
|
||||||
const { allowRanks } = diff;
|
const { resource } = json.expose;
|
||||||
for (const rank of allowRanks) {
|
const resourcePath = resourceAsPath(resource);
|
||||||
policy.open.banRanks.delete(rank);
|
state.groups[resourcePath].hidden = false;
|
||||||
}
|
}
|
||||||
} else if ('banRanks' in diff) {
|
return state;
|
||||||
const { banRanks } = diff;
|
}
|
||||||
for (const rank of banRanks) {
|
|
||||||
policy.open.banRanks.delete(rank);
|
const inviteChangePolicy = (diff: InvitePolicyDiff, policy: InvitePolicy) => {
|
||||||
}
|
if ('addInvites' in diff) {
|
||||||
} else if ('allowShips' in diff) {
|
const { addInvites } = diff;
|
||||||
console.log('allowing ships');
|
for (const ship of addInvites) {
|
||||||
const { allowShips } = diff;
|
policy.invite.pending.add(ship);
|
||||||
for (const ship of allowShips) {
|
|
||||||
policy.open.banned.delete(ship);
|
|
||||||
}
|
|
||||||
} else if ('banShips' in diff) {
|
|
||||||
console.log('banning ships');
|
|
||||||
const { banShips } = diff;
|
|
||||||
for (const ship of banShips) {
|
|
||||||
policy.open.banned.add(ship);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('bad policy change');
|
|
||||||
}
|
}
|
||||||
|
} else if ('removeInvites' in diff) {
|
||||||
|
const { removeInvites } = diff;
|
||||||
|
for (const ship of removeInvites) {
|
||||||
|
policy.invite.pending.delete(ship);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('bad policy change');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openChangePolicy = (diff: OpenPolicyDiff, policy: OpenPolicy) => {
|
||||||
|
if ('allowRanks' in diff) {
|
||||||
|
const { allowRanks } = diff;
|
||||||
|
for (const rank of allowRanks) {
|
||||||
|
policy.open.banRanks.delete(rank);
|
||||||
|
}
|
||||||
|
} else if ('banRanks' in diff) {
|
||||||
|
const { banRanks } = diff;
|
||||||
|
for (const rank of banRanks) {
|
||||||
|
policy.open.banRanks.delete(rank);
|
||||||
|
}
|
||||||
|
} else if ('allowShips' in diff) {
|
||||||
|
const { allowShips } = diff;
|
||||||
|
for (const ship of allowShips) {
|
||||||
|
policy.open.banned.delete(ship);
|
||||||
|
}
|
||||||
|
} else if ('banShips' in diff) {
|
||||||
|
const { banShips } = diff;
|
||||||
|
for (const ship of banShips) {
|
||||||
|
policy.open.banned.add(ship);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('bad policy change');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
|
import { GroupUpdate } from '@urbit/api/groups';
|
||||||
import { resourceAsPath } from '~/logic/lib/util';
|
import { resourceAsPath } from '~/logic/lib/util';
|
||||||
|
import { reduceState } from '../state/base';
|
||||||
|
import useGroupState, { GroupState } from '../state/group';
|
||||||
|
|
||||||
const initial = (json: any, state: any) => {
|
const initial = (json: any, state: GroupState): GroupState => {
|
||||||
const data = json.initial;
|
const data = json.initial;
|
||||||
if(data) {
|
if(data) {
|
||||||
state.pendingJoin = data;
|
state.pendingJoin = data;
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
};
|
};
|
||||||
|
|
||||||
const progress = (json: any, state: any) => {
|
const progress = (json: any, state: GroupState): GroupState => {
|
||||||
const data = json.progress;
|
const data = json.progress;
|
||||||
if(data) {
|
if(data) {
|
||||||
const { progress, resource } = data;
|
const { progress, resource } = data;
|
||||||
@ -18,12 +22,15 @@ const progress = (json: any, state: any) => {
|
|||||||
}, 10000);
|
}, 10000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GroupViewReducer = (json: any, state: any) => {
|
export const GroupViewReducer = (json: any) => {
|
||||||
const data = json['group-view-update'];
|
const data = json['group-view-update'];
|
||||||
if(data) {
|
if (data) {
|
||||||
progress(data, state);
|
reduceState<GroupState, GroupUpdate>(useGroupState, data, [
|
||||||
initial(data, state);
|
progress,
|
||||||
|
initial
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
};
|
};
|
@ -3,51 +3,94 @@ import {
|
|||||||
NotifIndex,
|
NotifIndex,
|
||||||
NotificationGraphConfig,
|
NotificationGraphConfig,
|
||||||
GroupNotificationsConfig,
|
GroupNotificationsConfig,
|
||||||
UnreadStats
|
UnreadStats,
|
||||||
|
Timebox
|
||||||
} from '@urbit/api';
|
} from '@urbit/api';
|
||||||
import { makePatDa } from '~/logic/lib/util';
|
import { makePatDa } from '~/logic/lib/util';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { StoreState } from '../store/type';
|
|
||||||
import { BigIntOrderedMap } from '../lib/BigIntOrderedMap';
|
import { BigIntOrderedMap } from '../lib/BigIntOrderedMap';
|
||||||
|
import useHarkState, { HarkState } from '../state/hark';
|
||||||
|
import { compose } from 'lodash/fp';
|
||||||
|
import { reduceState } from '../state/base';
|
||||||
|
import bigInt, {BigInteger} from 'big-integer';
|
||||||
|
|
||||||
type HarkState = Pick<StoreState, 'notifications' | 'notificationsGraphConfig' | 'notificationsGroupConfig' | 'unreads' >;
|
export const HarkReducer = (json: any) => {
|
||||||
|
|
||||||
export const HarkReducer = (json: any, state: HarkState) => {
|
|
||||||
const data = _.get(json, 'harkUpdate', false);
|
const data = _.get(json, 'harkUpdate', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
reduce(data, state);
|
reduce(data);
|
||||||
}
|
}
|
||||||
const graphHookData = _.get(json, 'hark-graph-hook-update', false);
|
const graphHookData = _.get(json, 'hark-graph-hook-update', false);
|
||||||
if (graphHookData) {
|
if (graphHookData) {
|
||||||
graphInitial(graphHookData, state);
|
reduceState<HarkState, any>(useHarkState, graphHookData, [
|
||||||
graphIgnore(graphHookData, state);
|
graphInitial,
|
||||||
graphListen(graphHookData, state);
|
graphIgnore,
|
||||||
graphWatchSelf(graphHookData, state);
|
graphListen,
|
||||||
graphMentions(graphHookData, state);
|
graphWatchSelf,
|
||||||
|
graphMentions,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
const groupHookData = _.get(json, 'hark-group-hook-update', false);
|
const groupHookData = _.get(json, 'hark-group-hook-update', false);
|
||||||
if (groupHookData) {
|
if (groupHookData) {
|
||||||
groupInitial(groupHookData, state);
|
reduceState<HarkState, any>(useHarkState, groupHookData, [
|
||||||
groupListen(groupHookData, state);
|
groupInitial,
|
||||||
groupIgnore(groupHookData, state);
|
groupListen,
|
||||||
|
groupIgnore,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function groupInitial(json: any, state: HarkState) {
|
function reduce(data) {
|
||||||
|
reduceState<HarkState, any>(useHarkState, data, [
|
||||||
|
unread,
|
||||||
|
read,
|
||||||
|
archive,
|
||||||
|
timebox,
|
||||||
|
more,
|
||||||
|
dnd,
|
||||||
|
added,
|
||||||
|
unreads,
|
||||||
|
readEach,
|
||||||
|
readSince,
|
||||||
|
unreadSince,
|
||||||
|
unreadEach,
|
||||||
|
seenIndex,
|
||||||
|
removeGraph,
|
||||||
|
readAll,
|
||||||
|
calculateCount
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateCount(json: any, state: HarkState) {
|
||||||
|
let count = 0;
|
||||||
|
_.forEach(state.unreads.graph, (graphs) => {
|
||||||
|
_.forEach(graphs, graph => {
|
||||||
|
count += (graph?.notifications || []).length;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
_.forEach(state.unreads.group, group => {
|
||||||
|
count += (group?.notifications || []).length;
|
||||||
|
})
|
||||||
|
state.notificationsCount = count;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupInitial(json: any, state: HarkState): HarkState {
|
||||||
const data = _.get(json, 'initial', false);
|
const data = _.get(json, 'initial', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
state.notificationsGroupConfig = data;
|
state.notificationsGroupConfig = data;
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
function graphInitial(json: any, state: HarkState) {
|
function graphInitial(json: any, state: HarkState): HarkState {
|
||||||
const data = _.get(json, 'initial', false);
|
const data = _.get(json, 'initial', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
state.notificationsGraphConfig = data;
|
state.notificationsGraphConfig = data;
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
function graphListen(json: any, state: HarkState) {
|
function graphListen(json: any, state: HarkState): HarkState {
|
||||||
const data = _.get(json, 'listen', false);
|
const data = _.get(json, 'listen', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
state.notificationsGraphConfig.watching = [
|
state.notificationsGraphConfig.watching = [
|
||||||
@ -55,134 +98,133 @@ function graphListen(json: any, state: HarkState) {
|
|||||||
data
|
data
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
function graphIgnore(json: any, state: HarkState) {
|
function graphIgnore(json: any, state: HarkState): HarkState {
|
||||||
const data = _.get(json, 'ignore', false);
|
const data = _.get(json, 'ignore', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
state.notificationsGraphConfig.watching = state.notificationsGraphConfig.watching.filter(
|
state.notificationsGraphConfig.watching = state.notificationsGraphConfig.watching.filter(
|
||||||
({ graph, index }) => !(graph === data.graph && index === data.index)
|
({ graph, index }) => !(graph === data.graph && index === data.index)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
function groupListen(json: any, state: HarkState) {
|
function groupListen(json: any, state: HarkState): HarkState {
|
||||||
const data = _.get(json, 'listen', false);
|
const data = _.get(json, 'listen', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
state.notificationsGroupConfig = [...state.notificationsGroupConfig, data];
|
state.notificationsGroupConfig = [...state.notificationsGroupConfig, data];
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
function groupIgnore(json: any, state: HarkState) {
|
function groupIgnore(json: any, state: HarkState): HarkState {
|
||||||
const data = _.get(json, 'ignore', false);
|
const data = _.get(json, 'ignore', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
state.notificationsGroupConfig = state.notificationsGroupConfig.filter(
|
state.notificationsGroupConfig = state.notificationsGroupConfig.filter(
|
||||||
n => n !== data
|
n => n !== data
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
function graphMentions(json: any, state: HarkState) {
|
function graphMentions(json: any, state: HarkState): HarkState {
|
||||||
const data = _.get(json, 'set-mentions', undefined);
|
const data = _.get(json, 'set-mentions', undefined);
|
||||||
if (!_.isUndefined(data)) {
|
if (!_.isUndefined(data)) {
|
||||||
state.notificationsGraphConfig.mentions = data;
|
state.notificationsGraphConfig.mentions = data;
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
function graphWatchSelf(json: any, state: HarkState) {
|
function graphWatchSelf(json: any, state: HarkState): HarkState {
|
||||||
const data = _.get(json, 'set-watch-on-self', undefined);
|
const data = _.get(json, 'set-watch-on-self', undefined);
|
||||||
if (!_.isUndefined(data)) {
|
if (!_.isUndefined(data)) {
|
||||||
state.notificationsGraphConfig.watchOnSelf = data;
|
state.notificationsGraphConfig.watchOnSelf = data;
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
function reduce(data: any, state: HarkState) {
|
function readAll(json: any, state: HarkState): HarkState {
|
||||||
unread(data, state);
|
|
||||||
read(data, state);
|
|
||||||
archive(data, state);
|
|
||||||
timebox(data, state);
|
|
||||||
more(data, state);
|
|
||||||
dnd(data, state);
|
|
||||||
added(data, state);
|
|
||||||
unreads(data, state);
|
|
||||||
readEach(data, state);
|
|
||||||
readSince(data, state);
|
|
||||||
unreadSince(data, state);
|
|
||||||
unreadEach(data, state);
|
|
||||||
seenIndex(data, state);
|
|
||||||
removeGraph(data, state);
|
|
||||||
readAll(data, state);
|
|
||||||
}
|
|
||||||
|
|
||||||
function readAll(json: any, state: HarkState) {
|
|
||||||
const data = _.get(json, 'read-all');
|
const data = _.get(json, 'read-all');
|
||||||
if(data) {
|
if(data) {
|
||||||
clearState(state);
|
state = clearState(state);
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeGraph(json: any, state: HarkState) {
|
function removeGraph(json: any, state: HarkState): HarkState {
|
||||||
const data = _.get(json, 'remove-graph');
|
const data = _.get(json, 'remove-graph');
|
||||||
if(data) {
|
if(data) {
|
||||||
delete state.unreads.graph[data];
|
delete state.unreads.graph[data];
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
function seenIndex(json: any, state: HarkState) {
|
function seenIndex(json: any, state: HarkState): HarkState {
|
||||||
const data = _.get(json, 'seen-index');
|
const data = _.get(json, 'seen-index');
|
||||||
if(data) {
|
if(data) {
|
||||||
updateNotificationStats(state, data.index, 'last', () => data.time);
|
state = updateNotificationStats(state, data.index, 'last', () => data.time);
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
function readEach(json: any, state: HarkState) {
|
function readEach(json: any, state: HarkState): HarkState {
|
||||||
const data = _.get(json, 'read-each');
|
const data = _.get(json, 'read-each');
|
||||||
if(data) {
|
if (data) {
|
||||||
updateUnreads(state, data.index, u => u.delete(data.target));
|
state = updateUnreads(state, data.index, u => u.delete(data.target));
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
function readSince(json: any, state: HarkState) {
|
function readSince(json: any, state: HarkState): HarkState {
|
||||||
const data = _.get(json, 'read-count');
|
const data = _.get(json, 'read-count');
|
||||||
if(data) {
|
if(data) {
|
||||||
updateUnreadCount(state, data, () => 0);
|
state = updateUnreadCount(state, data, () => 0);
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
function unreadSince(json: any, state: HarkState) {
|
function unreadSince(json: any, state: HarkState): HarkState {
|
||||||
const data = _.get(json, 'unread-count');
|
const data = _.get(json, 'unread-count');
|
||||||
if(data) {
|
if(data) {
|
||||||
updateUnreadCount(state, data.index, u => u + 1);
|
state = updateUnreadCount(state, data.index, u => u + 1);
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
function unreadEach(json: any, state: HarkState) {
|
function unreadEach(json: any, state: HarkState): HarkState {
|
||||||
const data = _.get(json, 'unread-each');
|
const data = _.get(json, 'unread-each');
|
||||||
if(data) {
|
if(data) {
|
||||||
updateUnreads(state, data.index, us => us.add(data.target));
|
state = updateUnreads(state, data.index, us => us.add(data.target));
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
function unreads(json: any, state: HarkState) {
|
function unreads(json: any, state: HarkState): HarkState {
|
||||||
const data = _.get(json, 'unreads');
|
const data = _.get(json, 'unreads');
|
||||||
if(data) {
|
if(data) {
|
||||||
clearState(state);
|
state = clearState(state);
|
||||||
data.forEach(({ index, stats }) => {
|
data.forEach(({ index, stats }) => {
|
||||||
const { unreads, notifications, last } = stats;
|
const { unreads, notifications, last } = stats;
|
||||||
updateNotificationStats(state, index, 'notifications', x => x + notifications);
|
|
||||||
updateNotificationStats(state, index, 'last', () => last);
|
updateNotificationStats(state, index, 'last', () => last);
|
||||||
|
_.each(notifications, ({ time, index }) => {
|
||||||
|
addNotificationToUnread(state, index, makePatDa(time));
|
||||||
|
});
|
||||||
if('count' in unreads) {
|
if('count' in unreads) {
|
||||||
updateUnreadCount(state, index, (u = 0) => u + unreads.count);
|
state = updateUnreadCount(state, index, (u = 0) => u + unreads.count);
|
||||||
} else {
|
} else {
|
||||||
|
state = updateUnreads(state, index, s => new Set());
|
||||||
unreads.each.forEach((u: string) => {
|
unreads.each.forEach((u: string) => {
|
||||||
updateUnreads(state, index, s => s.add(u));
|
state = updateUnreads(state, index, s => s.add(u));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearState(state) {
|
function clearState(state: HarkState): HarkState {
|
||||||
const initialState = {
|
const initialState = {
|
||||||
notifications: new BigIntOrderedMap<Timebox>(),
|
notifications: new BigIntOrderedMap<Timebox>(),
|
||||||
archivedNotifications: new BigIntOrderedMap<Timebox>(),
|
archivedNotifications: new BigIntOrderedMap<Timebox>(),
|
||||||
@ -202,73 +244,110 @@ function clearState(state) {
|
|||||||
Object.keys(initialState).forEach((key) => {
|
Object.keys(initialState).forEach((key) => {
|
||||||
state[key] = initialState[key];
|
state[key] = initialState[key];
|
||||||
});
|
});
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateUnreadCount(state: HarkState, index: NotifIndex, count: (c: number) => number) {
|
function updateUnreadCount(state: HarkState, index: NotifIndex, count: (c: number) => number): HarkState {
|
||||||
if(!('graph' in index)) {
|
if(!('graph' in index)) {
|
||||||
return;
|
return state;
|
||||||
}
|
}
|
||||||
const property = [index.graph.graph, index.graph.index, 'unreads'];
|
const property = [index.graph.graph, index.graph.index, 'unreads'];
|
||||||
const curr = _.get(state.unreads.graph, property, 0);
|
const curr = _.get(state.unreads.graph, property, 0);
|
||||||
const newCount = count(curr);
|
const newCount = count(curr);
|
||||||
_.set(state.unreads.graph, property, newCount);
|
_.set(state.unreads.graph, property, newCount);
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateUnreads(state: HarkState, index: NotifIndex, f: (us: Set<string>) => void) {
|
function updateUnreads(state: HarkState, index: NotifIndex, f: (us: Set<string>) => void): HarkState {
|
||||||
if(!('graph' in index)) {
|
if(!('graph' in index)) {
|
||||||
return;
|
return state;
|
||||||
}
|
}
|
||||||
const unreads = _.get(state.unreads.graph, [index.graph.graph, index.graph.index, 'unreads'], new Set<string>());
|
let unreads = _.get(state.unreads.graph, [index.graph.graph, index.graph.index, 'unreads'], new Set<string>());
|
||||||
const oldSize = unreads.size;
|
|
||||||
f(unreads);
|
f(unreads);
|
||||||
const newSize = unreads.size;
|
|
||||||
_.set(state.unreads.graph, [index.graph.graph, index.graph.index, 'unreads'], unreads);
|
_.set(state.unreads.graph, [index.graph.graph, index.graph.index, 'unreads'], unreads);
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateNotificationStats(state: HarkState, index: NotifIndex, statField: 'notifications' | 'unreads' | 'last', f: (x: number) => number) {
|
function addNotificationToUnread(state: HarkState, index: NotifIndex, time: BigInteger) {
|
||||||
if(statField === 'notifications') {
|
if('graph' in index) {
|
||||||
state.notificationsCount = f(state.notificationsCount);
|
const path = [index.graph.graph, index.graph.index, 'notifications'];
|
||||||
}
|
const curr = _.get(state.unreads.graph, path, []);
|
||||||
|
_.set(state.unreads.graph, path,
|
||||||
|
[
|
||||||
|
...curr.filter(c => !(c.time.eq(time) && notifIdxEqual(c.index, index))),
|
||||||
|
{ time, index}
|
||||||
|
]
|
||||||
|
);
|
||||||
|
} else if ('group' in index) {
|
||||||
|
const path = [index.group.group, 'notifications'];
|
||||||
|
const curr = _.get(state.unreads.group, path, []);
|
||||||
|
_.set(state.unreads.group, path,
|
||||||
|
[
|
||||||
|
...curr.filter(c => !(c.time.eq(time) && notifIdxEqual(c.index, index))),
|
||||||
|
{ time, index}
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeNotificationFromUnread(state: HarkState, index: NotifIndex, time: BigInteger) {
|
||||||
|
if('graph' in index) {
|
||||||
|
const path = [index.graph.graph, index.graph.index, 'notifications'];
|
||||||
|
const curr = _.get(state.unreads.graph, path, []);
|
||||||
|
_.set(state.unreads.graph, path,
|
||||||
|
curr.filter(c => !(c.time.eq(time) && notifIdxEqual(c.index, index))),
|
||||||
|
);
|
||||||
|
} else if ('group' in index) {
|
||||||
|
const path = [index.group.group, 'notifications'];
|
||||||
|
const curr = _.get(state.unreads.group, path, []);
|
||||||
|
_.set(state.unreads.group, path,
|
||||||
|
curr.filter(c => !(c.time.eq(time) && notifIdxEqual(c.index, index))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateNotificationStats(state: HarkState, index: NotifIndex, statField: 'unreads' | 'last', f: (x: number) => number) {
|
||||||
|
|
||||||
if('graph' in index) {
|
if('graph' in index) {
|
||||||
const curr = _.get(state.unreads.graph, [index.graph.graph, index.graph.index, statField], 0);
|
const curr = _.get(state.unreads.graph, [index.graph.graph, index.graph.index, statField], 0);
|
||||||
_.set(state.unreads.graph, [index.graph.graph, index.graph.index, statField], f(curr));
|
_.set(state.unreads.graph, [index.graph.graph, index.graph.index, statField], f(curr));
|
||||||
} else if('group' in index) {
|
} else if('group' in index) {
|
||||||
const curr = _.get(state.unreads.group, [index.group, statField], 0);
|
const curr = _.get(state.unreads.group, [index.group.group, statField], 0);
|
||||||
_.set(state.unreads.group, [index.group, statField], f(curr));
|
_.set(state.unreads.group, [index.group.group, statField], f(curr));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function added(json: any, state: HarkState) {
|
function added(json: any, state: HarkState): HarkState {
|
||||||
const data = _.get(json, 'added', false);
|
const data = _.get(json, 'added', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
const { index, notification } = data;
|
const { index, notification } = data;
|
||||||
const time = makePatDa(data.time);
|
const time = makePatDa(data.time);
|
||||||
const timebox = state.notifications.get(time) || [];
|
const timebox = state.notifications.get(time) || [];
|
||||||
|
addNotificationToUnread(state, index, time);
|
||||||
|
|
||||||
const arrIdx = timebox.findIndex(idxNotif =>
|
const arrIdx = timebox.findIndex(idxNotif =>
|
||||||
notifIdxEqual(index, idxNotif.index)
|
notifIdxEqual(index, idxNotif.index)
|
||||||
);
|
);
|
||||||
if (arrIdx !== -1) {
|
if (arrIdx !== -1) {
|
||||||
if(timebox[arrIdx]?.notification?.read) {
|
|
||||||
updateNotificationStats(state, index, 'notifications', x => x+1);
|
|
||||||
}
|
|
||||||
timebox[arrIdx] = { index, notification };
|
timebox[arrIdx] = { index, notification };
|
||||||
state.notifications.set(time, timebox);
|
state.notifications.set(time, timebox);
|
||||||
} else {
|
} else {
|
||||||
updateNotificationStats(state, index, 'notifications', x => x+1);
|
|
||||||
state.notifications.set(time, [...timebox, { index, notification }]);
|
state.notifications.set(time, [...timebox, { index, notification }]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dnd = (json: any, state: HarkState) => {
|
const dnd = (json: any, state: HarkState): HarkState => {
|
||||||
const data = _.get(json, 'set-dnd', undefined);
|
const data = _.get(json, 'set-dnd', undefined);
|
||||||
if (!_.isUndefined(data)) {
|
if (!_.isUndefined(data)) {
|
||||||
state.doNotDisturb = data;
|
state.doNotDisturb = data;
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
};
|
};
|
||||||
|
|
||||||
const timebox = (json: any, state: HarkState) => {
|
const timebox = (json: any, state: HarkState): HarkState => {
|
||||||
const data = _.get(json, 'timebox', false);
|
const data = _.get(json, 'timebox', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
const time = makePatDa(data.time);
|
const time = makePatDa(data.time);
|
||||||
@ -276,13 +355,15 @@ const timebox = (json: any, state: HarkState) => {
|
|||||||
state.notifications.set(time, data.notifications);
|
state.notifications.set(time, data.notifications);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
};
|
};
|
||||||
|
|
||||||
function more(json: any, state: HarkState) {
|
function more(json: any, state: HarkState): HarkState {
|
||||||
const data = _.get(json, 'more', false);
|
const data = _.get(json, 'more', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
_.forEach(data, d => reduce(d, state));
|
_.forEach(data, d => reduce(d));
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
function notifIdxEqual(a: NotifIndex, b: NotifIndex) {
|
function notifIdxEqual(a: NotifIndex, b: NotifIndex) {
|
||||||
@ -307,51 +388,55 @@ function setRead(
|
|||||||
index: NotifIndex,
|
index: NotifIndex,
|
||||||
read: boolean,
|
read: boolean,
|
||||||
state: HarkState
|
state: HarkState
|
||||||
) {
|
): HarkState {
|
||||||
const patDa = makePatDa(time);
|
const patDa = makePatDa(time);
|
||||||
const timebox = state.notifications.get(patDa);
|
const timebox = state.notifications.get(patDa);
|
||||||
if (_.isNull(timebox)) {
|
if (_.isNull(timebox)) {
|
||||||
console.warn('Modifying nonexistent timebox');
|
console.warn('Modifying nonexistent timebox');
|
||||||
return;
|
return state;
|
||||||
}
|
}
|
||||||
const arrIdx = timebox.findIndex(idxNotif =>
|
const arrIdx = timebox.findIndex(idxNotif =>
|
||||||
notifIdxEqual(index, idxNotif.index)
|
notifIdxEqual(index, idxNotif.index)
|
||||||
);
|
);
|
||||||
if (arrIdx === -1) {
|
if (arrIdx === -1) {
|
||||||
console.warn('Modifying nonexistent index');
|
console.warn('Modifying nonexistent index');
|
||||||
return;
|
return state;
|
||||||
}
|
}
|
||||||
timebox[arrIdx].notification.read = read;
|
timebox[arrIdx].notification.read = read;
|
||||||
state.notifications.set(patDa, timebox);
|
state.notifications.set(patDa, timebox);
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
function read(json: any, state: HarkState) {
|
function read(json: any, state: HarkState): HarkState {
|
||||||
const data = _.get(json, 'read-note', false);
|
const data = _.get(json, 'read-note', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
const { time, index } = data;
|
const { time, index } = data;
|
||||||
updateNotificationStats(state, index, 'notifications', x => x-1);
|
removeNotificationFromUnread(state, index, makePatDa(time));
|
||||||
setRead(time, index, true, state);
|
setRead(time, index, true, state);
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
function unread(json: any, state: HarkState) {
|
function unread(json: any, state: HarkState): HarkState {
|
||||||
const data = _.get(json, 'unread-note', false);
|
const data = _.get(json, 'unread-note', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
const { time, index } = data;
|
const { time, index } = data;
|
||||||
updateNotificationStats(state, index, 'notifications', x => x+1);
|
addNotificationToUnread(state, index, makePatDa(time));
|
||||||
setRead(time, index, false, state);
|
setRead(time, index, false, state);
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
function archive(json: any, state: HarkState) {
|
function archive(json: any, state: HarkState): HarkState {
|
||||||
const data = _.get(json, 'archive', false);
|
const data = _.get(json, 'archive', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
const { index } = data;
|
const { index } = data;
|
||||||
|
removeNotificationFromUnread(state, index, makePatDa(data.time))
|
||||||
const time = makePatDa(data.time);
|
const time = makePatDa(data.time);
|
||||||
const timebox = state.notifications.get(time);
|
const timebox = state.notifications.get(time);
|
||||||
if (!timebox) {
|
if (!timebox) {
|
||||||
console.warn('Modifying nonexistent timebox');
|
console.warn('Modifying nonexistent timebox');
|
||||||
return;
|
return state;
|
||||||
}
|
}
|
||||||
const [archived, unarchived] = _.partition(timebox, idxNotif =>
|
const [archived, unarchived] = _.partition(timebox, idxNotif =>
|
||||||
notifIdxEqual(index, idxNotif.index)
|
notifIdxEqual(index, idxNotif.index)
|
||||||
@ -362,7 +447,6 @@ function archive(json: any, state: HarkState) {
|
|||||||
} else {
|
} else {
|
||||||
state.notifications.set(time, unarchived);
|
state.notifications.set(time, unarchived);
|
||||||
}
|
}
|
||||||
const newlyRead = archived.filter(x => !x.notification.read).length;
|
|
||||||
updateNotificationStats(state, index, 'notifications', x => x - newlyRead);
|
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
@ -1,62 +1,72 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { StoreState } from '../../store/type';
|
import { compose } from 'lodash/fp';
|
||||||
import { Cage } from '~/types/cage';
|
|
||||||
import { InviteUpdate } from '@urbit/api/invite';
|
import { InviteUpdate } from '@urbit/api/invite';
|
||||||
|
|
||||||
type InviteState = Pick<StoreState, 'invites'>;
|
import { Cage } from '~/types/cage';
|
||||||
|
import useInviteState, { InviteState } from '../state/invite';
|
||||||
|
import { reduceState } from '../state/base';
|
||||||
|
|
||||||
export default class InviteReducer<S extends InviteState> {
|
export default class InviteReducer {
|
||||||
reduce(json: Cage, state: S) {
|
reduce(json: Cage) {
|
||||||
const data = json['invite-update'];
|
const data = json['invite-update'];
|
||||||
if (data) {
|
if (data) {
|
||||||
this.initial(data, state);
|
reduceState<InviteState, InviteUpdate>(useInviteState, data, [
|
||||||
this.create(data, state);
|
initial,
|
||||||
this.delete(data, state);
|
create,
|
||||||
this.invite(data, state);
|
deleteInvite,
|
||||||
this.accepted(data, state);
|
invite,
|
||||||
this.decline(data, state);
|
accepted,
|
||||||
}
|
decline,
|
||||||
}
|
]);
|
||||||
|
|
||||||
initial(json: InviteUpdate, state: S) {
|
|
||||||
const data = _.get(json, 'initial', false);
|
|
||||||
if (data) {
|
|
||||||
state.invites = data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
create(json: InviteUpdate, state: S) {
|
|
||||||
const data = _.get(json, 'create', false);
|
|
||||||
if (data) {
|
|
||||||
state.invites[data] = {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(json: InviteUpdate, state: S) {
|
|
||||||
const data = _.get(json, 'delete', false);
|
|
||||||
if (data) {
|
|
||||||
delete state.invites[data];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
invite(json: InviteUpdate, state: S) {
|
|
||||||
const data = _.get(json, 'invite', false);
|
|
||||||
if (data) {
|
|
||||||
state.invites[data.term][data.uid] = data.invite;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
accepted(json: InviteUpdate, state: S) {
|
|
||||||
const data = _.get(json, 'accepted', false);
|
|
||||||
if (data) {
|
|
||||||
delete state.invites[data.term][data.uid];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
decline(json: InviteUpdate, state: S) {
|
|
||||||
const data = _.get(json, 'decline', false);
|
|
||||||
if (data) {
|
|
||||||
delete state.invites[data.term][data.uid];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const initial = (json: InviteUpdate, state: InviteState): InviteState => {
|
||||||
|
const data = _.get(json, 'initial', false);
|
||||||
|
if (data) {
|
||||||
|
state.invites = data;
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const create = (json: InviteUpdate, state: InviteState): InviteState => {
|
||||||
|
const data = _.get(json, 'create', false);
|
||||||
|
if (data) {
|
||||||
|
state.invites[data] = {};
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteInvite = (json: InviteUpdate, state: InviteState): InviteState => {
|
||||||
|
const data = _.get(json, 'delete', false);
|
||||||
|
if (data) {
|
||||||
|
delete state.invites[data];
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const invite = (json: InviteUpdate, state: InviteState): InviteState => {
|
||||||
|
const data = _.get(json, 'invite', false);
|
||||||
|
if (data) {
|
||||||
|
state.invites[data.term][data.uid] = data.invite;
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const accepted = (json: InviteUpdate, state: InviteState): InviteState => {
|
||||||
|
const data = _.get(json, 'accepted', false);
|
||||||
|
if (data) {
|
||||||
|
delete state.invites[data.term][data.uid];
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const decline = (json: InviteUpdate, state: InviteState): InviteState => {
|
||||||
|
const data = _.get(json, 'decline', false);
|
||||||
|
if (data) {
|
||||||
|
delete state.invites[data.term][data.uid];
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
@ -1,61 +1,79 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { LaunchUpdate } from '~/types/launch-update';
|
import { LaunchUpdate, WeatherState } from '~/types/launch-update';
|
||||||
import { Cage } from '~/types/cage';
|
import { Cage } from '~/types/cage';
|
||||||
import { StoreState } from '../../store/type';
|
import useLaunchState, { LaunchState } from '../state/launch';
|
||||||
|
import { compose } from 'lodash/fp';
|
||||||
|
import { reduceState } from '../state/base';
|
||||||
|
|
||||||
type LaunchState = Pick<StoreState, 'launch' | 'weather' | 'userLocation'>;
|
export default class LaunchReducer {
|
||||||
|
reduce(json: Cage) {
|
||||||
export default class LaunchReducer<S extends LaunchState> {
|
|
||||||
reduce(json: Cage, state: S) {
|
|
||||||
const data = _.get(json, 'launch-update', false);
|
const data = _.get(json, 'launch-update', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
this.initial(data, state);
|
reduceState<LaunchState, LaunchUpdate>(useLaunchState, data, [
|
||||||
this.changeFirstTime(data, state);
|
initial,
|
||||||
this.changeOrder(data, state);
|
changeFirstTime,
|
||||||
this.changeFirstTime(data, state);
|
changeOrder,
|
||||||
this.changeIsShown(data, state);
|
changeFirstTime,
|
||||||
|
changeIsShown,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const weatherData = _.get(json, 'weather', false);
|
const weatherData: WeatherState = _.get(json, 'weather', false);
|
||||||
if (weatherData) {
|
if (weatherData) {
|
||||||
state.weather = weatherData;
|
useLaunchState.getState().set(state => {
|
||||||
|
state.weather = weatherData;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const locationData = _.get(json, 'location', false);
|
const locationData = _.get(json, 'location', false);
|
||||||
if (locationData) {
|
if (locationData) {
|
||||||
state.userLocation = locationData;
|
useLaunchState.getState().set(state => {
|
||||||
|
state.userLocation = locationData;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
initial(json: LaunchUpdate, state: S) {
|
const baseHash = _.get(json, 'baseHash', false);
|
||||||
const data = _.get(json, 'initial', false);
|
if (baseHash) {
|
||||||
if (data) {
|
useLaunchState.getState().set(state => {
|
||||||
state.launch = data;
|
state.baseHash = baseHash;
|
||||||
}
|
})
|
||||||
}
|
|
||||||
|
|
||||||
changeFirstTime(json: LaunchUpdate, state: S) {
|
|
||||||
const data = _.get(json, 'changeFirstTime', false);
|
|
||||||
if (data) {
|
|
||||||
state.launch.firstTime = data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
changeOrder(json: LaunchUpdate, state: S) {
|
|
||||||
const data = _.get(json, 'changeOrder', false);
|
|
||||||
if (data) {
|
|
||||||
state.launch.tileOrdering = data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
changeIsShown(json: LaunchUpdate, state: S) {
|
|
||||||
const data = _.get(json, 'changeIsShown', false);
|
|
||||||
if (data) {
|
|
||||||
const tile = state.launch.tiles[data.name];
|
|
||||||
console.log(tile);
|
|
||||||
if (tile) {
|
|
||||||
tile.isShown = data.isShown;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const initial = (json: LaunchUpdate, state: LaunchState): LaunchState => {
|
||||||
|
const data = _.get(json, 'initial', false);
|
||||||
|
if (data) {
|
||||||
|
Object.keys(data).forEach(key => {
|
||||||
|
state[key] = data[key];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const changeFirstTime = (json: LaunchUpdate, state: LaunchState): LaunchState => {
|
||||||
|
const data = _.get(json, 'changeFirstTime', false);
|
||||||
|
if (data) {
|
||||||
|
state.firstTime = data;
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const changeOrder = (json: LaunchUpdate, state: LaunchState): LaunchState => {
|
||||||
|
const data = _.get(json, 'changeOrder', false);
|
||||||
|
if (data) {
|
||||||
|
state.tileOrdering = data;
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const changeIsShown = (json: LaunchUpdate, state: LaunchState): LaunchState => {
|
||||||
|
const data = _.get(json, 'changeIsShown', false);
|
||||||
|
if (data) {
|
||||||
|
const tile = state.tiles[data.name];
|
||||||
|
if (tile) {
|
||||||
|
tile.isShown = data.isShown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
@ -1,33 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import { StoreState } from '~/store/type';
|
|
||||||
import { Cage } from '~/types/cage';
|
|
||||||
import { LocalUpdate } from '~/types/local-update';
|
|
||||||
|
|
||||||
type LocalState = Pick<StoreState, 'baseHash'>;
|
|
||||||
|
|
||||||
export default class LocalReducer<S extends LocalState> {
|
|
||||||
rehydrate(state: S) {
|
|
||||||
try {
|
|
||||||
const json = JSON.parse(localStorage.getItem('localReducer') || '{}');
|
|
||||||
_.forIn(json, (value, key) => {
|
|
||||||
state[key] = value;
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Failed to rehydrate localStorage state', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dehydrate(state: S) {
|
|
||||||
}
|
|
||||||
reduce(json: Cage, state: S) {
|
|
||||||
const data = json['local'];
|
|
||||||
if (data) {
|
|
||||||
this.baseHash(data, state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
baseHash(obj: LocalUpdate, state: S) {
|
|
||||||
if ('baseHash' in obj) {
|
|
||||||
state.baseHash = obj.baseHash;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,103 +1,108 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import { compose } from 'lodash/fp';
|
||||||
import { StoreState } from '../../store/type';
|
|
||||||
|
|
||||||
import { MetadataUpdate } from '@urbit/api/metadata';
|
import { MetadataUpdate } from '@urbit/api/metadata';
|
||||||
|
|
||||||
import { Cage } from '~/types/cage';
|
import { Cage } from '~/types/cage';
|
||||||
|
import useMetadataState, { MetadataState } from '../state/metadata';
|
||||||
|
import { reduceState } from '../state/base';
|
||||||
|
|
||||||
type MetadataState = Pick<StoreState, 'associations'>;
|
export default class MetadataReducer {
|
||||||
|
reduce(json: Cage) {
|
||||||
export default class MetadataReducer<S extends MetadataState> {
|
|
||||||
reduce(json: Cage, state: S) {
|
|
||||||
const data = json['metadata-update'];
|
const data = json['metadata-update'];
|
||||||
if (data) {
|
if (data) {
|
||||||
console.log(data);
|
reduceState<MetadataState, MetadataUpdate>(useMetadataState, data, [
|
||||||
this.associations(data, state);
|
associations,
|
||||||
this.add(data, state);
|
add,
|
||||||
this.update(data, state);
|
update,
|
||||||
this.remove(data, state);
|
remove,
|
||||||
this.groupInitial(data, state);
|
groupInitial,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
groupInitial(json: MetadataUpdate, state: S) {
|
const groupInitial = (json: MetadataUpdate, state: MetadataState): MetadataState => {
|
||||||
const data = _.get(json, 'initial-group', false);
|
const data = _.get(json, 'initial-group', false);
|
||||||
console.log(data);
|
if(data) {
|
||||||
if(data) {
|
state = associations(data, state);
|
||||||
this.associations(data, state);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
associations(json: MetadataUpdate, state: S) {
|
const associations = (json: MetadataUpdate, state: MetadataState): MetadataState => {
|
||||||
const data = _.get(json, 'associations', false);
|
const data = _.get(json, 'associations', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
const metadata = state.associations;
|
const metadata = state.associations;
|
||||||
Object.keys(data).forEach((key) => {
|
Object.keys(data).forEach((key) => {
|
||||||
const val = data[key];
|
const val = data[key];
|
||||||
const appName = val['app-name'];
|
const appName = val['app-name'];
|
||||||
const rid = val.resource;
|
const rid = val.resource;
|
||||||
if (!(appName in metadata)) {
|
|
||||||
metadata[appName] = {};
|
|
||||||
}
|
|
||||||
if (!(rid in metadata[appName])) {
|
|
||||||
metadata[appName][rid] = {};
|
|
||||||
}
|
|
||||||
metadata[appName][rid] = val;
|
|
||||||
});
|
|
||||||
|
|
||||||
state.associations = metadata;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
add(json: MetadataUpdate, state: S) {
|
|
||||||
const data = _.get(json, 'add', false);
|
|
||||||
if (data) {
|
|
||||||
const metadata = state.associations;
|
|
||||||
const appName = data['app-name'];
|
|
||||||
const appPath = data.resource;
|
|
||||||
|
|
||||||
if (!(appName in metadata)) {
|
|
||||||
metadata[appName] = {};
|
|
||||||
}
|
|
||||||
if (!(appPath in metadata[appName])) {
|
|
||||||
metadata[appName][appPath] = {};
|
|
||||||
}
|
|
||||||
metadata[appName][appPath] = data;
|
|
||||||
|
|
||||||
state.associations = metadata;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
update(json: MetadataUpdate, state: S) {
|
|
||||||
const data = _.get(json, 'update-metadata', false);
|
|
||||||
if (data) {
|
|
||||||
const metadata = state.associations;
|
|
||||||
const appName = data['app-name'];
|
|
||||||
const rid = data.resource;
|
|
||||||
|
|
||||||
if (!(appName in metadata)) {
|
if (!(appName in metadata)) {
|
||||||
metadata[appName] = {};
|
metadata[appName] = {};
|
||||||
}
|
}
|
||||||
if (!(rid in metadata[appName])) {
|
if (!(rid in metadata[appName])) {
|
||||||
metadata[appName][rid] = {};
|
metadata[appName][rid] = {};
|
||||||
}
|
}
|
||||||
metadata[appName][rid] = data;
|
metadata[appName][rid] = val;
|
||||||
|
});
|
||||||
|
|
||||||
state.associations = metadata;
|
state.associations = metadata;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
remove(json: MetadataUpdate, state: S) {
|
|
||||||
const data = _.get(json, 'remove', false);
|
|
||||||
if (data) {
|
|
||||||
const metadata = state.associations;
|
|
||||||
const appName = data['app-name'];
|
|
||||||
const rid = data.resource;
|
|
||||||
|
|
||||||
if (appName in metadata && rid in metadata[appName]) {
|
|
||||||
delete metadata[appName][rid];
|
|
||||||
}
|
|
||||||
state.associations = metadata;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const add = (json: MetadataUpdate, state: MetadataState): MetadataState => {
|
||||||
|
const data = _.get(json, 'add', false);
|
||||||
|
if (data) {
|
||||||
|
const metadata = state.associations;
|
||||||
|
const appName = data['app-name'];
|
||||||
|
const appPath = data.resource;
|
||||||
|
|
||||||
|
if (!(appName in metadata)) {
|
||||||
|
metadata[appName] = {};
|
||||||
|
}
|
||||||
|
if (!(appPath in metadata[appName])) {
|
||||||
|
metadata[appName][appPath] = {};
|
||||||
|
}
|
||||||
|
metadata[appName][appPath] = data;
|
||||||
|
|
||||||
|
state.associations = metadata;
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const update = (json: MetadataUpdate, state: MetadataState): MetadataState => {
|
||||||
|
const data = _.get(json, 'update-metadata', false);
|
||||||
|
if (data) {
|
||||||
|
const metadata = state.associations;
|
||||||
|
const appName = data['app-name'];
|
||||||
|
const rid = data.resource;
|
||||||
|
|
||||||
|
if (!(appName in metadata)) {
|
||||||
|
metadata[appName] = {};
|
||||||
|
}
|
||||||
|
if (!(rid in metadata[appName])) {
|
||||||
|
metadata[appName][rid] = {};
|
||||||
|
}
|
||||||
|
metadata[appName][rid] = data;
|
||||||
|
|
||||||
|
state.associations = metadata;
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const remove = (json: MetadataUpdate, state: MetadataState): MetadataState => {
|
||||||
|
const data = _.get(json, 'remove', false);
|
||||||
|
if (data) {
|
||||||
|
const metadata = state.associations;
|
||||||
|
const appName = data['app-name'];
|
||||||
|
const rid = data.resource;
|
||||||
|
|
||||||
|
if (appName in metadata && rid in metadata[appName]) {
|
||||||
|
delete metadata[appName][rid];
|
||||||
|
}
|
||||||
|
state.associations = metadata;
|
||||||
|
}
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
@ -1,83 +1,93 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { StoreState } from '../../store/type';
|
import { compose } from 'lodash/fp';
|
||||||
import { Cage } from '~/types/cage';
|
import { Cage } from '~/types/cage';
|
||||||
import { S3Update } from '~/types/s3-update';
|
import { S3Update } from '~/types/s3-update';
|
||||||
|
import { reduceState } from '../state/base';
|
||||||
|
import useStorageState, { StorageState } from '../state/storage';
|
||||||
|
|
||||||
type S3State = Pick<StoreState, 's3'>;
|
|
||||||
|
|
||||||
export default class S3Reducer<S extends S3State> {
|
export default class S3Reducer {
|
||||||
reduce(json: Cage, state: S) {
|
reduce(json: Cage) {
|
||||||
const data = _.get(json, 's3-update', false);
|
const data = _.get(json, 's3-update', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
this.credentials(data, state);
|
reduceState<StorageState, S3Update>(useStorageState, data, [
|
||||||
this.configuration(data, state);
|
credentials,
|
||||||
this.currentBucket(data, state);
|
configuration,
|
||||||
this.addBucket(data, state);
|
currentBucket,
|
||||||
this.removeBucket(data, state);
|
addBucket,
|
||||||
this.endpoint(data, state);
|
removeBucket,
|
||||||
this.accessKeyId(data, state);
|
endpoint,
|
||||||
this.secretAccessKey(data, state);
|
accessKeyId,
|
||||||
}
|
secretAccessKey,
|
||||||
}
|
]);
|
||||||
|
|
||||||
credentials(json: S3Update, state: S) {
|
|
||||||
const data = _.get(json, 'credentials', false);
|
|
||||||
if (data) {
|
|
||||||
state.storage.s3.credentials = data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
configuration(json: S3Update, state: S) {
|
|
||||||
const data = _.get(json, 'configuration', false);
|
|
||||||
if (data) {
|
|
||||||
state.storage.s3.configuration = {
|
|
||||||
buckets: new Set(data.buckets),
|
|
||||||
currentBucket: data.currentBucket
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
currentBucket(json: S3Update, state: S) {
|
|
||||||
const data = _.get(json, 'setCurrentBucket', false);
|
|
||||||
if (data && state.storage.s3) {
|
|
||||||
state.storage.s3.configuration.currentBucket = data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addBucket(json: S3Update, state: S) {
|
|
||||||
const data = _.get(json, 'addBucket', false);
|
|
||||||
if (data) {
|
|
||||||
state.storage.s3.configuration.buckets =
|
|
||||||
state.storage.s3.configuration.buckets.add(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
removeBucket(json: S3Update, state: S) {
|
|
||||||
const data = _.get(json, 'removeBucket', false);
|
|
||||||
if (data) {
|
|
||||||
state.storage.s3.configuration.buckets.delete(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
endpoint(json: S3Update, state: S) {
|
|
||||||
const data = _.get(json, 'setEndpoint', false);
|
|
||||||
if (data && state.storage.s3.credentials) {
|
|
||||||
state.storage.s3.credentials.endpoint = data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
accessKeyId(json: S3Update , state: S) {
|
|
||||||
const data = _.get(json, 'setAccessKeyId', false);
|
|
||||||
if (data && state.storage.s3.credentials) {
|
|
||||||
state.storage.s3.credentials.accessKeyId = data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
secretAccessKey(json: S3Update, state: S) {
|
|
||||||
const data = _.get(json, 'setSecretAccessKey', false);
|
|
||||||
if (data && state.storage.s3.credentials) {
|
|
||||||
state.storage.s3.credentials.secretAccessKey = data;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const credentials = (json: S3Update, state: StorageState): StorageState => {
|
||||||
|
const data = _.get(json, 'credentials', false);
|
||||||
|
if (data) {
|
||||||
|
state.s3.credentials = data;
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const configuration = (json: S3Update, state: StorageState): StorageState => {
|
||||||
|
const data = _.get(json, 'configuration', false);
|
||||||
|
if (data) {
|
||||||
|
state.s3.configuration = {
|
||||||
|
buckets: new Set(data.buckets),
|
||||||
|
currentBucket: data.currentBucket
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentBucket = (json: S3Update, state: StorageState): StorageState => {
|
||||||
|
const data = _.get(json, 'setCurrentBucket', false);
|
||||||
|
if (data && state.s3) {
|
||||||
|
state.s3.configuration.currentBucket = data;
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const addBucket = (json: S3Update, state: StorageState): StorageState => {
|
||||||
|
const data = _.get(json, 'addBucket', false);
|
||||||
|
if (data) {
|
||||||
|
state.s3.configuration.buckets =
|
||||||
|
state.s3.configuration.buckets.add(data);
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeBucket = (json: S3Update, state: StorageState): StorageState => {
|
||||||
|
const data = _.get(json, 'removeBucket', false);
|
||||||
|
if (data) {
|
||||||
|
state.s3.configuration.buckets.delete(data);
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const endpoint = (json: S3Update, state: StorageState): StorageState => {
|
||||||
|
const data = _.get(json, 'setEndpoint', false);
|
||||||
|
if (data && state.s3.credentials) {
|
||||||
|
state.s3.credentials.endpoint = data;
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessKeyId = (json: S3Update , state: StorageState): StorageState => {
|
||||||
|
const data = _.get(json, 'setAccessKeyId', false);
|
||||||
|
if (data && state.s3.credentials) {
|
||||||
|
state.s3.credentials.accessKeyId = data;
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const secretAccessKey = (json: S3Update, state: StorageState): StorageState => {
|
||||||
|
const data = _.get(json, 'setSecretAccessKey', false);
|
||||||
|
if (data && state.s3.credentials) {
|
||||||
|
state.s3.credentials.secretAccessKey = data;
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
@ -1,46 +1,46 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { SettingsUpdate } from '~/types/settings';
|
import useSettingsState, { SettingsState } from "~/logic/state/settings";
|
||||||
import useSettingsState, { SettingsStateZus } from "~/logic/state/settings";
|
import { SettingsUpdate } from '@urbit/api/dist/settings';
|
||||||
import produce from 'immer';
|
import { reduceState } from '../state/base';
|
||||||
|
|
||||||
export default class SettingsStateZusettingsReducer{
|
export default class SettingsReducer {
|
||||||
reduce(json: any) {
|
reduce(json: any) {
|
||||||
const old = useSettingsState.getState();
|
let data = json["settings-event"];
|
||||||
const newState = produce(old, state => {
|
if (data) {
|
||||||
let data = json["settings-event"];
|
reduceState<SettingsState, SettingsUpdate>(useSettingsState, data, [
|
||||||
if (data) {
|
this.putBucket,
|
||||||
console.log(data);
|
this.delBucket,
|
||||||
this.putBucket(data, state);
|
this.putEntry,
|
||||||
this.delBucket(data, state);
|
this.delEntry,
|
||||||
this.putEntry(data, state);
|
]);
|
||||||
this.delEntry(data, state);
|
}
|
||||||
}
|
data = json["settings-data"];
|
||||||
data = json["settings-data"];
|
if (data) {
|
||||||
if (data) {
|
reduceState<SettingsState, SettingsUpdate>(useSettingsState, data, [
|
||||||
console.log(data);
|
this.getAll,
|
||||||
this.getAll(data, state);
|
this.getBucket,
|
||||||
this.getBucket(data, state);
|
this.getEntry,
|
||||||
this.getEntry(data, state);
|
]);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
useSettingsState.setState(newState);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
putBucket(json: SettingsUpdate, state: SettingsStateZus) {
|
putBucket(json: SettingsUpdate, state: SettingsState): SettingsState {
|
||||||
const data = _.get(json, 'put-bucket', false);
|
const data = _.get(json, 'put-bucket', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
state[data["bucket-key"]] = data.bucket;
|
state[data["bucket-key"]] = data.bucket;
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
delBucket(json: SettingsUpdate, state: SettingsStateZus) {
|
delBucket(json: SettingsUpdate, state: SettingsState): SettingsState {
|
||||||
const data = _.get(json, 'del-bucket', false);
|
const data = _.get(json, 'del-bucket', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
delete settings[data['bucket-key']];
|
delete state[data['bucket-key']];
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
putEntry(json: SettingsUpdate, state: SettingsStateZus) {
|
putEntry(json: SettingsUpdate, state: SettingsState): SettingsState {
|
||||||
const data = _.get(json, 'put-entry', false);
|
const data = _.get(json, 'put-entry', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
if (!state[data["bucket-key"]]) {
|
if (!state[data["bucket-key"]]) {
|
||||||
@ -48,36 +48,41 @@ export default class SettingsStateZusettingsReducer{
|
|||||||
}
|
}
|
||||||
state[data["bucket-key"]][data["entry-key"]] = data.value;
|
state[data["bucket-key"]][data["entry-key"]] = data.value;
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
delEntry(json: SettingsUpdate, state: SettingsStateZus) {
|
delEntry(json: SettingsUpdate, state: SettingsState): SettingsState {
|
||||||
const data = _.get(json, 'del-entry', false);
|
const data = _.get(json, 'del-entry', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
delete state[data["bucket-key"]][data["entry-key"]];
|
delete state[data["bucket-key"]][data["entry-key"]];
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
getAll(json: any, state: SettingsStateZus) {
|
getAll(json: any, state: SettingsState): SettingsState {
|
||||||
const data = _.get(json, 'all');
|
const data = _.get(json, 'all');
|
||||||
if(data) {
|
if(data) {
|
||||||
_.mergeWith(state, data, (obj, src) => _.isArray(src) ? src : undefined)
|
_.mergeWith(state, data, (obj, src) => _.isArray(src) ? src : undefined)
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
getBucket(json: any, state: SettingsStateZus) {
|
getBucket(json: any, state: SettingsState): SettingsState {
|
||||||
const key = _.get(json, 'bucket-key', false);
|
const key = _.get(json, 'bucket-key', false);
|
||||||
const bucket = _.get(json, 'bucket', false);
|
const bucket = _.get(json, 'bucket', false);
|
||||||
if (key && bucket) {
|
if (key && bucket) {
|
||||||
state[key] = bucket;
|
state[key] = bucket;
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
getEntry(json: any, state: SettingsStateZus) {
|
getEntry(json: any, state: SettingsState) {
|
||||||
const bucketKey = _.get(json, 'bucket-key', false);
|
const bucketKey = _.get(json, 'bucket-key', false);
|
||||||
const entryKey = _.get(json, 'entry-key', false);
|
const entryKey = _.get(json, 'entry-key', false);
|
||||||
const entry = _.get(json, 'entry', false);
|
const entry = _.get(json, 'entry', false);
|
||||||
if (bucketKey && entryKey && entry) {
|
if (bucketKey && entryKey && entry) {
|
||||||
state[bucketKey][entryKey] = entry;
|
state[bucketKey][entryKey] = entry;
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
64
pkg/interface/src/logic/state/base.ts
Normal file
64
pkg/interface/src/logic/state/base.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import produce from "immer";
|
||||||
|
import { compose } from "lodash/fp";
|
||||||
|
import create, { State, UseStore } from "zustand";
|
||||||
|
import { persist } from "zustand/middleware";
|
||||||
|
|
||||||
|
|
||||||
|
export const stateSetter = <StateType>(
|
||||||
|
fn: (state: StateType) => void,
|
||||||
|
set
|
||||||
|
): void => {
|
||||||
|
// fn = (state: StateType) => {
|
||||||
|
// // TODO this is a stub for the store debugging
|
||||||
|
// fn(state);
|
||||||
|
// }
|
||||||
|
return set(fn);
|
||||||
|
// TODO we want to use the below, but it makes everything read-only
|
||||||
|
return set(produce(fn));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const reduceState = <
|
||||||
|
StateType extends BaseState<StateType>,
|
||||||
|
UpdateType
|
||||||
|
>(
|
||||||
|
state: UseStore<StateType>,
|
||||||
|
data: UpdateType,
|
||||||
|
reducers: ((data: UpdateType, state: StateType) => StateType)[]
|
||||||
|
): void => {
|
||||||
|
const oldState = state.getState();
|
||||||
|
const reducer = compose(reducers.map(reducer => reducer.bind(reducer, data)));
|
||||||
|
const newState = reducer(oldState);
|
||||||
|
state.getState().set(state => state = newState);
|
||||||
|
};
|
||||||
|
|
||||||
|
export let stateStorageKeys: string[] = [];
|
||||||
|
|
||||||
|
export const stateStorageKey = (stateName: string) => {
|
||||||
|
stateName = `Landscape${stateName}State`;
|
||||||
|
stateStorageKeys = [...new Set([...stateStorageKeys, stateName])];
|
||||||
|
return stateName;
|
||||||
|
};
|
||||||
|
|
||||||
|
(window as any).clearStates = () => {
|
||||||
|
stateStorageKeys.forEach(key => {
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BaseState<StateType> extends State {
|
||||||
|
set: (fn: (state: StateType) => void) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createState = <StateType extends BaseState<any>>(
|
||||||
|
name: string,
|
||||||
|
properties: Omit<StateType, 'set'>,
|
||||||
|
blacklist: string[] = []
|
||||||
|
): UseStore<StateType> => create(persist((set, get) => ({
|
||||||
|
// TODO why does this typing break?
|
||||||
|
set: fn => stateSetter(fn, set),
|
||||||
|
...properties
|
||||||
|
}), {
|
||||||
|
blacklist,
|
||||||
|
name: stateStorageKey(name),
|
||||||
|
version: 1, // TODO version these according to base hash
|
||||||
|
}));
|
31
pkg/interface/src/logic/state/contact.ts
Normal file
31
pkg/interface/src/logic/state/contact.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { Patp, Rolodex, Scry } from "@urbit/api";
|
||||||
|
|
||||||
|
import { BaseState, createState } from "./base";
|
||||||
|
|
||||||
|
export interface ContactState extends BaseState<ContactState> {
|
||||||
|
contacts: Rolodex;
|
||||||
|
isContactPublic: boolean;
|
||||||
|
nackedContacts: Set<Patp>;
|
||||||
|
// fetchIsAllowed: (entity, name, ship, personal) => Promise<boolean>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useContactState = createState<ContactState>('Contact', {
|
||||||
|
contacts: {},
|
||||||
|
nackedContacts: new Set(),
|
||||||
|
isContactPublic: false,
|
||||||
|
// fetchIsAllowed: async (
|
||||||
|
// entity,
|
||||||
|
// name,
|
||||||
|
// ship,
|
||||||
|
// personal
|
||||||
|
// ): Promise<boolean> => {
|
||||||
|
// const isPersonal = personal ? 'true' : 'false';
|
||||||
|
// const api = useApi();
|
||||||
|
// return api.scry({
|
||||||
|
// app: 'contact-store',
|
||||||
|
// path: `/is-allowed/${entity}/${name}/${ship}/${isPersonal}`
|
||||||
|
// });
|
||||||
|
// },
|
||||||
|
}, ['nackedContacts']);
|
||||||
|
|
||||||
|
export default useContactState;
|
127
pkg/interface/src/logic/state/graph.ts
Normal file
127
pkg/interface/src/logic/state/graph.ts
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import { Graphs, decToUd, numToUd } from "@urbit/api";
|
||||||
|
|
||||||
|
import { BaseState, createState } from "./base";
|
||||||
|
|
||||||
|
export interface GraphState extends BaseState<GraphState> {
|
||||||
|
graphs: Graphs;
|
||||||
|
graphKeys: Set<string>;
|
||||||
|
pendingIndices: Record<string, any>;
|
||||||
|
graphTimesentMap: Record<string, any>;
|
||||||
|
// getKeys: () => Promise<void>;
|
||||||
|
// getTags: () => Promise<void>;
|
||||||
|
// getTagQueries: () => Promise<void>;
|
||||||
|
// getGraph: (ship: string, resource: string) => Promise<void>;
|
||||||
|
// getNewest: (ship: string, resource: string, count: number, index?: string) => Promise<void>;
|
||||||
|
// getOlderSiblings: (ship: string, resource: string, count: number, index?: string) => Promise<void>;
|
||||||
|
// getYoungerSiblings: (ship: string, resource: string, count: number, index?: string) => Promise<void>;
|
||||||
|
// getGraphSubset: (ship: string, resource: string, start: string, end: string) => Promise<void>;
|
||||||
|
// getNode: (ship: string, resource: string, index: string) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useGraphState = createState<GraphState>('Graph', {
|
||||||
|
graphs: {},
|
||||||
|
graphKeys: new Set(),
|
||||||
|
pendingIndices: {},
|
||||||
|
graphTimesentMap: {},
|
||||||
|
// getKeys: async () => {
|
||||||
|
// const api = useApi();
|
||||||
|
// const keys = await api.scry({
|
||||||
|
// app: 'graph-store',
|
||||||
|
// path: '/keys'
|
||||||
|
// });
|
||||||
|
// graphReducer(keys);
|
||||||
|
// },
|
||||||
|
// getTags: async () => {
|
||||||
|
// const api = useApi();
|
||||||
|
// const tags = await api.scry({
|
||||||
|
// app: 'graph-store',
|
||||||
|
// path: '/tags'
|
||||||
|
// });
|
||||||
|
// graphReducer(tags);
|
||||||
|
// },
|
||||||
|
// getTagQueries: async () => {
|
||||||
|
// const api = useApi();
|
||||||
|
// const tagQueries = await api.scry({
|
||||||
|
// app: 'graph-store',
|
||||||
|
// path: '/tag-queries'
|
||||||
|
// });
|
||||||
|
// graphReducer(tagQueries);
|
||||||
|
// },
|
||||||
|
// getGraph: async (ship: string, resource: string) => {
|
||||||
|
// const api = useApi();
|
||||||
|
// const graph = await api.scry({
|
||||||
|
// app: 'graph-store',
|
||||||
|
// path: `/graph/${ship}/${resource}`
|
||||||
|
// });
|
||||||
|
// graphReducer(graph);
|
||||||
|
// },
|
||||||
|
// getNewest: async (
|
||||||
|
// ship: string,
|
||||||
|
// resource: string,
|
||||||
|
// count: number,
|
||||||
|
// index: string = ''
|
||||||
|
// ) => {
|
||||||
|
// const api = useApi();
|
||||||
|
// const data = await api.scry({
|
||||||
|
// app: 'graph-store',
|
||||||
|
// path: `/newest/${ship}/${resource}/${count}${index}`
|
||||||
|
// });
|
||||||
|
// graphReducer(data);
|
||||||
|
// },
|
||||||
|
// getOlderSiblings: async (
|
||||||
|
// ship: string,
|
||||||
|
// resource: string,
|
||||||
|
// count: number,
|
||||||
|
// index: string = ''
|
||||||
|
// ) => {
|
||||||
|
// const api = useApi();
|
||||||
|
// index = index.split('/').map(decToUd).join('/');
|
||||||
|
// const data = await api.scry({
|
||||||
|
// app: 'graph-store',
|
||||||
|
// path: `/node-siblings/older/${ship}/${resource}/${count}${index}`
|
||||||
|
// });
|
||||||
|
// graphReducer(data);
|
||||||
|
// },
|
||||||
|
// getYoungerSiblings: async (
|
||||||
|
// ship: string,
|
||||||
|
// resource: string,
|
||||||
|
// count: number,
|
||||||
|
// index: string = ''
|
||||||
|
// ) => {
|
||||||
|
// const api = useApi();
|
||||||
|
// index = index.split('/').map(decToUd).join('/');
|
||||||
|
// const data = await api.scry({
|
||||||
|
// app: 'graph-store',
|
||||||
|
// path: `/node-siblings/younger/${ship}/${resource}/${count}${index}`
|
||||||
|
// });
|
||||||
|
// graphReducer(data);
|
||||||
|
// },
|
||||||
|
// getGraphSubset: async (
|
||||||
|
// ship: string,
|
||||||
|
// resource: string,
|
||||||
|
// start: string,
|
||||||
|
// end: string
|
||||||
|
// ) => {
|
||||||
|
// const api = useApi();
|
||||||
|
// const subset = await api.scry({
|
||||||
|
// app: 'graph-store',
|
||||||
|
// path: `/graph-subset/${ship}/${resource}/${end}/${start}`
|
||||||
|
// });
|
||||||
|
// graphReducer(subset);
|
||||||
|
// },
|
||||||
|
// getNode: async (
|
||||||
|
// ship: string,
|
||||||
|
// resource: string,
|
||||||
|
// index: string
|
||||||
|
// ) => {
|
||||||
|
// const api = useApi();
|
||||||
|
// index = index.split('/').map(numToUd).join('/');
|
||||||
|
// const node = api.scry({
|
||||||
|
// app: 'graph-store',
|
||||||
|
// path: `/node/${ship}/${resource}${index}`
|
||||||
|
// });
|
||||||
|
// graphReducer(node);
|
||||||
|
// },
|
||||||
|
}, ['graphs', 'graphKeys']);
|
||||||
|
|
||||||
|
export default useGraphState;
|
15
pkg/interface/src/logic/state/group.ts
Normal file
15
pkg/interface/src/logic/state/group.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { Path, JoinRequests } from "@urbit/api";
|
||||||
|
|
||||||
|
import { BaseState, createState } from "./base";
|
||||||
|
|
||||||
|
export interface GroupState extends BaseState<GroupState> {
|
||||||
|
groups: Set<Path>;
|
||||||
|
pendingJoin: JoinRequests;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useGroupState = createState<GroupState>('Group', {
|
||||||
|
groups: new Set(),
|
||||||
|
pendingJoin: {},
|
||||||
|
}, ['groups']);
|
||||||
|
|
||||||
|
export default useGroupState;
|
69
pkg/interface/src/logic/state/hark.ts
Normal file
69
pkg/interface/src/logic/state/hark.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { NotificationGraphConfig, Timebox, Unreads, dateToDa } from "@urbit/api";
|
||||||
|
import BigIntOrderedMap from "@urbit/api/lib/BigIntOrderedMap";
|
||||||
|
|
||||||
|
// import { harkGraphHookReducer, harkGroupHookReducer, harkReducer } from "~/logic/subscription/hark";
|
||||||
|
import { BaseState, createState } from "./base";
|
||||||
|
|
||||||
|
export const HARK_FETCH_MORE_COUNT = 3;
|
||||||
|
|
||||||
|
export interface HarkState extends BaseState<HarkState> {
|
||||||
|
archivedNotifications: BigIntOrderedMap<Timebox>;
|
||||||
|
doNotDisturb: boolean;
|
||||||
|
// getMore: () => Promise<boolean>;
|
||||||
|
// getSubset: (offset: number, count: number, isArchive: boolean) => Promise<void>;
|
||||||
|
// getTimeSubset: (start?: Date, end?: Date) => Promise<void>;
|
||||||
|
notifications: BigIntOrderedMap<Timebox>;
|
||||||
|
notificationsCount: number;
|
||||||
|
notificationsGraphConfig: NotificationGraphConfig; // TODO unthread this everywhere
|
||||||
|
notificationsGroupConfig: []; // TODO type this
|
||||||
|
unreads: Unreads;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useHarkState = createState<HarkState>('Hark', {
|
||||||
|
archivedNotifications: new BigIntOrderedMap<Timebox>(),
|
||||||
|
doNotDisturb: false,
|
||||||
|
// getMore: async (): Promise<boolean> => {
|
||||||
|
// const state = get();
|
||||||
|
// const offset = state.notifications.size || 0;
|
||||||
|
// await state.getSubset(offset, HARK_FETCH_MORE_COUNT, false);
|
||||||
|
// // TODO make sure that state has mutated at this point.
|
||||||
|
// return offset === (state.notifications.size || 0);
|
||||||
|
// },
|
||||||
|
// getSubset: async (offset, count, isArchive): Promise<void> => {
|
||||||
|
// const api = useApi();
|
||||||
|
// const where = isArchive ? 'archive' : 'inbox';
|
||||||
|
// const result = await api.scry({
|
||||||
|
// app: 'hark-store',
|
||||||
|
// path: `/recent/${where}/${offset}/${count}`
|
||||||
|
// });
|
||||||
|
// harkReducer(result);
|
||||||
|
// return;
|
||||||
|
// },
|
||||||
|
// getTimeSubset: async (start, end): Promise<void> => {
|
||||||
|
// const api = useApi();
|
||||||
|
// const s = start ? dateToDa(start) : '-';
|
||||||
|
// const e = end ? dateToDa(end) : '-';
|
||||||
|
// const result = await api.scry({
|
||||||
|
// app: 'hark-hook',
|
||||||
|
// path: `/recent/${s}/${e}`
|
||||||
|
// });
|
||||||
|
// harkGroupHookReducer(result);
|
||||||
|
// harkGraphHookReducer(result);
|
||||||
|
// return;
|
||||||
|
// },
|
||||||
|
notifications: new BigIntOrderedMap<Timebox>(),
|
||||||
|
notificationsCount: 0,
|
||||||
|
notificationsGraphConfig: {
|
||||||
|
watchOnSelf: false,
|
||||||
|
mentions: false,
|
||||||
|
watching: []
|
||||||
|
},
|
||||||
|
notificationsGroupConfig: [],
|
||||||
|
unreads: {
|
||||||
|
graph: {},
|
||||||
|
group: {}
|
||||||
|
},
|
||||||
|
}, ['notifications', 'archivedNotifications', 'unreads', 'notificationsCount']);
|
||||||
|
|
||||||
|
|
||||||
|
export default useHarkState;
|
12
pkg/interface/src/logic/state/invite.ts
Normal file
12
pkg/interface/src/logic/state/invite.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Invites } from '@urbit/api';
|
||||||
|
import { BaseState, createState } from './base';
|
||||||
|
|
||||||
|
export interface InviteState extends BaseState<InviteState> {
|
||||||
|
invites: Invites;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useInviteState = createState<InviteState>('Invite', {
|
||||||
|
invites: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default useInviteState;
|
27
pkg/interface/src/logic/state/launch.ts
Normal file
27
pkg/interface/src/logic/state/launch.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { Tile, WeatherState } from "~/types/launch-update";
|
||||||
|
|
||||||
|
import { BaseState, createState } from "./base";
|
||||||
|
|
||||||
|
|
||||||
|
export interface LaunchState extends BaseState<LaunchState> {
|
||||||
|
firstTime: boolean;
|
||||||
|
tileOrdering: string[];
|
||||||
|
tiles: {
|
||||||
|
[app: string]: Tile;
|
||||||
|
},
|
||||||
|
weather: WeatherState | null,
|
||||||
|
userLocation: string | null;
|
||||||
|
baseHash: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useLaunchState = createState<LaunchState>('Launch', {
|
||||||
|
firstTime: true,
|
||||||
|
tileOrdering: [],
|
||||||
|
tiles: {},
|
||||||
|
weather: null,
|
||||||
|
userLocation: null,
|
||||||
|
baseHash: null
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export default useLaunchState;
|
57
pkg/interface/src/logic/state/metadata.ts
Normal file
57
pkg/interface/src/logic/state/metadata.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { MetadataUpdatePreview, Associations } from "@urbit/api";
|
||||||
|
|
||||||
|
import { BaseState, createState } from "./base";
|
||||||
|
|
||||||
|
export const METADATA_MAX_PREVIEW_WAIT = 150000;
|
||||||
|
|
||||||
|
export interface MetadataState extends BaseState<MetadataState> {
|
||||||
|
associations: Associations;
|
||||||
|
// preview: (group: string) => Promise<MetadataUpdatePreview>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useMetadataState = createState<MetadataState>('Metadata', {
|
||||||
|
associations: { groups: {}, graph: {}, contacts: {}, chat: {}, link: {}, publish: {} },
|
||||||
|
// preview: async (group): Promise<MetadataUpdatePreview> => {
|
||||||
|
// return new Promise<MetadataUpdatePreview>((resolve, reject) => {
|
||||||
|
// const api = useApi();
|
||||||
|
// let done = false;
|
||||||
|
|
||||||
|
// setTimeout(() => {
|
||||||
|
// if (done) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// done = true;
|
||||||
|
// reject(new Error('offline'));
|
||||||
|
// }, METADATA_MAX_PREVIEW_WAIT);
|
||||||
|
|
||||||
|
// api.subscribe({
|
||||||
|
// app: 'metadata-pull-hook',
|
||||||
|
// path: `/preview${group}`,
|
||||||
|
// // TODO type this message?
|
||||||
|
// event: (message) => {
|
||||||
|
// if ('metadata-hook-update' in message) {
|
||||||
|
// done = true;
|
||||||
|
// const update = message['metadata-hook-update'].preview as MetadataUpdatePreview;
|
||||||
|
// resolve(update);
|
||||||
|
// } else {
|
||||||
|
// done = true;
|
||||||
|
// reject(new Error('no-permissions'));
|
||||||
|
// }
|
||||||
|
// // TODO how to delete this subscription? Perhaps return the susbcription ID as the second parameter of all the handlers
|
||||||
|
// },
|
||||||
|
// err: (error) => {
|
||||||
|
// console.error(error);
|
||||||
|
// reject(error);
|
||||||
|
// },
|
||||||
|
// quit: () => {
|
||||||
|
// if (!done) {
|
||||||
|
// reject(new Error('offline'));
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// },
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export default useMetadataState;
|
@ -1,12 +1,9 @@
|
|||||||
import React, { ReactNode } from "react";
|
|
||||||
import f from 'lodash/fp';
|
import f from 'lodash/fp';
|
||||||
import create, { State } from 'zustand';
|
import { RemoteContentPolicy, LeapCategories, leapCategories } from "~/types/local-update";
|
||||||
import { persist } from 'zustand/middleware';
|
import { BaseState, createState } from '~/logic/state/base';
|
||||||
import produce from 'immer';
|
|
||||||
import { BackgroundConfig, RemoteContentPolicy, TutorialProgress, tutorialProgress, LeapCategories, leapCategories } from "~/types/local-update";
|
|
||||||
|
|
||||||
|
|
||||||
export interface SettingsState {
|
export interface SettingsState extends BaseState<SettingsState> {
|
||||||
display: {
|
display: {
|
||||||
backgroundType: 'none' | 'url' | 'color';
|
backgroundType: 'none' | 'url' | 'color';
|
||||||
background?: string;
|
background?: string;
|
||||||
@ -28,11 +25,8 @@ export interface SettingsState {
|
|||||||
seen: boolean;
|
seen: boolean;
|
||||||
joined?: number;
|
joined?: number;
|
||||||
};
|
};
|
||||||
set: (fn: (state: SettingsState) => void) => void
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SettingsStateZus = SettingsState & State;
|
|
||||||
|
|
||||||
export const selectSettingsState =
|
export const selectSettingsState =
|
||||||
<K extends keyof SettingsState>(keys: K[]) => f.pick<SettingsState, K>(keys);
|
<K extends keyof SettingsState>(keys: K[]) => f.pick<SettingsState, K>(keys);
|
||||||
|
|
||||||
@ -40,7 +34,7 @@ export const selectCalmState = (s: SettingsState) => s.calm;
|
|||||||
|
|
||||||
export const selectDisplayState = (s: SettingsState) => s.display;
|
export const selectDisplayState = (s: SettingsState) => s.display;
|
||||||
|
|
||||||
const useSettingsState = create<SettingsStateZus>((set) => ({
|
const useSettingsState = createState<SettingsState>('Settings', {
|
||||||
display: {
|
display: {
|
||||||
backgroundType: 'none',
|
backgroundType: 'none',
|
||||||
background: undefined,
|
background: undefined,
|
||||||
@ -66,17 +60,7 @@ const useSettingsState = create<SettingsStateZus>((set) => ({
|
|||||||
tutorial: {
|
tutorial: {
|
||||||
seen: false,
|
seen: false,
|
||||||
joined: undefined
|
joined: undefined
|
||||||
},
|
}
|
||||||
set: (fn: (state: SettingsState) => void) => set(produce(fn))
|
});
|
||||||
}));
|
|
||||||
|
|
||||||
function withSettingsState<P, S extends keyof SettingsState>(Component: any, stateMemberKeys?: S[]) {
|
export default useSettingsState;
|
||||||
return React.forwardRef((props: Omit<P, S>, ref) => {
|
|
||||||
const localState = stateMemberKeys
|
|
||||||
? useSettingsState(selectSettingsState(stateMemberKeys))
|
|
||||||
: useSettingsState();
|
|
||||||
return <Component ref={ref} {...localState} {...props} />
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export { useSettingsState as default, withSettingsState };
|
|
33
pkg/interface/src/logic/state/storage.ts
Normal file
33
pkg/interface/src/logic/state/storage.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { BaseState, createState } from "./base";
|
||||||
|
|
||||||
|
export interface GcpToken {
|
||||||
|
accessKey: string;
|
||||||
|
expiresIn: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StorageState extends BaseState<StorageState> {
|
||||||
|
gcp: {
|
||||||
|
configured?: boolean;
|
||||||
|
token?: GcpToken;
|
||||||
|
},
|
||||||
|
s3: {
|
||||||
|
configuration: {
|
||||||
|
buckets: Set<string>;
|
||||||
|
currentBucket: string;
|
||||||
|
};
|
||||||
|
credentials: any | null; // TODO better type
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const useStorageState = createState<StorageState>('Storage', {
|
||||||
|
gcp: {},
|
||||||
|
s3: {
|
||||||
|
configuration: {
|
||||||
|
buckets: new Set(),
|
||||||
|
currentBucket: ''
|
||||||
|
},
|
||||||
|
credentials: null,
|
||||||
|
}
|
||||||
|
}, ['s3']);
|
||||||
|
|
||||||
|
export default useStorageState;
|
@ -5,10 +5,6 @@ export default class BaseStore<S extends object> {
|
|||||||
this.state = this.initialState();
|
this.state = this.initialState();
|
||||||
}
|
}
|
||||||
|
|
||||||
dehydrate() {}
|
|
||||||
|
|
||||||
rehydrate() {}
|
|
||||||
|
|
||||||
initialState() {
|
initialState() {
|
||||||
return {} as S;
|
return {} as S;
|
||||||
}
|
}
|
||||||
|
@ -20,11 +20,11 @@ import GcpReducer from '../reducers/gcp-reducer';
|
|||||||
import { OrderedMap } from '../lib/OrderedMap';
|
import { OrderedMap } from '../lib/OrderedMap';
|
||||||
import { BigIntOrderedMap } from '../lib/BigIntOrderedMap';
|
import { BigIntOrderedMap } from '../lib/BigIntOrderedMap';
|
||||||
import { GroupViewReducer } from '../reducers/group-view';
|
import { GroupViewReducer } from '../reducers/group-view';
|
||||||
|
import { unstable_batchedUpdates } from 'react-dom';
|
||||||
|
|
||||||
export default class GlobalStore extends BaseStore<StoreState> {
|
export default class GlobalStore extends BaseStore<StoreState> {
|
||||||
inviteReducer = new InviteReducer();
|
inviteReducer = new InviteReducer();
|
||||||
metadataReducer = new MetadataReducer();
|
metadataReducer = new MetadataReducer();
|
||||||
localReducer = new LocalReducer();
|
|
||||||
s3Reducer = new S3Reducer();
|
s3Reducer = new S3Reducer();
|
||||||
groupReducer = new GroupReducer();
|
groupReducer = new GroupReducer();
|
||||||
launchReducer = new LaunchReducer();
|
launchReducer = new LaunchReducer();
|
||||||
@ -44,84 +44,30 @@ export default class GlobalStore extends BaseStore<StoreState> {
|
|||||||
console.log(_.pick(this.state, stateKeys));
|
console.log(_.pick(this.state, stateKeys));
|
||||||
}
|
}
|
||||||
|
|
||||||
rehydrate() {
|
|
||||||
this.localReducer.rehydrate(this.state);
|
|
||||||
}
|
|
||||||
|
|
||||||
dehydrate() {
|
|
||||||
this.localReducer.dehydrate(this.state);
|
|
||||||
}
|
|
||||||
|
|
||||||
initialState(): StoreState {
|
initialState(): StoreState {
|
||||||
return {
|
return {
|
||||||
connection: 'connected',
|
connection: 'connected',
|
||||||
baseHash: null,
|
|
||||||
invites: {},
|
|
||||||
associations: {
|
|
||||||
groups: {},
|
|
||||||
graph: {}
|
|
||||||
},
|
|
||||||
groups: {},
|
|
||||||
groupKeys: new Set(),
|
|
||||||
graphs: {},
|
|
||||||
graphKeys: new Set(),
|
|
||||||
launch: {
|
|
||||||
firstTime: false,
|
|
||||||
tileOrdering: [],
|
|
||||||
tiles: {}
|
|
||||||
},
|
|
||||||
weather: {},
|
|
||||||
userLocation: null,
|
|
||||||
storage: {
|
|
||||||
gcp: {},
|
|
||||||
s3: {
|
|
||||||
configuration: {
|
|
||||||
buckets: new Set(),
|
|
||||||
currentBucket: ''
|
|
||||||
},
|
|
||||||
credentials: null
|
|
||||||
},
|
|
||||||
},
|
|
||||||
isContactPublic: false,
|
|
||||||
contacts: {},
|
|
||||||
nackedContacts: new Set(),
|
|
||||||
notifications: new BigIntOrderedMap<Timebox>(),
|
|
||||||
archivedNotifications: new BigIntOrderedMap<Timebox>(),
|
|
||||||
notificationsGroupConfig: [],
|
|
||||||
notificationsGraphConfig: {
|
|
||||||
watchOnSelf: false,
|
|
||||||
mentions: false,
|
|
||||||
watching: []
|
|
||||||
},
|
|
||||||
unreads: {
|
|
||||||
graph: {},
|
|
||||||
group: {}
|
|
||||||
},
|
|
||||||
notificationsCount: 0,
|
|
||||||
settings: {},
|
|
||||||
pendingJoin: {},
|
|
||||||
graphTimesentMap: {}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
reduce(data: Cage, state: StoreState) {
|
reduce(data: Cage, state: StoreState) {
|
||||||
// debug shim
|
unstable_batchedUpdates(() => {
|
||||||
const tag = Object.keys(data)[0];
|
// debug shim
|
||||||
const oldActions = this.pastActions[tag] || [];
|
const tag = Object.keys(data)[0];
|
||||||
this.pastActions[tag] = [data[tag], ...oldActions.slice(0,14)];
|
const oldActions = this.pastActions[tag] || [];
|
||||||
|
this.pastActions[tag] = [data[tag], ...oldActions.slice(0, 14)];
|
||||||
this.inviteReducer.reduce(data, this.state);
|
this.inviteReducer.reduce(data);
|
||||||
this.metadataReducer.reduce(data, this.state);
|
this.metadataReducer.reduce(data);
|
||||||
this.localReducer.reduce(data, this.state);
|
this.s3Reducer.reduce(data);
|
||||||
this.s3Reducer.reduce(data, this.state);
|
this.groupReducer.reduce(data);
|
||||||
this.groupReducer.reduce(data, this.state);
|
GroupViewReducer(data);
|
||||||
this.launchReducer.reduce(data, this.state);
|
this.launchReducer.reduce(data);
|
||||||
this.connReducer.reduce(data, this.state);
|
this.connReducer.reduce(data, this.state);
|
||||||
GraphReducer(data, this.state);
|
GraphReducer(data);
|
||||||
HarkReducer(data, this.state);
|
HarkReducer(data);
|
||||||
ContactReducer(data, this.state);
|
ContactReducer(data);
|
||||||
this.settingsReducer.reduce(data);
|
this.settingsReducer.reduce(data);
|
||||||
this.gcpReducer.reduce(data, this.state);
|
this.gcpReducer.reduce(data);
|
||||||
GroupViewReducer(data, this.state);
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,52 +1,6 @@
|
|||||||
import { Path } from '@urbit/api';
|
|
||||||
import { Invites } from '@urbit/api/invite';
|
|
||||||
import { Associations } from '@urbit/api/metadata';
|
|
||||||
import { Rolodex } from '@urbit/api/contacts';
|
|
||||||
import { Groups } from '@urbit/api/groups';
|
|
||||||
import { StorageState } from '~/types/storage-state';
|
|
||||||
import { LaunchState, WeatherState } from '~/types/launch-update';
|
|
||||||
import { ConnectionStatus } from '~/types/connection';
|
import { ConnectionStatus } from '~/types/connection';
|
||||||
import { Graphs } from '@urbit/api/graph';
|
|
||||||
import {
|
|
||||||
Notifications,
|
|
||||||
NotificationGraphConfig,
|
|
||||||
GroupNotificationsConfig,
|
|
||||||
Unreads,
|
|
||||||
JoinRequests,
|
|
||||||
Patp
|
|
||||||
} from '@urbit/api';
|
|
||||||
|
|
||||||
export interface StoreState {
|
export interface StoreState {
|
||||||
// local state
|
// local state
|
||||||
connection: ConnectionStatus;
|
connection: ConnectionStatus;
|
||||||
baseHash: string | null;
|
|
||||||
|
|
||||||
// invite state
|
|
||||||
invites: Invites;
|
|
||||||
// metadata state
|
|
||||||
associations: Associations;
|
|
||||||
// contact state
|
|
||||||
contacts: Rolodex;
|
|
||||||
// groups state
|
|
||||||
groups: Groups;
|
|
||||||
groupKeys: Set<Path>;
|
|
||||||
nackedContacts: Set<Patp>
|
|
||||||
storage: StorageState;
|
|
||||||
graphs: Graphs;
|
|
||||||
graphKeys: Set<string>;
|
|
||||||
|
|
||||||
// App specific states
|
|
||||||
// launch state
|
|
||||||
launch: LaunchState;
|
|
||||||
weather: WeatherState | {} | null;
|
|
||||||
userLocation: string | null;
|
|
||||||
|
|
||||||
archivedNotifications: Notifications;
|
|
||||||
notifications: Notifications;
|
|
||||||
notificationsGraphConfig: NotificationGraphConfig;
|
|
||||||
notificationsGroupConfig: GroupNotificationsConfig;
|
|
||||||
notificationsCount: number,
|
|
||||||
unreads: Unreads;
|
|
||||||
doNotDisturb: boolean;
|
|
||||||
pendingJoin: JoinRequests;
|
|
||||||
}
|
}
|
||||||
|
@ -27,12 +27,15 @@ import GlobalSubscription from '~/logic/subscription/global';
|
|||||||
import GlobalApi from '~/logic/api/global';
|
import GlobalApi from '~/logic/api/global';
|
||||||
import { uxToHex } from '~/logic/lib/util';
|
import { uxToHex } from '~/logic/lib/util';
|
||||||
import { foregroundFromBackground } from '~/logic/lib/sigil';
|
import { foregroundFromBackground } from '~/logic/lib/sigil';
|
||||||
|
import withState from '~/logic/lib/withState';
|
||||||
|
import useLocalState from '~/logic/state/local';
|
||||||
|
import useContactState from '~/logic/state/contact';
|
||||||
|
import useGroupState from '~/logic/state/group';
|
||||||
|
import useSettingsState from '~/logic/state/settings';
|
||||||
import gcpManager from '~/logic/lib/gcpManager';
|
import gcpManager from '~/logic/lib/gcpManager';
|
||||||
import { withLocalState } from '~/logic/state/local';
|
|
||||||
import { withSettingsState } from '~/logic/state/settings';
|
|
||||||
|
|
||||||
|
|
||||||
const Root = withSettingsState(styled.div`
|
const Root = withState(styled.div`
|
||||||
font-family: ${p => p.theme.fonts.sans};
|
font-family: ${p => p.theme.fonts.sans};
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -66,7 +69,9 @@ const Root = withSettingsState(styled.div`
|
|||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
border: 0px solid transparent;
|
border: 0px solid transparent;
|
||||||
}
|
}
|
||||||
`, ['display']);
|
`, [
|
||||||
|
[useSettingsState, ['display']]
|
||||||
|
]);
|
||||||
|
|
||||||
const StatusBarWithRouter = withRouter(StatusBar);
|
const StatusBarWithRouter = withRouter(StatusBar);
|
||||||
class App extends React.Component {
|
class App extends React.Component {
|
||||||
@ -79,7 +84,7 @@ class App extends React.Component {
|
|||||||
|
|
||||||
this.appChannel = new window.channel();
|
this.appChannel = new window.channel();
|
||||||
this.api = new GlobalApi(this.ship, this.appChannel, this.store);
|
this.api = new GlobalApi(this.ship, this.appChannel, this.store);
|
||||||
gcpManager.configure(this.api, this.store);
|
gcpManager.configure(this.api);
|
||||||
this.subscription =
|
this.subscription =
|
||||||
new GlobalSubscription(this.store, this.api, this.appChannel);
|
new GlobalSubscription(this.store, this.api, this.appChannel);
|
||||||
|
|
||||||
@ -98,7 +103,6 @@ class App extends React.Component {
|
|||||||
}, 500);
|
}, 500);
|
||||||
this.api.local.getBaseHash();
|
this.api.local.getBaseHash();
|
||||||
this.api.settings.getAll();
|
this.api.settings.getAll();
|
||||||
this.store.rehydrate();
|
|
||||||
gcpManager.start();
|
gcpManager.start();
|
||||||
Mousetrap.bindGlobal(['command+/', 'ctrl+/'], (e) => {
|
Mousetrap.bindGlobal(['command+/', 'ctrl+/'], (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -119,8 +123,8 @@ class App extends React.Component {
|
|||||||
|
|
||||||
faviconString() {
|
faviconString() {
|
||||||
let background = '#ffffff';
|
let background = '#ffffff';
|
||||||
if (this.state.contacts.hasOwnProperty(`~${window.ship}`)) {
|
if (this.props.contacts.hasOwnProperty(`~${window.ship}`)) {
|
||||||
background = `#${uxToHex(this.state.contacts[`~${window.ship}`].color)}`;
|
background = `#${uxToHex(this.props.contacts[`~${window.ship}`].color)}`;
|
||||||
}
|
}
|
||||||
const foreground = foregroundFromBackground(background);
|
const foreground = foregroundFromBackground(background);
|
||||||
const svg = sigiljs({
|
const svg = sigiljs({
|
||||||
@ -135,16 +139,12 @@ class App extends React.Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { state, props } = this;
|
const { state, props } = this;
|
||||||
const associations = state.associations ?
|
|
||||||
state.associations : { contacts: {} };
|
|
||||||
const theme =
|
const theme =
|
||||||
((props.dark && props?.display?.theme == "auto") ||
|
((props.dark && props?.display?.theme == "auto") ||
|
||||||
props?.display?.theme == "dark"
|
props?.display?.theme == "dark"
|
||||||
) ? dark : light;
|
) ? dark : light;
|
||||||
|
|
||||||
const notificationsCount = state.notificationsCount || 0;
|
const ourContact = this.props.contacts[`~${this.ship}`] || null;
|
||||||
const doNotDisturb = state.doNotDisturb || false;
|
|
||||||
const ourContact = this.state.contacts[`~${this.ship}`] || null;
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
@ -158,27 +158,17 @@ class App extends React.Component {
|
|||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<StatusBarWithRouter
|
<StatusBarWithRouter
|
||||||
props={this.props}
|
props={this.props}
|
||||||
associations={associations}
|
|
||||||
invites={this.state.invites}
|
|
||||||
ourContact={ourContact}
|
ourContact={ourContact}
|
||||||
api={this.api}
|
api={this.api}
|
||||||
connection={this.state.connection}
|
connection={this.state.connection}
|
||||||
subscription={this.subscription}
|
subscription={this.subscription}
|
||||||
ship={this.ship}
|
ship={this.ship}
|
||||||
doNotDisturb={doNotDisturb}
|
|
||||||
notificationsCount={notificationsCount}
|
|
||||||
/>
|
/>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Omnibox
|
<Omnibox
|
||||||
associations={state.associations}
|
associations={state.associations}
|
||||||
apps={state.launch}
|
|
||||||
tiles={state.launch.tiles}
|
|
||||||
api={this.api}
|
api={this.api}
|
||||||
contacts={state.contacts}
|
|
||||||
notifications={state.notificationsCount}
|
|
||||||
invites={state.invites}
|
|
||||||
groups={state.groups}
|
|
||||||
show={this.props.omniboxShown}
|
show={this.props.omniboxShown}
|
||||||
toggle={this.props.toggleOmnibox}
|
toggle={this.props.toggleOmnibox}
|
||||||
/>
|
/>
|
||||||
@ -188,7 +178,7 @@ class App extends React.Component {
|
|||||||
ship={this.ship}
|
ship={this.ship}
|
||||||
api={this.api}
|
api={this.api}
|
||||||
subscription={this.subscription}
|
subscription={this.subscription}
|
||||||
{...state}
|
connection={this.state.connection}
|
||||||
/>
|
/>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</Router>
|
</Router>
|
||||||
@ -199,4 +189,9 @@ class App extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withSettingsState(withLocalState(process.env.NODE_ENV === 'production' ? App : hot(App)), ['display']);
|
export default withState(process.env.NODE_ENV === 'production' ? App : hot(App), [
|
||||||
|
[useGroupState],
|
||||||
|
[useContactState],
|
||||||
|
[useSettingsState, ['display']],
|
||||||
|
[useLocalState]
|
||||||
|
]);
|
@ -16,6 +16,10 @@ import { Loading } from '~/views/components/Loading';
|
|||||||
import { isWriter, resourceFromPath } from '~/logic/lib/group';
|
import { isWriter, resourceFromPath } from '~/logic/lib/group';
|
||||||
|
|
||||||
import './css/custom.css';
|
import './css/custom.css';
|
||||||
|
import useContactState from '~/logic/state/contact';
|
||||||
|
import useGraphState from '~/logic/state/graph';
|
||||||
|
import useGroupState from '~/logic/state/group';
|
||||||
|
import useHarkState from '~/logic/state/hark';
|
||||||
|
|
||||||
type ChatResourceProps = StoreState & {
|
type ChatResourceProps = StoreState & {
|
||||||
association: Association;
|
association: Association;
|
||||||
@ -26,12 +30,15 @@ type ChatResourceProps = StoreState & {
|
|||||||
export function ChatResource(props: ChatResourceProps) {
|
export function ChatResource(props: ChatResourceProps) {
|
||||||
const station = props.association.resource;
|
const station = props.association.resource;
|
||||||
const groupPath = props.association.group;
|
const groupPath = props.association.group;
|
||||||
const group = props.groups[groupPath];
|
const groups = useGroupState(state => state.groups);
|
||||||
const contacts = props.contacts;
|
const group = groups[groupPath];
|
||||||
|
const contacts = useContactState(state => state.contacts);
|
||||||
|
const graphs = useGraphState(state => state.graphs);
|
||||||
const graphPath = station.slice(7);
|
const graphPath = station.slice(7);
|
||||||
const graph = props.graphs[station.slice(7)];
|
const graph = graphs[graphPath];
|
||||||
const isChatMissing = !props.graphKeys.has(station.slice(7));
|
const unreads = useHarkState(state => state.unreads);
|
||||||
const unreadCount = props.unreads.graph?.[station]?.['/']?.unreads || 0;
|
const unreadCount = unreads.graph?.[station]?.['/']?.unreads || 0;
|
||||||
|
const graphTimesentMap = useGraphState(state => state.graphTimesentMap);
|
||||||
const [,, owner, name] = station.split('/');
|
const [,, owner, name] = station.split('/');
|
||||||
const ourContact = contacts?.[`~${window.ship}`];
|
const ourContact = contacts?.[`~${window.ship}`];
|
||||||
const chatInput = useRef<ChatInput>();
|
const chatInput = useRef<ChatInput>();
|
||||||
@ -132,9 +139,6 @@ export function ChatResource(props: ChatResourceProps) {
|
|||||||
return <Loading />;
|
return <Loading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const modifiedContacts = { ...contacts };
|
|
||||||
delete modifiedContacts[`~${window.ship}`];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col {...bind} height="100%" overflow="hidden" position="relative">
|
<Col {...bind} height="100%" overflow="hidden" position="relative">
|
||||||
<ShareProfile
|
<ShareProfile
|
||||||
@ -152,15 +156,11 @@ export function ChatResource(props: ChatResourceProps) {
|
|||||||
key={station}
|
key={station}
|
||||||
history={props.history}
|
history={props.history}
|
||||||
graph={graph}
|
graph={graph}
|
||||||
|
graphSize={graph.size}
|
||||||
unreadCount={unreadCount}
|
unreadCount={unreadCount}
|
||||||
contacts={
|
showOurContact={ !showBanner && hasLoadedAllowed }
|
||||||
(!showBanner && hasLoadedAllowed) ?
|
|
||||||
contacts : modifiedContacts
|
|
||||||
}
|
|
||||||
association={props.association}
|
association={props.association}
|
||||||
associations={props.associations}
|
pendingSize={Object.keys(graphTimesentMap[graphPath] || {}).length}
|
||||||
groups={props.groups}
|
|
||||||
pendingSize={Object.keys(props.graphTimesentMap[graphPath] || {}).length}
|
|
||||||
group={group}
|
group={group}
|
||||||
ship={owner}
|
ship={owner}
|
||||||
station={station}
|
station={station}
|
||||||
@ -176,11 +176,7 @@ export function ChatResource(props: ChatResourceProps) {
|
|||||||
(!showBanner && hasLoadedAllowed) ? ourContact : null
|
(!showBanner && hasLoadedAllowed) ? ourContact : null
|
||||||
}
|
}
|
||||||
envelopes={[]}
|
envelopes={[]}
|
||||||
contacts={
|
|
||||||
(!showBanner && hasLoadedAllowed) ? contacts : modifiedContacts
|
|
||||||
}
|
|
||||||
onUnmount={appendUnsent}
|
onUnmount={appendUnsent}
|
||||||
storage={props.storage}
|
|
||||||
placeholder="Message..."
|
placeholder="Message..."
|
||||||
message={unsent[station] || ''}
|
message={unsent[station] || ''}
|
||||||
deleteMessage={clearUnsent}
|
deleteMessage={clearUnsent}
|
||||||
|
@ -19,14 +19,12 @@ type ChatInputProps = IuseStorage & {
|
|||||||
station: unknown;
|
station: unknown;
|
||||||
ourContact: unknown;
|
ourContact: unknown;
|
||||||
envelopes: Envelope[];
|
envelopes: Envelope[];
|
||||||
contacts: Contacts;
|
|
||||||
onUnmount(msg: string): void;
|
onUnmount(msg: string): void;
|
||||||
storage: StorageState;
|
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
message: string;
|
message: string;
|
||||||
deleteMessage(): void;
|
deleteMessage(): void;
|
||||||
hideAvatars: boolean;
|
hideAvatars: boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
interface ChatInputState {
|
interface ChatInputState {
|
||||||
inCodeMode: boolean;
|
inCodeMode: boolean;
|
||||||
@ -62,20 +60,23 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
|||||||
|
|
||||||
submit(text) {
|
submit(text) {
|
||||||
const { props, state } = this;
|
const { props, state } = this;
|
||||||
const [,,ship,name] = props.station.split('/');
|
const [, , ship, name] = props.station.split('/');
|
||||||
if (state.inCodeMode) {
|
if (state.inCodeMode) {
|
||||||
this.setState({
|
this.setState(
|
||||||
inCodeMode: false
|
{
|
||||||
}, async () => {
|
inCodeMode: false
|
||||||
const output = await props.api.graph.eval(text);
|
},
|
||||||
const contents: Content[] = [{ code: { output, expression: text } }];
|
async () => {
|
||||||
const post = createPost(contents);
|
const output = await props.api.graph.eval(text);
|
||||||
props.api.graph.addPost(ship, name, post);
|
const contents: Content[] = [{ code: { output, expression: text } }];
|
||||||
});
|
const post = createPost(contents);
|
||||||
|
props.api.graph.addPost(ship, name, post);
|
||||||
|
}
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const post = createPost(tokenizeMessage((text)));
|
const post = createPost(tokenizeMessage(text));
|
||||||
|
|
||||||
props.deleteMessage();
|
props.deleteMessage();
|
||||||
|
|
||||||
@ -88,8 +89,8 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
|||||||
this.chatEditor.current.editor.setValue(url);
|
this.chatEditor.current.editor.setValue(url);
|
||||||
this.setState({ uploadingPaste: false });
|
this.setState({ uploadingPaste: false });
|
||||||
} else {
|
} else {
|
||||||
const [,,ship,name] = props.station.split('/');
|
const [, , ship, name] = props.station.split('/');
|
||||||
props.api.graph.addPost(ship,name, createPost([{ url }]));
|
props.api.graph.addPost(ship, name, createPost([{ url }]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,7 +113,8 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Array.from(files).forEach((file) => {
|
Array.from(files).forEach((file) => {
|
||||||
this.props.uploadDefault(file)
|
this.props
|
||||||
|
.uploadDefault(file)
|
||||||
.then(this.uploadSuccess)
|
.then(this.uploadSuccess)
|
||||||
.catch(this.uploadError);
|
.catch(this.uploadError);
|
||||||
});
|
});
|
||||||
@ -121,32 +123,40 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
|||||||
render() {
|
render() {
|
||||||
const { props, state } = this;
|
const { props, state } = this;
|
||||||
|
|
||||||
const color = props.ourContact
|
const color = props.ourContact ? uxToHex(props.ourContact.color) : '000000';
|
||||||
? uxToHex(props.ourContact.color) : '000000';
|
|
||||||
|
|
||||||
const sigilClass = props.ourContact
|
const sigilClass = props.ourContact ? '' : 'mix-blend-diff';
|
||||||
? '' : 'mix-blend-diff';
|
|
||||||
|
|
||||||
const avatar = (
|
const avatar =
|
||||||
props.ourContact &&
|
props.ourContact && props.ourContact?.avatar && !props.hideAvatars ? (
|
||||||
((props.ourContact?.avatar) && !props.hideAvatars)
|
<BaseImage
|
||||||
)
|
|
||||||
? <BaseImage
|
|
||||||
src={props.ourContact.avatar}
|
src={props.ourContact.avatar}
|
||||||
height={16}
|
height={24}
|
||||||
width={16}
|
width={24}
|
||||||
style={{ objectFit: 'cover' }}
|
style={{ objectFit: 'cover' }}
|
||||||
borderRadius={1}
|
borderRadius={1}
|
||||||
display='inline-block'
|
display='inline-block'
|
||||||
/>
|
/>
|
||||||
: <Sigil
|
) : (
|
||||||
ship={window.ship}
|
<Box
|
||||||
size={16}
|
width={24}
|
||||||
color={`#${color}`}
|
height={24}
|
||||||
classes={sigilClass}
|
display='flex'
|
||||||
icon
|
justifyContent='center'
|
||||||
padding={2}
|
alignItems='center'
|
||||||
/>;
|
backgroundColor={`#${color}`}
|
||||||
|
borderRadius={1}
|
||||||
|
>
|
||||||
|
<Sigil
|
||||||
|
ship={window.ship}
|
||||||
|
size={16}
|
||||||
|
color={`#${color}`}
|
||||||
|
classes={sigilClass}
|
||||||
|
icon
|
||||||
|
padding={2}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row
|
<Row
|
||||||
@ -160,7 +170,7 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
|||||||
className='cf'
|
className='cf'
|
||||||
zIndex={0}
|
zIndex={0}
|
||||||
>
|
>
|
||||||
<Row p='2' alignItems='center'>
|
<Row p='12px 4px 12px 12px' alignItems='center'>
|
||||||
{avatar}
|
{avatar}
|
||||||
</Row>
|
</Row>
|
||||||
<ChatEditor
|
<ChatEditor
|
||||||
@ -172,31 +182,23 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
|||||||
onPaste={this.onPaste.bind(this)}
|
onPaste={this.onPaste.bind(this)}
|
||||||
placeholder='Message...'
|
placeholder='Message...'
|
||||||
/>
|
/>
|
||||||
<Box
|
<Box mx={2} flexShrink={0} height='16px' width='16px' flexBasis='16px'>
|
||||||
mx={2}
|
{this.props.canUpload ? (
|
||||||
flexShrink={0}
|
this.props.uploading ? (
|
||||||
height='16px'
|
<LoadingSpinner />
|
||||||
width='16px'
|
) : (
|
||||||
flexBasis='16px'
|
<Icon
|
||||||
>
|
icon='Links'
|
||||||
{this.props.canUpload
|
width='16'
|
||||||
? this.props.uploading
|
height='16'
|
||||||
? <LoadingSpinner />
|
onClick={() =>
|
||||||
: <Icon icon='Links'
|
this.props.promptUpload().then(this.uploadSuccess)
|
||||||
width="16"
|
}
|
||||||
height="16"
|
/>
|
||||||
onClick={() => this.props.promptUpload().then(this.uploadSuccess)}
|
)
|
||||||
/>
|
) : null}
|
||||||
: null
|
|
||||||
}
|
|
||||||
</Box>
|
</Box>
|
||||||
<Box
|
<Box mr={2} flexShrink={0} height='16px' width='16px' flexBasis='16px'>
|
||||||
mr={2}
|
|
||||||
flexShrink={0}
|
|
||||||
height='16px'
|
|
||||||
width='16px'
|
|
||||||
flexBasis='16px'
|
|
||||||
>
|
|
||||||
<Icon
|
<Icon
|
||||||
icon='Dojo'
|
icon='Dojo'
|
||||||
onClick={this.toggleCode}
|
onClick={this.toggleCode}
|
||||||
@ -208,4 +210,6 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withLocalState(withStorage(ChatInput, { accept: 'image/*' }), ['hideAvatars']);
|
export default withLocalState(withStorage(ChatInput, { accept: 'image/*' }), [
|
||||||
|
'hideAvatars'
|
||||||
|
]);
|
||||||
|
@ -10,7 +10,7 @@ import React, {
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import VisibilitySensor from 'react-visibility-sensor';
|
import VisibilitySensor from 'react-visibility-sensor';
|
||||||
import { Box, Row, Text, Rule, BaseImage } from '@tlon/indigo-react';
|
import { Box, Row, Text, Rule, BaseImage, Icon, Col } from '@tlon/indigo-react';
|
||||||
import { Sigil } from '~/logic/lib/sigil';
|
import { Sigil } from '~/logic/lib/sigil';
|
||||||
import OverlaySigil from '~/views/components/OverlaySigil';
|
import OverlaySigil from '~/views/components/OverlaySigil';
|
||||||
import {
|
import {
|
||||||
@ -33,11 +33,13 @@ import TextContent from './content/text';
|
|||||||
import CodeContent from './content/code';
|
import CodeContent from './content/code';
|
||||||
import RemoteContent from '~/views/components/RemoteContent';
|
import RemoteContent from '~/views/components/RemoteContent';
|
||||||
import { Mention } from '~/views/components/MentionText';
|
import { Mention } from '~/views/components/MentionText';
|
||||||
|
import { Dropdown } from '~/views/components/Dropdown';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import useLocalState from '~/logic/state/local';
|
import useLocalState from '~/logic/state/local';
|
||||||
import useSettingsState, {selectCalmState} from "~/logic/state/settings";
|
import useSettingsState, { selectCalmState } from '~/logic/state/settings';
|
||||||
import Timestamp from '~/views/components/Timestamp';
|
import Timestamp from '~/views/components/Timestamp';
|
||||||
import {useIdlingState} from '~/logic/lib/idling';
|
import useContactState from '~/logic/state/contact';
|
||||||
|
import { useIdlingState } from '~/logic/lib/idling';
|
||||||
|
|
||||||
export const DATESTAMP_FORMAT = '[~]YYYY.M.D';
|
export const DATESTAMP_FORMAT = '[~]YYYY.M.D';
|
||||||
|
|
||||||
@ -56,46 +58,179 @@ export const DayBreak = ({ when, shimTop = false }: DayBreakProps) => (
|
|||||||
mt={shimTop ? '-8px' : '0'}
|
mt={shimTop ? '-8px' : '0'}
|
||||||
>
|
>
|
||||||
<Rule borderColor='lightGray' />
|
<Rule borderColor='lightGray' />
|
||||||
<Text gray flexShrink='0' fontSize={0} px={2}>
|
<Text
|
||||||
|
gray
|
||||||
|
flexShrink='0'
|
||||||
|
whiteSpace='nowrap'
|
||||||
|
textAlign='center'
|
||||||
|
fontSize={0}
|
||||||
|
px={2}
|
||||||
|
>
|
||||||
{moment(when).calendar(null, { sameElse: DATESTAMP_FORMAT })}
|
{moment(when).calendar(null, { sameElse: DATESTAMP_FORMAT })}
|
||||||
</Text>
|
</Text>
|
||||||
<Rule borderColor='lightGray' />
|
<Rule borderColor='lightGray' />
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const UnreadMarker = React.forwardRef(({ dayBreak, when, api, association }, ref) => {
|
export const UnreadMarker = React.forwardRef(
|
||||||
const [visible, setVisible] = useState(false);
|
({ dayBreak, when, api, association }, ref) => {
|
||||||
const idling = useIdlingState();
|
const [visible, setVisible] = useState(false);
|
||||||
const dismiss = useCallback(() => {
|
const idling = useIdlingState();
|
||||||
api.hark.markCountAsRead(association, '/', 'message');
|
const dismiss = useCallback(() => {
|
||||||
}, [api, association]);
|
api.hark.markCountAsRead(association, '/', 'message');
|
||||||
|
}, [api, association]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(visible && !idling) {
|
if (visible && !idling) {
|
||||||
dismiss();
|
dismiss();
|
||||||
}
|
}
|
||||||
}, [visible, idling]);
|
}, [visible, idling]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row
|
||||||
|
position='absolute'
|
||||||
|
ref={ref}
|
||||||
|
px={2}
|
||||||
|
mt={0}
|
||||||
|
height={5}
|
||||||
|
justifyContent='center'
|
||||||
|
alignItems='center'
|
||||||
|
width='100%'
|
||||||
|
>
|
||||||
|
<Rule borderColor='lightBlue' />
|
||||||
|
<VisibilitySensor onChange={setVisible}>
|
||||||
|
<Text
|
||||||
|
color='blue'
|
||||||
|
fontSize={0}
|
||||||
|
flexShrink='0'
|
||||||
|
whiteSpace='nowrap'
|
||||||
|
textAlign='center'
|
||||||
|
px={2}
|
||||||
|
>
|
||||||
|
New messages below
|
||||||
|
</Text>
|
||||||
|
</VisibilitySensor>
|
||||||
|
<Rule borderColor='lightBlue' />
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const MessageActionItem = (props) => {
|
||||||
return (
|
return (
|
||||||
<Row
|
<Row
|
||||||
position='absolute'
|
color='black'
|
||||||
ref={ref}
|
cursor='pointer'
|
||||||
px={2}
|
fontSize={1}
|
||||||
mt={2}
|
fontWeight='500'
|
||||||
height={5}
|
px={3}
|
||||||
justifyContent='center'
|
py={2}
|
||||||
alignItems='center'
|
onClick={props.onClick}
|
||||||
width='100%'
|
>
|
||||||
>
|
<Text fontWeight='500' color={props.color}>
|
||||||
<Rule borderColor='lightBlue' />
|
{props.children}
|
||||||
<VisibilitySensor onChange={setVisible}>
|
</Text>
|
||||||
<Text color='blue' fontSize={0} flexShrink='0' px={2}>
|
</Row>
|
||||||
New messages below
|
);
|
||||||
</Text>
|
};
|
||||||
</VisibilitySensor>
|
|
||||||
<Rule borderColor='lightBlue' />
|
const MessageActions = ({ api, history, msg, group }) => {
|
||||||
</Row>
|
const isAdmin = () => group.tags.role.admin.has(window.ship);
|
||||||
)});
|
const isOwn = () => msg.author === window.ship;
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
borderRadius={1}
|
||||||
|
background='white'
|
||||||
|
border='1px solid'
|
||||||
|
borderColor='lightGray'
|
||||||
|
position='absolute'
|
||||||
|
top='-12px'
|
||||||
|
right={2}
|
||||||
|
>
|
||||||
|
<Row>
|
||||||
|
{isOwn() ? (
|
||||||
|
<Box
|
||||||
|
padding={1}
|
||||||
|
size={'24px'}
|
||||||
|
cursor='pointer'
|
||||||
|
onClick={(e) => console.log(e)}
|
||||||
|
>
|
||||||
|
<Icon icon='NullIcon' size={3} />
|
||||||
|
</Box>
|
||||||
|
) : null}
|
||||||
|
<Box
|
||||||
|
padding={1}
|
||||||
|
size={'24px'}
|
||||||
|
cursor='pointer'
|
||||||
|
onClick={(e) => console.log(e)}
|
||||||
|
>
|
||||||
|
<Icon icon='Chat' size={3} />
|
||||||
|
</Box>
|
||||||
|
<Dropdown
|
||||||
|
dropWidth='250px'
|
||||||
|
width='auto'
|
||||||
|
alignY='top'
|
||||||
|
alignX='right'
|
||||||
|
flexShrink={'0'}
|
||||||
|
offsetY={8}
|
||||||
|
offsetX={-24}
|
||||||
|
options={
|
||||||
|
<Col
|
||||||
|
py={2}
|
||||||
|
backgroundColor='white'
|
||||||
|
color='washedGray'
|
||||||
|
border={1}
|
||||||
|
borderRadius={2}
|
||||||
|
borderColor='lightGray'
|
||||||
|
boxShadow='0px 0px 0px 3px'
|
||||||
|
>
|
||||||
|
{isOwn() ? (
|
||||||
|
<MessageActionItem onClick={(e) => console.log(e)}>
|
||||||
|
Edit Message
|
||||||
|
</MessageActionItem>
|
||||||
|
) : null}
|
||||||
|
<MessageActionItem onClick={(e) => console.log(e)}>
|
||||||
|
Reply
|
||||||
|
</MessageActionItem>
|
||||||
|
<MessageActionItem onClick={(e) => console.log(e)}>
|
||||||
|
Copy Message Link
|
||||||
|
</MessageActionItem>
|
||||||
|
{isAdmin() || isOwn() ? (
|
||||||
|
<MessageActionItem onClick={(e) => console.log(e)} color='red'>
|
||||||
|
Delete Message
|
||||||
|
</MessageActionItem>
|
||||||
|
) : null}
|
||||||
|
<MessageActionItem onClick={(e) => console.log(e)}>
|
||||||
|
View Signature
|
||||||
|
</MessageActionItem>
|
||||||
|
</Col>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Box padding={1} size={'24px'} cursor='pointer'>
|
||||||
|
<Icon icon='Menu' size={3} />
|
||||||
|
</Box>
|
||||||
|
</Dropdown>
|
||||||
|
</Row>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const MessageWrapper = (props) => {
|
||||||
|
const { hovering, bind } = useHovering();
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
py='1'
|
||||||
|
backgroundColor={
|
||||||
|
hovering && !props.hideHover ? 'washedGray' : 'transparent'
|
||||||
|
}
|
||||||
|
position='relative'
|
||||||
|
{...bind}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
{/* {hovering ? <MessageActions {...props} /> : null} */}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
interface ChatMessageProps {
|
interface ChatMessageProps {
|
||||||
msg: Post;
|
msg: Post;
|
||||||
@ -104,7 +239,6 @@ interface ChatMessageProps {
|
|||||||
isLastRead: boolean;
|
isLastRead: boolean;
|
||||||
group: Group;
|
group: Group;
|
||||||
association: Association;
|
association: Association;
|
||||||
contacts: Contacts;
|
|
||||||
className?: string;
|
className?: string;
|
||||||
isPending: boolean;
|
isPending: boolean;
|
||||||
style?: unknown;
|
style?: unknown;
|
||||||
@ -115,6 +249,7 @@ interface ChatMessageProps {
|
|||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
highlighted?: boolean;
|
highlighted?: boolean;
|
||||||
renderSigil?: boolean;
|
renderSigil?: boolean;
|
||||||
|
hideHover?: boolean;
|
||||||
innerRef: (el: HTMLDivElement | null) => void;
|
innerRef: (el: HTMLDivElement | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,8 +261,7 @@ class ChatMessage extends Component<ChatMessageProps> {
|
|||||||
this.divRef = React.createRef();
|
this.divRef = React.createRef();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {}
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
@ -137,7 +271,6 @@ class ChatMessage extends Component<ChatMessageProps> {
|
|||||||
isLastRead,
|
isLastRead,
|
||||||
group,
|
group,
|
||||||
association,
|
association,
|
||||||
contacts,
|
|
||||||
className = '',
|
className = '',
|
||||||
isPending,
|
isPending,
|
||||||
style,
|
style,
|
||||||
@ -147,9 +280,9 @@ class ChatMessage extends Component<ChatMessageProps> {
|
|||||||
history,
|
history,
|
||||||
api,
|
api,
|
||||||
highlighted,
|
highlighted,
|
||||||
|
showOurContact,
|
||||||
fontSize,
|
fontSize,
|
||||||
groups,
|
hideHover
|
||||||
associations
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
let { renderSigil } = this.props;
|
let { renderSigil } = this.props;
|
||||||
@ -173,23 +306,21 @@ class ChatMessage extends Component<ChatMessageProps> {
|
|||||||
.unix(msg['time-sent'] / 1000)
|
.unix(msg['time-sent'] / 1000)
|
||||||
.format(renderSigil ? 'h:mm A' : 'h:mm');
|
.format(renderSigil ? 'h:mm A' : 'h:mm');
|
||||||
|
|
||||||
|
|
||||||
const messageProps = {
|
const messageProps = {
|
||||||
msg,
|
msg,
|
||||||
timestamp,
|
timestamp,
|
||||||
contacts,
|
|
||||||
association,
|
association,
|
||||||
group,
|
group,
|
||||||
style,
|
style,
|
||||||
containerClass,
|
containerClass,
|
||||||
isPending,
|
isPending,
|
||||||
|
showOurContact,
|
||||||
history,
|
history,
|
||||||
api,
|
api,
|
||||||
scrollWindow,
|
scrollWindow,
|
||||||
highlighted,
|
highlighted,
|
||||||
fontSize,
|
fontSize,
|
||||||
associations,
|
hideHover
|
||||||
groups,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const unreadContainerStyle = {
|
const unreadContainerStyle = {
|
||||||
@ -200,7 +331,7 @@ class ChatMessage extends Component<ChatMessageProps> {
|
|||||||
<Box
|
<Box
|
||||||
ref={this.props.innerRef}
|
ref={this.props.innerRef}
|
||||||
pt={renderSigil ? 2 : 0}
|
pt={renderSigil ? 2 : 0}
|
||||||
pb={isLastMessage ? 4 : 2}
|
pb={isLastMessage ? '20px' : 0}
|
||||||
className={containerClass}
|
className={containerClass}
|
||||||
backgroundColor={highlighted ? 'blue' : 'white'}
|
backgroundColor={highlighted ? 'blue' : 'white'}
|
||||||
style={style}
|
style={style}
|
||||||
@ -209,12 +340,14 @@ class ChatMessage extends Component<ChatMessageProps> {
|
|||||||
<DayBreak when={msg['time-sent']} shimTop={renderSigil} />
|
<DayBreak when={msg['time-sent']} shimTop={renderSigil} />
|
||||||
) : null}
|
) : null}
|
||||||
{renderSigil ? (
|
{renderSigil ? (
|
||||||
<>
|
<MessageWrapper {...messageProps}>
|
||||||
<MessageAuthor pb={'2px'} {...messageProps} />
|
<MessageAuthor pb={1} {...messageProps} />
|
||||||
<Message pl={5} pr={4} {...messageProps} />
|
<Message pl={'44px'} pr={4} {...messageProps} />
|
||||||
</>
|
</MessageWrapper>
|
||||||
) : (
|
) : (
|
||||||
<Message pl={5} pr={4} timestampHover {...messageProps} />
|
<MessageWrapper {...messageProps}>
|
||||||
|
<Message pl={'44px'} pr={4} timestampHover {...messageProps} />
|
||||||
|
</MessageWrapper>
|
||||||
)}
|
)}
|
||||||
<Box style={unreadContainerStyle}>
|
<Box style={unreadContainerStyle}>
|
||||||
{isLastRead ? (
|
{isLastRead ? (
|
||||||
@ -232,30 +365,36 @@ class ChatMessage extends Component<ChatMessageProps> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default React.forwardRef((props, ref) => <ChatMessage {...props} innerRef={ref} />);
|
export default React.forwardRef((props, ref) => (
|
||||||
|
<ChatMessage {...props} innerRef={ref} />
|
||||||
|
));
|
||||||
|
|
||||||
export const MessageAuthor = ({
|
export const MessageAuthor = ({
|
||||||
timestamp,
|
timestamp,
|
||||||
contacts,
|
|
||||||
msg,
|
msg,
|
||||||
group,
|
group,
|
||||||
api,
|
api,
|
||||||
associations,
|
|
||||||
groups,
|
|
||||||
history,
|
history,
|
||||||
scrollWindow,
|
scrollWindow,
|
||||||
|
showOurContact,
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
const osDark = useLocalState((state) => state.dark);
|
const osDark = useLocalState((state) => state.dark);
|
||||||
|
|
||||||
const theme = useSettingsState(s => s.display.theme);
|
const theme = useSettingsState((s) => s.display.theme);
|
||||||
const dark = theme === 'dark' || (theme === 'auto' && osDark)
|
const dark = theme === 'dark' || (theme === 'auto' && osDark);
|
||||||
|
const contacts = useContactState((state) => state.contacts);
|
||||||
|
|
||||||
const datestamp = moment
|
const datestamp = moment
|
||||||
.unix(msg['time-sent'] / 1000)
|
.unix(msg['time-sent'] / 1000)
|
||||||
.format(DATESTAMP_FORMAT);
|
.format(DATESTAMP_FORMAT);
|
||||||
const contact =
|
const contact =
|
||||||
`~${msg.author}` in contacts ? contacts[`~${msg.author}`] : false;
|
((msg.author === window.ship && showOurContact) ||
|
||||||
|
msg.author !== window.ship) &&
|
||||||
|
`~${msg.author}` in contacts
|
||||||
|
? contacts[`~${msg.author}`]
|
||||||
|
: false;
|
||||||
|
|
||||||
const showNickname = useShowNickname(contact);
|
const showNickname = useShowNickname(contact);
|
||||||
const { hideAvatars } = useSettingsState(selectCalmState);
|
const { hideAvatars } = useSettingsState(selectCalmState);
|
||||||
const shipName = showNickname ? contact.nickname : cite(msg.author);
|
const shipName = showNickname ? contact.nickname : cite(msg.author);
|
||||||
@ -297,31 +436,44 @@ export const MessageAuthor = ({
|
|||||||
contact?.avatar && !hideAvatars ? (
|
contact?.avatar && !hideAvatars ? (
|
||||||
<BaseImage
|
<BaseImage
|
||||||
display='inline-block'
|
display='inline-block'
|
||||||
|
referrerPolicy='no-referrer'
|
||||||
style={{ objectFit: 'cover' }}
|
style={{ objectFit: 'cover' }}
|
||||||
src={contact.avatar}
|
src={contact.avatar}
|
||||||
height={16}
|
height={24}
|
||||||
width={16}
|
width={24}
|
||||||
borderRadius={1}
|
borderRadius={1}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Sigil
|
<Box
|
||||||
ship={msg.author}
|
width={24}
|
||||||
size={16}
|
height={24}
|
||||||
color={color}
|
display='flex'
|
||||||
classes={sigilClass}
|
justifyContent='center'
|
||||||
icon
|
alignItems='center'
|
||||||
padding={2}
|
backgroundColor={color}
|
||||||
/>
|
borderRadius={1}
|
||||||
|
>
|
||||||
|
<Sigil
|
||||||
|
ship={msg.author}
|
||||||
|
size={12}
|
||||||
|
display='block'
|
||||||
|
color={color}
|
||||||
|
classes={sigilClass}
|
||||||
|
icon
|
||||||
|
padding={0}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Box display='flex' alignItems='center' {...rest}>
|
<Box display='flex' alignItems='flex-start' {...rest}>
|
||||||
<Box
|
<Box
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowOverlay(true);
|
setShowOverlay(true);
|
||||||
}}
|
}}
|
||||||
height={16}
|
height={24}
|
||||||
pr={2}
|
pr={2}
|
||||||
pl={2}
|
mt={'1px'}
|
||||||
|
pl={'12px'}
|
||||||
cursor='pointer'
|
cursor='pointer'
|
||||||
position='relative'
|
position='relative'
|
||||||
>
|
>
|
||||||
@ -348,12 +500,12 @@ export const MessageAuthor = ({
|
|||||||
pt={1}
|
pt={1}
|
||||||
pb={1}
|
pb={1}
|
||||||
display='flex'
|
display='flex'
|
||||||
alignItems='center'
|
alignItems='baseline'
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
fontSize={0}
|
fontSize={1}
|
||||||
mr={2}
|
mr={2}
|
||||||
flexShrink={0}
|
flexShrink={1}
|
||||||
mono={nameMono}
|
mono={nameMono}
|
||||||
fontWeight={nameMono ? '400' : '500'}
|
fontWeight={nameMono ? '400' : '500'}
|
||||||
cursor='pointer'
|
cursor='pointer'
|
||||||
@ -385,23 +537,23 @@ export const MessageAuthor = ({
|
|||||||
|
|
||||||
export const Message = ({
|
export const Message = ({
|
||||||
timestamp,
|
timestamp,
|
||||||
contacts,
|
|
||||||
msg,
|
msg,
|
||||||
group,
|
group,
|
||||||
api,
|
api,
|
||||||
associations,
|
|
||||||
groups,
|
|
||||||
scrollWindow,
|
scrollWindow,
|
||||||
timestampHover,
|
timestampHover,
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
const { hovering, bind } = useHovering();
|
const { hovering, bind } = useHovering();
|
||||||
|
const contacts = useContactState((state) => state.contacts);
|
||||||
return (
|
return (
|
||||||
<Box position='relative' {...rest}>
|
<Box position='relative' {...rest}>
|
||||||
{timestampHover ? (
|
{timestampHover ? (
|
||||||
<Text
|
<Text
|
||||||
display={hovering ? 'block' : 'none'}
|
display={hovering ? 'block' : 'none'}
|
||||||
position='absolute'
|
position='absolute'
|
||||||
|
width='36px'
|
||||||
|
textAlign='right'
|
||||||
left='0'
|
left='0'
|
||||||
top='3px'
|
top='3px'
|
||||||
fontSize={0}
|
fontSize={0}
|
||||||
@ -418,8 +570,7 @@ export const Message = ({
|
|||||||
case 'text':
|
case 'text':
|
||||||
return (
|
return (
|
||||||
<TextContent
|
<TextContent
|
||||||
associations={associations}
|
key={i}
|
||||||
groups={groups}
|
|
||||||
api={api}
|
api={api}
|
||||||
fontSize={1}
|
fontSize={1}
|
||||||
lineHeight={'20px'}
|
lineHeight={'20px'}
|
||||||
@ -427,10 +578,11 @@ export const Message = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'code':
|
case 'code':
|
||||||
return <CodeContent content={content} />;
|
return <CodeContent key={i} content={content} />;
|
||||||
case 'url':
|
case 'url':
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
key={i}
|
||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
fontSize={1}
|
fontSize={1}
|
||||||
lineHeight='20px'
|
lineHeight='20px'
|
||||||
@ -464,9 +616,10 @@ export const Message = ({
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
case 'mention':
|
case 'mention':
|
||||||
const first = (i) => (i === 0);
|
const first = (i) => i === 0;
|
||||||
return (
|
return (
|
||||||
<Mention
|
<Mention
|
||||||
|
key={i}
|
||||||
first={first(i)}
|
first={first(i)}
|
||||||
group={group}
|
group={group}
|
||||||
scrollWindow={scrollWindow}
|
scrollWindow={scrollWindow}
|
||||||
|
@ -20,6 +20,10 @@ import VirtualScroller from '~/views/components/VirtualScroller';
|
|||||||
|
|
||||||
import ChatMessage, { MessagePlaceholder } from './ChatMessage';
|
import ChatMessage, { MessagePlaceholder } from './ChatMessage';
|
||||||
import { UnreadNotice } from './unread-notice';
|
import { UnreadNotice } from './unread-notice';
|
||||||
|
import withState from '~/logic/lib/withState';
|
||||||
|
import useGroupState from '~/logic/state/group';
|
||||||
|
import useMetadataState from '~/logic/state/metadata';
|
||||||
|
import useGraphState from '~/logic/state/graph';
|
||||||
|
|
||||||
const INITIAL_LOAD = 20;
|
const INITIAL_LOAD = 20;
|
||||||
const DEFAULT_BACKLOG_SIZE = 100;
|
const DEFAULT_BACKLOG_SIZE = 100;
|
||||||
@ -32,15 +36,13 @@ type ChatWindowProps = RouteComponentProps<{
|
|||||||
}> & {
|
}> & {
|
||||||
unreadCount: number;
|
unreadCount: number;
|
||||||
graph: Graph;
|
graph: Graph;
|
||||||
contacts: Contacts;
|
graphSize: number;
|
||||||
association: Association;
|
association: Association;
|
||||||
group: Group;
|
group: Group;
|
||||||
ship: Patp;
|
ship: Patp;
|
||||||
station: any;
|
station: any;
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
scrollTo?: number;
|
scrollTo?: number;
|
||||||
associations: Associations;
|
|
||||||
groups: Groups;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ChatWindowState {
|
interface ChatWindowState {
|
||||||
@ -52,16 +54,14 @@ interface ChatWindowState {
|
|||||||
|
|
||||||
const virtScrollerStyle = { height: '100%' };
|
const virtScrollerStyle = { height: '100%' };
|
||||||
|
|
||||||
export default class ChatWindow extends Component<
|
class ChatWindow extends Component<
|
||||||
ChatWindowProps,
|
ChatWindowProps,
|
||||||
ChatWindowState
|
ChatWindowState
|
||||||
> {
|
> {
|
||||||
private virtualList: VirtualScroller | null;
|
private virtualList: VirtualScroller | null;
|
||||||
private unreadMarkerRef: React.RefObject<HTMLDivElement>;
|
private unreadMarkerRef: React.RefObject<HTMLDivElement>;
|
||||||
private prevSize = 0;
|
private prevSize = 0;
|
||||||
private loadedNewest = false;
|
private unreadSet = false;
|
||||||
private loadedOldest = false;
|
|
||||||
private fetchPending = false;
|
|
||||||
|
|
||||||
INITIALIZATION_MAX_TIME = 100;
|
INITIALIZATION_MAX_TIME = 100;
|
||||||
|
|
||||||
@ -99,6 +99,10 @@ export default class ChatWindow extends Component<
|
|||||||
|
|
||||||
calculateUnreadIndex() {
|
calculateUnreadIndex() {
|
||||||
const { graph, unreadCount } = this.props;
|
const { graph, unreadCount } = this.props;
|
||||||
|
const { state } = this;
|
||||||
|
if(state.unreadIndex.neq(bigInt.zero)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const unreadIndex = graph.keys()[unreadCount];
|
const unreadIndex = graph.keys()[unreadCount];
|
||||||
if (!unreadIndex || unreadCount === 0) {
|
if (!unreadIndex || unreadCount === 0) {
|
||||||
this.setState({
|
this.setState({
|
||||||
@ -111,6 +115,13 @@ export default class ChatWindow extends Component<
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dismissedInitialUnread() {
|
||||||
|
const { unreadCount, graph } = this.props;
|
||||||
|
|
||||||
|
return this.state.unreadIndex.neq(bigInt.zero) &&
|
||||||
|
this.state.unreadIndex.neq(graph.keys()?.[unreadCount]?.[0] ?? bigInt.zero);
|
||||||
|
}
|
||||||
|
|
||||||
handleWindowBlur() {
|
handleWindowBlur() {
|
||||||
this.setState({ idle: true });
|
this.setState({ idle: true });
|
||||||
}
|
}
|
||||||
@ -123,10 +134,22 @@ export default class ChatWindow extends Component<
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: ChatWindowProps, prevState) {
|
componentDidUpdate(prevProps: ChatWindowProps, prevState) {
|
||||||
const { history, graph, unreadCount, station } = this.props;
|
const { history, graph, unreadCount, graphSize, station } = this.props;
|
||||||
|
if(unreadCount === 0 && prevProps.unreadCount !== unreadCount) {
|
||||||
|
this.unreadSet = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.prevSize !== graphSize) {
|
||||||
|
this.prevSize = graphSize;
|
||||||
|
if(this.state.unreadIndex.eq(bigInt.zero)) {
|
||||||
|
this.calculateUnreadIndex();
|
||||||
|
}
|
||||||
|
if(this.unreadSet &&
|
||||||
|
this.dismissedInitialUnread() &&
|
||||||
|
this.virtualList?.startOffset() < 5) {
|
||||||
|
this.dismissUnread();
|
||||||
|
}
|
||||||
|
|
||||||
if (graph.size !== prevProps.graph.size && this.fetchPending) {
|
|
||||||
this.fetchPending = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (unreadCount > prevProps.unreadCount) {
|
if (unreadCount > prevProps.unreadCount) {
|
||||||
@ -146,6 +169,12 @@ export default class ChatWindow extends Component<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onBottomLoaded = () => {
|
||||||
|
if(this.state.unreadIndex.eq(bigInt.zero)) {
|
||||||
|
this.calculateUnreadIndex();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
scrollToUnread() {
|
scrollToUnread() {
|
||||||
const { unreadIndex } = this.state;
|
const { unreadIndex } = this.state;
|
||||||
if (unreadIndex.eq(bigInt.zero)) {
|
if (unreadIndex.eq(bigInt.zero)) {
|
||||||
@ -170,30 +199,28 @@ export default class ChatWindow extends Component<
|
|||||||
|
|
||||||
fetchMessages = async (newer: boolean): Promise<boolean> => {
|
fetchMessages = async (newer: boolean): Promise<boolean> => {
|
||||||
const { api, station, graph } = this.props;
|
const { api, station, graph } = this.props;
|
||||||
if(this.fetchPending) {
|
const pageSize = 100;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
this.fetchPending = true;
|
|
||||||
|
|
||||||
const [, , ship, name] = station.split('/');
|
const [, , ship, name] = station.split('/');
|
||||||
const currSize = graph.size;
|
const expectedSize = graph.size + pageSize;
|
||||||
if (newer) {
|
if (newer) {
|
||||||
const [index] = graph.peekLargest()!;
|
const [index] = graph.peekLargest()!;
|
||||||
await api.graph.getYoungerSiblings(
|
await api.graph.getYoungerSiblings(
|
||||||
ship,
|
ship,
|
||||||
name,
|
name,
|
||||||
100,
|
pageSize,
|
||||||
`/${index.toString()}`
|
`/${index.toString()}`
|
||||||
);
|
);
|
||||||
|
return expectedSize !== graph.size;
|
||||||
} else {
|
} else {
|
||||||
const [index] = graph.peekSmallest()!;
|
const [index] = graph.peekSmallest()!;
|
||||||
await api.graph.getOlderSiblings(ship, name, 100, `/${index.toString()}`);
|
await api.graph.getOlderSiblings(ship, name, pageSize, `/${index.toString()}`);
|
||||||
this.calculateUnreadIndex();
|
const done = expectedSize !== graph.size;
|
||||||
|
if(done) {
|
||||||
|
this.calculateUnreadIndex();
|
||||||
|
}
|
||||||
|
return done;
|
||||||
}
|
}
|
||||||
this.fetchPending = false;
|
|
||||||
return currSize === graph.size;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onScroll = ({ scrollTop, scrollHeight, windowHeight }) => {
|
onScroll = ({ scrollTop, scrollHeight, windowHeight }) => {
|
||||||
@ -208,7 +235,7 @@ export default class ChatWindow extends Component<
|
|||||||
api,
|
api,
|
||||||
association,
|
association,
|
||||||
group,
|
group,
|
||||||
contacts,
|
showOurContact,
|
||||||
graph,
|
graph,
|
||||||
history,
|
history,
|
||||||
groups,
|
groups,
|
||||||
@ -218,13 +245,14 @@ export default class ChatWindow extends Component<
|
|||||||
const messageProps = {
|
const messageProps = {
|
||||||
association,
|
association,
|
||||||
group,
|
group,
|
||||||
contacts,
|
showOurContact,
|
||||||
unreadMarkerRef,
|
unreadMarkerRef,
|
||||||
history,
|
history,
|
||||||
api,
|
api,
|
||||||
groups,
|
groups,
|
||||||
associations
|
associations
|
||||||
};
|
};
|
||||||
|
|
||||||
const msg = graph.get(index)?.post;
|
const msg = graph.get(index)?.post;
|
||||||
if (!msg) return null;
|
if (!msg) return null;
|
||||||
if (!this.state.initialized) {
|
if (!this.state.initialized) {
|
||||||
@ -255,6 +283,7 @@ export default class ChatWindow extends Component<
|
|||||||
msg,
|
msg,
|
||||||
...messageProps
|
...messageProps
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChatMessage
|
<ChatMessage
|
||||||
key={index.toString()}
|
key={index.toString()}
|
||||||
@ -270,15 +299,13 @@ export default class ChatWindow extends Component<
|
|||||||
const {
|
const {
|
||||||
unreadCount,
|
unreadCount,
|
||||||
api,
|
api,
|
||||||
ship,
|
|
||||||
station,
|
|
||||||
association,
|
association,
|
||||||
group,
|
group,
|
||||||
contacts,
|
|
||||||
graph,
|
graph,
|
||||||
history,
|
history,
|
||||||
groups,
|
groups,
|
||||||
associations,
|
associations,
|
||||||
|
showOurContact,
|
||||||
pendingSize
|
pendingSize
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@ -286,19 +313,21 @@ export default class ChatWindow extends Component<
|
|||||||
const messageProps = {
|
const messageProps = {
|
||||||
association,
|
association,
|
||||||
group,
|
group,
|
||||||
contacts,
|
|
||||||
unreadMarkerRef,
|
unreadMarkerRef,
|
||||||
history,
|
history,
|
||||||
api,
|
api,
|
||||||
groups,
|
|
||||||
associations
|
associations
|
||||||
};
|
};
|
||||||
const unreadIndex = graph.keys()[this.props.unreadCount];
|
const unreadMsg = graph.get(this.state.unreadIndex);
|
||||||
const unreadMsg = unreadIndex && graph.get(unreadIndex);
|
|
||||||
|
// hack to force a re-render when we toggle showing contact
|
||||||
|
const contactsModified =
|
||||||
|
showOurContact ? 0 : 100;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col height='100%' overflow='hidden' position='relative'>
|
<Col height='100%' overflow='hidden' position='relative'>
|
||||||
<UnreadNotice
|
{ this.dismissedInitialUnread() &&
|
||||||
|
(<UnreadNotice
|
||||||
unreadCount={unreadCount}
|
unreadCount={unreadCount}
|
||||||
unreadMsg={
|
unreadMsg={
|
||||||
unreadCount === 1 &&
|
unreadCount === 1 &&
|
||||||
@ -309,7 +338,7 @@ export default class ChatWindow extends Component<
|
|||||||
}
|
}
|
||||||
dismissUnread={this.dismissUnread}
|
dismissUnread={this.dismissUnread}
|
||||||
onClick={this.scrollToUnread}
|
onClick={this.scrollToUnread}
|
||||||
/>
|
/>)}
|
||||||
<VirtualScroller
|
<VirtualScroller
|
||||||
ref={(list) => {
|
ref={(list) => {
|
||||||
this.virtualList = list;
|
this.virtualList = list;
|
||||||
@ -318,10 +347,11 @@ export default class ChatWindow extends Component<
|
|||||||
origin='bottom'
|
origin='bottom'
|
||||||
style={virtScrollerStyle}
|
style={virtScrollerStyle}
|
||||||
onStartReached={this.setActive}
|
onStartReached={this.setActive}
|
||||||
|
onBottomLoaded={this.onBottomLoaded}
|
||||||
onScroll={this.onScroll}
|
onScroll={this.onScroll}
|
||||||
data={graph}
|
data={graph}
|
||||||
size={graph.size}
|
size={graph.size}
|
||||||
pendingSize={pendingSize}
|
pendingSize={pendingSize + contactsModified}
|
||||||
id={association.resource}
|
id={association.resource}
|
||||||
averageHeight={22}
|
averageHeight={22}
|
||||||
renderer={this.renderer}
|
renderer={this.renderer}
|
||||||
@ -331,3 +361,9 @@ export default class ChatWindow extends Component<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default withState(ChatWindow, [
|
||||||
|
[useGroupState, ['groups']],
|
||||||
|
[useMetadataState, ['associations']],
|
||||||
|
[useGraphState, ['pendingSize']]
|
||||||
|
]);
|
||||||
|
@ -199,6 +199,7 @@ export default class ChatEditor extends Component {
|
|||||||
width='calc(100% - 88px)'
|
width='calc(100% - 88px)'
|
||||||
className={inCodeMode ? 'chat code' : 'chat'}
|
className={inCodeMode ? 'chat code' : 'chat'}
|
||||||
color="black"
|
color="black"
|
||||||
|
overflow='auto'
|
||||||
>
|
>
|
||||||
{MOBILE_BROWSER_REGEX.test(navigator.userAgent)
|
{MOBILE_BROWSER_REGEX.test(navigator.userAgent)
|
||||||
? <MobileBox
|
? <MobileBox
|
||||||
|
@ -91,7 +91,7 @@ const MessageMarkdown = React.memo((props) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return lines.map((line, i) => (
|
return lines.map((line, i) => (
|
||||||
<>
|
<React.Fragment key={i}>
|
||||||
{i !== 0 && <Row height={2} />}
|
{i !== 0 && <Row height={2} />}
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
{...rest}
|
{...rest}
|
||||||
@ -123,7 +123,7 @@ const MessageMarkdown = React.memo((props) => {
|
|||||||
]
|
]
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</>
|
</React.Fragment>
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -145,8 +145,6 @@ export default function TextContent(props) {
|
|||||||
<GroupLink
|
<GroupLink
|
||||||
resource={resource}
|
resource={resource}
|
||||||
api={props.api}
|
api={props.api}
|
||||||
associations={props.associations}
|
|
||||||
groups={props.groups}
|
|
||||||
pl='2'
|
pl='2'
|
||||||
border='1'
|
border='1'
|
||||||
borderRadius='2'
|
borderRadius='2'
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { Box, Text } from '@tlon/indigo-react';
|
import { Box, Text, Center, Icon } from '@tlon/indigo-react';
|
||||||
import VisibilitySensor from 'react-visibility-sensor';
|
import VisibilitySensor from 'react-visibility-sensor';
|
||||||
|
|
||||||
import Timestamp from '~/views/components/Timestamp';
|
import Timestamp from '~/views/components/Timestamp';
|
||||||
@ -8,51 +8,67 @@ import Timestamp from '~/views/components/Timestamp';
|
|||||||
export const UnreadNotice = (props) => {
|
export const UnreadNotice = (props) => {
|
||||||
const { unreadCount, unreadMsg, dismissUnread, onClick } = props;
|
const { unreadCount, unreadMsg, dismissUnread, onClick } = props;
|
||||||
|
|
||||||
if (!unreadMsg || (unreadCount === 0)) {
|
if (!unreadMsg || unreadCount === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const stamp = moment.unix(unreadMsg.post['time-sent'] / 1000);
|
const stamp = moment.unix(unreadMsg.post['time-sent'] / 1000);
|
||||||
|
|
||||||
let datestamp = moment.unix(unreadMsg.post['time-sent'] / 1000).format('YYYY.M.D');
|
let datestamp = moment
|
||||||
const timestamp = moment.unix(unreadMsg.post['time-sent'] / 1000).format('HH:mm');
|
.unix(unreadMsg.post['time-sent'] / 1000)
|
||||||
|
.format('YYYY.M.D');
|
||||||
|
const timestamp = moment
|
||||||
|
.unix(unreadMsg.post['time-sent'] / 1000)
|
||||||
|
.format('HH:mm');
|
||||||
|
|
||||||
if (datestamp === moment().format('YYYY.M.D')) {
|
if (datestamp === moment().format('YYYY.M.D')) {
|
||||||
datestamp = null;
|
datestamp = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box style={{ left: '0px', top: '0px' }}
|
<Box
|
||||||
p='4'
|
style={{ left: '0px', top: '0px' }}
|
||||||
|
p='12px'
|
||||||
width='100%'
|
width='100%'
|
||||||
position='absolute'
|
position='absolute'
|
||||||
zIndex='1'
|
zIndex='1'
|
||||||
className='unread-notice'
|
className='unread-notice'
|
||||||
>
|
>
|
||||||
<Box
|
<Center>
|
||||||
backgroundColor='white'
|
<Box backgroundColor='white' borderRadius='2'>
|
||||||
display='flex'
|
<Box
|
||||||
alignItems='center'
|
backgroundColor='washedBlue'
|
||||||
p='2'
|
display='flex'
|
||||||
fontSize='0'
|
alignItems='center'
|
||||||
justifyContent='space-between'
|
p='2'
|
||||||
borderRadius='1'
|
fontSize='0'
|
||||||
border='1'
|
justifyContent='space-between'
|
||||||
borderColor='blue'>
|
borderRadius='3'
|
||||||
<Text flexShrink='1' textOverflow='ellipsis' whiteSpace='pre' overflow='hidden' display='flex' cursor='pointer' onClick={onClick}>
|
border='1'
|
||||||
{unreadCount} new message{unreadCount > 1 ? 's' : ''} since{' '}
|
borderColor='lightBlue'
|
||||||
<Timestamp stamp={stamp} color='blue' date={true} fontSize={1} />
|
>
|
||||||
</Text>
|
<Text
|
||||||
<Text
|
textOverflow='ellipsis'
|
||||||
ml='4'
|
whiteSpace='pre'
|
||||||
color='blue'
|
overflow='hidden'
|
||||||
cursor='pointer'
|
display='flex'
|
||||||
textAlign='right'
|
cursor='pointer'
|
||||||
flexShrink='0'
|
onClick={onClick}
|
||||||
onClick={dismissUnread}>
|
>
|
||||||
Mark as Read
|
{unreadCount} new message{unreadCount > 1 ? 's' : ''} since{' '}
|
||||||
</Text>
|
<Timestamp stamp={stamp} color='black' date={true} fontSize={1} />
|
||||||
</Box>
|
</Text>
|
||||||
|
<Icon
|
||||||
|
icon='X'
|
||||||
|
ml='4'
|
||||||
|
color='black'
|
||||||
|
cursor='pointer'
|
||||||
|
textAlign='right'
|
||||||
|
onClick={dismissUnread}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Center>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
@ -1,59 +1,55 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { Switch, Route } from 'react-router-dom';
|
import { Switch, Route, useHistory } from 'react-router-dom';
|
||||||
import { Center, Text } from "@tlon/indigo-react";
|
import { Center, Text } from "@tlon/indigo-react";
|
||||||
import { deSig } from '~/logic/lib/util';
|
import { deSig } from '~/logic/lib/util';
|
||||||
|
import useGraphState from '~/logic/state/graph';
|
||||||
|
import useMetadataState from '~/logic/state/metadata';
|
||||||
|
|
||||||
|
const GraphApp = (props) => {
|
||||||
|
const associations= useMetadataState(state => state.associations);
|
||||||
|
const graphKeys = useGraphState(state => state.graphKeys);
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const { api } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Switch>
|
||||||
|
<Route exact path="/~graph/join/ship/:ship/:name/:module?"
|
||||||
|
render={(props) => {
|
||||||
|
const resource =
|
||||||
|
`${deSig(props.match.params.ship)}/${props.match.params.name}`;
|
||||||
|
const { ship, name } = props.match.params;
|
||||||
|
const path = `/ship/~${deSig(ship)}/${name}`;
|
||||||
|
const association = associations.graph[path];
|
||||||
|
|
||||||
|
|
||||||
export default class GraphApp extends PureComponent {
|
const autoJoin = () => {
|
||||||
render() {
|
try {
|
||||||
const { props } = this;
|
api.graph.joinGraph(
|
||||||
const contacts = props.contacts ? props.contacts : {};
|
`~${deSig(props.match.params.ship)}`,
|
||||||
const groups = props.groups ? props.groups : {};
|
props.match.params.name
|
||||||
const associations =
|
);
|
||||||
props.associations ? props.associations : { graph: {}, contacts: {} };
|
|
||||||
const graphKeys = props.graphKeys || new Set([]);
|
|
||||||
const graphs = props.graphs || {};
|
|
||||||
|
|
||||||
const { api } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Switch>
|
|
||||||
<Route exact path="/~graph/join/ship/:ship/:name/:module?"
|
|
||||||
render={ (props) => {
|
|
||||||
const resource =
|
|
||||||
`${deSig(props.match.params.ship)}/${props.match.params.name}`;
|
|
||||||
const { ship, name } = props.match.params;
|
|
||||||
const path = `/ship/~${deSig(ship)}/${name}`;
|
|
||||||
const association = associations.graph[path];
|
|
||||||
|
|
||||||
|
|
||||||
const autoJoin = () => {
|
} catch(err) {
|
||||||
try {
|
setTimeout(autoJoin, 2000);
|
||||||
api.graph.joinGraph(
|
|
||||||
`~${deSig(props.match.params.ship)}`,
|
|
||||||
props.match.params.name
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
} catch(err) {
|
|
||||||
setTimeout(autoJoin, 2000);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if(!graphKeys.has(resource)) {
|
|
||||||
autoJoin();
|
|
||||||
} else if(!!association) {
|
|
||||||
props.history.push(`/~landscape/home/resource/${association.metadata.module}${path}`);
|
|
||||||
}
|
}
|
||||||
return (
|
};
|
||||||
<Center width="100%" height="100%">
|
|
||||||
<Text fontSize={1}>Redirecting...</Text>
|
if(!graphKeys.has(resource)) {
|
||||||
</Center>
|
autoJoin();
|
||||||
);
|
} else if(!!association) {
|
||||||
}}
|
history.push(`/~landscape/home/resource/${association.metadata.module}${path}`);
|
||||||
/>
|
}
|
||||||
</Switch>
|
return (
|
||||||
);
|
<Center width="100%" height="100%">
|
||||||
}
|
<Text fontSize={1}>Redirecting...</Text>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Switch>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default GraphApp;
|
@ -1,13 +1,12 @@
|
|||||||
import React, { useState, useMemo, useEffect } from 'react';
|
import React, { useState, useMemo, useEffect } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { useHistory } from 'react-router-dom';
|
|
||||||
import f from 'lodash/fp';
|
import f from 'lodash/fp';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
import { Col, Button, Box, Row, Icon, Text } from '@tlon/indigo-react';
|
import { Col, Button, Box, Row, Icon, Text } from '@tlon/indigo-react';
|
||||||
|
|
||||||
import './css/custom.css';
|
import './css/custom.css';
|
||||||
|
import useContactState from '~/logic/state/contact';
|
||||||
import Tiles from './components/tiles';
|
import Tiles from './components/tiles';
|
||||||
import Tile from './components/tiles/tile';
|
import Tile from './components/tiles/tile';
|
||||||
import Groups from './components/Groups';
|
import Groups from './components/Groups';
|
||||||
@ -20,6 +19,7 @@ import { NewGroup } from "~/views/landscape/components/NewGroup";
|
|||||||
import { JoinGroup } from "~/views/landscape/components/JoinGroup";
|
import { JoinGroup } from "~/views/landscape/components/JoinGroup";
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import useLocalState from "~/logic/state/local";
|
import useLocalState from "~/logic/state/local";
|
||||||
|
import useHarkState from '~/logic/state/hark';
|
||||||
import { useWaitForProps } from '~/logic/lib/useWaitForProps';
|
import { useWaitForProps } from '~/logic/lib/useWaitForProps';
|
||||||
import { useQuery } from "~/logic/lib/useQuery";
|
import { useQuery } from "~/logic/lib/useQuery";
|
||||||
import {
|
import {
|
||||||
@ -30,7 +30,9 @@ import {
|
|||||||
TUTORIAL_CHAT,
|
TUTORIAL_CHAT,
|
||||||
TUTORIAL_LINKS
|
TUTORIAL_LINKS
|
||||||
} from '~/logic/lib/tutorialModal';
|
} from '~/logic/lib/tutorialModal';
|
||||||
|
import useLaunchState from '~/logic/state/launch';
|
||||||
import useSettingsState, { selectCalmState } from '~/logic/state/settings';
|
import useSettingsState, { selectCalmState } from '~/logic/state/settings';
|
||||||
|
import useMetadataState from '~/logic/state/metadata';
|
||||||
|
|
||||||
|
|
||||||
const ScrollbarLessBox = styled(Box)`
|
const ScrollbarLessBox = styled(Box)`
|
||||||
@ -44,9 +46,22 @@ const ScrollbarLessBox = styled(Box)`
|
|||||||
const tutSelector = f.pick(['tutorialProgress', 'nextTutStep', 'hideGroups']);
|
const tutSelector = f.pick(['tutorialProgress', 'nextTutStep', 'hideGroups']);
|
||||||
|
|
||||||
export default function LaunchApp(props) {
|
export default function LaunchApp(props) {
|
||||||
const history = useHistory();
|
const connection = { props };
|
||||||
const [hashText, setHashText] = useState(props.baseHash);
|
const baseHash = useLaunchState(state => state.baseHash);
|
||||||
|
const [hashText, setHashText] = useState(baseHash);
|
||||||
const [exitingTut, setExitingTut] = useState(false);
|
const [exitingTut, setExitingTut] = useState(false);
|
||||||
|
const seen = useSettingsState(s => s?.tutorial?.seen) ?? true;
|
||||||
|
const associations = useMetadataState(s => s.associations);
|
||||||
|
const contacts = useContactState(state => state.contacts);
|
||||||
|
const hasLoaded = useMemo(() => Boolean(connection === "connected"), [connection]);
|
||||||
|
const notificationsCount = useHarkState(state => state.notificationsCount);
|
||||||
|
const calmState = useSettingsState(selectCalmState);
|
||||||
|
const { hideUtilities } = calmState;
|
||||||
|
const { tutorialProgress, nextTutStep } = useLocalState(tutSelector);
|
||||||
|
let { hideGroups } = useLocalState(tutSelector);
|
||||||
|
!hideGroups ? { hideGroups } = calmState : null;
|
||||||
|
|
||||||
|
const waiter = useWaitForProps({ ...props, associations });
|
||||||
const hashBox = (
|
const hashBox = (
|
||||||
<Box
|
<Box
|
||||||
position={["relative", "absolute"]}
|
position={["relative", "absolute"]}
|
||||||
@ -60,15 +75,15 @@ export default function LaunchApp(props) {
|
|||||||
fontSize={0}
|
fontSize={0}
|
||||||
cursor="pointer"
|
cursor="pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
writeText(props.baseHash);
|
writeText(baseHash);
|
||||||
setHashText('copied');
|
setHashText('copied');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setHashText(props.baseHash);
|
setHashText(baseHash);
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box backgroundColor="washedGray" p={2}>
|
<Box backgroundColor="washedGray" p={2}>
|
||||||
<Text mono bold>{hashText || props.baseHash}</Text>
|
<Text mono bold>{hashText || baseHash}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
@ -77,7 +92,7 @@ export default function LaunchApp(props) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(query.get('tutorial')) {
|
if(query.get('tutorial')) {
|
||||||
if(hasTutorialGroup(props)) {
|
if(hasTutorialGroup({ associations })) {
|
||||||
nextTutStep();
|
nextTutStep();
|
||||||
} else {
|
} else {
|
||||||
showModal();
|
showModal();
|
||||||
@ -85,13 +100,6 @@ export default function LaunchApp(props) {
|
|||||||
}
|
}
|
||||||
}, [query]);
|
}, [query]);
|
||||||
|
|
||||||
const { hideUtilities } = useSettingsState(selectCalmState);
|
|
||||||
const { tutorialProgress, nextTutStep } = useLocalState(tutSelector);
|
|
||||||
let { hideGroups } = useLocalState(tutSelector);
|
|
||||||
!hideGroups ? { hideGroups } = useSettingsState(selectCalmState) : null;
|
|
||||||
|
|
||||||
const waiter = useWaitForProps(props);
|
|
||||||
|
|
||||||
const { modal, showModal } = useModal({
|
const { modal, showModal } = useModal({
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
maxWidth: '350px',
|
maxWidth: '350px',
|
||||||
@ -103,7 +111,7 @@ export default function LaunchApp(props) {
|
|||||||
};
|
};
|
||||||
const onContinue = async (e) => {
|
const onContinue = async (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if(!hasTutorialGroup(props)) {
|
if(!hasTutorialGroup({ associations })) {
|
||||||
await props.api.groups.join(TUTORIAL_HOST, TUTORIAL_GROUP);
|
await props.api.groups.join(TUTORIAL_HOST, TUTORIAL_GROUP);
|
||||||
await props.api.settings.putEntry('tutorial', 'joined', Date.now());
|
await props.api.settings.putEntry('tutorial', 'joined', Date.now());
|
||||||
await waiter(hasTutorialGroup);
|
await waiter(hasTutorialGroup);
|
||||||
@ -154,19 +162,17 @@ export default function LaunchApp(props) {
|
|||||||
</Col>
|
</Col>
|
||||||
)}
|
)}
|
||||||
});
|
});
|
||||||
const hasLoaded = useMemo(() => Object.keys(props.contacts).length > 0, [props.contacts]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const seenTutorial = _.get(props.settings, ['tutorial', 'seen'], true);
|
if(hasLoaded && !seen && tutorialProgress === 'hidden') {
|
||||||
if(hasLoaded && !seenTutorial && tutorialProgress === 'hidden') {
|
|
||||||
showModal();
|
showModal();
|
||||||
}
|
}
|
||||||
}, [props.settings, hasLoaded]);
|
}, [seen, hasLoaded]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Helmet defer={false}>
|
<Helmet defer={false}>
|
||||||
<title>{ props.notificationsCount ? `(${String(props.notificationsCount) }) `: '' }Landscape</title>
|
<title>{ notificationsCount ? `(${String(notificationsCount) }) `: '' }Landscape</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<ScrollbarLessBox height='100%' overflowY='scroll' display="flex" flexDirection="column">
|
<ScrollbarLessBox height='100%' overflowY='scroll' display="flex" flexDirection="column">
|
||||||
{modal}
|
{modal}
|
||||||
@ -196,11 +202,7 @@ export default function LaunchApp(props) {
|
|||||||
</Box>
|
</Box>
|
||||||
</Tile>
|
</Tile>
|
||||||
<Tiles
|
<Tiles
|
||||||
tiles={props.launch.tiles}
|
|
||||||
tileOrdering={props.launch.tileOrdering}
|
|
||||||
api={props.api}
|
api={props.api}
|
||||||
location={props.userLocation}
|
|
||||||
weather={props.weather}
|
|
||||||
/>
|
/>
|
||||||
<ModalButton
|
<ModalButton
|
||||||
icon="Plus"
|
icon="Plus"
|
||||||
@ -221,7 +223,7 @@ export default function LaunchApp(props) {
|
|||||||
</ModalButton>
|
</ModalButton>
|
||||||
</>}
|
</>}
|
||||||
{!hideGroups &&
|
{!hideGroups &&
|
||||||
(<Groups unreads={props.unreads} groups={props.groups} associations={props.associations} />)
|
(<Groups />)
|
||||||
}
|
}
|
||||||
</Box>
|
</Box>
|
||||||
<Box alignSelf="flex-start" display={["block", "none"]}>{hashBox}</Box>
|
<Box alignSelf="flex-start" display={["block", "none"]}>{hashBox}</Box>
|
||||||
|
@ -9,12 +9,13 @@ import { alphabeticalOrder } from '~/logic/lib/util';
|
|||||||
import { getUnreadCount, getNotificationCount } from '~/logic/lib/hark';
|
import { getUnreadCount, getNotificationCount } from '~/logic/lib/hark';
|
||||||
import Tile from '../components/tiles/tile';
|
import Tile from '../components/tiles/tile';
|
||||||
import { useTutorialModal } from '~/views/components/useTutorialModal';
|
import { useTutorialModal } from '~/views/components/useTutorialModal';
|
||||||
|
import useGroupState from '~/logic/state/group';
|
||||||
|
import useHarkState from '~/logic/state/hark';
|
||||||
|
import useMetadataState from '~/logic/state/metadata';
|
||||||
import { TUTORIAL_HOST, TUTORIAL_GROUP, TUTORIAL_GROUP_RESOURCE } from '~/logic/lib/tutorialModal';
|
import { TUTORIAL_HOST, TUTORIAL_GROUP, TUTORIAL_GROUP_RESOURCE } from '~/logic/lib/tutorialModal';
|
||||||
import useSettingsState, { selectCalmState, SettingsState } from '~/logic/state/settings';
|
import useSettingsState, { selectCalmState, SettingsState } from '~/logic/state/settings';
|
||||||
|
|
||||||
interface GroupsProps {
|
interface GroupsProps {}
|
||||||
associations: Associations;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sortGroupsAlph = (a: Association, b: Association) =>
|
const sortGroupsAlph = (a: Association, b: Association) =>
|
||||||
a.group === TUTORIAL_GROUP_RESOURCE
|
a.group === TUTORIAL_GROUP_RESOURCE
|
||||||
@ -40,10 +41,13 @@ const getGraphNotifications = (associations: Associations, unreads: Unreads) =>
|
|||||||
)(associations.graph);
|
)(associations.graph);
|
||||||
|
|
||||||
export default function Groups(props: GroupsProps & Parameters<typeof Box>[0]) {
|
export default function Groups(props: GroupsProps & Parameters<typeof Box>[0]) {
|
||||||
const { associations, unreads, inbox, ...boxProps } = props;
|
const { inbox, ...boxProps } = props;
|
||||||
|
const unreads = useHarkState(state => state.unreads);
|
||||||
|
const groupState = useGroupState(state => state.groups);
|
||||||
|
const associations = useMetadataState(state => state.associations);
|
||||||
|
|
||||||
const groups = Object.values(associations?.groups || {})
|
const groups = Object.values(associations?.groups || {})
|
||||||
.filter(e => e?.group in props.groups)
|
.filter(e => e?.group in groupState)
|
||||||
.sort(sortGroupsAlph);
|
.sort(sortGroupsAlph);
|
||||||
const graphUnreads = getGraphUnreads(associations || {}, unreads);
|
const graphUnreads = getGraphUnreads(associations || {}, unreads);
|
||||||
const graphNotifications = getGraphNotifications(associations || {}, unreads);
|
const graphNotifications = getGraphNotifications(associations || {}, unreads);
|
||||||
@ -87,15 +91,19 @@ function Group(props: GroupProps) {
|
|||||||
isTutorialGroup,
|
isTutorialGroup,
|
||||||
anchorRef
|
anchorRef
|
||||||
);
|
);
|
||||||
const { hideUnreads } = useSettingsState(selectCalmState)
|
const { hideUnreads } = useSettingsState(selectCalmState);
|
||||||
const joined = useSettingsState(selectJoined);
|
const joined = useSettingsState(selectJoined);
|
||||||
|
const days = Math.max(0, Math.floor(moment.duration(moment(joined)
|
||||||
|
.add(14, 'days')
|
||||||
|
.diff(moment()))
|
||||||
|
.as('days'))) || 0;
|
||||||
return (
|
return (
|
||||||
<Tile ref={anchorRef} position="relative" bg={isTutorialGroup ? 'lightBlue' : undefined} to={`/~landscape${path}`} gridColumnStart={first ? '1' : null}>
|
<Tile ref={anchorRef} position="relative" bg={isTutorialGroup ? 'lightBlue' : undefined} to={`/~landscape${path}`} gridColumnStart={first ? '1' : null}>
|
||||||
<Col height="100%" justifyContent="space-between">
|
<Col height="100%" justifyContent="space-between">
|
||||||
<Text>{title}</Text>
|
<Text>{title}</Text>
|
||||||
{!hideUnreads && (<Col>
|
{!hideUnreads && (<Col>
|
||||||
{isTutorialGroup && joined &&
|
{isTutorialGroup && joined &&
|
||||||
(<Text>{Math.floor(moment.duration(moment(joined).add(14, 'days').diff(moment())).as('days'))} days remaining</Text>)
|
(<Text>{days} day{days !== 1 && 's'} remaining</Text>)
|
||||||
}
|
}
|
||||||
{updates > 0 &&
|
{updates > 0 &&
|
||||||
(<Text mt="1" color="blue">{updates} update{updates !== 1 && 's'} </Text>)
|
(<Text mt="1" color="blue">{updates} update{updates !== 1 && 's'} </Text>)
|
||||||
|
@ -5,50 +5,50 @@ import CustomTile from './tiles/custom';
|
|||||||
import ClockTile from './tiles/clock';
|
import ClockTile from './tiles/clock';
|
||||||
import WeatherTile from './tiles/weather';
|
import WeatherTile from './tiles/weather';
|
||||||
|
|
||||||
export default class Tiles extends React.PureComponent {
|
import useLaunchState from '~/logic/state/launch';
|
||||||
render() {
|
|
||||||
const { props } = this;
|
|
||||||
|
|
||||||
const tiles = props.tileOrdering.filter((key) => {
|
const Tiles = (props) => {
|
||||||
const tile = props.tiles[key];
|
const weather = useLaunchState(state => state.weather);
|
||||||
|
const tileOrdering = useLaunchState(state => state.tileOrdering);
|
||||||
|
const tileState = useLaunchState(state => state.tiles);
|
||||||
|
const tiles = tileOrdering.filter((key) => {
|
||||||
|
const tile = tileState[key];
|
||||||
|
|
||||||
return tile.isShown;
|
return tile.isShown;
|
||||||
}).map((key) => {
|
}).map((key) => {
|
||||||
const tile = props.tiles[key];
|
const tile = tileState[key];
|
||||||
if ('basic' in tile.type) {
|
if ('basic' in tile.type) {
|
||||||
const basic = tile.type.basic;
|
const basic = tile.type.basic;
|
||||||
|
return (
|
||||||
|
<BasicTile
|
||||||
|
key={key}
|
||||||
|
title={basic.title}
|
||||||
|
iconUrl={basic.iconUrl}
|
||||||
|
linkedUrl={basic.linkedUrl}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if ('custom' in tile.type) {
|
||||||
|
if (key === 'weather') {
|
||||||
return (
|
return (
|
||||||
<BasicTile
|
<WeatherTile
|
||||||
key={key}
|
key={key}
|
||||||
title={basic.title}
|
api={props.api}
|
||||||
iconUrl={basic.iconUrl}
|
|
||||||
linkedUrl={basic.linkedUrl}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if ('custom' in tile.type) {
|
} else if (key === 'clock') {
|
||||||
if (key === 'weather') {
|
const location = weather && 'nearest-area' in weather ? weather['nearest-area'][0] : '';
|
||||||
return (
|
return (
|
||||||
<WeatherTile
|
<ClockTile key={key} location={location} />
|
||||||
key={key}
|
);
|
||||||
api={props.api}
|
|
||||||
weather={props.weather}
|
|
||||||
location={props.location}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (key === 'clock') {
|
|
||||||
const location = 'nearest-area' in props.weather ? props.weather['nearest-area'][0] : '';
|
|
||||||
return (
|
|
||||||
<ClockTile key={key} location={location} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return <CustomTile key={key} />;
|
|
||||||
}
|
}
|
||||||
});
|
} else {
|
||||||
|
return <CustomTile key={key} />;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>{tiles}</React.Fragment>
|
<>{tiles}</>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default Tiles;
|
||||||
|
@ -2,6 +2,8 @@ import React from 'react';
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { Box, Icon, Text, BaseAnchor, BaseInput } from '@tlon/indigo-react';
|
import { Box, Icon, Text, BaseAnchor, BaseInput } from '@tlon/indigo-react';
|
||||||
import ErrorBoundary from '~/views/components/ErrorBoundary';
|
import ErrorBoundary from '~/views/components/ErrorBoundary';
|
||||||
|
import withState from '~/logic/lib/withState';
|
||||||
|
import useLaunchState from '~/logic/state/launch';
|
||||||
|
|
||||||
import Tile from './tile';
|
import Tile from './tile';
|
||||||
|
|
||||||
@ -34,7 +36,7 @@ const imperialCountries = [
|
|||||||
'Liberia',
|
'Liberia',
|
||||||
];
|
];
|
||||||
|
|
||||||
export default class WeatherTile extends React.Component {
|
class WeatherTile extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
@ -289,3 +291,4 @@ export default class WeatherTile extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default withState(WeatherTile, [[useLaunchState]]);
|
||||||
|
@ -8,11 +8,14 @@ import { StoreState } from '~/logic/store/type';
|
|||||||
import { RouteComponentProps } from 'react-router-dom';
|
import { RouteComponentProps } from 'react-router-dom';
|
||||||
|
|
||||||
import { LinkItem } from './components/LinkItem';
|
import { LinkItem } from './components/LinkItem';
|
||||||
import { LinkWindow } from './LinkWindow';
|
import LinkWindow from './LinkWindow';
|
||||||
import { Comments } from '~/views/components/Comments';
|
import { Comments } from '~/views/components/Comments';
|
||||||
|
|
||||||
import './css/custom.css';
|
import './css/custom.css';
|
||||||
import { Association } from '@urbit/api/metadata';
|
import { Association } from '@urbit/api/metadata';
|
||||||
|
import useGraphState from '~/logic/state/graph';
|
||||||
|
import useMetadataState from '~/logic/state/metadata';
|
||||||
|
import useGroupState from '../../../logic/state/group';
|
||||||
|
|
||||||
const emptyMeasure = () => {};
|
const emptyMeasure = () => {};
|
||||||
|
|
||||||
@ -27,29 +30,24 @@ export function LinkResource(props: LinkResourceProps) {
|
|||||||
association,
|
association,
|
||||||
api,
|
api,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
graphs,
|
|
||||||
contacts,
|
|
||||||
groups,
|
|
||||||
associations,
|
|
||||||
graphKeys,
|
|
||||||
unreads,
|
|
||||||
graphTimesentMap,
|
|
||||||
storage,
|
|
||||||
history
|
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const rid = association.resource;
|
const rid = association.resource;
|
||||||
|
|
||||||
const relativePath = (p: string) => `${baseUrl}/resource/link${rid}${p}`;
|
const relativePath = (p: string) => `${baseUrl}/resource/link${rid}${p}`;
|
||||||
|
const associations = useMetadataState(state => state.associations);
|
||||||
|
|
||||||
const [, , ship, name] = rid.split('/');
|
const [, , ship, name] = rid.split('/');
|
||||||
const resourcePath = `${ship.slice(1)}/${name}`;
|
const resourcePath = `${ship.slice(1)}/${name}`;
|
||||||
const resource = associations.graph[rid]
|
const resource = associations.graph[rid]
|
||||||
? associations.graph[rid]
|
? associations.graph[rid]
|
||||||
: { metadata: {} };
|
: { metadata: {} };
|
||||||
|
const groups = useGroupState(state => state.groups);
|
||||||
const group = groups[resource?.group] || {};
|
const group = groups[resource?.group] || {};
|
||||||
|
|
||||||
|
const graphs = useGraphState(state => state.graphs);
|
||||||
const graph = graphs[resourcePath] || null;
|
const graph = graphs[resourcePath] || null;
|
||||||
|
const graphTimesentMap = useGraphState(state => state.graphTimesentMap);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api.graph.getGraph(ship, name);
|
api.graph.getGraph(ship, name);
|
||||||
@ -70,12 +68,9 @@ export function LinkResource(props: LinkResourceProps) {
|
|||||||
return (
|
return (
|
||||||
<LinkWindow
|
<LinkWindow
|
||||||
key={rid}
|
key={rid}
|
||||||
storage={storage}
|
|
||||||
association={resource}
|
association={resource}
|
||||||
contacts={contacts}
|
|
||||||
resource={resourcePath}
|
resource={resourcePath}
|
||||||
graph={graph}
|
graph={graph}
|
||||||
unreads={unreads}
|
|
||||||
baseUrl={resourceUrl}
|
baseUrl={resourceUrl}
|
||||||
group={group}
|
group={group}
|
||||||
path={resource.group}
|
path={resource.group}
|
||||||
@ -106,12 +101,10 @@ export function LinkResource(props: LinkResourceProps) {
|
|||||||
<Col width="100%" p={3} maxWidth="768px">
|
<Col width="100%" p={3} maxWidth="768px">
|
||||||
<Link to={resourceUrl}><Text px={3} bold>{'<- Back'}</Text></Link>
|
<Link to={resourceUrl}><Text px={3} bold>{'<- Back'}</Text></Link>
|
||||||
<LinkItem
|
<LinkItem
|
||||||
contacts={contacts}
|
|
||||||
key={node.post.index}
|
key={node.post.index}
|
||||||
resource={resourcePath}
|
resource={resourcePath}
|
||||||
node={node}
|
node={node}
|
||||||
baseUrl={resourceUrl}
|
baseUrl={resourceUrl}
|
||||||
unreads={unreads}
|
|
||||||
group={group}
|
group={group}
|
||||||
path={resource?.group}
|
path={resource?.group}
|
||||||
api={api}
|
api={api}
|
||||||
@ -124,8 +117,6 @@ export function LinkResource(props: LinkResourceProps) {
|
|||||||
comments={node}
|
comments={node}
|
||||||
resource={resourcePath}
|
resource={resourcePath}
|
||||||
association={association}
|
association={association}
|
||||||
unreads={unreads}
|
|
||||||
contacts={contacts}
|
|
||||||
api={api}
|
api={api}
|
||||||
editCommentId={editCommentId}
|
editCommentId={editCommentId}
|
||||||
history={props.history}
|
history={props.history}
|
||||||
|
@ -16,20 +16,20 @@ import { LinkItem } from "./components/LinkItem";
|
|||||||
import LinkSubmit from "./components/LinkSubmit";
|
import LinkSubmit from "./components/LinkSubmit";
|
||||||
import { isWriter } from "~/logic/lib/group";
|
import { isWriter } from "~/logic/lib/group";
|
||||||
import { StorageState } from "~/types";
|
import { StorageState } from "~/types";
|
||||||
|
import withState from "~/logic/lib/withState";
|
||||||
|
import useGraphState from "~/logic/state/graph";
|
||||||
|
|
||||||
interface LinkWindowProps {
|
interface LinkWindowProps {
|
||||||
association: Association;
|
association: Association;
|
||||||
contacts: Rolodex;
|
|
||||||
resource: string;
|
resource: string;
|
||||||
graph: Graph;
|
graph: Graph;
|
||||||
unreads: Unreads;
|
|
||||||
hideNicknames: boolean;
|
hideNicknames: boolean;
|
||||||
hideAvatars: boolean;
|
hideAvatars: boolean;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
group: Group;
|
group: Group;
|
||||||
path: string;
|
path: string;
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
storage: StorageState;
|
pendingSize: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const style = {
|
const style = {
|
||||||
@ -40,7 +40,7 @@ const style = {
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
};
|
};
|
||||||
|
|
||||||
export class LinkWindow extends Component<LinkWindowProps, {}> {
|
class LinkWindow extends Component<LinkWindowProps, {}> {
|
||||||
fetchLinks = async () => true;
|
fetchLinks = async () => true;
|
||||||
|
|
||||||
canWrite() {
|
canWrite() {
|
||||||
@ -75,7 +75,6 @@ export class LinkWindow extends Component<LinkWindowProps, {}> {
|
|||||||
px={3}
|
px={3}
|
||||||
>
|
>
|
||||||
<LinkSubmit
|
<LinkSubmit
|
||||||
storage={props.storage}
|
|
||||||
name={name}
|
name={name}
|
||||||
ship={ship.slice(1)}
|
ship={ship.slice(1)}
|
||||||
api={api}
|
api={api}
|
||||||
@ -89,7 +88,7 @@ export class LinkWindow extends Component<LinkWindowProps, {}> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { graph, api, association, storage, pendingSize } = this.props;
|
const { graph, api, association } = this.props;
|
||||||
const first = graph.peekLargest()?.[0];
|
const first = graph.peekLargest()?.[0];
|
||||||
const [, , ship, name] = association.resource.split("/");
|
const [, , ship, name] = association.resource.split("/");
|
||||||
if (!first) {
|
if (!first) {
|
||||||
@ -105,7 +104,6 @@ export class LinkWindow extends Component<LinkWindowProps, {}> {
|
|||||||
>
|
>
|
||||||
{this.canWrite() ? (
|
{this.canWrite() ? (
|
||||||
<LinkSubmit
|
<LinkSubmit
|
||||||
storage={storage}
|
|
||||||
name={name}
|
name={name}
|
||||||
ship={ship.slice(1)}
|
ship={ship.slice(1)}
|
||||||
api={api}
|
api={api}
|
||||||
@ -129,7 +127,7 @@ export class LinkWindow extends Component<LinkWindowProps, {}> {
|
|||||||
data={graph}
|
data={graph}
|
||||||
averageHeight={100}
|
averageHeight={100}
|
||||||
size={graph.size}
|
size={graph.size}
|
||||||
pendingSize={pendingSize}
|
pendingSize={this.props.pendingSize}
|
||||||
renderer={this.renderItem}
|
renderer={this.renderItem}
|
||||||
loadRows={this.fetchLinks}
|
loadRows={this.fetchLinks}
|
||||||
/>
|
/>
|
||||||
@ -137,3 +135,5 @@ export class LinkWindow extends Component<LinkWindowProps, {}> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default LinkWindow;
|
@ -10,6 +10,7 @@ import { roleForShip } from '~/logic/lib/group';
|
|||||||
import GlobalApi from '~/logic/api/global';
|
import GlobalApi from '~/logic/api/global';
|
||||||
import { Dropdown } from '~/views/components/Dropdown';
|
import { Dropdown } from '~/views/components/Dropdown';
|
||||||
import RemoteContent from '~/views/components/RemoteContent';
|
import RemoteContent from '~/views/components/RemoteContent';
|
||||||
|
import useHarkState from '~/logic/state/hark';
|
||||||
|
|
||||||
interface LinkItemProps {
|
interface LinkItemProps {
|
||||||
node: GraphNode;
|
node: GraphNode;
|
||||||
@ -17,8 +18,6 @@ interface LinkItemProps {
|
|||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
group: Group;
|
group: Group;
|
||||||
path: string;
|
path: string;
|
||||||
contacts: Rolodex;
|
|
||||||
unreads: Unreads;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LinkItem = (props: LinkItemProps): ReactElement => {
|
export const LinkItem = (props: LinkItemProps): ReactElement => {
|
||||||
@ -28,7 +27,6 @@ export const LinkItem = (props: LinkItemProps): ReactElement => {
|
|||||||
api,
|
api,
|
||||||
group,
|
group,
|
||||||
path,
|
path,
|
||||||
contacts,
|
|
||||||
...rest
|
...rest
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
@ -89,8 +87,9 @@ export const LinkItem = (props: LinkItemProps): ReactElement => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const appPath = `/ship/~${resource}`;
|
const appPath = `/ship/~${resource}`;
|
||||||
const commColor = (props.unreads.graph?.[appPath]?.[`/${index}`]?.unreads ?? 0) > 0 ? 'blue' : 'gray';
|
const unreads = useHarkState(state => state.unreads);
|
||||||
const isUnread = props.unreads.graph?.[appPath]?.['/']?.unreads?.has(node.post.index);
|
const commColor = (unreads.graph?.[appPath]?.[`/${index}`]?.unreads ?? 0) > 0 ? 'blue' : 'gray';
|
||||||
|
const isUnread = unreads.graph?.[appPath]?.['/']?.unreads?.has(node.post.index);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@ -149,18 +148,13 @@ export const LinkItem = (props: LinkItemProps): ReactElement => {
|
|||||||
</Anchor>
|
</Anchor>
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Row minWidth='0' flexShrink={0} width="100%" justifyContent="space-between" py={3} bg="white">
|
<Row minWidth='0' flexShrink={0} width="100%" justifyContent="space-between" py={3} bg="white">
|
||||||
|
|
||||||
<Author
|
<Author
|
||||||
showImage
|
showImage
|
||||||
contacts={contacts}
|
|
||||||
ship={author}
|
ship={author}
|
||||||
date={node.post['time-sent']}
|
date={node.post['time-sent']}
|
||||||
group={group}
|
group={group}
|
||||||
api={api}
|
/>
|
||||||
></Author>
|
|
||||||
|
|
||||||
<Box ml="auto">
|
<Box ml="auto">
|
||||||
<Link
|
<Link
|
||||||
to={node.post.pending ? '#' : `${baseUrl}/${index}`}
|
to={node.post.pending ? '#' : `${baseUrl}/${index}`}
|
||||||
|
@ -10,14 +10,13 @@ import { hasProvider } from 'oembed-parser';
|
|||||||
|
|
||||||
interface LinkSubmitProps {
|
interface LinkSubmitProps {
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
storage: StorageState;
|
|
||||||
name: string;
|
name: string;
|
||||||
ship: string;
|
ship: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LinkSubmit = (props: LinkSubmitProps) => {
|
const LinkSubmit = (props: LinkSubmitProps) => {
|
||||||
const { canUpload, uploadDefault, uploading, promptUpload } =
|
const { canUpload, uploadDefault, uploading, promptUpload } =
|
||||||
useStorage(props.storage);
|
useStorage();
|
||||||
|
|
||||||
const [submitFocused, setSubmitFocused] = useState(false);
|
const [submitFocused, setSubmitFocused] = useState(false);
|
||||||
const [urlFocused, setUrlFocused] = useState(false);
|
const [urlFocused, setUrlFocused] = useState(false);
|
||||||
|
@ -18,6 +18,8 @@ import { getSnippet } from '~/logic/lib/publish';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { MentionText } from '~/views/components/MentionText';
|
import { MentionText } from '~/views/components/MentionText';
|
||||||
import ChatMessage from '../chat/components/ChatMessage';
|
import ChatMessage from '../chat/components/ChatMessage';
|
||||||
|
import useContactState from '~/logic/state/contact';
|
||||||
|
import useGroupState from '~/logic/state/group';
|
||||||
|
|
||||||
function getGraphModuleIcon(module: string) {
|
function getGraphModuleIcon(module: string) {
|
||||||
if (module === 'link') {
|
if (module === 'link') {
|
||||||
@ -30,7 +32,7 @@ const FilterBox = styled(Box)`
|
|||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
to bottom,
|
to bottom,
|
||||||
transparent,
|
transparent,
|
||||||
${p => p.theme.colors.white}
|
${(p) => p.theme.colors.white}
|
||||||
);
|
);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -67,7 +69,6 @@ const GraphUrl = ({ url, title }) => (
|
|||||||
const GraphNodeContent = ({
|
const GraphNodeContent = ({
|
||||||
group,
|
group,
|
||||||
post,
|
post,
|
||||||
contacts,
|
|
||||||
mod,
|
mod,
|
||||||
description,
|
description,
|
||||||
index,
|
index,
|
||||||
@ -80,9 +81,7 @@ const GraphNodeContent = ({
|
|||||||
const [{ text }, { url }] = contents;
|
const [{ text }, { url }] = contents;
|
||||||
return <GraphUrl title={text} url={url} />;
|
return <GraphUrl title={text} url={url} />;
|
||||||
} else if (idx.length === 3) {
|
} else if (idx.length === 3) {
|
||||||
return (
|
return <MentionText content={contents} group={group} />;
|
||||||
<MentionText content={contents} contacts={contacts} group={group} />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -92,7 +91,6 @@ const GraphNodeContent = ({
|
|||||||
<MentionText
|
<MentionText
|
||||||
content={contents}
|
content={contents}
|
||||||
group={group}
|
group={group}
|
||||||
contacts={contacts}
|
|
||||||
fontSize='14px'
|
fontSize='14px'
|
||||||
lineHeight='tall'
|
lineHeight='tall'
|
||||||
/>
|
/>
|
||||||
@ -133,12 +131,12 @@ const GraphNodeContent = ({
|
|||||||
renderSigil={false}
|
renderSigil={false}
|
||||||
containerClass='items-top cf hide-child'
|
containerClass='items-top cf hide-child'
|
||||||
group={group}
|
group={group}
|
||||||
contacts={contacts}
|
|
||||||
groups={{}}
|
groups={{}}
|
||||||
associations={{ graph: {}, groups: {} }}
|
associations={{ graph: {}, groups: {} }}
|
||||||
msg={post}
|
msg={post}
|
||||||
fontSize='0'
|
fontSize='0'
|
||||||
pt='2'
|
pt='2'
|
||||||
|
hideHover={true}
|
||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
@ -173,7 +171,6 @@ function getNodeUrl(
|
|||||||
}
|
}
|
||||||
const GraphNode = ({
|
const GraphNode = ({
|
||||||
post,
|
post,
|
||||||
contacts,
|
|
||||||
author,
|
author,
|
||||||
mod,
|
mod,
|
||||||
description,
|
description,
|
||||||
@ -184,10 +181,11 @@ const GraphNode = ({
|
|||||||
group,
|
group,
|
||||||
read,
|
read,
|
||||||
onRead,
|
onRead,
|
||||||
showContact = false,
|
showContact = false
|
||||||
}) => {
|
}) => {
|
||||||
author = deSig(author);
|
author = deSig(author);
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
const contacts = useContactState((state) => state.contacts);
|
||||||
|
|
||||||
const nodeUrl = getNodeUrl(mod, group?.hidden, groupPath, graph, index);
|
const nodeUrl = getNodeUrl(mod, group?.hidden, groupPath, graph, index);
|
||||||
|
|
||||||
@ -199,22 +197,18 @@ const GraphNode = ({
|
|||||||
}, [read, onRead]);
|
}, [read, onRead]);
|
||||||
|
|
||||||
const showNickname = useShowNickname(contacts?.[`~${author}`]);
|
const showNickname = useShowNickname(contacts?.[`~${author}`]);
|
||||||
const nickname = (contacts?.[`~${author}`]?.nickname && showNickname) ? contacts[`~${author}`].nickname : cite(author);
|
const nickname =
|
||||||
|
contacts?.[`~${author}`]?.nickname && showNickname
|
||||||
|
? contacts[`~${author}`].nickname
|
||||||
|
: cite(author);
|
||||||
return (
|
return (
|
||||||
<Row onClick={onClick} gapX='2' pt={showContact ? 2 : 0}>
|
<Row onClick={onClick} gapX='2' pt={showContact ? 2 : 0}>
|
||||||
<Col flexGrow={1} alignItems='flex-start'>
|
<Col flexGrow={1} alignItems='flex-start'>
|
||||||
{showContact && (
|
{showContact && (
|
||||||
<Author
|
<Author showImage ship={author} date={time} group={group} />
|
||||||
showImage
|
|
||||||
contacts={contacts}
|
|
||||||
ship={author}
|
|
||||||
date={time}
|
|
||||||
group={group}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
<Row width='100%' p='1' flexDirection='column'>
|
<Row width='100%' p='1' flexDirection='column'>
|
||||||
<GraphNodeContent
|
<GraphNodeContent
|
||||||
contacts={contacts}
|
|
||||||
post={post}
|
post={post}
|
||||||
mod={mod}
|
mod={mod}
|
||||||
description={description}
|
description={description}
|
||||||
@ -235,12 +229,9 @@ export function GraphNotification(props: {
|
|||||||
read: boolean;
|
read: boolean;
|
||||||
time: number;
|
time: number;
|
||||||
timebox: BigInteger;
|
timebox: BigInteger;
|
||||||
associations: Associations;
|
|
||||||
groups: Groups;
|
|
||||||
contacts: Rolodex;
|
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
}) {
|
}) {
|
||||||
const { contents, index, read, time, api, timebox, groups } = props;
|
const { contents, index, read, time, api, timebox } = props;
|
||||||
|
|
||||||
const authors = _.map(contents, 'author');
|
const authors = _.map(contents, 'author');
|
||||||
const { graph, group } = index;
|
const { graph, group } = index;
|
||||||
@ -255,6 +246,8 @@ export function GraphNotification(props: {
|
|||||||
return api.hark['read'](timebox, { graph: index });
|
return api.hark['read'](timebox, { graph: index });
|
||||||
}, [api, timebox, index, read]);
|
}, [api, timebox, index, read]);
|
||||||
|
|
||||||
|
const groups = useGroupState((state) => state.groups);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header
|
<Header
|
||||||
@ -265,17 +258,14 @@ export function GraphNotification(props: {
|
|||||||
authors={authors}
|
authors={authors}
|
||||||
moduleIcon={icon}
|
moduleIcon={icon}
|
||||||
channel={graph}
|
channel={graph}
|
||||||
contacts={props.contacts}
|
|
||||||
group={group}
|
group={group}
|
||||||
description={desc}
|
description={desc}
|
||||||
associations={props.associations}
|
|
||||||
/>
|
/>
|
||||||
<Box flexGrow={1} width='100%' pl={5} gridArea='main'>
|
<Box flexGrow={1} width='100%' pl={5} gridArea='main'>
|
||||||
{_.map(contents, (content, idx) => (
|
{_.map(contents, (content, idx) => (
|
||||||
<GraphNode
|
<GraphNode
|
||||||
post={content}
|
post={content}
|
||||||
author={content.author}
|
author={content.author}
|
||||||
contacts={props.contacts}
|
|
||||||
mod={index.module}
|
mod={index.module}
|
||||||
time={content?.['time-sent']}
|
time={content?.['time-sent']}
|
||||||
description={index.description}
|
description={index.description}
|
||||||
|
@ -41,13 +41,11 @@ interface GroupNotificationProps {
|
|||||||
read: boolean;
|
read: boolean;
|
||||||
time: number;
|
time: number;
|
||||||
timebox: BigInteger;
|
timebox: BigInteger;
|
||||||
associations: Associations;
|
|
||||||
contacts: Rolodex;
|
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GroupNotification(props: GroupNotificationProps): ReactElement {
|
export function GroupNotification(props: GroupNotificationProps): ReactElement {
|
||||||
const { contents, index, read, time, api, timebox, associations } = props;
|
const { contents, index, read, time, api, timebox } = props;
|
||||||
|
|
||||||
const authors = _.flatten(_.map(contents, getGroupUpdateParticipants));
|
const authors = _.flatten(_.map(contents, getGroupUpdateParticipants));
|
||||||
|
|
||||||
@ -69,10 +67,8 @@ export function GroupNotification(props: GroupNotificationProps): ReactElement {
|
|||||||
time={time}
|
time={time}
|
||||||
read={read}
|
read={read}
|
||||||
group={group}
|
group={group}
|
||||||
contacts={props.contacts}
|
|
||||||
authors={authors}
|
authors={authors}
|
||||||
description={desc}
|
description={desc}
|
||||||
associations={associations}
|
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
);
|
);
|
||||||
|
@ -9,16 +9,19 @@ import { Associations, Contact, Contacts, Rolodex } from '@urbit/api';
|
|||||||
import { PropFunc } from '~/types/util';
|
import { PropFunc } from '~/types/util';
|
||||||
import { useShowNickname } from '~/logic/lib/util';
|
import { useShowNickname } from '~/logic/lib/util';
|
||||||
import Timestamp from '~/views/components/Timestamp';
|
import Timestamp from '~/views/components/Timestamp';
|
||||||
|
import useContactState from '~/logic/state/contact';
|
||||||
|
import useMetadataState from '~/logic/state/metadata';
|
||||||
|
|
||||||
const Text = (props: PropFunc<typeof Text>) => (
|
const Text = (props: PropFunc<typeof Text>) => (
|
||||||
<NormalText fontWeight="500" {...props} />
|
<NormalText fontWeight="500" {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
function Author(props: { patp: string; contacts: Contacts; last?: boolean }): ReactElement {
|
function Author(props: { patp: string; last?: boolean }): ReactElement {
|
||||||
const contact: Contact | undefined = props.contacts?.[`~${props.patp}`];
|
const contacts = useContactState(state => state.contacts);
|
||||||
|
const contact: Contact | undefined = contacts?.[`~${props.patp}`];
|
||||||
|
|
||||||
const showNickname = useShowNickname(contact);
|
const showNickname = useShowNickname(contact);
|
||||||
const name = contact?.nickname || `~${props.patp}`;
|
const name = showNickname ? contact.nickname : `~${props.patp}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Text mono={!showNickname}>
|
<Text mono={!showNickname}>
|
||||||
@ -33,14 +36,13 @@ export function Header(props: {
|
|||||||
archived?: boolean;
|
archived?: boolean;
|
||||||
channel?: string;
|
channel?: string;
|
||||||
group: string;
|
group: string;
|
||||||
contacts: Rolodex;
|
|
||||||
description: string;
|
description: string;
|
||||||
moduleIcon?: string;
|
moduleIcon?: string;
|
||||||
time: number;
|
time: number;
|
||||||
read: boolean;
|
read: boolean;
|
||||||
associations: Associations;
|
|
||||||
} & PropFunc<typeof Row> ): ReactElement {
|
} & PropFunc<typeof Row> ): ReactElement {
|
||||||
const { description, channel, contacts, moduleIcon, read } = props;
|
const { description, channel, moduleIcon, read } = props;
|
||||||
|
const associations = useMetadataState(state => state.associations);
|
||||||
|
|
||||||
const authors = _.uniq(props.authors);
|
const authors = _.uniq(props.authors);
|
||||||
|
|
||||||
@ -50,7 +52,7 @@ export function Header(props: {
|
|||||||
f.map(([idx, p]: [string, string]) => {
|
f.map(([idx, p]: [string, string]) => {
|
||||||
const lent = Math.min(3, authors.length);
|
const lent = Math.min(3, authors.length);
|
||||||
const last = lent - 1 === parseInt(idx, 10);
|
const last = lent - 1 === parseInt(idx, 10);
|
||||||
return <Author key={idx} contacts={contacts} patp={p} last={last} />;
|
return <Author key={idx} patp={p} last={last} />;
|
||||||
}),
|
}),
|
||||||
auths => (
|
auths => (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
@ -64,11 +66,11 @@ export function Header(props: {
|
|||||||
|
|
||||||
const time = moment(props.time).format('HH:mm');
|
const time = moment(props.time).format('HH:mm');
|
||||||
const groupTitle =
|
const groupTitle =
|
||||||
props.associations.groups?.[props.group]?.metadata?.title;
|
associations.groups?.[props.group]?.metadata?.title;
|
||||||
|
|
||||||
const app = 'graph';
|
const app = 'graph';
|
||||||
const channelTitle =
|
const channelTitle =
|
||||||
(channel && props.associations?.[app]?.[channel]?.metadata?.title) ||
|
(channel && associations?.[app]?.[channel]?.metadata?.title) ||
|
||||||
channel;
|
channel;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -23,10 +23,12 @@ import GlobalApi from '~/logic/api/global';
|
|||||||
import { Notification } from './notification';
|
import { Notification } from './notification';
|
||||||
import { Invites } from './invites';
|
import { Invites } from './invites';
|
||||||
import { useLazyScroll } from '~/logic/lib/useLazyScroll';
|
import { useLazyScroll } from '~/logic/lib/useLazyScroll';
|
||||||
|
import useHarkState from '~/logic/state/hark';
|
||||||
|
import useInviteState from '~/logic/state/invite';
|
||||||
|
|
||||||
type DatedTimebox = [BigInteger, Timebox];
|
type DatedTimebox = [BigInteger, Timebox];
|
||||||
|
|
||||||
function filterNotification(associations: Associations, groups: string[]) {
|
function filterNotification(groups: string[]) {
|
||||||
if (groups.length === 0) {
|
if (groups.length === 0) {
|
||||||
return () => true;
|
return () => true;
|
||||||
}
|
}
|
||||||
@ -43,21 +45,13 @@ function filterNotification(associations: Associations, groups: string[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Inbox(props: {
|
export default function Inbox(props: {
|
||||||
notifications: Notifications;
|
|
||||||
notificationsSize: number;
|
|
||||||
archive: Notifications;
|
archive: Notifications;
|
||||||
groups: Groups;
|
|
||||||
showArchive?: boolean;
|
showArchive?: boolean;
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
associations: Associations;
|
|
||||||
contacts: Rolodex;
|
|
||||||
filter: string[];
|
filter: string[];
|
||||||
invites: InviteType;
|
|
||||||
pendingJoin: JoinRequests;
|
pendingJoin: JoinRequests;
|
||||||
notificationsGroupConfig: GroupNotificationsConfig;
|
|
||||||
notificationsGraphConfig: NotificationGraphConfig;
|
|
||||||
}) {
|
}) {
|
||||||
const { api, associations, invites } = props;
|
const { api } = props;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let seen = false;
|
let seen = false;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -70,8 +64,11 @@ export default function Inbox(props: {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const notificationState = useHarkState(state => state.notifications);
|
||||||
|
const archivedNotifications = useHarkState(state => state.archivedNotifications);
|
||||||
|
|
||||||
const notifications =
|
const notifications =
|
||||||
Array.from(props.showArchive ? props.archive : props.notifications) || [];
|
Array.from(props.showArchive ? archivedNotifications : notificationState) || [];
|
||||||
|
|
||||||
const calendar = {
|
const calendar = {
|
||||||
...MOMENT_CALENDAR_DATE, sameDay: function (now) {
|
...MOMENT_CALENDAR_DATE, sameDay: function (now) {
|
||||||
@ -86,7 +83,7 @@ export default function Inbox(props: {
|
|||||||
const notificationsByDay = f.flow(
|
const notificationsByDay = f.flow(
|
||||||
f.map<DatedTimebox, DatedTimebox>(([date, nots]) => [
|
f.map<DatedTimebox, DatedTimebox>(([date, nots]) => [
|
||||||
date,
|
date,
|
||||||
nots.filter(filterNotification(associations, props.filter))
|
nots.filter(filterNotification(props.filter))
|
||||||
]),
|
]),
|
||||||
f.groupBy<DatedTimebox>(([d]) => {
|
f.groupBy<DatedTimebox>(([d]) => {
|
||||||
const date = moment(daToUnix(d));
|
const date = moment(daToUnix(d));
|
||||||
@ -119,7 +116,7 @@ export default function Inbox(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Col ref={scrollRef} position="relative" height="100%" overflowY="auto">
|
<Col ref={scrollRef} position="relative" height="100%" overflowY="auto">
|
||||||
<Invites groups={props.groups} pendingJoin={props.pendingJoin} invites={invites} api={api} associations={associations} />
|
<Invites pendingJoin={props.pendingJoin} api={api} />
|
||||||
{[...notificationsByDayMap.keys()].sort().reverse().map((day, index) => {
|
{[...notificationsByDayMap.keys()].sort().reverse().map((day, index) => {
|
||||||
const timeboxes = notificationsByDayMap.get(day)!;
|
const timeboxes = notificationsByDayMap.get(day)!;
|
||||||
return timeboxes.length > 0 && (
|
return timeboxes.length > 0 && (
|
||||||
@ -127,13 +124,8 @@ export default function Inbox(props: {
|
|||||||
key={day}
|
key={day}
|
||||||
label={day === 'latest' ? 'Today' : moment(day).calendar(null, calendar)}
|
label={day === 'latest' ? 'Today' : moment(day).calendar(null, calendar)}
|
||||||
timeboxes={timeboxes}
|
timeboxes={timeboxes}
|
||||||
contacts={props.contacts}
|
|
||||||
archive={Boolean(props.showArchive)}
|
archive={Boolean(props.showArchive)}
|
||||||
associations={props.associations}
|
|
||||||
api={api}
|
api={api}
|
||||||
groups={props.groups}
|
|
||||||
graphConfig={props.notificationsGraphConfig}
|
|
||||||
groupConfig={props.notificationsGroupConfig}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -165,14 +157,9 @@ function sortIndexedNotification(
|
|||||||
|
|
||||||
function DaySection({
|
function DaySection({
|
||||||
label,
|
label,
|
||||||
contacts,
|
|
||||||
groups,
|
|
||||||
archive,
|
archive,
|
||||||
timeboxes,
|
timeboxes,
|
||||||
associations,
|
|
||||||
api,
|
api,
|
||||||
groupConfig,
|
|
||||||
graphConfig
|
|
||||||
}) {
|
}) {
|
||||||
const lent = timeboxes.map(([,nots]) => nots.length).reduce(f.add, 0);
|
const lent = timeboxes.map(([,nots]) => nots.length).reduce(f.add, 0);
|
||||||
if (lent === 0 || timeboxes.length === 0) {
|
if (lent === 0 || timeboxes.length === 0) {
|
||||||
@ -195,14 +182,9 @@ function DaySection({
|
|||||||
<Box flexShrink={0} height="4px" bg="scales.black05" />
|
<Box flexShrink={0} height="4px" bg="scales.black05" />
|
||||||
)}
|
)}
|
||||||
<Notification
|
<Notification
|
||||||
graphConfig={graphConfig}
|
|
||||||
groupConfig={groupConfig}
|
|
||||||
api={api}
|
api={api}
|
||||||
associations={associations}
|
|
||||||
notification={not}
|
notification={not}
|
||||||
archived={archive}
|
archived={archive}
|
||||||
contacts={contacts}
|
|
||||||
groups={groups}
|
|
||||||
time={date}
|
time={date}
|
||||||
/>
|
/>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
@ -7,14 +7,11 @@ import { Invites as IInvites, Associations, Invite, JoinRequests, Groups, Contac
|
|||||||
import GlobalApi from '~/logic/api/global';
|
import GlobalApi from '~/logic/api/global';
|
||||||
import { resourceAsPath, alphabeticalOrder } from '~/logic/lib/util';
|
import { resourceAsPath, alphabeticalOrder } from '~/logic/lib/util';
|
||||||
import InviteItem from '~/views/components/Invite';
|
import InviteItem from '~/views/components/Invite';
|
||||||
|
import useInviteState from '~/logic/state/invite';
|
||||||
|
import useGroupState from '~/logic/state/group';
|
||||||
|
|
||||||
interface InvitesProps {
|
interface InvitesProps {
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
invites: IInvites;
|
|
||||||
groups: Groups;
|
|
||||||
contacts: Contacts;
|
|
||||||
associations: Associations;
|
|
||||||
pendingJoin: JoinRequests;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InviteRef {
|
interface InviteRef {
|
||||||
@ -24,7 +21,9 @@ interface InviteRef {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Invites(props: InvitesProps): ReactElement {
|
export function Invites(props: InvitesProps): ReactElement {
|
||||||
const { api, invites, pendingJoin } = props;
|
const { api } = props;
|
||||||
|
const pendingJoin = useGroupState(s => s.pendingJoin);
|
||||||
|
const invites = useInviteState(state => state.invites);
|
||||||
|
|
||||||
const inviteArr: InviteRef[] = _.reduce(invites, (acc: InviteRef[], val: AppInvites, app: string) => {
|
const inviteArr: InviteRef[] = _.reduce(invites, (acc: InviteRef[], val: AppInvites, app: string) => {
|
||||||
const appInvites = _.reduce(val, (invs: InviteRef[], invite: Invite, uid: string) => {
|
const appInvites = _.reduce(val, (invs: InviteRef[], invite: Invite, uid: string) => {
|
||||||
@ -34,7 +33,7 @@ export function Invites(props: InvitesProps): ReactElement {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const invitesAndStatus: { [rid: string]: JoinProgress | InviteRef } =
|
const invitesAndStatus: { [rid: string]: JoinProgress | InviteRef } =
|
||||||
{ ..._.keyBy(inviteArr, ({ invite }) => resourceAsPath(invite.resource)), ...props.pendingJoin };
|
{ ..._.keyBy(inviteArr, ({ invite }) => resourceAsPath(invite.resource)), ...pendingJoin };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col
|
<Col
|
||||||
@ -50,33 +49,27 @@ export function Invites(props: InvitesProps): ReactElement {
|
|||||||
.sort(alphabeticalOrder)
|
.sort(alphabeticalOrder)
|
||||||
.map((resource) => {
|
.map((resource) => {
|
||||||
const inviteOrStatus = invitesAndStatus[resource];
|
const inviteOrStatus = invitesAndStatus[resource];
|
||||||
if(typeof inviteOrStatus === 'string') {
|
const join = pendingJoin[resource];
|
||||||
|
if(typeof inviteOrStatus === 'string') {
|
||||||
return (
|
return (
|
||||||
<InviteItem
|
<InviteItem
|
||||||
key={resource}
|
key={resource}
|
||||||
contacts={props.contacts}
|
|
||||||
groups={props.groups}
|
|
||||||
associations={props.associations}
|
|
||||||
resource={resource}
|
resource={resource}
|
||||||
pendingJoin={pendingJoin}
|
pendingJoin={join}
|
||||||
api={api}
|
api={api}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const { app, uid, invite } = inviteOrStatus;
|
const { app, uid, invite } = inviteOrStatus;
|
||||||
console.log(inviteOrStatus);
|
|
||||||
return (
|
return (
|
||||||
<InviteItem
|
<InviteItem
|
||||||
key={resource}
|
key={resource}
|
||||||
api={api}
|
api={api}
|
||||||
invite={invite}
|
invite={invite}
|
||||||
|
pendingJoin={join}
|
||||||
app={app}
|
app={app}
|
||||||
uid={uid}
|
uid={uid}
|
||||||
pendingJoin={pendingJoin}
|
|
||||||
resource={resource}
|
resource={resource}
|
||||||
contacts={props.contacts}
|
|
||||||
groups={props.groups}
|
|
||||||
associations={props.associations}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -18,17 +18,13 @@ import { GroupNotification } from './group';
|
|||||||
import { GraphNotification } from './graph';
|
import { GraphNotification } from './graph';
|
||||||
import { BigInteger } from 'big-integer';
|
import { BigInteger } from 'big-integer';
|
||||||
import { useHovering } from '~/logic/lib/util';
|
import { useHovering } from '~/logic/lib/util';
|
||||||
|
import useHarkState from '~/logic/state/hark';
|
||||||
|
|
||||||
interface NotificationProps {
|
interface NotificationProps {
|
||||||
notification: IndexedNotification;
|
notification: IndexedNotification;
|
||||||
time: BigInteger;
|
time: BigInteger;
|
||||||
associations: Associations;
|
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
archived: boolean;
|
archived: boolean;
|
||||||
groups: Groups;
|
|
||||||
contacts: Contacts;
|
|
||||||
graphConfig: NotificationGraphConfig;
|
|
||||||
groupConfig: GroupNotificationsConfig;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMuted(
|
function getMuted(
|
||||||
@ -61,8 +57,6 @@ function NotificationWrapper(props: {
|
|||||||
notif: IndexedNotification;
|
notif: IndexedNotification;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
archived: boolean;
|
archived: boolean;
|
||||||
graphConfig: NotificationGraphConfig;
|
|
||||||
groupConfig: GroupNotificationsConfig;
|
|
||||||
}) {
|
}) {
|
||||||
const { api, time, notif, children } = props;
|
const { api, time, notif, children } = props;
|
||||||
|
|
||||||
@ -70,10 +64,13 @@ function NotificationWrapper(props: {
|
|||||||
return api.hark.archive(time, notif.index);
|
return api.hark.archive(time, notif.index);
|
||||||
}, [time, notif]);
|
}, [time, notif]);
|
||||||
|
|
||||||
|
const groupConfig = useHarkState(state => state.notificationsGroupConfig);
|
||||||
|
const graphConfig = useHarkState(state => state.notificationsGraphConfig);
|
||||||
|
|
||||||
const isMuted = getMuted(
|
const isMuted = getMuted(
|
||||||
notif,
|
notif,
|
||||||
props.groupConfig,
|
groupConfig,
|
||||||
props.graphConfig
|
graphConfig
|
||||||
);
|
);
|
||||||
|
|
||||||
const onChangeMute = useCallback(async () => {
|
const onChangeMute = useCallback(async () => {
|
||||||
@ -119,8 +116,6 @@ export function Notification(props: NotificationProps) {
|
|||||||
notif={notification}
|
notif={notification}
|
||||||
time={props.time}
|
time={props.time}
|
||||||
api={props.api}
|
api={props.api}
|
||||||
graphConfig={props.graphConfig}
|
|
||||||
groupConfig={props.groupConfig}
|
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</NotificationWrapper>
|
</NotificationWrapper>
|
||||||
@ -136,13 +131,10 @@ export function Notification(props: NotificationProps) {
|
|||||||
api={props.api}
|
api={props.api}
|
||||||
index={index}
|
index={index}
|
||||||
contents={c}
|
contents={c}
|
||||||
contacts={props.contacts}
|
|
||||||
groups={props.groups}
|
|
||||||
read={read}
|
read={read}
|
||||||
archived={archived}
|
archived={archived}
|
||||||
timebox={props.time}
|
timebox={props.time}
|
||||||
time={time}
|
time={time}
|
||||||
associations={associations}
|
|
||||||
/>
|
/>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
@ -156,13 +148,10 @@ export function Notification(props: NotificationProps) {
|
|||||||
api={props.api}
|
api={props.api}
|
||||||
index={index}
|
index={index}
|
||||||
contents={c}
|
contents={c}
|
||||||
contacts={props.contacts}
|
|
||||||
groups={props.groups}
|
|
||||||
read={read}
|
read={read}
|
||||||
timebox={props.time}
|
timebox={props.time}
|
||||||
archived={archived}
|
archived={archived}
|
||||||
time={time}
|
time={time}
|
||||||
associations={associations}
|
|
||||||
/>
|
/>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
|
@ -12,6 +12,8 @@ import { Dropdown } from '~/views/components/Dropdown';
|
|||||||
import { FormikOnBlur } from '~/views/components/FormikOnBlur';
|
import { FormikOnBlur } from '~/views/components/FormikOnBlur';
|
||||||
import GroupSearch from '~/views/components/GroupSearch';
|
import GroupSearch from '~/views/components/GroupSearch';
|
||||||
import { useTutorialModal } from '~/views/components/useTutorialModal';
|
import { useTutorialModal } from '~/views/components/useTutorialModal';
|
||||||
|
import useHarkState from '~/logic/state/hark';
|
||||||
|
import useMetadataState from '~/logic/state/metadata';
|
||||||
|
|
||||||
const baseUrl = '/~notifications';
|
const baseUrl = '/~notifications';
|
||||||
|
|
||||||
@ -38,6 +40,7 @@ export default function NotificationsScreen(props: any): ReactElement {
|
|||||||
const relativePath = (p: string) => baseUrl + p;
|
const relativePath = (p: string) => baseUrl + p;
|
||||||
|
|
||||||
const [filter, setFilter] = useState<NotificationFilter>({ groups: [] });
|
const [filter, setFilter] = useState<NotificationFilter>({ groups: [] });
|
||||||
|
const associations = useMetadataState(state => state.associations);
|
||||||
const onSubmit = async ({ groups } : NotificationFilter) => {
|
const onSubmit = async ({ groups } : NotificationFilter) => {
|
||||||
setFilter({ groups });
|
setFilter({ groups });
|
||||||
};
|
};
|
||||||
@ -48,10 +51,11 @@ export default function NotificationsScreen(props: any): ReactElement {
|
|||||||
filter.groups.length === 0
|
filter.groups.length === 0
|
||||||
? 'All'
|
? 'All'
|
||||||
: filter.groups
|
: filter.groups
|
||||||
.map(g => props.associations?.groups?.[g]?.metadata?.title)
|
.map(g => associations.groups?.[g]?.metadata?.title)
|
||||||
.join(', ');
|
.join(', ');
|
||||||
const anchorRef = useRef<HTMLElement | null>(null);
|
const anchorRef = useRef<HTMLElement | null>(null);
|
||||||
useTutorialModal('notifications', true, anchorRef);
|
useTutorialModal('notifications', true, anchorRef);
|
||||||
|
const notificationsCount = useHarkState(state => state.notificationsCount);
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route
|
<Route
|
||||||
@ -61,7 +65,7 @@ export default function NotificationsScreen(props: any): ReactElement {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Helmet defer={false}>
|
<Helmet defer={false}>
|
||||||
<title>{ props.notificationsCount ? `(${String(props.notificationsCount) }) `: '' }Landscape - Notifications</title>
|
<title>{ notificationsCount ? `(${String(notificationsCount) }) `: '' }Landscape - Notifications</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<Body>
|
<Body>
|
||||||
<Col overflowY="hidden" height="100%">
|
<Col overflowY="hidden" height="100%">
|
||||||
@ -110,7 +114,6 @@ export default function NotificationsScreen(props: any): ReactElement {
|
|||||||
id="groups"
|
id="groups"
|
||||||
label="Filter Groups"
|
label="Filter Groups"
|
||||||
caption="Only show notifications from this group"
|
caption="Only show notifications from this group"
|
||||||
associations={props.associations}
|
|
||||||
/>
|
/>
|
||||||
</FormikOnBlur>
|
</FormikOnBlur>
|
||||||
</Col>
|
</Col>
|
||||||
|
@ -21,6 +21,7 @@ import { ImageInput } from '~/views/components/ImageInput';
|
|||||||
import { MarkdownField } from '~/views/apps/publish/components/MarkdownField';
|
import { MarkdownField } from '~/views/apps/publish/components/MarkdownField';
|
||||||
import { resourceFromPath } from '~/logic/lib/group';
|
import { resourceFromPath } from '~/logic/lib/group';
|
||||||
import GroupSearch from '~/views/components/GroupSearch';
|
import GroupSearch from '~/views/components/GroupSearch';
|
||||||
|
import useContactState from '~/logic/state/contact';
|
||||||
import {
|
import {
|
||||||
ProfileHeader,
|
ProfileHeader,
|
||||||
ProfileControls,
|
ProfileControls,
|
||||||
@ -48,7 +49,7 @@ const emptyContact = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function ProfileHeaderImageEdit(props: any): ReactElement {
|
export function ProfileHeaderImageEdit(props: any): ReactElement {
|
||||||
const { contact, storage, setFieldValue, handleHideCover } = { ...props };
|
const { contact, setFieldValue, handleHideCover } = props;
|
||||||
const [editCover, setEditCover] = useState(false);
|
const [editCover, setEditCover] = useState(false);
|
||||||
const [removedCoverLabel, setRemovedCoverLabel] = useState('Remove Header');
|
const [removedCoverLabel, setRemovedCoverLabel] = useState('Remove Header');
|
||||||
const handleClear = (e) => {
|
const handleClear = (e) => {
|
||||||
@ -63,7 +64,7 @@ export function ProfileHeaderImageEdit(props: any): ReactElement {
|
|||||||
{contact?.cover ? (
|
{contact?.cover ? (
|
||||||
<div>
|
<div>
|
||||||
{editCover ? (
|
{editCover ? (
|
||||||
<ImageInput id='cover' storage={storage} marginTop='-8px' />
|
<ImageInput id='cover' marginTop='-8px' />
|
||||||
) : (
|
) : (
|
||||||
<Row>
|
<Row>
|
||||||
<Button mr='2' onClick={() => setEditCover(true)}>
|
<Button mr='2' onClick={() => setEditCover(true)}>
|
||||||
@ -76,14 +77,15 @@ export function ProfileHeaderImageEdit(props: any): ReactElement {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<ImageInput id='cover' storage={storage} marginTop='-8px' />
|
<ImageInput id='cover' marginTop='-8px' />
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EditProfile(props: any): ReactElement {
|
export function EditProfile(props: any): ReactElement {
|
||||||
const { contact, storage, ship, api, isPublic } = props;
|
const { contact, ship, api } = props;
|
||||||
|
const isPublic = useContactState((state) => state.isContactPublic);
|
||||||
const [hideCover, setHideCover] = useState(false);
|
const [hideCover, setHideCover] = useState(false);
|
||||||
|
|
||||||
const handleHideCover = (value) => {
|
const handleHideCover = (value) => {
|
||||||
@ -148,7 +150,7 @@ export function EditProfile(props: any): ReactElement {
|
|||||||
<Form width='100%' height='100%'>
|
<Form width='100%' height='100%'>
|
||||||
<ProfileHeader>
|
<ProfileHeader>
|
||||||
<ProfileControls>
|
<ProfileControls>
|
||||||
<Row>
|
<Row alignItems='baseline'>
|
||||||
<Button
|
<Button
|
||||||
type='submit'
|
type='submit'
|
||||||
display='inline'
|
display='inline'
|
||||||
@ -176,10 +178,13 @@ export function EditProfile(props: any): ReactElement {
|
|||||||
</Row>
|
</Row>
|
||||||
<ProfileStatus contact={contact} />
|
<ProfileStatus contact={contact} />
|
||||||
</ProfileControls>
|
</ProfileControls>
|
||||||
<ProfileImages hideCover={hideCover} contact={contact} ship={ship}>
|
<ProfileImages
|
||||||
|
hideCover={hideCover}
|
||||||
|
contact={contact}
|
||||||
|
ship={ship}
|
||||||
|
>
|
||||||
<ProfileHeaderImageEdit
|
<ProfileHeaderImageEdit
|
||||||
contact={contact}
|
contact={contact}
|
||||||
storage={storage}
|
|
||||||
setFieldValue={setFieldValue}
|
setFieldValue={setFieldValue}
|
||||||
handleHideCover={handleHideCover}
|
handleHideCover={handleHideCover}
|
||||||
/>
|
/>
|
||||||
@ -193,23 +198,16 @@ export function EditProfile(props: any): ReactElement {
|
|||||||
<ImageInput
|
<ImageInput
|
||||||
id='avatar'
|
id='avatar'
|
||||||
label='Overlay Avatar (may be hidden by other users)'
|
label='Overlay Avatar (may be hidden by other users)'
|
||||||
storage={storage}
|
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Input id='nickname' label='Custom Name' mb={3} />
|
<Input id='nickname' label='Custom Name' mb={3} />
|
||||||
<Col width='100%'>
|
<Col width='100%'>
|
||||||
<Text mb={2}>Description</Text>
|
<Text mb={2}>Description</Text>
|
||||||
<MarkdownField id='bio' mb={3} storage={storage} />
|
<MarkdownField id='bio' mb={3} />
|
||||||
</Col>
|
</Col>
|
||||||
<Checkbox mb={3} id='isPublic' label='Public Profile' />
|
<Checkbox mb={3} id='isPublic' label='Public Profile' />
|
||||||
<GroupSearch
|
<GroupSearch label='Pinned Groups' id='groups' publicOnly />
|
||||||
label='Pinned Groups'
|
|
||||||
id='groups'
|
|
||||||
groups={props.groups}
|
|
||||||
associations={props.associations}
|
|
||||||
publicOnly
|
|
||||||
/>
|
|
||||||
<AsyncButton primary loadingText='Updating...' border mt={3}>
|
<AsyncButton primary loadingText='Updating...' border mt={3}>
|
||||||
Submit
|
Submit
|
||||||
</AsyncButton>
|
</AsyncButton>
|
||||||
|
@ -9,13 +9,14 @@ import { EditProfile } from './EditProfile';
|
|||||||
import { SetStatusBarModal } from '~/views/components/SetStatusBarModal';
|
import { SetStatusBarModal } from '~/views/components/SetStatusBarModal';
|
||||||
import { uxToHex } from '~/logic/lib/util';
|
import { uxToHex } from '~/logic/lib/util';
|
||||||
import { useTutorialModal } from '~/views/components/useTutorialModal';
|
import { useTutorialModal } from '~/views/components/useTutorialModal';
|
||||||
|
import useContactState from '~/logic/state/contact';
|
||||||
|
|
||||||
export function ProfileHeader(props: any): ReactElement {
|
export function ProfileHeader(props: any): ReactElement {
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
border='1px solid'
|
border='1px solid'
|
||||||
borderColor='lightGray'
|
borderColor='washedGray'
|
||||||
borderRadius='2'
|
borderRadius='3'
|
||||||
overflow='hidden'
|
overflow='hidden'
|
||||||
marginBottom='calc(64px + 2rem)'
|
marginBottom='calc(64px + 2rem)'
|
||||||
>
|
>
|
||||||
@ -39,6 +40,7 @@ export function ProfileImages(props: any): ReactElement {
|
|||||||
src={contact.cover}
|
src={contact.cover}
|
||||||
width='100%'
|
width='100%'
|
||||||
height='100%'
|
height='100%'
|
||||||
|
referrerPolicy="no-referrer"
|
||||||
style={{ objectFit: 'cover' }}
|
style={{ objectFit: 'cover' }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@ -56,6 +58,7 @@ export function ProfileImages(props: any): ReactElement {
|
|||||||
src={contact.avatar}
|
src={contact.avatar}
|
||||||
width='100%'
|
width='100%'
|
||||||
height='100%'
|
height='100%'
|
||||||
|
referrerPolicy="no-referrer"
|
||||||
style={{ objectFit: 'cover' }}
|
style={{ objectFit: 'cover' }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@ -64,7 +67,7 @@ export function ProfileImages(props: any): ReactElement {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Row ref={anchorRef} width='100%' height='300px' position='relative'>
|
<Row ref={anchorRef} width='100%' height='400px' position='relative'>
|
||||||
{cover}
|
{cover}
|
||||||
<Center position='absolute' width='100%' height='100%'>
|
<Center position='absolute' width='100%' height='100%'>
|
||||||
{props.children}
|
{props.children}
|
||||||
@ -73,7 +76,7 @@ export function ProfileImages(props: any): ReactElement {
|
|||||||
<Box
|
<Box
|
||||||
height='128px'
|
height='128px'
|
||||||
width='128px'
|
width='128px'
|
||||||
borderRadius='2'
|
borderRadius='3'
|
||||||
overflow='hidden'
|
overflow='hidden'
|
||||||
position='absolute'
|
position='absolute'
|
||||||
left='50%'
|
left='50%'
|
||||||
@ -109,6 +112,7 @@ export function ProfileStatus(props: any): ReactElement {
|
|||||||
display='inline-block'
|
display='inline-block'
|
||||||
verticalAlign='middle'
|
verticalAlign='middle'
|
||||||
color='gray'
|
color='gray'
|
||||||
|
title={contact?.status ?? ''}
|
||||||
>
|
>
|
||||||
{contact?.status ?? ''}
|
{contact?.status ?? ''}
|
||||||
</RichText>
|
</RichText>
|
||||||
@ -157,10 +161,12 @@ export function ProfileActions(props: any): ReactElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Profile(props: any): ReactElement {
|
export function Profile(props: any): ReactElement | null {
|
||||||
|
const { hideAvatars } = useSettingsState(selectCalmState);
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
const nackedContacts = useContactState(state => state.nackedContacts);
|
||||||
|
|
||||||
const { contact, nackedContacts, hasLoaded, isPublic, isEdit, ship } = props;
|
const { contact, hasLoaded, isEdit, ship } = props;
|
||||||
const nacked = nackedContacts.has(ship);
|
const nacked = nackedContacts.has(ship);
|
||||||
const formRef = useRef(null);
|
const formRef = useRef(null);
|
||||||
|
|
||||||
@ -183,21 +189,14 @@ export function Profile(props: any): ReactElement {
|
|||||||
<EditProfile
|
<EditProfile
|
||||||
ship={ship}
|
ship={ship}
|
||||||
contact={contact}
|
contact={contact}
|
||||||
storage={props.storage}
|
|
||||||
api={props.api}
|
api={props.api}
|
||||||
groups={props.groups}
|
|
||||||
associations={props.associations}
|
|
||||||
isPublic={isPublic}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ViewProfile
|
<ViewProfile
|
||||||
nacked={nacked}
|
nacked={nacked}
|
||||||
ship={ship}
|
ship={ship}
|
||||||
contact={contact}
|
|
||||||
isPublic={isPublic}
|
|
||||||
api={props.api}
|
api={props.api}
|
||||||
groups={props.groups}
|
contact={contact}
|
||||||
associations={props.associations}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -2,17 +2,19 @@ import React, {
|
|||||||
useState,
|
useState,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
ChangeEvent
|
ChangeEvent,
|
||||||
|
useRef
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Row,
|
Row,
|
||||||
|
Text,
|
||||||
Button,
|
Button,
|
||||||
StatelessTextInput as Input
|
StatelessTextInput as Input
|
||||||
} from '@tlon/indigo-react';
|
} from '@tlon/indigo-react';
|
||||||
|
|
||||||
export function SetStatus(props: any) {
|
export function SetStatus(props: any) {
|
||||||
const { contact, ship, api, callback } = props;
|
const { contact, ship, api, callback } = props;
|
||||||
|
const inputRef = useRef(null);
|
||||||
const [_status, setStatus] = useState('');
|
const [_status, setStatus] = useState('');
|
||||||
const onStatusChange = useCallback(
|
const onStatusChange = useCallback(
|
||||||
(e: ChangeEvent<HTMLInputElement>) => {
|
(e: ChangeEvent<HTMLInputElement>) => {
|
||||||
@ -27,19 +29,20 @@ export function SetStatus(props: any) {
|
|||||||
|
|
||||||
const editStatus = () => {
|
const editStatus = () => {
|
||||||
api.contacts.edit(ship, { status: _status });
|
api.contacts.edit(ship, { status: _status });
|
||||||
|
inputRef.current.blur();
|
||||||
if (callback) {
|
if (callback) {
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row width="100%" my={3}>
|
<Row width='100%' my={3}>
|
||||||
<Input
|
<Input
|
||||||
|
ref={inputRef}
|
||||||
onChange={onStatusChange}
|
onChange={onStatusChange}
|
||||||
value={_status}
|
value={_status}
|
||||||
autocomplete="off"
|
autocomplete='off'
|
||||||
width="75%"
|
width='75%'
|
||||||
mr={2}
|
mr={2}
|
||||||
onKeyPress={(evt) => {
|
onKeyPress={(evt) => {
|
||||||
if (evt.key === 'Enter') {
|
if (evt.key === 'Enter') {
|
||||||
@ -47,16 +50,9 @@ export function SetStatus(props: any) {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button primary color='white' ml={2} width='25%' onClick={editStatus}>
|
||||||
primary
|
|
||||||
color="white"
|
|
||||||
ml={2}
|
|
||||||
width="25%"
|
|
||||||
onClick={editStatus}
|
|
||||||
>
|
|
||||||
Set Status
|
Set Status
|
||||||
</Button>
|
</Button>
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { ReactElement } from 'react';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { Center, Box, Text, Row, Col } from '@tlon/indigo-react';
|
import { Center, Box, Text, Row, Col } from '@tlon/indigo-react';
|
||||||
@ -15,11 +15,13 @@ import {
|
|||||||
ProfileStatus,
|
ProfileStatus,
|
||||||
ProfileImages
|
ProfileImages
|
||||||
} from './Profile';
|
} from './Profile';
|
||||||
|
import useContactState from '~/logic/state/contact';
|
||||||
|
|
||||||
export function ViewProfile(props: any) {
|
export function ViewProfile(props: any): ReactElement {
|
||||||
const history = useHistory();
|
|
||||||
const { hideNicknames } = useSettingsState(selectCalmState);
|
const { hideNicknames } = useSettingsState(selectCalmState);
|
||||||
const { api, contact, nacked, isPublic, ship, associations, groups } = props;
|
const { api, contact, nacked, ship } = props;
|
||||||
|
|
||||||
|
const isPublic = useContactState(state => state.isContactPublic);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -37,7 +39,7 @@ export function ViewProfile(props: any) {
|
|||||||
</ProfileHeader>
|
</ProfileHeader>
|
||||||
<Row pb={2} alignItems='center' width='100%'>
|
<Row pb={2} alignItems='center' width='100%'>
|
||||||
<Center width='100%'>
|
<Center width='100%'>
|
||||||
<Text>
|
<Text fontWeight='500'>
|
||||||
{!hideNicknames && contact?.nickname ? contact.nickname : ''}
|
{!hideNicknames && contact?.nickname ? contact.nickname : ''}
|
||||||
</Text>
|
</Text>
|
||||||
</Center>
|
</Center>
|
||||||
@ -49,7 +51,7 @@ export function ViewProfile(props: any) {
|
|||||||
</Text>
|
</Text>
|
||||||
</Center>
|
</Center>
|
||||||
</Row>
|
</Row>
|
||||||
<Col pb={2} alignItems='center' justifyContent='center' width='100%'>
|
<Col pb={2} mt='3' alignItems='center' justifyContent='center' width='100%'>
|
||||||
<Center flexDirection='column' maxWidth='32rem'>
|
<Center flexDirection='column' maxWidth='32rem'>
|
||||||
<RichText width='100%' disableRemoteContent>
|
<RichText width='100%' disableRemoteContent>
|
||||||
{contact?.bio ? contact.bio : ''}
|
{contact?.bio ? contact.bio : ''}
|
||||||
@ -64,8 +66,6 @@ export function ViewProfile(props: any) {
|
|||||||
<GroupLink
|
<GroupLink
|
||||||
api={api}
|
api={api}
|
||||||
resource={g}
|
resource={g}
|
||||||
groups={groups}
|
|
||||||
associations={associations}
|
|
||||||
measure={() => {}}
|
measure={() => {}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
@ -4,16 +4,19 @@ import Helmet from 'react-helmet';
|
|||||||
|
|
||||||
import { Box } from '@tlon/indigo-react';
|
import { Box } from '@tlon/indigo-react';
|
||||||
|
|
||||||
import { Profile } from "./components/Profile";
|
import { Profile } from './components/Profile';
|
||||||
|
import useContactState from '~/logic/state/contact';
|
||||||
|
import useHarkState from '~/logic/state/hark';
|
||||||
|
|
||||||
export default function ProfileScreen(props: any) {
|
export default function ProfileScreen(props: any) {
|
||||||
const { dark } = props;
|
const contacts = useContactState(state => state.contacts);
|
||||||
|
const notificationsCount = useHarkState(state => state.notificationsCount);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Helmet defer={false}>
|
<Helmet defer={false}>
|
||||||
<title>
|
<title>
|
||||||
{props.notificationsCount
|
{notificationsCount
|
||||||
? `(${String(props.notificationsCount)}) `
|
? `(${String(notificationsCount)}) `
|
||||||
: ''}
|
: ''}
|
||||||
Landscape - Profile
|
Landscape - Profile
|
||||||
</title>
|
</title>
|
||||||
@ -23,8 +26,7 @@ export default function ProfileScreen(props: any) {
|
|||||||
render={({ match }) => {
|
render={({ match }) => {
|
||||||
const ship = match.params.ship;
|
const ship = match.params.ship;
|
||||||
const isEdit = match.url.includes('edit');
|
const isEdit = match.url.includes('edit');
|
||||||
const isPublic = props.isContactPublic;
|
const contact = contacts?.[ship];
|
||||||
const contact = props.contacts?.[ship];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box height='100%' px={[0, 3]} pb={[0, 3]} borderRadius={2}>
|
<Box height='100%' px={[0, 3]} pb={[0, 3]} borderRadius={2}>
|
||||||
@ -41,15 +43,10 @@ export default function ProfileScreen(props: any) {
|
|||||||
<Box>
|
<Box>
|
||||||
<Profile
|
<Profile
|
||||||
ship={ship}
|
ship={ship}
|
||||||
hasLoaded={Object.keys(props.contacts).length !== 0}
|
hasLoaded={Object.keys(contacts).length !== 0}
|
||||||
associations={props.associations}
|
|
||||||
groups={props.groups}
|
|
||||||
contact={contact}
|
contact={contact}
|
||||||
api={props.api}
|
api={props.api}
|
||||||
storage={props.storage}
|
|
||||||
isEdit={isEdit}
|
isEdit={isEdit}
|
||||||
isPublic={isPublic}
|
|
||||||
nackedContacts={props.nackedContacts}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -24,18 +24,12 @@ export function PublishResource(props: PublishResourceProps) {
|
|||||||
api={api}
|
api={api}
|
||||||
ship={ship}
|
ship={ship}
|
||||||
book={book}
|
book={book}
|
||||||
contacts={props.contacts}
|
|
||||||
groups={props.groups}
|
|
||||||
associations={props.associations}
|
|
||||||
association={association}
|
association={association}
|
||||||
rootUrl={baseUrl}
|
rootUrl={baseUrl}
|
||||||
baseUrl={`${baseUrl}/resource/publish/ship/${ship}/${book}`}
|
baseUrl={`${baseUrl}/resource/publish/ship/${ship}/${book}`}
|
||||||
history={props.history}
|
history={props.history}
|
||||||
match={props.match}
|
match={props.match}
|
||||||
location={props.location}
|
location={props.location}
|
||||||
unreads={props.unreads}
|
|
||||||
graphs={props.graphs}
|
|
||||||
storage={props.storage}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
@ -9,7 +9,6 @@ import { PostFormSchema, PostForm } from './NoteForm';
|
|||||||
import GlobalApi from '~/logic/api/global';
|
import GlobalApi from '~/logic/api/global';
|
||||||
import { getLatestRevision, editPost } from '~/logic/lib/publish';
|
import { getLatestRevision, editPost } from '~/logic/lib/publish';
|
||||||
import { useWaitForProps } from '~/logic/lib/useWaitForProps';
|
import { useWaitForProps } from '~/logic/lib/useWaitForProps';
|
||||||
import { StorageState } from '~/types';
|
|
||||||
|
|
||||||
interface EditPostProps {
|
interface EditPostProps {
|
||||||
ship: string;
|
ship: string;
|
||||||
@ -17,11 +16,10 @@ interface EditPostProps {
|
|||||||
note: GraphNode;
|
note: GraphNode;
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
book: string;
|
book: string;
|
||||||
storage: StorageState;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EditPost(props: EditPostProps & RouteComponentProps): ReactElement {
|
export function EditPost(props: EditPostProps & RouteComponentProps): ReactElement {
|
||||||
const { note, book, noteId, api, ship, history, storage } = props;
|
const { note, book, noteId, api, ship, history } = props;
|
||||||
const [revNum, title, body] = getLatestRevision(note);
|
const [revNum, title, body] = getLatestRevision(note);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
@ -58,7 +56,6 @@ export function EditPost(props: EditPostProps & RouteComponentProps): ReactEleme
|
|||||||
cancel
|
cancel
|
||||||
history={history}
|
history={history}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
storage={storage}
|
|
||||||
submitLabel="Update"
|
submitLabel="Update"
|
||||||
loadingText="Updating..."
|
loadingText="Updating..."
|
||||||
/>
|
/>
|
||||||
|
@ -28,7 +28,6 @@ interface MarkdownEditorProps {
|
|||||||
value: string;
|
value: string;
|
||||||
onChange: (s: string) => void;
|
onChange: (s: string) => void;
|
||||||
onBlur?: (e: any) => void;
|
onBlur?: (e: any) => void;
|
||||||
storage: StorageState;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const PromptIfDirty = () => {
|
const PromptIfDirty = () => {
|
||||||
@ -74,7 +73,7 @@ export function MarkdownEditor(
|
|||||||
[onBlur]
|
[onBlur]
|
||||||
);
|
);
|
||||||
|
|
||||||
const { uploadDefault, canUpload } = useStorage(props.storage);
|
const { uploadDefault, canUpload } = useStorage();
|
||||||
|
|
||||||
const onFileDrag = useCallback(
|
const onFileDrag = useCallback(
|
||||||
async (files: FileList | File[], e: DragEvent) => {
|
async (files: FileList | File[], e: DragEvent) => {
|
||||||
|
@ -6,7 +6,6 @@ import { MarkdownEditor } from './MarkdownEditor';
|
|||||||
|
|
||||||
export const MarkdownField = ({
|
export const MarkdownField = ({
|
||||||
id,
|
id,
|
||||||
storage,
|
|
||||||
...rest
|
...rest
|
||||||
}: { id: string } & Parameters<typeof Box>[0]) => {
|
}: { id: string } & Parameters<typeof Box>[0]) => {
|
||||||
const [{ value, onBlur }, { error, touched }, { setValue }] = useField(id);
|
const [{ value, onBlur }, { error, touched }, { setValue }] = useField(id);
|
||||||
@ -36,7 +35,6 @@ export const MarkdownField = ({
|
|||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={setValue}
|
onChange={setValue}
|
||||||
storage={storage}
|
|
||||||
/>
|
/>
|
||||||
<ErrorLabel mt="2" hasError={Boolean(error && touched)}>
|
<ErrorLabel mt="2" hasError={Boolean(error && touched)}>
|
||||||
{error}
|
{error}
|
||||||
|
@ -22,7 +22,6 @@ interface MetadataFormProps {
|
|||||||
host: string;
|
host: string;
|
||||||
book: string;
|
book: string;
|
||||||
association: Association;
|
association: Association;
|
||||||
contacts: Contacts;
|
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Box, Text, Col, Anchor } from '@tlon/indigo-react';
|
import { Box, Text, Col, Anchor, Row } from '@tlon/indigo-react';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import bigInt from 'big-integer';
|
import bigInt from 'big-integer';
|
||||||
|
|
||||||
@ -9,6 +9,7 @@ import { Comments } from '~/views/components/Comments';
|
|||||||
import { NoteNavigation } from './NoteNavigation';
|
import { NoteNavigation } from './NoteNavigation';
|
||||||
import GlobalApi from '~/logic/api/global';
|
import GlobalApi from '~/logic/api/global';
|
||||||
import { getLatestRevision, getComments } from '~/logic/lib/publish';
|
import { getLatestRevision, getComments } from '~/logic/lib/publish';
|
||||||
|
import { roleForShip } from '~/logic/lib/group';
|
||||||
import Author from '~/views/components/Author';
|
import Author from '~/views/components/Author';
|
||||||
import { Contacts, GraphNode, Graph, Association, Unreads, Group } from '@urbit/api';
|
import { Contacts, GraphNode, Graph, Association, Unreads, Group } from '@urbit/api';
|
||||||
|
|
||||||
@ -16,10 +17,8 @@ interface NoteProps {
|
|||||||
ship: string;
|
ship: string;
|
||||||
book: string;
|
book: string;
|
||||||
note: GraphNode;
|
note: GraphNode;
|
||||||
unreads: Unreads;
|
|
||||||
association: Association;
|
association: Association;
|
||||||
notebook: Graph;
|
notebook: Graph;
|
||||||
contacts: Contacts;
|
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
rootUrl: string;
|
rootUrl: string;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
@ -29,7 +28,7 @@ interface NoteProps {
|
|||||||
export function Note(props: NoteProps & RouteComponentProps) {
|
export function Note(props: NoteProps & RouteComponentProps) {
|
||||||
const [deleting, setDeleting] = useState(false);
|
const [deleting, setDeleting] = useState(false);
|
||||||
|
|
||||||
const { notebook, note, contacts, ship, book, api, rootUrl, baseUrl, group } = props;
|
const { notebook, note, ship, book, api, rootUrl, baseUrl, group } = props;
|
||||||
const editCommentId = props.match.params.commentId;
|
const editCommentId = props.match.params.commentId;
|
||||||
|
|
||||||
const renderers = {
|
const renderers = {
|
||||||
@ -56,29 +55,37 @@ export function Note(props: NoteProps & RouteComponentProps) {
|
|||||||
api.hark.markEachAsRead(props.association, '/',`/${index[1]}/1/1`, 'note', 'publish');
|
api.hark.markEachAsRead(props.association, '/',`/${index[1]}/1/1`, 'note', 'publish');
|
||||||
}, [props.association, props.note]);
|
}, [props.association, props.note]);
|
||||||
|
|
||||||
let adminLinks: JSX.Element | null = null;
|
let adminLinks: JSX.Element[] = [];
|
||||||
|
const ourRole = roleForShip(group, window.ship);
|
||||||
if (window.ship === note?.post?.author) {
|
if (window.ship === note?.post?.author) {
|
||||||
adminLinks = (
|
adminLinks.push(
|
||||||
<Box display="inline-block" verticalAlign="middle">
|
<Link
|
||||||
<Link to={`${baseUrl}/edit`}>
|
style={{ 'display': 'inline-block' }}
|
||||||
<Text
|
to={`${baseUrl}/edit`}
|
||||||
color="green"
|
|
||||||
ml={2}
|
|
||||||
>
|
>
|
||||||
Update
|
<Text
|
||||||
</Text>
|
color="blue"
|
||||||
|
ml={2}
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</Text>
|
||||||
</Link>
|
</Link>
|
||||||
<Text
|
)
|
||||||
color="red"
|
};
|
||||||
ml={2}
|
|
||||||
onClick={deletePost}
|
if (window.ship === note?.post?.author || ourRole === "admin") {
|
||||||
style={{ cursor: 'pointer' }}
|
adminLinks.push(
|
||||||
>
|
<Text
|
||||||
Delete
|
color="red"
|
||||||
</Text>
|
display='inline-block'
|
||||||
</Box>
|
ml={2}
|
||||||
);
|
onClick={deletePost}
|
||||||
}
|
style={{ cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
const windowRef = React.useRef(null);
|
const windowRef = React.useRef(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -105,14 +112,15 @@ export function Note(props: NoteProps & RouteComponentProps) {
|
|||||||
</Link>
|
</Link>
|
||||||
<Col>
|
<Col>
|
||||||
<Text display="block" mb={2}>{title || ''}</Text>
|
<Text display="block" mb={2}>{title || ''}</Text>
|
||||||
<Box display="flex">
|
<Row alignItems="center">
|
||||||
<Author
|
<Author
|
||||||
|
showImage
|
||||||
ship={post?.author}
|
ship={post?.author}
|
||||||
contacts={contacts}
|
|
||||||
date={post?.['time-sent']}
|
date={post?.['time-sent']}
|
||||||
|
group={group}
|
||||||
/>
|
/>
|
||||||
<Text ml={2}>{adminLinks}</Text>
|
<Text ml={1}>{adminLinks}</Text>
|
||||||
</Box>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
<Box color="black" className="md" style={{ overflowWrap: 'break-word', overflow: 'hidden' }}>
|
<Box color="black" className="md" style={{ overflowWrap: 'break-word', overflow: 'hidden' }}>
|
||||||
<ReactMarkdown source={body} linkTarget={'_blank'} renderers={renderers} />
|
<ReactMarkdown source={body} linkTarget={'_blank'} renderers={renderers} />
|
||||||
@ -126,9 +134,7 @@ export function Note(props: NoteProps & RouteComponentProps) {
|
|||||||
<Comments
|
<Comments
|
||||||
ship={ship}
|
ship={ship}
|
||||||
name={props.book}
|
name={props.book}
|
||||||
unreads={props.unreads}
|
|
||||||
comments={comments}
|
comments={comments}
|
||||||
contacts={props.contacts}
|
|
||||||
association={props.association}
|
association={props.association}
|
||||||
api={props.api}
|
api={props.api}
|
||||||
baseUrl={baseUrl}
|
baseUrl={baseUrl}
|
||||||
|
@ -9,7 +9,6 @@ import {
|
|||||||
import { AsyncButton } from '../../../components/AsyncButton';
|
import { AsyncButton } from '../../../components/AsyncButton';
|
||||||
import { Formik, Form, FormikHelpers } from 'formik';
|
import { Formik, Form, FormikHelpers } from 'formik';
|
||||||
import { MarkdownField } from './MarkdownField';
|
import { MarkdownField } from './MarkdownField';
|
||||||
import { StorageState } from '~/types';
|
|
||||||
|
|
||||||
interface PostFormProps {
|
interface PostFormProps {
|
||||||
initial: PostFormSchema;
|
initial: PostFormSchema;
|
||||||
@ -21,7 +20,6 @@ interface PostFormProps {
|
|||||||
) => Promise<any>;
|
) => Promise<any>;
|
||||||
submitLabel: string;
|
submitLabel: string;
|
||||||
loadingText: string;
|
loadingText: string;
|
||||||
storage: StorageState;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const formSchema = Yup.object({
|
const formSchema = Yup.object({
|
||||||
@ -35,7 +33,7 @@ export interface PostFormSchema {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function PostForm(props: PostFormProps) {
|
export function PostForm(props: PostFormProps) {
|
||||||
const { initial, onSubmit, submitLabel, loadingText, storage, cancel, history } = props;
|
const { initial, onSubmit, submitLabel, loadingText, cancel, history } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col width="100%" height="100%" p={[2, 4]}>
|
<Col width="100%" height="100%" p={[2, 4]}>
|
||||||
@ -67,7 +65,7 @@ export function PostForm(props: PostFormProps) {
|
|||||||
>Cancel</Button>}
|
>Cancel</Button>}
|
||||||
</Row>
|
</Row>
|
||||||
</Row>
|
</Row>
|
||||||
<MarkdownField flexGrow={1} id="body" storage={storage} />
|
<MarkdownField flexGrow={1} id="body" />
|
||||||
</Form>
|
</Form>
|
||||||
</Formik>
|
</Formik>
|
||||||
</Col>
|
</Col>
|
||||||
|
@ -12,17 +12,14 @@ import {
|
|||||||
getSnippet
|
getSnippet
|
||||||
} from '~/logic/lib/publish';
|
} from '~/logic/lib/publish';
|
||||||
import { Unreads } from '@urbit/api';
|
import { Unreads } from '@urbit/api';
|
||||||
import GlobalApi from '~/logic/api/global';
|
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
import useHarkState from '~/logic/state/hark';
|
||||||
|
|
||||||
interface NotePreviewProps {
|
interface NotePreviewProps {
|
||||||
host: string;
|
host: string;
|
||||||
book: string;
|
book: string;
|
||||||
node: GraphNode;
|
node: GraphNode;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
unreads: Unreads;
|
|
||||||
contacts: Contacts;
|
|
||||||
api: GlobalApi;
|
|
||||||
group: Group;
|
group: Group;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,7 +28,7 @@ const WrappedBox = styled(Box)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export function NotePreview(props: NotePreviewProps) {
|
export function NotePreview(props: NotePreviewProps) {
|
||||||
const { node, contacts, group } = props;
|
const { node, group } = props;
|
||||||
const { post } = node;
|
const { post } = node;
|
||||||
if (!post) {
|
if (!post) {
|
||||||
return null;
|
return null;
|
||||||
@ -43,11 +40,12 @@ export function NotePreview(props: NotePreviewProps) {
|
|||||||
|
|
||||||
const [rev, title, body, content] = getLatestRevision(node);
|
const [rev, title, body, content] = getLatestRevision(node);
|
||||||
const appPath = `/ship/${props.host}/${props.book}`;
|
const appPath = `/ship/${props.host}/${props.book}`;
|
||||||
const isUnread = props.unreads.graph?.[appPath]?.['/']?.unreads?.has(`/${noteId}/1/1`);
|
const unreads = useHarkState(state => state.unreads);
|
||||||
|
const isUnread = unreads.graph?.[appPath]?.['/']?.unreads?.has(`/${noteId}/1/1`);
|
||||||
|
|
||||||
const snippet = getSnippet(body);
|
const snippet = getSnippet(body);
|
||||||
|
|
||||||
const commColor = (props.unreads.graph?.[appPath]?.[`/${noteId}`]?.unreads ?? 0) > 0 ? 'blue' : 'gray';
|
const commColor = (unreads.graph?.[appPath]?.[`/${noteId}`]?.unreads ?? 0) > 0 ? 'blue' : 'gray';
|
||||||
|
|
||||||
const cursorStyle = post.pending ? 'default' : 'pointer';
|
const cursorStyle = post.pending ? 'default' : 'pointer';
|
||||||
|
|
||||||
@ -92,12 +90,10 @@ export function NotePreview(props: NotePreviewProps) {
|
|||||||
<Row minWidth='0' flexShrink={0} width="100%" justifyContent="space-between" py={3} bg="white">
|
<Row minWidth='0' flexShrink={0} width="100%" justifyContent="space-between" py={3} bg="white">
|
||||||
<Author
|
<Author
|
||||||
showImage
|
showImage
|
||||||
contacts={contacts}
|
|
||||||
ship={post?.author}
|
ship={post?.author}
|
||||||
date={post?.['time-sent']}
|
date={post?.['time-sent']}
|
||||||
group={group}
|
group={group}
|
||||||
unread={isUnread}
|
unread={isUnread}
|
||||||
api={props.api}
|
|
||||||
/>
|
/>
|
||||||
<Box ml="auto" mr={1}>
|
<Box ml="auto" mr={1}>
|
||||||
<Link to={url}>
|
<Link to={url}>
|
||||||
|
@ -15,13 +15,11 @@ interface NoteRoutesProps {
|
|||||||
note: GraphNode;
|
note: GraphNode;
|
||||||
noteId: number;
|
noteId: number;
|
||||||
notebook: Graph;
|
notebook: Graph;
|
||||||
contacts: Contacts;
|
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
association: Association;
|
association: Association;
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
rootUrl?: string;
|
rootUrl?: string;
|
||||||
group: Group;
|
group: Group
|
||||||
storage: StorageState;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NoteRoutes(props: NoteRoutesProps & RouteComponentProps) {
|
export function NoteRoutes(props: NoteRoutesProps & RouteComponentProps) {
|
||||||
|
@ -5,45 +5,42 @@ import { Col, Box, Text, Row } from '@tlon/indigo-react';
|
|||||||
import { Contacts, Rolodex, Groups, Associations, Graph, Association, Unreads } from '@urbit/api';
|
import { Contacts, Rolodex, Groups, Associations, Graph, Association, Unreads } from '@urbit/api';
|
||||||
|
|
||||||
import { NotebookPosts } from './NotebookPosts';
|
import { NotebookPosts } from './NotebookPosts';
|
||||||
import GlobalApi from '~/logic/api/global';
|
|
||||||
import { useShowNickname } from '~/logic/lib/util';
|
import { useShowNickname } from '~/logic/lib/util';
|
||||||
|
import useContactState from '~/logic/state/contact';
|
||||||
|
import useGroupState from '~/logic/state/group';
|
||||||
|
|
||||||
interface NotebookProps {
|
interface NotebookProps {
|
||||||
api: GlobalApi;
|
|
||||||
ship: string;
|
ship: string;
|
||||||
book: string;
|
book: string;
|
||||||
graph: Graph;
|
graph: Graph;
|
||||||
association: Association;
|
association: Association;
|
||||||
associations: Associations;
|
|
||||||
contacts: Rolodex;
|
|
||||||
groups: Groups;
|
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
rootUrl: string;
|
rootUrl: string;
|
||||||
unreads: Unreads;
|
unreads: Unreads;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Notebook(props: NotebookProps & RouteComponentProps): ReactElement {
|
export function Notebook(props: NotebookProps & RouteComponentProps): ReactElement | null {
|
||||||
const {
|
const {
|
||||||
ship,
|
ship,
|
||||||
book,
|
book,
|
||||||
contacts,
|
|
||||||
groups,
|
|
||||||
association,
|
association,
|
||||||
graph
|
graph
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const group = groups[association?.group];
|
const groups = useGroupState(state => state.groups);
|
||||||
if (!group) {
|
const contacts = useContactState(state => state.contacts);
|
||||||
return null; // Waiting on groups to populate
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const group = groups[association?.group];
|
||||||
const relativePath = (p: string) => props.baseUrl + p;
|
const relativePath = (p: string) => props.baseUrl + p;
|
||||||
|
|
||||||
const contact = contacts?.[`~${ship}`];
|
const contact = contacts?.[`~${ship}`];
|
||||||
console.log(association.resource);
|
|
||||||
|
|
||||||
const showNickname = useShowNickname(contact);
|
const showNickname = useShowNickname(contact);
|
||||||
|
|
||||||
|
if (!group) {
|
||||||
|
return null; // Waiting on groups to populate
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col gapY="4" pt={4} mx="auto" px={3} maxWidth="768px">
|
<Col gapY="4" pt={4} mx="auto" px={3} maxWidth="768px">
|
||||||
<Row justifyContent="space-between">
|
<Row justifyContent="space-between">
|
||||||
@ -60,10 +57,7 @@ export function Notebook(props: NotebookProps & RouteComponentProps): ReactEleme
|
|||||||
graph={graph}
|
graph={graph}
|
||||||
host={ship}
|
host={ship}
|
||||||
book={book}
|
book={book}
|
||||||
contacts={contacts}
|
|
||||||
unreads={props.unreads}
|
|
||||||
baseUrl={props.baseUrl}
|
baseUrl={props.baseUrl}
|
||||||
api={props.api}
|
|
||||||
group={group}
|
group={group}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
@ -2,21 +2,20 @@ import React, { Component } from 'react';
|
|||||||
import { Col } from '@tlon/indigo-react';
|
import { Col } from '@tlon/indigo-react';
|
||||||
import { NotePreview } from './NotePreview';
|
import { NotePreview } from './NotePreview';
|
||||||
import { Contacts, Graph, Unreads, Group } from '@urbit/api';
|
import { Contacts, Graph, Unreads, Group } from '@urbit/api';
|
||||||
|
import useContactState from '~/logic/state/contact';
|
||||||
|
|
||||||
interface NotebookPostsProps {
|
interface NotebookPostsProps {
|
||||||
contacts: Contacts;
|
|
||||||
graph: Graph;
|
graph: Graph;
|
||||||
host: string;
|
host: string;
|
||||||
book: string;
|
book: string;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
unreads: Unreads;
|
|
||||||
hideAvatars?: boolean;
|
hideAvatars?: boolean;
|
||||||
hideNicknames?: boolean;
|
hideNicknames?: boolean;
|
||||||
api: GlobalApi;
|
|
||||||
group: Group;
|
group: Group;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NotebookPosts(props: NotebookPostsProps) {
|
export function NotebookPosts(props: NotebookPostsProps) {
|
||||||
|
const contacts = useContactState(state => state.contacts);
|
||||||
return (
|
return (
|
||||||
<Col>
|
<Col>
|
||||||
{Array.from(props.graph || []).map(
|
{Array.from(props.graph || []).map(
|
||||||
@ -26,12 +25,9 @@ export function NotebookPosts(props: NotebookPostsProps) {
|
|||||||
key={date.toString()}
|
key={date.toString()}
|
||||||
host={props.host}
|
host={props.host}
|
||||||
book={props.book}
|
book={props.book}
|
||||||
unreads={props.unreads}
|
contact={contacts[`~${node.post.author}`]}
|
||||||
contact={props.contacts[`~${node.post.author}`]}
|
|
||||||
contacts={props.contacts}
|
|
||||||
node={node}
|
node={node}
|
||||||
baseUrl={props.baseUrl}
|
baseUrl={props.baseUrl}
|
||||||
api={props.api}
|
|
||||||
group={props.group}
|
group={props.group}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@ -17,32 +17,32 @@ import bigInt from 'big-integer';
|
|||||||
import Notebook from './Notebook';
|
import Notebook from './Notebook';
|
||||||
import NewPost from './new-post';
|
import NewPost from './new-post';
|
||||||
import { NoteRoutes } from './NoteRoutes';
|
import { NoteRoutes } from './NoteRoutes';
|
||||||
|
import useGraphState from '~/logic/state/graph';
|
||||||
|
import useGroupState from '~/logic/state/group';
|
||||||
|
|
||||||
interface NotebookRoutesProps {
|
interface NotebookRoutesProps {
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
ship: string;
|
ship: string;
|
||||||
book: string;
|
book: string;
|
||||||
graphs: Graphs;
|
|
||||||
unreads: Unreads;
|
|
||||||
contacts: Rolodex;
|
|
||||||
groups: Groups;
|
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
rootUrl: string;
|
rootUrl: string;
|
||||||
association: Association;
|
association: Association;
|
||||||
associations: Associations;
|
|
||||||
storage: StorageState;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NotebookRoutes(
|
export function NotebookRoutes(
|
||||||
props: NotebookRoutesProps & RouteComponentProps
|
props: NotebookRoutesProps & RouteComponentProps
|
||||||
) {
|
) {
|
||||||
const { ship, book, api, contacts, baseUrl, rootUrl, groups } = props;
|
const { ship, book, api, baseUrl, rootUrl } = props;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
ship && book && api.graph.getGraph(ship, book);
|
ship && book && api.graph.getGraph(ship, book);
|
||||||
}, [ship, book]);
|
}, [ship, book]);
|
||||||
|
|
||||||
const graph = props.graphs[`${ship.slice(1)}/${book}`];
|
const graphs = useGraphState(state => state.graphs);
|
||||||
|
|
||||||
|
const graph = graphs[`${ship.slice(1)}/${book}`];
|
||||||
|
|
||||||
|
const groups = useGroupState(state => state.groups);
|
||||||
|
|
||||||
const group = groups?.[props.association?.group];
|
const group = groups?.[props.association?.group];
|
||||||
|
|
||||||
@ -59,7 +59,6 @@ export function NotebookRoutes(
|
|||||||
return <Notebook
|
return <Notebook
|
||||||
{...props}
|
{...props}
|
||||||
graph={graph}
|
graph={graph}
|
||||||
contacts={contacts}
|
|
||||||
association={props.association}
|
association={props.association}
|
||||||
rootUrl={rootUrl}
|
rootUrl={rootUrl}
|
||||||
baseUrl={baseUrl}
|
baseUrl={baseUrl}
|
||||||
@ -77,7 +76,6 @@ export function NotebookRoutes(
|
|||||||
association={props.association}
|
association={props.association}
|
||||||
graph={graph}
|
graph={graph}
|
||||||
baseUrl={baseUrl}
|
baseUrl={baseUrl}
|
||||||
storage={props.storage}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -104,12 +102,9 @@ export function NotebookRoutes(
|
|||||||
ship={ship}
|
ship={ship}
|
||||||
note={note}
|
note={note}
|
||||||
notebook={graph}
|
notebook={graph}
|
||||||
unreads={props.unreads}
|
|
||||||
noteId={noteIdNum}
|
noteId={noteIdNum}
|
||||||
contacts={contacts}
|
|
||||||
association={props.association}
|
association={props.association}
|
||||||
group={group}
|
group={group}
|
||||||
storage={props.storage}
|
|
||||||
{...routeProps}
|
{...routeProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -7,7 +7,7 @@ import { AsyncButton } from '~/views/components/AsyncButton';
|
|||||||
|
|
||||||
export class Writers extends Component {
|
export class Writers extends Component {
|
||||||
render() {
|
render() {
|
||||||
const { association, groups, contacts, api } = this.props;
|
const { association, groups, api } = this.props;
|
||||||
|
|
||||||
const resource = resourceFromPath(association?.group);
|
const resource = resourceFromPath(association?.group);
|
||||||
|
|
||||||
@ -39,8 +39,6 @@ export class Writers extends Component {
|
|||||||
>
|
>
|
||||||
<Form>
|
<Form>
|
||||||
<ShipSearch
|
<ShipSearch
|
||||||
groups={groups}
|
|
||||||
contacts={contacts}
|
|
||||||
id="ships"
|
id="ships"
|
||||||
label=""
|
label=""
|
||||||
maxLength={undefined}
|
maxLength={undefined}
|
||||||
|
@ -17,7 +17,6 @@ interface NewPostProps {
|
|||||||
graph: Graph;
|
graph: Graph;
|
||||||
association: Association;
|
association: Association;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
storage: StorageState;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function NewPost(props: NewPostProps & RouteComponentProps) {
|
export default function NewPost(props: NewPostProps & RouteComponentProps) {
|
||||||
@ -51,7 +50,6 @@ export default function NewPost(props: NewPostProps & RouteComponentProps) {
|
|||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
submitLabel="Publish"
|
submitLabel="Publish"
|
||||||
loadingText="Posting..."
|
loadingText="Posting..."
|
||||||
storage={props.storage}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -142,6 +142,10 @@
|
|||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.md ul ul {
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
.md h2, .md h3, .md h4, .md h5, .md p, .md a, .md ul {
|
.md h2, .md h3, .md h4, .md h5, .md p, .md a, .md ul {
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
@ -4,8 +4,16 @@ import { Text } from '@tlon/indigo-react';
|
|||||||
|
|
||||||
export function BackButton(props: {}) {
|
export function BackButton(props: {}) {
|
||||||
return (
|
return (
|
||||||
<Link to="/~settings">
|
<Link to='/~settings'>
|
||||||
<Text display={["block", "none"]} fontSize="2" fontWeight="medium">{"<- Back to System Preferences"}</Text>
|
<Text
|
||||||
|
display={['block', 'none']}
|
||||||
|
fontSize='2'
|
||||||
|
fontWeight='medium'
|
||||||
|
p={4}
|
||||||
|
pb={0}
|
||||||
|
>
|
||||||
|
{'<- Back to System Preferences'}
|
||||||
|
</Text>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -20,13 +20,11 @@ export function BackgroundPicker({
|
|||||||
bgType,
|
bgType,
|
||||||
bgUrl,
|
bgUrl,
|
||||||
api,
|
api,
|
||||||
storage
|
|
||||||
}: {
|
}: {
|
||||||
bgType: BgType;
|
bgType: BgType;
|
||||||
bgUrl?: string;
|
bgUrl?: string;
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
storage: StorageState;
|
}): ReactElement {
|
||||||
}) {
|
|
||||||
const rowSpace = { my: 0, alignItems: 'center' };
|
const rowSpace = { my: 0, alignItems: 'center' };
|
||||||
const colProps = { my: 3, mr: 4, gapY: 1 };
|
const colProps = { my: 3, mr: 4, gapY: 1 };
|
||||||
return (
|
return (
|
||||||
@ -39,7 +37,6 @@ export function BackgroundPicker({
|
|||||||
<ImageInput
|
<ImageInput
|
||||||
ml="5"
|
ml="5"
|
||||||
api={api}
|
api={api}
|
||||||
storage={storage}
|
|
||||||
id="bgUrl"
|
id="bgUrl"
|
||||||
placeholder="Drop or upload a file, or paste a link here"
|
placeholder="Drop or upload a file, or paste a link here"
|
||||||
name="bgUrl"
|
name="bgUrl"
|
||||||
|
@ -54,10 +54,10 @@ export function CalmPrefs(props: {
|
|||||||
hideUnreads,
|
hideUnreads,
|
||||||
hideGroups,
|
hideGroups,
|
||||||
hideUtilities,
|
hideUtilities,
|
||||||
imageShown,
|
imageShown: !imageShown,
|
||||||
videoShown,
|
videoShown: !videoShown,
|
||||||
oembedShown,
|
oembedShown: !oembedShown,
|
||||||
audioShown,
|
audioShown: !audioShown
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = useCallback(async (v: FormSchema, actions: FormikHelpers<FormSchema>) => {
|
const onSubmit = useCallback(async (v: FormSchema, actions: FormikHelpers<FormSchema>) => {
|
||||||
@ -67,10 +67,10 @@ export function CalmPrefs(props: {
|
|||||||
api.settings.putEntry('calm', 'hideUnreads', v.hideUnreads),
|
api.settings.putEntry('calm', 'hideUnreads', v.hideUnreads),
|
||||||
api.settings.putEntry('calm', 'hideGroups', v.hideGroups),
|
api.settings.putEntry('calm', 'hideGroups', v.hideGroups),
|
||||||
api.settings.putEntry('calm', 'hideUtilities', v.hideUtilities),
|
api.settings.putEntry('calm', 'hideUtilities', v.hideUtilities),
|
||||||
api.settings.putEntry('remoteContentPolicy', 'imageShown', v.imageShown),
|
api.settings.putEntry('remoteContentPolicy', 'imageShown', !v.imageShown),
|
||||||
api.settings.putEntry('remoteContentPolicy', 'videoShown', v.videoShown),
|
api.settings.putEntry('remoteContentPolicy', 'videoShown', !v.videoShown),
|
||||||
api.settings.putEntry('remoteContentPolicy', 'audioShown', v.audioShown),
|
api.settings.putEntry('remoteContentPolicy', 'audioShown', !v.audioShown),
|
||||||
api.settings.putEntry('remoteContentPolicy', 'oembedShown', v.oembedShown),
|
api.settings.putEntry('remoteContentPolicy', 'oembedShown', !v.oembedShown),
|
||||||
]);
|
]);
|
||||||
actions.setStatus({ success: null });
|
actions.setStatus({ success: null });
|
||||||
}, [api]);
|
}, [api]);
|
||||||
@ -80,7 +80,7 @@ export function CalmPrefs(props: {
|
|||||||
<Form>
|
<Form>
|
||||||
<BackButton/>
|
<BackButton/>
|
||||||
<Col borderBottom="1" borderBottomColor="washedGray" p="5" pt="4" gapY="5">
|
<Col borderBottom="1" borderBottomColor="washedGray" p="5" pt="4" gapY="5">
|
||||||
<Col gapY="1" mt="0">
|
<Col gapY="1" mt="0">
|
||||||
<Text color="black" fontSize={2} fontWeight="medium">
|
<Text color="black" fontSize={2} fontWeight="medium">
|
||||||
CalmEngine
|
CalmEngine
|
||||||
</Text>
|
</Text>
|
||||||
@ -115,24 +115,24 @@ export function CalmPrefs(props: {
|
|||||||
id="hideNicknames"
|
id="hideNicknames"
|
||||||
caption="Do not show user-set nicknames"
|
caption="Do not show user-set nicknames"
|
||||||
/>
|
/>
|
||||||
<Text fontWeight="medium">Remote Content</Text>
|
<Text fontWeight="medium">Remote content</Text>
|
||||||
<Toggle
|
<Toggle
|
||||||
label="Load images"
|
label="Disable images"
|
||||||
id="imageShown"
|
id="imageShown"
|
||||||
caption="Images will be replaced with an inline placeholder that must be clicked to be viewed"
|
caption="Images will be replaced with an inline placeholder that must be clicked to be viewed"
|
||||||
/>
|
/>
|
||||||
<Toggle
|
<Toggle
|
||||||
label="Load audio files"
|
label="Disable audio files"
|
||||||
id="audioShown"
|
id="audioShown"
|
||||||
caption="Audio content will be replaced with an inline placeholder that must be clicked to be viewed"
|
caption="Audio content will be replaced with an inline placeholder that must be clicked to be viewed"
|
||||||
/>
|
/>
|
||||||
<Toggle
|
<Toggle
|
||||||
label="Load video files"
|
label="Disable video files"
|
||||||
id="videoShown"
|
id="videoShown"
|
||||||
caption="Video content will be replaced with an inline placeholder that must be clicked to be viewed"
|
caption="Video content will be replaced with an inline placeholder that must be clicked to be viewed"
|
||||||
/>
|
/>
|
||||||
<Toggle
|
<Toggle
|
||||||
label="Load embedded content"
|
label="Disable embedded content"
|
||||||
id="oembedShown"
|
id="oembedShown"
|
||||||
caption="Embedded content may contain scripts that can track you"
|
caption="Embedded content may contain scripts that can track you"
|
||||||
/>
|
/>
|
||||||
|
101
pkg/interface/src/views/apps/settings/components/lib/Debug.tsx
Normal file
101
pkg/interface/src/views/apps/settings/components/lib/Debug.tsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import { BaseInput, Box, Col, Text } from "@tlon/indigo-react";
|
||||||
|
import _ from "lodash";
|
||||||
|
import React, { useCallback, useState } from "react";
|
||||||
|
import { UseStore } from "zustand";
|
||||||
|
import { BaseState } from "~/logic/state/base";
|
||||||
|
import useContactState from "~/logic/state/contact";
|
||||||
|
import useGraphState from "~/logic/state/graph";
|
||||||
|
import useGroupState from "~/logic/state/group";
|
||||||
|
import useHarkState from "~/logic/state/hark";
|
||||||
|
import useInviteState from "~/logic/state/invite";
|
||||||
|
import useLaunchState from "~/logic/state/launch";
|
||||||
|
import useMetadataState from "~/logic/state/metadata";
|
||||||
|
import useSettingsState from "~/logic/state/settings";
|
||||||
|
import useStorageState from "~/logic/state/storage";
|
||||||
|
import { BackButton } from "./BackButton";
|
||||||
|
|
||||||
|
interface StoreDebuggerProps {
|
||||||
|
name: string;
|
||||||
|
useStore: UseStore<BaseState<any>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const objectToString = (obj: any): string => JSON.stringify(obj, null, ' ');
|
||||||
|
|
||||||
|
const StoreDebugger = (props: StoreDebuggerProps) => {
|
||||||
|
const name = props.name;
|
||||||
|
const state = props.useStore();
|
||||||
|
const [filter, setFilter] = useState('');
|
||||||
|
const [text, setText] = useState(objectToString(state));
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
|
||||||
|
const tryFilter = useCallback((filterToTry) => {
|
||||||
|
let output: any = false;
|
||||||
|
try {
|
||||||
|
output = _.get(state, filterToTry, undefined);
|
||||||
|
} catch (e) { }
|
||||||
|
if (output) {
|
||||||
|
console.log(output);
|
||||||
|
setText(objectToString(output));
|
||||||
|
setFilter(filterToTry);
|
||||||
|
}
|
||||||
|
}, [state, filter, text]);
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box p={1}>
|
||||||
|
<Text cursor="pointer" onClick={() => setVisible(!visible)}>{name}</Text>
|
||||||
|
{visible && <Box>
|
||||||
|
<BaseInput
|
||||||
|
position="sticky"
|
||||||
|
top={0}
|
||||||
|
my={1}
|
||||||
|
p={2}
|
||||||
|
backgroundColor='white'
|
||||||
|
color='black'
|
||||||
|
border='1px solid transparent'
|
||||||
|
borderRadius='2'
|
||||||
|
fontSize={1}
|
||||||
|
placeholder="Drill Down"
|
||||||
|
width="100%"
|
||||||
|
onKeyUp={event => {
|
||||||
|
if (event.target.value) {
|
||||||
|
tryFilter(event.target.value);
|
||||||
|
} else {
|
||||||
|
setFilter('');
|
||||||
|
setText(objectToString(state));
|
||||||
|
}
|
||||||
|
}} />
|
||||||
|
<Text mono p='1' borderRadius='1' display='block' overflow='auto' backgroundColor='washedGray' style={{ whiteSpace: 'pre', wordWrap: 'break-word' }}>{text}</Text>
|
||||||
|
</Box>}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const DebugPane = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<BackButton />
|
||||||
|
<Col borderBottom="1" borderBottomColor="washedGray" p="5" pt="4" gapY="5">
|
||||||
|
<Col gapY="1" mt="0">
|
||||||
|
<Text color="black" fontSize={2} fontWeight="medium">
|
||||||
|
Debug Menu
|
||||||
|
</Text>
|
||||||
|
<Text gray>
|
||||||
|
Debug Landscape state. Click any state to see its contents and drill down.
|
||||||
|
</Text>
|
||||||
|
</Col>
|
||||||
|
<StoreDebugger name="Contacts" useStore={useContactState} />
|
||||||
|
<StoreDebugger name="Graph" useStore={useGraphState} />
|
||||||
|
<StoreDebugger name="Group" useStore={useGroupState} />
|
||||||
|
<StoreDebugger name="Hark" useStore={useHarkState} />
|
||||||
|
<StoreDebugger name="Invite" useStore={useInviteState} />
|
||||||
|
<StoreDebugger name="Launch" useStore={useLaunchState} />
|
||||||
|
<StoreDebugger name="Metadata" useStore={useMetadataState} />
|
||||||
|
<StoreDebugger name="Settings" useStore={useSettingsState} />
|
||||||
|
<StoreDebugger name="Storage" useStore={useStorageState} />
|
||||||
|
</Col>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DebugPane;
|
@ -36,13 +36,12 @@ interface FormSchema {
|
|||||||
|
|
||||||
interface DisplayFormProps {
|
interface DisplayFormProps {
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
storage: StorageState;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const settingsSel = selectSettingsState(["display"]);
|
const settingsSel = selectSettingsState(["display"]);
|
||||||
|
|
||||||
export default function DisplayForm(props: DisplayFormProps) {
|
export default function DisplayForm(props: DisplayFormProps) {
|
||||||
const { api, storage } = props;
|
const { api } = props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
display: {
|
display: {
|
||||||
@ -108,7 +107,6 @@ export default function DisplayForm(props: DisplayFormProps) {
|
|||||||
bgType={props.values.bgType}
|
bgType={props.values.bgType}
|
||||||
bgUrl={props.values.bgUrl}
|
bgUrl={props.values.bgUrl}
|
||||||
api={api}
|
api={api}
|
||||||
storage={storage}
|
|
||||||
/>
|
/>
|
||||||
<Label>Theme</Label>
|
<Label>Theme</Label>
|
||||||
<Radio name="theme" id="light" label="Light"/>
|
<Radio name="theme" id="light" label="Light"/>
|
||||||
|
@ -4,11 +4,12 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
ManagedToggleSwitchField as Toggle,
|
ManagedToggleSwitchField as Toggle,
|
||||||
} from "@tlon/indigo-react";
|
} from "@tlon/indigo-react";
|
||||||
import { Form, FormikHelpers } from "formik";
|
import { Formik, Form, FormikHelpers } from "formik";
|
||||||
import { FormikOnBlur } from "~/views/components/FormikOnBlur";
|
|
||||||
import { BackButton } from "./BackButton";
|
import { BackButton } from "./BackButton";
|
||||||
import GlobalApi from "~/logic/api/global";
|
import GlobalApi from "~/logic/api/global";
|
||||||
import {NotificationGraphConfig} from "~/types";
|
import useHarkState from "~/logic/state/hark";
|
||||||
|
import _ from "lodash";
|
||||||
|
import {AsyncButton} from "~/views/components/AsyncButton";
|
||||||
|
|
||||||
interface FormSchema {
|
interface FormSchema {
|
||||||
mentions: boolean;
|
mentions: boolean;
|
||||||
@ -18,10 +19,10 @@ interface FormSchema {
|
|||||||
|
|
||||||
export function NotificationPreferences(props: {
|
export function NotificationPreferences(props: {
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
graphConfig: NotificationGraphConfig;
|
|
||||||
dnd: boolean;
|
|
||||||
}) {
|
}) {
|
||||||
const { graphConfig, api, dnd } = props;
|
const { api } = props;
|
||||||
|
const dnd = useHarkState(state => state.doNotDisturb);
|
||||||
|
const graphConfig = useHarkState(state => state.notificationsGraphConfig);
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
mentions: graphConfig.mentions,
|
mentions: graphConfig.mentions,
|
||||||
dnd: dnd,
|
dnd: dnd,
|
||||||
@ -43,12 +44,11 @@ export function NotificationPreferences(props: {
|
|||||||
|
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
actions.setStatus({ success: null });
|
actions.setStatus({ success: null });
|
||||||
actions.resetForm({ values: initialValues });
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
actions.setStatus({ error: e.message });
|
actions.setStatus({ error: e.message });
|
||||||
}
|
}
|
||||||
}, [api]);
|
}, [api, graphConfig, dnd]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -63,7 +63,7 @@ export function NotificationPreferences(props: {
|
|||||||
messaging
|
messaging
|
||||||
</Text>
|
</Text>
|
||||||
</Col>
|
</Col>
|
||||||
<FormikOnBlur initialValues={initialValues} onSubmit={onSubmit}>
|
<Formik initialValues={initialValues} onSubmit={onSubmit}>
|
||||||
<Form>
|
<Form>
|
||||||
<Col gapY="4">
|
<Col gapY="4">
|
||||||
<Toggle
|
<Toggle
|
||||||
@ -81,9 +81,12 @@ export function NotificationPreferences(props: {
|
|||||||
id="mentions"
|
id="mentions"
|
||||||
caption="Notify me if someone mentions my @p in a channel I've joined"
|
caption="Notify me if someone mentions my @p in a channel I've joined"
|
||||||
/>
|
/>
|
||||||
|
<AsyncButton primary width="fit-content">
|
||||||
|
Save
|
||||||
|
</AsyncButton>
|
||||||
</Col>
|
</Col>
|
||||||
</Form>
|
</Form>
|
||||||
</FormikOnBlur>
|
</Formik>
|
||||||
</Col>
|
</Col>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -1,85 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
ManagedCheckboxField as Checkbox
|
|
||||||
} from '@tlon/indigo-react';
|
|
||||||
import { Formik, Form } from 'formik';
|
|
||||||
import * as Yup from 'yup';
|
|
||||||
|
|
||||||
import GlobalApi from '~/logic/api/global';
|
|
||||||
import useSettingsState, {selectSettingsState} from '~/logic/state/settings';
|
|
||||||
|
|
||||||
const formSchema = Yup.object().shape({
|
|
||||||
imageShown: Yup.boolean(),
|
|
||||||
audioShown: Yup.boolean(),
|
|
||||||
videoShown: Yup.boolean(),
|
|
||||||
oembedShown: Yup.boolean()
|
|
||||||
});
|
|
||||||
|
|
||||||
interface FormSchema {
|
|
||||||
imageShown: boolean;
|
|
||||||
audioShown: boolean;
|
|
||||||
videoShown: boolean;
|
|
||||||
oembedShown: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RemoteContentFormProps {
|
|
||||||
api: GlobalApi;
|
|
||||||
}
|
|
||||||
const selState = selectSettingsState(['remoteContentPolicy', 'set']);
|
|
||||||
|
|
||||||
export default function RemoteContentForm(props: RemoteContentFormProps) {
|
|
||||||
const { api } = props;
|
|
||||||
const { remoteContentPolicy, set: setRemoteContentPolicy} = useSettingsState(selState);
|
|
||||||
const imageShown = remoteContentPolicy.imageShown;
|
|
||||||
const audioShown = remoteContentPolicy.audioShown;
|
|
||||||
const videoShown = remoteContentPolicy.videoShown;
|
|
||||||
const oembedShown = remoteContentPolicy.oembedShown;
|
|
||||||
return (
|
|
||||||
<Formik
|
|
||||||
validationSchema={formSchema}
|
|
||||||
initialValues={
|
|
||||||
{
|
|
||||||
imageShown,
|
|
||||||
audioShown,
|
|
||||||
videoShown,
|
|
||||||
oembedShown
|
|
||||||
} as FormSchema
|
|
||||||
}
|
|
||||||
onSubmit={(values, actions) => {
|
|
||||||
setRemoteContentPolicy((state) => {
|
|
||||||
Object.assign(state.remoteContentPolicy, values);
|
|
||||||
});
|
|
||||||
actions.setSubmitting(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{props => (
|
|
||||||
<Form>
|
|
||||||
<Box
|
|
||||||
display="grid"
|
|
||||||
gridTemplateColumns="1fr"
|
|
||||||
gridTemplateRows="audio"
|
|
||||||
gridRowGap={5}
|
|
||||||
>
|
|
||||||
<Box color="black" fontSize={1} fontWeight={900}>
|
|
||||||
Remote Content
|
|
||||||
</Box>
|
|
||||||
<Checkbox label="Load images" id="imageShown" />
|
|
||||||
<Checkbox label="Load audio files" id="audioShown" />
|
|
||||||
<Checkbox label="Load video files" id="videoShown" />
|
|
||||||
<Checkbox
|
|
||||||
label="Load embedded content"
|
|
||||||
id="oembedShown"
|
|
||||||
caption="Embedded content may contain scripts"
|
|
||||||
/>
|
|
||||||
<Button style={{ cursor: 'pointer' }} border={1} borderColor="washedGray" type="submit">
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Form>
|
|
||||||
)}
|
|
||||||
</Formik>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
|||||||
import React, { ReactElement, useCallback } from 'react';
|
import React, { ReactElement, useCallback } from 'react';
|
||||||
import { Formik } from 'formik';
|
import { Formik, FormikHelpers } from 'formik';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ManagedTextInputField as Input,
|
ManagedTextInputField as Input,
|
||||||
@ -10,12 +10,15 @@ import {
|
|||||||
Col,
|
Col,
|
||||||
Anchor
|
Anchor
|
||||||
} from '@tlon/indigo-react';
|
} from '@tlon/indigo-react';
|
||||||
|
import { AsyncButton } from "~/views/components/AsyncButton";
|
||||||
|
|
||||||
import GlobalApi from "~/logic/api/global";
|
import GlobalApi from '~/logic/api/global';
|
||||||
import { BucketList } from "./BucketList";
|
import { BucketList } from './BucketList';
|
||||||
import { S3State } from '~/types/s3-update';
|
import { S3State } from '~/types/s3-update';
|
||||||
|
import useS3State from '~/logic/state/storage';
|
||||||
import { BackButton } from './BackButton';
|
import { BackButton } from './BackButton';
|
||||||
import {StorageState} from '~/types';
|
import { StorageState } from '~/types';
|
||||||
|
import useStorageState from '~/logic/state/storage';
|
||||||
|
|
||||||
interface FormSchema {
|
interface FormSchema {
|
||||||
s3bucket: string;
|
s3bucket: string;
|
||||||
@ -27,33 +30,33 @@ interface FormSchema {
|
|||||||
|
|
||||||
interface S3FormProps {
|
interface S3FormProps {
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
storage: StorageState;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function S3Form(props: S3FormProps): ReactElement {
|
export default function S3Form(props: S3FormProps): ReactElement {
|
||||||
const { api, storage } = props;
|
const { api } = props;
|
||||||
const { s3 } = storage;
|
const s3 = useStorageState((state) => state.s3);
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(async (values: FormSchema, actions: FormikHelpers<FormSchema>) => {
|
||||||
(values: FormSchema) => {
|
|
||||||
if (values.s3secretAccessKey !== s3.credentials?.secretAccessKey) {
|
if (values.s3secretAccessKey !== s3.credentials?.secretAccessKey) {
|
||||||
api.s3.setSecretAccessKey(values.s3secretAccessKey);
|
await api.s3.setSecretAccessKey(values.s3secretAccessKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (values.s3endpoint !== s3.credentials?.endpoint) {
|
if (values.s3endpoint !== s3.credentials?.endpoint) {
|
||||||
api.s3.setEndpoint(values.s3endpoint);
|
await api.s3.setEndpoint(values.s3endpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (values.s3accessKeyId !== s3.credentials?.accessKeyId) {
|
if (values.s3accessKeyId !== s3.credentials?.accessKeyId) {
|
||||||
api.s3.setAccessKeyId(values.s3accessKeyId);
|
await api.s3.setAccessKeyId(values.s3accessKeyId);
|
||||||
}
|
}
|
||||||
|
actions.setStatus({ success: null });
|
||||||
},
|
},
|
||||||
[api, s3]
|
[api, s3]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Col p="5" pt="4" borderBottom="1" borderBottomColor="washedGray">
|
<BackButton />
|
||||||
|
<Col p='5' pt='4' borderBottom='1' borderBottomColor='washedGray'>
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={
|
initialValues={
|
||||||
{
|
{
|
||||||
@ -67,42 +70,42 @@ export default function S3Form(props: S3FormProps): ReactElement {
|
|||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
>
|
>
|
||||||
<Form>
|
<Form>
|
||||||
<BackButton/>
|
<Col maxWidth='600px' gapY='5'>
|
||||||
<Col maxWidth="600px" gapY="5">
|
<Col gapY='1' mt='0'>
|
||||||
<Col gapY="1" mt="0">
|
<Text color='black' fontSize={2} fontWeight='medium'>
|
||||||
<Text color="black" fontSize={2} fontWeight="medium">
|
|
||||||
S3 Storage Setup
|
S3 Storage Setup
|
||||||
</Text>
|
</Text>
|
||||||
<Text gray>
|
<Text gray>
|
||||||
Store credentials for your S3 object storage buckets on your
|
Store credentials for your S3 object storage buckets on your
|
||||||
Urbit ship, and upload media freely to various modules.
|
Urbit ship, and upload media freely to various modules.
|
||||||
<Anchor
|
<Anchor
|
||||||
target="_blank"
|
target='_blank'
|
||||||
style={{ textDecoration: 'none' }}
|
style={{ textDecoration: 'none' }}
|
||||||
borderBottom="1"
|
borderBottom='1'
|
||||||
ml="1"
|
ml='1'
|
||||||
href="https://urbit.org/using/operations/using-your-ship/#bucket-setup">
|
href='https://urbit.org/using/operations/using-your-ship/#bucket-setup'
|
||||||
|
>
|
||||||
Learn more
|
Learn more
|
||||||
</Anchor>
|
</Anchor>
|
||||||
</Text>
|
</Text>
|
||||||
</Col>
|
</Col>
|
||||||
<Input label="Endpoint" id="s3endpoint" />
|
<Input label='Endpoint' id='s3endpoint' />
|
||||||
<Input label="Access Key ID" id="s3accessKeyId" />
|
<Input label='Access Key ID' id='s3accessKeyId' />
|
||||||
<Input
|
<Input
|
||||||
type="password"
|
type='password'
|
||||||
label="Secret Access Key"
|
label='Secret Access Key'
|
||||||
id="s3secretAccessKey"
|
id='s3secretAccessKey'
|
||||||
/>
|
/>
|
||||||
<Button style={{ cursor: "pointer" }} type="submit">
|
<AsyncButton primary style={{ cursor: 'pointer' }} type='submit'>
|
||||||
Submit
|
Submit
|
||||||
</Button>
|
</AsyncButton>
|
||||||
</Col>
|
</Col>
|
||||||
</Form>
|
</Form>
|
||||||
</Formik>
|
</Formik>
|
||||||
</Col>
|
</Col>
|
||||||
<Col maxWidth="600px" p="5" gapY="4">
|
<Col maxWidth='600px' p='5' gapY='4'>
|
||||||
<Col gapY="1">
|
<Col gapY='1'>
|
||||||
<Text color="black" mb={4} fontSize={2} fontWeight="medium">
|
<Text color='black' mb={4} fontSize={2} fontWeight='medium'>
|
||||||
S3 Buckets
|
S3 Buckets
|
||||||
</Text>
|
</Text>
|
||||||
<Text gray>
|
<Text gray>
|
||||||
|
@ -7,7 +7,6 @@ import { StoreState } from "~/logic/store/type";
|
|||||||
import DisplayForm from "./lib/DisplayForm";
|
import DisplayForm from "./lib/DisplayForm";
|
||||||
import S3Form from "./lib/S3Form";
|
import S3Form from "./lib/S3Form";
|
||||||
import SecuritySettings from "./lib/Security";
|
import SecuritySettings from "./lib/Security";
|
||||||
import RemoteContentForm from "./lib/RemoteContent";
|
|
||||||
import { NotificationPreferences } from "./lib/NotificationPref";
|
import { NotificationPreferences } from "./lib/NotificationPref";
|
||||||
import { CalmPrefs } from "./lib/CalmPref";
|
import { CalmPrefs } from "./lib/CalmPref";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
@ -1,35 +1,44 @@
|
|||||||
import React, { ReactNode } from "react";
|
import React, { ReactNode, useEffect } from 'react';
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from 'react-router-dom';
|
||||||
import Helmet from "react-helmet";
|
import Helmet from 'react-helmet';
|
||||||
|
|
||||||
import { Text, Box, Col, Row } from '@tlon/indigo-react';
|
import { Text, Box, Col, Row } from '@tlon/indigo-react';
|
||||||
|
|
||||||
import { NotificationPreferences } from "./components/lib/NotificationPref";
|
import { NotificationPreferences } from './components/lib/NotificationPref';
|
||||||
import DisplayForm from "./components/lib/DisplayForm";
|
import DisplayForm from './components/lib/DisplayForm';
|
||||||
import S3Form from "./components/lib/S3Form";
|
import S3Form from './components/lib/S3Form';
|
||||||
import { CalmPrefs } from "./components/lib/CalmPref";
|
import { CalmPrefs } from './components/lib/CalmPref';
|
||||||
import SecuritySettings from "./components/lib/Security";
|
import SecuritySettings from './components/lib/Security';
|
||||||
import { LeapSettings } from "./components/lib/LeapSettings";
|
import { LeapSettings } from './components/lib/LeapSettings';
|
||||||
import { useHashLink } from "~/logic/lib/useHashLink";
|
import { useHashLink } from '~/logic/lib/useHashLink';
|
||||||
import { SidebarItem as BaseSidebarItem } from "~/views/landscape/components/SidebarItem";
|
import { SidebarItem as BaseSidebarItem } from '~/views/landscape/components/SidebarItem';
|
||||||
import { PropFunc } from "~/types";
|
import { PropFunc } from '~/types';
|
||||||
|
import DebugPane from './components/lib/Debug';
|
||||||
|
import useHarkState from '~/logic/state/hark';
|
||||||
|
|
||||||
export const Skeleton = (props: { children: ReactNode }) => (
|
export const Skeleton = (props: { children: ReactNode }) => (
|
||||||
<Box height="100%" width="100%" px={[0, 3]} pb={[0, 3]} borderRadius={1}>
|
<Box height='100%' width='100%' px={[0, 3]} pb={[0, 3]} borderRadius={1}>
|
||||||
<Box
|
<Box
|
||||||
height="100%"
|
display='grid'
|
||||||
width="100%"
|
gridTemplateColumns={[
|
||||||
borderRadius={1}
|
'100%',
|
||||||
bg="white"
|
'minmax(150px, 1fr) 3fr',
|
||||||
|
'minmax(250px, 1fr) 4fr'
|
||||||
|
]}
|
||||||
|
gridTemplateRows='100%'
|
||||||
|
height='100%'
|
||||||
|
width='100%'
|
||||||
|
borderRadius={2}
|
||||||
|
bg='white'
|
||||||
border={1}
|
border={1}
|
||||||
borderColor="washedGray"
|
borderColor='washedGray'
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
||||||
type ProvSideProps = "to" | "selected";
|
type ProvSideProps = 'to' | 'selected';
|
||||||
type BaseProps = PropFunc<typeof BaseSidebarItem>;
|
type BaseProps = PropFunc<typeof BaseSidebarItem>;
|
||||||
function SidebarItem(props: { hash: string } & Omit<BaseProps, ProvSideProps>) {
|
function SidebarItem(props: { hash: string } & Omit<BaseProps, ProvSideProps>) {
|
||||||
const { hash, icon, text, ...rest } = props;
|
const { hash, icon, text, ...rest } = props;
|
||||||
@ -54,85 +63,81 @@ function SettingsItem(props: { children: ReactNode }) {
|
|||||||
const { children } = props;
|
const { children } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box borderBottom="1" borderBottomColor="washedGray">
|
<Box borderBottom='1' borderBottomColor='washedGray'>
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SettingsScreen(props: any) {
|
export default function SettingsScreen(props: any) {
|
||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const hash = location.hash.slice(1)
|
const hash = location.hash.slice(1);
|
||||||
|
const notificationsCount = useHarkState(state => state.notificationsCount);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const debugShower = (event) => {
|
||||||
|
if (hash) return;
|
||||||
|
if (event.key === '~') {
|
||||||
|
window.location.hash = 'debug';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('keyup', debugShower);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keyup', debugShower);
|
||||||
|
}
|
||||||
|
}, [hash]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Helmet defer={false}>
|
<Helmet defer={false}>
|
||||||
<title>Landscape - Settings</title>
|
<title>{ notificationsCount ? `(${String(notificationsCount) }) `: '' }Landscape - Settings</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<Skeleton>
|
<Skeleton>
|
||||||
<Row height="100%" overflow="hidden">
|
<Col
|
||||||
<Col
|
height='100%'
|
||||||
height="100%"
|
borderRight='1'
|
||||||
borderRight="1"
|
borderRightColor='washedGray'
|
||||||
borderRightColor="washedGray"
|
display={hash === '' ? 'flex' : ['none', 'flex']}
|
||||||
display={hash === "" ? "flex" : ["none", "flex"]}
|
width='100%'
|
||||||
minWidth="250px"
|
overflowY='auto'
|
||||||
width="100%"
|
>
|
||||||
maxWidth={["100vw", "350px"]}
|
<Text display='block' mt='4' mb='3' mx='3' fontSize='2' fontWeight='700'>
|
||||||
>
|
System Preferences
|
||||||
<Text
|
</Text>
|
||||||
display="block"
|
<Col>
|
||||||
my="4"
|
<SidebarItem
|
||||||
mx="3"
|
icon='Inbox'
|
||||||
fontSize="2"
|
text='Notifications'
|
||||||
fontWeight="medium"
|
hash='notifications'
|
||||||
>
|
/>
|
||||||
System Preferences
|
<SidebarItem icon='Image' text='Display' hash='display' />
|
||||||
</Text>
|
<SidebarItem icon='Upload' text='Remote Storage' hash='s3' />
|
||||||
<Col gapY="1">
|
<SidebarItem icon='LeapArrow' text='Leap' hash='leap' />
|
||||||
<SidebarItem
|
<SidebarItem icon='Node' text='CalmEngine' hash='calm' />
|
||||||
icon="Inbox"
|
<SidebarItem
|
||||||
text="Notifications"
|
icon='Locked'
|
||||||
hash="notifications"
|
text='Devices + Security'
|
||||||
/>
|
hash='security'
|
||||||
<SidebarItem icon="Image" text="Display" hash="display" />
|
/>
|
||||||
<SidebarItem icon="Upload" text="Remote Storage" hash="s3" />
|
|
||||||
<SidebarItem icon="LeapArrow" text="Leap" hash="leap" />
|
|
||||||
<SidebarItem icon="Node" text="CalmEngine" hash="calm" />
|
|
||||||
<SidebarItem
|
|
||||||
icon="Locked"
|
|
||||||
text="Devices + Security"
|
|
||||||
hash="security"
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Col>
|
</Col>
|
||||||
<Col flexGrow={1} overflowY="auto">
|
</Col>
|
||||||
<SettingsItem>
|
<Col flexGrow={1} overflowY='auto'>
|
||||||
{hash === "notifications" && (
|
<SettingsItem>
|
||||||
<NotificationPreferences
|
{hash === 'notifications' && (
|
||||||
{...props}
|
<NotificationPreferences
|
||||||
graphConfig={props.notificationsGraphConfig}
|
{...props}
|
||||||
/>
|
graphConfig={props.notificationsGraphConfig}
|
||||||
)}
|
/>
|
||||||
{hash === "display" && (
|
)}
|
||||||
<DisplayForm storage={props.storage} api={props.api} />
|
{hash === 'display' && <DisplayForm api={props.api} />}
|
||||||
)}
|
{hash === 's3' && <S3Form api={props.api} />}
|
||||||
{hash === "s3" && (
|
{hash === 'leap' && <LeapSettings api={props.api} />}
|
||||||
<S3Form storage={props.storage} api={props.api} />
|
{hash === 'calm' && <CalmPrefs api={props.api} />}
|
||||||
)}
|
{hash === 'security' && <SecuritySettings api={props.api} />}
|
||||||
{hash === "leap" && (
|
{hash === 'debug' && <DebugPane />}
|
||||||
<LeapSettings api={props.api} />
|
</SettingsItem>
|
||||||
)}
|
</Col>
|
||||||
{hash === "calm" && (
|
|
||||||
<CalmPrefs api={props.api} />
|
|
||||||
)}
|
|
||||||
{hash === "security" && (
|
|
||||||
<SecuritySettings api={props.api} />
|
|
||||||
)}
|
|
||||||
</SettingsItem>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user