mirror of
https://github.com/urbit/shrub.git
synced 2024-12-19 08:32:39 +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
|
||||
|
||||
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:
|
||||
urbit:
|
||||
|
@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a8b19cbe89f770f8d6c1e05972be7a3a01545b93b0f2d4523809e7df18635f3c
|
||||
size 9462938
|
||||
oid sha256:59285407abdc63642ff71384d922f63f4b2c82b3a0daa3673a861c97c59e292f
|
||||
size 9729397
|
||||
|
@ -45,17 +45,16 @@ Most parts of Arvo have dedicated maintainers.
|
||||
* `/sys/vane/ames`: @belisarius222 (~rovnys-ricfer) & @philipcmonk (~wicdev-wisryt)
|
||||
* `/sys/vane/behn`: @belisarius222 (~rovnys-ricfer)
|
||||
* `/sys/vane/clay`: @philipcmonk (~wicdev-wisryt) & @belisarius222 (~rovnys-ricfer)
|
||||
* `/sys/vane/dill`: @joemfb (~master-morzod)
|
||||
* `/sys/vane/eyre`: @eglaysher (~littel-ponnys)
|
||||
* `/sys/vane/dill`: @fang- (~palfun-foslup)
|
||||
* `/sys/vane/eyre`: @fang- (~palfun-foslup)
|
||||
* `/sys/vane/gall`: @philipcmonk (~wicdev-wisryt)
|
||||
* `/sys/vane/jael`: @fang- (~palfun-foslup) & @philipcmonk (~wicdev-wisryt)
|
||||
* `/app/acme`: @joemfb (~master-morzod)
|
||||
* `/app/dns`: @joemfb (~master-morzod)
|
||||
* `/app/aqua`: @philipcmonk (~wicdev-wisryt)
|
||||
* `/app/hood`: @belisarius222 (~rovnys-ricfer)
|
||||
* `/lib/hood/drum`: @philipcmonk (~wicdev-wisryt)
|
||||
* `/lib/hood/drum`: @fang- (~palfun-foslup)
|
||||
* `/lib/hood/kiln`: @philipcmonk (~wicdev-wisryt)
|
||||
* `/lib/test`: @eglaysher (~littel-ponnys)
|
||||
|
||||
## Contributing
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
/- glob
|
||||
/+ 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))]
|
||||
+$ all-states
|
||||
$% state-0
|
||||
|
@ -293,7 +293,7 @@
|
||||
~(tap by unreads-count)
|
||||
|= [=stats-index:store count=@ud]
|
||||
:* stats-index
|
||||
~(wyt in (~(gut by by-index) stats-index ~))
|
||||
(~(gut by by-index) stats-index ~)
|
||||
[%count count]
|
||||
(~(gut by last-seen) stats-index *time)
|
||||
==
|
||||
@ -304,7 +304,7 @@
|
||||
~(tap by unreads-each)
|
||||
|= [=stats-index:store indices=(set index:graph-store)]
|
||||
:* stats-index
|
||||
~(wyt in (~(gut by by-index) stats-index ~))
|
||||
(~(gut by by-index) stats-index ~)
|
||||
[%each indices]
|
||||
(~(gut by last-seen) stats-index *time)
|
||||
==
|
||||
@ -317,7 +317,7 @@
|
||||
~
|
||||
:- ~
|
||||
:* stats-index
|
||||
~(wyt in nots)
|
||||
nots
|
||||
[%count 0]
|
||||
*time
|
||||
==
|
||||
|
@ -24,6 +24,6 @@
|
||||
<div id="portal-root"></div>
|
||||
<script src="/~landscape/js/channel.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>
|
||||
</html>
|
||||
|
@ -151,7 +151,7 @@
|
||||
^- json
|
||||
%- pairs
|
||||
:~ unreads+(unread unreads.s)
|
||||
notifications+(numb notifications.s)
|
||||
notifications+a+(turn ~(tap in notifications.s) notif-ref)
|
||||
last+(time last-seen.s)
|
||||
==
|
||||
++ added
|
||||
|
@ -150,7 +150,7 @@
|
||||
[index notification]
|
||||
::
|
||||
+$ stats
|
||||
[notifications=@ud =unreads last-seen=@da]
|
||||
[notifications=(set [time index]) =unreads last-seen=@da]
|
||||
::
|
||||
+$ unreads
|
||||
$% [%count num=@ud]
|
||||
|
@ -94,7 +94,11 @@ module.exports = {
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
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: [
|
||||
'@babel/transform-runtime',
|
||||
'@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": {
|
||||
"@babel/runtime": {
|
||||
"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": {
|
||||
"regenerator-runtime": "^0.13.4"
|
||||
}
|
||||
},
|
||||
"@types/lodash": {
|
||||
"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": {
|
||||
"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": {
|
||||
"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": {
|
||||
"version": "4.17.20",
|
||||
"bundled": true
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
|
||||
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
|
||||
},
|
||||
"regenerator-runtime": {
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"@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": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
|
||||
@ -2071,6 +2086,11 @@
|
||||
"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": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
|
||||
@ -3974,6 +3994,14 @@
|
||||
"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": {
|
||||
"version": "1.18.0-next.2",
|
||||
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.2.tgz",
|
||||
@ -8743,6 +8771,45 @@
|
||||
"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": {
|
||||
"version": "1.0.3",
|
||||
"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",
|
||||
"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"is-extendable": "^0.1.0"
|
||||
}
|
||||
@ -9921,6 +9989,7 @@
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
|
||||
"integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"kind-of": "^3.0.2"
|
||||
}
|
||||
|
@ -13,6 +13,7 @@
|
||||
"@tlon/indigo-react": "^1.2.19",
|
||||
"@tlon/sigil-js": "^1.4.3",
|
||||
"@urbit/api": "file:../npm/api",
|
||||
"any-ascii": "^0.1.7",
|
||||
"aws-sdk": "^2.830.0",
|
||||
"big-integer": "^1.6.48",
|
||||
"classnames": "^2.2.6",
|
||||
@ -41,6 +42,7 @@
|
||||
"react-visibility-sensor": "^5.1.1",
|
||||
"remark-breaks": "^2.0.1",
|
||||
"remark-disable-tokenizers": "^1.0.24",
|
||||
"stacktrace-js": "^2.0.2",
|
||||
"style-loader": "^1.3.0",
|
||||
"styled-components": "^5.1.1",
|
||||
"styled-system": "^5.1.5",
|
||||
@ -71,6 +73,7 @@
|
||||
"@types/yup": "^0.29.11",
|
||||
"@typescript-eslint/eslint-plugin": "^4.15.0",
|
||||
"@urbit/eslint-config": "file:../npm/eslint-config",
|
||||
"@welldone-software/why-did-you-render": "^6.1.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-loader": "^8.2.2",
|
||||
"babel-plugin-lodash": "^3.3.4",
|
||||
|
@ -1,3 +1,4 @@
|
||||
import './wdyr';
|
||||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
|
||||
|
@ -4,7 +4,7 @@ import { StoreState } from '../store/type';
|
||||
export default class LocalApi extends BaseApi<StoreState> {
|
||||
getBaseHash() {
|
||||
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();
|
||||
},
|
||||
(ev: any) => {
|
||||
console.log(ev);
|
||||
if ('metadata-hook-update' in ev) {
|
||||
done = true;
|
||||
tempChannel.delete();
|
||||
|
@ -14,16 +14,14 @@
|
||||
//
|
||||
//
|
||||
import GlobalApi from '../api/global';
|
||||
import GlobalStore from '../store/store';
|
||||
import useStorageState from '../state/storage';
|
||||
|
||||
|
||||
class GcpManager {
|
||||
#api: GlobalApi | null = null;
|
||||
#store: GlobalStore | null = null;
|
||||
|
||||
configure(api: GlobalApi, store: GlobalStore) {
|
||||
configure(api: GlobalApi) {
|
||||
this.#api = api;
|
||||
this.#store = store;
|
||||
}
|
||||
|
||||
#running = false;
|
||||
@ -34,8 +32,8 @@ class GcpManager {
|
||||
console.warn('GcpManager already running');
|
||||
return;
|
||||
}
|
||||
if (!this.#api || !this.#store) {
|
||||
console.error('GcpManager must have api and store set');
|
||||
if (!this.#api) {
|
||||
console.error('GcpManager must have api set');
|
||||
return;
|
||||
}
|
||||
this.#running = true;
|
||||
@ -65,7 +63,7 @@ class GcpManager {
|
||||
#consecutiveFailures: number = 0;
|
||||
|
||||
private isConfigured() {
|
||||
return this.#store.state.storage.gcp.configured;
|
||||
return useStorageState.getState().gcp.configured;
|
||||
}
|
||||
|
||||
private refreshLoop() {
|
||||
@ -78,7 +76,8 @@ class GcpManager {
|
||||
if (this.isConfigured()) {
|
||||
this.refreshLoop();
|
||||
} else {
|
||||
this.refreshAfter(10_000);
|
||||
console.log('GcpManager: GCP storage not configured; stopping.');
|
||||
this.stop();
|
||||
}
|
||||
})
|
||||
.catch((reason) => {
|
||||
@ -89,7 +88,7 @@ class GcpManager {
|
||||
}
|
||||
this.#api.gcp.getToken()
|
||||
.then(() => {
|
||||
const token = this.#store.state.storage.gcp?.token;
|
||||
const token = useStorageState.getState().gcp.token;
|
||||
if (token) {
|
||||
this.#consecutiveFailures = 0;
|
||||
const interval = this.refreshInterval(token.expiresIn);
|
||||
|
@ -18,10 +18,6 @@ export function useMigrateSettings(api: GlobalApi) {
|
||||
const { display, remoteContentPolicy, calm } = useSettingsState();
|
||||
|
||||
return async () => {
|
||||
if (!localStorage?.has("localReducer")) {
|
||||
return;
|
||||
}
|
||||
|
||||
let promises: Promise<any>[] = [];
|
||||
|
||||
if (local.hideAvatars !== calm.hideAvatars) {
|
||||
|
@ -107,7 +107,9 @@ export function getComments(node: GraphNode): GraphNode {
|
||||
}
|
||||
|
||||
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}...`;
|
||||
}
|
||||
|
||||
|
@ -23,9 +23,10 @@ export const Sigil = memo(
|
||||
size,
|
||||
svgClass = '',
|
||||
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 foregroundColor = foreground
|
||||
? foreground
|
||||
@ -34,14 +35,14 @@ export const Sigil = memo(
|
||||
<Box
|
||||
backgroundColor={color}
|
||||
borderRadius={icon ? '1' : '0'}
|
||||
display='inline-block'
|
||||
display={display}
|
||||
height={size}
|
||||
width={size}
|
||||
className={classes}
|
||||
/>
|
||||
) : (
|
||||
<Box
|
||||
display='inline-block'
|
||||
display={display}
|
||||
borderRadius={icon ? '1' : '0'}
|
||||
flexBasis={size}
|
||||
backgroundColor={color}
|
||||
|
@ -8,6 +8,7 @@ import S3 from 'aws-sdk/clients/s3';
|
||||
import GcpClient from './GcpClient';
|
||||
import { StorageClient, StorageAcl } from './StorageClient';
|
||||
import { dateToDa, deSig } from './util';
|
||||
import useStorageState from '../state/storage';
|
||||
|
||||
|
||||
export interface IuseStorage {
|
||||
@ -18,9 +19,10 @@ export interface IuseStorage {
|
||||
promptUpload: () => Promise<string | undefined>;
|
||||
}
|
||||
|
||||
const useStorage = ({gcp, s3}: StorageState,
|
||||
{ accept = '*' } = { accept: '*' }): IuseStorage => {
|
||||
const useStorage = ({ accept = '*' } = { accept: '*' }): IuseStorage => {
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const gcp = useStorageState(state => state.gcp);
|
||||
const s3 = useStorageState(state => state.s3);
|
||||
|
||||
const client = useRef<StorageClient | null>(null);
|
||||
|
||||
|
@ -1,9 +1,17 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import _ from "lodash";
|
||||
import f, { memoize } from "lodash/fp";
|
||||
import bigInt, { BigInteger } from "big-integer";
|
||||
import { Contact } from '~/types';
|
||||
import _ from 'lodash';
|
||||
import f, { compose, memoize } from 'lodash/fp';
|
||||
import bigInt, { BigInteger } from 'big-integer';
|
||||
import { Association, Contact } from '@urbit/api';
|
||||
import useLocalState from '../state/local';
|
||||
import produce, { enableMapSet } from 'immer';
|
||||
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;
|
||||
|
||||
@ -17,7 +25,7 @@ export const MOMENT_CALENDAR_DATE = {
|
||||
};
|
||||
|
||||
export const getModuleIcon = (mod: string) => {
|
||||
if (mod === 'link') {
|
||||
if (mod === 'link') {
|
||||
return 'Collection';
|
||||
}
|
||||
return _.capitalize(mod);
|
||||
@ -50,7 +58,7 @@ export function daToUnix(da: BigInteger) {
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@ -300,7 +308,7 @@ export function stringToTa(str: string) {
|
||||
|
||||
export function amOwnerOfGroup(groupPath: string) {
|
||||
if (!groupPath)
|
||||
return false;
|
||||
return false;
|
||||
const groupOwner = /(\/~)?\/~([a-z-]{3,})\/.*/.exec(groupPath)?.[2];
|
||||
return window.ship === groupOwner;
|
||||
}
|
||||
@ -319,11 +327,12 @@ export function getContactDetails(contact: any) {
|
||||
}
|
||||
|
||||
export function stringToSymbol(str: string) {
|
||||
const ascii = anyAscii(str);
|
||||
let result = '';
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const n = str.charCodeAt(i);
|
||||
for (let i = 0; i < ascii.length; i++) {
|
||||
const n = ascii.charCodeAt(i);
|
||||
if ((n >= 97 && n <= 122) || (n >= 48 && n <= 57)) {
|
||||
result += str[i];
|
||||
result += ascii[i];
|
||||
} else if (n >= 65 && n <= 90) {
|
||||
result += String.fromCharCode(n + 32);
|
||||
} else {
|
||||
@ -337,7 +346,6 @@ export function stringToSymbol(str: string) {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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?') {
|
||||
useEffect(() => {
|
||||
if (!shouldPreventDefault)
|
||||
return;
|
||||
return;
|
||||
const handleBeforeUnload = (event) => {
|
||||
event.preventDefault();
|
||||
return message;
|
||||
@ -371,12 +379,13 @@ return;
|
||||
}
|
||||
|
||||
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
|
||||
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);
|
||||
}
|
||||
|
||||
@ -399,12 +408,13 @@ export const useHovering = (): useHoveringInterface => {
|
||||
|
||||
const DM_REGEX = /ship\/~([a-z]|-)*\/dm--/;
|
||||
export function getItemTitle(association: Association) {
|
||||
if(DM_REGEX.test(association.resource)) {
|
||||
const [,,ship,name] = association.resource.split('/');
|
||||
if(ship.slice(1) === window.ship) {
|
||||
if (DM_REGEX.test(association.resource)) {
|
||||
const [, , ship, name] = association.resource.split('/');
|
||||
if (ship.slice(1) === window.ship) {
|
||||
return cite(`~${name.slice(4)}`);
|
||||
}
|
||||
return cite(ship);
|
||||
}
|
||||
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 { StoreState } from '../../store/type';
|
||||
import { Cage } from '~/types/cage';
|
||||
import { ContactUpdate } from '@urbit/api/contacts';
|
||||
import { resourceAsPath } from '../lib/util';
|
||||
import { compose } from 'lodash/fp';
|
||||
|
||||
type ContactState = Pick<StoreState, 'contacts'>;
|
||||
import { ContactUpdate } from '@urbit/api';
|
||||
|
||||
export const ContactReducer = (json, state) => {
|
||||
const data = _.get(json, 'contact-update', false);
|
||||
import useContactState, { ContactState } from '../state/contact';
|
||||
import { reduceState } from '../state/base';
|
||||
|
||||
|
||||
export const ContactReducer = (json) => {
|
||||
const data: ContactUpdate = _.get(json, 'contact-update', false);
|
||||
if (data) {
|
||||
initial(data, state);
|
||||
add(data, state);
|
||||
remove(data, state);
|
||||
edit(data, state);
|
||||
setPublic(data, state);
|
||||
reduceState<ContactState, ContactUpdate>(useContactState, data, [
|
||||
initial,
|
||||
add,
|
||||
remove,
|
||||
edit,
|
||||
setPublic
|
||||
]);
|
||||
}
|
||||
|
||||
// TODO: better isolation
|
||||
const res = _.get(json, 'resource', false);
|
||||
if(res) {
|
||||
state.nackedContacts = state.nackedContacts.add(`~${res.ship}`);
|
||||
if (res) {
|
||||
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);
|
||||
if (data) {
|
||||
state.contacts = data.rolodex;
|
||||
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);
|
||||
if (data) {
|
||||
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);
|
||||
if (
|
||||
data &&
|
||||
@ -46,9 +53,10 @@ const remove = (json: ContactUpdate, state: S) => {
|
||||
) {
|
||||
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 ship = `~${data.ship}`;
|
||||
if (
|
||||
@ -57,7 +65,7 @@ const edit = (json: ContactUpdate, state: S) => {
|
||||
) {
|
||||
const [field] = Object.keys(data['edit-field']);
|
||||
if (!field) {
|
||||
return;
|
||||
return state;
|
||||
}
|
||||
|
||||
const value = data['edit-field'][field];
|
||||
@ -71,10 +79,12 @@ const edit = (json: ContactUpdate, state: S) => {
|
||||
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);
|
||||
state.isContactPublic = data;
|
||||
return state;
|
||||
};
|
||||
|
||||
|
@ -1,37 +1,43 @@
|
||||
import _ from 'lodash';
|
||||
import {StoreState} from '../store/type';
|
||||
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<S extends GcpState>{
|
||||
reduce(json: Cage, state: S) {
|
||||
this.reduceConfigured(json, state);
|
||||
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');
|
||||
export default class GcpReducer {
|
||||
reduce(json: Cage) {
|
||||
reduceState<StorageState, any>(useStorageState, json, [
|
||||
reduceConfigured,
|
||||
reduceToken
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
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 { BigIntOrderedMap } from "~/logic/lib/BigIntOrderedMap";
|
||||
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);
|
||||
|
||||
if (data) {
|
||||
keys(data, state);
|
||||
addGraph(data, state);
|
||||
removeGraph(data, state);
|
||||
addNodes(data, state);
|
||||
removeNodes(data, state);
|
||||
reduceState<GraphState, any>(useGraphState, data, [
|
||||
keys,
|
||||
addGraph,
|
||||
removeGraph,
|
||||
addNodes,
|
||||
removeNodes
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
const keys = (json, state) => {
|
||||
const keys = (json, state: GraphState): GraphState => {
|
||||
const data = _.get(json, 'keys', false);
|
||||
if (data) {
|
||||
state.graphKeys = new Set(data.map((res) => {
|
||||
@ -22,9 +26,10 @@ const keys = (json, state) => {
|
||||
return resource;
|
||||
}));
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
const addGraph = (json, state) => {
|
||||
const addGraph = (json, state: GraphState): GraphState => {
|
||||
|
||||
const _processNode = (node) => {
|
||||
// is empty
|
||||
@ -72,10 +77,10 @@ const addGraph = (json, state) => {
|
||||
}
|
||||
state.graphKeys.add(resource);
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
const removeGraph = (json, state) => {
|
||||
const removeGraph = (json, state: GraphState): GraphState => {
|
||||
const data = _.get(json, 'remove-graph', false);
|
||||
if (data) {
|
||||
|
||||
@ -86,6 +91,7 @@ const removeGraph = (json, state) => {
|
||||
state.graphKeys.delete(resource);
|
||||
delete state.graphs[resource];
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
const mapifyChildren = (children) => {
|
||||
@ -98,7 +104,7 @@ const mapifyChildren = (children) => {
|
||||
};
|
||||
|
||||
const addNodes = (json, state) => {
|
||||
const _addNode = (graph, index, node, resource) => {
|
||||
const _addNode = (graph, index, node) => {
|
||||
// set child of graph
|
||||
if (index.length === 1) {
|
||||
graph.set(index[0], node);
|
||||
@ -160,7 +166,7 @@ const addNodes = (json, state) => {
|
||||
|
||||
const data = _.get(json, 'add-nodes', false);
|
||||
if (data) {
|
||||
if (!('graphs' in state)) { return; }
|
||||
if (!('graphs' in state)) { return state; }
|
||||
|
||||
let resource = data.resource.ship + '/' + data.resource.name;
|
||||
if (!(resource in state.graphs)) {
|
||||
@ -192,7 +198,7 @@ const addNodes = (json, state) => {
|
||||
return bigInt(ind);
|
||||
});
|
||||
|
||||
if (indexArr.length === 0) { return; }
|
||||
if (indexArr.length === 0) { return state; }
|
||||
|
||||
if (node.post.pending) {
|
||||
state.graphTimesentMap[resource][node.post['time-sent']] = index;
|
||||
@ -210,10 +216,10 @@ const addNodes = (json, state) => {
|
||||
|
||||
state.graphs[resource] = graph;
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
|
||||
const removeNodes = (json, state) => {
|
||||
const removeNodes = (json, state: GraphState): GraphState => {
|
||||
const _remove = (graph, index) => {
|
||||
if (index.length === 1) {
|
||||
graph.delete(index[0]);
|
||||
@ -230,7 +236,7 @@ const removeNodes = (json, state) => {
|
||||
if (data) {
|
||||
const { ship, name } = data.resource;
|
||||
const res = `${ship}/${name}`;
|
||||
if (!(res in state.graphs)) { return; }
|
||||
if (!(res in state.graphs)) { return state; }
|
||||
|
||||
data.indices.forEach((index) => {
|
||||
if (index.split('/').length === 0) { return; }
|
||||
@ -240,4 +246,5 @@ const removeNodes = (json, state) => {
|
||||
_remove(state.graphs[res], indexArr);
|
||||
});
|
||||
}
|
||||
return state;
|
||||
};
|
@ -1,5 +1,4 @@
|
||||
import _ from 'lodash';
|
||||
import { StoreState } from '../../store/type';
|
||||
import { Cage } from '~/types/cage';
|
||||
import {
|
||||
GroupUpdate,
|
||||
@ -14,8 +13,9 @@ import {
|
||||
} from '@urbit/api/groups';
|
||||
import { Enc, PatpNoSig } from '@urbit/api';
|
||||
import { resourceAsPath } from '../lib/util';
|
||||
|
||||
type GroupState = Pick<StoreState, 'groups' | 'groupKeys'>;
|
||||
import useGroupState, { GroupState } from '../state/group';
|
||||
import { compose } from 'lodash/fp';
|
||||
import { reduceState } from '../state/base';
|
||||
|
||||
function decodeGroup(group: Enc<Group>): Group {
|
||||
const members = new Set(group.members);
|
||||
@ -57,186 +57,195 @@ function decodeTags(tags: Enc<Tags>): Tags {
|
||||
);
|
||||
}
|
||||
|
||||
export default class GroupReducer<S extends GroupState> {
|
||||
reduce(json: Cage, state: S) {
|
||||
export default class GroupReducer {
|
||||
reduce(json: Cage) {
|
||||
const data = json.groupUpdate;
|
||||
if (data) {
|
||||
console.log(data);
|
||||
this.initial(data, state);
|
||||
this.addMembers(data, state);
|
||||
this.addTag(data, state);
|
||||
this.removeMembers(data, state);
|
||||
this.initialGroup(data, state);
|
||||
this.removeTag(data, state);
|
||||
this.initial(data, state);
|
||||
this.addGroup(data, state);
|
||||
this.removeGroup(data, state);
|
||||
this.changePolicy(data, state);
|
||||
this.expose(data, state);
|
||||
reduceState<GroupState, GroupUpdate>(useGroupState, data, [
|
||||
initial,
|
||||
addMembers,
|
||||
addTag,
|
||||
removeMembers,
|
||||
initialGroup,
|
||||
removeTag,
|
||||
addGroup,
|
||||
removeGroup,
|
||||
changePolicy,
|
||||
expose,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
initial(json: GroupUpdate, state: S) {
|
||||
const data = json['initial'];
|
||||
if (data) {
|
||||
state.groups = _.mapValues(data, decodeGroup);
|
||||
}
|
||||
const initial = (json: GroupUpdate, state: GroupState): GroupState => {
|
||||
const data = json['initial'];
|
||||
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) {
|
||||
if ('initialGroup' in json) {
|
||||
const { resource, group } = json.initialGroup;
|
||||
const path = resourceAsPath(resource);
|
||||
state.groups[path] = decodeGroup(group);
|
||||
const removeMembers = (json: GroupUpdate, state: GroupState): GroupState => {
|
||||
if ('removeMembers' in json) {
|
||||
const { resource, ships } = json.removeMembers;
|
||||
const resourcePath = resourceAsPath(resource);
|
||||
for (const member of ships) {
|
||||
state.groups[resourcePath].members.delete(member);
|
||||
}
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
addGroup(json: GroupUpdate, state: S) {
|
||||
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
|
||||
};
|
||||
const addTag = (json: GroupUpdate, state: GroupState): GroupState => {
|
||||
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);
|
||||
}
|
||||
removeGroup(json: GroupUpdate, state: S) {
|
||||
if('removeGroup' in json) {
|
||||
const { resource } = json.removeGroup;
|
||||
const resourcePath = resourceAsPath(resource);
|
||||
delete state.groups[resourcePath];
|
||||
return state;
|
||||
}
|
||||
|
||||
const removeTag = (json: GroupUpdate, state: GroupState): GroupState => {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
for (const ship of ships) {
|
||||
tagged.delete(ship);
|
||||
}
|
||||
_.set(tags, tagAccessors, tagged);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
removeMembers(json: GroupUpdate, state: S) {
|
||||
if ('removeMembers' in json) {
|
||||
const { resource, ships } = json.removeMembers;
|
||||
const resourcePath = resourceAsPath(resource);
|
||||
for (const member of ships) {
|
||||
state.groups[resourcePath].members.delete(member);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addTag(json: GroupUpdate, state: S) {
|
||||
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);
|
||||
}
|
||||
const changePolicy = (json: GroupUpdate, state: GroupState): GroupState => {
|
||||
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) {
|
||||
openChangePolicy(diff.open, policy);
|
||||
} else if ('invite' in policy && 'invite' in diff) {
|
||||
inviteChangePolicy(diff.invite, policy);
|
||||
} else if ('replace' in diff) {
|
||||
state.groups[resourcePath].policy = diff.replace;
|
||||
} else {
|
||||
console.log('bad policy change');
|
||||
console.log('bad policy diff');
|
||||
}
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
private 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) {
|
||||
console.log('allowing ships');
|
||||
const { allowShips } = diff;
|
||||
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');
|
||||
const expose = (json: GroupUpdate, state: GroupState): GroupState => {
|
||||
if( 'expose' in json && state) {
|
||||
const { resource } = json.expose;
|
||||
const resourcePath = resourceAsPath(resource);
|
||||
state.groups[resourcePath].hidden = false;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
const 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 {
|
||||
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 { 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;
|
||||
if(data) {
|
||||
state.pendingJoin = data;
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
const progress = (json: any, state: any) => {
|
||||
const progress = (json: any, state: GroupState): GroupState => {
|
||||
const data = json.progress;
|
||||
if(data) {
|
||||
const { progress, resource } = data;
|
||||
@ -18,12 +22,15 @@ const progress = (json: any, state: any) => {
|
||||
}, 10000);
|
||||
}
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
export const GroupViewReducer = (json: any, state: any) => {
|
||||
export const GroupViewReducer = (json: any) => {
|
||||
const data = json['group-view-update'];
|
||||
if(data) {
|
||||
progress(data, state);
|
||||
initial(data, state);
|
||||
if (data) {
|
||||
reduceState<GroupState, GroupUpdate>(useGroupState, data, [
|
||||
progress,
|
||||
initial
|
||||
]);
|
||||
}
|
||||
};
|
||||
};
|
@ -3,51 +3,94 @@ import {
|
||||
NotifIndex,
|
||||
NotificationGraphConfig,
|
||||
GroupNotificationsConfig,
|
||||
UnreadStats
|
||||
UnreadStats,
|
||||
Timebox
|
||||
} from '@urbit/api';
|
||||
import { makePatDa } from '~/logic/lib/util';
|
||||
import _ from 'lodash';
|
||||
import { StoreState } from '../store/type';
|
||||
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, state: HarkState) => {
|
||||
export const HarkReducer = (json: any) => {
|
||||
const data = _.get(json, 'harkUpdate', false);
|
||||
if (data) {
|
||||
reduce(data, state);
|
||||
reduce(data);
|
||||
}
|
||||
const graphHookData = _.get(json, 'hark-graph-hook-update', false);
|
||||
if (graphHookData) {
|
||||
graphInitial(graphHookData, state);
|
||||
graphIgnore(graphHookData, state);
|
||||
graphListen(graphHookData, state);
|
||||
graphWatchSelf(graphHookData, state);
|
||||
graphMentions(graphHookData, state);
|
||||
reduceState<HarkState, any>(useHarkState, graphHookData, [
|
||||
graphInitial,
|
||||
graphIgnore,
|
||||
graphListen,
|
||||
graphWatchSelf,
|
||||
graphMentions,
|
||||
]);
|
||||
}
|
||||
const groupHookData = _.get(json, 'hark-group-hook-update', false);
|
||||
if (groupHookData) {
|
||||
groupInitial(groupHookData, state);
|
||||
groupListen(groupHookData, state);
|
||||
groupIgnore(groupHookData, state);
|
||||
reduceState<HarkState, any>(useHarkState, groupHookData, [
|
||||
groupInitial,
|
||||
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);
|
||||
if (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);
|
||||
if (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);
|
||||
if (data) {
|
||||
state.notificationsGraphConfig.watching = [
|
||||
@ -55,134 +98,133 @@ function graphListen(json: any, state: HarkState) {
|
||||
data
|
||||
];
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function graphIgnore(json: any, state: HarkState) {
|
||||
function graphIgnore(json: any, state: HarkState): HarkState {
|
||||
const data = _.get(json, 'ignore', false);
|
||||
if (data) {
|
||||
state.notificationsGraphConfig.watching = state.notificationsGraphConfig.watching.filter(
|
||||
({ 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);
|
||||
if (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);
|
||||
if (data) {
|
||||
state.notificationsGroupConfig = state.notificationsGroupConfig.filter(
|
||||
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);
|
||||
if (!_.isUndefined(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);
|
||||
if (!_.isUndefined(data)) {
|
||||
state.notificationsGraphConfig.watchOnSelf = data;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function reduce(data: any, state: 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) {
|
||||
function readAll(json: any, state: HarkState): HarkState {
|
||||
const data = _.get(json, 'read-all');
|
||||
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');
|
||||
if(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');
|
||||
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');
|
||||
if(data) {
|
||||
updateUnreads(state, data.index, u => u.delete(data.target));
|
||||
if (data) {
|
||||
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');
|
||||
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');
|
||||
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');
|
||||
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');
|
||||
if(data) {
|
||||
clearState(state);
|
||||
state = clearState(state);
|
||||
data.forEach(({ index, stats }) => {
|
||||
const { unreads, notifications, last } = stats;
|
||||
updateNotificationStats(state, index, 'notifications', x => x + notifications);
|
||||
updateNotificationStats(state, index, 'last', () => last);
|
||||
_.each(notifications, ({ time, index }) => {
|
||||
addNotificationToUnread(state, index, makePatDa(time));
|
||||
});
|
||||
if('count' in unreads) {
|
||||
updateUnreadCount(state, index, (u = 0) => u + unreads.count);
|
||||
state = updateUnreadCount(state, index, (u = 0) => u + unreads.count);
|
||||
} else {
|
||||
state = updateUnreads(state, index, s => new Set());
|
||||
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 = {
|
||||
notifications: new BigIntOrderedMap<Timebox>(),
|
||||
archivedNotifications: new BigIntOrderedMap<Timebox>(),
|
||||
@ -202,73 +244,110 @@ function clearState(state) {
|
||||
Object.keys(initialState).forEach((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)) {
|
||||
return;
|
||||
return state;
|
||||
}
|
||||
const property = [index.graph.graph, index.graph.index, 'unreads'];
|
||||
const curr = _.get(state.unreads.graph, property, 0);
|
||||
const newCount = count(curr);
|
||||
_.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)) {
|
||||
return;
|
||||
return state;
|
||||
}
|
||||
const unreads = _.get(state.unreads.graph, [index.graph.graph, index.graph.index, 'unreads'], new Set<string>());
|
||||
const oldSize = unreads.size;
|
||||
let unreads = _.get(state.unreads.graph, [index.graph.graph, index.graph.index, 'unreads'], new Set<string>());
|
||||
f(unreads);
|
||||
const newSize = unreads.size;
|
||||
|
||||
_.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) {
|
||||
if(statField === 'notifications') {
|
||||
state.notificationsCount = f(state.notificationsCount);
|
||||
}
|
||||
function addNotificationToUnread(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))),
|
||||
{ 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) {
|
||||
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));
|
||||
} else if('group' in index) {
|
||||
const curr = _.get(state.unreads.group, [index.group, statField], 0);
|
||||
_.set(state.unreads.group, [index.group, statField], f(curr));
|
||||
const curr = _.get(state.unreads.group, [index.group.group, statField], 0);
|
||||
_.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);
|
||||
if (data) {
|
||||
const { index, notification } = data;
|
||||
const time = makePatDa(data.time);
|
||||
const timebox = state.notifications.get(time) || [];
|
||||
addNotificationToUnread(state, index, time);
|
||||
|
||||
const arrIdx = timebox.findIndex(idxNotif =>
|
||||
notifIdxEqual(index, idxNotif.index)
|
||||
);
|
||||
if (arrIdx !== -1) {
|
||||
if(timebox[arrIdx]?.notification?.read) {
|
||||
updateNotificationStats(state, index, 'notifications', x => x+1);
|
||||
}
|
||||
timebox[arrIdx] = { index, notification };
|
||||
state.notifications.set(time, timebox);
|
||||
} else {
|
||||
updateNotificationStats(state, index, 'notifications', x => x+1);
|
||||
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);
|
||||
if (!_.isUndefined(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);
|
||||
if (data) {
|
||||
const time = makePatDa(data.time);
|
||||
@ -276,13 +355,15 @@ const timebox = (json: any, state: HarkState) => {
|
||||
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);
|
||||
if (data) {
|
||||
_.forEach(data, d => reduce(d, state));
|
||||
_.forEach(data, d => reduce(d));
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function notifIdxEqual(a: NotifIndex, b: NotifIndex) {
|
||||
@ -307,51 +388,55 @@ function setRead(
|
||||
index: NotifIndex,
|
||||
read: boolean,
|
||||
state: HarkState
|
||||
) {
|
||||
): HarkState {
|
||||
const patDa = makePatDa(time);
|
||||
const timebox = state.notifications.get(patDa);
|
||||
if (_.isNull(timebox)) {
|
||||
console.warn('Modifying nonexistent timebox');
|
||||
return;
|
||||
return state;
|
||||
}
|
||||
const arrIdx = timebox.findIndex(idxNotif =>
|
||||
notifIdxEqual(index, idxNotif.index)
|
||||
);
|
||||
if (arrIdx === -1) {
|
||||
console.warn('Modifying nonexistent index');
|
||||
return;
|
||||
return state;
|
||||
}
|
||||
timebox[arrIdx].notification.read = read;
|
||||
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);
|
||||
if (data) {
|
||||
const { time, index } = data;
|
||||
updateNotificationStats(state, index, 'notifications', x => x-1);
|
||||
removeNotificationFromUnread(state, index, makePatDa(time));
|
||||
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);
|
||||
if (data) {
|
||||
const { time, index } = data;
|
||||
updateNotificationStats(state, index, 'notifications', x => x+1);
|
||||
addNotificationToUnread(state, index, makePatDa(time));
|
||||
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);
|
||||
if (data) {
|
||||
const { index } = data;
|
||||
removeNotificationFromUnread(state, index, makePatDa(data.time))
|
||||
const time = makePatDa(data.time);
|
||||
const timebox = state.notifications.get(time);
|
||||
if (!timebox) {
|
||||
console.warn('Modifying nonexistent timebox');
|
||||
return;
|
||||
return state;
|
||||
}
|
||||
const [archived, unarchived] = _.partition(timebox, idxNotif =>
|
||||
notifIdxEqual(index, idxNotif.index)
|
||||
@ -362,7 +447,6 @@ function archive(json: any, state: HarkState) {
|
||||
} else {
|
||||
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 { StoreState } from '../../store/type';
|
||||
import { Cage } from '~/types/cage';
|
||||
import { compose } from 'lodash/fp';
|
||||
|
||||
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> {
|
||||
reduce(json: Cage, state: S) {
|
||||
export default class InviteReducer {
|
||||
reduce(json: Cage) {
|
||||
const data = json['invite-update'];
|
||||
if (data) {
|
||||
this.initial(data, state);
|
||||
this.create(data, state);
|
||||
this.delete(data, state);
|
||||
this.invite(data, state);
|
||||
this.accepted(data, state);
|
||||
this.decline(data, state);
|
||||
}
|
||||
}
|
||||
|
||||
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];
|
||||
reduceState<InviteState, InviteUpdate>(useInviteState, data, [
|
||||
initial,
|
||||
create,
|
||||
deleteInvite,
|
||||
invite,
|
||||
accepted,
|
||||
decline,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 { LaunchUpdate } from '~/types/launch-update';
|
||||
import { LaunchUpdate, WeatherState } from '~/types/launch-update';
|
||||
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<S extends LaunchState> {
|
||||
reduce(json: Cage, state: S) {
|
||||
export default class LaunchReducer {
|
||||
reduce(json: Cage) {
|
||||
const data = _.get(json, 'launch-update', false);
|
||||
if (data) {
|
||||
this.initial(data, state);
|
||||
this.changeFirstTime(data, state);
|
||||
this.changeOrder(data, state);
|
||||
this.changeFirstTime(data, state);
|
||||
this.changeIsShown(data, state);
|
||||
reduceState<LaunchState, LaunchUpdate>(useLaunchState, data, [
|
||||
initial,
|
||||
changeFirstTime,
|
||||
changeOrder,
|
||||
changeFirstTime,
|
||||
changeIsShown,
|
||||
]);
|
||||
}
|
||||
|
||||
const weatherData = _.get(json, 'weather', false);
|
||||
const weatherData: WeatherState = _.get(json, 'weather', false);
|
||||
if (weatherData) {
|
||||
state.weather = weatherData;
|
||||
useLaunchState.getState().set(state => {
|
||||
state.weather = weatherData;
|
||||
});
|
||||
}
|
||||
|
||||
const locationData = _.get(json, 'location', false);
|
||||
if (locationData) {
|
||||
state.userLocation = locationData;
|
||||
useLaunchState.getState().set(state => {
|
||||
state.userLocation = locationData;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
initial(json: LaunchUpdate, state: S) {
|
||||
const data = _.get(json, 'initial', false);
|
||||
if (data) {
|
||||
state.launch = data;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
const baseHash = _.get(json, 'baseHash', false);
|
||||
if (baseHash) {
|
||||
useLaunchState.getState().set(state => {
|
||||
state.baseHash = baseHash;
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 { StoreState } from '../../store/type';
|
||||
import { compose } from 'lodash/fp';
|
||||
|
||||
import { MetadataUpdate } from '@urbit/api/metadata';
|
||||
|
||||
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<S extends MetadataState> {
|
||||
reduce(json: Cage, state: S) {
|
||||
export default class MetadataReducer {
|
||||
reduce(json: Cage) {
|
||||
const data = json['metadata-update'];
|
||||
if (data) {
|
||||
console.log(data);
|
||||
this.associations(data, state);
|
||||
this.add(data, state);
|
||||
this.update(data, state);
|
||||
this.remove(data, state);
|
||||
this.groupInitial(data, state);
|
||||
reduceState<MetadataState, MetadataUpdate>(useMetadataState, data, [
|
||||
associations,
|
||||
add,
|
||||
update,
|
||||
remove,
|
||||
groupInitial,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
groupInitial(json: MetadataUpdate, state: S) {
|
||||
const data = _.get(json, 'initial-group', false);
|
||||
console.log(data);
|
||||
if(data) {
|
||||
this.associations(data, state);
|
||||
}
|
||||
const groupInitial = (json: MetadataUpdate, state: MetadataState): MetadataState => {
|
||||
const data = _.get(json, 'initial-group', false);
|
||||
if(data) {
|
||||
state = associations(data, state);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
associations(json: MetadataUpdate, state: S) {
|
||||
const data = _.get(json, 'associations', false);
|
||||
if (data) {
|
||||
const metadata = state.associations;
|
||||
Object.keys(data).forEach((key) => {
|
||||
const val = data[key];
|
||||
const appName = val['app-name'];
|
||||
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;
|
||||
|
||||
const associations = (json: MetadataUpdate, state: MetadataState): MetadataState => {
|
||||
const data = _.get(json, 'associations', false);
|
||||
if (data) {
|
||||
const metadata = state.associations;
|
||||
Object.keys(data).forEach((key) => {
|
||||
const val = data[key];
|
||||
const appName = val['app-name'];
|
||||
const rid = val.resource;
|
||||
if (!(appName in metadata)) {
|
||||
metadata[appName] = {};
|
||||
}
|
||||
if (!(rid in metadata[appName])) {
|
||||
metadata[appName][rid] = {};
|
||||
}
|
||||
metadata[appName][rid] = data;
|
||||
metadata[appName][rid] = val;
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
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 { StoreState } from '../../store/type';
|
||||
import { compose } from 'lodash/fp';
|
||||
import { Cage } from '~/types/cage';
|
||||
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> {
|
||||
reduce(json: Cage, state: S) {
|
||||
export default class S3Reducer {
|
||||
reduce(json: Cage) {
|
||||
const data = _.get(json, 's3-update', false);
|
||||
if (data) {
|
||||
this.credentials(data, state);
|
||||
this.configuration(data, state);
|
||||
this.currentBucket(data, state);
|
||||
this.addBucket(data, state);
|
||||
this.removeBucket(data, state);
|
||||
this.endpoint(data, state);
|
||||
this.accessKeyId(data, state);
|
||||
this.secretAccessKey(data, state);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
reduceState<StorageState, S3Update>(useStorageState, data, [
|
||||
credentials,
|
||||
configuration,
|
||||
currentBucket,
|
||||
addBucket,
|
||||
removeBucket,
|
||||
endpoint,
|
||||
accessKeyId,
|
||||
secretAccessKey,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 { SettingsUpdate } from '~/types/settings';
|
||||
import useSettingsState, { SettingsStateZus } from "~/logic/state/settings";
|
||||
import produce from 'immer';
|
||||
import useSettingsState, { SettingsState } from "~/logic/state/settings";
|
||||
import { SettingsUpdate } from '@urbit/api/dist/settings';
|
||||
import { reduceState } from '../state/base';
|
||||
|
||||
export default class SettingsStateZusettingsReducer{
|
||||
export default class SettingsReducer {
|
||||
reduce(json: any) {
|
||||
const old = useSettingsState.getState();
|
||||
const newState = produce(old, state => {
|
||||
let data = json["settings-event"];
|
||||
if (data) {
|
||||
console.log(data);
|
||||
this.putBucket(data, state);
|
||||
this.delBucket(data, state);
|
||||
this.putEntry(data, state);
|
||||
this.delEntry(data, state);
|
||||
}
|
||||
data = json["settings-data"];
|
||||
if (data) {
|
||||
console.log(data);
|
||||
this.getAll(data, state);
|
||||
this.getBucket(data, state);
|
||||
this.getEntry(data, state);
|
||||
}
|
||||
});
|
||||
useSettingsState.setState(newState);
|
||||
let data = json["settings-event"];
|
||||
if (data) {
|
||||
reduceState<SettingsState, SettingsUpdate>(useSettingsState, data, [
|
||||
this.putBucket,
|
||||
this.delBucket,
|
||||
this.putEntry,
|
||||
this.delEntry,
|
||||
]);
|
||||
}
|
||||
data = json["settings-data"];
|
||||
if (data) {
|
||||
reduceState<SettingsState, SettingsUpdate>(useSettingsState, data, [
|
||||
this.getAll,
|
||||
this.getBucket,
|
||||
this.getEntry,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
putBucket(json: SettingsUpdate, state: SettingsStateZus) {
|
||||
putBucket(json: SettingsUpdate, state: SettingsState): SettingsState {
|
||||
const data = _.get(json, 'put-bucket', false);
|
||||
if (data) {
|
||||
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);
|
||||
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);
|
||||
if (data) {
|
||||
if (!state[data["bucket-key"]]) {
|
||||
@ -48,36 +48,41 @@ export default class SettingsStateZusettingsReducer{
|
||||
}
|
||||
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);
|
||||
if (data) {
|
||||
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');
|
||||
if(data) {
|
||||
_.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 bucket = _.get(json, 'bucket', false);
|
||||
if (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 entryKey = _.get(json, 'entry-key', false);
|
||||
const entry = _.get(json, 'entry', false);
|
||||
if (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 create, { State } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import produce from 'immer';
|
||||
import { BackgroundConfig, RemoteContentPolicy, TutorialProgress, tutorialProgress, LeapCategories, leapCategories } from "~/types/local-update";
|
||||
import { RemoteContentPolicy, LeapCategories, leapCategories } from "~/types/local-update";
|
||||
import { BaseState, createState } from '~/logic/state/base';
|
||||
|
||||
|
||||
export interface SettingsState {
|
||||
export interface SettingsState extends BaseState<SettingsState> {
|
||||
display: {
|
||||
backgroundType: 'none' | 'url' | 'color';
|
||||
background?: string;
|
||||
@ -28,11 +25,8 @@ export interface SettingsState {
|
||||
seen: boolean;
|
||||
joined?: number;
|
||||
};
|
||||
set: (fn: (state: SettingsState) => void) => void
|
||||
};
|
||||
|
||||
export type SettingsStateZus = SettingsState & State;
|
||||
|
||||
export const selectSettingsState =
|
||||
<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;
|
||||
|
||||
const useSettingsState = create<SettingsStateZus>((set) => ({
|
||||
const useSettingsState = createState<SettingsState>('Settings', {
|
||||
display: {
|
||||
backgroundType: 'none',
|
||||
background: undefined,
|
||||
@ -66,17 +60,7 @@ const useSettingsState = create<SettingsStateZus>((set) => ({
|
||||
tutorial: {
|
||||
seen: false,
|
||||
joined: undefined
|
||||
},
|
||||
set: (fn: (state: SettingsState) => void) => set(produce(fn))
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
function withSettingsState<P, S extends keyof SettingsState>(Component: any, stateMemberKeys?: S[]) {
|
||||
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 };
|
||||
export default useSettingsState;
|
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();
|
||||
}
|
||||
|
||||
dehydrate() {}
|
||||
|
||||
rehydrate() {}
|
||||
|
||||
initialState() {
|
||||
return {} as S;
|
||||
}
|
||||
|
@ -20,11 +20,11 @@ import GcpReducer from '../reducers/gcp-reducer';
|
||||
import { OrderedMap } from '../lib/OrderedMap';
|
||||
import { BigIntOrderedMap } from '../lib/BigIntOrderedMap';
|
||||
import { GroupViewReducer } from '../reducers/group-view';
|
||||
import { unstable_batchedUpdates } from 'react-dom';
|
||||
|
||||
export default class GlobalStore extends BaseStore<StoreState> {
|
||||
inviteReducer = new InviteReducer();
|
||||
metadataReducer = new MetadataReducer();
|
||||
localReducer = new LocalReducer();
|
||||
s3Reducer = new S3Reducer();
|
||||
groupReducer = new GroupReducer();
|
||||
launchReducer = new LaunchReducer();
|
||||
@ -44,84 +44,30 @@ export default class GlobalStore extends BaseStore<StoreState> {
|
||||
console.log(_.pick(this.state, stateKeys));
|
||||
}
|
||||
|
||||
rehydrate() {
|
||||
this.localReducer.rehydrate(this.state);
|
||||
}
|
||||
|
||||
dehydrate() {
|
||||
this.localReducer.dehydrate(this.state);
|
||||
}
|
||||
|
||||
initialState(): StoreState {
|
||||
return {
|
||||
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) {
|
||||
// debug shim
|
||||
const tag = Object.keys(data)[0];
|
||||
const oldActions = this.pastActions[tag] || [];
|
||||
this.pastActions[tag] = [data[tag], ...oldActions.slice(0,14)];
|
||||
|
||||
this.inviteReducer.reduce(data, this.state);
|
||||
this.metadataReducer.reduce(data, this.state);
|
||||
this.localReducer.reduce(data, this.state);
|
||||
this.s3Reducer.reduce(data, this.state);
|
||||
this.groupReducer.reduce(data, this.state);
|
||||
this.launchReducer.reduce(data, this.state);
|
||||
this.connReducer.reduce(data, this.state);
|
||||
GraphReducer(data, this.state);
|
||||
HarkReducer(data, this.state);
|
||||
ContactReducer(data, this.state);
|
||||
this.settingsReducer.reduce(data);
|
||||
this.gcpReducer.reduce(data, this.state);
|
||||
GroupViewReducer(data, this.state);
|
||||
unstable_batchedUpdates(() => {
|
||||
// debug shim
|
||||
const tag = Object.keys(data)[0];
|
||||
const oldActions = this.pastActions[tag] || [];
|
||||
this.pastActions[tag] = [data[tag], ...oldActions.slice(0, 14)];
|
||||
this.inviteReducer.reduce(data);
|
||||
this.metadataReducer.reduce(data);
|
||||
this.s3Reducer.reduce(data);
|
||||
this.groupReducer.reduce(data);
|
||||
GroupViewReducer(data);
|
||||
this.launchReducer.reduce(data);
|
||||
this.connReducer.reduce(data, this.state);
|
||||
GraphReducer(data);
|
||||
HarkReducer(data);
|
||||
ContactReducer(data);
|
||||
this.settingsReducer.reduce(data);
|
||||
this.gcpReducer.reduce(data);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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 { Graphs } from '@urbit/api/graph';
|
||||
import {
|
||||
Notifications,
|
||||
NotificationGraphConfig,
|
||||
GroupNotificationsConfig,
|
||||
Unreads,
|
||||
JoinRequests,
|
||||
Patp
|
||||
} from '@urbit/api';
|
||||
|
||||
export interface StoreState {
|
||||
// local state
|
||||
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 { uxToHex } from '~/logic/lib/util';
|
||||
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 { 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};
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
@ -66,7 +69,9 @@ const Root = withSettingsState(styled.div`
|
||||
border-radius: 1rem;
|
||||
border: 0px solid transparent;
|
||||
}
|
||||
`, ['display']);
|
||||
`, [
|
||||
[useSettingsState, ['display']]
|
||||
]);
|
||||
|
||||
const StatusBarWithRouter = withRouter(StatusBar);
|
||||
class App extends React.Component {
|
||||
@ -79,7 +84,7 @@ class App extends React.Component {
|
||||
|
||||
this.appChannel = new window.channel();
|
||||
this.api = new GlobalApi(this.ship, this.appChannel, this.store);
|
||||
gcpManager.configure(this.api, this.store);
|
||||
gcpManager.configure(this.api);
|
||||
this.subscription =
|
||||
new GlobalSubscription(this.store, this.api, this.appChannel);
|
||||
|
||||
@ -98,7 +103,6 @@ class App extends React.Component {
|
||||
}, 500);
|
||||
this.api.local.getBaseHash();
|
||||
this.api.settings.getAll();
|
||||
this.store.rehydrate();
|
||||
gcpManager.start();
|
||||
Mousetrap.bindGlobal(['command+/', 'ctrl+/'], (e) => {
|
||||
e.preventDefault();
|
||||
@ -119,8 +123,8 @@ class App extends React.Component {
|
||||
|
||||
faviconString() {
|
||||
let background = '#ffffff';
|
||||
if (this.state.contacts.hasOwnProperty(`~${window.ship}`)) {
|
||||
background = `#${uxToHex(this.state.contacts[`~${window.ship}`].color)}`;
|
||||
if (this.props.contacts.hasOwnProperty(`~${window.ship}`)) {
|
||||
background = `#${uxToHex(this.props.contacts[`~${window.ship}`].color)}`;
|
||||
}
|
||||
const foreground = foregroundFromBackground(background);
|
||||
const svg = sigiljs({
|
||||
@ -135,16 +139,12 @@ class App extends React.Component {
|
||||
|
||||
render() {
|
||||
const { state, props } = this;
|
||||
const associations = state.associations ?
|
||||
state.associations : { contacts: {} };
|
||||
const theme =
|
||||
((props.dark && props?.display?.theme == "auto") ||
|
||||
props?.display?.theme == "dark"
|
||||
) ? dark : light;
|
||||
|
||||
const notificationsCount = state.notificationsCount || 0;
|
||||
const doNotDisturb = state.doNotDisturb || false;
|
||||
const ourContact = this.state.contacts[`~${this.ship}`] || null;
|
||||
const ourContact = this.props.contacts[`~${this.ship}`] || null;
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<Helmet>
|
||||
@ -158,27 +158,17 @@ class App extends React.Component {
|
||||
<ErrorBoundary>
|
||||
<StatusBarWithRouter
|
||||
props={this.props}
|
||||
associations={associations}
|
||||
invites={this.state.invites}
|
||||
ourContact={ourContact}
|
||||
api={this.api}
|
||||
connection={this.state.connection}
|
||||
subscription={this.subscription}
|
||||
ship={this.ship}
|
||||
doNotDisturb={doNotDisturb}
|
||||
notificationsCount={notificationsCount}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
<ErrorBoundary>
|
||||
<Omnibox
|
||||
associations={state.associations}
|
||||
apps={state.launch}
|
||||
tiles={state.launch.tiles}
|
||||
api={this.api}
|
||||
contacts={state.contacts}
|
||||
notifications={state.notificationsCount}
|
||||
invites={state.invites}
|
||||
groups={state.groups}
|
||||
show={this.props.omniboxShown}
|
||||
toggle={this.props.toggleOmnibox}
|
||||
/>
|
||||
@ -188,7 +178,7 @@ class App extends React.Component {
|
||||
ship={this.ship}
|
||||
api={this.api}
|
||||
subscription={this.subscription}
|
||||
{...state}
|
||||
connection={this.state.connection}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</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 './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 & {
|
||||
association: Association;
|
||||
@ -26,12 +30,15 @@ type ChatResourceProps = StoreState & {
|
||||
export function ChatResource(props: ChatResourceProps) {
|
||||
const station = props.association.resource;
|
||||
const groupPath = props.association.group;
|
||||
const group = props.groups[groupPath];
|
||||
const contacts = props.contacts;
|
||||
const groups = useGroupState(state => state.groups);
|
||||
const group = groups[groupPath];
|
||||
const contacts = useContactState(state => state.contacts);
|
||||
const graphs = useGraphState(state => state.graphs);
|
||||
const graphPath = station.slice(7);
|
||||
const graph = props.graphs[station.slice(7)];
|
||||
const isChatMissing = !props.graphKeys.has(station.slice(7));
|
||||
const unreadCount = props.unreads.graph?.[station]?.['/']?.unreads || 0;
|
||||
const graph = graphs[graphPath];
|
||||
const unreads = useHarkState(state => state.unreads);
|
||||
const unreadCount = unreads.graph?.[station]?.['/']?.unreads || 0;
|
||||
const graphTimesentMap = useGraphState(state => state.graphTimesentMap);
|
||||
const [,, owner, name] = station.split('/');
|
||||
const ourContact = contacts?.[`~${window.ship}`];
|
||||
const chatInput = useRef<ChatInput>();
|
||||
@ -132,9 +139,6 @@ export function ChatResource(props: ChatResourceProps) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
const modifiedContacts = { ...contacts };
|
||||
delete modifiedContacts[`~${window.ship}`];
|
||||
|
||||
return (
|
||||
<Col {...bind} height="100%" overflow="hidden" position="relative">
|
||||
<ShareProfile
|
||||
@ -152,15 +156,11 @@ export function ChatResource(props: ChatResourceProps) {
|
||||
key={station}
|
||||
history={props.history}
|
||||
graph={graph}
|
||||
graphSize={graph.size}
|
||||
unreadCount={unreadCount}
|
||||
contacts={
|
||||
(!showBanner && hasLoadedAllowed) ?
|
||||
contacts : modifiedContacts
|
||||
}
|
||||
showOurContact={ !showBanner && hasLoadedAllowed }
|
||||
association={props.association}
|
||||
associations={props.associations}
|
||||
groups={props.groups}
|
||||
pendingSize={Object.keys(props.graphTimesentMap[graphPath] || {}).length}
|
||||
pendingSize={Object.keys(graphTimesentMap[graphPath] || {}).length}
|
||||
group={group}
|
||||
ship={owner}
|
||||
station={station}
|
||||
@ -176,11 +176,7 @@ export function ChatResource(props: ChatResourceProps) {
|
||||
(!showBanner && hasLoadedAllowed) ? ourContact : null
|
||||
}
|
||||
envelopes={[]}
|
||||
contacts={
|
||||
(!showBanner && hasLoadedAllowed) ? contacts : modifiedContacts
|
||||
}
|
||||
onUnmount={appendUnsent}
|
||||
storage={props.storage}
|
||||
placeholder="Message..."
|
||||
message={unsent[station] || ''}
|
||||
deleteMessage={clearUnsent}
|
||||
|
@ -19,14 +19,12 @@ type ChatInputProps = IuseStorage & {
|
||||
station: unknown;
|
||||
ourContact: unknown;
|
||||
envelopes: Envelope[];
|
||||
contacts: Contacts;
|
||||
onUnmount(msg: string): void;
|
||||
storage: StorageState;
|
||||
placeholder: string;
|
||||
message: string;
|
||||
deleteMessage(): void;
|
||||
hideAvatars: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
interface ChatInputState {
|
||||
inCodeMode: boolean;
|
||||
@ -62,20 +60,23 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
||||
|
||||
submit(text) {
|
||||
const { props, state } = this;
|
||||
const [,,ship,name] = props.station.split('/');
|
||||
const [, , ship, name] = props.station.split('/');
|
||||
if (state.inCodeMode) {
|
||||
this.setState({
|
||||
inCodeMode: false
|
||||
}, async () => {
|
||||
const output = await props.api.graph.eval(text);
|
||||
const contents: Content[] = [{ code: { output, expression: text } }];
|
||||
const post = createPost(contents);
|
||||
props.api.graph.addPost(ship, name, post);
|
||||
});
|
||||
this.setState(
|
||||
{
|
||||
inCodeMode: false
|
||||
},
|
||||
async () => {
|
||||
const output = await props.api.graph.eval(text);
|
||||
const contents: Content[] = [{ code: { output, expression: text } }];
|
||||
const post = createPost(contents);
|
||||
props.api.graph.addPost(ship, name, post);
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const post = createPost(tokenizeMessage((text)));
|
||||
const post = createPost(tokenizeMessage(text));
|
||||
|
||||
props.deleteMessage();
|
||||
|
||||
@ -88,8 +89,8 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
||||
this.chatEditor.current.editor.setValue(url);
|
||||
this.setState({ uploadingPaste: false });
|
||||
} else {
|
||||
const [,,ship,name] = props.station.split('/');
|
||||
props.api.graph.addPost(ship,name, createPost([{ url }]));
|
||||
const [, , ship, name] = props.station.split('/');
|
||||
props.api.graph.addPost(ship, name, createPost([{ url }]));
|
||||
}
|
||||
}
|
||||
|
||||
@ -112,7 +113,8 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
||||
return;
|
||||
}
|
||||
Array.from(files).forEach((file) => {
|
||||
this.props.uploadDefault(file)
|
||||
this.props
|
||||
.uploadDefault(file)
|
||||
.then(this.uploadSuccess)
|
||||
.catch(this.uploadError);
|
||||
});
|
||||
@ -121,32 +123,40 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
||||
render() {
|
||||
const { props, state } = this;
|
||||
|
||||
const color = props.ourContact
|
||||
? uxToHex(props.ourContact.color) : '000000';
|
||||
const color = props.ourContact ? uxToHex(props.ourContact.color) : '000000';
|
||||
|
||||
const sigilClass = props.ourContact
|
||||
? '' : 'mix-blend-diff';
|
||||
const sigilClass = props.ourContact ? '' : 'mix-blend-diff';
|
||||
|
||||
const avatar = (
|
||||
props.ourContact &&
|
||||
((props.ourContact?.avatar) && !props.hideAvatars)
|
||||
)
|
||||
? <BaseImage
|
||||
const avatar =
|
||||
props.ourContact && props.ourContact?.avatar && !props.hideAvatars ? (
|
||||
<BaseImage
|
||||
src={props.ourContact.avatar}
|
||||
height={16}
|
||||
width={16}
|
||||
height={24}
|
||||
width={24}
|
||||
style={{ objectFit: 'cover' }}
|
||||
borderRadius={1}
|
||||
display='inline-block'
|
||||
/>
|
||||
: <Sigil
|
||||
ship={window.ship}
|
||||
size={16}
|
||||
color={`#${color}`}
|
||||
classes={sigilClass}
|
||||
icon
|
||||
padding={2}
|
||||
/>;
|
||||
) : (
|
||||
<Box
|
||||
width={24}
|
||||
height={24}
|
||||
display='flex'
|
||||
justifyContent='center'
|
||||
alignItems='center'
|
||||
backgroundColor={`#${color}`}
|
||||
borderRadius={1}
|
||||
>
|
||||
<Sigil
|
||||
ship={window.ship}
|
||||
size={16}
|
||||
color={`#${color}`}
|
||||
classes={sigilClass}
|
||||
icon
|
||||
padding={2}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<Row
|
||||
@ -160,7 +170,7 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
||||
className='cf'
|
||||
zIndex={0}
|
||||
>
|
||||
<Row p='2' alignItems='center'>
|
||||
<Row p='12px 4px 12px 12px' alignItems='center'>
|
||||
{avatar}
|
||||
</Row>
|
||||
<ChatEditor
|
||||
@ -172,31 +182,23 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
||||
onPaste={this.onPaste.bind(this)}
|
||||
placeholder='Message...'
|
||||
/>
|
||||
<Box
|
||||
mx={2}
|
||||
flexShrink={0}
|
||||
height='16px'
|
||||
width='16px'
|
||||
flexBasis='16px'
|
||||
>
|
||||
{this.props.canUpload
|
||||
? this.props.uploading
|
||||
? <LoadingSpinner />
|
||||
: <Icon icon='Links'
|
||||
width="16"
|
||||
height="16"
|
||||
onClick={() => this.props.promptUpload().then(this.uploadSuccess)}
|
||||
/>
|
||||
: null
|
||||
}
|
||||
<Box mx={2} flexShrink={0} height='16px' width='16px' flexBasis='16px'>
|
||||
{this.props.canUpload ? (
|
||||
this.props.uploading ? (
|
||||
<LoadingSpinner />
|
||||
) : (
|
||||
<Icon
|
||||
icon='Links'
|
||||
width='16'
|
||||
height='16'
|
||||
onClick={() =>
|
||||
this.props.promptUpload().then(this.uploadSuccess)
|
||||
}
|
||||
/>
|
||||
)
|
||||
) : null}
|
||||
</Box>
|
||||
<Box
|
||||
mr={2}
|
||||
flexShrink={0}
|
||||
height='16px'
|
||||
width='16px'
|
||||
flexBasis='16px'
|
||||
>
|
||||
<Box mr={2} flexShrink={0} height='16px' width='16px' flexBasis='16px'>
|
||||
<Icon
|
||||
icon='Dojo'
|
||||
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 _ from 'lodash';
|
||||
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 OverlaySigil from '~/views/components/OverlaySigil';
|
||||
import {
|
||||
@ -33,11 +33,13 @@ import TextContent from './content/text';
|
||||
import CodeContent from './content/code';
|
||||
import RemoteContent from '~/views/components/RemoteContent';
|
||||
import { Mention } from '~/views/components/MentionText';
|
||||
import { Dropdown } from '~/views/components/Dropdown';
|
||||
import styled from 'styled-components';
|
||||
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 {useIdlingState} from '~/logic/lib/idling';
|
||||
import useContactState from '~/logic/state/contact';
|
||||
import { useIdlingState } from '~/logic/lib/idling';
|
||||
|
||||
export const DATESTAMP_FORMAT = '[~]YYYY.M.D';
|
||||
|
||||
@ -56,46 +58,179 @@ export const DayBreak = ({ when, shimTop = false }: DayBreakProps) => (
|
||||
mt={shimTop ? '-8px' : '0'}
|
||||
>
|
||||
<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 })}
|
||||
</Text>
|
||||
<Rule borderColor='lightGray' />
|
||||
</Row>
|
||||
);
|
||||
|
||||
export const UnreadMarker = React.forwardRef(({ dayBreak, when, api, association }, ref) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const idling = useIdlingState();
|
||||
const dismiss = useCallback(() => {
|
||||
api.hark.markCountAsRead(association, '/', 'message');
|
||||
}, [api, association]);
|
||||
export const UnreadMarker = React.forwardRef(
|
||||
({ dayBreak, when, api, association }, ref) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const idling = useIdlingState();
|
||||
const dismiss = useCallback(() => {
|
||||
api.hark.markCountAsRead(association, '/', 'message');
|
||||
}, [api, association]);
|
||||
|
||||
useEffect(() => {
|
||||
if(visible && !idling) {
|
||||
dismiss();
|
||||
}
|
||||
}, [visible, idling]);
|
||||
useEffect(() => {
|
||||
if (visible && !idling) {
|
||||
dismiss();
|
||||
}
|
||||
}, [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 (
|
||||
<Row
|
||||
position='absolute'
|
||||
ref={ref}
|
||||
px={2}
|
||||
mt={2}
|
||||
height={5}
|
||||
justifyContent='center'
|
||||
alignItems='center'
|
||||
width='100%'
|
||||
>
|
||||
<Rule borderColor='lightBlue' />
|
||||
<VisibilitySensor onChange={setVisible}>
|
||||
<Text color='blue' fontSize={0} flexShrink='0' px={2}>
|
||||
New messages below
|
||||
</Text>
|
||||
</VisibilitySensor>
|
||||
<Rule borderColor='lightBlue' />
|
||||
</Row>
|
||||
)});
|
||||
<Row
|
||||
color='black'
|
||||
cursor='pointer'
|
||||
fontSize={1}
|
||||
fontWeight='500'
|
||||
px={3}
|
||||
py={2}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
<Text fontWeight='500' color={props.color}>
|
||||
{props.children}
|
||||
</Text>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
const MessageActions = ({ api, history, msg, group }) => {
|
||||
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 {
|
||||
msg: Post;
|
||||
@ -104,7 +239,6 @@ interface ChatMessageProps {
|
||||
isLastRead: boolean;
|
||||
group: Group;
|
||||
association: Association;
|
||||
contacts: Contacts;
|
||||
className?: string;
|
||||
isPending: boolean;
|
||||
style?: unknown;
|
||||
@ -115,6 +249,7 @@ interface ChatMessageProps {
|
||||
api: GlobalApi;
|
||||
highlighted?: boolean;
|
||||
renderSigil?: boolean;
|
||||
hideHover?: boolean;
|
||||
innerRef: (el: HTMLDivElement | null) => void;
|
||||
}
|
||||
|
||||
@ -126,8 +261,7 @@ class ChatMessage extends Component<ChatMessageProps> {
|
||||
this.divRef = React.createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
}
|
||||
componentDidMount() {}
|
||||
|
||||
render() {
|
||||
const {
|
||||
@ -137,7 +271,6 @@ class ChatMessage extends Component<ChatMessageProps> {
|
||||
isLastRead,
|
||||
group,
|
||||
association,
|
||||
contacts,
|
||||
className = '',
|
||||
isPending,
|
||||
style,
|
||||
@ -147,9 +280,9 @@ class ChatMessage extends Component<ChatMessageProps> {
|
||||
history,
|
||||
api,
|
||||
highlighted,
|
||||
showOurContact,
|
||||
fontSize,
|
||||
groups,
|
||||
associations
|
||||
hideHover
|
||||
} = this.props;
|
||||
|
||||
let { renderSigil } = this.props;
|
||||
@ -173,23 +306,21 @@ class ChatMessage extends Component<ChatMessageProps> {
|
||||
.unix(msg['time-sent'] / 1000)
|
||||
.format(renderSigil ? 'h:mm A' : 'h:mm');
|
||||
|
||||
|
||||
const messageProps = {
|
||||
msg,
|
||||
timestamp,
|
||||
contacts,
|
||||
association,
|
||||
group,
|
||||
style,
|
||||
containerClass,
|
||||
isPending,
|
||||
showOurContact,
|
||||
history,
|
||||
api,
|
||||
scrollWindow,
|
||||
highlighted,
|
||||
fontSize,
|
||||
associations,
|
||||
groups,
|
||||
hideHover
|
||||
};
|
||||
|
||||
const unreadContainerStyle = {
|
||||
@ -200,7 +331,7 @@ class ChatMessage extends Component<ChatMessageProps> {
|
||||
<Box
|
||||
ref={this.props.innerRef}
|
||||
pt={renderSigil ? 2 : 0}
|
||||
pb={isLastMessage ? 4 : 2}
|
||||
pb={isLastMessage ? '20px' : 0}
|
||||
className={containerClass}
|
||||
backgroundColor={highlighted ? 'blue' : 'white'}
|
||||
style={style}
|
||||
@ -209,12 +340,14 @@ class ChatMessage extends Component<ChatMessageProps> {
|
||||
<DayBreak when={msg['time-sent']} shimTop={renderSigil} />
|
||||
) : null}
|
||||
{renderSigil ? (
|
||||
<>
|
||||
<MessageAuthor pb={'2px'} {...messageProps} />
|
||||
<Message pl={5} pr={4} {...messageProps} />
|
||||
</>
|
||||
<MessageWrapper {...messageProps}>
|
||||
<MessageAuthor pb={1} {...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}>
|
||||
{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 = ({
|
||||
timestamp,
|
||||
contacts,
|
||||
msg,
|
||||
group,
|
||||
api,
|
||||
associations,
|
||||
groups,
|
||||
history,
|
||||
scrollWindow,
|
||||
showOurContact,
|
||||
...rest
|
||||
}) => {
|
||||
const osDark = useLocalState((state) => state.dark);
|
||||
|
||||
const theme = useSettingsState(s => s.display.theme);
|
||||
const dark = theme === 'dark' || (theme === 'auto' && osDark)
|
||||
const theme = useSettingsState((s) => s.display.theme);
|
||||
const dark = theme === 'dark' || (theme === 'auto' && osDark);
|
||||
const contacts = useContactState((state) => state.contacts);
|
||||
|
||||
const datestamp = moment
|
||||
.unix(msg['time-sent'] / 1000)
|
||||
.format(DATESTAMP_FORMAT);
|
||||
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 { hideAvatars } = useSettingsState(selectCalmState);
|
||||
const shipName = showNickname ? contact.nickname : cite(msg.author);
|
||||
@ -297,31 +436,44 @@ export const MessageAuthor = ({
|
||||
contact?.avatar && !hideAvatars ? (
|
||||
<BaseImage
|
||||
display='inline-block'
|
||||
referrerPolicy='no-referrer'
|
||||
style={{ objectFit: 'cover' }}
|
||||
src={contact.avatar}
|
||||
height={16}
|
||||
width={16}
|
||||
height={24}
|
||||
width={24}
|
||||
borderRadius={1}
|
||||
/>
|
||||
) : (
|
||||
<Sigil
|
||||
ship={msg.author}
|
||||
size={16}
|
||||
color={color}
|
||||
classes={sigilClass}
|
||||
icon
|
||||
padding={2}
|
||||
/>
|
||||
<Box
|
||||
width={24}
|
||||
height={24}
|
||||
display='flex'
|
||||
justifyContent='center'
|
||||
alignItems='center'
|
||||
backgroundColor={color}
|
||||
borderRadius={1}
|
||||
>
|
||||
<Sigil
|
||||
ship={msg.author}
|
||||
size={12}
|
||||
display='block'
|
||||
color={color}
|
||||
classes={sigilClass}
|
||||
icon
|
||||
padding={0}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
return (
|
||||
<Box display='flex' alignItems='center' {...rest}>
|
||||
<Box display='flex' alignItems='flex-start' {...rest}>
|
||||
<Box
|
||||
onClick={() => {
|
||||
setShowOverlay(true);
|
||||
}}
|
||||
height={16}
|
||||
height={24}
|
||||
pr={2}
|
||||
pl={2}
|
||||
mt={'1px'}
|
||||
pl={'12px'}
|
||||
cursor='pointer'
|
||||
position='relative'
|
||||
>
|
||||
@ -348,12 +500,12 @@ export const MessageAuthor = ({
|
||||
pt={1}
|
||||
pb={1}
|
||||
display='flex'
|
||||
alignItems='center'
|
||||
alignItems='baseline'
|
||||
>
|
||||
<Text
|
||||
fontSize={0}
|
||||
fontSize={1}
|
||||
mr={2}
|
||||
flexShrink={0}
|
||||
flexShrink={1}
|
||||
mono={nameMono}
|
||||
fontWeight={nameMono ? '400' : '500'}
|
||||
cursor='pointer'
|
||||
@ -385,23 +537,23 @@ export const MessageAuthor = ({
|
||||
|
||||
export const Message = ({
|
||||
timestamp,
|
||||
contacts,
|
||||
msg,
|
||||
group,
|
||||
api,
|
||||
associations,
|
||||
groups,
|
||||
scrollWindow,
|
||||
timestampHover,
|
||||
...rest
|
||||
}) => {
|
||||
const { hovering, bind } = useHovering();
|
||||
const contacts = useContactState((state) => state.contacts);
|
||||
return (
|
||||
<Box position='relative' {...rest}>
|
||||
{timestampHover ? (
|
||||
<Text
|
||||
display={hovering ? 'block' : 'none'}
|
||||
position='absolute'
|
||||
width='36px'
|
||||
textAlign='right'
|
||||
left='0'
|
||||
top='3px'
|
||||
fontSize={0}
|
||||
@ -418,8 +570,7 @@ export const Message = ({
|
||||
case 'text':
|
||||
return (
|
||||
<TextContent
|
||||
associations={associations}
|
||||
groups={groups}
|
||||
key={i}
|
||||
api={api}
|
||||
fontSize={1}
|
||||
lineHeight={'20px'}
|
||||
@ -427,10 +578,11 @@ export const Message = ({
|
||||
/>
|
||||
);
|
||||
case 'code':
|
||||
return <CodeContent content={content} />;
|
||||
return <CodeContent key={i} content={content} />;
|
||||
case 'url':
|
||||
return (
|
||||
<Box
|
||||
key={i}
|
||||
flexShrink={0}
|
||||
fontSize={1}
|
||||
lineHeight='20px'
|
||||
@ -464,9 +616,10 @@ export const Message = ({
|
||||
</Box>
|
||||
);
|
||||
case 'mention':
|
||||
const first = (i) => (i === 0);
|
||||
const first = (i) => i === 0;
|
||||
return (
|
||||
<Mention
|
||||
key={i}
|
||||
first={first(i)}
|
||||
group={group}
|
||||
scrollWindow={scrollWindow}
|
||||
|
@ -20,6 +20,10 @@ import VirtualScroller from '~/views/components/VirtualScroller';
|
||||
|
||||
import ChatMessage, { MessagePlaceholder } from './ChatMessage';
|
||||
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 DEFAULT_BACKLOG_SIZE = 100;
|
||||
@ -32,15 +36,13 @@ type ChatWindowProps = RouteComponentProps<{
|
||||
}> & {
|
||||
unreadCount: number;
|
||||
graph: Graph;
|
||||
contacts: Contacts;
|
||||
graphSize: number;
|
||||
association: Association;
|
||||
group: Group;
|
||||
ship: Patp;
|
||||
station: any;
|
||||
api: GlobalApi;
|
||||
scrollTo?: number;
|
||||
associations: Associations;
|
||||
groups: Groups;
|
||||
};
|
||||
|
||||
interface ChatWindowState {
|
||||
@ -52,16 +54,14 @@ interface ChatWindowState {
|
||||
|
||||
const virtScrollerStyle = { height: '100%' };
|
||||
|
||||
export default class ChatWindow extends Component<
|
||||
class ChatWindow extends Component<
|
||||
ChatWindowProps,
|
||||
ChatWindowState
|
||||
> {
|
||||
private virtualList: VirtualScroller | null;
|
||||
private unreadMarkerRef: React.RefObject<HTMLDivElement>;
|
||||
private prevSize = 0;
|
||||
private loadedNewest = false;
|
||||
private loadedOldest = false;
|
||||
private fetchPending = false;
|
||||
private unreadSet = false;
|
||||
|
||||
INITIALIZATION_MAX_TIME = 100;
|
||||
|
||||
@ -99,6 +99,10 @@ export default class ChatWindow extends Component<
|
||||
|
||||
calculateUnreadIndex() {
|
||||
const { graph, unreadCount } = this.props;
|
||||
const { state } = this;
|
||||
if(state.unreadIndex.neq(bigInt.zero)) {
|
||||
return;
|
||||
}
|
||||
const unreadIndex = graph.keys()[unreadCount];
|
||||
if (!unreadIndex || unreadCount === 0) {
|
||||
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() {
|
||||
this.setState({ idle: true });
|
||||
}
|
||||
@ -123,10 +134,22 @@ export default class ChatWindow extends Component<
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -146,6 +169,12 @@ export default class ChatWindow extends Component<
|
||||
}
|
||||
}
|
||||
|
||||
onBottomLoaded = () => {
|
||||
if(this.state.unreadIndex.eq(bigInt.zero)) {
|
||||
this.calculateUnreadIndex();
|
||||
}
|
||||
}
|
||||
|
||||
scrollToUnread() {
|
||||
const { unreadIndex } = this.state;
|
||||
if (unreadIndex.eq(bigInt.zero)) {
|
||||
@ -170,30 +199,28 @@ export default class ChatWindow extends Component<
|
||||
|
||||
fetchMessages = async (newer: boolean): Promise<boolean> => {
|
||||
const { api, station, graph } = this.props;
|
||||
if(this.fetchPending) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
this.fetchPending = true;
|
||||
const pageSize = 100;
|
||||
|
||||
const [, , ship, name] = station.split('/');
|
||||
const currSize = graph.size;
|
||||
const expectedSize = graph.size + pageSize;
|
||||
if (newer) {
|
||||
const [index] = graph.peekLargest()!;
|
||||
await api.graph.getYoungerSiblings(
|
||||
ship,
|
||||
name,
|
||||
100,
|
||||
pageSize,
|
||||
`/${index.toString()}`
|
||||
);
|
||||
return expectedSize !== graph.size;
|
||||
} else {
|
||||
const [index] = graph.peekSmallest()!;
|
||||
await api.graph.getOlderSiblings(ship, name, 100, `/${index.toString()}`);
|
||||
this.calculateUnreadIndex();
|
||||
await api.graph.getOlderSiblings(ship, name, pageSize, `/${index.toString()}`);
|
||||
const done = expectedSize !== graph.size;
|
||||
if(done) {
|
||||
this.calculateUnreadIndex();
|
||||
}
|
||||
return done;
|
||||
}
|
||||
this.fetchPending = false;
|
||||
return currSize === graph.size;
|
||||
}
|
||||
|
||||
onScroll = ({ scrollTop, scrollHeight, windowHeight }) => {
|
||||
@ -208,7 +235,7 @@ export default class ChatWindow extends Component<
|
||||
api,
|
||||
association,
|
||||
group,
|
||||
contacts,
|
||||
showOurContact,
|
||||
graph,
|
||||
history,
|
||||
groups,
|
||||
@ -218,13 +245,14 @@ export default class ChatWindow extends Component<
|
||||
const messageProps = {
|
||||
association,
|
||||
group,
|
||||
contacts,
|
||||
showOurContact,
|
||||
unreadMarkerRef,
|
||||
history,
|
||||
api,
|
||||
groups,
|
||||
associations
|
||||
};
|
||||
|
||||
const msg = graph.get(index)?.post;
|
||||
if (!msg) return null;
|
||||
if (!this.state.initialized) {
|
||||
@ -255,6 +283,7 @@ export default class ChatWindow extends Component<
|
||||
msg,
|
||||
...messageProps
|
||||
};
|
||||
|
||||
return (
|
||||
<ChatMessage
|
||||
key={index.toString()}
|
||||
@ -270,15 +299,13 @@ export default class ChatWindow extends Component<
|
||||
const {
|
||||
unreadCount,
|
||||
api,
|
||||
ship,
|
||||
station,
|
||||
association,
|
||||
group,
|
||||
contacts,
|
||||
graph,
|
||||
history,
|
||||
groups,
|
||||
associations,
|
||||
showOurContact,
|
||||
pendingSize
|
||||
} = this.props;
|
||||
|
||||
@ -286,19 +313,21 @@ export default class ChatWindow extends Component<
|
||||
const messageProps = {
|
||||
association,
|
||||
group,
|
||||
contacts,
|
||||
unreadMarkerRef,
|
||||
history,
|
||||
api,
|
||||
groups,
|
||||
associations
|
||||
};
|
||||
const unreadIndex = graph.keys()[this.props.unreadCount];
|
||||
const unreadMsg = unreadIndex && graph.get(unreadIndex);
|
||||
const unreadMsg = graph.get(this.state.unreadIndex);
|
||||
|
||||
// hack to force a re-render when we toggle showing contact
|
||||
const contactsModified =
|
||||
showOurContact ? 0 : 100;
|
||||
|
||||
return (
|
||||
<Col height='100%' overflow='hidden' position='relative'>
|
||||
<UnreadNotice
|
||||
{ this.dismissedInitialUnread() &&
|
||||
(<UnreadNotice
|
||||
unreadCount={unreadCount}
|
||||
unreadMsg={
|
||||
unreadCount === 1 &&
|
||||
@ -309,7 +338,7 @@ export default class ChatWindow extends Component<
|
||||
}
|
||||
dismissUnread={this.dismissUnread}
|
||||
onClick={this.scrollToUnread}
|
||||
/>
|
||||
/>)}
|
||||
<VirtualScroller
|
||||
ref={(list) => {
|
||||
this.virtualList = list;
|
||||
@ -318,10 +347,11 @@ export default class ChatWindow extends Component<
|
||||
origin='bottom'
|
||||
style={virtScrollerStyle}
|
||||
onStartReached={this.setActive}
|
||||
onBottomLoaded={this.onBottomLoaded}
|
||||
onScroll={this.onScroll}
|
||||
data={graph}
|
||||
size={graph.size}
|
||||
pendingSize={pendingSize}
|
||||
pendingSize={pendingSize + contactsModified}
|
||||
id={association.resource}
|
||||
averageHeight={22}
|
||||
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)'
|
||||
className={inCodeMode ? 'chat code' : 'chat'}
|
||||
color="black"
|
||||
overflow='auto'
|
||||
>
|
||||
{MOBILE_BROWSER_REGEX.test(navigator.userAgent)
|
||||
? <MobileBox
|
||||
|
@ -91,7 +91,7 @@ const MessageMarkdown = React.memo((props) => {
|
||||
}, []);
|
||||
|
||||
return lines.map((line, i) => (
|
||||
<>
|
||||
<React.Fragment key={i}>
|
||||
{i !== 0 && <Row height={2} />}
|
||||
<ReactMarkdown
|
||||
{...rest}
|
||||
@ -123,7 +123,7 @@ const MessageMarkdown = React.memo((props) => {
|
||||
]
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
</React.Fragment>
|
||||
));
|
||||
});
|
||||
|
||||
@ -145,8 +145,6 @@ export default function TextContent(props) {
|
||||
<GroupLink
|
||||
resource={resource}
|
||||
api={props.api}
|
||||
associations={props.associations}
|
||||
groups={props.groups}
|
||||
pl='2'
|
||||
border='1'
|
||||
borderRadius='2'
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
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 Timestamp from '~/views/components/Timestamp';
|
||||
@ -8,51 +8,67 @@ import Timestamp from '~/views/components/Timestamp';
|
||||
export const UnreadNotice = (props) => {
|
||||
const { unreadCount, unreadMsg, dismissUnread, onClick } = props;
|
||||
|
||||
if (!unreadMsg || (unreadCount === 0)) {
|
||||
if (!unreadMsg || unreadCount === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const stamp = moment.unix(unreadMsg.post['time-sent'] / 1000);
|
||||
|
||||
let datestamp = moment.unix(unreadMsg.post['time-sent'] / 1000).format('YYYY.M.D');
|
||||
const timestamp = moment.unix(unreadMsg.post['time-sent'] / 1000).format('HH:mm');
|
||||
let datestamp = moment
|
||||
.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')) {
|
||||
datestamp = null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box style={{ left: '0px', top: '0px' }}
|
||||
p='4'
|
||||
<Box
|
||||
style={{ left: '0px', top: '0px' }}
|
||||
p='12px'
|
||||
width='100%'
|
||||
position='absolute'
|
||||
zIndex='1'
|
||||
className='unread-notice'
|
||||
>
|
||||
<Box
|
||||
backgroundColor='white'
|
||||
display='flex'
|
||||
alignItems='center'
|
||||
p='2'
|
||||
fontSize='0'
|
||||
justifyContent='space-between'
|
||||
borderRadius='1'
|
||||
border='1'
|
||||
borderColor='blue'>
|
||||
<Text flexShrink='1' textOverflow='ellipsis' whiteSpace='pre' overflow='hidden' display='flex' cursor='pointer' onClick={onClick}>
|
||||
{unreadCount} new message{unreadCount > 1 ? 's' : ''} since{' '}
|
||||
<Timestamp stamp={stamp} color='blue' date={true} fontSize={1} />
|
||||
</Text>
|
||||
<Text
|
||||
ml='4'
|
||||
color='blue'
|
||||
cursor='pointer'
|
||||
textAlign='right'
|
||||
flexShrink='0'
|
||||
onClick={dismissUnread}>
|
||||
Mark as Read
|
||||
</Text>
|
||||
</Box>
|
||||
<Center>
|
||||
<Box backgroundColor='white' borderRadius='2'>
|
||||
<Box
|
||||
backgroundColor='washedBlue'
|
||||
display='flex'
|
||||
alignItems='center'
|
||||
p='2'
|
||||
fontSize='0'
|
||||
justifyContent='space-between'
|
||||
borderRadius='3'
|
||||
border='1'
|
||||
borderColor='lightBlue'
|
||||
>
|
||||
<Text
|
||||
textOverflow='ellipsis'
|
||||
whiteSpace='pre'
|
||||
overflow='hidden'
|
||||
display='flex'
|
||||
cursor='pointer'
|
||||
onClick={onClick}
|
||||
>
|
||||
{unreadCount} new message{unreadCount > 1 ? 's' : ''} since{' '}
|
||||
<Timestamp stamp={stamp} color='black' date={true} fontSize={1} />
|
||||
</Text>
|
||||
<Icon
|
||||
icon='X'
|
||||
ml='4'
|
||||
color='black'
|
||||
cursor='pointer'
|
||||
textAlign='right'
|
||||
onClick={dismissUnread}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Center>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -1,59 +1,55 @@
|
||||
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 { 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 {
|
||||
render() {
|
||||
const { props } = this;
|
||||
const contacts = props.contacts ? props.contacts : {};
|
||||
const groups = props.groups ? props.groups : {};
|
||||
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 = () => {
|
||||
try {
|
||||
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}`);
|
||||
const autoJoin = () => {
|
||||
try {
|
||||
api.graph.joinGraph(
|
||||
`~${deSig(props.match.params.ship)}`,
|
||||
props.match.params.name
|
||||
);
|
||||
|
||||
|
||||
} catch(err) {
|
||||
setTimeout(autoJoin, 2000);
|
||||
}
|
||||
return (
|
||||
<Center width="100%" height="100%">
|
||||
<Text fontSize={1}>Redirecting...</Text>
|
||||
</Center>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if(!graphKeys.has(resource)) {
|
||||
autoJoin();
|
||||
} else if(!!association) {
|
||||
history.push(`/~landscape/home/resource/${association.metadata.module}${path}`);
|
||||
}
|
||||
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 styled from 'styled-components';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import f from 'lodash/fp';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { Col, Button, Box, Row, Icon, Text } from '@tlon/indigo-react';
|
||||
|
||||
import './css/custom.css';
|
||||
|
||||
import useContactState from '~/logic/state/contact';
|
||||
import Tiles from './components/tiles';
|
||||
import Tile from './components/tiles/tile';
|
||||
import Groups from './components/Groups';
|
||||
@ -20,6 +19,7 @@ import { NewGroup } from "~/views/landscape/components/NewGroup";
|
||||
import { JoinGroup } from "~/views/landscape/components/JoinGroup";
|
||||
import { Helmet } from 'react-helmet';
|
||||
import useLocalState from "~/logic/state/local";
|
||||
import useHarkState from '~/logic/state/hark';
|
||||
import { useWaitForProps } from '~/logic/lib/useWaitForProps';
|
||||
import { useQuery } from "~/logic/lib/useQuery";
|
||||
import {
|
||||
@ -30,7 +30,9 @@ import {
|
||||
TUTORIAL_CHAT,
|
||||
TUTORIAL_LINKS
|
||||
} from '~/logic/lib/tutorialModal';
|
||||
import useLaunchState from '~/logic/state/launch';
|
||||
import useSettingsState, { selectCalmState } from '~/logic/state/settings';
|
||||
import useMetadataState from '~/logic/state/metadata';
|
||||
|
||||
|
||||
const ScrollbarLessBox = styled(Box)`
|
||||
@ -44,9 +46,22 @@ const ScrollbarLessBox = styled(Box)`
|
||||
const tutSelector = f.pick(['tutorialProgress', 'nextTutStep', 'hideGroups']);
|
||||
|
||||
export default function LaunchApp(props) {
|
||||
const history = useHistory();
|
||||
const [hashText, setHashText] = useState(props.baseHash);
|
||||
const connection = { props };
|
||||
const baseHash = useLaunchState(state => state.baseHash);
|
||||
const [hashText, setHashText] = useState(baseHash);
|
||||
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 = (
|
||||
<Box
|
||||
position={["relative", "absolute"]}
|
||||
@ -60,15 +75,15 @@ export default function LaunchApp(props) {
|
||||
fontSize={0}
|
||||
cursor="pointer"
|
||||
onClick={() => {
|
||||
writeText(props.baseHash);
|
||||
writeText(baseHash);
|
||||
setHashText('copied');
|
||||
setTimeout(() => {
|
||||
setHashText(props.baseHash);
|
||||
setHashText(baseHash);
|
||||
}, 2000);
|
||||
}}
|
||||
>
|
||||
<Box backgroundColor="washedGray" p={2}>
|
||||
<Text mono bold>{hashText || props.baseHash}</Text>
|
||||
<Text mono bold>{hashText || baseHash}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
@ -77,7 +92,7 @@ export default function LaunchApp(props) {
|
||||
|
||||
useEffect(() => {
|
||||
if(query.get('tutorial')) {
|
||||
if(hasTutorialGroup(props)) {
|
||||
if(hasTutorialGroup({ associations })) {
|
||||
nextTutStep();
|
||||
} else {
|
||||
showModal();
|
||||
@ -85,13 +100,6 @@ export default function LaunchApp(props) {
|
||||
}
|
||||
}, [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({
|
||||
position: 'relative',
|
||||
maxWidth: '350px',
|
||||
@ -103,7 +111,7 @@ export default function LaunchApp(props) {
|
||||
};
|
||||
const onContinue = async (e) => {
|
||||
e.stopPropagation();
|
||||
if(!hasTutorialGroup(props)) {
|
||||
if(!hasTutorialGroup({ associations })) {
|
||||
await props.api.groups.join(TUTORIAL_HOST, TUTORIAL_GROUP);
|
||||
await props.api.settings.putEntry('tutorial', 'joined', Date.now());
|
||||
await waiter(hasTutorialGroup);
|
||||
@ -137,14 +145,14 @@ export default function LaunchApp(props) {
|
||||
</Box>
|
||||
<Text mb="3" lineHeight="tall" fontWeight="medium">Welcome</Text>
|
||||
<Text mb="3" lineHeight="tall">
|
||||
You have been invited to use Landscape, an interface to chat
|
||||
You have been invited to use Landscape, an interface to chat
|
||||
and interact with communities
|
||||
<br />
|
||||
Would you like a tour of Landscape?
|
||||
</Text>
|
||||
<Row gapX="2" justifyContent="flex-end">
|
||||
<Button
|
||||
backgroundColor="washedGray"
|
||||
backgroundColor="washedGray"
|
||||
onClick={() => setExitingTut(true)}
|
||||
>Skip</Button>
|
||||
<StatelessAsyncButton primary onClick={onContinue}>
|
||||
@ -154,19 +162,17 @@ export default function LaunchApp(props) {
|
||||
</Col>
|
||||
)}
|
||||
});
|
||||
const hasLoaded = useMemo(() => Object.keys(props.contacts).length > 0, [props.contacts]);
|
||||
|
||||
useEffect(() => {
|
||||
const seenTutorial = _.get(props.settings, ['tutorial', 'seen'], true);
|
||||
if(hasLoaded && !seenTutorial && tutorialProgress === 'hidden') {
|
||||
if(hasLoaded && !seen && tutorialProgress === 'hidden') {
|
||||
showModal();
|
||||
}
|
||||
}, [props.settings, hasLoaded]);
|
||||
}, [seen, hasLoaded]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet defer={false}>
|
||||
<title>{ props.notificationsCount ? `(${String(props.notificationsCount) }) `: '' }Landscape</title>
|
||||
<title>{ notificationsCount ? `(${String(notificationsCount) }) `: '' }Landscape</title>
|
||||
</Helmet>
|
||||
<ScrollbarLessBox height='100%' overflowY='scroll' display="flex" flexDirection="column">
|
||||
{modal}
|
||||
@ -196,11 +202,7 @@ export default function LaunchApp(props) {
|
||||
</Box>
|
||||
</Tile>
|
||||
<Tiles
|
||||
tiles={props.launch.tiles}
|
||||
tileOrdering={props.launch.tileOrdering}
|
||||
api={props.api}
|
||||
location={props.userLocation}
|
||||
weather={props.weather}
|
||||
/>
|
||||
<ModalButton
|
||||
icon="Plus"
|
||||
@ -221,7 +223,7 @@ export default function LaunchApp(props) {
|
||||
</ModalButton>
|
||||
</>}
|
||||
{!hideGroups &&
|
||||
(<Groups unreads={props.unreads} groups={props.groups} associations={props.associations} />)
|
||||
(<Groups />)
|
||||
}
|
||||
</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 Tile from '../components/tiles/tile';
|
||||
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 useSettingsState, { selectCalmState, SettingsState } from '~/logic/state/settings';
|
||||
|
||||
interface GroupsProps {
|
||||
associations: Associations;
|
||||
}
|
||||
interface GroupsProps {}
|
||||
|
||||
const sortGroupsAlph = (a: Association, b: Association) =>
|
||||
a.group === TUTORIAL_GROUP_RESOURCE
|
||||
@ -40,10 +41,13 @@ const getGraphNotifications = (associations: Associations, unreads: Unreads) =>
|
||||
)(associations.graph);
|
||||
|
||||
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 || {})
|
||||
.filter(e => e?.group in props.groups)
|
||||
.filter(e => e?.group in groupState)
|
||||
.sort(sortGroupsAlph);
|
||||
const graphUnreads = getGraphUnreads(associations || {}, unreads);
|
||||
const graphNotifications = getGraphNotifications(associations || {}, unreads);
|
||||
@ -87,15 +91,19 @@ function Group(props: GroupProps) {
|
||||
isTutorialGroup,
|
||||
anchorRef
|
||||
);
|
||||
const { hideUnreads } = useSettingsState(selectCalmState)
|
||||
const { hideUnreads } = useSettingsState(selectCalmState);
|
||||
const joined = useSettingsState(selectJoined);
|
||||
const days = Math.max(0, Math.floor(moment.duration(moment(joined)
|
||||
.add(14, 'days')
|
||||
.diff(moment()))
|
||||
.as('days'))) || 0;
|
||||
return (
|
||||
<Tile ref={anchorRef} position="relative" bg={isTutorialGroup ? 'lightBlue' : undefined} to={`/~landscape${path}`} gridColumnStart={first ? '1' : null}>
|
||||
<Col height="100%" justifyContent="space-between">
|
||||
<Text>{title}</Text>
|
||||
{!hideUnreads && (<Col>
|
||||
{isTutorialGroup && joined &&
|
||||
(<Text>{Math.floor(moment.duration(moment(joined).add(14, 'days').diff(moment())).as('days'))} days remaining</Text>)
|
||||
{isTutorialGroup && joined &&
|
||||
(<Text>{days} day{days !== 1 && 's'} remaining</Text>)
|
||||
}
|
||||
{updates > 0 &&
|
||||
(<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 WeatherTile from './tiles/weather';
|
||||
|
||||
export default class Tiles extends React.PureComponent {
|
||||
render() {
|
||||
const { props } = this;
|
||||
import useLaunchState from '~/logic/state/launch';
|
||||
|
||||
const tiles = props.tileOrdering.filter((key) => {
|
||||
const tile = props.tiles[key];
|
||||
const Tiles = (props) => {
|
||||
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;
|
||||
}).map((key) => {
|
||||
const tile = props.tiles[key];
|
||||
if ('basic' in tile.type) {
|
||||
const basic = tile.type.basic;
|
||||
return tile.isShown;
|
||||
}).map((key) => {
|
||||
const tile = tileState[key];
|
||||
if ('basic' in tile.type) {
|
||||
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 (
|
||||
<BasicTile
|
||||
<WeatherTile
|
||||
key={key}
|
||||
title={basic.title}
|
||||
iconUrl={basic.iconUrl}
|
||||
linkedUrl={basic.linkedUrl}
|
||||
api={props.api}
|
||||
/>
|
||||
);
|
||||
} else if ('custom' in tile.type) {
|
||||
if (key === 'weather') {
|
||||
return (
|
||||
<WeatherTile
|
||||
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 if (key === 'clock') {
|
||||
const location = weather && 'nearest-area' in weather ? weather['nearest-area'][0] : '';
|
||||
return (
|
||||
<ClockTile key={key} location={location} />
|
||||
);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return <CustomTile key={key} />;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<React.Fragment>{tiles}</React.Fragment>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>{tiles}</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Tiles;
|
||||
|
@ -2,6 +2,8 @@ import React from 'react';
|
||||
import moment from 'moment';
|
||||
import { Box, Icon, Text, BaseAnchor, BaseInput } from '@tlon/indigo-react';
|
||||
import ErrorBoundary from '~/views/components/ErrorBoundary';
|
||||
import withState from '~/logic/lib/withState';
|
||||
import useLaunchState from '~/logic/state/launch';
|
||||
|
||||
import Tile from './tile';
|
||||
|
||||
@ -34,7 +36,7 @@ const imperialCountries = [
|
||||
'Liberia',
|
||||
];
|
||||
|
||||
export default class WeatherTile extends React.Component {
|
||||
class WeatherTile extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
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 { LinkItem } from './components/LinkItem';
|
||||
import { LinkWindow } from './LinkWindow';
|
||||
import LinkWindow from './LinkWindow';
|
||||
import { Comments } from '~/views/components/Comments';
|
||||
|
||||
import './css/custom.css';
|
||||
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 = () => {};
|
||||
|
||||
@ -27,29 +30,24 @@ export function LinkResource(props: LinkResourceProps) {
|
||||
association,
|
||||
api,
|
||||
baseUrl,
|
||||
graphs,
|
||||
contacts,
|
||||
groups,
|
||||
associations,
|
||||
graphKeys,
|
||||
unreads,
|
||||
graphTimesentMap,
|
||||
storage,
|
||||
history
|
||||
} = props;
|
||||
|
||||
const rid = association.resource;
|
||||
|
||||
const relativePath = (p: string) => `${baseUrl}/resource/link${rid}${p}`;
|
||||
const associations = useMetadataState(state => state.associations);
|
||||
|
||||
const [, , ship, name] = rid.split('/');
|
||||
const resourcePath = `${ship.slice(1)}/${name}`;
|
||||
const resource = associations.graph[rid]
|
||||
? associations.graph[rid]
|
||||
: { metadata: {} };
|
||||
const groups = useGroupState(state => state.groups);
|
||||
const group = groups[resource?.group] || {};
|
||||
|
||||
const graphs = useGraphState(state => state.graphs);
|
||||
const graph = graphs[resourcePath] || null;
|
||||
const graphTimesentMap = useGraphState(state => state.graphTimesentMap);
|
||||
|
||||
useEffect(() => {
|
||||
api.graph.getGraph(ship, name);
|
||||
@ -70,12 +68,9 @@ export function LinkResource(props: LinkResourceProps) {
|
||||
return (
|
||||
<LinkWindow
|
||||
key={rid}
|
||||
storage={storage}
|
||||
association={resource}
|
||||
contacts={contacts}
|
||||
resource={resourcePath}
|
||||
graph={graph}
|
||||
unreads={unreads}
|
||||
baseUrl={resourceUrl}
|
||||
group={group}
|
||||
path={resource.group}
|
||||
@ -106,12 +101,10 @@ export function LinkResource(props: LinkResourceProps) {
|
||||
<Col width="100%" p={3} maxWidth="768px">
|
||||
<Link to={resourceUrl}><Text px={3} bold>{'<- Back'}</Text></Link>
|
||||
<LinkItem
|
||||
contacts={contacts}
|
||||
key={node.post.index}
|
||||
resource={resourcePath}
|
||||
node={node}
|
||||
baseUrl={resourceUrl}
|
||||
unreads={unreads}
|
||||
group={group}
|
||||
path={resource?.group}
|
||||
api={api}
|
||||
@ -124,8 +117,6 @@ export function LinkResource(props: LinkResourceProps) {
|
||||
comments={node}
|
||||
resource={resourcePath}
|
||||
association={association}
|
||||
unreads={unreads}
|
||||
contacts={contacts}
|
||||
api={api}
|
||||
editCommentId={editCommentId}
|
||||
history={props.history}
|
||||
|
@ -16,20 +16,20 @@ import { LinkItem } from "./components/LinkItem";
|
||||
import LinkSubmit from "./components/LinkSubmit";
|
||||
import { isWriter } from "~/logic/lib/group";
|
||||
import { StorageState } from "~/types";
|
||||
import withState from "~/logic/lib/withState";
|
||||
import useGraphState from "~/logic/state/graph";
|
||||
|
||||
interface LinkWindowProps {
|
||||
association: Association;
|
||||
contacts: Rolodex;
|
||||
resource: string;
|
||||
graph: Graph;
|
||||
unreads: Unreads;
|
||||
hideNicknames: boolean;
|
||||
hideAvatars: boolean;
|
||||
baseUrl: string;
|
||||
group: Group;
|
||||
path: string;
|
||||
api: GlobalApi;
|
||||
storage: StorageState;
|
||||
pendingSize: number;
|
||||
}
|
||||
|
||||
const style = {
|
||||
@ -40,7 +40,7 @@ const style = {
|
||||
alignItems: "center",
|
||||
};
|
||||
|
||||
export class LinkWindow extends Component<LinkWindowProps, {}> {
|
||||
class LinkWindow extends Component<LinkWindowProps, {}> {
|
||||
fetchLinks = async () => true;
|
||||
|
||||
canWrite() {
|
||||
@ -75,7 +75,6 @@ export class LinkWindow extends Component<LinkWindowProps, {}> {
|
||||
px={3}
|
||||
>
|
||||
<LinkSubmit
|
||||
storage={props.storage}
|
||||
name={name}
|
||||
ship={ship.slice(1)}
|
||||
api={api}
|
||||
@ -89,7 +88,7 @@ export class LinkWindow extends Component<LinkWindowProps, {}> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { graph, api, association, storage, pendingSize } = this.props;
|
||||
const { graph, api, association } = this.props;
|
||||
const first = graph.peekLargest()?.[0];
|
||||
const [, , ship, name] = association.resource.split("/");
|
||||
if (!first) {
|
||||
@ -105,7 +104,6 @@ export class LinkWindow extends Component<LinkWindowProps, {}> {
|
||||
>
|
||||
{this.canWrite() ? (
|
||||
<LinkSubmit
|
||||
storage={storage}
|
||||
name={name}
|
||||
ship={ship.slice(1)}
|
||||
api={api}
|
||||
@ -129,7 +127,7 @@ export class LinkWindow extends Component<LinkWindowProps, {}> {
|
||||
data={graph}
|
||||
averageHeight={100}
|
||||
size={graph.size}
|
||||
pendingSize={pendingSize}
|
||||
pendingSize={this.props.pendingSize}
|
||||
renderer={this.renderItem}
|
||||
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 { Dropdown } from '~/views/components/Dropdown';
|
||||
import RemoteContent from '~/views/components/RemoteContent';
|
||||
import useHarkState from '~/logic/state/hark';
|
||||
|
||||
interface LinkItemProps {
|
||||
node: GraphNode;
|
||||
@ -17,8 +18,6 @@ interface LinkItemProps {
|
||||
api: GlobalApi;
|
||||
group: Group;
|
||||
path: string;
|
||||
contacts: Rolodex;
|
||||
unreads: Unreads;
|
||||
}
|
||||
|
||||
export const LinkItem = (props: LinkItemProps): ReactElement => {
|
||||
@ -28,7 +27,6 @@ export const LinkItem = (props: LinkItemProps): ReactElement => {
|
||||
api,
|
||||
group,
|
||||
path,
|
||||
contacts,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
@ -89,8 +87,9 @@ export const LinkItem = (props: LinkItemProps): ReactElement => {
|
||||
};
|
||||
|
||||
const appPath = `/ship/~${resource}`;
|
||||
const commColor = (props.unreads.graph?.[appPath]?.[`/${index}`]?.unreads ?? 0) > 0 ? 'blue' : 'gray';
|
||||
const isUnread = props.unreads.graph?.[appPath]?.['/']?.unreads?.has(node.post.index);
|
||||
const unreads = useHarkState(state => state.unreads);
|
||||
const commColor = (unreads.graph?.[appPath]?.[`/${index}`]?.unreads ?? 0) > 0 ? 'blue' : 'gray';
|
||||
const isUnread = unreads.graph?.[appPath]?.['/']?.unreads?.has(node.post.index);
|
||||
|
||||
return (
|
||||
<Box
|
||||
@ -149,18 +148,13 @@ export const LinkItem = (props: LinkItemProps): ReactElement => {
|
||||
</Anchor>
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Row minWidth='0' flexShrink={0} width="100%" justifyContent="space-between" py={3} bg="white">
|
||||
|
||||
<Author
|
||||
showImage
|
||||
contacts={contacts}
|
||||
ship={author}
|
||||
date={node.post['time-sent']}
|
||||
group={group}
|
||||
api={api}
|
||||
></Author>
|
||||
|
||||
/>
|
||||
<Box ml="auto">
|
||||
<Link
|
||||
to={node.post.pending ? '#' : `${baseUrl}/${index}`}
|
||||
|
@ -10,14 +10,13 @@ import { hasProvider } from 'oembed-parser';
|
||||
|
||||
interface LinkSubmitProps {
|
||||
api: GlobalApi;
|
||||
storage: StorageState;
|
||||
name: string;
|
||||
ship: string;
|
||||
}
|
||||
|
||||
const LinkSubmit = (props: LinkSubmitProps) => {
|
||||
const { canUpload, uploadDefault, uploading, promptUpload } =
|
||||
useStorage(props.storage);
|
||||
useStorage();
|
||||
|
||||
const [submitFocused, setSubmitFocused] = useState(false);
|
||||
const [urlFocused, setUrlFocused] = useState(false);
|
||||
|
@ -18,6 +18,8 @@ import { getSnippet } from '~/logic/lib/publish';
|
||||
import styled from 'styled-components';
|
||||
import { MentionText } from '~/views/components/MentionText';
|
||||
import ChatMessage from '../chat/components/ChatMessage';
|
||||
import useContactState from '~/logic/state/contact';
|
||||
import useGroupState from '~/logic/state/group';
|
||||
|
||||
function getGraphModuleIcon(module: string) {
|
||||
if (module === 'link') {
|
||||
@ -30,7 +32,7 @@ const FilterBox = styled(Box)`
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
transparent,
|
||||
${p => p.theme.colors.white}
|
||||
${(p) => p.theme.colors.white}
|
||||
);
|
||||
`;
|
||||
|
||||
@ -67,7 +69,6 @@ const GraphUrl = ({ url, title }) => (
|
||||
const GraphNodeContent = ({
|
||||
group,
|
||||
post,
|
||||
contacts,
|
||||
mod,
|
||||
description,
|
||||
index,
|
||||
@ -80,9 +81,7 @@ const GraphNodeContent = ({
|
||||
const [{ text }, { url }] = contents;
|
||||
return <GraphUrl title={text} url={url} />;
|
||||
} else if (idx.length === 3) {
|
||||
return (
|
||||
<MentionText content={contents} contacts={contacts} group={group} />
|
||||
);
|
||||
return <MentionText content={contents} group={group} />;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@ -92,7 +91,6 @@ const GraphNodeContent = ({
|
||||
<MentionText
|
||||
content={contents}
|
||||
group={group}
|
||||
contacts={contacts}
|
||||
fontSize='14px'
|
||||
lineHeight='tall'
|
||||
/>
|
||||
@ -133,12 +131,12 @@ const GraphNodeContent = ({
|
||||
renderSigil={false}
|
||||
containerClass='items-top cf hide-child'
|
||||
group={group}
|
||||
contacts={contacts}
|
||||
groups={{}}
|
||||
associations={{ graph: {}, groups: {} }}
|
||||
msg={post}
|
||||
fontSize='0'
|
||||
pt='2'
|
||||
hideHover={true}
|
||||
/>
|
||||
</Row>
|
||||
);
|
||||
@ -173,7 +171,6 @@ function getNodeUrl(
|
||||
}
|
||||
const GraphNode = ({
|
||||
post,
|
||||
contacts,
|
||||
author,
|
||||
mod,
|
||||
description,
|
||||
@ -184,10 +181,11 @@ const GraphNode = ({
|
||||
group,
|
||||
read,
|
||||
onRead,
|
||||
showContact = false,
|
||||
showContact = false
|
||||
}) => {
|
||||
author = deSig(author);
|
||||
const history = useHistory();
|
||||
const contacts = useContactState((state) => state.contacts);
|
||||
|
||||
const nodeUrl = getNodeUrl(mod, group?.hidden, groupPath, graph, index);
|
||||
|
||||
@ -199,22 +197,18 @@ const GraphNode = ({
|
||||
}, [read, onRead]);
|
||||
|
||||
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 (
|
||||
<Row onClick={onClick} gapX='2' pt={showContact ? 2 : 0}>
|
||||
<Col flexGrow={1} alignItems='flex-start'>
|
||||
{showContact && (
|
||||
<Author
|
||||
showImage
|
||||
contacts={contacts}
|
||||
ship={author}
|
||||
date={time}
|
||||
group={group}
|
||||
/>
|
||||
<Author showImage ship={author} date={time} group={group} />
|
||||
)}
|
||||
<Row width='100%' p='1' flexDirection='column'>
|
||||
<GraphNodeContent
|
||||
contacts={contacts}
|
||||
post={post}
|
||||
mod={mod}
|
||||
description={description}
|
||||
@ -235,12 +229,9 @@ export function GraphNotification(props: {
|
||||
read: boolean;
|
||||
time: number;
|
||||
timebox: BigInteger;
|
||||
associations: Associations;
|
||||
groups: Groups;
|
||||
contacts: Rolodex;
|
||||
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 { graph, group } = index;
|
||||
@ -255,6 +246,8 @@ export function GraphNotification(props: {
|
||||
return api.hark['read'](timebox, { graph: index });
|
||||
}, [api, timebox, index, read]);
|
||||
|
||||
const groups = useGroupState((state) => state.groups);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header
|
||||
@ -265,17 +258,14 @@ export function GraphNotification(props: {
|
||||
authors={authors}
|
||||
moduleIcon={icon}
|
||||
channel={graph}
|
||||
contacts={props.contacts}
|
||||
group={group}
|
||||
description={desc}
|
||||
associations={props.associations}
|
||||
/>
|
||||
<Box flexGrow={1} width='100%' pl={5} gridArea='main'>
|
||||
{_.map(contents, (content, idx) => (
|
||||
<GraphNode
|
||||
post={content}
|
||||
author={content.author}
|
||||
contacts={props.contacts}
|
||||
mod={index.module}
|
||||
time={content?.['time-sent']}
|
||||
description={index.description}
|
||||
|
@ -41,13 +41,11 @@ interface GroupNotificationProps {
|
||||
read: boolean;
|
||||
time: number;
|
||||
timebox: BigInteger;
|
||||
associations: Associations;
|
||||
contacts: Rolodex;
|
||||
api: GlobalApi;
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
@ -69,10 +67,8 @@ export function GroupNotification(props: GroupNotificationProps): ReactElement {
|
||||
time={time}
|
||||
read={read}
|
||||
group={group}
|
||||
contacts={props.contacts}
|
||||
authors={authors}
|
||||
description={desc}
|
||||
associations={associations}
|
||||
/>
|
||||
</Col>
|
||||
);
|
||||
|
@ -9,16 +9,19 @@ import { Associations, Contact, Contacts, Rolodex } from '@urbit/api';
|
||||
import { PropFunc } from '~/types/util';
|
||||
import { useShowNickname } from '~/logic/lib/util';
|
||||
import Timestamp from '~/views/components/Timestamp';
|
||||
import useContactState from '~/logic/state/contact';
|
||||
import useMetadataState from '~/logic/state/metadata';
|
||||
|
||||
const Text = (props: PropFunc<typeof Text>) => (
|
||||
<NormalText fontWeight="500" {...props} />
|
||||
);
|
||||
|
||||
function Author(props: { patp: string; contacts: Contacts; last?: boolean }): ReactElement {
|
||||
const contact: Contact | undefined = props.contacts?.[`~${props.patp}`];
|
||||
function Author(props: { patp: string; last?: boolean }): ReactElement {
|
||||
const contacts = useContactState(state => state.contacts);
|
||||
const contact: Contact | undefined = contacts?.[`~${props.patp}`];
|
||||
|
||||
const showNickname = useShowNickname(contact);
|
||||
const name = contact?.nickname || `~${props.patp}`;
|
||||
const name = showNickname ? contact.nickname : `~${props.patp}`;
|
||||
|
||||
return (
|
||||
<Text mono={!showNickname}>
|
||||
@ -33,14 +36,13 @@ export function Header(props: {
|
||||
archived?: boolean;
|
||||
channel?: string;
|
||||
group: string;
|
||||
contacts: Rolodex;
|
||||
description: string;
|
||||
moduleIcon?: string;
|
||||
time: number;
|
||||
read: boolean;
|
||||
associations: Associations;
|
||||
} & 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);
|
||||
|
||||
@ -50,7 +52,7 @@ export function Header(props: {
|
||||
f.map(([idx, p]: [string, string]) => {
|
||||
const lent = Math.min(3, authors.length);
|
||||
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 => (
|
||||
<React.Fragment>
|
||||
@ -64,11 +66,11 @@ export function Header(props: {
|
||||
|
||||
const time = moment(props.time).format('HH:mm');
|
||||
const groupTitle =
|
||||
props.associations.groups?.[props.group]?.metadata?.title;
|
||||
associations.groups?.[props.group]?.metadata?.title;
|
||||
|
||||
const app = 'graph';
|
||||
const channelTitle =
|
||||
(channel && props.associations?.[app]?.[channel]?.metadata?.title) ||
|
||||
(channel && associations?.[app]?.[channel]?.metadata?.title) ||
|
||||
channel;
|
||||
|
||||
return (
|
||||
|
@ -23,10 +23,12 @@ import GlobalApi from '~/logic/api/global';
|
||||
import { Notification } from './notification';
|
||||
import { Invites } from './invites';
|
||||
import { useLazyScroll } from '~/logic/lib/useLazyScroll';
|
||||
import useHarkState from '~/logic/state/hark';
|
||||
import useInviteState from '~/logic/state/invite';
|
||||
|
||||
type DatedTimebox = [BigInteger, Timebox];
|
||||
|
||||
function filterNotification(associations: Associations, groups: string[]) {
|
||||
function filterNotification(groups: string[]) {
|
||||
if (groups.length === 0) {
|
||||
return () => true;
|
||||
}
|
||||
@ -43,21 +45,13 @@ function filterNotification(associations: Associations, groups: string[]) {
|
||||
}
|
||||
|
||||
export default function Inbox(props: {
|
||||
notifications: Notifications;
|
||||
notificationsSize: number;
|
||||
archive: Notifications;
|
||||
groups: Groups;
|
||||
showArchive?: boolean;
|
||||
api: GlobalApi;
|
||||
associations: Associations;
|
||||
contacts: Rolodex;
|
||||
filter: string[];
|
||||
invites: InviteType;
|
||||
pendingJoin: JoinRequests;
|
||||
notificationsGroupConfig: GroupNotificationsConfig;
|
||||
notificationsGraphConfig: NotificationGraphConfig;
|
||||
}) {
|
||||
const { api, associations, invites } = props;
|
||||
const { api } = props;
|
||||
useEffect(() => {
|
||||
let seen = false;
|
||||
setTimeout(() => {
|
||||
@ -70,8 +64,11 @@ export default function Inbox(props: {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const notificationState = useHarkState(state => state.notifications);
|
||||
const archivedNotifications = useHarkState(state => state.archivedNotifications);
|
||||
|
||||
const notifications =
|
||||
Array.from(props.showArchive ? props.archive : props.notifications) || [];
|
||||
Array.from(props.showArchive ? archivedNotifications : notificationState) || [];
|
||||
|
||||
const calendar = {
|
||||
...MOMENT_CALENDAR_DATE, sameDay: function (now) {
|
||||
@ -86,7 +83,7 @@ export default function Inbox(props: {
|
||||
const notificationsByDay = f.flow(
|
||||
f.map<DatedTimebox, DatedTimebox>(([date, nots]) => [
|
||||
date,
|
||||
nots.filter(filterNotification(associations, props.filter))
|
||||
nots.filter(filterNotification(props.filter))
|
||||
]),
|
||||
f.groupBy<DatedTimebox>(([d]) => {
|
||||
const date = moment(daToUnix(d));
|
||||
@ -119,7 +116,7 @@ export default function Inbox(props: {
|
||||
|
||||
return (
|
||||
<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) => {
|
||||
const timeboxes = notificationsByDayMap.get(day)!;
|
||||
return timeboxes.length > 0 && (
|
||||
@ -127,13 +124,8 @@ export default function Inbox(props: {
|
||||
key={day}
|
||||
label={day === 'latest' ? 'Today' : moment(day).calendar(null, calendar)}
|
||||
timeboxes={timeboxes}
|
||||
contacts={props.contacts}
|
||||
archive={Boolean(props.showArchive)}
|
||||
associations={props.associations}
|
||||
api={api}
|
||||
groups={props.groups}
|
||||
graphConfig={props.notificationsGraphConfig}
|
||||
groupConfig={props.notificationsGroupConfig}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@ -165,14 +157,9 @@ function sortIndexedNotification(
|
||||
|
||||
function DaySection({
|
||||
label,
|
||||
contacts,
|
||||
groups,
|
||||
archive,
|
||||
timeboxes,
|
||||
associations,
|
||||
api,
|
||||
groupConfig,
|
||||
graphConfig
|
||||
}) {
|
||||
const lent = timeboxes.map(([,nots]) => nots.length).reduce(f.add, 0);
|
||||
if (lent === 0 || timeboxes.length === 0) {
|
||||
@ -195,14 +182,9 @@ function DaySection({
|
||||
<Box flexShrink={0} height="4px" bg="scales.black05" />
|
||||
)}
|
||||
<Notification
|
||||
graphConfig={graphConfig}
|
||||
groupConfig={groupConfig}
|
||||
api={api}
|
||||
associations={associations}
|
||||
notification={not}
|
||||
archived={archive}
|
||||
contacts={contacts}
|
||||
groups={groups}
|
||||
time={date}
|
||||
/>
|
||||
</React.Fragment>
|
||||
|
@ -7,14 +7,11 @@ import { Invites as IInvites, Associations, Invite, JoinRequests, Groups, Contac
|
||||
import GlobalApi from '~/logic/api/global';
|
||||
import { resourceAsPath, alphabeticalOrder } from '~/logic/lib/util';
|
||||
import InviteItem from '~/views/components/Invite';
|
||||
import useInviteState from '~/logic/state/invite';
|
||||
import useGroupState from '~/logic/state/group';
|
||||
|
||||
interface InvitesProps {
|
||||
api: GlobalApi;
|
||||
invites: IInvites;
|
||||
groups: Groups;
|
||||
contacts: Contacts;
|
||||
associations: Associations;
|
||||
pendingJoin: JoinRequests;
|
||||
}
|
||||
|
||||
interface InviteRef {
|
||||
@ -24,7 +21,9 @@ interface InviteRef {
|
||||
}
|
||||
|
||||
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 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 } =
|
||||
{ ..._.keyBy(inviteArr, ({ invite }) => resourceAsPath(invite.resource)), ...props.pendingJoin };
|
||||
{ ..._.keyBy(inviteArr, ({ invite }) => resourceAsPath(invite.resource)), ...pendingJoin };
|
||||
|
||||
return (
|
||||
<Col
|
||||
@ -50,33 +49,27 @@ export function Invites(props: InvitesProps): ReactElement {
|
||||
.sort(alphabeticalOrder)
|
||||
.map((resource) => {
|
||||
const inviteOrStatus = invitesAndStatus[resource];
|
||||
if(typeof inviteOrStatus === 'string') {
|
||||
const join = pendingJoin[resource];
|
||||
if(typeof inviteOrStatus === 'string') {
|
||||
return (
|
||||
<InviteItem
|
||||
key={resource}
|
||||
contacts={props.contacts}
|
||||
groups={props.groups}
|
||||
associations={props.associations}
|
||||
resource={resource}
|
||||
pendingJoin={pendingJoin}
|
||||
pendingJoin={join}
|
||||
api={api}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
const { app, uid, invite } = inviteOrStatus;
|
||||
console.log(inviteOrStatus);
|
||||
return (
|
||||
<InviteItem
|
||||
key={resource}
|
||||
api={api}
|
||||
invite={invite}
|
||||
pendingJoin={join}
|
||||
app={app}
|
||||
uid={uid}
|
||||
pendingJoin={pendingJoin}
|
||||
resource={resource}
|
||||
contacts={props.contacts}
|
||||
groups={props.groups}
|
||||
associations={props.associations}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -18,17 +18,13 @@ import { GroupNotification } from './group';
|
||||
import { GraphNotification } from './graph';
|
||||
import { BigInteger } from 'big-integer';
|
||||
import { useHovering } from '~/logic/lib/util';
|
||||
import useHarkState from '~/logic/state/hark';
|
||||
|
||||
interface NotificationProps {
|
||||
notification: IndexedNotification;
|
||||
time: BigInteger;
|
||||
associations: Associations;
|
||||
api: GlobalApi;
|
||||
archived: boolean;
|
||||
groups: Groups;
|
||||
contacts: Contacts;
|
||||
graphConfig: NotificationGraphConfig;
|
||||
groupConfig: GroupNotificationsConfig;
|
||||
}
|
||||
|
||||
function getMuted(
|
||||
@ -61,8 +57,6 @@ function NotificationWrapper(props: {
|
||||
notif: IndexedNotification;
|
||||
children: ReactNode;
|
||||
archived: boolean;
|
||||
graphConfig: NotificationGraphConfig;
|
||||
groupConfig: GroupNotificationsConfig;
|
||||
}) {
|
||||
const { api, time, notif, children } = props;
|
||||
|
||||
@ -70,10 +64,13 @@ function NotificationWrapper(props: {
|
||||
return api.hark.archive(time, notif.index);
|
||||
}, [time, notif]);
|
||||
|
||||
const groupConfig = useHarkState(state => state.notificationsGroupConfig);
|
||||
const graphConfig = useHarkState(state => state.notificationsGraphConfig);
|
||||
|
||||
const isMuted = getMuted(
|
||||
notif,
|
||||
props.groupConfig,
|
||||
props.graphConfig
|
||||
groupConfig,
|
||||
graphConfig
|
||||
);
|
||||
|
||||
const onChangeMute = useCallback(async () => {
|
||||
@ -119,8 +116,6 @@ export function Notification(props: NotificationProps) {
|
||||
notif={notification}
|
||||
time={props.time}
|
||||
api={props.api}
|
||||
graphConfig={props.graphConfig}
|
||||
groupConfig={props.groupConfig}
|
||||
>
|
||||
{children}
|
||||
</NotificationWrapper>
|
||||
@ -136,13 +131,10 @@ export function Notification(props: NotificationProps) {
|
||||
api={props.api}
|
||||
index={index}
|
||||
contents={c}
|
||||
contacts={props.contacts}
|
||||
groups={props.groups}
|
||||
read={read}
|
||||
archived={archived}
|
||||
timebox={props.time}
|
||||
time={time}
|
||||
associations={associations}
|
||||
/>
|
||||
</Wrapper>
|
||||
);
|
||||
@ -156,13 +148,10 @@ export function Notification(props: NotificationProps) {
|
||||
api={props.api}
|
||||
index={index}
|
||||
contents={c}
|
||||
contacts={props.contacts}
|
||||
groups={props.groups}
|
||||
read={read}
|
||||
timebox={props.time}
|
||||
archived={archived}
|
||||
time={time}
|
||||
associations={associations}
|
||||
/>
|
||||
</Wrapper>
|
||||
);
|
||||
|
@ -12,6 +12,8 @@ import { Dropdown } from '~/views/components/Dropdown';
|
||||
import { FormikOnBlur } from '~/views/components/FormikOnBlur';
|
||||
import GroupSearch from '~/views/components/GroupSearch';
|
||||
import { useTutorialModal } from '~/views/components/useTutorialModal';
|
||||
import useHarkState from '~/logic/state/hark';
|
||||
import useMetadataState from '~/logic/state/metadata';
|
||||
|
||||
const baseUrl = '/~notifications';
|
||||
|
||||
@ -38,6 +40,7 @@ export default function NotificationsScreen(props: any): ReactElement {
|
||||
const relativePath = (p: string) => baseUrl + p;
|
||||
|
||||
const [filter, setFilter] = useState<NotificationFilter>({ groups: [] });
|
||||
const associations = useMetadataState(state => state.associations);
|
||||
const onSubmit = async ({ groups } : NotificationFilter) => {
|
||||
setFilter({ groups });
|
||||
};
|
||||
@ -48,10 +51,11 @@ export default function NotificationsScreen(props: any): ReactElement {
|
||||
filter.groups.length === 0
|
||||
? 'All'
|
||||
: filter.groups
|
||||
.map(g => props.associations?.groups?.[g]?.metadata?.title)
|
||||
.map(g => associations.groups?.[g]?.metadata?.title)
|
||||
.join(', ');
|
||||
const anchorRef = useRef<HTMLElement | null>(null);
|
||||
useTutorialModal('notifications', true, anchorRef);
|
||||
const notificationsCount = useHarkState(state => state.notificationsCount);
|
||||
return (
|
||||
<Switch>
|
||||
<Route
|
||||
@ -61,7 +65,7 @@ export default function NotificationsScreen(props: any): ReactElement {
|
||||
return (
|
||||
<>
|
||||
<Helmet defer={false}>
|
||||
<title>{ props.notificationsCount ? `(${String(props.notificationsCount) }) `: '' }Landscape - Notifications</title>
|
||||
<title>{ notificationsCount ? `(${String(notificationsCount) }) `: '' }Landscape - Notifications</title>
|
||||
</Helmet>
|
||||
<Body>
|
||||
<Col overflowY="hidden" height="100%">
|
||||
@ -110,7 +114,6 @@ export default function NotificationsScreen(props: any): ReactElement {
|
||||
id="groups"
|
||||
label="Filter Groups"
|
||||
caption="Only show notifications from this group"
|
||||
associations={props.associations}
|
||||
/>
|
||||
</FormikOnBlur>
|
||||
</Col>
|
||||
|
@ -21,6 +21,7 @@ import { ImageInput } from '~/views/components/ImageInput';
|
||||
import { MarkdownField } from '~/views/apps/publish/components/MarkdownField';
|
||||
import { resourceFromPath } from '~/logic/lib/group';
|
||||
import GroupSearch from '~/views/components/GroupSearch';
|
||||
import useContactState from '~/logic/state/contact';
|
||||
import {
|
||||
ProfileHeader,
|
||||
ProfileControls,
|
||||
@ -48,7 +49,7 @@ const emptyContact = {
|
||||
};
|
||||
|
||||
export function ProfileHeaderImageEdit(props: any): ReactElement {
|
||||
const { contact, storage, setFieldValue, handleHideCover } = { ...props };
|
||||
const { contact, setFieldValue, handleHideCover } = props;
|
||||
const [editCover, setEditCover] = useState(false);
|
||||
const [removedCoverLabel, setRemovedCoverLabel] = useState('Remove Header');
|
||||
const handleClear = (e) => {
|
||||
@ -63,7 +64,7 @@ export function ProfileHeaderImageEdit(props: any): ReactElement {
|
||||
{contact?.cover ? (
|
||||
<div>
|
||||
{editCover ? (
|
||||
<ImageInput id='cover' storage={storage} marginTop='-8px' />
|
||||
<ImageInput id='cover' marginTop='-8px' />
|
||||
) : (
|
||||
<Row>
|
||||
<Button mr='2' onClick={() => setEditCover(true)}>
|
||||
@ -76,14 +77,15 @@ export function ProfileHeaderImageEdit(props: any): ReactElement {
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<ImageInput id='cover' storage={storage} marginTop='-8px' />
|
||||
<ImageInput id='cover' marginTop='-8px' />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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 handleHideCover = (value) => {
|
||||
@ -148,7 +150,7 @@ export function EditProfile(props: any): ReactElement {
|
||||
<Form width='100%' height='100%'>
|
||||
<ProfileHeader>
|
||||
<ProfileControls>
|
||||
<Row>
|
||||
<Row alignItems='baseline'>
|
||||
<Button
|
||||
type='submit'
|
||||
display='inline'
|
||||
@ -176,10 +178,13 @@ export function EditProfile(props: any): ReactElement {
|
||||
</Row>
|
||||
<ProfileStatus contact={contact} />
|
||||
</ProfileControls>
|
||||
<ProfileImages hideCover={hideCover} contact={contact} ship={ship}>
|
||||
<ProfileImages
|
||||
hideCover={hideCover}
|
||||
contact={contact}
|
||||
ship={ship}
|
||||
>
|
||||
<ProfileHeaderImageEdit
|
||||
contact={contact}
|
||||
storage={storage}
|
||||
setFieldValue={setFieldValue}
|
||||
handleHideCover={handleHideCover}
|
||||
/>
|
||||
@ -193,23 +198,16 @@ export function EditProfile(props: any): ReactElement {
|
||||
<ImageInput
|
||||
id='avatar'
|
||||
label='Overlay Avatar (may be hidden by other users)'
|
||||
storage={storage}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Input id='nickname' label='Custom Name' mb={3} />
|
||||
<Col width='100%'>
|
||||
<Text mb={2}>Description</Text>
|
||||
<MarkdownField id='bio' mb={3} storage={storage} />
|
||||
<MarkdownField id='bio' mb={3} />
|
||||
</Col>
|
||||
<Checkbox mb={3} id='isPublic' label='Public Profile' />
|
||||
<GroupSearch
|
||||
label='Pinned Groups'
|
||||
id='groups'
|
||||
groups={props.groups}
|
||||
associations={props.associations}
|
||||
publicOnly
|
||||
/>
|
||||
<GroupSearch label='Pinned Groups' id='groups' publicOnly />
|
||||
<AsyncButton primary loadingText='Updating...' border mt={3}>
|
||||
Submit
|
||||
</AsyncButton>
|
||||
|
@ -9,13 +9,14 @@ import { EditProfile } from './EditProfile';
|
||||
import { SetStatusBarModal } from '~/views/components/SetStatusBarModal';
|
||||
import { uxToHex } from '~/logic/lib/util';
|
||||
import { useTutorialModal } from '~/views/components/useTutorialModal';
|
||||
import useContactState from '~/logic/state/contact';
|
||||
|
||||
export function ProfileHeader(props: any): ReactElement {
|
||||
return (
|
||||
<Box
|
||||
border='1px solid'
|
||||
borderColor='lightGray'
|
||||
borderRadius='2'
|
||||
borderColor='washedGray'
|
||||
borderRadius='3'
|
||||
overflow='hidden'
|
||||
marginBottom='calc(64px + 2rem)'
|
||||
>
|
||||
@ -39,6 +40,7 @@ export function ProfileImages(props: any): ReactElement {
|
||||
src={contact.cover}
|
||||
width='100%'
|
||||
height='100%'
|
||||
referrerPolicy="no-referrer"
|
||||
style={{ objectFit: 'cover' }}
|
||||
/>
|
||||
) : (
|
||||
@ -56,6 +58,7 @@ export function ProfileImages(props: any): ReactElement {
|
||||
src={contact.avatar}
|
||||
width='100%'
|
||||
height='100%'
|
||||
referrerPolicy="no-referrer"
|
||||
style={{ objectFit: 'cover' }}
|
||||
/>
|
||||
) : (
|
||||
@ -64,7 +67,7 @@ export function ProfileImages(props: any): ReactElement {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row ref={anchorRef} width='100%' height='300px' position='relative'>
|
||||
<Row ref={anchorRef} width='100%' height='400px' position='relative'>
|
||||
{cover}
|
||||
<Center position='absolute' width='100%' height='100%'>
|
||||
{props.children}
|
||||
@ -73,7 +76,7 @@ export function ProfileImages(props: any): ReactElement {
|
||||
<Box
|
||||
height='128px'
|
||||
width='128px'
|
||||
borderRadius='2'
|
||||
borderRadius='3'
|
||||
overflow='hidden'
|
||||
position='absolute'
|
||||
left='50%'
|
||||
@ -109,6 +112,7 @@ export function ProfileStatus(props: any): ReactElement {
|
||||
display='inline-block'
|
||||
verticalAlign='middle'
|
||||
color='gray'
|
||||
title={contact?.status ?? ''}
|
||||
>
|
||||
{contact?.status ?? ''}
|
||||
</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 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 formRef = useRef(null);
|
||||
|
||||
@ -183,21 +189,14 @@ export function Profile(props: any): ReactElement {
|
||||
<EditProfile
|
||||
ship={ship}
|
||||
contact={contact}
|
||||
storage={props.storage}
|
||||
api={props.api}
|
||||
groups={props.groups}
|
||||
associations={props.associations}
|
||||
isPublic={isPublic}
|
||||
/>
|
||||
) : (
|
||||
<ViewProfile
|
||||
nacked={nacked}
|
||||
ship={ship}
|
||||
contact={contact}
|
||||
isPublic={isPublic}
|
||||
api={props.api}
|
||||
groups={props.groups}
|
||||
associations={props.associations}
|
||||
contact={contact}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
@ -2,17 +2,19 @@ import React, {
|
||||
useState,
|
||||
useCallback,
|
||||
useEffect,
|
||||
ChangeEvent
|
||||
ChangeEvent,
|
||||
useRef
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
Row,
|
||||
Text,
|
||||
Button,
|
||||
StatelessTextInput as Input
|
||||
} from '@tlon/indigo-react';
|
||||
|
||||
export function SetStatus(props: any) {
|
||||
const { contact, ship, api, callback } = props;
|
||||
const inputRef = useRef(null);
|
||||
const [_status, setStatus] = useState('');
|
||||
const onStatusChange = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
@ -27,19 +29,20 @@ export function SetStatus(props: any) {
|
||||
|
||||
const editStatus = () => {
|
||||
api.contacts.edit(ship, { status: _status });
|
||||
|
||||
inputRef.current.blur();
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Row width="100%" my={3}>
|
||||
<Row width='100%' my={3}>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
onChange={onStatusChange}
|
||||
value={_status}
|
||||
autocomplete="off"
|
||||
width="75%"
|
||||
autocomplete='off'
|
||||
width='75%'
|
||||
mr={2}
|
||||
onKeyPress={(evt) => {
|
||||
if (evt.key === 'Enter') {
|
||||
@ -47,16 +50,9 @@ export function SetStatus(props: any) {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
primary
|
||||
color="white"
|
||||
ml={2}
|
||||
width="25%"
|
||||
onClick={editStatus}
|
||||
>
|
||||
<Button primary color='white' ml={2} width='25%' onClick={editStatus}>
|
||||
Set Status
|
||||
</Button>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { ReactElement } from 'react';
|
||||
import _ from 'lodash';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Center, Box, Text, Row, Col } from '@tlon/indigo-react';
|
||||
@ -15,11 +15,13 @@ import {
|
||||
ProfileStatus,
|
||||
ProfileImages
|
||||
} from './Profile';
|
||||
import useContactState from '~/logic/state/contact';
|
||||
|
||||
export function ViewProfile(props: any) {
|
||||
const history = useHistory();
|
||||
export function ViewProfile(props: any): ReactElement {
|
||||
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 (
|
||||
<>
|
||||
@ -37,7 +39,7 @@ export function ViewProfile(props: any) {
|
||||
</ProfileHeader>
|
||||
<Row pb={2} alignItems='center' width='100%'>
|
||||
<Center width='100%'>
|
||||
<Text>
|
||||
<Text fontWeight='500'>
|
||||
{!hideNicknames && contact?.nickname ? contact.nickname : ''}
|
||||
</Text>
|
||||
</Center>
|
||||
@ -49,7 +51,7 @@ export function ViewProfile(props: any) {
|
||||
</Text>
|
||||
</Center>
|
||||
</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'>
|
||||
<RichText width='100%' disableRemoteContent>
|
||||
{contact?.bio ? contact.bio : ''}
|
||||
@ -64,8 +66,6 @@ export function ViewProfile(props: any) {
|
||||
<GroupLink
|
||||
api={api}
|
||||
resource={g}
|
||||
groups={groups}
|
||||
associations={associations}
|
||||
measure={() => {}}
|
||||
/>
|
||||
))}
|
||||
|
@ -4,16 +4,19 @@ import Helmet from 'react-helmet';
|
||||
|
||||
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) {
|
||||
const { dark } = props;
|
||||
const contacts = useContactState(state => state.contacts);
|
||||
const notificationsCount = useHarkState(state => state.notificationsCount);
|
||||
return (
|
||||
<>
|
||||
<Helmet defer={false}>
|
||||
<title>
|
||||
{props.notificationsCount
|
||||
? `(${String(props.notificationsCount)}) `
|
||||
{notificationsCount
|
||||
? `(${String(notificationsCount)}) `
|
||||
: ''}
|
||||
Landscape - Profile
|
||||
</title>
|
||||
@ -23,8 +26,7 @@ export default function ProfileScreen(props: any) {
|
||||
render={({ match }) => {
|
||||
const ship = match.params.ship;
|
||||
const isEdit = match.url.includes('edit');
|
||||
const isPublic = props.isContactPublic;
|
||||
const contact = props.contacts?.[ship];
|
||||
const contact = contacts?.[ship];
|
||||
|
||||
return (
|
||||
<Box height='100%' px={[0, 3]} pb={[0, 3]} borderRadius={2}>
|
||||
@ -41,15 +43,10 @@ export default function ProfileScreen(props: any) {
|
||||
<Box>
|
||||
<Profile
|
||||
ship={ship}
|
||||
hasLoaded={Object.keys(props.contacts).length !== 0}
|
||||
associations={props.associations}
|
||||
groups={props.groups}
|
||||
hasLoaded={Object.keys(contacts).length !== 0}
|
||||
contact={contact}
|
||||
api={props.api}
|
||||
storage={props.storage}
|
||||
isEdit={isEdit}
|
||||
isPublic={isPublic}
|
||||
nackedContacts={props.nackedContacts}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
@ -24,18 +24,12 @@ export function PublishResource(props: PublishResourceProps) {
|
||||
api={api}
|
||||
ship={ship}
|
||||
book={book}
|
||||
contacts={props.contacts}
|
||||
groups={props.groups}
|
||||
associations={props.associations}
|
||||
association={association}
|
||||
rootUrl={baseUrl}
|
||||
baseUrl={`${baseUrl}/resource/publish/ship/${ship}/${book}`}
|
||||
history={props.history}
|
||||
match={props.match}
|
||||
location={props.location}
|
||||
unreads={props.unreads}
|
||||
graphs={props.graphs}
|
||||
storage={props.storage}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
@ -9,7 +9,6 @@ import { PostFormSchema, PostForm } from './NoteForm';
|
||||
import GlobalApi from '~/logic/api/global';
|
||||
import { getLatestRevision, editPost } from '~/logic/lib/publish';
|
||||
import { useWaitForProps } from '~/logic/lib/useWaitForProps';
|
||||
import { StorageState } from '~/types';
|
||||
|
||||
interface EditPostProps {
|
||||
ship: string;
|
||||
@ -17,11 +16,10 @@ interface EditPostProps {
|
||||
note: GraphNode;
|
||||
api: GlobalApi;
|
||||
book: string;
|
||||
storage: StorageState;
|
||||
}
|
||||
|
||||
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 location = useLocation();
|
||||
|
||||
@ -58,7 +56,6 @@ export function EditPost(props: EditPostProps & RouteComponentProps): ReactEleme
|
||||
cancel
|
||||
history={history}
|
||||
onSubmit={onSubmit}
|
||||
storage={storage}
|
||||
submitLabel="Update"
|
||||
loadingText="Updating..."
|
||||
/>
|
||||
|
@ -28,7 +28,6 @@ interface MarkdownEditorProps {
|
||||
value: string;
|
||||
onChange: (s: string) => void;
|
||||
onBlur?: (e: any) => void;
|
||||
storage: StorageState;
|
||||
}
|
||||
|
||||
const PromptIfDirty = () => {
|
||||
@ -74,7 +73,7 @@ export function MarkdownEditor(
|
||||
[onBlur]
|
||||
);
|
||||
|
||||
const { uploadDefault, canUpload } = useStorage(props.storage);
|
||||
const { uploadDefault, canUpload } = useStorage();
|
||||
|
||||
const onFileDrag = useCallback(
|
||||
async (files: FileList | File[], e: DragEvent) => {
|
||||
|
@ -6,7 +6,6 @@ import { MarkdownEditor } from './MarkdownEditor';
|
||||
|
||||
export const MarkdownField = ({
|
||||
id,
|
||||
storage,
|
||||
...rest
|
||||
}: { id: string } & Parameters<typeof Box>[0]) => {
|
||||
const [{ value, onBlur }, { error, touched }, { setValue }] = useField(id);
|
||||
@ -36,7 +35,6 @@ export const MarkdownField = ({
|
||||
onBlur={handleBlur}
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
storage={storage}
|
||||
/>
|
||||
<ErrorLabel mt="2" hasError={Boolean(error && touched)}>
|
||||
{error}
|
||||
|
@ -22,7 +22,6 @@ interface MetadataFormProps {
|
||||
host: string;
|
||||
book: string;
|
||||
association: Association;
|
||||
contacts: Contacts;
|
||||
api: GlobalApi;
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 bigInt from 'big-integer';
|
||||
|
||||
@ -9,6 +9,7 @@ import { Comments } from '~/views/components/Comments';
|
||||
import { NoteNavigation } from './NoteNavigation';
|
||||
import GlobalApi from '~/logic/api/global';
|
||||
import { getLatestRevision, getComments } from '~/logic/lib/publish';
|
||||
import { roleForShip } from '~/logic/lib/group';
|
||||
import Author from '~/views/components/Author';
|
||||
import { Contacts, GraphNode, Graph, Association, Unreads, Group } from '@urbit/api';
|
||||
|
||||
@ -16,10 +17,8 @@ interface NoteProps {
|
||||
ship: string;
|
||||
book: string;
|
||||
note: GraphNode;
|
||||
unreads: Unreads;
|
||||
association: Association;
|
||||
notebook: Graph;
|
||||
contacts: Contacts;
|
||||
api: GlobalApi;
|
||||
rootUrl: string;
|
||||
baseUrl: string;
|
||||
@ -29,7 +28,7 @@ interface NoteProps {
|
||||
export function Note(props: NoteProps & RouteComponentProps) {
|
||||
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 renderers = {
|
||||
@ -56,29 +55,37 @@ export function Note(props: NoteProps & RouteComponentProps) {
|
||||
api.hark.markEachAsRead(props.association, '/',`/${index[1]}/1/1`, 'note', 'publish');
|
||||
}, [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) {
|
||||
adminLinks = (
|
||||
<Box display="inline-block" verticalAlign="middle">
|
||||
<Link to={`${baseUrl}/edit`}>
|
||||
<Text
|
||||
color="green"
|
||||
ml={2}
|
||||
adminLinks.push(
|
||||
<Link
|
||||
style={{ 'display': 'inline-block' }}
|
||||
to={`${baseUrl}/edit`}
|
||||
>
|
||||
Update
|
||||
</Text>
|
||||
<Text
|
||||
color="blue"
|
||||
ml={2}
|
||||
>
|
||||
Update
|
||||
</Text>
|
||||
</Link>
|
||||
<Text
|
||||
color="red"
|
||||
ml={2}
|
||||
onClick={deletePost}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
Delete
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
)
|
||||
};
|
||||
|
||||
if (window.ship === note?.post?.author || ourRole === "admin") {
|
||||
adminLinks.push(
|
||||
<Text
|
||||
color="red"
|
||||
display='inline-block'
|
||||
ml={2}
|
||||
onClick={deletePost}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
Delete
|
||||
</Text>
|
||||
)
|
||||
};
|
||||
|
||||
const windowRef = React.useRef(null);
|
||||
useEffect(() => {
|
||||
@ -105,14 +112,15 @@ export function Note(props: NoteProps & RouteComponentProps) {
|
||||
</Link>
|
||||
<Col>
|
||||
<Text display="block" mb={2}>{title || ''}</Text>
|
||||
<Box display="flex">
|
||||
<Row alignItems="center">
|
||||
<Author
|
||||
showImage
|
||||
ship={post?.author}
|
||||
contacts={contacts}
|
||||
date={post?.['time-sent']}
|
||||
group={group}
|
||||
/>
|
||||
<Text ml={2}>{adminLinks}</Text>
|
||||
</Box>
|
||||
<Text ml={1}>{adminLinks}</Text>
|
||||
</Row>
|
||||
</Col>
|
||||
<Box color="black" className="md" style={{ overflowWrap: 'break-word', overflow: 'hidden' }}>
|
||||
<ReactMarkdown source={body} linkTarget={'_blank'} renderers={renderers} />
|
||||
@ -126,9 +134,7 @@ export function Note(props: NoteProps & RouteComponentProps) {
|
||||
<Comments
|
||||
ship={ship}
|
||||
name={props.book}
|
||||
unreads={props.unreads}
|
||||
comments={comments}
|
||||
contacts={props.contacts}
|
||||
association={props.association}
|
||||
api={props.api}
|
||||
baseUrl={baseUrl}
|
||||
|
@ -9,7 +9,6 @@ import {
|
||||
import { AsyncButton } from '../../../components/AsyncButton';
|
||||
import { Formik, Form, FormikHelpers } from 'formik';
|
||||
import { MarkdownField } from './MarkdownField';
|
||||
import { StorageState } from '~/types';
|
||||
|
||||
interface PostFormProps {
|
||||
initial: PostFormSchema;
|
||||
@ -21,7 +20,6 @@ interface PostFormProps {
|
||||
) => Promise<any>;
|
||||
submitLabel: string;
|
||||
loadingText: string;
|
||||
storage: StorageState;
|
||||
}
|
||||
|
||||
const formSchema = Yup.object({
|
||||
@ -35,7 +33,7 @@ export interface PostFormSchema {
|
||||
}
|
||||
|
||||
export function PostForm(props: PostFormProps) {
|
||||
const { initial, onSubmit, submitLabel, loadingText, storage, cancel, history } = props;
|
||||
const { initial, onSubmit, submitLabel, loadingText, cancel, history } = props;
|
||||
|
||||
return (
|
||||
<Col width="100%" height="100%" p={[2, 4]}>
|
||||
@ -67,7 +65,7 @@ export function PostForm(props: PostFormProps) {
|
||||
>Cancel</Button>}
|
||||
</Row>
|
||||
</Row>
|
||||
<MarkdownField flexGrow={1} id="body" storage={storage} />
|
||||
<MarkdownField flexGrow={1} id="body" />
|
||||
</Form>
|
||||
</Formik>
|
||||
</Col>
|
||||
|
@ -12,17 +12,14 @@ import {
|
||||
getSnippet
|
||||
} from '~/logic/lib/publish';
|
||||
import { Unreads } from '@urbit/api';
|
||||
import GlobalApi from '~/logic/api/global';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import useHarkState from '~/logic/state/hark';
|
||||
|
||||
interface NotePreviewProps {
|
||||
host: string;
|
||||
book: string;
|
||||
node: GraphNode;
|
||||
baseUrl: string;
|
||||
unreads: Unreads;
|
||||
contacts: Contacts;
|
||||
api: GlobalApi;
|
||||
group: Group;
|
||||
}
|
||||
|
||||
@ -31,7 +28,7 @@ const WrappedBox = styled(Box)`
|
||||
`;
|
||||
|
||||
export function NotePreview(props: NotePreviewProps) {
|
||||
const { node, contacts, group } = props;
|
||||
const { node, group } = props;
|
||||
const { post } = node;
|
||||
if (!post) {
|
||||
return null;
|
||||
@ -43,11 +40,12 @@ export function NotePreview(props: NotePreviewProps) {
|
||||
|
||||
const [rev, title, body, content] = getLatestRevision(node);
|
||||
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 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';
|
||||
|
||||
@ -92,12 +90,10 @@ export function NotePreview(props: NotePreviewProps) {
|
||||
<Row minWidth='0' flexShrink={0} width="100%" justifyContent="space-between" py={3} bg="white">
|
||||
<Author
|
||||
showImage
|
||||
contacts={contacts}
|
||||
ship={post?.author}
|
||||
date={post?.['time-sent']}
|
||||
group={group}
|
||||
unread={isUnread}
|
||||
api={props.api}
|
||||
/>
|
||||
<Box ml="auto" mr={1}>
|
||||
<Link to={url}>
|
||||
|
@ -15,13 +15,11 @@ interface NoteRoutesProps {
|
||||
note: GraphNode;
|
||||
noteId: number;
|
||||
notebook: Graph;
|
||||
contacts: Contacts;
|
||||
api: GlobalApi;
|
||||
association: Association;
|
||||
baseUrl?: string;
|
||||
rootUrl?: string;
|
||||
group: Group;
|
||||
storage: StorageState;
|
||||
group: Group
|
||||
}
|
||||
|
||||
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 { NotebookPosts } from './NotebookPosts';
|
||||
import GlobalApi from '~/logic/api/global';
|
||||
import { useShowNickname } from '~/logic/lib/util';
|
||||
import useContactState from '~/logic/state/contact';
|
||||
import useGroupState from '~/logic/state/group';
|
||||
|
||||
interface NotebookProps {
|
||||
api: GlobalApi;
|
||||
ship: string;
|
||||
book: string;
|
||||
graph: Graph;
|
||||
association: Association;
|
||||
associations: Associations;
|
||||
contacts: Rolodex;
|
||||
groups: Groups;
|
||||
baseUrl: string;
|
||||
rootUrl: string;
|
||||
unreads: Unreads;
|
||||
}
|
||||
|
||||
export function Notebook(props: NotebookProps & RouteComponentProps): ReactElement {
|
||||
export function Notebook(props: NotebookProps & RouteComponentProps): ReactElement | null {
|
||||
const {
|
||||
ship,
|
||||
book,
|
||||
contacts,
|
||||
groups,
|
||||
association,
|
||||
graph
|
||||
} = props;
|
||||
|
||||
const group = groups[association?.group];
|
||||
if (!group) {
|
||||
return null; // Waiting on groups to populate
|
||||
}
|
||||
const groups = useGroupState(state => state.groups);
|
||||
const contacts = useContactState(state => state.contacts);
|
||||
|
||||
const group = groups[association?.group];
|
||||
const relativePath = (p: string) => props.baseUrl + p;
|
||||
|
||||
const contact = contacts?.[`~${ship}`];
|
||||
console.log(association.resource);
|
||||
|
||||
const showNickname = useShowNickname(contact);
|
||||
|
||||
if (!group) {
|
||||
return null; // Waiting on groups to populate
|
||||
}
|
||||
|
||||
return (
|
||||
<Col gapY="4" pt={4} mx="auto" px={3} maxWidth="768px">
|
||||
<Row justifyContent="space-between">
|
||||
@ -60,10 +57,7 @@ export function Notebook(props: NotebookProps & RouteComponentProps): ReactEleme
|
||||
graph={graph}
|
||||
host={ship}
|
||||
book={book}
|
||||
contacts={contacts}
|
||||
unreads={props.unreads}
|
||||
baseUrl={props.baseUrl}
|
||||
api={props.api}
|
||||
group={group}
|
||||
/>
|
||||
</Col>
|
||||
|
@ -2,21 +2,20 @@ import React, { Component } from 'react';
|
||||
import { Col } from '@tlon/indigo-react';
|
||||
import { NotePreview } from './NotePreview';
|
||||
import { Contacts, Graph, Unreads, Group } from '@urbit/api';
|
||||
import useContactState from '~/logic/state/contact';
|
||||
|
||||
interface NotebookPostsProps {
|
||||
contacts: Contacts;
|
||||
graph: Graph;
|
||||
host: string;
|
||||
book: string;
|
||||
baseUrl: string;
|
||||
unreads: Unreads;
|
||||
hideAvatars?: boolean;
|
||||
hideNicknames?: boolean;
|
||||
api: GlobalApi;
|
||||
group: Group;
|
||||
}
|
||||
|
||||
export function NotebookPosts(props: NotebookPostsProps) {
|
||||
const contacts = useContactState(state => state.contacts);
|
||||
return (
|
||||
<Col>
|
||||
{Array.from(props.graph || []).map(
|
||||
@ -26,12 +25,9 @@ export function NotebookPosts(props: NotebookPostsProps) {
|
||||
key={date.toString()}
|
||||
host={props.host}
|
||||
book={props.book}
|
||||
unreads={props.unreads}
|
||||
contact={props.contacts[`~${node.post.author}`]}
|
||||
contacts={props.contacts}
|
||||
contact={contacts[`~${node.post.author}`]}
|
||||
node={node}
|
||||
baseUrl={props.baseUrl}
|
||||
api={props.api}
|
||||
group={props.group}
|
||||
/>
|
||||
)
|
||||
|
@ -17,32 +17,32 @@ import bigInt from 'big-integer';
|
||||
import Notebook from './Notebook';
|
||||
import NewPost from './new-post';
|
||||
import { NoteRoutes } from './NoteRoutes';
|
||||
import useGraphState from '~/logic/state/graph';
|
||||
import useGroupState from '~/logic/state/group';
|
||||
|
||||
interface NotebookRoutesProps {
|
||||
api: GlobalApi;
|
||||
ship: string;
|
||||
book: string;
|
||||
graphs: Graphs;
|
||||
unreads: Unreads;
|
||||
contacts: Rolodex;
|
||||
groups: Groups;
|
||||
baseUrl: string;
|
||||
rootUrl: string;
|
||||
association: Association;
|
||||
associations: Associations;
|
||||
storage: StorageState;
|
||||
}
|
||||
|
||||
export function NotebookRoutes(
|
||||
props: NotebookRoutesProps & RouteComponentProps
|
||||
) {
|
||||
const { ship, book, api, contacts, baseUrl, rootUrl, groups } = props;
|
||||
const { ship, book, api, baseUrl, rootUrl } = props;
|
||||
|
||||
useEffect(() => {
|
||||
ship && book && api.graph.getGraph(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];
|
||||
|
||||
@ -59,7 +59,6 @@ export function NotebookRoutes(
|
||||
return <Notebook
|
||||
{...props}
|
||||
graph={graph}
|
||||
contacts={contacts}
|
||||
association={props.association}
|
||||
rootUrl={rootUrl}
|
||||
baseUrl={baseUrl}
|
||||
@ -77,7 +76,6 @@ export function NotebookRoutes(
|
||||
association={props.association}
|
||||
graph={graph}
|
||||
baseUrl={baseUrl}
|
||||
storage={props.storage}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -104,12 +102,9 @@ export function NotebookRoutes(
|
||||
ship={ship}
|
||||
note={note}
|
||||
notebook={graph}
|
||||
unreads={props.unreads}
|
||||
noteId={noteIdNum}
|
||||
contacts={contacts}
|
||||
association={props.association}
|
||||
group={group}
|
||||
storage={props.storage}
|
||||
{...routeProps}
|
||||
/>
|
||||
);
|
||||
|
@ -7,7 +7,7 @@ import { AsyncButton } from '~/views/components/AsyncButton';
|
||||
|
||||
export class Writers extends Component {
|
||||
render() {
|
||||
const { association, groups, contacts, api } = this.props;
|
||||
const { association, groups, api } = this.props;
|
||||
|
||||
const resource = resourceFromPath(association?.group);
|
||||
|
||||
@ -39,8 +39,6 @@ export class Writers extends Component {
|
||||
>
|
||||
<Form>
|
||||
<ShipSearch
|
||||
groups={groups}
|
||||
contacts={contacts}
|
||||
id="ships"
|
||||
label=""
|
||||
maxLength={undefined}
|
||||
|
@ -17,7 +17,6 @@ interface NewPostProps {
|
||||
graph: Graph;
|
||||
association: Association;
|
||||
baseUrl: string;
|
||||
storage: StorageState;
|
||||
}
|
||||
|
||||
export default function NewPost(props: NewPostProps & RouteComponentProps) {
|
||||
@ -51,7 +50,6 @@ export default function NewPost(props: NewPostProps & RouteComponentProps) {
|
||||
onSubmit={onSubmit}
|
||||
submitLabel="Publish"
|
||||
loadingText="Posting..."
|
||||
storage={props.storage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -142,6 +142,10 @@
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.md ul ul {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.md h2, .md h3, .md h4, .md h5, .md p, .md a, .md ul {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
@ -4,8 +4,16 @@ import { Text } from '@tlon/indigo-react';
|
||||
|
||||
export function BackButton(props: {}) {
|
||||
return (
|
||||
<Link to="/~settings">
|
||||
<Text display={["block", "none"]} fontSize="2" fontWeight="medium">{"<- Back to System Preferences"}</Text>
|
||||
<Link to='/~settings'>
|
||||
<Text
|
||||
display={['block', 'none']}
|
||||
fontSize='2'
|
||||
fontWeight='medium'
|
||||
p={4}
|
||||
pb={0}
|
||||
>
|
||||
{'<- Back to System Preferences'}
|
||||
</Text>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
@ -20,13 +20,11 @@ export function BackgroundPicker({
|
||||
bgType,
|
||||
bgUrl,
|
||||
api,
|
||||
storage
|
||||
}: {
|
||||
bgType: BgType;
|
||||
bgUrl?: string;
|
||||
api: GlobalApi;
|
||||
storage: StorageState;
|
||||
}) {
|
||||
}): ReactElement {
|
||||
const rowSpace = { my: 0, alignItems: 'center' };
|
||||
const colProps = { my: 3, mr: 4, gapY: 1 };
|
||||
return (
|
||||
@ -39,7 +37,6 @@ export function BackgroundPicker({
|
||||
<ImageInput
|
||||
ml="5"
|
||||
api={api}
|
||||
storage={storage}
|
||||
id="bgUrl"
|
||||
placeholder="Drop or upload a file, or paste a link here"
|
||||
name="bgUrl"
|
||||
|
@ -54,10 +54,10 @@ export function CalmPrefs(props: {
|
||||
hideUnreads,
|
||||
hideGroups,
|
||||
hideUtilities,
|
||||
imageShown,
|
||||
videoShown,
|
||||
oembedShown,
|
||||
audioShown,
|
||||
imageShown: !imageShown,
|
||||
videoShown: !videoShown,
|
||||
oembedShown: !oembedShown,
|
||||
audioShown: !audioShown
|
||||
};
|
||||
|
||||
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', 'hideGroups', v.hideGroups),
|
||||
api.settings.putEntry('calm', 'hideUtilities', v.hideUtilities),
|
||||
api.settings.putEntry('remoteContentPolicy', 'imageShown', v.imageShown),
|
||||
api.settings.putEntry('remoteContentPolicy', 'videoShown', v.videoShown),
|
||||
api.settings.putEntry('remoteContentPolicy', 'audioShown', v.audioShown),
|
||||
api.settings.putEntry('remoteContentPolicy', 'oembedShown', v.oembedShown),
|
||||
api.settings.putEntry('remoteContentPolicy', 'imageShown', !v.imageShown),
|
||||
api.settings.putEntry('remoteContentPolicy', 'videoShown', !v.videoShown),
|
||||
api.settings.putEntry('remoteContentPolicy', 'audioShown', !v.audioShown),
|
||||
api.settings.putEntry('remoteContentPolicy', 'oembedShown', !v.oembedShown),
|
||||
]);
|
||||
actions.setStatus({ success: null });
|
||||
}, [api]);
|
||||
@ -80,7 +80,7 @@ export function CalmPrefs(props: {
|
||||
<Form>
|
||||
<BackButton/>
|
||||
<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">
|
||||
CalmEngine
|
||||
</Text>
|
||||
@ -115,24 +115,24 @@ export function CalmPrefs(props: {
|
||||
id="hideNicknames"
|
||||
caption="Do not show user-set nicknames"
|
||||
/>
|
||||
<Text fontWeight="medium">Remote Content</Text>
|
||||
<Text fontWeight="medium">Remote content</Text>
|
||||
<Toggle
|
||||
label="Load images"
|
||||
label="Disable images"
|
||||
id="imageShown"
|
||||
caption="Images will be replaced with an inline placeholder that must be clicked to be viewed"
|
||||
/>
|
||||
<Toggle
|
||||
label="Load audio files"
|
||||
label="Disable audio files"
|
||||
id="audioShown"
|
||||
caption="Audio content will be replaced with an inline placeholder that must be clicked to be viewed"
|
||||
/>
|
||||
<Toggle
|
||||
label="Load video files"
|
||||
label="Disable video files"
|
||||
id="videoShown"
|
||||
caption="Video content will be replaced with an inline placeholder that must be clicked to be viewed"
|
||||
/>
|
||||
<Toggle
|
||||
label="Load embedded content"
|
||||
label="Disable embedded content"
|
||||
id="oembedShown"
|
||||
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 {
|
||||
api: GlobalApi;
|
||||
storage: StorageState;
|
||||
}
|
||||
|
||||
const settingsSel = selectSettingsState(["display"]);
|
||||
|
||||
export default function DisplayForm(props: DisplayFormProps) {
|
||||
const { api, storage } = props;
|
||||
const { api } = props;
|
||||
|
||||
const {
|
||||
display: {
|
||||
@ -108,7 +107,6 @@ export default function DisplayForm(props: DisplayFormProps) {
|
||||
bgType={props.values.bgType}
|
||||
bgUrl={props.values.bgUrl}
|
||||
api={api}
|
||||
storage={storage}
|
||||
/>
|
||||
<Label>Theme</Label>
|
||||
<Radio name="theme" id="light" label="Light"/>
|
||||
|
@ -4,11 +4,12 @@ import {
|
||||
Text,
|
||||
ManagedToggleSwitchField as Toggle,
|
||||
} from "@tlon/indigo-react";
|
||||
import { Form, FormikHelpers } from "formik";
|
||||
import { FormikOnBlur } from "~/views/components/FormikOnBlur";
|
||||
import { Formik, Form, FormikHelpers } from "formik";
|
||||
import { BackButton } from "./BackButton";
|
||||
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 {
|
||||
mentions: boolean;
|
||||
@ -18,10 +19,10 @@ interface FormSchema {
|
||||
|
||||
export function NotificationPreferences(props: {
|
||||
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 = {
|
||||
mentions: graphConfig.mentions,
|
||||
dnd: dnd,
|
||||
@ -43,12 +44,11 @@ export function NotificationPreferences(props: {
|
||||
|
||||
await Promise.all(promises);
|
||||
actions.setStatus({ success: null });
|
||||
actions.resetForm({ values: initialValues });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
actions.setStatus({ error: e.message });
|
||||
}
|
||||
}, [api]);
|
||||
}, [api, graphConfig, dnd]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -63,7 +63,7 @@ export function NotificationPreferences(props: {
|
||||
messaging
|
||||
</Text>
|
||||
</Col>
|
||||
<FormikOnBlur initialValues={initialValues} onSubmit={onSubmit}>
|
||||
<Formik initialValues={initialValues} onSubmit={onSubmit}>
|
||||
<Form>
|
||||
<Col gapY="4">
|
||||
<Toggle
|
||||
@ -81,9 +81,12 @@ export function NotificationPreferences(props: {
|
||||
id="mentions"
|
||||
caption="Notify me if someone mentions my @p in a channel I've joined"
|
||||
/>
|
||||
<AsyncButton primary width="fit-content">
|
||||
Save
|
||||
</AsyncButton>
|
||||
</Col>
|
||||
</Form>
|
||||
</FormikOnBlur>
|
||||
</Formik>
|
||||
</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 { Formik } from 'formik';
|
||||
import { Formik, FormikHelpers } from 'formik';
|
||||
|
||||
import {
|
||||
ManagedTextInputField as Input,
|
||||
@ -10,12 +10,15 @@ import {
|
||||
Col,
|
||||
Anchor
|
||||
} from '@tlon/indigo-react';
|
||||
import { AsyncButton } from "~/views/components/AsyncButton";
|
||||
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import { BucketList } from "./BucketList";
|
||||
import GlobalApi from '~/logic/api/global';
|
||||
import { BucketList } from './BucketList';
|
||||
import { S3State } from '~/types/s3-update';
|
||||
import useS3State from '~/logic/state/storage';
|
||||
import { BackButton } from './BackButton';
|
||||
import {StorageState} from '~/types';
|
||||
import { StorageState } from '~/types';
|
||||
import useStorageState from '~/logic/state/storage';
|
||||
|
||||
interface FormSchema {
|
||||
s3bucket: string;
|
||||
@ -27,33 +30,33 @@ interface FormSchema {
|
||||
|
||||
interface S3FormProps {
|
||||
api: GlobalApi;
|
||||
storage: StorageState;
|
||||
}
|
||||
|
||||
export default function S3Form(props: S3FormProps): ReactElement {
|
||||
const { api, storage } = props;
|
||||
const { s3 } = storage;
|
||||
const { api } = props;
|
||||
const s3 = useStorageState((state) => state.s3);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(values: FormSchema) => {
|
||||
const onSubmit = useCallback(async (values: FormSchema, actions: FormikHelpers<FormSchema>) => {
|
||||
if (values.s3secretAccessKey !== s3.credentials?.secretAccessKey) {
|
||||
api.s3.setSecretAccessKey(values.s3secretAccessKey);
|
||||
await api.s3.setSecretAccessKey(values.s3secretAccessKey);
|
||||
}
|
||||
|
||||
if (values.s3endpoint !== s3.credentials?.endpoint) {
|
||||
api.s3.setEndpoint(values.s3endpoint);
|
||||
await api.s3.setEndpoint(values.s3endpoint);
|
||||
}
|
||||
|
||||
if (values.s3accessKeyId !== s3.credentials?.accessKeyId) {
|
||||
api.s3.setAccessKeyId(values.s3accessKeyId);
|
||||
await api.s3.setAccessKeyId(values.s3accessKeyId);
|
||||
}
|
||||
actions.setStatus({ success: null });
|
||||
},
|
||||
[api, s3]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Col p="5" pt="4" borderBottom="1" borderBottomColor="washedGray">
|
||||
<BackButton />
|
||||
<Col p='5' pt='4' borderBottom='1' borderBottomColor='washedGray'>
|
||||
<Formik
|
||||
initialValues={
|
||||
{
|
||||
@ -67,42 +70,42 @@ export default function S3Form(props: S3FormProps): ReactElement {
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<Form>
|
||||
<BackButton/>
|
||||
<Col maxWidth="600px" gapY="5">
|
||||
<Col gapY="1" mt="0">
|
||||
<Text color="black" fontSize={2} fontWeight="medium">
|
||||
<Col maxWidth='600px' gapY='5'>
|
||||
<Col gapY='1' mt='0'>
|
||||
<Text color='black' fontSize={2} fontWeight='medium'>
|
||||
S3 Storage Setup
|
||||
</Text>
|
||||
<Text gray>
|
||||
Store credentials for your S3 object storage buckets on your
|
||||
Urbit ship, and upload media freely to various modules.
|
||||
<Anchor
|
||||
target="_blank"
|
||||
target='_blank'
|
||||
style={{ textDecoration: 'none' }}
|
||||
borderBottom="1"
|
||||
ml="1"
|
||||
href="https://urbit.org/using/operations/using-your-ship/#bucket-setup">
|
||||
borderBottom='1'
|
||||
ml='1'
|
||||
href='https://urbit.org/using/operations/using-your-ship/#bucket-setup'
|
||||
>
|
||||
Learn more
|
||||
</Anchor>
|
||||
</Text>
|
||||
</Col>
|
||||
<Input label="Endpoint" id="s3endpoint" />
|
||||
<Input label="Access Key ID" id="s3accessKeyId" />
|
||||
<Input label='Endpoint' id='s3endpoint' />
|
||||
<Input label='Access Key ID' id='s3accessKeyId' />
|
||||
<Input
|
||||
type="password"
|
||||
label="Secret Access Key"
|
||||
id="s3secretAccessKey"
|
||||
type='password'
|
||||
label='Secret Access Key'
|
||||
id='s3secretAccessKey'
|
||||
/>
|
||||
<Button style={{ cursor: "pointer" }} type="submit">
|
||||
<AsyncButton primary style={{ cursor: 'pointer' }} type='submit'>
|
||||
Submit
|
||||
</Button>
|
||||
</AsyncButton>
|
||||
</Col>
|
||||
</Form>
|
||||
</Formik>
|
||||
</Col>
|
||||
<Col maxWidth="600px" p="5" gapY="4">
|
||||
<Col gapY="1">
|
||||
<Text color="black" mb={4} fontSize={2} fontWeight="medium">
|
||||
<Col maxWidth='600px' p='5' gapY='4'>
|
||||
<Col gapY='1'>
|
||||
<Text color='black' mb={4} fontSize={2} fontWeight='medium'>
|
||||
S3 Buckets
|
||||
</Text>
|
||||
<Text gray>
|
||||
|
@ -7,7 +7,6 @@ import { StoreState } from "~/logic/store/type";
|
||||
import DisplayForm from "./lib/DisplayForm";
|
||||
import S3Form from "./lib/S3Form";
|
||||
import SecuritySettings from "./lib/Security";
|
||||
import RemoteContentForm from "./lib/RemoteContent";
|
||||
import { NotificationPreferences } from "./lib/NotificationPref";
|
||||
import { CalmPrefs } from "./lib/CalmPref";
|
||||
import { Link } from "react-router-dom";
|
||||
|
@ -1,35 +1,44 @@
|
||||
import React, { ReactNode } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import Helmet from "react-helmet";
|
||||
import React, { ReactNode, useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import Helmet from 'react-helmet';
|
||||
|
||||
import { Text, Box, Col, Row } from '@tlon/indigo-react';
|
||||
|
||||
import { NotificationPreferences } from "./components/lib/NotificationPref";
|
||||
import DisplayForm from "./components/lib/DisplayForm";
|
||||
import S3Form from "./components/lib/S3Form";
|
||||
import { CalmPrefs } from "./components/lib/CalmPref";
|
||||
import SecuritySettings from "./components/lib/Security";
|
||||
import { LeapSettings } from "./components/lib/LeapSettings";
|
||||
import { useHashLink } from "~/logic/lib/useHashLink";
|
||||
import { SidebarItem as BaseSidebarItem } from "~/views/landscape/components/SidebarItem";
|
||||
import { PropFunc } from "~/types";
|
||||
import { NotificationPreferences } from './components/lib/NotificationPref';
|
||||
import DisplayForm from './components/lib/DisplayForm';
|
||||
import S3Form from './components/lib/S3Form';
|
||||
import { CalmPrefs } from './components/lib/CalmPref';
|
||||
import SecuritySettings from './components/lib/Security';
|
||||
import { LeapSettings } from './components/lib/LeapSettings';
|
||||
import { useHashLink } from '~/logic/lib/useHashLink';
|
||||
import { SidebarItem as BaseSidebarItem } from '~/views/landscape/components/SidebarItem';
|
||||
import { PropFunc } from '~/types';
|
||||
import DebugPane from './components/lib/Debug';
|
||||
import useHarkState from '~/logic/state/hark';
|
||||
|
||||
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
|
||||
height="100%"
|
||||
width="100%"
|
||||
borderRadius={1}
|
||||
bg="white"
|
||||
display='grid'
|
||||
gridTemplateColumns={[
|
||||
'100%',
|
||||
'minmax(150px, 1fr) 3fr',
|
||||
'minmax(250px, 1fr) 4fr'
|
||||
]}
|
||||
gridTemplateRows='100%'
|
||||
height='100%'
|
||||
width='100%'
|
||||
borderRadius={2}
|
||||
bg='white'
|
||||
border={1}
|
||||
borderColor="washedGray"
|
||||
borderColor='washedGray'
|
||||
>
|
||||
{props.children}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
type ProvSideProps = "to" | "selected";
|
||||
type ProvSideProps = 'to' | 'selected';
|
||||
type BaseProps = PropFunc<typeof BaseSidebarItem>;
|
||||
function SidebarItem(props: { hash: string } & Omit<BaseProps, ProvSideProps>) {
|
||||
const { hash, icon, text, ...rest } = props;
|
||||
@ -54,85 +63,81 @@ function SettingsItem(props: { children: ReactNode }) {
|
||||
const { children } = props;
|
||||
|
||||
return (
|
||||
<Box borderBottom="1" borderBottomColor="washedGray">
|
||||
<Box borderBottom='1' borderBottomColor='washedGray'>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SettingsScreen(props: any) {
|
||||
|
||||
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 (
|
||||
<>
|
||||
<Helmet defer={false}>
|
||||
<title>Landscape - Settings</title>
|
||||
<title>{ notificationsCount ? `(${String(notificationsCount) }) `: '' }Landscape - Settings</title>
|
||||
</Helmet>
|
||||
<Skeleton>
|
||||
<Row height="100%" overflow="hidden">
|
||||
<Col
|
||||
height="100%"
|
||||
borderRight="1"
|
||||
borderRightColor="washedGray"
|
||||
display={hash === "" ? "flex" : ["none", "flex"]}
|
||||
minWidth="250px"
|
||||
width="100%"
|
||||
maxWidth={["100vw", "350px"]}
|
||||
>
|
||||
<Text
|
||||
display="block"
|
||||
my="4"
|
||||
mx="3"
|
||||
fontSize="2"
|
||||
fontWeight="medium"
|
||||
>
|
||||
System Preferences
|
||||
</Text>
|
||||
<Col gapY="1">
|
||||
<SidebarItem
|
||||
icon="Inbox"
|
||||
text="Notifications"
|
||||
hash="notifications"
|
||||
/>
|
||||
<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
|
||||
height='100%'
|
||||
borderRight='1'
|
||||
borderRightColor='washedGray'
|
||||
display={hash === '' ? 'flex' : ['none', 'flex']}
|
||||
width='100%'
|
||||
overflowY='auto'
|
||||
>
|
||||
<Text display='block' mt='4' mb='3' mx='3' fontSize='2' fontWeight='700'>
|
||||
System Preferences
|
||||
</Text>
|
||||
<Col>
|
||||
<SidebarItem
|
||||
icon='Inbox'
|
||||
text='Notifications'
|
||||
hash='notifications'
|
||||
/>
|
||||
<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 flexGrow={1} overflowY="auto">
|
||||
<SettingsItem>
|
||||
{hash === "notifications" && (
|
||||
<NotificationPreferences
|
||||
{...props}
|
||||
graphConfig={props.notificationsGraphConfig}
|
||||
/>
|
||||
)}
|
||||
{hash === "display" && (
|
||||
<DisplayForm storage={props.storage} api={props.api} />
|
||||
)}
|
||||
{hash === "s3" && (
|
||||
<S3Form storage={props.storage} api={props.api} />
|
||||
)}
|
||||
{hash === "leap" && (
|
||||
<LeapSettings api={props.api} />
|
||||
)}
|
||||
{hash === "calm" && (
|
||||
<CalmPrefs api={props.api} />
|
||||
)}
|
||||
{hash === "security" && (
|
||||
<SecuritySettings api={props.api} />
|
||||
)}
|
||||
</SettingsItem>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col flexGrow={1} overflowY='auto'>
|
||||
<SettingsItem>
|
||||
{hash === 'notifications' && (
|
||||
<NotificationPreferences
|
||||
{...props}
|
||||
graphConfig={props.notificationsGraphConfig}
|
||||
/>
|
||||
)}
|
||||
{hash === 'display' && <DisplayForm api={props.api} />}
|
||||
{hash === 's3' && <S3Form api={props.api} />}
|
||||
{hash === 'leap' && <LeapSettings api={props.api} />}
|
||||
{hash === 'calm' && <CalmPrefs api={props.api} />}
|
||||
{hash === 'security' && <SecuritySettings api={props.api} />}
|
||||
{hash === 'debug' && <DebugPane />}
|
||||
</SettingsItem>
|
||||
</Col>
|
||||
</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