mirror of
https://github.com/urbit/shrub.git
synced 2024-12-19 08:32:39 +03:00
Merge branch 'release/next-js' into lf/tutorial-revive
This commit is contained in:
commit
8f63b5dcf1
@ -101,7 +101,7 @@
|
|||||||
==
|
==
|
||||||
::
|
::
|
||||||
++ handle-initial
|
++ handle-initial
|
||||||
|= [rolo=rolodex:store is-public=?]
|
|= [rolo=rolodex:store *]
|
||||||
^- (quip card _state)
|
^- (quip card _state)
|
||||||
=/ our-contact (~(got by rolodex) our.bowl)
|
=/ our-contact (~(got by rolodex) our.bowl)
|
||||||
=/ diff-rolo=rolodex:store
|
=/ diff-rolo=rolodex:store
|
||||||
@ -143,7 +143,7 @@
|
|||||||
|= [=ship =edit-field:store timestamp=@da]
|
|= [=ship =edit-field:store timestamp=@da]
|
||||||
|^
|
|^
|
||||||
^- (quip card _state)
|
^- (quip card _state)
|
||||||
=/ old (~(got by rolodex) ship)
|
=/ old (fall (~(get by rolodex) ship) *contact:store)
|
||||||
?: (lte timestamp last-updated.old)
|
?: (lte timestamp last-updated.old)
|
||||||
[~ state]
|
[~ state]
|
||||||
=/ contact (edit-contact old edit-field)
|
=/ contact (edit-contact old edit-field)
|
||||||
|
BIN
pkg/arvo/app/landscape/fonts/sourcecodepro-bold.woff2
Normal file
BIN
pkg/arvo/app/landscape/fonts/sourcecodepro-bold.woff2
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -115,6 +115,18 @@
|
|||||||
=/ entry=(unit val) (~(get by bucket) key)
|
=/ entry=(unit val) (~(get by bucket) key)
|
||||||
?~ entry [~ ~]
|
?~ entry [~ ~]
|
||||||
``settings-data+!>(entry+u.entry)
|
``settings-data+!>(entry+u.entry)
|
||||||
|
::
|
||||||
|
[%x %has-bucket @ ~]
|
||||||
|
=* buc i.t.t.pax
|
||||||
|
=/ has-bucket=? (~(has by settings) buc)
|
||||||
|
``noun+!>(has-bucket)
|
||||||
|
::
|
||||||
|
[%x %has-entry @ @ ~]
|
||||||
|
=* buc i.t.t.pax
|
||||||
|
=* key i.t.t.t.pax
|
||||||
|
=/ =bucket (fall (~(get by settings) buc) ~)
|
||||||
|
=/ has-entry=? (~(has by bucket) key)
|
||||||
|
``noun+!>(has-entry)
|
||||||
==
|
==
|
||||||
::
|
::
|
||||||
++ on-agent on-agent:def
|
++ on-agent on-agent:def
|
||||||
|
@ -1,186 +1,3 @@
|
|||||||
const env = {
|
|
||||||
"browser": true,
|
|
||||||
"es6": true,
|
|
||||||
"node": true
|
|
||||||
};
|
|
||||||
|
|
||||||
const rules = {
|
|
||||||
"array-bracket-spacing": ["error", "never"],
|
|
||||||
"arrow-parens": [
|
|
||||||
"error",
|
|
||||||
"as-needed",
|
|
||||||
{
|
|
||||||
"requireForBlockBody": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"arrow-spacing": "error",
|
|
||||||
"block-spacing": ["error", "always"],
|
|
||||||
"brace-style": ["error", "1tbs"],
|
|
||||||
"camelcase": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"properties": "never"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"comma-dangle": ["error", "never"],
|
|
||||||
"eol-last": ["error", "always"],
|
|
||||||
"func-name-matching": "error",
|
|
||||||
"indent": [
|
|
||||||
"off",
|
|
||||||
2,
|
|
||||||
{
|
|
||||||
"ArrayExpression": "off",
|
|
||||||
"SwitchCase": 1,
|
|
||||||
"CallExpression": {
|
|
||||||
"arguments": "off"
|
|
||||||
},
|
|
||||||
"FunctionDeclaration": {
|
|
||||||
"parameters": "off"
|
|
||||||
},
|
|
||||||
"FunctionExpression": {
|
|
||||||
"parameters": "off"
|
|
||||||
},
|
|
||||||
"MemberExpression": "off",
|
|
||||||
"ObjectExpression": "off",
|
|
||||||
"ImportDeclaration": "off"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"handle-callback-err": "off",
|
|
||||||
"linebreak-style": ["error", "unix"],
|
|
||||||
"max-lines": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"max": 300,
|
|
||||||
"skipBlankLines": true,
|
|
||||||
"skipComments": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"max-lines-per-function": [
|
|
||||||
"warn",
|
|
||||||
{
|
|
||||||
"skipBlankLines": true,
|
|
||||||
"skipComments": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"max-statements-per-line": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"max": 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"new-cap": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"newIsCap": true,
|
|
||||||
"capIsNew": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"new-parens": "error",
|
|
||||||
"no-buffer-constructor": "error",
|
|
||||||
"no-console": "off",
|
|
||||||
"no-extra-semi": "off",
|
|
||||||
"no-fallthrough": "off",
|
|
||||||
"no-func-assign": "off",
|
|
||||||
"no-implicit-coercion": "error",
|
|
||||||
"no-multi-assign": "error",
|
|
||||||
"no-multiple-empty-lines": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"max": 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"no-nested-ternary": "error",
|
|
||||||
"no-param-reassign": "off",
|
|
||||||
"no-return-assign": "error",
|
|
||||||
"no-return-await": "off",
|
|
||||||
"no-shadow-restricted-names": "error",
|
|
||||||
"no-tabs": "error",
|
|
||||||
"no-trailing-spaces": "error",
|
|
||||||
"no-unused-vars": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"vars": "all",
|
|
||||||
"args": "none",
|
|
||||||
"ignoreRestSiblings": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"no-use-before-define": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"functions": false,
|
|
||||||
"classes": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"no-useless-escape": "off",
|
|
||||||
"no-var": "error",
|
|
||||||
"nonblock-statement-body-position": ["error", "below"],
|
|
||||||
"object-curly-spacing": ["error", "always"],
|
|
||||||
"padded-blocks": ["error", "never"],
|
|
||||||
"prefer-arrow-callback": "error",
|
|
||||||
"prefer-const": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"destructuring": "all",
|
|
||||||
"ignoreReadBeforeAssign": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"prefer-template": "off",
|
|
||||||
"quotes": ["error", "single"],
|
|
||||||
"semi": ["error", "always"],
|
|
||||||
"spaced-comment": [
|
|
||||||
"error",
|
|
||||||
"always",
|
|
||||||
{
|
|
||||||
"exceptions": ["!"]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"space-before-blocks": "error",
|
|
||||||
"unicode-bom": ["error", "never"],
|
|
||||||
"valid-jsdoc": "error",
|
|
||||||
"wrap-iife": ["error", "inside"],
|
|
||||||
"react/jsx-closing-bracket-location": 1,
|
|
||||||
"react/jsx-tag-spacing": 1,
|
|
||||||
"react/jsx-max-props-per-line": ["error", { "maximum": 2, "when": "multiline" }],
|
|
||||||
"react/prop-types": 0
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
"env": env,
|
extends: "@urbit"
|
||||||
"extends": [
|
|
||||||
"plugin:react/recommended",
|
|
||||||
"eslint:recommended",
|
|
||||||
],
|
|
||||||
"settings": {
|
|
||||||
"react": {
|
|
||||||
"version": "^16.5.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"parser": "babel-eslint",
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": 10,
|
|
||||||
"requireConfigFile": false,
|
|
||||||
"sourceType": "module"
|
|
||||||
},
|
|
||||||
"root": true,
|
|
||||||
"rules": rules,
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"files": ["**/*.ts", "**/*.tsx"],
|
|
||||||
"env": env,
|
|
||||||
"extends": [
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:@typescript-eslint/eslint-recommended",
|
|
||||||
"plugin:@typescript-eslint/recommended"
|
|
||||||
],
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaFeatures": { "jsx": true },
|
|
||||||
"ecmaVersion": 10,
|
|
||||||
"requireConfigFile": false,
|
|
||||||
"sourceType": "module"
|
|
||||||
},
|
|
||||||
"plugins": ["@typescript-eslint"],
|
|
||||||
"rules": rules
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
};
|
914
pkg/interface/package-lock.json
generated
914
pkg/interface/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -12,6 +12,7 @@
|
|||||||
"@tlon/indigo-light": "^1.0.6",
|
"@tlon/indigo-light": "^1.0.6",
|
||||||
"@tlon/indigo-react": "1.2.17",
|
"@tlon/indigo-react": "1.2.17",
|
||||||
"@tlon/sigil-js": "^1.4.3",
|
"@tlon/sigil-js": "^1.4.3",
|
||||||
|
"@urbit/api": "file:../npm/api",
|
||||||
"aws-sdk": "^2.830.0",
|
"aws-sdk": "^2.830.0",
|
||||||
"big-integer": "^1.6.48",
|
"big-integer": "^1.6.48",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
@ -63,15 +64,15 @@
|
|||||||
"@types/styled-components": "^5.1.7",
|
"@types/styled-components": "^5.1.7",
|
||||||
"@types/styled-system": "^5.1.10",
|
"@types/styled-system": "^5.1.10",
|
||||||
"@types/yup": "^0.29.11",
|
"@types/yup": "^0.29.11",
|
||||||
"@typescript-eslint/eslint-plugin": "^3.10.1",
|
"@typescript-eslint/eslint-plugin": "^4.15.0",
|
||||||
"@typescript-eslint/parser": "^3.10.1",
|
"@urbit/eslint-config": "file:../npm/eslint-config",
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
"babel-loader": "^8.2.2",
|
"babel-loader": "^8.2.2",
|
||||||
"babel-plugin-lodash": "^3.3.4",
|
"babel-plugin-lodash": "^3.3.4",
|
||||||
"babel-plugin-root-import": "^6.6.0",
|
"babel-plugin-root-import": "^6.6.0",
|
||||||
"clean-webpack-plugin": "^3.0.0",
|
"clean-webpack-plugin": "^3.0.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"eslint": "^6.8.0",
|
"eslint": "^7.19.0",
|
||||||
"eslint-plugin-react": "^7.22.0",
|
"eslint-plugin-react": "^7.22.0",
|
||||||
"file-loader": "^6.2.0",
|
"file-loader": "^6.2.0",
|
||||||
"html-webpack-plugin": "^4.5.1",
|
"html-webpack-plugin": "^4.5.1",
|
||||||
@ -85,7 +86,7 @@
|
|||||||
"webpack-dev-server": "^3.11.2"
|
"webpack-dev-server": "^3.11.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint ./src/**/*.{js,ts,tsx}",
|
"lint": "eslint ./src/**/*.{ts,tsx}",
|
||||||
"lint-file": "eslint",
|
"lint-file": "eslint",
|
||||||
"tsc": "tsc",
|
"tsc": "tsc",
|
||||||
"tsc:watch": "tsc --watch",
|
"tsc:watch": "tsc --watch",
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import _ from "lodash";
|
import _ from 'lodash';
|
||||||
import { uuid } from "../lib/util";
|
import { Patp, Path } from '@urbit/api';
|
||||||
import { Patp, Path } from "~/types/noun";
|
|
||||||
import BaseStore from '../store/base';
|
import BaseStore from '../store/base';
|
||||||
|
|
||||||
export default class BaseApi<S extends object = {}> {
|
export default class BaseApi<S extends object = {}> {
|
||||||
@ -26,8 +25,8 @@ export default class BaseApi<S extends object = {}> {
|
|||||||
data: event,
|
data: event,
|
||||||
from: {
|
from: {
|
||||||
ship,
|
ship,
|
||||||
path,
|
path
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
(qui) => {
|
(qui) => {
|
||||||
@ -50,8 +49,12 @@ export default class BaseApi<S extends object = {}> {
|
|||||||
appl,
|
appl,
|
||||||
mark,
|
mark,
|
||||||
data,
|
data,
|
||||||
(json) => { resolve(json); },
|
(json) => {
|
||||||
(err) => { reject(err); }
|
resolve(json);
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -69,5 +72,4 @@ export default class BaseApi<S extends object = {}> {
|
|||||||
|
|
||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import BaseApi from './base';
|
import BaseApi from './base';
|
||||||
import { StoreState } from '../store/type';
|
import { StoreState } from '../store/type';
|
||||||
import { Patp, Path, Enc } from '~/types/noun';
|
import { Patp } from '@urbit/api';
|
||||||
import { Contact, ContactEdit } from '~/types/contact-update';
|
import { ContactEdit } from '@urbit/api/contacts';
|
||||||
import { GroupPolicy, Resource } from '~/types/group-update';
|
|
||||||
|
|
||||||
export default class ContactsApi extends BaseApi<StoreState> {
|
export default class ContactsApi extends BaseApi<StoreState> {
|
||||||
add(ship: Patp, contact: any) {
|
add(ship: Patp, contact: any) {
|
||||||
@ -31,7 +30,7 @@ export default class ContactsApi extends BaseApi<StoreState> {
|
|||||||
ship,
|
ship,
|
||||||
'edit-field': editField,
|
'edit-field': editField,
|
||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,7 +61,7 @@ export default class ContactsApi extends BaseApi<StoreState> {
|
|||||||
return this.action(
|
return this.action(
|
||||||
'contact-push-hook',
|
'contact-push-hook',
|
||||||
'contact-share',
|
'contact-share',
|
||||||
{ share: recipient },
|
{ share: recipient }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,7 +84,7 @@ export default class ContactsApi extends BaseApi<StoreState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private storeAction(action: any): Promise<any> {
|
private storeAction(action: any): Promise<any> {
|
||||||
return this.action('contact-store', 'contact-update', action)
|
return this.action('contact-store', 'contact-update', action);
|
||||||
}
|
}
|
||||||
|
|
||||||
private viewAction(threadName: string, action: any) {
|
private viewAction(threadName: string, action: any) {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Patp } from '~/types/noun';
|
import { Patp } from '@urbit/api';
|
||||||
import BaseApi from './base';
|
import BaseApi from './base';
|
||||||
import { StoreState } from '../store/type';
|
import { StoreState } from '../store/type';
|
||||||
import GlobalStore from '../store/store';
|
import GlobalStore from '../store/store';
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import BaseApi from './base';
|
import BaseApi from './base';
|
||||||
import { StoreState } from '../store/type';
|
import { StoreState } from '../store/type';
|
||||||
import { Patp, Path, PatpNoSig } from '~/types/noun';
|
import { Patp, Path } from '@urbit/api';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { makeResource, resourceFromPath } from '../lib/group';
|
import { makeResource, resourceFromPath } from '../lib/group';
|
||||||
import {GroupPolicy, Enc, Post, NodeMap, Content, Resource} from '~/types';
|
import { GroupPolicy, Enc, Post, Content } from '@urbit/api';
|
||||||
import { numToUd, unixToDa, decToUd, deSig, resourceAsPath } from '~/logic/lib/util';
|
import { numToUd, unixToDa, decToUd, deSig, resourceAsPath } from '~/logic/lib/util';
|
||||||
|
|
||||||
export const createBlankNodeWithChildPost = (
|
export const createBlankNodeWithChildPost = (
|
||||||
parentIndex: string = '',
|
parentIndex = '',
|
||||||
childIndex: string = '',
|
childIndex = '',
|
||||||
contents: Content[]
|
contents: Content[]
|
||||||
) => {
|
) => {
|
||||||
const date = unixToDa(Date.now()).toString();
|
const date = unixToDa(Date.now()).toString();
|
||||||
@ -41,7 +41,7 @@ export const createBlankNodeWithChildPost = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
function markPending(nodes: any) {
|
function markPending(nodes: any) {
|
||||||
_.forEach(nodes, node => {
|
_.forEach(nodes, (node) => {
|
||||||
node.post.author = deSig(node.post.author);
|
node.post.author = deSig(node.post.author);
|
||||||
node.post.pending = true;
|
node.post.pending = true;
|
||||||
markPending(node.children || {});
|
markPending(node.children || {});
|
||||||
@ -50,8 +50,8 @@ function markPending(nodes: any) {
|
|||||||
|
|
||||||
export const createPost = (
|
export const createPost = (
|
||||||
contents: Content[],
|
contents: Content[],
|
||||||
parentIndex: string = '',
|
parentIndex = '',
|
||||||
childIndex:string = 'DATE_PLACEHOLDER'
|
childIndex = 'DATE_PLACEHOLDER'
|
||||||
) => {
|
) => {
|
||||||
if (childIndex === 'DATE_PLACEHOLDER') {
|
if (childIndex === 'DATE_PLACEHOLDER') {
|
||||||
childIndex = unixToDa(Date.now()).toString();
|
childIndex = unixToDa(Date.now()).toString();
|
||||||
@ -80,11 +80,10 @@ function moduleToMark(mod: string): string | undefined {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default class GraphApi extends BaseApi<StoreState> {
|
export default class GraphApi extends BaseApi<StoreState> {
|
||||||
|
|
||||||
joiningGraphs = new Set<string>();
|
joiningGraphs = new Set<string>();
|
||||||
|
|
||||||
private storeAction(action: any): Promise<any> {
|
private storeAction(action: any): Promise<any> {
|
||||||
return this.action('graph-store', 'graph-update', action)
|
return this.action('graph-store', 'graph-update', action);
|
||||||
}
|
}
|
||||||
|
|
||||||
private viewAction(threadName: string, action: any) {
|
private viewAction(threadName: string, action: any) {
|
||||||
@ -106,12 +105,12 @@ export default class GraphApi extends BaseApi<StoreState> {
|
|||||||
const resource = makeResource(`~${window.ship}`, name);
|
const resource = makeResource(`~${window.ship}`, name);
|
||||||
|
|
||||||
return this.viewAction('graph-create', {
|
return this.viewAction('graph-create', {
|
||||||
"create": {
|
'create': {
|
||||||
resource,
|
resource,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
associated,
|
associated,
|
||||||
"module": mod,
|
'module': mod,
|
||||||
mark: moduleToMark(mod)
|
mark: moduleToMark(mod)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -127,12 +126,12 @@ export default class GraphApi extends BaseApi<StoreState> {
|
|||||||
const resource = makeResource(`~${window.ship}`, name);
|
const resource = makeResource(`~${window.ship}`, name);
|
||||||
|
|
||||||
return this.viewAction('graph-create', {
|
return this.viewAction('graph-create', {
|
||||||
"create": {
|
'create': {
|
||||||
resource,
|
resource,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
associated: { policy },
|
associated: { policy },
|
||||||
"module": mod,
|
'module': mod,
|
||||||
mark: moduleToMark(mod)
|
mark: moduleToMark(mod)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -148,9 +147,9 @@ export default class GraphApi extends BaseApi<StoreState> {
|
|||||||
return this.viewAction('graph-join', {
|
return this.viewAction('graph-join', {
|
||||||
join: {
|
join: {
|
||||||
resource,
|
resource,
|
||||||
ship,
|
ship
|
||||||
}
|
}
|
||||||
}).then(res => {
|
}).then((res) => {
|
||||||
this.joiningGraphs.delete(rid);
|
this.joiningGraphs.delete(rid);
|
||||||
return res;
|
return res;
|
||||||
});
|
});
|
||||||
@ -159,7 +158,7 @@ export default class GraphApi extends BaseApi<StoreState> {
|
|||||||
deleteGraph(name: string) {
|
deleteGraph(name: string) {
|
||||||
const resource = makeResource(`~${window.ship}`, name);
|
const resource = makeResource(`~${window.ship}`, name);
|
||||||
return this.viewAction('graph-delete', {
|
return this.viewAction('graph-delete', {
|
||||||
"delete": {
|
'delete': {
|
||||||
resource
|
resource
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -168,7 +167,7 @@ export default class GraphApi extends BaseApi<StoreState> {
|
|||||||
leaveGraph(ship: Patp, name: string) {
|
leaveGraph(ship: Patp, name: string) {
|
||||||
const resource = makeResource(ship, name);
|
const resource = makeResource(ship, name);
|
||||||
return this.viewAction('graph-leave', {
|
return this.viewAction('graph-leave', {
|
||||||
"leave": {
|
'leave': {
|
||||||
resource
|
resource
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -203,7 +202,7 @@ export default class GraphApi extends BaseApi<StoreState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addPost(ship: Patp, name: string, post: Post) {
|
addPost(ship: Patp, name: string, post: Post) {
|
||||||
let nodes = {};
|
const nodes = {};
|
||||||
nodes[post.index] = {
|
nodes[post.index] = {
|
||||||
post,
|
post,
|
||||||
children: null
|
children: null
|
||||||
@ -212,7 +211,7 @@ export default class GraphApi extends BaseApi<StoreState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addNode(ship: Patp, name: string, node: Object) {
|
addNode(ship: Patp, name: string, node: Object) {
|
||||||
let nodes = {};
|
const nodes = {};
|
||||||
nodes[node.post.index] = node;
|
nodes[node.post.index] = node;
|
||||||
|
|
||||||
return this.addNodes(ship, name, nodes);
|
return this.addNodes(ship, name, nodes);
|
||||||
@ -300,7 +299,6 @@ export default class GraphApi extends BaseApi<StoreState> {
|
|||||||
this.store.handleEvent({ data });
|
this.store.handleEvent({ data });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
getGraphSubset(ship: string, resource: string, start: string, end: string) {
|
getGraphSubset(ship: string, resource: string, start: string, end: string) {
|
||||||
return this.scry<any>(
|
return this.scry<any>(
|
||||||
'graph-store',
|
'graph-store',
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import BaseApi from './base';
|
import BaseApi from './base';
|
||||||
import { StoreState } from '../store/type';
|
import { StoreState } from '../store/type';
|
||||||
import { Path, Patp, Enc } from '~/types/noun';
|
import { Path, Patp, Enc } from '@urbit/api';
|
||||||
import {
|
import {
|
||||||
GroupAction,
|
GroupAction,
|
||||||
GroupPolicy,
|
GroupPolicy,
|
||||||
Resource,
|
Resource,
|
||||||
Tag,
|
Tag,
|
||||||
GroupPolicyDiff,
|
GroupPolicyDiff
|
||||||
} from '~/types/group-update';
|
} from '@urbit/api/groups';
|
||||||
import { makeResource } from '../lib/group';
|
import { makeResource } from '../lib/group';
|
||||||
|
|
||||||
export default class GroupsApi extends BaseApi<StoreState> {
|
export default class GroupsApi extends BaseApi<StoreState> {
|
||||||
@ -76,7 +76,6 @@ export default class GroupsApi extends BaseApi<StoreState> {
|
|||||||
description
|
description
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private proxyAction(action: GroupAction) {
|
private proxyAction(action: GroupAction) {
|
||||||
@ -93,6 +92,5 @@ export default class GroupsApi extends BaseApi<StoreState> {
|
|||||||
|
|
||||||
private viewAction(action: any) {
|
private viewAction(action: any) {
|
||||||
return this.action('group-view', 'group-view-action', action);
|
return this.action('group-view', 'group-view-action', action);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,24 +1,23 @@
|
|||||||
import BaseApi from "./base";
|
import BaseApi from './base';
|
||||||
import { StoreState } from "../store/type";
|
import { StoreState } from '../store/type';
|
||||||
import { dateToDa, decToUd } from "../lib/util";
|
import { dateToDa, decToUd } from '../lib/util';
|
||||||
import {NotifIndex, IndexedNotification, Association, GraphNotifDescription} from "~/types";
|
import { NotifIndex, IndexedNotification, Association, GraphNotifDescription } from '@urbit/api';
|
||||||
import { BigInteger } from 'big-integer';
|
import { BigInteger } from 'big-integer';
|
||||||
import {getParentIndex} from "../lib/notification";
|
import { getParentIndex } from '../lib/notification';
|
||||||
|
|
||||||
export class HarkApi extends BaseApi<StoreState> {
|
export class HarkApi extends BaseApi<StoreState> {
|
||||||
private harkAction(action: any): Promise<any> {
|
private harkAction(action: any): Promise<any> {
|
||||||
return this.action("hark-store", "hark-action", action);
|
return this.action('hark-store', 'hark-action', action);
|
||||||
}
|
}
|
||||||
|
|
||||||
private graphHookAction(action: any) {
|
private graphHookAction(action: any) {
|
||||||
return this.action("hark-graph-hook", "hark-graph-hook-action", action);
|
return this.action('hark-graph-hook', 'hark-graph-hook-action', action);
|
||||||
}
|
}
|
||||||
|
|
||||||
private groupHookAction(action: any) {
|
private groupHookAction(action: any) {
|
||||||
return this.action("hark-group-hook", "hark-group-hook-action", action);
|
return this.action('hark-group-hook', 'hark-group-hook-action', action);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private actOnNotification(frond: string, intTime: BigInteger, index: NotifIndex) {
|
private actOnNotification(frond: string, intTime: BigInteger, index: NotifIndex) {
|
||||||
const time = decToUd(intTime.toString());
|
const time = decToUd(intTime.toString());
|
||||||
return this.harkAction({
|
return this.harkAction({
|
||||||
@ -74,12 +73,10 @@ export class HarkApi extends BaseApi<StoreState> {
|
|||||||
module: association.metadata.module,
|
module: association.metadata.module,
|
||||||
description,
|
description,
|
||||||
index: parent
|
index: parent
|
||||||
} },
|
} }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
markEachAsRead(association: Association, parent: string, child: string, description: GraphNotifDescription, mod: string) {
|
markEachAsRead(association: Association, parent: string, child: string, description: GraphNotifDescription, mod: string) {
|
||||||
return this.harkAction({
|
return this.harkAction({
|
||||||
'read-each': {
|
'read-each': {
|
||||||
@ -116,7 +113,7 @@ export class HarkApi extends BaseApi<StoreState> {
|
|||||||
mute(notif: IndexedNotification) {
|
mute(notif: IndexedNotification) {
|
||||||
if('graph' in notif.index && 'graph' in notif.notification.contents) {
|
if('graph' in notif.index && 'graph' in notif.notification.contents) {
|
||||||
const { index } = notif;
|
const { index } = notif;
|
||||||
const parentIndex = getParentIndex(index.graph, notif.notification.contents.graph)
|
const parentIndex = getParentIndex(index.graph, notif.notification.contents.graph);
|
||||||
if(!parentIndex) {
|
if(!parentIndex) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
@ -132,7 +129,7 @@ export class HarkApi extends BaseApi<StoreState> {
|
|||||||
unmute(notif: IndexedNotification) {
|
unmute(notif: IndexedNotification) {
|
||||||
if('graph' in notif.index && 'graph' in notif.notification.contents) {
|
if('graph' in notif.index && 'graph' in notif.notification.contents) {
|
||||||
const { index } = notif;
|
const { index } = notif;
|
||||||
const parentIndex = getParentIndex(index.graph, notif.notification.contents.graph)
|
const parentIndex = getParentIndex(index.graph, notif.notification.contents.graph);
|
||||||
if(!parentIndex) {
|
if(!parentIndex) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
@ -147,7 +144,7 @@ export class HarkApi extends BaseApi<StoreState> {
|
|||||||
ignoreGroup(group: string) {
|
ignoreGroup(group: string) {
|
||||||
return this.groupHookAction({
|
return this.groupHookAction({
|
||||||
ignore: group
|
ignore: group
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ignoreGraph(graph: string, index: string) {
|
ignoreGraph(graph: string, index: string) {
|
||||||
@ -156,13 +153,13 @@ export class HarkApi extends BaseApi<StoreState> {
|
|||||||
graph,
|
graph,
|
||||||
index
|
index
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
listenGroup(group: string) {
|
listenGroup(group: string) {
|
||||||
return this.groupHookAction({
|
return this.groupHookAction({
|
||||||
listen: group
|
listen: group
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
listenGraph(graph: string, index: string) {
|
listenGraph(graph: string, index: string) {
|
||||||
@ -171,7 +168,7 @@ export class HarkApi extends BaseApi<StoreState> {
|
|||||||
graph,
|
graph,
|
||||||
index
|
index
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMore(): Promise<boolean> {
|
async getMore(): Promise<boolean> {
|
||||||
@ -183,16 +180,16 @@ export class HarkApi extends BaseApi<StoreState> {
|
|||||||
|
|
||||||
async getSubset(offset:number, count:number, isArchive: boolean) {
|
async getSubset(offset:number, count:number, isArchive: boolean) {
|
||||||
const where = isArchive ? 'archive' : 'inbox';
|
const where = isArchive ? 'archive' : 'inbox';
|
||||||
const data = await this.scry("hark-store", `/recent/${where}/${offset}/${count}`);
|
const data = await this.scry('hark-store', `/recent/${where}/${offset}/${count}`);
|
||||||
this.store.handleEvent({ data });
|
this.store.handleEvent({ data });
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTimeSubset(start?: Date, end?: Date) {
|
async getTimeSubset(start?: Date, end?: Date) {
|
||||||
const s = start ? dateToDa(start) : "-";
|
const s = start ? dateToDa(start) : '-';
|
||||||
const e = end ? dateToDa(end) : "-";
|
const e = end ? dateToDa(end) : '-';
|
||||||
const result = await this.scry("hark-hook", `/recent/${s}/${e}`);
|
const result = await this.scry('hark-hook', `/recent/${s}/${e}`);
|
||||||
this.store.handleEvent({
|
this.store.handleEvent({
|
||||||
data: result,
|
data: result
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import BaseApi from "./base";
|
import BaseApi from './base';
|
||||||
import { StoreState } from "../store/type";
|
import { StoreState } from '../store/type';
|
||||||
import { Serial, Path } from "~/types/noun";
|
import { Serial, Path } from '@urbit/api';
|
||||||
|
|
||||||
export default class InviteApi extends BaseApi<StoreState> {
|
export default class InviteApi extends BaseApi<StoreState> {
|
||||||
accept(app: string, uid: Serial) {
|
accept(app: string, uid: Serial) {
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import BaseApi from "./base";
|
import BaseApi from './base';
|
||||||
import { StoreState } from "../store/type";
|
import { StoreState } from '../store/type';
|
||||||
|
|
||||||
export default class LocalApi extends BaseApi<StoreState> {
|
export default class LocalApi extends BaseApi<StoreState> {
|
||||||
getBaseHash() {
|
getBaseHash() {
|
||||||
this.scry<string>('file-server', '/clay/base/hash').then(baseHash => {
|
this.scry<string>('file-server', '/clay/base/hash').then((baseHash) => {
|
||||||
this.store.handleEvent({ data: { local: { baseHash } } });
|
this.store.handleEvent({ data: { local: { baseHash } } });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -11,5 +11,4 @@ export default class LocalApi extends BaseApi<StoreState> {
|
|||||||
dehydrate() {
|
dehydrate() {
|
||||||
this.store.dehydrate();
|
this.store.dehydrate();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
|
|
||||||
import BaseApi from './base';
|
import BaseApi from './base';
|
||||||
import { StoreState } from '../store/type';
|
import { StoreState } from '../store/type';
|
||||||
import { Path, Patp, Association, Metadata, MetadataUpdatePreview } from '~/types';
|
import { Path, Patp, Association, Metadata, MetadataUpdatePreview } from '@urbit/api';
|
||||||
import { uxToHex } from '../lib/util';
|
import { uxToHex } from '../lib/util';
|
||||||
|
|
||||||
export default class MetadataApi extends BaseApi<StoreState> {
|
export default class MetadataApi extends BaseApi<StoreState> {
|
||||||
|
|
||||||
|
|
||||||
metadataAdd(appName: string, resource: Path, group: Path, title: string, description: string, dateCreated: string, color: string, moduleName: string) {
|
metadataAdd(appName: string, resource: Path, group: Path, title: string, description: string, dateCreated: string, color: string, moduleName: string) {
|
||||||
const creator = `~${this.ship}`;
|
const creator = `~${this.ship}`;
|
||||||
return this.metadataAction({
|
return this.metadataAction({
|
||||||
@ -69,10 +67,10 @@ export default class MetadataApi extends BaseApi<StoreState> {
|
|||||||
}
|
}
|
||||||
done = true;
|
done = true;
|
||||||
tempChannel.delete();
|
tempChannel.delete();
|
||||||
reject(new Error("offline"))
|
reject(new Error('offline'));
|
||||||
}, 15000);
|
}, 15000);
|
||||||
|
|
||||||
tempChannel.subscribe(window.ship, "metadata-pull-hook", `/preview${group}`,
|
tempChannel.subscribe(window.ship, 'metadata-pull-hook', `/preview${group}`,
|
||||||
(err) => {
|
(err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
reject(err);
|
reject(err);
|
||||||
@ -88,24 +86,22 @@ export default class MetadataApi extends BaseApi<StoreState> {
|
|||||||
} else {
|
} else {
|
||||||
done = true;
|
done = true;
|
||||||
tempChannel.delete();
|
tempChannel.delete();
|
||||||
reject(new Error("no-permissions"));
|
reject(new Error('no-permissions'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(quit) => {
|
(quit) => {
|
||||||
tempChannel.delete();
|
tempChannel.delete();
|
||||||
if(!done) {
|
if(!done) {
|
||||||
reject(new Error("offline"))
|
reject(new Error('offline'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(a) => {
|
(a) => {
|
||||||
console.log(a);
|
console.log(a);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private metadataAction(data) {
|
private metadataAction(data) {
|
||||||
return this.action('metadata-push-hook', 'metadata-update', data);
|
return this.action('metadata-push-hook', 'metadata-update', data);
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,7 @@ import BaseApi from './base';
|
|||||||
import { StoreState } from '../store/type';
|
import { StoreState } from '../store/type';
|
||||||
import { S3Update } from '../../types/s3-update';
|
import { S3Update } from '../../types/s3-update';
|
||||||
|
|
||||||
|
|
||||||
export default class S3Api extends BaseApi<StoreState> {
|
export default class S3Api extends BaseApi<StoreState> {
|
||||||
|
|
||||||
setCurrentBucket(bucket: string) {
|
setCurrentBucket(bucket: string) {
|
||||||
return this.s3Action({ 'set-current-bucket': bucket });
|
return this.s3Action({ 'set-current-bucket': bucket });
|
||||||
}
|
}
|
||||||
@ -32,6 +30,5 @@ export default class S3Api extends BaseApi<StoreState> {
|
|||||||
private s3Action(data: any) {
|
private s3Action(data: any) {
|
||||||
return this.action('s3-store', 's3-action', data);
|
return this.action('s3-store', 's3-action', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,13 +1,9 @@
|
|||||||
import BaseApi from './base';
|
import BaseApi from './base';
|
||||||
import { StoreState } from '../store/type';
|
import { StoreState } from '../store/type';
|
||||||
import {
|
import { Key,
|
||||||
SettingsUpdate,
|
|
||||||
SettingsData,
|
|
||||||
Key,
|
|
||||||
Value,
|
Value,
|
||||||
Bucket,
|
Bucket
|
||||||
} from '~/types/settings';
|
} from '@urbit/api/settings';
|
||||||
|
|
||||||
|
|
||||||
export default class SettingsApi extends BaseApi<StoreState> {
|
export default class SettingsApi extends BaseApi<StoreState> {
|
||||||
private storeAction(action: SettingsEvent): Promise<any> {
|
private storeAction(action: SettingsEvent): Promise<any> {
|
||||||
@ -16,59 +12,59 @@ export default class SettingsApi extends BaseApi<StoreState> {
|
|||||||
|
|
||||||
putBucket(key: Key, bucket: Bucket) {
|
putBucket(key: Key, bucket: Bucket) {
|
||||||
this.storeAction({
|
this.storeAction({
|
||||||
"put-bucket": {
|
'put-bucket': {
|
||||||
"bucket-key": key,
|
'bucket-key': key,
|
||||||
"bucket": bucket,
|
'bucket': bucket
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
delBucket(key: Key) {
|
delBucket(key: Key) {
|
||||||
this.storeAction({
|
this.storeAction({
|
||||||
"del-bucket": {
|
'del-bucket': {
|
||||||
"bucket-key": key,
|
'bucket-key': key
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
putEntry(buc: Key, key: Key, val: Value) {
|
putEntry(buc: Key, key: Key, val: Value) {
|
||||||
return this.storeAction({
|
return this.storeAction({
|
||||||
"put-entry": {
|
'put-entry': {
|
||||||
"bucket-key": buc,
|
'bucket-key': buc,
|
||||||
"entry-key": key,
|
'entry-key': key,
|
||||||
"value": val,
|
'value': val
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
delEntry(buc: Key, key: Key) {
|
delEntry(buc: Key, key: Key) {
|
||||||
this.storeAction({
|
this.storeAction({
|
||||||
"put-entry": {
|
'put-entry': {
|
||||||
"bucket-key": buc,
|
'bucket-key': buc,
|
||||||
"entry-key": key,
|
'entry-key': key
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAll() {
|
async getAll() {
|
||||||
const data = await this.scry("settings-store", "/all");
|
const data = await this.scry('settings-store', '/all');
|
||||||
this.store.handleEvent({data: {"settings-data": data.all}});
|
this.store.handleEvent({ data: { 'settings-data': data.all } });
|
||||||
}
|
}
|
||||||
|
|
||||||
async getBucket(bucket: Key) {
|
async getBucket(bucket: Key) {
|
||||||
const data = await this.scry('settings-store', `/bucket/${bucket}`);
|
const data = await this.scry('settings-store', `/bucket/${bucket}`);
|
||||||
this.store.handleEvent({data: {"settings-data": {
|
this.store.handleEvent({ data: { 'settings-data': {
|
||||||
"bucket-key": bucket,
|
'bucket-key': bucket,
|
||||||
"bucket": data.bucket,
|
'bucket': data.bucket
|
||||||
} } });
|
} } });
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEntry(bucket: Key, entry: Key) {
|
async getEntry(bucket: Key, entry: Key) {
|
||||||
const data = await this.scry('settings-store', `/entry/${bucket}/${entry}`);
|
const data = await this.scry('settings-store', `/entry/${bucket}/${entry}`);
|
||||||
this.store.handleEvent({data: {"settings-data": {
|
this.store.handleEvent({ data: { 'settings-data': {
|
||||||
"bucket-key": bucket,
|
'bucket-key': bucket,
|
||||||
"entry-key": entry,
|
'entry-key': entry,
|
||||||
"entry": data.entry,
|
'entry': data.entry
|
||||||
} } });
|
} } });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import bigInt, { BigInteger } from "big-integer";
|
import bigInt, { BigInteger } from 'big-integer';
|
||||||
|
|
||||||
interface NonemptyNode<V> {
|
interface NonemptyNode<V> {
|
||||||
n: [BigInteger, V];
|
n: [BigInteger, V];
|
||||||
@ -14,7 +14,7 @@ type MapNode<V> = NonemptyNode<V> | null;
|
|||||||
*/
|
*/
|
||||||
export class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
|
export class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
|
||||||
private root: MapNode<V> = null;
|
private root: MapNode<V> = null;
|
||||||
size: number = 0;
|
size = 0;
|
||||||
|
|
||||||
constructor(initial: [BigInteger, V][] = []) {
|
constructor(initial: [BigInteger, V][] = []) {
|
||||||
initial.forEach(([key, val]) => {
|
initial.forEach(([key, val]) => {
|
||||||
@ -48,13 +48,12 @@ export class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
|
|||||||
* Put an item by a key
|
* Put an item by a key
|
||||||
*/
|
*/
|
||||||
set(key: BigInteger, value: V): void {
|
set(key: BigInteger, value: V): void {
|
||||||
|
|
||||||
const inner = (node: MapNode<V>) => {
|
const inner = (node: MapNode<V>) => {
|
||||||
if (!node) {
|
if (!node) {
|
||||||
return {
|
return {
|
||||||
n: [key, value],
|
n: [key, value],
|
||||||
l: null,
|
l: null,
|
||||||
r: null,
|
r: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const [k] = node.n;
|
const [k] = node.n;
|
||||||
@ -62,22 +61,22 @@ export class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
|
|||||||
this.size--;
|
this.size--;
|
||||||
return {
|
return {
|
||||||
...node,
|
...node,
|
||||||
n: [k, value],
|
n: [k, value]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (key.gt(k)) {
|
if (key.gt(k)) {
|
||||||
const l = inner(node.l);
|
const l = inner(node.l);
|
||||||
if (!l) {
|
if (!l) {
|
||||||
throw new Error("invariant violation");
|
throw new Error('invariant violation');
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...node,
|
...node,
|
||||||
l,
|
l
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const r = inner(node.r);
|
const r = inner(node.r);
|
||||||
if (!r) {
|
if (!r) {
|
||||||
throw new Error("invariant violation");
|
throw new Error('invariant violation');
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ...node, r };
|
return { ...node, r };
|
||||||
@ -133,8 +132,8 @@ export class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
|
|||||||
bool,
|
bool,
|
||||||
{
|
{
|
||||||
...node,
|
...node,
|
||||||
l,
|
l
|
||||||
},
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,8 +142,8 @@ export class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
|
|||||||
bool,
|
bool,
|
||||||
{
|
{
|
||||||
...node,
|
...node,
|
||||||
r,
|
r
|
||||||
},
|
}
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
const [ret, newRoot] = inner(this.root);
|
const [ret, newRoot] = inner(this.root);
|
||||||
@ -165,7 +164,7 @@ export class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...node.l,
|
...node.l,
|
||||||
r: inner(node.r),
|
r: inner(node.r)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
return inner(nod);
|
return inner(nod);
|
||||||
@ -180,7 +179,7 @@ export class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
|
|||||||
return inner(node.l);
|
return inner(node.l);
|
||||||
}
|
}
|
||||||
return node.n;
|
return node.n;
|
||||||
}
|
};
|
||||||
return inner(this.root);
|
return inner(this.root);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,7 +192,7 @@ export class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
|
|||||||
return inner(node.r);
|
return inner(node.r);
|
||||||
}
|
}
|
||||||
return node.n;
|
return node.n;
|
||||||
}
|
};
|
||||||
return inner(this.root);
|
return inner(this.root);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -208,7 +207,7 @@ export class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Symbol.iterator](): IterableIterator<[BigInteger, V]> {
|
[Symbol.iterator](): IterableIterator<[BigInteger, V]> {
|
||||||
let result: [BigInteger, V][] = [];
|
const result: [BigInteger, V][] = [];
|
||||||
const inner = (node: MapNode<V>) => {
|
const inner = (node: MapNode<V>) => {
|
||||||
if (!node) {
|
if (!node) {
|
||||||
return;
|
return;
|
||||||
@ -227,7 +226,7 @@ export class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
|
|||||||
return { value: result[idx++], done: false };
|
return { value: result[idx++], done: false };
|
||||||
}
|
}
|
||||||
return { done: true, value: null };
|
return { done: true, value: null };
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
|
|
||||||
export class OrderedMap<V> extends Map<number, V>
|
export class OrderedMap<V> extends Map<number, V>
|
||||||
implements Iterable<[number, V]> {
|
implements Iterable<[number, V]> {
|
||||||
|
|
||||||
[Symbol.iterator](): IterableIterator<[number, V]> {
|
[Symbol.iterator](): IterableIterator<[number, V]> {
|
||||||
const sorted = Array.from(super[Symbol.iterator]()).sort(
|
const sorted = Array.from(super[Symbol.iterator]()).sort(
|
||||||
([a], [b]) => b - a
|
([a], [b]) => b - a
|
||||||
@ -15,7 +14,7 @@ export class OrderedMap<V> extends Map<number, V>
|
|||||||
} else {
|
} else {
|
||||||
return { done: true, value: null };
|
return { done: true, value: null };
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import bigInt, { BigInteger } from "big-integer";
|
import bigInt, { BigInteger } from 'big-integer';
|
||||||
|
|
||||||
export function max(a: BigInteger, b: BigInteger) {
|
export function max(a: BigInteger, b: BigInteger) {
|
||||||
return a.gt(b) ? a : b;
|
return a.gt(b) ? a : b;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import _ from "lodash";
|
import _ from 'lodash';
|
||||||
import { roleTags, RoleTags, Group, Resource } from "~/types/group-update";
|
import { roleTags, RoleTags, Group, Resource } from '@urbit/api/groups';
|
||||||
import { PatpNoSig, Path } from "~/types/noun";
|
import { PatpNoSig, Path } from '@urbit/api';
|
||||||
import {deSig} from "./util";
|
import { deSig } from './util';
|
||||||
|
|
||||||
export function roleForShip(
|
export function roleForShip(
|
||||||
group: Group,
|
group: Group,
|
||||||
@ -14,7 +14,7 @@ export function roleForShip(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function resourceFromPath(path: Path): Resource {
|
export function resourceFromPath(path: Path): Resource {
|
||||||
const [, , ship, name] = path.split("/");
|
const [, , ship, name] = path.split('/');
|
||||||
return { ship, name };
|
return { ship, name };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,7 +25,7 @@ export function makeResource(ship: string, name: string) {
|
|||||||
export function isWriter(group: Group, resource: string) {
|
export function isWriter(group: Group, resource: string) {
|
||||||
const writers: Set<string> | undefined = _.get(
|
const writers: Set<string> | undefined = _.get(
|
||||||
group,
|
group,
|
||||||
["tags", "graph", resource, "writers"],
|
['tags', 'graph', resource, 'writers'],
|
||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
const admins = group?.tags?.role?.admin ?? new Set();
|
const admins = group?.tags?.role?.admin ?? new Set();
|
||||||
@ -36,18 +36,18 @@ export function isWriter(group: Group, resource: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isChannelAdmin(group: Group, resource: string, ship: string = `~${window.ship}`) {
|
export function isChannelAdmin(group: Group, resource: string, ship = `~${window.ship}`) {
|
||||||
const role = roleForShip(group, ship.slice(1));
|
const role = roleForShip(group, ship.slice(1));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
isHost(resource, ship) ||
|
isHost(resource, ship) ||
|
||||||
role === "admin" ||
|
role === 'admin' ||
|
||||||
role === "moderator"
|
role === 'moderator'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isHost(resource: string, ship: string = `~${window.ship}`) {
|
export function isHost(resource: string, ship = `~${window.ship}`) {
|
||||||
const [, , host] = resource.split("/");
|
const [, , host] = resource.split('/');
|
||||||
|
|
||||||
return ship === host;
|
return ship === host;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import bigInt, { BigInteger } from "big-integer";
|
import bigInt, { BigInteger } from 'big-integer';
|
||||||
import f from "lodash/fp";
|
import f from 'lodash/fp';
|
||||||
import { Unreads } from "~/types";
|
import { Unreads } from '@urbit/api';
|
||||||
|
|
||||||
export function getLastSeen(
|
export function getLastSeen(
|
||||||
unreads: Unreads,
|
unreads: Unreads,
|
||||||
@ -8,10 +8,10 @@ export function getLastSeen(
|
|||||||
index: string
|
index: string
|
||||||
): BigInteger | undefined {
|
): BigInteger | undefined {
|
||||||
const lastSeenIdx = unreads.graph?.[path]?.[index]?.unreads;
|
const lastSeenIdx = unreads.graph?.[path]?.[index]?.unreads;
|
||||||
if (!(typeof lastSeenIdx === "string")) {
|
if (!(typeof lastSeenIdx === 'string')) {
|
||||||
return bigInt.zero;
|
return bigInt.zero;
|
||||||
}
|
}
|
||||||
return f.flow(f.split("/"), f.last, (x) => (!!x ? bigInt(x) : undefined))(
|
return f.flow(f.split('/'), f.last, x => (x ? bigInt(x) : undefined))(
|
||||||
lastSeenIdx
|
lastSeenIdx
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
import { GraphNotifIndex, GraphNotificationContents } from "~/types";
|
import { GraphNotifIndex, GraphNotificationContents } from '@urbit/api';
|
||||||
|
|
||||||
export function getParentIndex(
|
export function getParentIndex(
|
||||||
idx: GraphNotifIndex,
|
idx: GraphNotifIndex,
|
||||||
contents: GraphNotificationContents
|
contents: GraphNotificationContents
|
||||||
) {
|
) {
|
||||||
const origIndex = contents[0].index.slice(1).split("/");
|
const origIndex = contents[0].index.slice(1).split('/');
|
||||||
const ret = (i: string[]) => `/${i.join("/")}`;
|
const ret = (i: string[]) => `/${i.join('/')}`;
|
||||||
switch (idx.description) {
|
switch (idx.description) {
|
||||||
case "link":
|
case 'link':
|
||||||
return "/";
|
return '/';
|
||||||
case "comment":
|
case 'comment':
|
||||||
return ret(origIndex.slice(0, 1));
|
return ret(origIndex.slice(0, 1));
|
||||||
case "note":
|
case 'note':
|
||||||
return "/";
|
return '/';
|
||||||
case "mention":
|
case 'mention':
|
||||||
return undefined;
|
return undefined;
|
||||||
default:
|
default:
|
||||||
return undefined;
|
return undefined;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Post, GraphNode } from "~/types";
|
import { Post, GraphNode } from '@urbit/api';
|
||||||
|
|
||||||
export const buntPost = (): Post => ({
|
export const buntPost = (): Post => ({
|
||||||
author: '',
|
author: '',
|
||||||
@ -10,7 +10,7 @@ export const buntPost = (): Post => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export function makeNodeMap(posts: Post[]): Record<string, GraphNode> {
|
export function makeNodeMap(posts: Post[]): Record<string, GraphNode> {
|
||||||
let nodes = {};
|
const nodes = {};
|
||||||
posts.forEach((p) => {
|
posts.forEach((p) => {
|
||||||
nodes[p.index] = { children: { empty: null }, post: p };
|
nodes[p.index] = { children: { empty: null }, post: p };
|
||||||
});
|
});
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Post, GraphNode, TextContent, Graph, NodeMap } from "~/types";
|
import { Post, GraphNode, TextContent, Graph, NodeMap } from '@urbit/api';
|
||||||
import { buntPost } from '~/logic/lib/post';
|
import { buntPost } from '~/logic/lib/post';
|
||||||
import { unixToDa } from "~/logic/lib/util";
|
import { unixToDa } from '~/logic/lib/util';
|
||||||
import {BigIntOrderedMap} from "./BigIntOrderedMap";
|
import { BigIntOrderedMap } from './BigIntOrderedMap';
|
||||||
import bigInt, { BigInteger } from 'big-integer';
|
import bigInt, { BigInteger } from 'big-integer';
|
||||||
|
|
||||||
export function newPost(
|
export function newPost(
|
||||||
@ -12,20 +12,20 @@ export function newPost(
|
|||||||
const nowDa = unixToDa(now);
|
const nowDa = unixToDa(now);
|
||||||
const root: Post = {
|
const root: Post = {
|
||||||
author: `~${window.ship}`,
|
author: `~${window.ship}`,
|
||||||
index: "/" + nowDa.toString(),
|
index: '/' + nowDa.toString(),
|
||||||
"time-sent": now,
|
'time-sent': now,
|
||||||
contents: [],
|
contents: [],
|
||||||
hash: null,
|
hash: null,
|
||||||
signatures: [],
|
signatures: []
|
||||||
};
|
};
|
||||||
|
|
||||||
const revContainer: Post = { ...root, index: root.index + "/1" };
|
const revContainer: Post = { ...root, index: root.index + '/1' };
|
||||||
const commentsContainer = { ...root, index: root.index + "/2" };
|
const commentsContainer = { ...root, index: root.index + '/2' };
|
||||||
|
|
||||||
const firstRevision: Post = {
|
const firstRevision: Post = {
|
||||||
...revContainer,
|
...revContainer,
|
||||||
index: revContainer.index + "/1",
|
index: revContainer.index + '/1',
|
||||||
contents: [{ text: title }, { text: body }],
|
contents: [{ text: title }, { text: body }]
|
||||||
};
|
};
|
||||||
|
|
||||||
const nodes = {
|
const nodes = {
|
||||||
@ -37,16 +37,16 @@ export function newPost(
|
|||||||
children: {
|
children: {
|
||||||
1: {
|
1: {
|
||||||
post: firstRevision,
|
post: firstRevision,
|
||||||
children: null,
|
children: null
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
2: {
|
2: {
|
||||||
post: commentsContainer,
|
post: commentsContainer,
|
||||||
children: null
|
children: null
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return [nowDa, nodes];
|
return [nowDa, nodes];
|
||||||
@ -57,10 +57,10 @@ export function editPost(rev: number, noteId: BigInteger, title: string, body: s
|
|||||||
const newRev: Post = {
|
const newRev: Post = {
|
||||||
author: `~${window.ship}`,
|
author: `~${window.ship}`,
|
||||||
index: `/${noteId.toString()}/1/${rev}`,
|
index: `/${noteId.toString()}/1/${rev}`,
|
||||||
"time-sent": now,
|
'time-sent': now,
|
||||||
contents: [{ text: title }, { text: body }],
|
contents: [{ text: title }, { text: body }],
|
||||||
hash: null,
|
hash: null,
|
||||||
signatures: [],
|
signatures: []
|
||||||
};
|
};
|
||||||
const nodes = {
|
const nodes = {
|
||||||
[newRev.index]: {
|
[newRev.index]: {
|
||||||
@ -74,7 +74,7 @@ export function editPost(rev: number, noteId: BigInteger, title: string, body: s
|
|||||||
|
|
||||||
export function getLatestRevision(node: GraphNode): [number, string, string, Post] {
|
export function getLatestRevision(node: GraphNode): [number, string, string, Post] {
|
||||||
const revs = node.children.get(bigInt(1));
|
const revs = node.children.get(bigInt(1));
|
||||||
const empty = [1, "", "", buntPost()] as [number, string, string, Post];
|
const empty = [1, '', '', buntPost()] as [number, string, string, Post];
|
||||||
if(!revs) {
|
if(!revs) {
|
||||||
return empty;
|
return empty;
|
||||||
}
|
}
|
||||||
@ -98,17 +98,16 @@ export function getLatestCommentRevision(node: GraphNode): [number, Post] {
|
|||||||
return [revNum.toJSNumber(), rev.post];
|
return [revNum.toJSNumber(), rev.post];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function getComments(node: GraphNode): GraphNode {
|
export function getComments(node: GraphNode): GraphNode {
|
||||||
const comments = node.children.get(bigInt(2));
|
const comments = node.children.get(bigInt(2));
|
||||||
if(!comments) {
|
if(!comments) {
|
||||||
return { post: buntPost(), children: new BigIntOrderedMap() }
|
return { post: buntPost(), children: new BigIntOrderedMap() };
|
||||||
}
|
}
|
||||||
return comments;
|
return comments;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSnippet(body: string) {
|
export function getSnippet(body: string) {
|
||||||
const start = body.slice(0, body.indexOf('\n', 2));
|
const start = body.slice(0, body.indexOf('\n', 2));
|
||||||
return (start === body || start.startsWith("![")) ? start : `${start}...`;
|
return (start === body || start.startsWith('![')) ? start : `${start}...`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
import _ from "lodash";
|
import _ from 'lodash';
|
||||||
|
|
||||||
export const alignY = ["top", "bottom"] as const;
|
export const alignY = ['top', 'bottom'] as const;
|
||||||
export type AlignY = typeof alignY[number];
|
export type AlignY = typeof alignY[number];
|
||||||
export const alignX = ["left", "right"] as const;
|
export const alignX = ['left', 'right'] as const;
|
||||||
export type AlignX = typeof alignX[number];
|
export type AlignX = typeof alignX[number];
|
||||||
|
|
||||||
export function getRelativePosition(
|
export function getRelativePosition(
|
||||||
relativeTo: HTMLElement | null,
|
relativeTo: HTMLElement | null,
|
||||||
alignX: AlignX | AlignX[],
|
alignX: AlignX | AlignX[],
|
||||||
alignY: AlignY | AlignY[],
|
alignY: AlignY | AlignY[],
|
||||||
offsetX: number = 0,
|
offsetX = 0,
|
||||||
offsetY: number = 0
|
offsetY = 0
|
||||||
) {
|
) {
|
||||||
const rect = relativeTo?.getBoundingClientRect();
|
const rect = relativeTo?.getBoundingClientRect();
|
||||||
if (!rect) {
|
if (!rect) {
|
||||||
@ -20,7 +20,7 @@ export function getRelativePosition(
|
|||||||
top: rect.top - offsetY,
|
top: rect.top - offsetY,
|
||||||
left: rect.left - offsetX,
|
left: rect.left - offsetX,
|
||||||
bottom: document.documentElement.clientHeight - rect.bottom - offsetY,
|
bottom: document.documentElement.clientHeight - rect.bottom - offsetY,
|
||||||
right: document.documentElement.clientWidth - rect.right - offsetX,
|
right: document.documentElement.clientWidth - rect.right - offsetX
|
||||||
};
|
};
|
||||||
const alignXArr = _.isArray(alignX) ? alignX : [alignX];
|
const alignXArr = _.isArray(alignX) ? alignX : [alignX];
|
||||||
const alignYArr = _.isArray(alignY) ? alignY : [alignY];
|
const alignYArr = _.isArray(alignY) ? alignY : [alignY];
|
||||||
@ -34,7 +34,7 @@ export function getRelativePosition(
|
|||||||
[...Array(idx), `${bounds[a]}px`],
|
[...Array(idx), `${bounds[a]}px`],
|
||||||
acc[a] || [],
|
acc[a] || [],
|
||||||
(a, b) => a || b || null
|
(a, b) => a || b || null
|
||||||
),
|
)
|
||||||
}),
|
}),
|
||||||
{}
|
{}
|
||||||
),
|
),
|
||||||
@ -46,10 +46,10 @@ export function getRelativePosition(
|
|||||||
[...Array(idx), `${bounds[a]}px`],
|
[...Array(idx), `${bounds[a]}px`],
|
||||||
acc[a] || [],
|
acc[a] || [],
|
||||||
(a, b) => a || b || null
|
(a, b) => a || b || null
|
||||||
),
|
)
|
||||||
}),
|
}),
|
||||||
{}
|
{}
|
||||||
),
|
)
|
||||||
} as Record<AlignY | AlignX, string[]>;
|
} as Record<AlignY | AlignX, string[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { TutorialProgress, Associations } from "~/types";
|
import { TutorialProgress, Associations } from '@urbit/api';
|
||||||
import { AlignX, AlignY } from "~/logic/lib/relativePosition";
|
import { AlignX, AlignY } from '~/logic/lib/relativePosition';
|
||||||
import { Direction } from "~/views/components/Triangle";
|
import { Direction } from '~/views/components/Triangle';
|
||||||
|
|
||||||
export const MODAL_WIDTH = 256;
|
export const MODAL_WIDTH = 256;
|
||||||
export const MODAL_HEIGHT = 256;
|
export const MODAL_HEIGHT = 256;
|
||||||
@ -44,7 +44,7 @@ export const getTrianglePosition = (dir: Direction) => {
|
|||||||
return {
|
return {
|
||||||
top: midY,
|
top: midY,
|
||||||
left: '-32px'
|
left: '-32px'
|
||||||
}
|
};
|
||||||
case 'North':
|
case 'North':
|
||||||
return {
|
return {
|
||||||
top: '-32px',
|
top: '-32px',
|
||||||
@ -56,112 +56,112 @@ export const getTrianglePosition = (dir: Direction) => {
|
|||||||
left: midX
|
left: midX
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export const progressDetails: Record<TutorialProgress, StepDetail> = {
|
export const progressDetails: Record<TutorialProgress, StepDetail> = {
|
||||||
hidden: {} as any,
|
hidden: {} as any,
|
||||||
exit: {} as any,
|
exit: {} as any,
|
||||||
done: {
|
done: {
|
||||||
title: "End",
|
title: 'End',
|
||||||
description:
|
description:
|
||||||
"This tutorial is finished. Would you like to leave Beginner Island?",
|
'This tutorial is finished. Would you like to leave Beginner Island?',
|
||||||
url: "/",
|
url: '/',
|
||||||
alignX: "right",
|
alignX: 'right',
|
||||||
alignY: "top",
|
alignY: 'top',
|
||||||
offsetX: MODAL_WIDTH + 8,
|
offsetX: MODAL_WIDTH + 8,
|
||||||
offsetY: 0,
|
offsetY: 0
|
||||||
},
|
},
|
||||||
start: {
|
start: {
|
||||||
title: "New Group added",
|
title: 'New Group added',
|
||||||
description:
|
description:
|
||||||
"We just added you to the Beginner island group to show you around. This group is public, but other groups can be private",
|
'We just added you to the Beginner island group to show you around. This group is public, but other groups can be private',
|
||||||
url: "/",
|
url: '/',
|
||||||
alignX: "right",
|
alignX: 'right',
|
||||||
alignY: "top",
|
alignY: 'top',
|
||||||
arrow: "West",
|
arrow: 'West',
|
||||||
offsetX: MODAL_WIDTH + 24,
|
offsetX: MODAL_WIDTH + 24,
|
||||||
offsetY: 64,
|
offsetY: 64
|
||||||
},
|
},
|
||||||
"group-desc": {
|
'group-desc': {
|
||||||
title: "What's a group",
|
title: 'What\'s a group',
|
||||||
description:
|
description:
|
||||||
"A group contains members and tends to be centered around a topic or multiple topics.",
|
'A group contains members and tends to be centered around a topic or multiple topics.',
|
||||||
url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}`,
|
url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}`,
|
||||||
alignX: "left",
|
alignX: 'left',
|
||||||
alignY: "top",
|
alignY: 'top',
|
||||||
arrow: "East",
|
arrow: 'East',
|
||||||
offsetX: MODAL_WIDTH + 24,
|
offsetX: MODAL_WIDTH + 24,
|
||||||
offsetY: 80,
|
offsetY: 80,
|
||||||
},
|
},
|
||||||
channels: {
|
channels: {
|
||||||
title: "Channels",
|
title: 'Channels',
|
||||||
description:
|
description:
|
||||||
"Inside a group you have three types of Channels: Chat, Collection, or Notebook. Mix and match these depending on your group context!",
|
'Inside a group you have three types of Channels: Chat, Collection, or Notebook. Mix and match these depending on your group context!',
|
||||||
url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}`,
|
url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}`,
|
||||||
alignY: "top",
|
alignY: 'top',
|
||||||
alignX: "right",
|
alignX: 'right',
|
||||||
arrow: "West",
|
arrow: 'West',
|
||||||
offsetX: MODAL_WIDTH + 24,
|
offsetX: MODAL_WIDTH + 24,
|
||||||
offsetY: -8,
|
offsetY: -8
|
||||||
},
|
},
|
||||||
chat: {
|
chat: {
|
||||||
title: "Chat",
|
title: 'Chat',
|
||||||
description:
|
description:
|
||||||
"Chat channels are for messaging within your group. Direct Messages are also supported, and are accessible from the “DMs” tile on the homescreen",
|
'Chat channels are for messaging within your group. Direct Messages are also supported, and are accessible from the “DMs” tile on the homescreen',
|
||||||
url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}/resource/chat/ship/${TUTORIAL_HOST}/${TUTORIAL_CHAT}`,
|
url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}/resource/chat/ship/${TUTORIAL_HOST}/${TUTORIAL_CHAT}`,
|
||||||
alignY: "top",
|
alignY: 'top',
|
||||||
arrow: "North",
|
arrow: 'North',
|
||||||
alignX: "right",
|
alignX: 'right',
|
||||||
offsetY: -56,
|
offsetY: -56,
|
||||||
offsetX: -8,
|
offsetX: -8
|
||||||
},
|
},
|
||||||
link: {
|
link: {
|
||||||
title: "Collection",
|
title: 'Collection',
|
||||||
description:
|
description:
|
||||||
"A collection is where you can share and view links, images, and other media within your group. Every item in a Collection can have it’s own comment thread.",
|
'A collection is where you can share and view links, images, and other media within your group. Every item in a Collection can have it’s own comment thread.',
|
||||||
url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}/resource/link/ship/${TUTORIAL_HOST}/${TUTORIAL_LINKS}`,
|
url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}/resource/link/ship/${TUTORIAL_HOST}/${TUTORIAL_LINKS}`,
|
||||||
alignY: "top",
|
alignY: 'top',
|
||||||
alignX: "right",
|
alignX: 'right',
|
||||||
arrow: "North",
|
arrow: 'North',
|
||||||
offsetX: -8,
|
offsetX: -8,
|
||||||
offsetY: -56,
|
offsetY: -56
|
||||||
},
|
},
|
||||||
publish: {
|
publish: {
|
||||||
title: "Notebook",
|
title: 'Notebook',
|
||||||
description:
|
description:
|
||||||
"Notebooks are for creating long-form content within your group. Use markdown to create rich posts with headers, lists and images.",
|
'Notebooks are for creating long-form content within your group. Use markdown to create rich posts with headers, lists and images.',
|
||||||
url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}/resource/publish/ship/${TUTORIAL_HOST}/${TUTORIAL_BOOK}`,
|
url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}/resource/publish/ship/${TUTORIAL_HOST}/${TUTORIAL_BOOK}`,
|
||||||
alignY: "top",
|
alignY: 'top',
|
||||||
alignX: "right",
|
alignX: 'right',
|
||||||
arrow: "North",
|
arrow: 'North',
|
||||||
offsetX: -8,
|
offsetX: -8,
|
||||||
offsetY: -56,
|
offsetY: -56
|
||||||
},
|
},
|
||||||
notifications: {
|
notifications: {
|
||||||
title: "Notifications",
|
title: 'Notifications',
|
||||||
description: "You will get updates from subscribed channels and mentions here. You can access Notifications through Leap.",
|
description: 'You will get updates from subscribed channels and mentions here. You can access Notifications through Leap.',
|
||||||
url: '/~notifications',
|
url: '/~notifications',
|
||||||
alignY: "top",
|
alignY: 'top',
|
||||||
alignX: "left",
|
alignX: 'left',
|
||||||
arrow: "North",
|
arrow: 'North',
|
||||||
offsetX: (MODAL_WIDTH / 2) - 16,
|
offsetX: (MODAL_WIDTH / 2) - 16,
|
||||||
offsetY: -48,
|
offsetY: -48
|
||||||
},
|
},
|
||||||
profile: {
|
profile: {
|
||||||
title: "Profile",
|
title: 'Profile',
|
||||||
description:
|
description:
|
||||||
"Your profile is customizable and can be shared with other ships. Enter as much or as little information as you’d like.",
|
'Your profile is customizable and can be shared with other ships. Enter as much or as little information as you’d like.',
|
||||||
url: `/~profile/~${window.ship}`,
|
url: `/~profile/~${window.ship}`,
|
||||||
alignY: "top",
|
alignY: 'top',
|
||||||
alignX: "right",
|
alignX: 'right',
|
||||||
arrow: "South",
|
arrow: 'South',
|
||||||
offsetX: -300 + MODAL_WIDTH / 2,
|
offsetX: -300 + MODAL_WIDTH / 2,
|
||||||
offsetY: -60,
|
offsetY: -60,
|
||||||
},
|
},
|
||||||
leap: {
|
leap: {
|
||||||
title: "Leap",
|
title: 'Leap',
|
||||||
description:
|
description:
|
||||||
"Leap allows you to go to a specific channel, message, collection, profile or group simply by typing in a command or selecting a shortcut from the dropdown menu.",
|
'Leap allows you to go to a specific channel, message, collection, profile or group simply by typing in a command or selecting a shortcut from the dropdown menu.',
|
||||||
url: `/~profile/~${window.ship}`,
|
url: `/~profile/~${window.ship}`,
|
||||||
alignY: "top",
|
alignY: "top",
|
||||||
alignX: "left",
|
alignX: "left",
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useState, useCallback, useMemo, useEffect } from "react";
|
import { useState, useCallback, useMemo, useEffect } from 'react';
|
||||||
|
|
||||||
function validateDragEvent(e: DragEvent): FileList | File[] | true | null {
|
function validateDragEvent(e: DragEvent): FileList | File[] | true | null {
|
||||||
const files: File[] = [];
|
const files: File[] = [];
|
||||||
@ -8,8 +8,8 @@ function validateDragEvent(e: DragEvent): FileList | File[] | true | null {
|
|||||||
}
|
}
|
||||||
if (e.dataTransfer?.items) {
|
if (e.dataTransfer?.items) {
|
||||||
Array.from(e.dataTransfer.items || [])
|
Array.from(e.dataTransfer.items || [])
|
||||||
.filter((i) => i.kind === 'file')
|
.filter(i => i.kind === 'file')
|
||||||
.forEach(f => {
|
.forEach((f) => {
|
||||||
valid = true; // Valid if file exists, but on DragOver, won't reveal its contents for security
|
valid = true; // Valid if file exists, but on DragOver, won't reveal its contents for security
|
||||||
const data = f.getAsFile();
|
const data = f.getAsFile();
|
||||||
if (data) {
|
if (data) {
|
||||||
@ -89,14 +89,14 @@ export function useFileDrag(dragged: (f: FileList | File[], e: DragEvent) => voi
|
|||||||
document.body.addEventListener('mouseout', mouseleave);
|
document.body.addEventListener('mouseout', mouseleave);
|
||||||
return () => {
|
return () => {
|
||||||
document.body.removeEventListener('mouseout', mouseleave);
|
document.body.removeEventListener('mouseout', mouseleave);
|
||||||
}
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const bind = {
|
const bind = {
|
||||||
onDragLeave,
|
onDragLeave,
|
||||||
onDragOver,
|
onDragOver,
|
||||||
onDrop,
|
onDrop,
|
||||||
onDragEnter,
|
onDragEnter
|
||||||
};
|
};
|
||||||
|
|
||||||
return { bind, dragging };
|
return { bind, dragging };
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect, useMemo, useCallback } from "react";
|
import { useState, useEffect, useMemo, useCallback } from 'react';
|
||||||
|
|
||||||
export function useDropdown<C>(
|
export function useDropdown<C>(
|
||||||
candidates: C[],
|
candidates: C[],
|
||||||
@ -12,10 +12,10 @@ export function useDropdown<C>(
|
|||||||
(s: string) => {
|
(s: string) => {
|
||||||
const exactMatch = isExact(s);
|
const exactMatch = isExact(s);
|
||||||
const exact = exactMatch ? [exactMatch] : [];
|
const exact = exactMatch ? [exactMatch] : [];
|
||||||
const opts = [...new Set([...exact, ...candidates.filter((c) => searchPred(s, c))])];
|
const opts = [...new Set([...exact, ...candidates.filter(c => searchPred(s, c))])];
|
||||||
setOptions(opts);
|
setOptions(opts);
|
||||||
if (selected) {
|
if (selected) {
|
||||||
const idx = opts.findIndex((c) => key(c) === key(selected));
|
const idx = opts.findIndex(c => key(c) === key(selected));
|
||||||
if (idx < 0) {
|
if (idx < 0) {
|
||||||
setSelected(undefined);
|
setSelected(undefined);
|
||||||
}
|
}
|
||||||
@ -29,9 +29,11 @@ export function useDropdown<C>(
|
|||||||
const select = (idx: number) => {
|
const select = (idx: number) => {
|
||||||
setSelected(options[idx]);
|
setSelected(options[idx]);
|
||||||
};
|
};
|
||||||
if(!selected) { select(0); return false; }
|
if(!selected) {
|
||||||
|
select(0); return false;
|
||||||
|
}
|
||||||
|
|
||||||
const idx = options.findIndex((c) => key(c) === key(selected));
|
const idx = options.findIndex(c => key(c) === key(selected));
|
||||||
if (
|
if (
|
||||||
idx === -1 ||
|
idx === -1 ||
|
||||||
(options.length - 1 <= idx && !backward)
|
(options.length - 1 <= idx && !backward)
|
||||||
@ -55,6 +57,6 @@ export function useDropdown<C>(
|
|||||||
back,
|
back,
|
||||||
search,
|
search,
|
||||||
selected,
|
selected,
|
||||||
options,
|
options
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import {useLocation} from "react-router-dom";
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
|
||||||
export function useHashLink() {
|
export function useHashLink() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@ -10,8 +9,5 @@ export function useHashLink() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
document.querySelector(location.hash)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
document.querySelector(location.hash)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
|
||||||
}, [location.hash]);
|
}, [location.hash]);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useEffect, RefObject, useRef, useState } from "react";
|
import { useEffect, RefObject, useRef, useState } from 'react';
|
||||||
import _ from "lodash";
|
import _ from 'lodash';
|
||||||
import usePreviousValue from "./usePreviousValue";
|
import usePreviousValue from './usePreviousValue';
|
||||||
|
|
||||||
export function distanceToBottom(el: HTMLElement) {
|
export function distanceToBottom(el: HTMLElement) {
|
||||||
const { scrollTop, scrollHeight, clientHeight } = el;
|
const { scrollTop, scrollHeight, clientHeight } = el;
|
||||||
@ -40,7 +40,6 @@ export function useLazyScroll(
|
|||||||
}
|
}
|
||||||
}, [count]);
|
}, [count]);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ref.current) {
|
if (!ref.current) {
|
||||||
return;
|
return;
|
||||||
@ -54,13 +53,12 @@ export function useLazyScroll(
|
|||||||
loadUntil(el);
|
loadUntil(el);
|
||||||
};
|
};
|
||||||
|
|
||||||
ref.current.addEventListener("scroll", onScroll, { passive: true });
|
ref.current.addEventListener('scroll', onScroll, { passive: true });
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
ref.current?.removeEventListener("scroll", onScroll);
|
ref.current?.removeEventListener('scroll', onScroll);
|
||||||
};
|
};
|
||||||
}, [ref?.current, count]);
|
}, [ref?.current, count]);
|
||||||
|
|
||||||
|
|
||||||
return { isDone, isLoading };
|
return { isDone, isLoading };
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useState, useCallback, useEffect } from "react";
|
import { useState, useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
function retrieve<T>(key: string, initial: T): T {
|
function retrieve<T>(key: string, initial: T): T {
|
||||||
const s = localStorage.getItem(key);
|
const s = localStorage.getItem(key);
|
||||||
@ -25,7 +25,7 @@ export function useLocalStorageState<T>(key: string, initial: T) {
|
|||||||
|
|
||||||
const setState = useCallback(
|
const setState = useCallback(
|
||||||
(s: SetState<T>) => {
|
(s: SetState<T>) => {
|
||||||
const updated = typeof s === "function" ? s(state) : s;
|
const updated = typeof s === 'function' ? s(state) : s;
|
||||||
_setState(updated);
|
_setState(updated);
|
||||||
localStorage.setItem(key, JSON.stringify(updated));
|
localStorage.setItem(key, JSON.stringify(updated));
|
||||||
},
|
},
|
||||||
|
@ -5,15 +5,15 @@ import React, {
|
|||||||
SyntheticEvent,
|
SyntheticEvent,
|
||||||
useMemo,
|
useMemo,
|
||||||
useEffect,
|
useEffect,
|
||||||
useRef,
|
useRef
|
||||||
} from "react";
|
} from 'react';
|
||||||
|
|
||||||
import { Box } from "@tlon/indigo-react";
|
import { Box } from '@tlon/indigo-react';
|
||||||
import { useOutsideClick } from "./useOutsideClick";
|
import { useOutsideClick } from './useOutsideClick';
|
||||||
import { ModalOverlay } from "~/views/components/ModalOverlay";
|
import { ModalOverlay } from '~/views/components/ModalOverlay';
|
||||||
import {Portal} from "~/views/components/Portal";
|
import { Portal } from '~/views/components/Portal';
|
||||||
import {ModalPortal} from "~/views/components/ModalPortal";
|
import { ModalPortal } from '~/views/components/ModalPortal';
|
||||||
import {PropFunc} from "~/types";
|
import { PropFunc } from '@urbit/api';
|
||||||
|
|
||||||
type ModalFunc = (dismiss: () => void) => JSX.Element;
|
type ModalFunc = (dismiss: () => void) => JSX.Element;
|
||||||
interface UseModalProps {
|
interface UseModalProps {
|
||||||
@ -42,7 +42,7 @@ export function useModal(props: UseModalProps & PropFunc<typeof Box>): UseModalR
|
|||||||
() =>
|
() =>
|
||||||
!modalShown
|
!modalShown
|
||||||
? null
|
? null
|
||||||
: typeof modal === "function"
|
: typeof modal === 'function'
|
||||||
? modal(dismiss)
|
? modal(dismiss)
|
||||||
: modal,
|
: modal,
|
||||||
[modalShown, modal, dismiss]
|
[modalShown, modal, dismiss]
|
||||||
@ -59,7 +59,7 @@ export function useModal(props: UseModalProps & PropFunc<typeof Box>): UseModalR
|
|||||||
bg="white"
|
bg="white"
|
||||||
borderRadius={2}
|
borderRadius={2}
|
||||||
border={[0, 1]}
|
border={[0, 1]}
|
||||||
borderColor={["washedGray", "washedGray"]}
|
borderColor={['washedGray', 'washedGray']}
|
||||||
display="flex"
|
display="flex"
|
||||||
alignItems="stretch"
|
alignItems="stretch"
|
||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
@ -76,6 +76,6 @@ export function useModal(props: UseModalProps & PropFunc<typeof Box>): UseModalR
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
showModal,
|
showModal,
|
||||||
modal: modalComponent,
|
modal: modalComponent
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { useEffect, RefObject } from "react";
|
import { useEffect, RefObject } from 'react';
|
||||||
|
|
||||||
export function useOutsideClick(
|
export function useOutsideClick(
|
||||||
ref: RefObject<HTMLElement | null | undefined>,
|
ref: RefObject<HTMLElement | null | undefined>,
|
||||||
onClick: () => void,
|
onClick: () => void
|
||||||
) {
|
) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleClick(event: MouseEvent) {
|
function handleClick(event: MouseEvent) {
|
||||||
@ -16,17 +16,16 @@ export function useOutsideClick(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleKeyDown(ev) {
|
function handleKeyDown(ev) {
|
||||||
if(ev.key === "Escape") {
|
if(ev.key === 'Escape') {
|
||||||
onClick();
|
onClick();
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
document.addEventListener("mousedown", handleClick);
|
document.addEventListener('mousedown', handleClick);
|
||||||
document.addEventListener("keydown", handleKeyDown);
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener("mousedown", handleClick);
|
document.removeEventListener('mousedown', handleClick);
|
||||||
document.removeEventListener("keydown", handleKeyDown);
|
document.removeEventListener('keydown', handleKeyDown);
|
||||||
};
|
};
|
||||||
}, [ref.current, onClick]);
|
}, [ref.current, onClick]);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
import { useRef } from "react";
|
import { useRef } from 'react';
|
||||||
import { Primitive } from "~/types";
|
import { Primitive } from '@urbit/api';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default function usePreviousValue<T extends Primitive>(value: T): T {
|
export default function usePreviousValue<T extends Primitive>(value: T): T {
|
||||||
const prev = useRef<T | null>(null);
|
const prev = useRef<T | null>(null);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useMemo, useCallback } from "react";
|
import { useMemo, useCallback } from 'react';
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from 'react-router-dom';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
export function useQuery() {
|
export function useQuery() {
|
||||||
@ -25,6 +25,6 @@ export function useQuery() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
query,
|
query,
|
||||||
appendQuery,
|
appendQuery
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useMemo, useEffect, useRef, useState } from "react";
|
import { useCallback, useMemo, useEffect, useRef, useState } from 'react';
|
||||||
import { S3State } from "../../types/s3-update";
|
import { S3State } from '../../types/s3-update';
|
||||||
import S3 from "aws-sdk/clients/s3";
|
import S3 from 'aws-sdk/clients/s3';
|
||||||
import { dateToDa, deSig } from "./util";
|
import { dateToDa, deSig } from './util';
|
||||||
|
|
||||||
export interface IuseS3 {
|
export interface IuseS3 {
|
||||||
canUpload: boolean;
|
canUpload: boolean;
|
||||||
@ -28,14 +28,14 @@ const useS3 = (s3: S3State, { accept = '*' } = { accept: '*' }): IuseS3 => {
|
|||||||
|
|
||||||
const canUpload = useMemo(
|
const canUpload = useMemo(
|
||||||
() =>
|
() =>
|
||||||
(client && s3.credentials && s3.configuration.currentBucket !== "") || false,
|
(client && s3.credentials && s3.configuration.currentBucket !== '') || false,
|
||||||
[s3.credentials, s3.configuration.currentBucket, client]
|
[s3.credentials, s3.configuration.currentBucket, client]
|
||||||
);
|
);
|
||||||
|
|
||||||
const upload = useCallback(
|
const upload = useCallback(
|
||||||
async (file: File, bucket: string) => {
|
async (file: File, bucket: string) => {
|
||||||
if (!client.current) {
|
if (!client.current) {
|
||||||
throw new Error("S3 not ready");
|
throw new Error('S3 not ready');
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileParts = file.name.split('.');
|
const fileParts = file.name.split('.');
|
||||||
@ -47,8 +47,8 @@ const useS3 = (s3: S3State, { accept = '*' } = { accept: '*' }): IuseS3 => {
|
|||||||
Bucket: bucket,
|
Bucket: bucket,
|
||||||
Key: `${window.ship}/${timestamp}-${fileName}.${fileExtension}`,
|
Key: `${window.ship}/${timestamp}-${fileName}.${fileExtension}`,
|
||||||
Body: file,
|
Body: file,
|
||||||
ACL: "public-read",
|
ACL: 'public-read',
|
||||||
ContentType: file.type,
|
ContentType: file.type
|
||||||
};
|
};
|
||||||
|
|
||||||
setUploading(true);
|
setUploading(true);
|
||||||
@ -63,8 +63,8 @@ const useS3 = (s3: S3State, { accept = '*' } = { accept: '*' }): IuseS3 => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const uploadDefault = useCallback(async (file: File) => {
|
const uploadDefault = useCallback(async (file: File) => {
|
||||||
if (s3.configuration.currentBucket === "") {
|
if (s3.configuration.currentBucket === '') {
|
||||||
throw new Error("current bucket not set");
|
throw new Error('current bucket not set');
|
||||||
}
|
}
|
||||||
return upload(file, s3.configuration.currentBucket);
|
return upload(file, s3.configuration.currentBucket);
|
||||||
}, [s3]);
|
}, [s3]);
|
||||||
@ -84,11 +84,10 @@ const useS3 = (s3: S3State, { accept = '*' } = { accept: '*' }): IuseS3 => {
|
|||||||
}
|
}
|
||||||
uploadDefault(files[0]).then(resolve);
|
uploadDefault(files[0]).then(resolve);
|
||||||
document.body.removeChild(fileSelector);
|
document.body.removeChild(fileSelector);
|
||||||
})
|
});
|
||||||
document.body.appendChild(fileSelector);
|
document.body.appendChild(fileSelector);
|
||||||
fileSelector.click();
|
fileSelector.click();
|
||||||
})
|
});
|
||||||
|
|
||||||
},
|
},
|
||||||
[uploadDefault]
|
[uploadDefault]
|
||||||
);
|
);
|
||||||
|
@ -1,23 +1,23 @@
|
|||||||
import { MouseEvent, useCallback, useState, useEffect } from "react";
|
import { MouseEvent, useCallback, useState, useEffect } from 'react';
|
||||||
export type AsyncClickableState = "waiting" | "error" | "loading" | "success";
|
export type AsyncClickableState = 'waiting' | 'error' | 'loading' | 'success';
|
||||||
|
|
||||||
export function useStatelessAsyncClickable(
|
export function useStatelessAsyncClickable(
|
||||||
onClick: (e: MouseEvent) => Promise<void>,
|
onClick: (e: MouseEvent) => Promise<void>,
|
||||||
name: string
|
name: string
|
||||||
) {
|
) {
|
||||||
const [state, setState] = useState<ButtonState>("waiting");
|
const [state, setState] = useState<ButtonState>('waiting');
|
||||||
const handleClick = useCallback(
|
const handleClick = useCallback(
|
||||||
async (e: MouseEvent) => {
|
async (e: MouseEvent) => {
|
||||||
try {
|
try {
|
||||||
setState("loading");
|
setState('loading');
|
||||||
await onClick(e);
|
await onClick(e);
|
||||||
setState("success");
|
setState('success');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
setState("error");
|
setState('error');
|
||||||
} finally {
|
} finally {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setState("waiting");
|
setState('waiting');
|
||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -26,7 +26,7 @@ export function useStatelessAsyncClickable(
|
|||||||
|
|
||||||
// When name changes, reset button
|
// When name changes, reset button
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setState("waiting");
|
setState('waiting');
|
||||||
}, [name]);
|
}, [name]);
|
||||||
|
|
||||||
return { buttonState: state, onClick: handleClick };
|
return { buttonState: state, onClick: handleClick };
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
|
||||||
|
export function useWaitForProps<P>(props: P, timeout = 0) {
|
||||||
export function useWaitForProps<P>(props: P, timeout: number = 0) {
|
|
||||||
const [resolve, setResolve] = useState<() => void>(() => () => {});
|
const [resolve, setResolve] = useState<() => void>(() => () => {});
|
||||||
const [ready, setReady] = useState<(p: P) => boolean | undefined>();
|
const [ready, setReady] = useState<(p: P) => boolean | undefined>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof ready === "function" && ready(props)) {
|
if (typeof ready === 'function' && ready(props)) {
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
}, [props, ready, resolve]);
|
}, [props, ready, resolve]);
|
||||||
@ -26,7 +25,7 @@ export function useWaitForProps<P>(props: P, timeout: number = 0) {
|
|||||||
setResolve(() => resolve);
|
setResolve(() => resolve);
|
||||||
if(timeout > 0) {
|
if(timeout > 0) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
reject(new Error("Timed out"));
|
reject(new Error('Timed out'));
|
||||||
}, timeout);
|
}, timeout);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,27 +1,27 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import _ from "lodash";
|
import _ from 'lodash';
|
||||||
import f, { memoize } from "lodash/fp";
|
import f, { memoize } from 'lodash/fp';
|
||||||
import bigInt, { BigInteger } from "big-integer";
|
import bigInt, { BigInteger } from 'big-integer';
|
||||||
import { Contact } from '~/types';
|
import { Contact } from '@urbit/api';
|
||||||
import useLocalState from '../state/local';
|
import useLocalState from '../state/local';
|
||||||
|
|
||||||
export const MOBILE_BROWSER_REGEX = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i;
|
export const MOBILE_BROWSER_REGEX = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i;
|
||||||
|
|
||||||
export const MOMENT_CALENDAR_DATE = {
|
export const MOMENT_CALENDAR_DATE = {
|
||||||
sameDay: "[Today]",
|
sameDay: '[Today]',
|
||||||
nextDay: "[Tomorrow]",
|
nextDay: '[Tomorrow]',
|
||||||
nextWeek: "dddd",
|
nextWeek: 'dddd',
|
||||||
lastDay: "[Yesterday]",
|
lastDay: '[Yesterday]',
|
||||||
lastWeek: "[Last] dddd",
|
lastWeek: '[Last] dddd',
|
||||||
sameElse: "~YYYY.M.D",
|
sameElse: '~YYYY.M.D'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getModuleIcon = (mod: string) => {
|
export const getModuleIcon = (mod: string) => {
|
||||||
if (mod === "link") {
|
if (mod === 'link') {
|
||||||
return "Collection";
|
return 'Collection';
|
||||||
}
|
}
|
||||||
return _.capitalize(mod);
|
return _.capitalize(mod);
|
||||||
}
|
};
|
||||||
|
|
||||||
export function wait(ms: number) {
|
export function wait(ms: number) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@ -37,8 +37,8 @@ export function parentPath(path: string) {
|
|||||||
return _.dropRight(path.split('/'), 1).join('/');
|
return _.dropRight(path.split('/'), 1).join('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
const DA_UNIX_EPOCH = bigInt("170141184475152167957503069145530368000"); // `@ud` ~1970.1.1
|
const DA_UNIX_EPOCH = bigInt('170141184475152167957503069145530368000'); // `@ud` ~1970.1.1
|
||||||
const DA_SECOND = bigInt("18446744073709551616"); // `@ud` ~s1
|
const DA_SECOND = bigInt('18446744073709551616'); // `@ud` ~s1
|
||||||
export function daToUnix(da: BigInteger) {
|
export function daToUnix(da: BigInteger) {
|
||||||
// ported from +time:enjs:format in hoon.hoon
|
// ported from +time:enjs:format in hoon.hoon
|
||||||
const offset = DA_SECOND.divide(bigInt(2000));
|
const offset = DA_SECOND.divide(bigInt(2000));
|
||||||
@ -59,20 +59,20 @@ export function makePatDa(patda: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function udToDec(ud: string): string {
|
export function udToDec(ud: string): string {
|
||||||
return ud.replace(/\./g, "");
|
return ud.replace(/\./g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function decToUd(str: string): string {
|
export function decToUd(str: string): string {
|
||||||
return _.trimStart(
|
return _.trimStart(
|
||||||
f.flow(
|
f.flow(
|
||||||
f.split(""),
|
f.split(''),
|
||||||
f.reverse,
|
f.reverse,
|
||||||
f.chunk(3),
|
f.chunk(3),
|
||||||
f.map(f.flow(f.reverse, f.join(""))),
|
f.map(f.flow(f.reverse, f.join(''))),
|
||||||
f.reverse,
|
f.reverse,
|
||||||
f.join(".")
|
f.join('.')
|
||||||
)(str),
|
)(str),
|
||||||
"0."
|
'0.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,12 +86,12 @@ export function clamp(x: number, min: number, max: number) {
|
|||||||
// color is a #000000 color
|
// color is a #000000 color
|
||||||
export function adjustHex(color: string, amount: number): string {
|
export function adjustHex(color: string, amount: number): string {
|
||||||
return f.flow(
|
return f.flow(
|
||||||
f.split(""),
|
f.split(''),
|
||||||
f.chunk(2), // get RGB channels
|
f.chunk(2), // get RGB channels
|
||||||
f.map((c) => parseInt(c.join(""), 16)), // as hex
|
f.map(c => parseInt(c.join(''), 16)), // as hex
|
||||||
f.map((c) => clamp(c + amount, 0, 255).toString(16)), // adjust
|
f.map(c => clamp(c + amount, 0, 255).toString(16)), // adjust
|
||||||
f.join(""),
|
f.join(''),
|
||||||
(res) => `#${res}` //format
|
res => `#${res}` // format
|
||||||
)(color.slice(1));
|
)(color.slice(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,12 +101,12 @@ export function resourceAsPath(resource: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function uuid() {
|
export function uuid() {
|
||||||
let str = "0v";
|
let str = '0v';
|
||||||
str += Math.ceil(Math.random() * 8) + ".";
|
str += Math.ceil(Math.random() * 8) + '.';
|
||||||
for (let i = 0; i < 5; i++) {
|
for (let i = 0; i < 5; i++) {
|
||||||
let _str = Math.ceil(Math.random() * 10000000).toString(32);
|
let _str = Math.ceil(Math.random() * 10000000).toString(32);
|
||||||
_str = ("00000" + _str).substr(-5, 5);
|
_str = ('00000' + _str).substr(-5, 5);
|
||||||
str += _str + ".";
|
str += _str + '.';
|
||||||
}
|
}
|
||||||
|
|
||||||
return str.slice(0, -1);
|
return str.slice(0, -1);
|
||||||
@ -120,11 +120,11 @@ export function uuid() {
|
|||||||
*/
|
*/
|
||||||
export function daToDate(st: string) {
|
export function daToDate(st: string) {
|
||||||
const dub = function (n: string) {
|
const dub = function (n: string) {
|
||||||
return parseInt(n) < 10 ? "0" + parseInt(n) : n.toString();
|
return parseInt(n) < 10 ? '0' + parseInt(n) : n.toString();
|
||||||
};
|
};
|
||||||
const da = st.split("..");
|
const da = st.split('..');
|
||||||
const bigEnd = da[0].split(".");
|
const bigEnd = da[0].split('.');
|
||||||
const lilEnd = da[1].split(".");
|
const lilEnd = da[1].split('.');
|
||||||
const ds = `${bigEnd[0].slice(1)}-${dub(bigEnd[1])}-${dub(bigEnd[2])}T${dub(
|
const ds = `${bigEnd[0].slice(1)}-${dub(bigEnd[1])}-${dub(bigEnd[2])}T${dub(
|
||||||
lilEnd[0]
|
lilEnd[0]
|
||||||
)}:${dub(lilEnd[1])}:${dub(lilEnd[2])}Z`;
|
)}:${dub(lilEnd[1])}:${dub(lilEnd[2])}Z`;
|
||||||
@ -138,9 +138,9 @@ export function daToDate(st: string) {
|
|||||||
~2018.7.17..23.15.09..5be5 // urbit @da
|
~2018.7.17..23.15.09..5be5 // urbit @da
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function dateToDa(d: Date, mil: boolean = false) {
|
export function dateToDa(d: Date, mil = false) {
|
||||||
const fil = function (n: number) {
|
const fil = function (n: number) {
|
||||||
return n >= 10 ? n : "0" + n;
|
return n >= 10 ? n : '0' + n;
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
`~${d.getUTCFullYear()}.` +
|
`~${d.getUTCFullYear()}.` +
|
||||||
@ -149,7 +149,7 @@ export function dateToDa(d: Date, mil: boolean = false) {
|
|||||||
`${fil(d.getUTCHours())}.` +
|
`${fil(d.getUTCHours())}.` +
|
||||||
`${fil(d.getUTCMinutes())}.` +
|
`${fil(d.getUTCMinutes())}.` +
|
||||||
`${fil(d.getUTCSeconds())}` +
|
`${fil(d.getUTCSeconds())}` +
|
||||||
`${mil ? "..0000" : ""}`
|
`${mil ? '..0000' : ''}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,16 +157,16 @@ export function deSig(ship: string) {
|
|||||||
if (!ship) {
|
if (!ship) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return ship.replace("~", "");
|
return ship.replace('~', '');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function uxToHex(ux: string) {
|
export function uxToHex(ux: string) {
|
||||||
if (ux.length > 2 && ux.substr(0, 2) === "0x") {
|
if (ux.length > 2 && ux.substr(0, 2) === '0x') {
|
||||||
const value = ux.substr(2).replace(".", "").padStart(6, "0");
|
const value = ux.substr(2).replace('.', '').padStart(6, '0');
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = ux.replace(".", "").padStart(6, "0");
|
const value = ux.replace('.', '').padStart(6, '0');
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -187,13 +187,13 @@ export function writeText(str: string) {
|
|||||||
|
|
||||||
let success = false;
|
let success = false;
|
||||||
function listener(e) {
|
function listener(e) {
|
||||||
e.clipboardData.setData("text/plain", str);
|
e.clipboardData.setData('text/plain', str);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
success = true;
|
success = true;
|
||||||
}
|
}
|
||||||
document.addEventListener("copy", listener);
|
document.addEventListener('copy', listener);
|
||||||
document.execCommand("copy");
|
document.execCommand('copy');
|
||||||
document.removeEventListener("copy", listener);
|
document.removeEventListener('copy', listener);
|
||||||
|
|
||||||
document?.getSelection()?.removeAllRanges();
|
document?.getSelection()?.removeAllRanges();
|
||||||
|
|
||||||
@ -206,21 +206,21 @@ export function writeText(str: string) {
|
|||||||
// trim patps to match dojo, chat-cli
|
// trim patps to match dojo, chat-cli
|
||||||
export function cite(ship: string) {
|
export function cite(ship: string) {
|
||||||
let patp = ship,
|
let patp = ship,
|
||||||
shortened = "";
|
shortened = '';
|
||||||
if (patp === null || patp === "") {
|
if (patp === null || patp === '') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (patp.startsWith("~")) {
|
if (patp.startsWith('~')) {
|
||||||
patp = patp.substr(1);
|
patp = patp.substr(1);
|
||||||
}
|
}
|
||||||
// comet
|
// comet
|
||||||
if (patp.length === 56) {
|
if (patp.length === 56) {
|
||||||
shortened = "~" + patp.slice(0, 6) + "_" + patp.slice(50, 56);
|
shortened = '~' + patp.slice(0, 6) + '_' + patp.slice(50, 56);
|
||||||
return shortened;
|
return shortened;
|
||||||
}
|
}
|
||||||
// moon
|
// moon
|
||||||
if (patp.length === 27) {
|
if (patp.length === 27) {
|
||||||
shortened = "~" + patp.slice(14, 20) + "^" + patp.slice(21, 27);
|
shortened = '~' + patp.slice(14, 20) + '^' + patp.slice(21, 27);
|
||||||
return shortened;
|
return shortened;
|
||||||
}
|
}
|
||||||
return `~${patp}`;
|
return `~${patp}`;
|
||||||
@ -232,7 +232,6 @@ export function alphabeticalOrder(a: string, b: string) {
|
|||||||
|
|
||||||
export function lengthOrder(a: string, b: string) {
|
export function lengthOrder(a: string, b: string) {
|
||||||
return b.length - a.length;
|
return b.length - a.length;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: deprecated
|
// TODO: deprecated
|
||||||
@ -244,13 +243,13 @@ export function alphabetiseAssociations(associations: any) {
|
|||||||
let bName = b.substr(1);
|
let bName = b.substr(1);
|
||||||
if (associations[a].metadata && associations[a].metadata.title) {
|
if (associations[a].metadata && associations[a].metadata.title) {
|
||||||
aName =
|
aName =
|
||||||
associations[a].metadata.title !== ""
|
associations[a].metadata.title !== ''
|
||||||
? associations[a].metadata.title
|
? associations[a].metadata.title
|
||||||
: a.substr(1);
|
: a.substr(1);
|
||||||
}
|
}
|
||||||
if (associations[b].metadata && associations[b].metadata.title) {
|
if (associations[b].metadata && associations[b].metadata.title) {
|
||||||
bName =
|
bName =
|
||||||
associations[b].metadata.title !== ""
|
associations[b].metadata.title !== ''
|
||||||
? associations[b].metadata.title
|
? associations[b].metadata.title
|
||||||
: b.substr(1);
|
: b.substr(1);
|
||||||
}
|
}
|
||||||
@ -266,41 +265,42 @@ export function alphabetiseAssociations(associations: any) {
|
|||||||
// for example, 'some Chars!' becomes '~.some.~43.hars~21.'
|
// for example, 'some Chars!' becomes '~.some.~43.hars~21.'
|
||||||
//
|
//
|
||||||
export function stringToTa(str: string) {
|
export function stringToTa(str: string) {
|
||||||
let out = "";
|
let out = '';
|
||||||
for (let i = 0; i < str.length; i++) {
|
for (let i = 0; i < str.length; i++) {
|
||||||
const char = str[i];
|
const char = str[i];
|
||||||
let add = "";
|
let add = '';
|
||||||
switch (char) {
|
switch (char) {
|
||||||
case " ":
|
case ' ':
|
||||||
add = ".";
|
add = '.';
|
||||||
break;
|
break;
|
||||||
case ".":
|
case '.':
|
||||||
add = "~.";
|
add = '~.';
|
||||||
break;
|
break;
|
||||||
case "~":
|
case '~':
|
||||||
add = "~~";
|
add = '~~';
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
const charCode = str.charCodeAt(i);
|
const charCode = str.charCodeAt(i);
|
||||||
if (
|
if (
|
||||||
(charCode >= 97 && charCode <= 122) || // a-z
|
(charCode >= 97 && charCode <= 122) || // a-z
|
||||||
(charCode >= 48 && charCode <= 57) || // 0-9
|
(charCode >= 48 && charCode <= 57) || // 0-9
|
||||||
char === "-"
|
char === '-'
|
||||||
) {
|
) {
|
||||||
add = char;
|
add = char;
|
||||||
} else {
|
} else {
|
||||||
// TODO behavior for unicode doesn't match +wood's,
|
// TODO behavior for unicode doesn't match +wood's,
|
||||||
// but we can probably get away with that for now.
|
// but we can probably get away with that for now.
|
||||||
add = "~" + charCode.toString(16) + ".";
|
add = '~' + charCode.toString(16) + '.';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out = out + add;
|
out = out + add;
|
||||||
}
|
}
|
||||||
return "~." + out;
|
return '~.' + out;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function amOwnerOfGroup(groupPath: string) {
|
export function amOwnerOfGroup(groupPath: string) {
|
||||||
if (!groupPath) return false;
|
if (!groupPath)
|
||||||
|
return false;
|
||||||
const groupOwner = /(\/~)?\/~([a-z-]{3,})\/.*/.exec(groupPath)?.[2];
|
const groupOwner = /(\/~)?\/~([a-z-]{3,})\/.*/.exec(groupPath)?.[2];
|
||||||
return window.ship === groupOwner;
|
return window.ship === groupOwner;
|
||||||
}
|
}
|
||||||
@ -308,18 +308,18 @@ export function amOwnerOfGroup(groupPath: string) {
|
|||||||
export function getContactDetails(contact: any) {
|
export function getContactDetails(contact: any) {
|
||||||
const member = !contact;
|
const member = !contact;
|
||||||
contact = contact || {
|
contact = contact || {
|
||||||
nickname: "",
|
nickname: '',
|
||||||
avatar: null,
|
avatar: null,
|
||||||
color: "0x0",
|
color: '0x0'
|
||||||
};
|
};
|
||||||
const nickname = contact.nickname || "";
|
const nickname = contact.nickname || '';
|
||||||
const color = uxToHex(contact.color || "0x0");
|
const color = uxToHex(contact.color || '0x0');
|
||||||
const avatar = contact.avatar || null;
|
const avatar = contact.avatar || null;
|
||||||
return { nickname, color, member, avatar };
|
return { nickname, color, member, avatar };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function stringToSymbol(str: string) {
|
export function stringToSymbol(str: string) {
|
||||||
let result = "";
|
let result = '';
|
||||||
for (let i = 0; i < str.length; i++) {
|
for (let i = 0; i < str.length; i++) {
|
||||||
const n = str.charCodeAt(i);
|
const n = str.charCodeAt(i);
|
||||||
if ((n >= 97 && n <= 122) || (n >= 48 && n <= 57)) {
|
if ((n >= 97 && n <= 122) || (n >= 48 && n <= 57)) {
|
||||||
@ -327,19 +327,17 @@ export function stringToSymbol(str: string) {
|
|||||||
} else if (n >= 65 && n <= 90) {
|
} else if (n >= 65 && n <= 90) {
|
||||||
result += String.fromCharCode(n + 32);
|
result += String.fromCharCode(n + 32);
|
||||||
} else {
|
} else {
|
||||||
result += "-";
|
result += '-';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result = result.replace(/^[\-\d]+|\-+/g, "-");
|
result = result.replace(/^[\-\d]+|\-+/g, '-');
|
||||||
result = result.replace(/^\-+|\-+$/g, "");
|
result = result.replace(/^\-+|\-+$/g, '');
|
||||||
if (result === "") {
|
if (result === '') {
|
||||||
return dateToDa(new Date());
|
return dateToDa(new Date());
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formats a numbers as a `@ud` inserting dot where needed
|
* Formats a numbers as a `@ud` inserting dot where needed
|
||||||
*/
|
*/
|
||||||
@ -351,23 +349,24 @@ export function numToUd(num: number) {
|
|||||||
f.reverse,
|
f.reverse,
|
||||||
f.map(s => s.join('')),
|
f.map(s => s.join('')),
|
||||||
f.join('.')
|
f.join('.')
|
||||||
)(num.toString())
|
)(num.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePreventWindowUnload(shouldPreventDefault: boolean, message = "You have unsaved changes. Are you sure you want to exit?") {
|
export function usePreventWindowUnload(shouldPreventDefault: boolean, message = 'You have unsaved changes. Are you sure you want to exit?') {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!shouldPreventDefault) return;
|
if (!shouldPreventDefault)
|
||||||
const handleBeforeUnload = event => {
|
return;
|
||||||
|
const handleBeforeUnload = (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
return message;
|
return message;
|
||||||
}
|
};
|
||||||
window.addEventListener("beforeunload", handleBeforeUnload);
|
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||||
window.onbeforeunload = handleBeforeUnload;
|
window.onbeforeunload = handleBeforeUnload;
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener("beforeunload", handleBeforeUnload);
|
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
window.onbeforeunload = undefined;
|
window.onbeforeunload = undefined;
|
||||||
}
|
};
|
||||||
}, [shouldPreventDefault]);
|
}, [shouldPreventDefault]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -378,7 +377,7 @@ export function pluralize(text: string, isPlural = false, vowel = false) {
|
|||||||
// Hide is an optional second parameter for when this function is used in class components
|
// Hide is an optional second parameter for when this function is used in class components
|
||||||
export function useShowNickname(contact: Contact | null, hide?: boolean): boolean {
|
export function useShowNickname(contact: Contact | null, hide?: boolean): boolean {
|
||||||
const hideNicknames = typeof hide !== 'undefined' ? hide : useLocalState(state => state.hideNicknames);
|
const hideNicknames = typeof hide !== 'undefined' ? hide : useLocalState(state => state.hideNicknames);
|
||||||
return !!(contact && contact.nickname && !hideNicknames);
|
return Boolean(contact && contact.nickname && !hideNicknames);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface useHoveringInterface {
|
interface useHoveringInterface {
|
||||||
@ -406,7 +405,6 @@ export function getItemTitle(association: Association) {
|
|||||||
return cite(`~${name.slice(4)}`);
|
return cite(`~${name.slice(4)}`);
|
||||||
}
|
}
|
||||||
return cite(ship);
|
return cite(ship);
|
||||||
|
|
||||||
}
|
}
|
||||||
return association.metadata.title || association.resource
|
return association.metadata.title || association.resource;
|
||||||
};
|
}
|
||||||
|
@ -1,24 +1,24 @@
|
|||||||
import { Associations, Workspace } from "~/types";
|
import { Associations, Workspace } from '@urbit/api';
|
||||||
|
|
||||||
export function getTitleFromWorkspace(
|
export function getTitleFromWorkspace(
|
||||||
associations: Associations,
|
associations: Associations,
|
||||||
workspace: Workspace
|
workspace: Workspace
|
||||||
) {
|
) {
|
||||||
switch (workspace.type) {
|
switch (workspace.type) {
|
||||||
case "home":
|
case 'home':
|
||||||
return "My Channels";
|
return 'My Channels';
|
||||||
case "messages":
|
case 'messages':
|
||||||
return "Messages";
|
return 'Messages';
|
||||||
case "group":
|
case 'group':
|
||||||
const association = associations.groups[workspace.group];
|
const association = associations.groups[workspace.group];
|
||||||
return association?.metadata?.title || "";
|
return association?.metadata?.title || '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getGroupFromWorkspace(
|
export function getGroupFromWorkspace(
|
||||||
workspace: Workspace
|
workspace: Workspace
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
if (workspace.type === "group") {
|
if (workspace.type === 'group') {
|
||||||
return workspace.group;
|
return workspace.group;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { StoreState } from '../../store/type';
|
import { StoreState } from '../../store/type';
|
||||||
import { Cage } from '~/types/cage';
|
import { Cage } from '~/types/cage';
|
||||||
import { ContactUpdate } from '~/types/contact-update';
|
import { ContactUpdate } from '@urbit/api/contacts';
|
||||||
import { resourceAsPath } from '../lib/util';
|
import { resourceAsPath } from '../lib/util';
|
||||||
|
|
||||||
type ContactState = Pick<StoreState, 'contacts'>;
|
type ContactState = Pick<StoreState, 'contacts'>;
|
||||||
@ -78,4 +78,3 @@ const setPublic = (json: ContactUpdate, state: S) => {
|
|||||||
state.isContactPublic = data;
|
state.isContactPublic = data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@ -10,9 +10,9 @@ import {
|
|||||||
OpenPolicyDiff,
|
OpenPolicyDiff,
|
||||||
OpenPolicy,
|
OpenPolicy,
|
||||||
InvitePolicyDiff,
|
InvitePolicyDiff,
|
||||||
InvitePolicy,
|
InvitePolicy
|
||||||
} from '~/types/group-update';
|
} from '@urbit/api/groups';
|
||||||
import { Enc, PatpNoSig } from '~/types/noun';
|
import { Enc, PatpNoSig } from '@urbit/api';
|
||||||
import { resourceAsPath } from '../lib/util';
|
import { resourceAsPath } from '../lib/util';
|
||||||
|
|
||||||
type GroupState = Pick<StoreState, 'groups' | 'groupKeys'>;
|
type GroupState = Pick<StoreState, 'groups' | 'groupKeys'>;
|
||||||
@ -23,7 +23,7 @@ function decodeGroup(group: Enc<Group>): Group {
|
|||||||
...group,
|
...group,
|
||||||
members,
|
members,
|
||||||
tags: decodeTags(group.tags),
|
tags: decodeTags(group.tags),
|
||||||
policy: decodePolicy(group.policy),
|
policy: decodePolicy(group.policy)
|
||||||
};
|
};
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
@ -35,7 +35,7 @@ function decodePolicy(policy: Enc<GroupPolicy>): GroupPolicy {
|
|||||||
} else {
|
} else {
|
||||||
const { open } = policy;
|
const { open } = policy;
|
||||||
return {
|
return {
|
||||||
open: { banned: new Set(open.banned), banRanks: new Set(open.banRanks) },
|
open: { banned: new Set(open.banned), banRanks: new Set(open.banRanks) }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -98,7 +98,7 @@ export default class GroupReducer<S extends GroupState> {
|
|||||||
members: new Set(),
|
members: new Set(),
|
||||||
tags: { role: { admin: new Set([window.ship]) } },
|
tags: { role: { admin: new Set([window.ship]) } },
|
||||||
policy: decodePolicy(policy),
|
policy: decodePolicy(policy),
|
||||||
hidden,
|
hidden
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -189,7 +189,6 @@ export default class GroupReducer<S extends GroupState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private inviteChangePolicy(diff: InvitePolicyDiff, policy: InvitePolicy) {
|
private inviteChangePolicy(diff: InvitePolicyDiff, policy: InvitePolicy) {
|
||||||
if ('addInvites' in diff) {
|
if ('addInvites' in diff) {
|
||||||
const { addInvites } = diff;
|
const { addInvites } = diff;
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import { resourceAsPath } from "~/logic/lib/util";
|
import { resourceAsPath } from '~/logic/lib/util';
|
||||||
|
|
||||||
|
|
||||||
const initial = (json: any, state: any) => {
|
const initial = (json: any, state: any) => {
|
||||||
const data = json.initial;
|
const data = json.initial;
|
||||||
if(data) {
|
if(data) {
|
||||||
state.pendingJoin = data;
|
state.pendingJoin = data;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const progress = (json: any, state: any) => {
|
const progress = (json: any, state: any) => {
|
||||||
const data = json.progress;
|
const data = json.progress;
|
||||||
@ -19,7 +18,7 @@ const progress = (json: any, state: any) => {
|
|||||||
}, 10000);
|
}, 10000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export const GroupViewReducer = (json: any, state: any) => {
|
export const GroupViewReducer = (json: any, state: any) => {
|
||||||
const data = json['group-view-update'];
|
const data = json['group-view-update'];
|
||||||
@ -27,4 +26,4 @@ export const GroupViewReducer = (json: any, state: any) => {
|
|||||||
progress(data, state);
|
progress(data, state);
|
||||||
initial(data, state);
|
initial(data, state);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
@ -3,22 +3,21 @@ import {
|
|||||||
NotifIndex,
|
NotifIndex,
|
||||||
NotificationGraphConfig,
|
NotificationGraphConfig,
|
||||||
GroupNotificationsConfig,
|
GroupNotificationsConfig,
|
||||||
UnreadStats,
|
UnreadStats
|
||||||
} from "~/types";
|
} from '@urbit/api';
|
||||||
import { makePatDa } from "~/logic/lib/util";
|
import { makePatDa } from '~/logic/lib/util';
|
||||||
import _ from "lodash";
|
import _ from 'lodash';
|
||||||
import {StoreState} from "../store/type";
|
import { StoreState } from '../store/type';
|
||||||
import { BigIntOrderedMap } from '../lib/BigIntOrderedMap';
|
import { BigIntOrderedMap } from '../lib/BigIntOrderedMap';
|
||||||
|
|
||||||
type HarkState = Pick<StoreState, "notifications" | "notificationsGraphConfig" | "notificationsGroupConfig" | "unreads" >;
|
type HarkState = Pick<StoreState, 'notifications' | 'notificationsGraphConfig' | 'notificationsGroupConfig' | 'unreads' >;
|
||||||
|
|
||||||
|
|
||||||
export const HarkReducer = (json: any, state: HarkState) => {
|
export const HarkReducer = (json: any, state: HarkState) => {
|
||||||
const data = _.get(json, "harkUpdate", false);
|
const data = _.get(json, 'harkUpdate', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
reduce(data, state);
|
reduce(data, state);
|
||||||
}
|
}
|
||||||
const graphHookData = _.get(json, "hark-graph-hook-update", false);
|
const graphHookData = _.get(json, 'hark-graph-hook-update', false);
|
||||||
if (graphHookData) {
|
if (graphHookData) {
|
||||||
graphInitial(graphHookData, state);
|
graphInitial(graphHookData, state);
|
||||||
graphIgnore(graphHookData, state);
|
graphIgnore(graphHookData, state);
|
||||||
@ -26,7 +25,7 @@ export const HarkReducer = (json: any, state: HarkState) => {
|
|||||||
graphWatchSelf(graphHookData, state);
|
graphWatchSelf(graphHookData, state);
|
||||||
graphMentions(graphHookData, state);
|
graphMentions(graphHookData, state);
|
||||||
}
|
}
|
||||||
const groupHookData = _.get(json, "hark-group-hook-update", false);
|
const groupHookData = _.get(json, 'hark-group-hook-update', false);
|
||||||
if (groupHookData) {
|
if (groupHookData) {
|
||||||
groupInitial(groupHookData, state);
|
groupInitial(groupHookData, state);
|
||||||
groupListen(groupHookData, state);
|
groupListen(groupHookData, state);
|
||||||
@ -35,31 +34,31 @@ export const HarkReducer = (json: any, state: HarkState) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function groupInitial(json: any, state: HarkState) {
|
function groupInitial(json: any, state: HarkState) {
|
||||||
const data = _.get(json, "initial", false);
|
const data = _.get(json, 'initial', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
state.notificationsGroupConfig = data;
|
state.notificationsGroupConfig = data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function graphInitial(json: any, state: HarkState) {
|
function graphInitial(json: any, state: HarkState) {
|
||||||
const data = _.get(json, "initial", false);
|
const data = _.get(json, 'initial', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
state.notificationsGraphConfig = data;
|
state.notificationsGraphConfig = data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function graphListen(json: any, state: HarkState) {
|
function graphListen(json: any, state: HarkState) {
|
||||||
const data = _.get(json, "listen", false);
|
const data = _.get(json, 'listen', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
state.notificationsGraphConfig.watching = [
|
state.notificationsGraphConfig.watching = [
|
||||||
...state.notificationsGraphConfig.watching,
|
...state.notificationsGraphConfig.watching,
|
||||||
data,
|
data
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function graphIgnore(json: any, state: HarkState) {
|
function graphIgnore(json: any, state: HarkState) {
|
||||||
const data = _.get(json, "ignore", false);
|
const data = _.get(json, 'ignore', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
state.notificationsGraphConfig.watching = state.notificationsGraphConfig.watching.filter(
|
state.notificationsGraphConfig.watching = state.notificationsGraphConfig.watching.filter(
|
||||||
({ graph, index }) => !(graph === data.graph && index === data.index)
|
({ graph, index }) => !(graph === data.graph && index === data.index)
|
||||||
@ -68,30 +67,30 @@ function graphIgnore(json: any, state: HarkState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function groupListen(json: any, state: HarkState) {
|
function groupListen(json: any, state: HarkState) {
|
||||||
const data = _.get(json, "listen", false);
|
const data = _.get(json, 'listen', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
state.notificationsGroupConfig = [...state.notificationsGroupConfig, data];
|
state.notificationsGroupConfig = [...state.notificationsGroupConfig, data];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function groupIgnore(json: any, state: HarkState) {
|
function groupIgnore(json: any, state: HarkState) {
|
||||||
const data = _.get(json, "ignore", false);
|
const data = _.get(json, 'ignore', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
state.notificationsGroupConfig = state.notificationsGroupConfig.filter(
|
state.notificationsGroupConfig = state.notificationsGroupConfig.filter(
|
||||||
(n) => n !== data
|
n => n !== data
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function graphMentions(json: any, state: HarkState) {
|
function graphMentions(json: any, state: HarkState) {
|
||||||
const data = _.get(json, "set-mentions", undefined);
|
const data = _.get(json, 'set-mentions', undefined);
|
||||||
if (!_.isUndefined(data)) {
|
if (!_.isUndefined(data)) {
|
||||||
state.notificationsGraphConfig.mentions = data;
|
state.notificationsGraphConfig.mentions = data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function graphWatchSelf(json: any, state: HarkState) {
|
function graphWatchSelf(json: any, state: HarkState) {
|
||||||
const data = _.get(json, "set-watch-on-self", undefined);
|
const data = _.get(json, 'set-watch-on-self', undefined);
|
||||||
if (!_.isUndefined(data)) {
|
if (!_.isUndefined(data)) {
|
||||||
state.notificationsGraphConfig.watchOnSelf = data;
|
state.notificationsGraphConfig.watchOnSelf = data;
|
||||||
}
|
}
|
||||||
@ -139,14 +138,14 @@ function seenIndex(json: any, state: HarkState) {
|
|||||||
function readEach(json: any, state: HarkState) {
|
function readEach(json: any, state: HarkState) {
|
||||||
const data = _.get(json, 'read-each');
|
const data = _.get(json, 'read-each');
|
||||||
if(data) {
|
if(data) {
|
||||||
updateUnreads(state, data.index, u => u.delete(data.target))
|
updateUnreads(state, data.index, u => u.delete(data.target));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function readSince(json: any, state: HarkState) {
|
function readSince(json: any, state: HarkState) {
|
||||||
const data = _.get(json, 'read-count');
|
const data = _.get(json, 'read-count');
|
||||||
if(data) {
|
if(data) {
|
||||||
updateUnreadCount(state, data, () => 0)
|
updateUnreadCount(state, data, () => 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,7 +159,7 @@ function unreadSince(json: any, state: HarkState) {
|
|||||||
function unreadEach(json: any, state: HarkState) {
|
function unreadEach(json: any, state: HarkState) {
|
||||||
const data = _.get(json, 'unread-each');
|
const data = _.get(json, 'unread-each');
|
||||||
if(data) {
|
if(data) {
|
||||||
updateUnreads(state, data.index, us => us.add(data.target))
|
updateUnreads(state, data.index, us => us.add(data.target));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,14 +183,14 @@ function unreads(json: any, state: HarkState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function clearState(state) {
|
function clearState(state) {
|
||||||
let initialState = {
|
const initialState = {
|
||||||
notifications: new BigIntOrderedMap<Timebox>(),
|
notifications: new BigIntOrderedMap<Timebox>(),
|
||||||
archivedNotifications: new BigIntOrderedMap<Timebox>(),
|
archivedNotifications: new BigIntOrderedMap<Timebox>(),
|
||||||
notificationsGroupConfig: [],
|
notificationsGroupConfig: [],
|
||||||
notificationsGraphConfig: {
|
notificationsGraphConfig: {
|
||||||
watchOnSelf: false,
|
watchOnSelf: false,
|
||||||
mentions: false,
|
mentions: false,
|
||||||
watching: [],
|
watching: []
|
||||||
},
|
},
|
||||||
unreads: {
|
unreads: {
|
||||||
graph: {},
|
graph: {},
|
||||||
@ -200,7 +199,7 @@ function clearState(state){
|
|||||||
notificationsCount: 0
|
notificationsCount: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.keys(initialState).forEach(key => {
|
Object.keys(initialState).forEach((key) => {
|
||||||
state[key] = initialState[key];
|
state[key] = initialState[key];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -211,7 +210,7 @@ function updateUnreadCount(state: HarkState, index: NotifIndex, count: (c: numbe
|
|||||||
}
|
}
|
||||||
const property = [index.graph.graph, index.graph.index, 'unreads'];
|
const property = [index.graph.graph, index.graph.index, 'unreads'];
|
||||||
const curr = _.get(state.unreads.graph, property, 0);
|
const curr = _.get(state.unreads.graph, property, 0);
|
||||||
const newCount = count(curr)
|
const newCount = count(curr);
|
||||||
_.set(state.unreads.graph, property, newCount);
|
_.set(state.unreads.graph, property, newCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -226,7 +225,6 @@ function updateUnreads(state: HarkState, index: NotifIndex, f: (us: Set<string>)
|
|||||||
_.set(state.unreads.graph, [index.graph.graph, index.graph.index, 'unreads'], unreads);
|
_.set(state.unreads.graph, [index.graph.graph, index.graph.index, 'unreads'], unreads);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function updateNotificationStats(state: HarkState, index: NotifIndex, statField: 'notifications' | 'unreads' | 'last', f: (x: number) => number) {
|
function updateNotificationStats(state: HarkState, index: NotifIndex, statField: 'notifications' | 'unreads' | 'last', f: (x: number) => number) {
|
||||||
if(statField === 'notifications') {
|
if(statField === 'notifications') {
|
||||||
state.notificationsCount = f(state.notificationsCount);
|
state.notificationsCount = f(state.notificationsCount);
|
||||||
@ -241,13 +239,13 @@ function updateNotificationStats(state: HarkState, index: NotifIndex, statField:
|
|||||||
}
|
}
|
||||||
|
|
||||||
function added(json: any, state: HarkState) {
|
function added(json: any, state: HarkState) {
|
||||||
const data = _.get(json, "added", false);
|
const data = _.get(json, 'added', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
const { index, notification } = data;
|
const { index, notification } = data;
|
||||||
const time = makePatDa(data.time);
|
const time = makePatDa(data.time);
|
||||||
const timebox = state.notifications.get(time) || [];
|
const timebox = state.notifications.get(time) || [];
|
||||||
|
|
||||||
const arrIdx = timebox.findIndex((idxNotif) =>
|
const arrIdx = timebox.findIndex(idxNotif =>
|
||||||
notifIdxEqual(index, idxNotif.index)
|
notifIdxEqual(index, idxNotif.index)
|
||||||
);
|
);
|
||||||
if (arrIdx !== -1) {
|
if (arrIdx !== -1) {
|
||||||
@ -264,14 +262,14 @@ function added(json: any, state: HarkState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const dnd = (json: any, state: HarkState) => {
|
const dnd = (json: any, state: HarkState) => {
|
||||||
const data = _.get(json, "set-dnd", undefined);
|
const data = _.get(json, 'set-dnd', undefined);
|
||||||
if (!_.isUndefined(data)) {
|
if (!_.isUndefined(data)) {
|
||||||
state.doNotDisturb = data;
|
state.doNotDisturb = data;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const timebox = (json: any, state: HarkState) => {
|
const timebox = (json: any, state: HarkState) => {
|
||||||
const data = _.get(json, "timebox", false);
|
const data = _.get(json, 'timebox', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
const time = makePatDa(data.time);
|
const time = makePatDa(data.time);
|
||||||
if (!data.archive) {
|
if (!data.archive) {
|
||||||
@ -281,21 +279,21 @@ const timebox = (json: any, state: HarkState) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function more(json: any, state: HarkState) {
|
function more(json: any, state: HarkState) {
|
||||||
const data = _.get(json, "more", false);
|
const data = _.get(json, 'more', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
_.forEach(data, (d) => reduce(d, state));
|
_.forEach(data, d => reduce(d, state));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function notifIdxEqual(a: NotifIndex, b: NotifIndex) {
|
function notifIdxEqual(a: NotifIndex, b: NotifIndex) {
|
||||||
if ("graph" in a && "graph" in b) {
|
if ('graph' in a && 'graph' in b) {
|
||||||
return (
|
return (
|
||||||
a.graph.graph === b.graph.graph &&
|
a.graph.graph === b.graph.graph &&
|
||||||
a.graph.group === b.graph.group &&
|
a.graph.group === b.graph.group &&
|
||||||
a.graph.module === b.graph.module &&
|
a.graph.module === b.graph.module &&
|
||||||
a.graph.description === b.graph.description
|
a.graph.description === b.graph.description
|
||||||
);
|
);
|
||||||
} else if ("group" in a && "group" in b) {
|
} else if ('group' in a && 'group' in b) {
|
||||||
return (
|
return (
|
||||||
a.group.group === b.group.group &&
|
a.group.group === b.group.group &&
|
||||||
a.group.description === b.group.description
|
a.group.description === b.group.description
|
||||||
@ -313,14 +311,14 @@ function setRead(
|
|||||||
const patDa = makePatDa(time);
|
const patDa = makePatDa(time);
|
||||||
const timebox = state.notifications.get(patDa);
|
const timebox = state.notifications.get(patDa);
|
||||||
if (_.isNull(timebox)) {
|
if (_.isNull(timebox)) {
|
||||||
console.warn("Modifying nonexistent timebox");
|
console.warn('Modifying nonexistent timebox');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const arrIdx = timebox.findIndex((idxNotif) =>
|
const arrIdx = timebox.findIndex(idxNotif =>
|
||||||
notifIdxEqual(index, idxNotif.index)
|
notifIdxEqual(index, idxNotif.index)
|
||||||
);
|
);
|
||||||
if (arrIdx === -1) {
|
if (arrIdx === -1) {
|
||||||
console.warn("Modifying nonexistent index");
|
console.warn('Modifying nonexistent index');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
timebox[arrIdx].notification.read = read;
|
timebox[arrIdx].notification.read = read;
|
||||||
@ -328,7 +326,7 @@ function setRead(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function read(json: any, state: HarkState) {
|
function read(json: any, state: HarkState) {
|
||||||
const data = _.get(json, "read-note", false);
|
const data = _.get(json, 'read-note', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
const { time, index } = data;
|
const { time, index } = data;
|
||||||
updateNotificationStats(state, index, 'notifications', x => x-1);
|
updateNotificationStats(state, index, 'notifications', x => x-1);
|
||||||
@ -337,7 +335,7 @@ function read(json: any, state: HarkState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function unread(json: any, state: HarkState) {
|
function unread(json: any, state: HarkState) {
|
||||||
const data = _.get(json, "unread-note", false);
|
const data = _.get(json, 'unread-note', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
const { time, index } = data;
|
const { time, index } = data;
|
||||||
updateNotificationStats(state, index, 'notifications', x => x+1);
|
updateNotificationStats(state, index, 'notifications', x => x+1);
|
||||||
@ -346,16 +344,16 @@ function unread(json: any, state: HarkState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function archive(json: any, state: HarkState) {
|
function archive(json: any, state: HarkState) {
|
||||||
const data = _.get(json, "archive", false);
|
const data = _.get(json, 'archive', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
const { index } = data;
|
const { index } = data;
|
||||||
const time = makePatDa(data.time);
|
const time = makePatDa(data.time);
|
||||||
const timebox = state.notifications.get(time);
|
const timebox = state.notifications.get(time);
|
||||||
if (!timebox) {
|
if (!timebox) {
|
||||||
console.warn("Modifying nonexistent timebox");
|
console.warn('Modifying nonexistent timebox');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const [archived, unarchived] = _.partition(timebox, (idxNotif) =>
|
const [archived, unarchived] = _.partition(timebox, idxNotif =>
|
||||||
notifIdxEqual(index, idxNotif.index)
|
notifIdxEqual(index, idxNotif.index)
|
||||||
);
|
);
|
||||||
if(unarchived.length === 0) {
|
if(unarchived.length === 0) {
|
||||||
@ -365,6 +363,6 @@ function archive(json: any, state: HarkState) {
|
|||||||
state.notifications.set(time, unarchived);
|
state.notifications.set(time, unarchived);
|
||||||
}
|
}
|
||||||
const newlyRead = archived.filter(x => !x.notification.read).length;
|
const newlyRead = archived.filter(x => !x.notification.read).length;
|
||||||
updateNotificationStats(state, index, 'notifications', (x) => x - newlyRead);
|
updateNotificationStats(state, index, 'notifications', x => x - newlyRead);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { StoreState } from '../../store/type';
|
import { StoreState } from '../../store/type';
|
||||||
import { Cage } from '~/types/cage';
|
import { Cage } from '~/types/cage';
|
||||||
import { InviteUpdate } from '~/types/invite-update';
|
import { InviteUpdate } from '@urbit/api/invite';
|
||||||
|
|
||||||
type InviteState = Pick<StoreState, "invites">;
|
|
||||||
|
|
||||||
|
type InviteState = Pick<StoreState, 'invites'>;
|
||||||
|
|
||||||
export default class InviteReducer<S extends InviteState> {
|
export default class InviteReducer<S extends InviteState> {
|
||||||
reduce(json: Cage, state: S) {
|
reduce(json: Cage, state: S) {
|
||||||
|
@ -51,12 +51,11 @@ export default class LaunchReducer<S extends LaunchState> {
|
|||||||
changeIsShown(json: LaunchUpdate, state: S) {
|
changeIsShown(json: LaunchUpdate, state: S) {
|
||||||
const data = _.get(json, 'changeIsShown', false);
|
const data = _.get(json, 'changeIsShown', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
let tile = state.launch.tiles[data.name];
|
const tile = state.launch.tiles[data.name];
|
||||||
console.log(tile);
|
console.log(tile);
|
||||||
if (tile) {
|
if (tile) {
|
||||||
tile.isShown = data.isShown;
|
tile.isShown = data.isShown;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -2,14 +2,14 @@ import _ from 'lodash';
|
|||||||
|
|
||||||
import { StoreState } from '../../store/type';
|
import { StoreState } from '../../store/type';
|
||||||
|
|
||||||
import { MetadataUpdate } from '~/types/metadata-update';
|
import { MetadataUpdate } from '@urbit/api/metadata';
|
||||||
import { Cage } from '~/types/cage';
|
import { Cage } from '~/types/cage';
|
||||||
|
|
||||||
type MetadataState = Pick<StoreState, 'associations'>;
|
type MetadataState = Pick<StoreState, 'associations'>;
|
||||||
|
|
||||||
export default class MetadataReducer<S extends MetadataState> {
|
export default class MetadataReducer<S extends MetadataState> {
|
||||||
reduce(json: Cage, state: S) {
|
reduce(json: Cage, state: S) {
|
||||||
let data = json['metadata-update']
|
const data = json['metadata-update'];
|
||||||
if (data) {
|
if (data) {
|
||||||
console.log(data);
|
console.log(data);
|
||||||
this.associations(data, state);
|
this.associations(data, state);
|
||||||
@ -29,13 +29,13 @@ export default class MetadataReducer<S extends MetadataState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
associations(json: MetadataUpdate, state: S) {
|
associations(json: MetadataUpdate, state: S) {
|
||||||
let data = _.get(json, 'associations', false);
|
const data = _.get(json, 'associations', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
let metadata = state.associations;
|
const metadata = state.associations;
|
||||||
Object.keys(data).forEach((key) => {
|
Object.keys(data).forEach((key) => {
|
||||||
let val = data[key];
|
const val = data[key];
|
||||||
let appName = val['app-name'];
|
const appName = val['app-name'];
|
||||||
let rid = val.resource;
|
const rid = val.resource;
|
||||||
if (!(appName in metadata)) {
|
if (!(appName in metadata)) {
|
||||||
metadata[appName] = {};
|
metadata[appName] = {};
|
||||||
}
|
}
|
||||||
@ -50,11 +50,11 @@ export default class MetadataReducer<S extends MetadataState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
add(json: MetadataUpdate, state: S) {
|
add(json: MetadataUpdate, state: S) {
|
||||||
let data = _.get(json, 'add', false);
|
const data = _.get(json, 'add', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
let metadata = state.associations;
|
const metadata = state.associations;
|
||||||
let appName = data['app-name'];
|
const appName = data['app-name'];
|
||||||
let appPath = data.resource;
|
const appPath = data.resource;
|
||||||
|
|
||||||
if (!(appName in metadata)) {
|
if (!(appName in metadata)) {
|
||||||
metadata[appName] = {};
|
metadata[appName] = {};
|
||||||
@ -69,11 +69,11 @@ export default class MetadataReducer<S extends MetadataState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
update(json: MetadataUpdate, state: S) {
|
update(json: MetadataUpdate, state: S) {
|
||||||
let data = _.get(json, 'update-metadata', false);
|
const data = _.get(json, 'update-metadata', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
let metadata = state.associations;
|
const metadata = state.associations;
|
||||||
let appName = data['app-name'];
|
const appName = data['app-name'];
|
||||||
let rid = data.resource;
|
const rid = data.resource;
|
||||||
|
|
||||||
if (!(appName in metadata)) {
|
if (!(appName in metadata)) {
|
||||||
metadata[appName] = {};
|
metadata[appName] = {};
|
||||||
@ -88,11 +88,11 @@ export default class MetadataReducer<S extends MetadataState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
remove(json: MetadataUpdate, state: S) {
|
remove(json: MetadataUpdate, state: S) {
|
||||||
let data = _.get(json, 'remove', false);
|
const data = _.get(json, 'remove', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
let metadata = state.associations;
|
const metadata = state.associations;
|
||||||
let appName = data['app-name'];
|
const appName = data['app-name'];
|
||||||
let rid = data.resource;
|
const rid = data.resource;
|
||||||
|
|
||||||
if (appName in metadata && rid in metadata[appName]) {
|
if (appName in metadata && rid in metadata[appName]) {
|
||||||
delete metadata[appName][rid];
|
delete metadata[appName][rid];
|
||||||
|
@ -1,21 +1,21 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { StoreState } from '../../store/type';
|
import { StoreState } from '../../store/type';
|
||||||
import {
|
import {
|
||||||
SettingsUpdate,
|
SettingsUpdate
|
||||||
} from '~/types/settings';
|
} from '@urbit/api/settings';
|
||||||
|
|
||||||
type SettingsState = Pick<StoreState, 'settings'>;
|
type SettingsState = Pick<StoreState, 'settings'>;
|
||||||
|
|
||||||
export default class SettingsReducer<S extends SettingsState> {
|
export default class SettingsReducer<S extends SettingsState> {
|
||||||
reduce(json: Cage, state: S) {
|
reduce(json: Cage, state: S) {
|
||||||
let data = json["settings-event"];
|
let data = json['settings-event'];
|
||||||
if (data) {
|
if (data) {
|
||||||
this.putBucket(data, state);
|
this.putBucket(data, state);
|
||||||
this.delBucket(data, state);
|
this.delBucket(data, state);
|
||||||
this.putEntry(data, state);
|
this.putEntry(data, state);
|
||||||
this.delEntry(data, state);
|
this.delEntry(data, state);
|
||||||
}
|
}
|
||||||
data = json["settings-data"];
|
data = json['settings-data'];
|
||||||
if (data) {
|
if (data) {
|
||||||
this.getAll(data, state);
|
this.getAll(data, state);
|
||||||
this.getBucket(data, state);
|
this.getBucket(data, state);
|
||||||
@ -26,31 +26,31 @@ export default class SettingsReducer<S extends SettingsState>{
|
|||||||
putBucket(json: SettingsUpdate, state: S) {
|
putBucket(json: SettingsUpdate, state: S) {
|
||||||
const data = _.get(json, 'put-bucket', false);
|
const data = _.get(json, 'put-bucket', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
state.settings[data["bucket-key"]] = data.bucket;
|
state.settings[data['bucket-key']] = data.bucket;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
delBucket(json: SettingsUpdate, state: S) {
|
delBucket(json: SettingsUpdate, state: S) {
|
||||||
const data = _.get(json, 'del-bucket', false);
|
const data = _.get(json, 'del-bucket', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
delete state.settings[data["bucket-key"]];
|
delete state.settings[data['bucket-key']];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
putEntry(json: SettingsUpdate, state: S) {
|
putEntry(json: SettingsUpdate, state: S) {
|
||||||
const data = _.get(json, 'put-entry', false);
|
const data = _.get(json, 'put-entry', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
if (!state.settings[data["bucket-key"]]) {
|
if (!state.settings[data['bucket-key']]) {
|
||||||
state.settings[data["bucket-key"]] = {};
|
state.settings[data['bucket-key']] = {};
|
||||||
}
|
}
|
||||||
state.settings[data["bucket-key"]][data["entry-key"]] = data.value;
|
state.settings[data['bucket-key']][data['entry-key']] = data.value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
delEntry(json: SettingsUpdate, state: S) {
|
delEntry(json: SettingsUpdate, state: S) {
|
||||||
const data = _.get(json, 'del-entry', false);
|
const data = _.get(json, 'del-entry', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
delete state.settings[data["bucket-key"]][data["entry-key"]];
|
delete state.settings[data['bucket-key']][data['entry-key']];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import React, { ReactNode } from "react";
|
import React, { ReactNode } from 'react';
|
||||||
import f from 'lodash/fp';
|
import f from 'lodash/fp';
|
||||||
import create, { State } from 'zustand';
|
import create, { State } from 'zustand';
|
||||||
import { persist } from 'zustand/middleware';
|
import { persist } from 'zustand/middleware';
|
||||||
import produce from 'immer';
|
import produce from 'immer';
|
||||||
import { BackgroundConfig, RemoteContentPolicy, TutorialProgress, tutorialProgress } from "~/types/local-update";
|
import { BackgroundConfig, RemoteContentPolicy, TutorialProgress, tutorialProgress } from '~/types/local-update';
|
||||||
|
|
||||||
|
|
||||||
export interface LocalState extends State {
|
export interface LocalState extends State {
|
||||||
hideAvatars: boolean;
|
hideAvatars: boolean;
|
||||||
@ -22,7 +21,7 @@ export interface LocalState extends State {
|
|||||||
suspendedFocus?: HTMLElement;
|
suspendedFocus?: HTMLElement;
|
||||||
toggleOmnibox: () => void;
|
toggleOmnibox: () => void;
|
||||||
set: (fn: (state: LocalState) => void) => void
|
set: (fn: (state: LocalState) => void) => void
|
||||||
};
|
}
|
||||||
export const selectLocalState =
|
export const selectLocalState =
|
||||||
<K extends keyof LocalState>(keys: K[]) => f.pick<LocalState, K>(keys);
|
<K extends keyof LocalState>(keys: K[]) => f.pick<LocalState, K>(keys);
|
||||||
|
|
||||||
@ -33,21 +32,21 @@ const useLocalState = create<LocalState>(persist((set, get) => ({
|
|||||||
hideNicknames: false,
|
hideNicknames: false,
|
||||||
tutorialProgress: 'hidden',
|
tutorialProgress: 'hidden',
|
||||||
tutorialRef: null,
|
tutorialRef: null,
|
||||||
setTutorialRef: (el: HTMLElement | null) => set(produce(state => {
|
setTutorialRef: (el: HTMLElement | null) => set(produce((state) => {
|
||||||
state.tutorialRef = el;
|
state.tutorialRef = el;
|
||||||
})),
|
})),
|
||||||
hideTutorial: () => set(produce(state => {
|
hideTutorial: () => set(produce((state) => {
|
||||||
state.tutorialProgress = 'hidden';
|
state.tutorialProgress = 'hidden';
|
||||||
state.tutorialRef = null;
|
state.tutorialRef = null;
|
||||||
})),
|
})),
|
||||||
nextTutStep: () => set(produce(state => {
|
nextTutStep: () => set(produce((state) => {
|
||||||
const currIdx = tutorialProgress.findIndex(p => p === state.tutorialProgress)
|
const currIdx = tutorialProgress.findIndex(p => p === state.tutorialProgress);
|
||||||
if(currIdx < tutorialProgress.length) {
|
if(currIdx < tutorialProgress.length) {
|
||||||
state.tutorialProgress = tutorialProgress[currIdx + 1];
|
state.tutorialProgress = tutorialProgress[currIdx + 1];
|
||||||
}
|
}
|
||||||
})),
|
})),
|
||||||
prevTutStep: () => set(produce(state => {
|
prevTutStep: () => set(produce((state) => {
|
||||||
const currIdx = tutorialProgress.findIndex(p => p === state.tutorialProgress)
|
const currIdx = tutorialProgress.findIndex(p => p === state.tutorialProgress);
|
||||||
if(currIdx > 0) {
|
if(currIdx > 0) {
|
||||||
state.tutorialProgress = tutorialProgress[currIdx - 1];
|
state.tutorialProgress = tutorialProgress[currIdx - 1];
|
||||||
}
|
}
|
||||||
@ -56,11 +55,11 @@ const useLocalState = create<LocalState>(persist((set, get) => ({
|
|||||||
imageShown: true,
|
imageShown: true,
|
||||||
audioShown: true,
|
audioShown: true,
|
||||||
videoShown: true,
|
videoShown: true,
|
||||||
oembedShown: true,
|
oembedShown: true
|
||||||
},
|
},
|
||||||
omniboxShown: false,
|
omniboxShown: false,
|
||||||
suspendedFocus: undefined,
|
suspendedFocus: undefined,
|
||||||
toggleOmnibox: () => set(produce(state => {
|
toggleOmnibox: () => set(produce((state) => {
|
||||||
state.omniboxShown = !state.omniboxShown;
|
state.omniboxShown = !state.omniboxShown;
|
||||||
if (typeof state.suspendedFocus?.focus === 'function') {
|
if (typeof state.suspendedFocus?.focus === 'function') {
|
||||||
state.suspendedFocus.focus();
|
state.suspendedFocus.focus();
|
||||||
@ -86,7 +85,7 @@ function withLocalState<P, S extends keyof LocalState>(Component: any, stateMemb
|
|||||||
(object, key) => ({ ...object, [key]: state[key] }), {}
|
(object, key) => ({ ...object, [key]: state[key] }), {}
|
||||||
)
|
)
|
||||||
): useLocalState();
|
): useLocalState();
|
||||||
return <Component ref={ref} {...localState} {...props} />
|
return <Component ref={ref} {...localState} {...props} />;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ export default class BaseStore<S extends object> {
|
|||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
this.handleEvent({
|
this.handleEvent({
|
||||||
data: { clear: true },
|
data: { clear: true }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,7 +30,7 @@ export default class BaseStore<S extends object> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("clear" in json && json.clear) {
|
if ('clear' in json && json.clear) {
|
||||||
this.setState(this.initialState());
|
this.setState(this.initialState());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ import MetadataReducer from '../reducers/metadata-update';
|
|||||||
import LocalReducer from '../reducers/local';
|
import LocalReducer from '../reducers/local';
|
||||||
|
|
||||||
import { StoreState } from './type';
|
import { StoreState } from './type';
|
||||||
import { Timebox } from '~/types';
|
import { Timebox } from '@urbit/api';
|
||||||
import { Cage } from '~/types/cage';
|
import { Cage } from '~/types/cage';
|
||||||
import S3Reducer from '../reducers/s3-update';
|
import S3Reducer from '../reducers/s3-update';
|
||||||
import { GraphReducer } from '../reducers/graph-update';
|
import { GraphReducer } from '../reducers/graph-update';
|
||||||
@ -20,7 +20,6 @@ import {OrderedMap} from '../lib/OrderedMap';
|
|||||||
import { BigIntOrderedMap } from '../lib/BigIntOrderedMap';
|
import { BigIntOrderedMap } from '../lib/BigIntOrderedMap';
|
||||||
import { GroupViewReducer } from '../reducers/group-view';
|
import { GroupViewReducer } from '../reducers/group-view';
|
||||||
|
|
||||||
|
|
||||||
export default class GlobalStore extends BaseStore<StoreState> {
|
export default class GlobalStore extends BaseStore<StoreState> {
|
||||||
inviteReducer = new InviteReducer();
|
inviteReducer = new InviteReducer();
|
||||||
metadataReducer = new MetadataReducer();
|
metadataReducer = new MetadataReducer();
|
||||||
@ -58,7 +57,7 @@ export default class GlobalStore extends BaseStore<StoreState> {
|
|||||||
invites: {},
|
invites: {},
|
||||||
associations: {
|
associations: {
|
||||||
groups: {},
|
groups: {},
|
||||||
graph: {},
|
graph: {}
|
||||||
},
|
},
|
||||||
groups: {},
|
groups: {},
|
||||||
groupKeys: new Set(),
|
groupKeys: new Set(),
|
||||||
@ -67,7 +66,7 @@ export default class GlobalStore extends BaseStore<StoreState> {
|
|||||||
launch: {
|
launch: {
|
||||||
firstTime: false,
|
firstTime: false,
|
||||||
tileOrdering: [],
|
tileOrdering: [],
|
||||||
tiles: {},
|
tiles: {}
|
||||||
},
|
},
|
||||||
weather: {},
|
weather: {},
|
||||||
userLocation: null,
|
userLocation: null,
|
||||||
@ -87,7 +86,7 @@ export default class GlobalStore extends BaseStore<StoreState> {
|
|||||||
notificationsGraphConfig: {
|
notificationsGraphConfig: {
|
||||||
watchOnSelf: false,
|
watchOnSelf: false,
|
||||||
mentions: false,
|
mentions: false,
|
||||||
watching: [],
|
watching: []
|
||||||
},
|
},
|
||||||
unreads: {
|
unreads: {
|
||||||
graph: {},
|
graph: {},
|
||||||
@ -95,7 +94,7 @@ export default class GlobalStore extends BaseStore<StoreState> {
|
|||||||
},
|
},
|
||||||
notificationsCount: 0,
|
notificationsCount: 0,
|
||||||
settings: {},
|
settings: {},
|
||||||
pendingJoin: {},
|
pendingJoin: {}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { Path } from '~/types/noun';
|
import { Path } from '@urbit/api';
|
||||||
import { Invites } from '~/types/invite-update';
|
import { Invites } from '@urbit/api/invite';
|
||||||
import { Associations } from '~/types/metadata-update';
|
import { Associations } from '@urbit/api/metadata';
|
||||||
import { Rolodex } from '~/types/contact-update';
|
import { Rolodex } from '@urbit/api/contacts';
|
||||||
import { Groups } from '~/types/group-update';
|
import { Groups } from '@urbit/api/groups';
|
||||||
import { S3State } from '~/types/s3-update';
|
import { S3State } from '~/types/s3-update';
|
||||||
import { LaunchState, WeatherState } from '~/types/launch-update';
|
import { LaunchState, WeatherState } from '~/types/launch-update';
|
||||||
import { ConnectionStatus } from '~/types/connection';
|
import { ConnectionStatus } from '~/types/connection';
|
||||||
import {Graphs} from '~/types/graph-update';
|
import { Graphs } from '@urbit/api/graph';
|
||||||
import {
|
import {
|
||||||
Notifications,
|
Notifications,
|
||||||
NotificationGraphConfig,
|
NotificationGraphConfig,
|
||||||
@ -14,7 +14,7 @@ import {
|
|||||||
Unreads,
|
Unreads,
|
||||||
JoinRequests,
|
JoinRequests,
|
||||||
Patp
|
Patp
|
||||||
} from "~/types";
|
} from '@urbit/api';
|
||||||
|
|
||||||
export interface StoreState {
|
export interface StoreState {
|
||||||
// local state
|
// local state
|
||||||
@ -35,7 +35,6 @@ export interface StoreState {
|
|||||||
graphs: Graphs;
|
graphs: Graphs;
|
||||||
graphKeys: Set<string>;
|
graphKeys: Set<string>;
|
||||||
|
|
||||||
|
|
||||||
// App specific states
|
// App specific states
|
||||||
// launch state
|
// launch state
|
||||||
launch: LaunchState;
|
launch: LaunchState;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import BaseStore from "../store/base";
|
import BaseStore from '../store/base';
|
||||||
import BaseApi from "../api/base";
|
import BaseApi from '../api/base';
|
||||||
import { Path } from "~/types/noun";
|
import { Path } from '@urbit/api';
|
||||||
|
|
||||||
export default class BaseSubscription<S extends object> {
|
export default class BaseSubscription<S extends object> {
|
||||||
private errorCount = 0;
|
private errorCount = 0;
|
||||||
@ -32,7 +32,7 @@ export default class BaseSubscription<S extends object> {
|
|||||||
console.error('event source error: ', err);
|
console.error('event source error: ', err);
|
||||||
this.errorCount++;
|
this.errorCount++;
|
||||||
if(this.errorCount >= 5) {
|
if(this.errorCount >= 5) {
|
||||||
console.error("bailing out, too many retries");
|
console.error('bailing out, too many retries');
|
||||||
this.handleEvent({ data: { connection: 'disconnected' } });
|
this.handleEvent({ data: { connection: 'disconnected' } });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import BaseSubscription from './base';
|
import BaseSubscription from './base';
|
||||||
import { StoreState } from '../store/type';
|
import { StoreState } from '../store/type';
|
||||||
import { Path } from '~/types/noun';
|
import { Path } from '@urbit/api';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Path to subscribe on and app to subscribe to
|
* Path to subscribe on and app to subscribe to
|
||||||
*/
|
*/
|
||||||
@ -68,7 +67,7 @@ export default class GlobalSubscription extends BaseSubscription<StoreState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
stopApp(app: AppName) {
|
stopApp(app: AppName) {
|
||||||
this.openSubscriptions[app].map(id => this.unsubscribe(id))
|
this.openSubscriptions[app].map(id => this.unsubscribe(id));
|
||||||
this.openSubscriptions[app] = [];
|
this.openSubscriptions[app] = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,17 @@
|
|||||||
import { ContactUpdate } from "./contact-update";
|
import { LocalUpdate } from './local-update';
|
||||||
import { InviteUpdate } from "./invite-update";
|
import { LaunchUpdate, WeatherState } from './launch-update';
|
||||||
import { LocalUpdate } from "./local-update";
|
import { ConnectionStatus } from './connection';
|
||||||
import { MetadataUpdate } from "./metadata-update";
|
import { ContactUpdate, GroupUpdate, InviteUpdate, MetadataUpdate } from '@urbit/api';
|
||||||
import { GroupUpdate } from "./group-update";
|
import { SettingsUpdate } from '@urbit/api/settings';
|
||||||
import { LaunchUpdate, WeatherState } from "./launch-update";
|
|
||||||
import { ConnectionStatus } from "./connection";
|
|
||||||
import { SettingsUpdate } from "./settings";
|
|
||||||
|
|
||||||
interface MarksToTypes {
|
interface MarksToTypes {
|
||||||
readonly json: any;
|
readonly json: any;
|
||||||
readonly "contact-update": ContactUpdate;
|
readonly 'contact-update': ContactUpdate;
|
||||||
readonly "invite-update": InviteUpdate;
|
readonly 'invite-update': InviteUpdate;
|
||||||
readonly "metadata-update": MetadataUpdate;
|
readonly 'metadata-update': MetadataUpdate;
|
||||||
readonly groupUpdate: GroupUpdate;
|
readonly groupUpdate: GroupUpdate;
|
||||||
readonly "launch-update": LaunchUpdate;
|
readonly 'launch-update': LaunchUpdate;
|
||||||
readonly "link-listen-update": LinkListenUpdate;
|
readonly 'settings-event': SettingsUpdate;
|
||||||
readonly "settings-event": SettingsUpdate;
|
|
||||||
// not really marks but w/e
|
// not really marks but w/e
|
||||||
readonly 'local': LocalUpdate;
|
readonly 'local': LocalUpdate;
|
||||||
readonly 'weather': WeatherState | {};
|
readonly 'weather': WeatherState | {};
|
||||||
|
@ -1,2 +1 @@
|
|||||||
|
|
||||||
export type ConnectionStatus = 'reconnecting' | 'disconnected' | 'connected';
|
export type ConnectionStatus = 'reconnecting' | 'disconnected' | 'connected';
|
||||||
|
@ -1,85 +0,0 @@
|
|||||||
import { Path, Patp } from "./noun";
|
|
||||||
|
|
||||||
export type ContactUpdate =
|
|
||||||
| ContactUpdateCreate
|
|
||||||
| ContactUpdateDelete
|
|
||||||
| ContactUpdateAdd
|
|
||||||
| ContactUpdateRemove
|
|
||||||
| ContactUpdateEdit
|
|
||||||
| ContactUpdateInitial
|
|
||||||
| ContactUpdateContacts;
|
|
||||||
|
|
||||||
interface ContactUpdateCreate {
|
|
||||||
create: Path;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ContactUpdateDelete {
|
|
||||||
delete: Path;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ContactUpdateAdd {
|
|
||||||
add: {
|
|
||||||
path: Path;
|
|
||||||
ship: Patp;
|
|
||||||
contact: Contact;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ContactUpdateRemove {
|
|
||||||
remove: {
|
|
||||||
path: Path;
|
|
||||||
ship: Patp;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ContactUpdateEdit {
|
|
||||||
edit: {
|
|
||||||
path: Path;
|
|
||||||
ship: Patp;
|
|
||||||
"edit-field": ContactEdit;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ContactUpdateInitial {
|
|
||||||
initial: Rolodex;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ContactUpdateContacts {
|
|
||||||
contacts: {
|
|
||||||
path: Path;
|
|
||||||
contacts: Contacts;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
|
|
||||||
type ContactAvatar = ContactAvatarUrl | ContactAvatarOcts;
|
|
||||||
|
|
||||||
export type Rolodex = {
|
|
||||||
[p in Path]: Contacts;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Contacts = {
|
|
||||||
[p in Patp]: Contact;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface ContactAvatarUrl {
|
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ContactAvatarOcts {
|
|
||||||
octs: string;
|
|
||||||
}
|
|
||||||
export interface Contact {
|
|
||||||
nickname: string;
|
|
||||||
email: string;
|
|
||||||
phone: string;
|
|
||||||
website: string;
|
|
||||||
notes: string;
|
|
||||||
color: string;
|
|
||||||
avatar: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ContactEdit = {
|
|
||||||
[k in keyof Contact]: Contact[k];
|
|
||||||
};
|
|
@ -1,4 +1,4 @@
|
|||||||
import { PatpNoSig } from "./noun";
|
import { PatpNoSig } from '@urbit/api';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
|
@ -1,16 +1,8 @@
|
|||||||
export * from './cage';
|
export * from './cage';
|
||||||
export * from './connection';
|
export * from './connection';
|
||||||
export * from './contact-update';
|
|
||||||
export * from './global';
|
export * from './global';
|
||||||
export * from './group-update';
|
|
||||||
export * from './group-view';
|
|
||||||
export * from './graph-update';
|
|
||||||
export * from './hark-update';
|
|
||||||
export * from './invite-update';
|
|
||||||
export * from './launch-update';
|
export * from './launch-update';
|
||||||
export * from './local-update';
|
export * from './local-update';
|
||||||
export * from './metadata-update';
|
|
||||||
export * from './noun';
|
|
||||||
export * from './s3-update';
|
export * from './s3-update';
|
||||||
export * from './workspace';
|
export * from './workspace';
|
||||||
export * from './util';
|
export * from './util';
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
|
|
||||||
export type LaunchUpdate =
|
export type LaunchUpdate =
|
||||||
LaunchUpdateInitial
|
LaunchUpdateInitial
|
||||||
| LaunchUpdateFirstTime
|
| LaunchUpdateFirstTime
|
||||||
| LaunchUpdateOrder
|
| LaunchUpdateOrder
|
||||||
| LaunchUpdateIsShown;
|
| LaunchUpdateIsShown;
|
||||||
|
|
||||||
|
|
||||||
interface LaunchUpdateInitial {
|
interface LaunchUpdateInitial {
|
||||||
initial: LaunchState;
|
initial: LaunchState;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
|
|
||||||
|
|
||||||
export interface S3Credentials {
|
export interface S3Credentials {
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
accessKeyId: string;
|
accessKeyId: string;
|
||||||
@ -51,7 +49,6 @@ interface S3UpdateSecretAccessKey {
|
|||||||
setSecretAccessKey: string;
|
setSecretAccessKey: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export type S3Update =
|
export type S3Update =
|
||||||
S3UpdateCredentials
|
S3UpdateCredentials
|
||||||
| S3UpdateConfiguration
|
| S3UpdateConfiguration
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Icon } from "@tlon/indigo-react";
|
import { Icon } from '@tlon/indigo-react';
|
||||||
export type PropFunc<T extends (...args: any[]) => any> = Parameters<T>[0];
|
export type PropFunc<T extends (...args: any[]) => any> = Parameters<T>[0];
|
||||||
|
|
||||||
export type Primitive = string | number | undefined | symbol | null | boolean;
|
export type Primitive = string | number | undefined | symbol | null | boolean;
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
|
|
||||||
|
|
||||||
interface GroupWorkspace {
|
interface GroupWorkspace {
|
||||||
type: 'group';
|
type: 'group';
|
||||||
group: string;
|
group: string;
|
||||||
|
@ -3,7 +3,7 @@ import { RouteComponentProps } from 'react-router-dom';
|
|||||||
import { Col } from '@tlon/indigo-react';
|
import { Col } from '@tlon/indigo-react';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
import { Association } from '~/types/metadata-update';
|
import { Association } from '@urbit/api/metadata';
|
||||||
import { StoreState } from '~/logic/store/type';
|
import { StoreState } from '~/logic/store/type';
|
||||||
import { useFileDrag } from '~/logic/lib/useDrag';
|
import { useFileDrag } from '~/logic/lib/useDrag';
|
||||||
import ChatWindow from './components/ChatWindow';
|
import ChatWindow from './components/ChatWindow';
|
||||||
@ -114,7 +114,6 @@ export function ChatResource(props: ChatResourceProps) {
|
|||||||
} else {
|
} else {
|
||||||
setShowBanner(false);
|
setShowBanner(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
const groupShared = await props.api.contacts.fetchIsAllowed(
|
const groupShared = await props.api.contacts.fetchIsAllowed(
|
||||||
`~${window.ship}`,
|
`~${window.ship}`,
|
||||||
@ -127,14 +126,13 @@ export function ChatResource(props: ChatResourceProps) {
|
|||||||
|
|
||||||
setHasLoadedAllowed(true);
|
setHasLoadedAllowed(true);
|
||||||
})();
|
})();
|
||||||
|
}, [groupPath, group]);
|
||||||
}, [groupPath]);
|
|
||||||
|
|
||||||
if(!graph) {
|
if(!graph) {
|
||||||
return <Loading />;
|
return <Loading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
var modifiedContacts = { ...contacts };
|
const modifiedContacts = { ...contacts };
|
||||||
delete modifiedContacts[`~${window.ship}`];
|
delete modifiedContacts[`~${window.ship}`];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -7,7 +7,7 @@ import { createPost } from '~/logic/api/graph';
|
|||||||
import tokenizeMessage, { isUrl } from '~/logic/lib/tokenizeMessage';
|
import tokenizeMessage, { isUrl } from '~/logic/lib/tokenizeMessage';
|
||||||
import GlobalApi from '~/logic/api/global';
|
import GlobalApi from '~/logic/api/global';
|
||||||
import { Envelope } from '~/types/chat-update';
|
import { Envelope } from '~/types/chat-update';
|
||||||
import { Contacts, Content } from '~/types';
|
import { Contacts, Content } from '@urbit/api';
|
||||||
import { Row, BaseImage, Box, Icon, LoadingSpinner } from '@tlon/indigo-react';
|
import { Row, BaseImage, Box, Icon, LoadingSpinner } from '@tlon/indigo-react';
|
||||||
import withS3 from '~/views/components/withS3';
|
import withS3 from '~/views/components/withS3';
|
||||||
import { withLocalState } from '~/logic/state/local';
|
import { withLocalState } from '~/logic/state/local';
|
||||||
@ -15,12 +15,12 @@ import { withLocalState } from '~/logic/state/local';
|
|||||||
type ChatInputProps = IuseS3 & {
|
type ChatInputProps = IuseS3 & {
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
numMsgs: number;
|
numMsgs: number;
|
||||||
station: any;
|
station: unknown;
|
||||||
ourContact: any;
|
ourContact: unknown;
|
||||||
envelopes: Envelope[];
|
envelopes: Envelope[];
|
||||||
contacts: Contacts;
|
contacts: Contacts;
|
||||||
onUnmount(msg: string): void;
|
onUnmount(msg: string): void;
|
||||||
s3: any;
|
s3: unknown;
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
message: string;
|
message: string;
|
||||||
deleteMessage(): void;
|
deleteMessage(): void;
|
||||||
@ -74,7 +74,7 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const post = createPost(tokenizeMessage((text)))
|
const post = createPost(tokenizeMessage((text)));
|
||||||
|
|
||||||
props.deleteMessage();
|
props.deleteMessage();
|
||||||
|
|
||||||
@ -110,7 +110,7 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
|||||||
if (!this.props.canUpload) {
|
if (!this.props.canUpload) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Array.from(files).forEach(file => {
|
Array.from(files).forEach((file) => {
|
||||||
this.props.uploadDefault(file)
|
this.props.uploadDefault(file)
|
||||||
.then(this.uploadSuccess)
|
.then(this.uploadSuccess)
|
||||||
.catch(this.uploadError);
|
.catch(this.uploadError);
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable max-lines-per-function */
|
||||||
import React, {
|
import React, {
|
||||||
useState,
|
useState,
|
||||||
useEffect,
|
useEffect,
|
||||||
@ -17,44 +18,65 @@ import {
|
|||||||
useShowNickname,
|
useShowNickname,
|
||||||
useHovering
|
useHovering
|
||||||
} from '~/logic/lib/util';
|
} from '~/logic/lib/util';
|
||||||
import { Group, Association, Contacts, Post, Groups, Associations } from '~/types';
|
import {
|
||||||
|
Group,
|
||||||
|
Association,
|
||||||
|
Contacts,
|
||||||
|
Post,
|
||||||
|
Groups,
|
||||||
|
Associations
|
||||||
|
} from '~/types';
|
||||||
import TextContent from './content/text';
|
import TextContent from './content/text';
|
||||||
import CodeContent from './content/code';
|
import CodeContent from './content/code';
|
||||||
import RemoteContent from '~/views/components/RemoteContent';
|
import RemoteContent from '~/views/components/RemoteContent';
|
||||||
import { Mention } from '~/views/components/MentionText';
|
import { Mention } from '~/views/components/MentionText';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import useLocalState from '~/logic/state/local';
|
import useLocalState from '~/logic/state/local';
|
||||||
|
import Timestamp from '~/views/components/Timestamp';
|
||||||
|
|
||||||
export const DATESTAMP_FORMAT = '[~]YYYY.M.D';
|
export const DATESTAMP_FORMAT = '[~]YYYY.M.D';
|
||||||
|
|
||||||
export const UnreadMarker = React.forwardRef(({ dayBreak, when }, ref) => (
|
interface DayBreakProps {
|
||||||
<Row
|
when: string;
|
||||||
flexShrink={0}
|
shimTop?: boolean;
|
||||||
ref={ref}
|
}
|
||||||
color='blue'
|
|
||||||
alignItems='center'
|
|
||||||
fontSize='0'
|
|
||||||
position='absolute'
|
|
||||||
width='100%'
|
|
||||||
py='2'
|
|
||||||
>
|
|
||||||
<Rule borderColor='blue' display={['none', 'block']} m='0' width='2rem' />
|
|
||||||
<Text flexShrink='0' display='block' zIndex='2' mx='4' color='blue'>
|
|
||||||
New messages below
|
|
||||||
</Text>
|
|
||||||
<Rule borderColor='blue' flexGrow='1' m='0' />
|
|
||||||
<Rule style={{ width: 'calc(50% - 48px)' }} borderColor='blue' m='0' />
|
|
||||||
</Row>
|
|
||||||
));
|
|
||||||
|
|
||||||
export const DayBreak = ({ when }) => (
|
export const DayBreak = ({ when, shimTop = false }: DayBreakProps) => (
|
||||||
<Row pb='3' alignItems='center' justifyContent='center' width='100%'>
|
<Row
|
||||||
<Text gray>
|
px={2}
|
||||||
|
height={5}
|
||||||
|
mb={2}
|
||||||
|
justifyContent='center'
|
||||||
|
alignItems='center'
|
||||||
|
mt={shimTop ? '-8px' : '0'}
|
||||||
|
>
|
||||||
|
<Rule borderColor='lightGray' />
|
||||||
|
<Text gray flexShrink='0' fontSize={0} px={2}>
|
||||||
{moment(when).calendar(null, { sameElse: DATESTAMP_FORMAT })}
|
{moment(when).calendar(null, { sameElse: DATESTAMP_FORMAT })}
|
||||||
</Text>
|
</Text>
|
||||||
|
<Rule borderColor='lightGray' />
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const UnreadMarker = React.forwardRef(({ dayBreak, when }, ref) => (
|
||||||
|
<Row
|
||||||
|
position='absolute'
|
||||||
|
ref={ref}
|
||||||
|
px={2}
|
||||||
|
mt={2}
|
||||||
|
height={5}
|
||||||
|
justifyContent='center'
|
||||||
|
alignItems='center'
|
||||||
|
width='100%'
|
||||||
|
>
|
||||||
|
<Rule borderColor='lightBlue' />
|
||||||
|
<Text color='blue' fontSize={0} flexShrink='0' px={2}>
|
||||||
|
New messages below
|
||||||
|
</Text>
|
||||||
|
<Rule borderColor='lightBlue' />
|
||||||
|
</Row>
|
||||||
|
));
|
||||||
|
|
||||||
interface ChatMessageProps {
|
interface ChatMessageProps {
|
||||||
measure(element): void;
|
measure(element): void;
|
||||||
msg: Post;
|
msg: Post;
|
||||||
@ -66,13 +88,14 @@ interface ChatMessageProps {
|
|||||||
contacts: Contacts;
|
contacts: Contacts;
|
||||||
className?: string;
|
className?: string;
|
||||||
isPending: boolean;
|
isPending: boolean;
|
||||||
style?: any;
|
style?: unknown;
|
||||||
scrollWindow: HTMLDivElement;
|
scrollWindow: HTMLDivElement;
|
||||||
isLastMessage?: boolean;
|
isLastMessage?: boolean;
|
||||||
unreadMarkerRef: React.RefObject<HTMLDivElement>;
|
unreadMarkerRef: React.RefObject<HTMLDivElement>;
|
||||||
history: any;
|
history: unknown;
|
||||||
api: any;
|
api: GlobalApi;
|
||||||
highlighted?: boolean;
|
highlighted?: boolean;
|
||||||
|
renderSigil?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class ChatMessage extends Component<ChatMessageProps> {
|
export default class ChatMessage extends Component<ChatMessageProps> {
|
||||||
@ -113,21 +136,26 @@ export default class ChatMessage extends Component<ChatMessageProps> {
|
|||||||
associations
|
associations
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const renderSigil = Boolean(
|
let { renderSigil } = this.props;
|
||||||
(nextMsg && msg.author !== nextMsg.author) || !nextMsg || msg.number === 1
|
|
||||||
|
if (renderSigil === undefined) {
|
||||||
|
renderSigil = Boolean(
|
||||||
|
(nextMsg && msg.author !== nextMsg.author) ||
|
||||||
|
!nextMsg ||
|
||||||
|
msg.number === 1
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const dayBreak =
|
const dayBreak =
|
||||||
nextMsg &&
|
nextMsg &&
|
||||||
new Date(msg['time-sent']).getDate() !==
|
new Date(msg['time-sent']).getDate() !==
|
||||||
new Date(nextMsg['time-sent']).getDate();
|
new Date(nextMsg['time-sent']).getDate();
|
||||||
|
|
||||||
const containerClass = `${
|
const containerClass = `${isPending ? 'o-40' : ''} ${className}`;
|
||||||
renderSigil ? 'cf pl2 lh-copy' : 'items-top cf hide-child'
|
|
||||||
} ${isPending ? 'o-40' : ''} ${className}`;
|
|
||||||
|
|
||||||
const timestamp = moment
|
const timestamp = moment
|
||||||
.unix(msg['time-sent'] / 1000)
|
.unix(msg['time-sent'] / 1000)
|
||||||
.format(renderSigil ? 'hh:mm a' : 'hh:mm');
|
.format(renderSigil ? 'h:mm A' : 'h:mm');
|
||||||
|
|
||||||
const reboundMeasure = (event) => {
|
const reboundMeasure = (event) => {
|
||||||
return measure(this.divRef.current);
|
return measure(this.divRef.current);
|
||||||
@ -149,7 +177,7 @@ export default class ChatMessage extends Component<ChatMessageProps> {
|
|||||||
highlighted,
|
highlighted,
|
||||||
fontSize,
|
fontSize,
|
||||||
associations,
|
associations,
|
||||||
groups,
|
groups
|
||||||
};
|
};
|
||||||
|
|
||||||
const unreadContainerStyle = {
|
const unreadContainerStyle = {
|
||||||
@ -158,34 +186,24 @@ export default class ChatMessage extends Component<ChatMessageProps> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
bg={highlighted ? 'washedBlue' : 'white'}
|
|
||||||
flexShrink={0}
|
|
||||||
width='100%'
|
|
||||||
display='flex'
|
|
||||||
flexWrap='wrap'
|
|
||||||
pt={this.props.pt ? this.props.pt : renderSigil ? 3 : 0}
|
|
||||||
pr={3}
|
|
||||||
pb={isLastMessage ? 3 : 0}
|
|
||||||
ref={this.divRef}
|
ref={this.divRef}
|
||||||
|
pt={renderSigil ? 2 : 0}
|
||||||
|
pb={2}
|
||||||
className={containerClass}
|
className={containerClass}
|
||||||
style={style}
|
style={style}
|
||||||
mb={1}
|
|
||||||
position='relative'
|
|
||||||
>
|
>
|
||||||
{dayBreak && !isLastRead ? <DayBreak when={msg['time-sent']} /> : null}
|
{dayBreak && !isLastRead ? (
|
||||||
|
<DayBreak when={msg['time-sent']} shimTop={renderSigil} />
|
||||||
|
) : null}
|
||||||
{renderSigil ? (
|
{renderSigil ? (
|
||||||
<MessageWithSigil {...messageProps} />
|
<>
|
||||||
|
<MessageAuthor pb={'2px'} {...messageProps} />
|
||||||
|
<Message pl={5} pr={3} {...messageProps} />
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<MessageWithoutSigil {...messageProps} />
|
<Message pl={5} pr={3} timestampHover {...messageProps} />
|
||||||
)}
|
)}
|
||||||
<Box
|
<Box style={unreadContainerStyle}>
|
||||||
flexShrink={0}
|
|
||||||
fontSize={0}
|
|
||||||
position='relative'
|
|
||||||
width='100%'
|
|
||||||
overflow='visible'
|
|
||||||
style={unreadContainerStyle}
|
|
||||||
>
|
|
||||||
{isLastRead ? (
|
{isLastRead ? (
|
||||||
<UnreadMarker
|
<UnreadMarker
|
||||||
dayBreak={dayBreak}
|
dayBreak={dayBreak}
|
||||||
@ -199,44 +217,30 @@ export default class ChatMessage extends Component<ChatMessageProps> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MessageProps {
|
export const MessageAuthor = ({
|
||||||
msg: Post;
|
|
||||||
timestamp: string;
|
|
||||||
group: Group;
|
|
||||||
association: Association;
|
|
||||||
contacts: Contacts;
|
|
||||||
containerClass: string;
|
|
||||||
isPending: boolean;
|
|
||||||
style: any;
|
|
||||||
measure(element): void;
|
|
||||||
scrollWindow: HTMLDivElement;
|
|
||||||
associations: Associations;
|
|
||||||
groups: Groups;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MessageWithSigil = (props) => {
|
|
||||||
const {
|
|
||||||
msg,
|
|
||||||
timestamp,
|
timestamp,
|
||||||
contacts,
|
contacts,
|
||||||
association,
|
msg,
|
||||||
|
measure,
|
||||||
|
group,
|
||||||
|
api,
|
||||||
associations,
|
associations,
|
||||||
groups,
|
groups,
|
||||||
group,
|
|
||||||
measure,
|
|
||||||
api,
|
|
||||||
history,
|
|
||||||
scrollWindow,
|
scrollWindow,
|
||||||
fontSize
|
...rest
|
||||||
} = props;
|
}) => {
|
||||||
|
|
||||||
const dark = useLocalState((state) => state.dark);
|
const dark = useLocalState((state) => state.dark);
|
||||||
|
|
||||||
const datestamp = moment
|
const datestamp = moment
|
||||||
.unix(msg['time-sent'] / 1000)
|
.unix(msg['time-sent'] / 1000)
|
||||||
.format(DATESTAMP_FORMAT);
|
.format(DATESTAMP_FORMAT);
|
||||||
const contact = `~${msg.author}` in contacts ? contacts[`~${msg.author}`] : false;
|
const contact =
|
||||||
|
`~${msg.author}` in contacts ? contacts[`~${msg.author}`] : false;
|
||||||
const showNickname = useShowNickname(contact);
|
const showNickname = useShowNickname(contact);
|
||||||
|
const { hideAvatars } =
|
||||||
|
useLocalState(({ hideAvatars }) =>
|
||||||
|
({ hideAvatars })
|
||||||
|
);
|
||||||
const shipName = showNickname ? contact.nickname : cite(msg.author);
|
const shipName = showNickname ? contact.nickname : cite(msg.author);
|
||||||
const copyNotice = 'Copied';
|
const copyNotice = 'Copied';
|
||||||
const color = contact
|
const color = contact
|
||||||
@ -270,10 +274,9 @@ export const MessageWithSigil = (props) => {
|
|||||||
};
|
};
|
||||||
const timer = setTimeout(() => resetDisplay(), 800);
|
const timer = setTimeout(() => resetDisplay(), 800);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [displayName]);
|
}, [shipName, displayName]);
|
||||||
|
|
||||||
const img =
|
const img = contact?.avatar && !hideAvatars ? (
|
||||||
contact && contact.avatar !== null ? (
|
|
||||||
<BaseImage
|
<BaseImage
|
||||||
display='inline-block'
|
display='inline-block'
|
||||||
src={contact.avatar}
|
src={contact.avatar}
|
||||||
@ -290,17 +293,16 @@ export const MessageWithSigil = (props) => {
|
|||||||
padding={2}
|
padding={2}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Box display='flex' alignItems='center' {...rest}>
|
||||||
<Box
|
<Box
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowOverlay(true);
|
setShowOverlay(true);
|
||||||
}}
|
}}
|
||||||
className='fl v-top pt1'
|
|
||||||
height={16}
|
height={16}
|
||||||
pr={3}
|
pr={2}
|
||||||
pl={2}
|
pl={2}
|
||||||
|
cursor='pointer'
|
||||||
position='relative'
|
position='relative'
|
||||||
>
|
>
|
||||||
{showOverlay && (
|
{showOverlay && (
|
||||||
@ -329,11 +331,11 @@ export const MessageWithSigil = (props) => {
|
|||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
fontSize={0}
|
fontSize={0}
|
||||||
mr={3}
|
mr={2}
|
||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
mono={nameMono}
|
mono={nameMono}
|
||||||
fontWeight={nameMono ? '400' : '500'}
|
fontWeight={nameMono ? '400' : '500'}
|
||||||
className={'mw5 db truncate pointer'}
|
className={'pointer'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
writeText(`~${msg.author}`);
|
writeText(`~${msg.author}`);
|
||||||
showCopyNotice();
|
showCopyNotice();
|
||||||
@ -342,48 +344,25 @@ export const MessageWithSigil = (props) => {
|
|||||||
>
|
>
|
||||||
{displayName}
|
{displayName}
|
||||||
</Text>
|
</Text>
|
||||||
<Text flexShrink={0} fontSize={0} gray mono>
|
<Text flexShrink={0} fontSize={0} gray>
|
||||||
{timestamp}
|
{timestamp}
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
fontSize={0}
|
fontSize={0}
|
||||||
gray
|
gray
|
||||||
mono
|
|
||||||
ml={2}
|
ml={2}
|
||||||
display={['none', hovering ? 'block' : 'none']}
|
display={['none', hovering ? 'block' : 'none']}
|
||||||
>
|
>
|
||||||
{datestamp}
|
{datestamp}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<ContentBox flexShrink={0} fontSize={fontSize ? fontSize : '14px'}>
|
|
||||||
{msg.contents.map((c, i) => (
|
|
||||||
<MessageContent
|
|
||||||
key={i}
|
|
||||||
contacts={contacts}
|
|
||||||
content={c}
|
|
||||||
measure={measure}
|
|
||||||
scrollWindow={scrollWindow}
|
|
||||||
fontSize={fontSize}
|
|
||||||
group={group}
|
|
||||||
api={api}
|
|
||||||
associations={associations}
|
|
||||||
groups={groups}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ContentBox>
|
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ContentBox = styled(Box)`
|
export const Message = ({
|
||||||
& > :first-child {
|
|
||||||
margin-left: 0px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const MessageWithoutSigil = ({
|
|
||||||
timestamp,
|
timestamp,
|
||||||
contacts,
|
contacts,
|
||||||
msg,
|
msg,
|
||||||
@ -392,70 +371,50 @@ export const MessageWithoutSigil = ({
|
|||||||
api,
|
api,
|
||||||
associations,
|
associations,
|
||||||
groups,
|
groups,
|
||||||
scrollWindow
|
scrollWindow,
|
||||||
|
timestampHover,
|
||||||
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
const { hovering, bind } = useHovering();
|
const { hovering, bind } = useHovering();
|
||||||
return (
|
return (
|
||||||
<>
|
<Box position='relative' {...rest}>
|
||||||
|
{timestampHover ? (
|
||||||
<Text
|
<Text
|
||||||
flexShrink={0}
|
|
||||||
mono
|
|
||||||
gray
|
|
||||||
display={hovering ? 'block' : 'none'}
|
display={hovering ? 'block' : 'none'}
|
||||||
pt='2px'
|
|
||||||
lineHeight='tall'
|
|
||||||
fontSize={0}
|
|
||||||
position='absolute'
|
position='absolute'
|
||||||
left={1}
|
left='0'
|
||||||
|
top='3px'
|
||||||
|
fontSize={0}
|
||||||
|
gray
|
||||||
>
|
>
|
||||||
{timestamp}
|
{timestamp}
|
||||||
</Text>
|
</Text>
|
||||||
<ContentBox
|
) : (
|
||||||
flexShrink={0}
|
<></>
|
||||||
fontSize='14px'
|
)}
|
||||||
className='clamp-message'
|
<Box {...bind}>
|
||||||
style={{ flexGrow: 1 }}
|
{msg.contents.map((content, i) => {
|
||||||
{...bind}
|
switch (Object.keys(content)[0]) {
|
||||||
pl={6}
|
case 'text':
|
||||||
>
|
return (
|
||||||
{msg.contents.map((c, i) => (
|
<TextContent
|
||||||
<MessageContent
|
|
||||||
key={i}
|
|
||||||
contacts={contacts}
|
|
||||||
content={c}
|
|
||||||
group={group}
|
|
||||||
measure={measure}
|
|
||||||
scrollWindow={scrollWindow}
|
|
||||||
groups={groups}
|
|
||||||
associations={associations}
|
associations={associations}
|
||||||
|
groups={groups}
|
||||||
|
measure={measure}
|
||||||
api={api}
|
api={api}
|
||||||
|
fontSize={1}
|
||||||
|
lineHeight={'20px'}
|
||||||
|
content={content}
|
||||||
/>
|
/>
|
||||||
))}
|
|
||||||
</ContentBox>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
case 'code':
|
||||||
|
|
||||||
export const MessageContent = ({
|
|
||||||
content,
|
|
||||||
contacts,
|
|
||||||
api,
|
|
||||||
associations,
|
|
||||||
groups,
|
|
||||||
measure,
|
|
||||||
scrollWindow,
|
|
||||||
fontSize,
|
|
||||||
group
|
|
||||||
}) => {
|
|
||||||
if ('code' in content) {
|
|
||||||
return <CodeContent content={content} />;
|
return <CodeContent content={content} />;
|
||||||
} else if ('url' in content) {
|
case 'url':
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
mx='2px'
|
|
||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
fontSize={fontSize ? fontSize : '14px'}
|
fontSize={1}
|
||||||
lineHeight='tall'
|
lineHeight='20px'
|
||||||
color='black'
|
color='black'
|
||||||
>
|
>
|
||||||
<RemoteContent
|
<RemoteContent
|
||||||
@ -464,13 +423,15 @@ export const MessageContent = ({
|
|||||||
imageProps={{
|
imageProps={{
|
||||||
style: {
|
style: {
|
||||||
maxWidth: 'min(100%,18rem)',
|
maxWidth: 'min(100%,18rem)',
|
||||||
display: 'block'
|
display: 'inline-block',
|
||||||
|
marginTop: '0.5rem'
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
videoProps={{
|
videoProps={{
|
||||||
style: {
|
style: {
|
||||||
maxWidth: '18rem',
|
maxWidth: '18rem',
|
||||||
display: 'block'
|
display: 'block',
|
||||||
|
marginTop: '0.5rem'
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
textProps={{
|
textProps={{
|
||||||
@ -483,17 +444,7 @@ export const MessageContent = ({
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
} else if ('text' in content) {
|
case 'mention':
|
||||||
return (
|
|
||||||
<TextContent
|
|
||||||
associations={associations}
|
|
||||||
groups={groups}
|
|
||||||
measure={measure}
|
|
||||||
api={api}
|
|
||||||
fontSize={fontSize}
|
|
||||||
content={content}
|
|
||||||
/>);
|
|
||||||
} else if ('mention' in content) {
|
|
||||||
return (
|
return (
|
||||||
<Mention
|
<Mention
|
||||||
group={group}
|
group={group}
|
||||||
@ -502,9 +453,13 @@ export const MessageContent = ({
|
|||||||
contact={contacts?.[`~${content.mention}`]}
|
contact={contacts?.[`~${content.mention}`]}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MessagePlaceholder = ({
|
export const MessagePlaceholder = ({
|
||||||
|
@ -1,22 +1,25 @@
|
|||||||
import React, { Component } from "react";
|
import React, { Component } from 'react';
|
||||||
import { RouteComponentProps } from "react-router-dom";
|
import { RouteComponentProps } from 'react-router-dom';
|
||||||
import _ from "lodash";
|
import _ from 'lodash';
|
||||||
import bigInt, { BigInteger } from 'big-integer';
|
import bigInt, { BigInteger } from 'big-integer';
|
||||||
|
|
||||||
import { Col } from '@tlon/indigo-react';
|
import { Col } from '@tlon/indigo-react';
|
||||||
|
import {
|
||||||
|
Patp,
|
||||||
|
Contacts,
|
||||||
|
Association,
|
||||||
|
Associations,
|
||||||
|
Group,
|
||||||
|
Groups,
|
||||||
|
Graph
|
||||||
|
} from '@urbit/api';
|
||||||
|
|
||||||
import GlobalApi from "~/logic/api/global";
|
import GlobalApi from '~/logic/api/global';
|
||||||
import { Patp, Path } from "~/types/noun";
|
|
||||||
import { Contacts } from "~/types/contact-update";
|
|
||||||
import { Association, Associations } from "~/types/metadata-update";
|
|
||||||
import { Group, Groups } from "~/types/group-update";
|
|
||||||
import { Envelope, IMessage } from "~/types/chat-update";
|
|
||||||
import { Graph } from "~/types";
|
|
||||||
|
|
||||||
import VirtualScroller from "~/views/components/VirtualScroller";
|
import VirtualScroller from '~/views/components/VirtualScroller';
|
||||||
|
|
||||||
import ChatMessage, { MessagePlaceholder } from './ChatMessage';
|
import ChatMessage, { MessagePlaceholder } from './ChatMessage';
|
||||||
import { UnreadNotice } from "./unread-notice";
|
import { UnreadNotice } from './unread-notice';
|
||||||
|
|
||||||
const INITIAL_LOAD = 20;
|
const INITIAL_LOAD = 20;
|
||||||
const DEFAULT_BACKLOG_SIZE = 100;
|
const DEFAULT_BACKLOG_SIZE = 100;
|
||||||
@ -38,7 +41,7 @@ type ChatWindowProps = RouteComponentProps<{
|
|||||||
scrollTo?: number;
|
scrollTo?: number;
|
||||||
associations: Associations;
|
associations: Associations;
|
||||||
groups: Groups;
|
groups: Groups;
|
||||||
}
|
};
|
||||||
|
|
||||||
interface ChatWindowState {
|
interface ChatWindowState {
|
||||||
fetchPending: boolean;
|
fetchPending: boolean;
|
||||||
@ -47,7 +50,10 @@ interface ChatWindowState {
|
|||||||
unreadIndex: BigInteger;
|
unreadIndex: BigInteger;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class ChatWindow extends Component<ChatWindowProps, ChatWindowState> {
|
export default class ChatWindow extends Component<
|
||||||
|
ChatWindowProps,
|
||||||
|
ChatWindowState
|
||||||
|
> {
|
||||||
private virtualList: VirtualScroller | null;
|
private virtualList: VirtualScroller | null;
|
||||||
private unreadMarkerRef: React.RefObject<HTMLDivElement>;
|
private unreadMarkerRef: React.RefObject<HTMLDivElement>;
|
||||||
private prevSize = 0;
|
private prevSize = 0;
|
||||||
@ -66,8 +72,6 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
|
|||||||
unreadIndex: bigInt.zero
|
unreadIndex: bigInt.zero
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
this.dismissUnread = this.dismissUnread.bind(this);
|
this.dismissUnread = this.dismissUnread.bind(this);
|
||||||
this.scrollToUnread = this.scrollToUnread.bind(this);
|
this.scrollToUnread = this.scrollToUnread.bind(this);
|
||||||
this.handleWindowBlur = this.handleWindowBlur.bind(this);
|
this.handleWindowBlur = this.handleWindowBlur.bind(this);
|
||||||
@ -110,7 +114,7 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
|
|||||||
}
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
unreadIndex
|
unreadIndex
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleWindowBlur() {
|
handleWindowBlur() {
|
||||||
@ -135,7 +139,6 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
|
|||||||
this.calculateUnreadIndex();
|
this.calculateUnreadIndex();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (this.prevSize !== graph.size) {
|
if (this.prevSize !== graph.size) {
|
||||||
if (this.state.unreadIndex.eq(bigInt.zero)) {
|
if (this.state.unreadIndex.eq(bigInt.zero)) {
|
||||||
this.calculateUnreadIndex();
|
this.calculateUnreadIndex();
|
||||||
@ -189,14 +192,19 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
|
|||||||
const currSize = graph.size;
|
const currSize = graph.size;
|
||||||
if (newer && !this.loadedNewest) {
|
if (newer && !this.loadedNewest) {
|
||||||
const [index] = graph.peekLargest()!;
|
const [index] = graph.peekLargest()!;
|
||||||
await api.graph.getYoungerSiblings(ship,name, 20, `/${index.toString()}`)
|
await api.graph.getYoungerSiblings(
|
||||||
|
ship,
|
||||||
|
name,
|
||||||
|
20,
|
||||||
|
`/${index.toString()}`
|
||||||
|
);
|
||||||
if (currSize === graph.size) {
|
if (currSize === graph.size) {
|
||||||
console.log('loaded all newest');
|
console.log('loaded all newest');
|
||||||
this.loadedNewest = true;
|
this.loadedNewest = true;
|
||||||
}
|
}
|
||||||
} else if (!newer && !this.loadedOldest) {
|
} else if (!newer && !this.loadedOldest) {
|
||||||
const [index] = graph.peekSmallest()!;
|
const [index] = graph.peekSmallest()!;
|
||||||
await api.graph.getOlderSiblings(ship,name, 20, `/${index.toString()}`)
|
await api.graph.getOlderSiblings(ship, name, 20, `/${index.toString()}`);
|
||||||
this.calculateUnreadIndex();
|
this.calculateUnreadIndex();
|
||||||
if (currSize === graph.size) {
|
if (currSize === graph.size) {
|
||||||
console.log('loaded all oldest');
|
console.log('loaded all oldest');
|
||||||
@ -204,7 +212,6 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.setState({ fetchPending: false });
|
this.setState({ fetchPending: false });
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onScroll({ scrollTop, scrollHeight, windowHeight }) {
|
onScroll({ scrollTop, scrollHeight, windowHeight }) {
|
||||||
@ -222,8 +229,8 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
|
|||||||
if (!parent) return;
|
if (!parent) return;
|
||||||
const { scrollTop, scrollHeight, offsetHeight } = this.virtualList.window;
|
const { scrollTop, scrollHeight, offsetHeight } = this.virtualList.window;
|
||||||
if (
|
if (
|
||||||
(scrollHeight - parent.offsetTop > scrollTop)
|
scrollHeight - parent.offsetTop > scrollTop &&
|
||||||
&& (scrollHeight - parent.offsetTop < scrollTop + offsetHeight)
|
scrollHeight - parent.offsetTop < scrollTop + offsetHeight
|
||||||
) {
|
) {
|
||||||
this.dismissUnread();
|
this.dismissUnread();
|
||||||
}
|
}
|
||||||
@ -245,25 +252,39 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
|
|||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const unreadMarkerRef = this.unreadMarkerRef;
|
const unreadMarkerRef = this.unreadMarkerRef;
|
||||||
|
const messageProps = {
|
||||||
|
association,
|
||||||
const messageProps = { association, group, contacts, unreadMarkerRef, history, api, groups, associations };
|
group,
|
||||||
|
contacts,
|
||||||
|
unreadMarkerRef,
|
||||||
|
history,
|
||||||
|
api,
|
||||||
|
groups,
|
||||||
|
associations
|
||||||
|
};
|
||||||
const keys = graph.keys().reverse();
|
const keys = graph.keys().reverse();
|
||||||
const unreadIndex = graph.keys()[this.props.unreadCount];
|
const unreadIndex = graph.keys()[this.props.unreadCount];
|
||||||
const unreadMsg = unreadIndex && graph.get(unreadIndex);
|
const unreadMsg = unreadIndex && graph.get(unreadIndex);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col height='100%' overflow='hidden' position="relative">
|
<Col height='100%' overflow='hidden' position='relative'>
|
||||||
<UnreadNotice
|
<UnreadNotice
|
||||||
unreadCount={unreadCount}
|
unreadCount={unreadCount}
|
||||||
unreadMsg={unreadCount === 1 && unreadMsg && unreadMsg?.post.author === window.ship ? false : unreadMsg}
|
unreadMsg={
|
||||||
|
unreadCount === 1 &&
|
||||||
|
unreadMsg &&
|
||||||
|
unreadMsg?.post.author === window.ship
|
||||||
|
? false
|
||||||
|
: unreadMsg
|
||||||
|
}
|
||||||
dismissUnread={this.dismissUnread}
|
dismissUnread={this.dismissUnread}
|
||||||
onClick={this.scrollToUnread}
|
onClick={this.scrollToUnread}
|
||||||
/>
|
/>
|
||||||
<VirtualScroller
|
<VirtualScroller
|
||||||
ref={list => {this.virtualList = list}}
|
ref={(list) => {
|
||||||
origin="bottom"
|
this.virtualList = list;
|
||||||
|
}}
|
||||||
|
origin='bottom'
|
||||||
style={{ height: '100%' }}
|
style={{ height: '100%' }}
|
||||||
onStartReached={() => {
|
onStartReached={() => {
|
||||||
this.setState({ idle: false });
|
this.setState({ idle: false });
|
||||||
@ -276,18 +297,33 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
|
|||||||
const msg = graph.get(index)?.post;
|
const msg = graph.get(index)?.post;
|
||||||
if (!msg) return null;
|
if (!msg) return null;
|
||||||
if (!this.state.initialized) {
|
if (!this.state.initialized) {
|
||||||
return <MessagePlaceholder key={index.toString()} height="64px" index={index} />;
|
return (
|
||||||
|
<MessagePlaceholder
|
||||||
|
key={index.toString()}
|
||||||
|
height='64px'
|
||||||
|
index={index}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const isPending: boolean = 'pending' in msg && Boolean(msg.pending);
|
const isPending: boolean = 'pending' in msg && Boolean(msg.pending);
|
||||||
const isLastMessage = index.eq(graph.peekLargest()?.[0] ?? bigInt.zero);
|
const isLastMessage = index.eq(
|
||||||
|
graph.peekLargest()?.[0] ?? bigInt.zero
|
||||||
|
);
|
||||||
const highlighted = bigInt(this.props.scrollTo || -1).eq(index);
|
const highlighted = bigInt(this.props.scrollTo || -1).eq(index);
|
||||||
const graphIdx = keys.findIndex(idx => idx.eq(index));
|
const graphIdx = keys.findIndex((idx) => idx.eq(index));
|
||||||
const prevIdx = keys[graphIdx + 1];
|
const prevIdx = keys[graphIdx + 1];
|
||||||
const nextIdx = keys[graphIdx - 1];
|
const nextIdx = keys[graphIdx - 1];
|
||||||
|
|
||||||
|
|
||||||
const isLastRead: boolean = this.state.unreadIndex.eq(index);
|
const isLastRead: boolean = this.state.unreadIndex.eq(index);
|
||||||
const props = { measure, highlighted, scrollWindow, isPending, isLastRead, isLastMessage, msg, ...messageProps };
|
const props = {
|
||||||
|
measure,
|
||||||
|
highlighted,
|
||||||
|
scrollWindow,
|
||||||
|
isPending,
|
||||||
|
isLastRead,
|
||||||
|
isLastMessage,
|
||||||
|
msg,
|
||||||
|
...messageProps
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<ChatMessage
|
<ChatMessage
|
||||||
key={index.toString()}
|
key={index.toString()}
|
||||||
@ -305,4 +341,3 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import ReactMarkdown from 'react-markdown';
|
|||||||
import RemarkDisableTokenizers from 'remark-disable-tokenizers';
|
import RemarkDisableTokenizers from 'remark-disable-tokenizers';
|
||||||
import urbitOb from 'urbit-ob';
|
import urbitOb from 'urbit-ob';
|
||||||
import { Text } from '@tlon/indigo-react';
|
import { Text } from '@tlon/indigo-react';
|
||||||
import { GroupLink } from "~/views/components/GroupLink";
|
import { GroupLink } from '~/views/components/GroupLink';
|
||||||
|
|
||||||
const DISABLED_BLOCK_TOKENS = [
|
const DISABLED_BLOCK_TOKENS = [
|
||||||
'indentedCode',
|
'indentedCode',
|
||||||
@ -27,13 +27,28 @@ const DISABLED_INLINE_TOKENS = [
|
|||||||
|
|
||||||
const renderers = {
|
const renderers = {
|
||||||
inlineCode: ({ language, value }) => {
|
inlineCode: ({ language, value }) => {
|
||||||
return <Text mono p='1' backgroundColor='washedGray' fontSize='0' style={{ whiteSpace: 'preWrap'}}>{value}</Text>
|
return (
|
||||||
|
<Text
|
||||||
|
mono
|
||||||
|
p='1'
|
||||||
|
backgroundColor='washedGray'
|
||||||
|
fontSize='0'
|
||||||
|
style={{ whiteSpace: 'preWrap' }}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
paragraph: ({ children }) => {
|
paragraph: ({ children }) => {
|
||||||
return (<Text fontSize="1">{children}</Text>);
|
return (
|
||||||
|
<Text fontSize='1' lineHeight={'20px'}>
|
||||||
|
{children}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
code: ({ language, value }) => {
|
code: ({ language, value }) => {
|
||||||
return <Text
|
return (
|
||||||
|
<Text
|
||||||
p='1'
|
p='1'
|
||||||
className='clamp-message'
|
className='clamp-message'
|
||||||
display='block'
|
display='block'
|
||||||
@ -42,25 +57,26 @@ const renderers = {
|
|||||||
fontSize='0'
|
fontSize='0'
|
||||||
backgroundColor='washedGray'
|
backgroundColor='washedGray'
|
||||||
overflowX='auto'
|
overflowX='auto'
|
||||||
style={{ whiteSpace: 'pre'}}>
|
style={{ whiteSpace: 'pre' }}
|
||||||
|
>
|
||||||
{value}
|
{value}
|
||||||
</Text>
|
</Text>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const MessageMarkdown = React.memo(props => {
|
const MessageMarkdown = React.memo((props) => {
|
||||||
const { source, ...rest } = props;
|
const { source, ...rest } = props;
|
||||||
const blockCode = source.split('```');
|
const blockCode = source.split('```');
|
||||||
const codeLines = blockCode.map(codes => codes.split("\n"));
|
const codeLines = blockCode.map((codes) => codes.split('\n'));
|
||||||
const lines = codeLines.reduce((acc, val, i) => {
|
const lines = codeLines.reduce((acc, val, i) => {
|
||||||
if((i % 2) === 1) {
|
if (i % 2 === 1) {
|
||||||
return [...acc, `\`\`\`${val.join('\n')}\`\`\``];
|
return [...acc, `\`\`\`${val.join('\n')}\`\`\``];
|
||||||
} else {
|
} else {
|
||||||
return [...acc, ...val];
|
return [...acc, ...val];
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
return lines.map((line, i) => (
|
return lines.map((line, i) => (
|
||||||
<>
|
<>
|
||||||
{i !== 0 && <br />}
|
{i !== 0 && <br />}
|
||||||
@ -71,25 +87,31 @@ const MessageMarkdown = React.memo(props => {
|
|||||||
renderers={renderers}
|
renderers={renderers}
|
||||||
allowNode={(node, index, parent) => {
|
allowNode={(node, index, parent) => {
|
||||||
if (
|
if (
|
||||||
node.type === 'blockquote'
|
node.type === 'blockquote' &&
|
||||||
&& parent.type === 'root'
|
parent.type === 'root' &&
|
||||||
&& node.children.length
|
node.children.length &&
|
||||||
&& node.children[0].type === 'paragraph'
|
node.children[0].type === 'paragraph' &&
|
||||||
&& node.children[0].position.start.offset < 2
|
node.children[0].position.start.offset < 2
|
||||||
) {
|
) {
|
||||||
node.children[0].children[0].value = '>' + node.children[0].children[0].value;
|
node.children[0].children[0].value =
|
||||||
|
'>' + node.children[0].children[0].value;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}}
|
}}
|
||||||
plugins={[[RemarkDisableTokenizers, {
|
plugins={[
|
||||||
|
[
|
||||||
|
RemarkDisableTokenizers,
|
||||||
|
{
|
||||||
block: DISABLED_BLOCK_TOKENS,
|
block: DISABLED_BLOCK_TOKENS,
|
||||||
inline: DISABLED_INLINE_TOKENS
|
inline: DISABLED_INLINE_TOKENS
|
||||||
}]]}
|
}
|
||||||
|
]
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
))
|
));
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function TextContent(props) {
|
export default function TextContent(props) {
|
||||||
@ -98,10 +120,11 @@ export default function TextContent(props) {
|
|||||||
const group = content.text.match(
|
const group = content.text.match(
|
||||||
/([~][/])?(~[a-z]{3,6})(-[a-z]{6})?([/])(([a-z0-9-])+([/-])?)+/
|
/([~][/])?(~[a-z]{3,6})(-[a-z]{6})?([/])(([a-z0-9-])+([/-])?)+/
|
||||||
);
|
);
|
||||||
const isGroupLink = ((group !== null) // matched possible chatroom
|
const isGroupLink =
|
||||||
&& (group[2].length > 2) // possible ship?
|
group !== null && // matched possible chatroom
|
||||||
&& (urbitOb.isValidPatp(group[2]) // valid patp?
|
group[2].length > 2 && // possible ship?
|
||||||
&& (group[0] === content.text))) // entire message is room name?
|
urbitOb.isValidPatp(group[2]) && // valid patp?
|
||||||
|
group[0] === content.text; // entire message is room name?
|
||||||
|
|
||||||
if (isGroupLink) {
|
if (isGroupLink) {
|
||||||
const resource = `/ship/${content.text}`;
|
const resource = `/ship/${content.text}`;
|
||||||
@ -120,7 +143,13 @@ export default function TextContent(props) {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<Text mx="2px" flexShrink={0} color='black' fontSize={props.fontSize ? props.fontSize : '14px'} lineHeight="tall" style={{ overflowWrap: 'break-word' }}>
|
<Text
|
||||||
|
flexShrink={0}
|
||||||
|
color='black'
|
||||||
|
fontSize={props.fontSize ? props.fontSize : '14px'}
|
||||||
|
lineHeight={props.lineHeight ? props.lineHeight : '20px'}
|
||||||
|
style={{ overflowWrap: 'break-word' }}
|
||||||
|
>
|
||||||
<MessageMarkdown source={content.text} />
|
<MessageMarkdown source={content.text} />
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
|
@ -2,6 +2,8 @@ import React from 'react';
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { Box, Text } from '@tlon/indigo-react';
|
import { Box, Text } from '@tlon/indigo-react';
|
||||||
|
|
||||||
|
import Timestamp from '~/views/components/Timestamp';
|
||||||
|
|
||||||
export const UnreadNotice = (props) => {
|
export const UnreadNotice = (props) => {
|
||||||
const { unreadCount, unreadMsg, dismissUnread, onClick } = props;
|
const { unreadCount, unreadMsg, dismissUnread, onClick } = props;
|
||||||
|
|
||||||
@ -9,6 +11,8 @@ export const UnreadNotice = (props) => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const stamp = moment.unix(unreadMsg.post['time-sent'] / 1000);
|
||||||
|
|
||||||
let datestamp = moment.unix(unreadMsg.post['time-sent'] / 1000).format('YYYY.M.D');
|
let datestamp = moment.unix(unreadMsg.post['time-sent'] / 1000).format('YYYY.M.D');
|
||||||
const timestamp = moment.unix(unreadMsg.post['time-sent'] / 1000).format('HH:mm');
|
const timestamp = moment.unix(unreadMsg.post['time-sent'] / 1000).format('HH:mm');
|
||||||
|
|
||||||
@ -34,14 +38,9 @@ export const UnreadNotice = (props) => {
|
|||||||
borderRadius='1'
|
borderRadius='1'
|
||||||
border='1'
|
border='1'
|
||||||
borderColor='blue'>
|
borderColor='blue'>
|
||||||
<Text flexShrink='1' textOverflow='ellipsis' whiteSpace='pre' overflow='hidden' display='block' cursor='pointer' onClick={onClick}>
|
<Text flexShrink='1' textOverflow='ellipsis' whiteSpace='pre' overflow='hidden' display='flex' cursor='pointer' onClick={onClick}>
|
||||||
{unreadCount} new message{unreadCount > 1 ? 's' : ''} since{' '}
|
{unreadCount} new message{unreadCount > 1 ? 's' : ''} since{' '}
|
||||||
{datestamp && (
|
<Timestamp stamp={stamp} color='blue' date={true} />
|
||||||
<>
|
|
||||||
<Text color='blue'>~{datestamp}</Text> at{' '}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<Text color='blue'>{timestamp}</Text>
|
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
ml='4'
|
ml='4'
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import React, {useRef} from "react";
|
import React, { useRef } from 'react';
|
||||||
import { Box, Text, Col } from "@tlon/indigo-react";
|
import { Box, Text, Col } from '@tlon/indigo-react';
|
||||||
import f from "lodash/fp";
|
import f from 'lodash/fp';
|
||||||
import _ from "lodash";
|
import _ from 'lodash';
|
||||||
|
|
||||||
import { Associations, Association, Unreads, UnreadStats } from "~/types";
|
import { Associations, Association, Unreads, UnreadStats } from "@urbit/api";
|
||||||
import { alphabeticalOrder } from "~/logic/lib/util";
|
import { alphabeticalOrder } from "~/logic/lib/util";
|
||||||
import { getUnreadCount, getNotificationCount } from "~/logic/lib/hark";
|
import { getUnreadCount, getNotificationCount } from "~/logic/lib/hark";
|
||||||
import Tile from "../components/tiles/tile";
|
import Tile from "../components/tiles/tile";
|
||||||
@ -21,7 +21,6 @@ const sortGroupsAlph = (a: Association, b: Association) =>
|
|||||||
? 1
|
? 1
|
||||||
: alphabeticalOrder(a.metadata.title, b.metadata.title);
|
: alphabeticalOrder(a.metadata.title, b.metadata.title);
|
||||||
|
|
||||||
|
|
||||||
const getGraphUnreads = (associations: Associations, unreads: Unreads) => (path: string) =>
|
const getGraphUnreads = (associations: Associations, unreads: Unreads) => (path: string) =>
|
||||||
f.flow(
|
f.flow(
|
||||||
f.pickBy((a: Association) => a.group === path),
|
f.pickBy((a: Association) => a.group === path),
|
||||||
@ -38,12 +37,11 @@ const getGraphNotifications = (associations: Associations, unreads: Unreads) =>
|
|||||||
f.reduce(f.add, 0)
|
f.reduce(f.add, 0)
|
||||||
)(associations.graph);
|
)(associations.graph);
|
||||||
|
|
||||||
|
|
||||||
export default function Groups(props: GroupsProps & Parameters<typeof Box>[0]) {
|
export default function Groups(props: GroupsProps & Parameters<typeof Box>[0]) {
|
||||||
const { associations, unreads, inbox, ...boxProps } = props;
|
const { associations, unreads, inbox, ...boxProps } = props;
|
||||||
|
|
||||||
const groups = Object.values(associations?.groups || {})
|
const groups = Object.values(associations?.groups || {})
|
||||||
.filter((e) => e?.group in props.groups)
|
.filter(e => e?.group in props.groups)
|
||||||
.sort(sortGroupsAlph);
|
.sort(sortGroupsAlph);
|
||||||
const graphUnreads = getGraphUnreads(associations || {}, unreads);
|
const graphUnreads = getGraphUnreads(associations || {}, unreads);
|
||||||
const graphNotifications = getGraphNotifications(associations || {}, unreads);
|
const graphNotifications = getGraphNotifications(associations || {}, unreads);
|
||||||
@ -52,7 +50,7 @@ export default function Groups(props: GroupsProps & Parameters<typeof Box>[0]) {
|
|||||||
<>
|
<>
|
||||||
{groups.map((group, index) => {
|
{groups.map((group, index) => {
|
||||||
const path = group?.group;
|
const path = group?.group;
|
||||||
const unreadCount = graphUnreads(path)
|
const unreadCount = graphUnreads(path);
|
||||||
const notCount = graphNotifications(path);
|
const notCount = graphNotifications(path);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,38 +1,33 @@
|
|||||||
import React from "react"
|
import React from 'react';
|
||||||
import { Box, Button, Icon, Text } from "@tlon/indigo-react"
|
import { Box, Button, Icon, Text } from '@tlon/indigo-react';
|
||||||
import {useModal} from "~/logic/lib/useModal";
|
import { useModal } from '~/logic/lib/useModal';
|
||||||
|
|
||||||
const ModalButton = (props) => {
|
const ModalButton = (props) => {
|
||||||
const {
|
const { children, icon, text, bg, color, ...rest } = props;
|
||||||
children,
|
|
||||||
icon,
|
|
||||||
text,
|
|
||||||
bg,
|
|
||||||
color,
|
|
||||||
...rest
|
|
||||||
} = props;
|
|
||||||
const { modal, showModal } = useModal({ modal: props.children });
|
const { modal, showModal } = useModal({ modal: props.children });
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{modal}
|
{modal}
|
||||||
<Button
|
<Button
|
||||||
onClick={showModal}
|
onClick={showModal}
|
||||||
display="flex"
|
display='flex'
|
||||||
alignItems="center"
|
alignItems='center'
|
||||||
cursor="pointer"
|
cursor='pointer'
|
||||||
bg={bg}
|
bg={bg}
|
||||||
p={2}
|
p={2}
|
||||||
borderRadius={2}
|
borderRadius={2}
|
||||||
boxShadow="0 0 0px 1px inset"
|
boxShadow='0 0 0px 1px inset'
|
||||||
color="scales.black20"
|
color='scales.black20'
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
<Icon icon={props.icon} mr={2} color={color}></Icon><Text color={color}>{props.text}</Text>
|
<Icon icon={props.icon} mr={2} color={color}></Icon>
|
||||||
|
<Text color={color} whiteSpace='nowrap'>
|
||||||
|
{props.text}
|
||||||
|
</Text>
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ModalButton;
|
export default ModalButton;
|
||||||
|
@ -1,19 +1,18 @@
|
|||||||
import React, { useEffect, useCallback } from "react";
|
import React, { useEffect } from 'react';
|
||||||
import { Box, Row, Col, Center, LoadingSpinner, Text } from "@tlon/indigo-react";
|
import { Box, Col, Center, LoadingSpinner, Text } from '@tlon/indigo-react';
|
||||||
import { Switch, Route, Link } from "react-router-dom";
|
import { Switch, Route, Link } from 'react-router-dom';
|
||||||
import bigInt from 'big-integer';
|
import bigInt from 'big-integer';
|
||||||
|
|
||||||
import GlobalApi from "~/logic/api/global";
|
import GlobalApi from '~/logic/api/global';
|
||||||
import { StoreState } from "~/logic/store/type";
|
import { StoreState } from '~/logic/store/type';
|
||||||
import { uxToHex } from '~/logic/lib/util';
|
import { RouteComponentProps } from 'react-router-dom';
|
||||||
import { RouteComponentProps } from "react-router-dom";
|
|
||||||
|
|
||||||
import { LinkItem } from "./components/LinkItem";
|
import { LinkItem } from './components/LinkItem';
|
||||||
import { LinkPreview } from "./components/link-preview";
|
import { LinkWindow } from './LinkWindow';
|
||||||
import { LinkWindow } from "./LinkWindow";
|
import { Comments } from '~/views/components/Comments';
|
||||||
import { Comments } from "~/views/components/Comments";
|
|
||||||
|
|
||||||
import "./css/custom.css";
|
import './css/custom.css';
|
||||||
|
import { Association } from '@urbit/api/metadata';
|
||||||
|
|
||||||
const emptyMeasure = () => {};
|
const emptyMeasure = () => {};
|
||||||
|
|
||||||
@ -42,7 +41,7 @@ export function LinkResource(props: LinkResourceProps) {
|
|||||||
|
|
||||||
const relativePath = (p: string) => `${baseUrl}/resource/link${rid}${p}`;
|
const relativePath = (p: string) => `${baseUrl}/resource/link${rid}${p}`;
|
||||||
|
|
||||||
const [, , ship, name] = rid.split("/");
|
const [, , ship, name] = rid.split('/');
|
||||||
const resourcePath = `${ship.slice(1)}/${name}`;
|
const resourcePath = `${ship.slice(1)}/${name}`;
|
||||||
const resource = associations.graph[rid]
|
const resource = associations.graph[rid]
|
||||||
? associations.graph[rid]
|
? associations.graph[rid]
|
||||||
@ -66,7 +65,7 @@ export function LinkResource(props: LinkResourceProps) {
|
|||||||
<Switch>
|
<Switch>
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
path={relativePath("")}
|
path={relativePath('')}
|
||||||
render={(props) => {
|
render={(props) => {
|
||||||
return (
|
return (
|
||||||
<LinkWindow
|
<LinkWindow
|
||||||
@ -86,7 +85,7 @@ export function LinkResource(props: LinkResourceProps) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path={relativePath("/:index(\\d+)/:commentId?")}
|
path={relativePath('/:index(\\d+)/:commentId?')}
|
||||||
render={(props) => {
|
render={(props) => {
|
||||||
const index = bigInt(props.match.params.index);
|
const index = bigInt(props.match.params.index);
|
||||||
const editCommentId = props.match.params.commentId || null;
|
const editCommentId = props.match.params.commentId || null;
|
||||||
@ -95,7 +94,7 @@ export function LinkResource(props: LinkResourceProps) {
|
|||||||
return <div>Malformed URL</div>;
|
return <div>Malformed URL</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const node = !!graph ? graph.get(index) : null;
|
const node = graph ? graph.get(index) : null;
|
||||||
|
|
||||||
if (!node) {
|
if (!node) {
|
||||||
return <Box>Not found</Box>;
|
return <Box>Not found</Box>;
|
||||||
@ -106,7 +105,7 @@ export function LinkResource(props: LinkResourceProps) {
|
|||||||
return (
|
return (
|
||||||
<Col alignItems="center" overflowY="auto" width="100%">
|
<Col alignItems="center" overflowY="auto" width="100%">
|
||||||
<Col width="100%" p={3} maxWidth="768px">
|
<Col width="100%" p={3} maxWidth="768px">
|
||||||
<Link to={resourceUrl}><Text px={3} bold>{"<- Back"}</Text></Link>
|
<Link to={resourceUrl}><Text px={3} bold>{'<- Back'}</Text></Link>
|
||||||
<LinkItem
|
<LinkItem
|
||||||
contacts={contacts}
|
contacts={contacts}
|
||||||
key={node.post.index}
|
key={node.post.index}
|
||||||
|
@ -1,21 +1,21 @@
|
|||||||
import React, { useRef, useCallback, useEffect, useMemo } from "react";
|
import React, { useRef, useCallback, useEffect, useMemo } from 'react';
|
||||||
import { Col, Text } from "@tlon/indigo-react";
|
|
||||||
|
import { Col, Text } from '@tlon/indigo-react';
|
||||||
import bigInt from 'big-integer';
|
import bigInt from 'big-integer';
|
||||||
import {
|
import {
|
||||||
Association,
|
Association,
|
||||||
Graph,
|
Graph,
|
||||||
Contacts,
|
|
||||||
Unreads,
|
Unreads,
|
||||||
LocalUpdateRemoteContentPolicy,
|
|
||||||
Group,
|
Group,
|
||||||
Rolodex,
|
Rolodex,
|
||||||
S3State,
|
} from '@urbit/api';
|
||||||
} from "~/types";
|
|
||||||
import GlobalApi from "~/logic/api/global";
|
import GlobalApi from '~/logic/api/global';
|
||||||
import VirtualScroller from "~/views/components/VirtualScroller";
|
import VirtualScroller from '~/views/components/VirtualScroller';
|
||||||
import { LinkItem } from "./components/LinkItem";
|
import { LinkItem } from './components/LinkItem';
|
||||||
import LinkSubmit from "./components/LinkSubmit";
|
import LinkSubmit from './components/LinkSubmit';
|
||||||
import {isWriter} from "~/logic/lib/group";
|
import { isWriter } from '~/logic/lib/group';
|
||||||
|
import { S3State } from '~/types/s3-update';
|
||||||
|
|
||||||
interface LinkWindowProps {
|
interface LinkWindowProps {
|
||||||
association: Association;
|
association: Association;
|
||||||
@ -33,8 +33,6 @@ interface LinkWindowProps {
|
|||||||
}
|
}
|
||||||
export function LinkWindow(props: LinkWindowProps) {
|
export function LinkWindow(props: LinkWindowProps) {
|
||||||
const { graph, api, association } = props;
|
const { graph, api, association } = props;
|
||||||
const loadedNewest = useRef(true);
|
|
||||||
const loadedOldest = useRef(false);
|
|
||||||
const virtualList = useRef<VirtualScroller>();
|
const virtualList = useRef<VirtualScroller>();
|
||||||
const fetchLinks = useCallback(
|
const fetchLinks = useCallback(
|
||||||
async (newer: boolean) => {
|
async (newer: boolean) => {
|
||||||
@ -44,18 +42,19 @@ export function LinkWindow(props: LinkWindowProps) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const list = virtualList?.current;
|
const list = virtualList?.current;
|
||||||
if(!list) return;
|
if(!list)
|
||||||
|
return;
|
||||||
list.calculateVisibleItems();
|
list.calculateVisibleItems();
|
||||||
}, [graph.size]);
|
}, [graph.size]);
|
||||||
|
|
||||||
const first = graph.peekLargest()?.[0];
|
const first = graph.peekLargest()?.[0];
|
||||||
const [,,ship, name] = association.resource.split('/');
|
const [,,ship, name] = association.resource.split('/');
|
||||||
const canWrite = isWriter(props.group, association.resource)
|
const canWrite = isWriter(props.group, association.resource);
|
||||||
|
|
||||||
const style = useMemo(() =>
|
const style = useMemo(() =>
|
||||||
({
|
({
|
||||||
height: "100%",
|
height: '100%',
|
||||||
width: "100%",
|
width: '100%',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
alignItems: 'center'
|
alignItems: 'center'
|
||||||
@ -76,7 +75,7 @@ export function LinkWindow(props: LinkWindowProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<VirtualScroller
|
<VirtualScroller
|
||||||
ref={(l) => (virtualList.current = l ?? undefined)}
|
ref={l => (virtualList.current = l ?? undefined)}
|
||||||
origin="top"
|
origin="top"
|
||||||
style={style}
|
style={style}
|
||||||
onStartReached={() => {}}
|
onStartReached={() => {}}
|
||||||
@ -86,11 +85,12 @@ export function LinkWindow(props: LinkWindowProps) {
|
|||||||
renderer={({ index, measure, scrollWindow }) => {
|
renderer={({ index, measure, scrollWindow }) => {
|
||||||
const node = graph.get(index);
|
const node = graph.get(index);
|
||||||
const post = node?.post;
|
const post = node?.post;
|
||||||
if (!node || !post) return null;
|
if (!node || !post)
|
||||||
|
return null;
|
||||||
const linkProps = {
|
const linkProps = {
|
||||||
...props,
|
...props,
|
||||||
node,
|
node,
|
||||||
measure,
|
measure
|
||||||
};
|
};
|
||||||
if(canWrite && index.eq(first ?? bigInt.zero)) {
|
if(canWrite && index.eq(first ?? bigInt.zero)) {
|
||||||
return (
|
return (
|
||||||
@ -100,7 +100,7 @@ export function LinkWindow(props: LinkWindowProps) {
|
|||||||
</Col>
|
</Col>
|
||||||
<LinkItem {...linkProps} />
|
<LinkItem {...linkProps} />
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
return <LinkItem key={index.toString()} {...linkProps} />;
|
return <LinkItem key={index.toString()} {...linkProps} />;
|
||||||
}}
|
}}
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
import React, { useState, useEffect, useRef, useCallback, ReactElement } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { Row, Col, Anchor, Box, Text, Icon, Action } from '@tlon/indigo-react';
|
import { Row, Col, Anchor, Box, Text, Icon, Action } from '@tlon/indigo-react';
|
||||||
|
import { GraphNode, Group, Rolodex, Unreads } from '@urbit/api';
|
||||||
|
|
||||||
import { writeText } from '~/logic/lib/util';
|
import { writeText } from '~/logic/lib/util';
|
||||||
import Author from '~/views/components/Author';
|
import Author from '~/views/components/Author';
|
||||||
|
|
||||||
import { roleForShip } from '~/logic/lib/group';
|
import { roleForShip } from '~/logic/lib/group';
|
||||||
import { Contacts, GraphNode, Group, Rolodex, Unreads } from '~/types';
|
|
||||||
import GlobalApi from '~/logic/api/global';
|
import GlobalApi from '~/logic/api/global';
|
||||||
import { Dropdown } from '~/views/components/Dropdown';
|
import { Dropdown } from '~/views/components/Dropdown';
|
||||||
import RemoteContent from '~/views/components/RemoteContent';
|
import RemoteContent from '~/views/components/RemoteContent';
|
||||||
@ -22,7 +22,7 @@ interface LinkItemProps {
|
|||||||
measure: (el: any) => void;
|
measure: (el: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LinkItem = (props: LinkItemProps) => {
|
export const LinkItem = (props: LinkItemProps): ReactElement => {
|
||||||
const {
|
const {
|
||||||
node,
|
node,
|
||||||
resource,
|
resource,
|
||||||
@ -55,8 +55,7 @@ export const LinkItem = (props: LinkItemProps) => {
|
|||||||
window.addEventListener('blur', onBlur);
|
window.addEventListener('blur', onBlur);
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('blur', onBlur);
|
window.removeEventListener('blur', onBlur);
|
||||||
}
|
};
|
||||||
|
|
||||||
}, [markRead]);
|
}, [markRead]);
|
||||||
|
|
||||||
const URLparser = new RegExp(
|
const URLparser = new RegExp(
|
||||||
@ -94,11 +93,9 @@ export const LinkItem = (props: LinkItemProps) => {
|
|||||||
const commColor = (props.unreads.graph?.[appPath]?.[`/${index}`]?.unreads ?? 0) > 0 ? 'blue' : 'gray';
|
const commColor = (props.unreads.graph?.[appPath]?.[`/${index}`]?.unreads ?? 0) > 0 ? 'blue' : 'gray';
|
||||||
const isUnread = props.unreads.graph?.[appPath]?.['/']?.unreads?.has(node.post.index);
|
const isUnread = props.unreads.graph?.[appPath]?.['/']?.unreads?.has(node.post.index);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const onMeasure = useCallback(() => {
|
const onMeasure = useCallback(() => {
|
||||||
ref.current && measure(ref.current);
|
ref.current && measure(ref.current);
|
||||||
}, [ref.current, measure])
|
}, [ref.current, measure]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onMeasure();
|
onMeasure();
|
||||||
@ -119,8 +116,10 @@ export const LinkItem = (props: LinkItemProps) => {
|
|||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
onClick={markRead}
|
onClick={markRead}
|
||||||
>
|
>
|
||||||
|
<Text p={2}>{contents[0].text}</Text>
|
||||||
<RemoteContent
|
<RemoteContent
|
||||||
ref={r => { remoteRef.current = r }}
|
ref={r => { remoteRef.current = r }}
|
||||||
|
renderUrl={false}
|
||||||
url={contents[1].url}
|
url={contents[1].url}
|
||||||
text={contents[0].text}
|
text={contents[0].text}
|
||||||
unfold={true}
|
unfold={true}
|
||||||
@ -143,7 +142,8 @@ export const LinkItem = (props: LinkItemProps) => {
|
|||||||
alignSelf: 'center',
|
alignSelf: 'center',
|
||||||
style: { textOverflow: 'ellipsis', whiteSpace: 'pre', width: '100%' },
|
style: { textOverflow: 'ellipsis', whiteSpace: 'pre', width: '100%' },
|
||||||
p: 2
|
p: 2
|
||||||
}} />
|
}}
|
||||||
|
/>
|
||||||
<Text color="gray" p={2} flexShrink={0}>
|
<Text color="gray" p={2} flexShrink={0}>
|
||||||
<Anchor target="_blank" rel="noopener noreferrer" style={{ textDecoration: 'none' }} href={contents[1].url}>
|
<Anchor target="_blank" rel="noopener noreferrer" style={{ textDecoration: 'none' }} href={contents[1].url}>
|
||||||
<Box display='flex'>
|
<Box display='flex'>
|
||||||
|
@ -1,22 +1,22 @@
|
|||||||
import { BaseInput, Box, Button, LoadingSpinner, Text } from "@tlon/indigo-react";
|
import { BaseInput, Box, Button, LoadingSpinner, Text } from '@tlon/indigo-react';
|
||||||
import React, { useCallback, useState } from "react";
|
import React, { useCallback, useState } from 'react';
|
||||||
import GlobalApi from "~/logic/api/global";
|
import GlobalApi from '~/logic/api/global';
|
||||||
import { useFileDrag } from "~/logic/lib/useDrag";
|
import { useFileDrag } from '~/logic/lib/useDrag';
|
||||||
import useS3 from "~/logic/lib/useS3";
|
import useS3 from '~/logic/lib/useS3';
|
||||||
import { S3State } from "~/types";
|
import { S3State } from '@urbit/api';
|
||||||
import SubmitDragger from "~/views/components/SubmitDragger";
|
import SubmitDragger from '~/views/components/SubmitDragger';
|
||||||
import { createPost } from "~/logic/api/graph";
|
import { createPost } from '~/logic/api/graph';
|
||||||
import { hasProvider } from "oembed-parser";
|
import { hasProvider } from 'oembed-parser';
|
||||||
|
|
||||||
interface LinkSubmitProps {
|
interface LinkSubmitProps {
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
s3: S3State;
|
s3: S3State;
|
||||||
name: string;
|
name: string;
|
||||||
ship: string;
|
ship: string;
|
||||||
};
|
}
|
||||||
|
|
||||||
const LinkSubmit = (props: LinkSubmitProps) => {
|
const LinkSubmit = (props: LinkSubmitProps) => {
|
||||||
let { canUpload, uploadDefault, uploading, promptUpload } = useS3(props.s3);
|
const { canUpload, uploadDefault, uploading, promptUpload } = useS3(props.s3);
|
||||||
|
|
||||||
const [submitFocused, setSubmitFocused] = useState(false);
|
const [submitFocused, setSubmitFocused] = useState(false);
|
||||||
const [urlFocused, setUrlFocused] = useState(false);
|
const [urlFocused, setUrlFocused] = useState(false);
|
||||||
@ -100,7 +100,7 @@ const LinkSubmit = (props: LinkSubmitProps) => {
|
|||||||
|
|
||||||
const onLinkChange = (linkValue: string) => {
|
const onLinkChange = (linkValue: string) => {
|
||||||
setLinkValueHook(linkValue);
|
setLinkValueHook(linkValue);
|
||||||
const link = validateLink(linkValue)
|
const link = validateLink(linkValue);
|
||||||
setLinkValid(link);
|
setLinkValid(link);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,31 +1,29 @@
|
|||||||
import React, { ReactNode, useCallback } from "react";
|
import React, { ReactNode, useCallback } from 'react';
|
||||||
import moment from "moment";
|
import moment from 'moment';
|
||||||
import { Row, Box, Col, Text, Anchor, Icon, Action } from "@tlon/indigo-react";
|
import { Row, Box, Col, Text, Anchor, Icon, Action } from '@tlon/indigo-react';
|
||||||
import { Link, useHistory } from "react-router-dom";
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
import _ from "lodash";
|
import _ from 'lodash';
|
||||||
import {
|
import {
|
||||||
Post,
|
|
||||||
GraphNotifIndex,
|
GraphNotifIndex,
|
||||||
GraphNotificationContents,
|
GraphNotificationContents,
|
||||||
Associations,
|
Associations,
|
||||||
Content,
|
|
||||||
Rolodex,
|
Rolodex,
|
||||||
Groups,
|
Groups
|
||||||
} from "~/types";
|
} from '~/types';
|
||||||
import { Header } from "./header";
|
import { Header } from './header';
|
||||||
import { cite, deSig, pluralize } from "~/logic/lib/util";
|
import { cite, deSig, pluralize } from '~/logic/lib/util';
|
||||||
import { Sigil } from "~/logic/lib/sigil";
|
import { Sigil } from '~/logic/lib/sigil';
|
||||||
import RichText from "~/views/components/RichText";
|
import RichText from '~/views/components/RichText';
|
||||||
import GlobalApi from "~/logic/api/global";
|
import GlobalApi from '~/logic/api/global';
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from 'react-markdown';
|
||||||
import { getSnippet } from "~/logic/lib/publish";
|
import { getSnippet } from '~/logic/lib/publish';
|
||||||
import styled from "styled-components";
|
import styled from 'styled-components';
|
||||||
import {MentionText} from "~/views/components/MentionText";
|
import { MentionText } from '~/views/components/MentionText';
|
||||||
import ChatMessage, {MessageWithoutSigil} from "../chat/components/ChatMessage";
|
import ChatMessage from '../chat/components/ChatMessage';
|
||||||
|
|
||||||
function getGraphModuleIcon(module: string) {
|
function getGraphModuleIcon(module: string) {
|
||||||
if (module === "link") {
|
if (module === 'link') {
|
||||||
return "Collection";
|
return 'Collection';
|
||||||
}
|
}
|
||||||
return _.capitalize(module);
|
return _.capitalize(module);
|
||||||
}
|
}
|
||||||
@ -34,81 +32,89 @@ const FilterBox = styled(Box)`
|
|||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
to bottom,
|
to bottom,
|
||||||
transparent,
|
transparent,
|
||||||
${(p) => p.theme.colors.white}
|
${p => p.theme.colors.white}
|
||||||
);
|
);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function describeNotification(description: string, plural: boolean) {
|
function describeNotification(description: string, plural: boolean): string {
|
||||||
switch (description) {
|
switch (description) {
|
||||||
case "link":
|
case 'link':
|
||||||
return `added ${pluralize("new link", plural)} to`;
|
return `added ${pluralize('new link', plural)} to`;
|
||||||
case "comment":
|
case 'comment':
|
||||||
return `left ${pluralize("comment", plural)} on`;
|
return `left ${pluralize('comment', plural)} on`;
|
||||||
case "edit-comment":
|
case 'edit-comment':
|
||||||
return `updated ${pluralize("comment", plural)} on`;
|
return `updated ${pluralize('comment', plural)} on`;
|
||||||
case "note":
|
case 'note':
|
||||||
return `posted ${pluralize("note", plural)} to`;
|
return `posted ${pluralize('note', plural)} to`;
|
||||||
case "edit-note":
|
case 'edit-note':
|
||||||
return `updated ${pluralize("note", plural)} in`;
|
return `updated ${pluralize('note', plural)} in`;
|
||||||
case "mention":
|
case 'mention':
|
||||||
return "mentioned you on";
|
return 'mentioned you on';
|
||||||
case "message":
|
case 'message':
|
||||||
return `sent ${pluralize("message", plural)} to`;
|
return `sent ${pluralize('message', plural)} to`;
|
||||||
default:
|
default:
|
||||||
return description;
|
return description;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const GraphUrl = ({ url, title }) => (
|
const GraphUrl = ({ url, title }) => (
|
||||||
<Box borderRadius="2" p="2" bg="scales.black05">
|
<Box borderRadius='2' p='2' bg='scales.black05'>
|
||||||
<Anchor underline={false} target="_blank" color="black" href={url}>
|
<Anchor underline={false} target='_blank' color='black' href={url}>
|
||||||
<Icon verticalAlign="bottom" mr="2" icon="ArrowExternal" />
|
<Icon verticalAlign='bottom' mr='2' icon='ArrowExternal' />
|
||||||
{title}
|
{title}
|
||||||
</Anchor>
|
</Anchor>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
||||||
const GraphNodeContent = ({ group, post, contacts, mod, description, index, remoteContentPolicy }) => {
|
const GraphNodeContent = ({
|
||||||
|
group,
|
||||||
|
post,
|
||||||
|
contacts,
|
||||||
|
mod,
|
||||||
|
description,
|
||||||
|
index,
|
||||||
|
remoteContentPolicy
|
||||||
|
}) => {
|
||||||
const { contents } = post;
|
const { contents } = post;
|
||||||
const idx = index.slice(1).split("/");
|
const idx = index.slice(1).split('/');
|
||||||
if (mod === "link") {
|
if (mod === 'link') {
|
||||||
if (idx.length === 1) {
|
if (idx.length === 1) {
|
||||||
const [{ text }, { url }] = contents;
|
const [{ text }, { url }] = contents;
|
||||||
return <GraphUrl title={text} url={url} />;
|
return <GraphUrl title={text} url={url} />;
|
||||||
} else if (idx.length === 3) {
|
} else if (idx.length === 3) {
|
||||||
return <MentionText
|
return (
|
||||||
content={contents}
|
<MentionText content={contents} contacts={contacts} group={group} />
|
||||||
contacts={contacts}
|
);
|
||||||
group={group}
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (mod === "publish") {
|
if (mod === 'publish') {
|
||||||
if (idx[1] === "2") {
|
if (idx[1] === '2') {
|
||||||
return <MentionText
|
return (
|
||||||
|
<MentionText
|
||||||
content={contents}
|
content={contents}
|
||||||
group={group}
|
group={group}
|
||||||
contacts={contacts}
|
contacts={contacts}
|
||||||
fontSize='14px'
|
fontSize='14px'
|
||||||
lineHeight="tall"
|
lineHeight='tall'
|
||||||
/>
|
/>
|
||||||
} else if (idx[1] === "1") {
|
);
|
||||||
|
} else if (idx[1] === '1') {
|
||||||
const [{ text: header }, { text: body }] = contents;
|
const [{ text: header }, { text: body }] = contents;
|
||||||
const snippet = getSnippet(body);
|
const snippet = getSnippet(body);
|
||||||
return (
|
return (
|
||||||
<Col>
|
<Col>
|
||||||
<Box mb="2" fontWeight="500">
|
<Box mb='2' fontWeight='500'>
|
||||||
<Text>{header}</Text>
|
<Text>{header}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box overflow="hidden" maxHeight="400px" position="relative">
|
<Box overflow='hidden' maxHeight='400px' position='relative'>
|
||||||
<Text lineHeight="tall">{snippet}</Text>
|
<Text lineHeight='tall'>{snippet}</Text>
|
||||||
<FilterBox
|
<FilterBox
|
||||||
width="100%"
|
width='100%'
|
||||||
zIndex="1"
|
zIndex='1'
|
||||||
height="calc(100% - 2em)"
|
height='calc(100% - 2em)'
|
||||||
bottom="-4px"
|
bottom='-4px'
|
||||||
position="absolute"
|
position='absolute'
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Col>
|
</Col>
|
||||||
@ -119,13 +125,15 @@ const GraphNodeContent = ({ group, post, contacts, mod, description, index, remo
|
|||||||
if (mod === 'chat') {
|
if (mod === 'chat') {
|
||||||
return (
|
return (
|
||||||
<Row
|
<Row
|
||||||
width="100%"
|
width='100%'
|
||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
flexGrow={1}
|
flexGrow={1}
|
||||||
flexWrap="wrap"
|
flexWrap='wrap'
|
||||||
|
marginLeft='-32px'
|
||||||
>
|
>
|
||||||
<MessageWithoutSigil
|
<ChatMessage
|
||||||
containerClass="items-top cf hide-child"
|
renderSigil={false}
|
||||||
|
containerClass='items-top cf hide-child'
|
||||||
measure={() => {}}
|
measure={() => {}}
|
||||||
group={group}
|
group={group}
|
||||||
contacts={contacts}
|
contacts={contacts}
|
||||||
@ -135,30 +143,36 @@ const GraphNodeContent = ({ group, post, contacts, mod, description, index, remo
|
|||||||
fontSize='0'
|
fontSize='0'
|
||||||
pt='2'
|
pt='2'
|
||||||
/>
|
/>
|
||||||
</Row>);
|
</Row>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
function getNodeUrl(mod: string, hidden: boolean, groupPath: string, graph: string, index: string) {
|
function getNodeUrl(
|
||||||
|
mod: string,
|
||||||
|
hidden: boolean,
|
||||||
|
groupPath: string,
|
||||||
|
graph: string,
|
||||||
|
index: string
|
||||||
|
) {
|
||||||
if (hidden && mod === 'chat') {
|
if (hidden && mod === 'chat') {
|
||||||
groupPath = '/messages';
|
groupPath = '/messages';
|
||||||
} else if (hidden) {
|
} else if (hidden) {
|
||||||
groupPath = '/home';
|
groupPath = '/home';
|
||||||
}
|
}
|
||||||
const graphUrl = `/~landscape${groupPath}/resource/${mod}${graph}`;
|
const graphUrl = `/~landscape${groupPath}/resource/${mod}${graph}`;
|
||||||
const idx = index.slice(1).split("/");
|
const idx = index.slice(1).split('/');
|
||||||
if (mod === "publish") {
|
if (mod === 'publish') {
|
||||||
const [noteId] = idx;
|
const [noteId] = idx;
|
||||||
return `${graphUrl}/note/${noteId}`;
|
return `${graphUrl}/note/${noteId}`;
|
||||||
} else if (mod === "link") {
|
} else if (mod === 'link') {
|
||||||
const [linkId] = idx;
|
const [linkId] = idx;
|
||||||
return `${graphUrl}/${linkId}`;
|
return `${graphUrl}/${linkId}`;
|
||||||
} else if (mod === 'chat') {
|
} else if (mod === 'chat') {
|
||||||
return graphUrl;
|
return graphUrl;
|
||||||
}
|
}
|
||||||
return "";
|
return '';
|
||||||
}
|
}
|
||||||
const GraphNode = ({
|
const GraphNode = ({
|
||||||
post,
|
post,
|
||||||
@ -174,9 +188,7 @@ const GraphNode = ({
|
|||||||
read,
|
read,
|
||||||
onRead,
|
onRead,
|
||||||
showContact = false,
|
showContact = false,
|
||||||
remoteContentPolicy
|
}): ReactElement => {
|
||||||
}) => {
|
|
||||||
const { contents } = post;
|
|
||||||
author = deSig(author);
|
author = deSig(author);
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
@ -186,10 +198,12 @@ const GraphNode = ({
|
|||||||
size={16}
|
size={16}
|
||||||
icon
|
icon
|
||||||
color={`#000000`}
|
color={`#000000`}
|
||||||
classes="mix-blend-diff"
|
classes='mix-blend-diff'
|
||||||
padding={2}
|
padding={2}
|
||||||
/>
|
/>
|
||||||
) : <Box style={{ width: '16px' }}></Box>;
|
) : (
|
||||||
|
<Box style={{ width: '16px' }}></Box>
|
||||||
|
);
|
||||||
|
|
||||||
const groupContacts = contacts[groupPath] ?? {};
|
const groupContacts = contacts[groupPath] ?? {};
|
||||||
|
|
||||||
@ -203,24 +217,26 @@ const GraphNode = ({
|
|||||||
}, [read, onRead]);
|
}, [read, onRead]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row onClick={onClick} gapX="2" pt={showContact ? 2 : 0}>
|
<Row onClick={onClick} gapX='2' pt={showContact ? 2 : 0}>
|
||||||
<Col>{img}</Col>
|
<Col>{img}</Col>
|
||||||
<Col flexGrow={1} alignItems="flex-start">
|
<Col flexGrow={1} alignItems='flex-start'>
|
||||||
{showContact && <Row
|
{showContact && (
|
||||||
mb="2"
|
<Row
|
||||||
height="16px"
|
mb='2'
|
||||||
alignItems="center"
|
height='16px'
|
||||||
p="1"
|
alignItems='center'
|
||||||
backgroundColor="white"
|
p='1'
|
||||||
|
backgroundColor='white'
|
||||||
>
|
>
|
||||||
<Text mono title={author}>
|
<Text mono title={author}>
|
||||||
{cite(author)}
|
{cite(author)}
|
||||||
</Text>
|
</Text>
|
||||||
<Text ml="2" gray>
|
<Text ml='2' gray>
|
||||||
{moment(time).format("HH:mm")}
|
{moment(time).format('HH:mm')}
|
||||||
</Text>
|
</Text>
|
||||||
</Row>}
|
</Row>
|
||||||
<Row width="100%" p="1" flexDirection="column">
|
)}
|
||||||
|
<Row width='100%' p='1' flexDirection='column'>
|
||||||
<GraphNodeContent
|
<GraphNodeContent
|
||||||
contacts={groupContacts}
|
contacts={groupContacts}
|
||||||
post={post}
|
post={post}
|
||||||
@ -249,7 +265,7 @@ export function GraphNotification(props: {
|
|||||||
}) {
|
}) {
|
||||||
const { contents, index, read, time, api, timebox, groups } = props;
|
const { contents, index, read, time, api, timebox, groups } = props;
|
||||||
|
|
||||||
const authors = _.map(contents, "author");
|
const authors = _.map(contents, 'author');
|
||||||
const { graph, group } = index;
|
const { graph, group } = index;
|
||||||
const icon = getGraphModuleIcon(index.module);
|
const icon = getGraphModuleIcon(index.module);
|
||||||
const desc = describeNotification(index.description, contents.length !== 1);
|
const desc = describeNotification(index.description, contents.length !== 1);
|
||||||
@ -259,7 +275,7 @@ export function GraphNotification(props: {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return api.hark["read"](timebox, { graph: index });
|
return api.hark['read'](timebox, { graph: index });
|
||||||
}, [api, timebox, index, read]);
|
}, [api, timebox, index, read]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -277,14 +293,14 @@ return (
|
|||||||
description={desc}
|
description={desc}
|
||||||
associations={props.associations}
|
associations={props.associations}
|
||||||
/>
|
/>
|
||||||
<Box flexGrow={1} width="100%" pl={5} gridArea="main">
|
<Box flexGrow={1} width='100%' pl={5} gridArea='main'>
|
||||||
{_.map(contents, (content, idx) => (
|
{_.map(contents, (content, idx) => (
|
||||||
<GraphNode
|
<GraphNode
|
||||||
post={content}
|
post={content}
|
||||||
author={content.author}
|
author={content.author}
|
||||||
contacts={props.contacts}
|
contacts={props.contacts}
|
||||||
mod={index.module}
|
mod={index.module}
|
||||||
time={content?.["time-sent"]}
|
time={content?.['time-sent']}
|
||||||
description={index.description}
|
description={index.description}
|
||||||
index={content.index}
|
index={content.index}
|
||||||
graph={graph}
|
graph={graph}
|
||||||
|
@ -1,44 +1,34 @@
|
|||||||
import React, { ReactNode, useCallback } from "react";
|
import React, { ReactElement, useCallback } from 'react';
|
||||||
import moment from "moment";
|
import _ from 'lodash';
|
||||||
import { Row, Box, Col, Text, Anchor, Icon, Action } from "@tlon/indigo-react";
|
|
||||||
import _ from "lodash";
|
import { Col } from '@tlon/indigo-react';
|
||||||
import { NotificationProps } from "./types";
|
|
||||||
import {
|
import {
|
||||||
Post,
|
|
||||||
GraphNotifIndex,
|
|
||||||
GraphNotificationContents,
|
|
||||||
Associations,
|
Associations,
|
||||||
Content,
|
|
||||||
IndexedNotification,
|
|
||||||
GroupNotificationContents,
|
GroupNotificationContents,
|
||||||
GroupNotifIndex,
|
GroupNotifIndex,
|
||||||
GroupUpdate,
|
GroupUpdate,
|
||||||
Rolodex,
|
Rolodex
|
||||||
} from "~/types";
|
} from '@urbit/api';
|
||||||
import { Header } from "./header";
|
|
||||||
import { cite, deSig } from "~/logic/lib/util";
|
|
||||||
import { Sigil } from "~/logic/lib/sigil";
|
|
||||||
import RichText from "~/views/components/RichText";
|
|
||||||
import GlobalApi from "~/logic/api/global";
|
|
||||||
import { StatelessAsyncAction } from "~/views/components/StatelessAsyncAction";
|
|
||||||
|
|
||||||
|
import { Header } from './header';
|
||||||
|
import GlobalApi from '~/logic/api/global';
|
||||||
|
|
||||||
function describeNotification(description: string, plural: boolean) {
|
function describeNotification(description: string, plural: boolean) {
|
||||||
switch (description) {
|
switch (description) {
|
||||||
case "add-members":
|
case 'add-members':
|
||||||
return "joined";
|
return 'joined';
|
||||||
case "remove-members":
|
case 'remove-members':
|
||||||
return "left";
|
return 'left';
|
||||||
default:
|
default:
|
||||||
return description;
|
return description;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getGroupUpdateParticipants(update: GroupUpdate) {
|
function getGroupUpdateParticipants(update: GroupUpdate): string[] {
|
||||||
if ("addMembers" in update) {
|
if ('addMembers' in update) {
|
||||||
return update.addMembers.ships;
|
return update.addMembers.ships;
|
||||||
}
|
}
|
||||||
if ("removeMembers" in update) {
|
if ('removeMembers' in update) {
|
||||||
return update.removeMembers.ships;
|
return update.removeMembers.ships;
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
@ -56,7 +46,7 @@ interface GroupNotificationProps {
|
|||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GroupNotification(props: GroupNotificationProps) {
|
export function GroupNotification(props: GroupNotificationProps): ReactElement {
|
||||||
const { contents, index, read, time, api, timebox, associations } = props;
|
const { contents, index, read, time, api, timebox, associations } = props;
|
||||||
|
|
||||||
const authors = _.flatten(_.map(contents, getGroupUpdateParticipants));
|
const authors = _.flatten(_.map(contents, getGroupUpdateParticipants));
|
||||||
@ -68,7 +58,7 @@ export function GroupNotification(props: GroupNotificationProps) {
|
|||||||
if (props.archived) {
|
if (props.archived) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const func = read ? "unread" : "read";
|
const func = read ? 'unread' : 'read';
|
||||||
return api.hark[func](timebox, { group: index });
|
return api.hark[func](timebox, { group: index });
|
||||||
}, [api, timebox, index, read]);
|
}, [api, timebox, index, read]);
|
||||||
|
|
||||||
|
@ -1,17 +1,20 @@
|
|||||||
import React from "react";
|
import React, { ReactElement } from 'react';
|
||||||
import { Text as NormalText, Row, Icon, Rule, Box } from "@tlon/indigo-react";
|
import f from 'lodash/fp';
|
||||||
import f from "lodash/fp";
|
import _ from 'lodash';
|
||||||
import _ from "lodash";
|
import moment from 'moment';
|
||||||
import moment from "moment";
|
|
||||||
import { PropFunc } from "~/types/util";
|
import { Text as NormalText, Row, Icon, Rule } from '@tlon/indigo-react';
|
||||||
import { getContactDetails, useShowNickname } from "~/logic/lib/util";
|
import { Associations, Contact, Contacts, Rolodex } from '@urbit/api';
|
||||||
import { Associations, Contact, Contacts, Rolodex } from "~/types";
|
|
||||||
|
import { PropFunc } from '~/types/util';
|
||||||
|
import { useShowNickname } from '~/logic/lib/util';
|
||||||
|
import Timestamp from '~/views/components/Timestamp';
|
||||||
|
|
||||||
const Text = (props: PropFunc<typeof Text>) => (
|
const Text = (props: PropFunc<typeof Text>) => (
|
||||||
<NormalText fontWeight="500" {...props} />
|
<NormalText fontWeight="500" {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
function Author(props: { patp: string; contacts: Contacts; last?: boolean }) {
|
function Author(props: { patp: string; contacts: Contacts; last?: boolean }): ReactElement {
|
||||||
const contact: Contact | undefined = props.contacts?.[props.patp];
|
const contact: Contact | undefined = props.contacts?.[props.patp];
|
||||||
|
|
||||||
const showNickname = useShowNickname(contact);
|
const showNickname = useShowNickname(contact);
|
||||||
@ -20,7 +23,7 @@ function Author(props: { patp: string; contacts: Contacts; last?: boolean }) {
|
|||||||
return (
|
return (
|
||||||
<Text mono={!showNickname}>
|
<Text mono={!showNickname}>
|
||||||
{name}
|
{name}
|
||||||
{!props.last && ", "}
|
{!props.last && ', '}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -36,7 +39,7 @@ export function Header(props: {
|
|||||||
time: number;
|
time: number;
|
||||||
read: boolean;
|
read: boolean;
|
||||||
associations: Associations;
|
associations: Associations;
|
||||||
} & PropFunc<typeof Row> ) {
|
} & PropFunc<typeof Row> ): ReactElement {
|
||||||
const { description, channel, group, moduleIcon, read } = props;
|
const { description, channel, group, moduleIcon, read } = props;
|
||||||
const contacts = props.contacts[group] || {};
|
const contacts = props.contacts[group] || {};
|
||||||
|
|
||||||
@ -50,17 +53,17 @@ export function Header(props: {
|
|||||||
const last = lent - 1 === parseInt(idx, 10);
|
const last = lent - 1 === parseInt(idx, 10);
|
||||||
return <Author key={idx} contacts={contacts} patp={p} last={last} />;
|
return <Author key={idx} contacts={contacts} patp={p} last={last} />;
|
||||||
}),
|
}),
|
||||||
(auths) => (
|
auths => (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{auths}
|
{auths}
|
||||||
|
|
||||||
{authors.length > 3 &&
|
{authors.length > 3 &&
|
||||||
` and ${authors.length - 3} other${authors.length === 4 ? "" : "s"}`}
|
` and ${authors.length - 3} other${authors.length === 4 ? '' : 's'}`}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
)(authors);
|
)(authors);
|
||||||
|
|
||||||
const time = moment(props.time).format("HH:mm");
|
const time = moment(props.time).format('HH:mm');
|
||||||
const groupTitle =
|
const groupTitle =
|
||||||
props.associations.groups?.[props.group]?.metadata?.title;
|
props.associations.groups?.[props.group]?.metadata?.title;
|
||||||
|
|
||||||
@ -84,8 +87,8 @@ export function Header(props: {
|
|||||||
{authorDesc}
|
{authorDesc}
|
||||||
</Text>
|
</Text>
|
||||||
<Text mr="1">{description}</Text>
|
<Text mr="1">{description}</Text>
|
||||||
{!!moduleIcon && <Icon icon={moduleIcon as any} mr={1} />}
|
{Boolean(moduleIcon) && <Icon icon={moduleIcon as any} mr={1} />}
|
||||||
{!!channel && <Text fontWeight="500" mr={1}>{channelTitle}</Text>}
|
{Boolean(channel) && <Text fontWeight="500" mr={1}>{channelTitle}</Text>}
|
||||||
<Rule vertical height="12px" mr={1} />
|
<Rule vertical height="12px" mr={1} />
|
||||||
{groupTitle &&
|
{groupTitle &&
|
||||||
<>
|
<>
|
||||||
@ -93,9 +96,7 @@ export function Header(props: {
|
|||||||
<Rule vertical height="12px" mr={1} />
|
<Rule vertical height="12px" mr={1} />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
<Text fontWeight="regular" color="lightGray">
|
<Timestamp stamp={moment(props.time)} color="lightGray" date={false} />
|
||||||
{time}
|
|
||||||
</Text>
|
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,28 @@
|
|||||||
import React, { useEffect, useCallback, useRef, useState } from "react";
|
import React, { useEffect, useCallback, useRef } from 'react';
|
||||||
import f from "lodash/fp";
|
import f from 'lodash/fp';
|
||||||
import _ from "lodash";
|
import _ from 'lodash';
|
||||||
import { Icon, Col, Center, Row, Box, Text, Anchor, Rule, LoadingSpinner } from "@tlon/indigo-react";
|
import moment from 'moment';
|
||||||
import moment from "moment";
|
import { BigInteger } from 'big-integer';
|
||||||
import { Notifications, Rolodex, Timebox, IndexedNotification, Groups, joinProgress, JoinRequests, GroupNotificationsConfig, NotificationGraphConfig } from "~/types";
|
|
||||||
import { MOMENT_CALENDAR_DATE, daToUnix, resourceAsPath } from "~/logic/lib/util";
|
import { Col, Center, Box, Text, LoadingSpinner } from '@tlon/indigo-react';
|
||||||
import { BigInteger } from "big-integer";
|
import {
|
||||||
import GlobalApi from "~/logic/api/global";
|
Associations,
|
||||||
import { Notification } from "./notification";
|
Notifications,
|
||||||
import { Associations } from "~/types";
|
Rolodex,
|
||||||
import { InviteItem } from '~/views/components/Invite';
|
Timebox,
|
||||||
import { useWaitForProps } from "~/logic/lib/useWaitForProps";
|
IndexedNotification,
|
||||||
import { useHistory } from "react-router-dom";
|
Groups,
|
||||||
import {useModal} from "~/logic/lib/useModal";
|
JoinRequests,
|
||||||
import {JoinGroup} from "~/views/landscape/components/JoinGroup";
|
GroupNotificationsConfig,
|
||||||
import {JoiningStatus} from "./joining";
|
NotificationGraphConfig,
|
||||||
import {Invites} from "./invites";
|
Invites as InviteType
|
||||||
import {useLazyScroll} from "~/logic/lib/useLazyScroll";
|
} from '@urbit/api';
|
||||||
|
|
||||||
|
import { MOMENT_CALENDAR_DATE, daToUnix } from '~/logic/lib/util';
|
||||||
|
import GlobalApi from '~/logic/api/global';
|
||||||
|
import { Notification } from './notification';
|
||||||
|
import { Invites } from './invites';
|
||||||
|
import { useLazyScroll } from '~/logic/lib/useLazyScroll';
|
||||||
|
|
||||||
type DatedTimebox = [BigInteger, Timebox];
|
type DatedTimebox = [BigInteger, Timebox];
|
||||||
|
|
||||||
@ -25,12 +31,12 @@ function filterNotification(associations: Associations, groups: string[]) {
|
|||||||
return () => true;
|
return () => true;
|
||||||
}
|
}
|
||||||
return (n: IndexedNotification) => {
|
return (n: IndexedNotification) => {
|
||||||
if ("graph" in n.index) {
|
if ('graph' in n.index) {
|
||||||
const { group } = n.index.graph;
|
const { group } = n.index.graph;
|
||||||
return groups.findIndex((g) => group === g) !== -1;
|
return groups.findIndex(g => group === g) !== -1;
|
||||||
} else if ("group" in n.index) {
|
} else if ('group' in n.index) {
|
||||||
const { group } = n.index.group;
|
const { group } = n.index.group;
|
||||||
return groups.findIndex((g) => group === g) !== -1;
|
return groups.findIndex(g => group === g) !== -1;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
@ -46,7 +52,7 @@ export default function Inbox(props: {
|
|||||||
associations: Associations;
|
associations: Associations;
|
||||||
contacts: Rolodex;
|
contacts: Rolodex;
|
||||||
filter: string[];
|
filter: string[];
|
||||||
invites: any;
|
invites: InviteType;
|
||||||
pendingJoin: JoinRequests;
|
pendingJoin: JoinRequests;
|
||||||
notificationsGroupConfig: GroupNotificationsConfig;
|
notificationsGroupConfig: GroupNotificationsConfig;
|
||||||
notificationsGraphConfig: NotificationGraphConfig;
|
notificationsGraphConfig: NotificationGraphConfig;
|
||||||
@ -70,30 +76,30 @@ export default function Inbox(props: {
|
|||||||
const calendar = {
|
const calendar = {
|
||||||
...MOMENT_CALENDAR_DATE, sameDay: function (now) {
|
...MOMENT_CALENDAR_DATE, sameDay: function (now) {
|
||||||
if (this.subtract(6, 'hours').isBefore(now)) {
|
if (this.subtract(6, 'hours').isBefore(now)) {
|
||||||
return "[Earlier Today]";
|
return '[Earlier Today]';
|
||||||
} else {
|
} else {
|
||||||
return MOMENT_CALENDAR_DATE.sameDay;
|
return MOMENT_CALENDAR_DATE.sameDay;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let notificationsByDay = f.flow(
|
const notificationsByDay = f.flow(
|
||||||
f.map<DatedTimebox, DatedTimebox>(([date, nots]) => [
|
f.map<DatedTimebox, DatedTimebox>(([date, nots]) => [
|
||||||
date,
|
date,
|
||||||
nots.filter(filterNotification(associations, props.filter)),
|
nots.filter(filterNotification(associations, props.filter))
|
||||||
]),
|
]),
|
||||||
f.groupBy<DatedTimebox>(([d]) => {
|
f.groupBy<DatedTimebox>(([d]) => {
|
||||||
const date = moment(daToUnix(d));
|
const date = moment(daToUnix(d));
|
||||||
if (moment().subtract(6, 'hours').isBefore(date)) {
|
if (moment().subtract(6, 'hours').isBefore(date)) {
|
||||||
return 'latest';
|
return 'latest';
|
||||||
} else {
|
} else {
|
||||||
return date.format("YYYYMMDD");
|
return date.format('YYYYMMDD');
|
||||||
}
|
}
|
||||||
}),
|
})
|
||||||
)(notifications);
|
)(notifications);
|
||||||
|
|
||||||
const notificationsByDayMap = new Map<string, DatedTimebox[]>(
|
const notificationsByDayMap = new Map<string, DatedTimebox[]>(
|
||||||
Object.keys(notificationsByDay).map(timebox => {
|
Object.keys(notificationsByDay).map((timebox) => {
|
||||||
return [timebox, notificationsByDay[timebox]];
|
return [timebox, notificationsByDay[timebox]];
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -111,7 +117,6 @@ export default function Inbox(props: {
|
|||||||
loadMore
|
loadMore
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col ref={scrollRef} position="relative" height="100%" overflowY="auto">
|
<Col ref={scrollRef} position="relative" height="100%" overflowY="auto">
|
||||||
<Invites groups={props.groups} pendingJoin={props.pendingJoin} invites={invites} api={api} associations={associations} />
|
<Invites groups={props.groups} pendingJoin={props.pendingJoin} invites={invites} api={api} associations={associations} />
|
||||||
@ -123,7 +128,7 @@ export default function Inbox(props: {
|
|||||||
label={day === 'latest' ? 'Today' : moment(day).calendar(null, calendar)}
|
label={day === 'latest' ? 'Today' : moment(day).calendar(null, calendar)}
|
||||||
timeboxes={timeboxes}
|
timeboxes={timeboxes}
|
||||||
contacts={props.contacts}
|
contacts={props.contacts}
|
||||||
archive={!!props.showArchive}
|
archive={Boolean(props.showArchive)}
|
||||||
associations={props.associations}
|
associations={props.associations}
|
||||||
api={api}
|
api={api}
|
||||||
groups={props.groups}
|
groups={props.groups}
|
||||||
@ -167,9 +172,8 @@ function DaySection({
|
|||||||
associations,
|
associations,
|
||||||
api,
|
api,
|
||||||
groupConfig,
|
groupConfig,
|
||||||
graphConfig,
|
graphConfig
|
||||||
}) {
|
}) {
|
||||||
|
|
||||||
const lent = timeboxes.map(([,nots]) => nots.length).reduce(f.add, 0);
|
const lent = timeboxes.map(([,nots]) => nots.length).reduce(f.add, 0);
|
||||||
if (lent === 0 || timeboxes.length === 0) {
|
if (lent === 0 || timeboxes.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -1,15 +1,12 @@
|
|||||||
import React, { useCallback, useState } from "react";
|
import React, { ReactElement } from 'react';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { Box, Row, Col } from "@tlon/indigo-react";
|
|
||||||
import GlobalApi from "~/logic/api/global";
|
import { Col } from '@tlon/indigo-react';
|
||||||
import { Invites as IInvites, Associations, Invite, JoinRequests, Groups, Contacts, AppInvites, JoinProgress } from "~/types";
|
import { Invites as IInvites, Associations, Invite, JoinRequests, Groups, Contacts, AppInvites, JoinProgress } from '@urbit/api';
|
||||||
import { resourceAsPath, alphabeticalOrder } from "~/logic/lib/util";
|
|
||||||
import { useHistory } from "react-router-dom";
|
import GlobalApi from '~/logic/api/global';
|
||||||
import { useWaitForProps } from "~/logic/lib/useWaitForProps";
|
import { resourceAsPath, alphabeticalOrder } from '~/logic/lib/util';
|
||||||
import InviteItem from "~/views/components/Invite";
|
import InviteItem from '~/views/components/Invite';
|
||||||
import {JoiningStatus} from "./joining";
|
|
||||||
import {useModal} from "~/logic/lib/useModal";
|
|
||||||
import {JoinGroup} from "~/views/landscape/components/JoinGroup";
|
|
||||||
|
|
||||||
interface InvitesProps {
|
interface InvitesProps {
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
@ -26,42 +23,12 @@ interface InviteRef {
|
|||||||
invite: Invite;
|
invite: Invite;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Invites(props: InvitesProps) {
|
export function Invites(props: InvitesProps): ReactElement {
|
||||||
const { api, invites, pendingJoin } = props;
|
const { api, invites, pendingJoin } = props;
|
||||||
const [selected, setSelected] = useState<[string, string, Invite] | undefined>()
|
|
||||||
|
|
||||||
const acceptInvite = (
|
|
||||||
app: string,
|
|
||||||
uid: string,
|
|
||||||
invite: Invite
|
|
||||||
) => async () => {
|
|
||||||
setSelected([app, uid, invite]);
|
|
||||||
showModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
const declineInvite = useCallback(
|
|
||||||
(app: string, uid: string) => () => api.invite.decline(app, uid),
|
|
||||||
[api]
|
|
||||||
);
|
|
||||||
|
|
||||||
const { modal, showModal } = useModal({ modal: () => {
|
|
||||||
const [app, uid, invite] = selected!;
|
|
||||||
const autojoin = `~${invite.resource.ship}/${invite.resource.name}`;
|
|
||||||
return (
|
|
||||||
<JoinGroup
|
|
||||||
groups={props.groups}
|
|
||||||
associations={props.associations}
|
|
||||||
api={api}
|
|
||||||
autojoin={autojoin}
|
|
||||||
inviteUid={uid}
|
|
||||||
inviteApp={app}
|
|
||||||
/>
|
|
||||||
)}});
|
|
||||||
|
|
||||||
const inviteArr: InviteRef[] = _.reduce(invites, (acc: InviteRef[], val: AppInvites, app: string) => {
|
const inviteArr: InviteRef[] = _.reduce(invites, (acc: InviteRef[], val: AppInvites, app: string) => {
|
||||||
const appInvites = _.reduce(val, (invs: InviteRef[], invite: Invite, uid: string) => {
|
const appInvites = _.reduce(val, (invs: InviteRef[], invite: Invite, uid: string) => {
|
||||||
return [...invs, { invite, uid, app }];
|
return [...invs, { invite, uid, app }];
|
||||||
|
|
||||||
}, []);
|
}, []);
|
||||||
return [...acc, ...appInvites];
|
return [...acc, ...appInvites];
|
||||||
}, []);
|
}, []);
|
||||||
@ -69,8 +36,6 @@ export function Invites(props: InvitesProps) {
|
|||||||
const invitesAndStatus: { [rid: string]: JoinProgress | InviteRef } =
|
const invitesAndStatus: { [rid: string]: JoinProgress | InviteRef } =
|
||||||
{ ..._.keyBy(inviteArr, ({ invite }) => resourceAsPath(invite.resource)), ...props.pendingJoin };
|
{ ..._.keyBy(inviteArr, ({ invite }) => resourceAsPath(invite.resource)), ...props.pendingJoin };
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col
|
<Col
|
||||||
zIndex={4}
|
zIndex={4}
|
||||||
@ -83,7 +48,7 @@ export function Invites(props: InvitesProps) {
|
|||||||
{ Object
|
{ Object
|
||||||
.keys(invitesAndStatus)
|
.keys(invitesAndStatus)
|
||||||
.sort(alphabeticalOrder)
|
.sort(alphabeticalOrder)
|
||||||
.map(resource => {
|
.map((resource) => {
|
||||||
const inviteOrStatus = invitesAndStatus[resource];
|
const inviteOrStatus = invitesAndStatus[resource];
|
||||||
if(typeof inviteOrStatus === 'string') {
|
if(typeof inviteOrStatus === 'string') {
|
||||||
return (
|
return (
|
||||||
@ -94,9 +59,9 @@ export function Invites(props: InvitesProps) {
|
|||||||
associations={props.associations}
|
associations={props.associations}
|
||||||
resource={resource}
|
resource={resource}
|
||||||
pendingJoin={pendingJoin}
|
pendingJoin={pendingJoin}
|
||||||
api={api} />
|
api={api}
|
||||||
)
|
/>
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
const { app, uid, invite } = inviteOrStatus;
|
const { app, uid, invite } = inviteOrStatus;
|
||||||
console.log(inviteOrStatus);
|
console.log(inviteOrStatus);
|
||||||
@ -113,7 +78,7 @@ export function Invites(props: InvitesProps) {
|
|||||||
groups={props.groups}
|
groups={props.groups}
|
||||||
associations={props.associations}
|
associations={props.associations}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
</Col>
|
</Col>
|
||||||
|
@ -1,35 +1,32 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React from 'react';
|
||||||
import { Col, Row, Text, SegmentedProgressBar, Box } from "@tlon/indigo-react";
|
import { Row, Text, SegmentedProgressBar, Box } from '@tlon/indigo-react';
|
||||||
import GlobalApi from "~/logic/api/global";
|
|
||||||
import {
|
import {
|
||||||
JoinProgress,
|
JoinProgress,
|
||||||
joinProgress,
|
joinProgress,
|
||||||
MetadataUpdatePreview,
|
joinError
|
||||||
joinError,
|
} from '@urbit/api';
|
||||||
} from "~/types";
|
|
||||||
import { clamp } from "~/logic/lib/util";
|
|
||||||
|
|
||||||
interface JoiningStatusProps {
|
interface JoiningStatusProps {
|
||||||
status: JoinProgress;
|
status: JoinProgress;
|
||||||
}
|
}
|
||||||
|
|
||||||
const description: string[] = [
|
const description: string[] = [
|
||||||
"Attempting to contact host",
|
'Attempting to contact host',
|
||||||
"Retrieving data",
|
'Retrieving data',
|
||||||
"Finished join",
|
'Finished join',
|
||||||
"Unable to join, you do not have the correct permissions",
|
'Unable to join, you do not have the correct permissions',
|
||||||
"Internal error, please file an issue",
|
'Internal error, please file an issue'
|
||||||
];
|
];
|
||||||
|
|
||||||
export function JoiningStatus(props: JoiningStatusProps) {
|
export function JoiningStatus(props: JoiningStatusProps) {
|
||||||
const { status } = props;
|
const { status } = props;
|
||||||
|
|
||||||
const current = joinProgress.indexOf(status);
|
const current = joinProgress.indexOf(status);
|
||||||
const desc = description?.[current] || "";
|
const desc = description?.[current] || '';
|
||||||
const isError = joinError.indexOf(status as any) !== -1;
|
const isError = joinError.indexOf(status as any) !== -1;
|
||||||
return (
|
return (
|
||||||
<Row
|
<Row
|
||||||
display={["flex-column", "flex"]}
|
display={['flex-column', 'flex']}
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
px="4"
|
px="4"
|
||||||
gapX="4"
|
gapX="4"
|
||||||
@ -37,7 +34,7 @@ export function JoiningStatus(props: JoiningStatusProps) {
|
|||||||
<Box flexGrow={1} maxWidth="400px">
|
<Box flexGrow={1} maxWidth="400px">
|
||||||
<SegmentedProgressBar current={current + 1} segments={3} />
|
<SegmentedProgressBar current={current + 1} segments={3} />
|
||||||
</Box>
|
</Box>
|
||||||
<Text display="block" flexShrink={0} color={isError ? "red" : "gray"}>
|
<Text display="block" flexShrink={0} color={isError ? 'red' : 'gray'}>
|
||||||
{desc}
|
{desc}
|
||||||
</Text>
|
</Text>
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import React from "react";
|
import React from 'react';
|
||||||
import { Box } from "@tlon/indigo-react";
|
import { Box } from '@tlon/indigo-react';
|
||||||
|
|
||||||
import { MetadataBody, NotificationProps } from "./types";
|
import { MetadataBody, NotificationProps } from './types';
|
||||||
import { Header } from "./header";
|
import { Header } from './header';
|
||||||
|
|
||||||
function getInvolvedUsers(body: MetadataBody) {
|
function getInvolvedUsers(body: MetadataBody) {
|
||||||
return [];
|
return [];
|
||||||
@ -10,22 +10,22 @@ function getInvolvedUsers(body: MetadataBody) {
|
|||||||
|
|
||||||
function getDescription(body: MetadataBody) {
|
function getDescription(body: MetadataBody) {
|
||||||
const b = body.metadata;
|
const b = body.metadata;
|
||||||
if ("new" in b) {
|
if ('new' in b) {
|
||||||
return "created";
|
return 'created';
|
||||||
} else if ("changedTitle" in b) {
|
} else if ('changedTitle' in b) {
|
||||||
return "changed the title to";
|
return 'changed the title to';
|
||||||
} else if ("changedDescription" in b) {
|
} else if ('changedDescription' in b) {
|
||||||
return "changed the description to";
|
return 'changed the description to';
|
||||||
} else if ("changedColor" in b) {
|
} else if ('changedColor' in b) {
|
||||||
return "changed the color to";
|
return 'changed the color to';
|
||||||
} else if ("deleted" in b) {
|
} else if ('deleted' in b) {
|
||||||
return "deleted";
|
return 'deleted';
|
||||||
} else {
|
} else {
|
||||||
throw new Error("bad metadata frond");
|
throw new Error('bad metadata frond');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MetadataNotification(props: NotificationProps<"metadata">) {
|
export function MetadataNotification(props: NotificationProps<'metadata'>) {
|
||||||
const { unread } = props;
|
const { unread } = props;
|
||||||
const description = getDescription(unread.unreads[0].body);
|
const description = getDescription(unread.unreads[0].body);
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { ReactNode, useCallback, useMemo, useState } from "react";
|
import React, { ReactNode, useCallback, useMemo, useState } from 'react';
|
||||||
import { Row, Box } from "@tlon/indigo-react";
|
import { Row, Box } from '@tlon/indigo-react';
|
||||||
import _ from "lodash";
|
import _ from 'lodash';
|
||||||
import {
|
import {
|
||||||
GraphNotificationContents,
|
GraphNotificationContents,
|
||||||
IndexedNotification,
|
IndexedNotification,
|
||||||
@ -9,15 +9,15 @@ import {
|
|||||||
GroupNotificationsConfig,
|
GroupNotificationsConfig,
|
||||||
Groups,
|
Groups,
|
||||||
Associations,
|
Associations,
|
||||||
Contacts,
|
Contacts
|
||||||
} from "~/types";
|
} from '@urbit/api';
|
||||||
import GlobalApi from "~/logic/api/global";
|
import GlobalApi from '~/logic/api/global';
|
||||||
import { getParentIndex } from "~/logic/lib/notification";
|
import { getParentIndex } from '~/logic/lib/notification';
|
||||||
import { StatelessAsyncAction } from "~/views/components/StatelessAsyncAction";
|
import { StatelessAsyncAction } from '~/views/components/StatelessAsyncAction';
|
||||||
import { GroupNotification } from "./group";
|
import { GroupNotification } from './group';
|
||||||
import { GraphNotification } from "./graph";
|
import { GraphNotification } from './graph';
|
||||||
import { BigInteger } from "big-integer";
|
import { BigInteger } from 'big-integer';
|
||||||
import { useHovering } from "~/logic/lib/util";
|
import { useHovering } from '~/logic/lib/util';
|
||||||
|
|
||||||
interface NotificationProps {
|
interface NotificationProps {
|
||||||
notification: IndexedNotification;
|
notification: IndexedNotification;
|
||||||
@ -37,7 +37,7 @@ function getMuted(
|
|||||||
graphs: NotificationGraphConfig
|
graphs: NotificationGraphConfig
|
||||||
) {
|
) {
|
||||||
const { index, notification } = idxNotif;
|
const { index, notification } = idxNotif;
|
||||||
if ("graph" in idxNotif.index) {
|
if ('graph' in idxNotif.index) {
|
||||||
const { graph } = idxNotif.index.graph;
|
const { graph } = idxNotif.index.graph;
|
||||||
if(!('graph' in notification.contents)) {
|
if(!('graph' in notification.contents)) {
|
||||||
throw new Error();
|
throw new Error();
|
||||||
@ -46,11 +46,11 @@ function getMuted(
|
|||||||
|
|
||||||
return _.findIndex(
|
return _.findIndex(
|
||||||
graphs?.watching || [],
|
graphs?.watching || [],
|
||||||
(g) => g.graph === graph && g.index === parent
|
g => g.graph === graph && g.index === parent
|
||||||
) === -1;
|
) === -1;
|
||||||
}
|
}
|
||||||
if ("group" in index) {
|
if ('group' in index) {
|
||||||
return _.findIndex(groups || [], (g) => g === index.group.group) === -1;
|
return _.findIndex(groups || [], g => g === index.group.group) === -1;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -77,13 +77,13 @@ function NotificationWrapper(props: {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const onChangeMute = useCallback(async () => {
|
const onChangeMute = useCallback(async () => {
|
||||||
const func = isMuted ? "unmute" : "mute";
|
const func = isMuted ? 'unmute' : 'mute';
|
||||||
return api.hark[func](notif);
|
return api.hark[func](notif);
|
||||||
}, [notif, api, isMuted]);
|
}, [notif, api, isMuted]);
|
||||||
|
|
||||||
const { hovering, bind } = useHovering();
|
const { hovering, bind } = useHovering();
|
||||||
|
|
||||||
const changeMuteDesc = isMuted ? "Unmute" : "Mute";
|
const changeMuteDesc = isMuted ? 'Unmute' : 'Mute';
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
width="100%"
|
width="100%"
|
||||||
@ -126,7 +126,7 @@ export function Notification(props: NotificationProps) {
|
|||||||
</NotificationWrapper>
|
</NotificationWrapper>
|
||||||
);
|
);
|
||||||
|
|
||||||
if ("graph" in notification.index) {
|
if ('graph' in notification.index) {
|
||||||
const index = notification.index.graph;
|
const index = notification.index.graph;
|
||||||
const c: GraphNotificationContents = (contents as any).graph;
|
const c: GraphNotificationContents = (contents as any).graph;
|
||||||
|
|
||||||
@ -147,7 +147,7 @@ export function Notification(props: NotificationProps) {
|
|||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if ("group" in notification.index) {
|
if ('group' in notification.index) {
|
||||||
const index = notification.index.group;
|
const index = notification.index.group;
|
||||||
const c: GroupNotificationContents = (contents as any).group;
|
const c: GroupNotificationContents = (contents as any).group;
|
||||||
return (
|
return (
|
||||||
|
@ -1,25 +1,25 @@
|
|||||||
import React, { useCallback, useState, useRef } from "react";
|
import React, { useCallback, useState, useRef, ReactElement } from 'react';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { Box, Col, Text, Row } from "@tlon/indigo-react";
|
import { Link, Switch, Route } from 'react-router-dom';
|
||||||
import { Link, Switch, Route } from "react-router-dom";
|
import Helmet from 'react-helmet';
|
||||||
import Helmet from "react-helmet";
|
|
||||||
|
|
||||||
import { Body } from "~/views/components/Body";
|
import { Box, Col, Text, Row } from '@tlon/indigo-react';
|
||||||
import { PropFunc } from "~/types/util";
|
|
||||||
import Inbox from "./inbox";
|
|
||||||
import NotificationPreferences from "./preferences";
|
|
||||||
import { Dropdown } from "~/views/components/Dropdown";
|
|
||||||
import { Formik } from "formik";
|
|
||||||
import { FormikOnBlur } from "~/views/components/FormikOnBlur";
|
|
||||||
import GroupSearch from "~/views/components/GroupSearch";
|
|
||||||
import {useTutorialModal} from "~/views/components/useTutorialModal";
|
|
||||||
|
|
||||||
const baseUrl = "/~notifications";
|
import { Body } from '~/views/components/Body';
|
||||||
|
import { PropFunc } from '~/types/util';
|
||||||
|
import Inbox from './inbox';
|
||||||
|
import NotificationPreferences from './preferences';
|
||||||
|
import { Dropdown } from '~/views/components/Dropdown';
|
||||||
|
import { FormikOnBlur } from '~/views/components/FormikOnBlur';
|
||||||
|
import GroupSearch from '~/views/components/GroupSearch';
|
||||||
|
import { useTutorialModal } from '~/views/components/useTutorialModal';
|
||||||
|
|
||||||
|
const baseUrl = '/~notifications';
|
||||||
|
|
||||||
const HeaderLink = React.forwardRef((
|
const HeaderLink = React.forwardRef((
|
||||||
props: PropFunc<typeof Text> & { view?: string; current: string },
|
props: PropFunc<typeof Text> & { view?: string; current: string },
|
||||||
ref
|
ref
|
||||||
) => {
|
): ReactElement => {
|
||||||
const { current, view, ...textProps } = props;
|
const { current, view, ...textProps } = props;
|
||||||
const to = view ? `${baseUrl}/${view}` : baseUrl;
|
const to = view ? `${baseUrl}/${view}` : baseUrl;
|
||||||
const active = view ? current === view : !current;
|
const active = view ? current === view : !current;
|
||||||
@ -35,7 +35,7 @@ interface NotificationFilter {
|
|||||||
groups: string[];
|
groups: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function NotificationsScreen(props: any) {
|
export default function NotificationsScreen(props: any): ReactElement {
|
||||||
const relativePath = (p: string) => baseUrl + p;
|
const relativePath = (p: string) => baseUrl + p;
|
||||||
|
|
||||||
const [filter, setFilter] = useState<NotificationFilter>({ groups: [] });
|
const [filter, setFilter] = useState<NotificationFilter>({ groups: [] });
|
||||||
@ -43,20 +43,20 @@ export default function NotificationsScreen(props: any) {
|
|||||||
setFilter({ groups });
|
setFilter({ groups });
|
||||||
};
|
};
|
||||||
const onReadAll = useCallback(() => {
|
const onReadAll = useCallback(() => {
|
||||||
props.api.hark.readAll()
|
props.api.hark.readAll();
|
||||||
}, []);
|
}, []);
|
||||||
const groupFilterDesc =
|
const groupFilterDesc =
|
||||||
filter.groups.length === 0
|
filter.groups.length === 0
|
||||||
? "All"
|
? 'All'
|
||||||
: filter.groups
|
: filter.groups
|
||||||
.map((g) => props.associations?.groups?.[g]?.metadata?.title)
|
.map(g => props.associations?.groups?.[g]?.metadata?.title)
|
||||||
.join(", ");
|
.join(', ');
|
||||||
const anchorRef = useRef<HTMLElement | null>(null);
|
const anchorRef = useRef<HTMLElement | null>(null);
|
||||||
useTutorialModal('notifications', true, anchorRef);
|
useTutorialModal('notifications', true, anchorRef);
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route
|
<Route
|
||||||
path={[relativePath("/:view"), relativePath("")]}
|
path={[relativePath('/:view'), relativePath('')]}
|
||||||
render={(routeProps) => {
|
render={(routeProps) => {
|
||||||
const { view } = routeProps.match.params;
|
const { view } = routeProps.match.params;
|
||||||
return (
|
return (
|
||||||
@ -89,7 +89,8 @@ export default function NotificationsScreen(props: any) {
|
|||||||
</Box>
|
</Box>
|
||||||
</Row>
|
</Row>
|
||||||
<Row
|
<Row
|
||||||
justifyContent="space-between">
|
justifyContent="space-between"
|
||||||
|
>
|
||||||
<Box
|
<Box
|
||||||
mr="1"
|
mr="1"
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
@ -136,7 +137,7 @@ export default function NotificationsScreen(props: any) {
|
|||||||
</Dropdown>
|
</Dropdown>
|
||||||
</Row>
|
</Row>
|
||||||
</Row>
|
</Row>
|
||||||
{view === "preferences" && (
|
{view === 'preferences' && (
|
||||||
<NotificationPreferences
|
<NotificationPreferences
|
||||||
graphConfig={props.notificationsGraphConfig}
|
graphConfig={props.notificationsGraphConfig}
|
||||||
api={props.api}
|
api={props.api}
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
import React, { useCallback } from "react";
|
import React, { ReactElement, useCallback } from 'react';
|
||||||
|
import { Form, FormikHelpers } from 'formik';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
import { Box, Col, ManagedCheckboxField as Checkbox } from "@tlon/indigo-react";
|
import { Col, ManagedCheckboxField as Checkbox } from '@tlon/indigo-react';
|
||||||
import { Formik, Form, FormikHelpers } from "formik";
|
import { NotificationGraphConfig } from '@urbit/api';
|
||||||
import * as Yup from "yup";
|
|
||||||
import _ from "lodash";
|
import { FormikOnBlur } from '~/views/components/FormikOnBlur';
|
||||||
import { AsyncButton } from "~/views/components/AsyncButton";
|
import GlobalApi from '~/logic/api/global';
|
||||||
import { FormikOnBlur } from "~/views/components/FormikOnBlur";
|
|
||||||
import { NotificationGraphConfig } from "~/types";
|
|
||||||
import GlobalApi from "~/logic/api/global";
|
|
||||||
|
|
||||||
interface FormSchema {
|
interface FormSchema {
|
||||||
mentions: boolean;
|
mentions: boolean;
|
||||||
@ -24,21 +23,21 @@ interface NotificationPreferencesProps {
|
|||||||
|
|
||||||
export default function NotificationPreferences(
|
export default function NotificationPreferences(
|
||||||
props: NotificationPreferencesProps
|
props: NotificationPreferencesProps
|
||||||
) {
|
): ReactElement {
|
||||||
const { graphConfig, api, dnd } = props;
|
const { graphConfig, api, dnd } = props;
|
||||||
|
|
||||||
const initialValues: FormSchema = {
|
const initialValues: FormSchema = {
|
||||||
mentions: graphConfig.mentions,
|
mentions: graphConfig.mentions,
|
||||||
watchOnSelf: graphConfig.watchOnSelf,
|
watchOnSelf: graphConfig.watchOnSelf,
|
||||||
dnd,
|
dnd,
|
||||||
watching: graphConfig.watching,
|
watching: graphConfig.watching
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(
|
||||||
async (values: FormSchema, actions: FormikHelpers<FormSchema>) => {
|
async (values: FormSchema, actions: FormikHelpers<FormSchema>) => {
|
||||||
console.log(values);
|
console.log(values);
|
||||||
try {
|
try {
|
||||||
let promises: Promise<any>[] = [];
|
const promises: Promise<any>[] = [];
|
||||||
if (values.mentions !== graphConfig.mentions) {
|
if (values.mentions !== graphConfig.mentions) {
|
||||||
promises.push(api.hark.setMentions(values.mentions));
|
promises.push(api.hark.setMentions(values.mentions));
|
||||||
}
|
}
|
||||||
@ -46,7 +45,7 @@ export default function NotificationPreferences(
|
|||||||
promises.push(api.hark.setWatchOnSelf(values.watchOnSelf));
|
promises.push(api.hark.setWatchOnSelf(values.watchOnSelf));
|
||||||
}
|
}
|
||||||
if (values.dnd !== dnd && !_.isUndefined(values.dnd)) {
|
if (values.dnd !== dnd && !_.isUndefined(values.dnd)) {
|
||||||
promises.push(api.hark.setDoNotDisturb(values.dnd))
|
promises.push(api.hark.setDoNotDisturb(values.dnd));
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
|
@ -1,30 +1,25 @@
|
|||||||
import React from "react";
|
import React, { ReactElement } from 'react';
|
||||||
import * as Yup from "yup";
|
import * as Yup from 'yup';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import { Formik } from 'formik';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ManagedForm as Form,
|
ManagedForm as Form,
|
||||||
ManagedTextInputField as Input,
|
ManagedTextInputField as Input,
|
||||||
ManagedCheckboxField as Checkbox,
|
ManagedCheckboxField as Checkbox,
|
||||||
Center,
|
|
||||||
Col,
|
Col,
|
||||||
Box,
|
|
||||||
Text,
|
Text,
|
||||||
Row,
|
Row,
|
||||||
Button,
|
} from '@tlon/indigo-react';
|
||||||
} from "@tlon/indigo-react";
|
|
||||||
import { Formik, FormikHelpers } from "formik";
|
|
||||||
import { useHistory } from "react-router-dom";
|
|
||||||
|
|
||||||
import { uxToHex } from "~/logic/lib/util";
|
|
||||||
import { Sigil } from "~/logic/lib/sigil";
|
|
||||||
import { AsyncButton } from "~/views/components/AsyncButton";
|
|
||||||
import { ColorInput } from "~/views/components/ColorInput";
|
|
||||||
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 { uxToHex } from '~/logic/lib/util';
|
||||||
|
import { AsyncButton } from '~/views/components/AsyncButton';
|
||||||
|
import { ColorInput } from '~/views/components/ColorInput';
|
||||||
|
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';
|
||||||
|
|
||||||
const formSchema = Yup.object({
|
const formSchema = Yup.object({
|
||||||
nickname: Yup.string(),
|
nickname: Yup.string(),
|
||||||
@ -45,8 +40,7 @@ const emptyContact = {
|
|||||||
isPublic: false
|
isPublic: false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function EditProfile(props: any): ReactElement {
|
||||||
export function EditProfile(props: any) {
|
|
||||||
const { contact, ship, api, isPublic } = props;
|
const { contact, ship, api, isPublic } = props;
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
if (contact) {
|
if (contact) {
|
||||||
@ -58,10 +52,10 @@ export function EditProfile(props: any) {
|
|||||||
try {
|
try {
|
||||||
await Object.keys(values).reduce((acc, key) => {
|
await Object.keys(values).reduce((acc, key) => {
|
||||||
console.log(key);
|
console.log(key);
|
||||||
const newValue = key !== "color" ? values[key] : uxToHex(values[key]);
|
const newValue = key !== 'color' ? values[key] : uxToHex(values[key]);
|
||||||
|
|
||||||
if (newValue !== contact[key]) {
|
if (newValue !== contact[key]) {
|
||||||
if (key === "isPublic") {
|
if (key === 'isPublic') {
|
||||||
return acc.then(() =>
|
return acc.then(() =>
|
||||||
api.contacts.setPublic(newValue)
|
api.contacts.setPublic(newValue)
|
||||||
);
|
);
|
||||||
@ -70,7 +64,7 @@ export function EditProfile(props: any) {
|
|||||||
console.log(toRemove);
|
console.log(toRemove);
|
||||||
const toAdd: string[] = _.difference(newValue, contact?.groups || []);
|
const toAdd: string[] = _.difference(newValue, contact?.groups || []);
|
||||||
console.log(toAdd);
|
console.log(toAdd);
|
||||||
let promises: Promise<any>[] = [];
|
const promises: Promise<any>[] = [];
|
||||||
|
|
||||||
promises.concat(
|
promises.concat(
|
||||||
toRemove.map(e =>
|
toRemove.map(e =>
|
||||||
@ -83,10 +77,9 @@ export function EditProfile(props: any) {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
return acc.then(() => Promise.all(promises));
|
return acc.then(() => Promise.all(promises));
|
||||||
|
|
||||||
} else if (
|
} else if (
|
||||||
key !== "last-updated" &&
|
key !== 'last-updated' &&
|
||||||
key !== "isPublic"
|
key !== 'isPublic'
|
||||||
) {
|
) {
|
||||||
return acc.then(() =>
|
return acc.then(() =>
|
||||||
api.contacts.edit(ship, { [key]: newValue })
|
api.contacts.edit(ship, { [key]: newValue })
|
||||||
|
@ -1,26 +1,24 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { ReactElement, useEffect, useRef, useState } from 'react';
|
||||||
import { Sigil } from "~/logic/lib/sigil";
|
import { useHistory } from 'react-router-dom';
|
||||||
import { ViewProfile } from './ViewProfile';
|
|
||||||
import { EditProfile } from './EditProfile';
|
|
||||||
import { SetStatusBarModal } from '~/views/components/SetStatusBarModal';
|
|
||||||
|
|
||||||
import { uxToHex } from "~/logic/lib/util";
|
|
||||||
import {
|
import {
|
||||||
Center,
|
Center,
|
||||||
Box,
|
Box,
|
||||||
Row,
|
Row,
|
||||||
BaseImage,
|
BaseImage,
|
||||||
StatelessTextInput as Input,
|
|
||||||
Button,
|
|
||||||
Text
|
Text
|
||||||
} from "@tlon/indigo-react";
|
} from "@tlon/indigo-react";
|
||||||
|
|
||||||
import RichText from '~/views/components/RichText'
|
import RichText from '~/views/components/RichText'
|
||||||
import useLocalState from "~/logic/state/local";
|
import useLocalState from "~/logic/state/local";
|
||||||
import { useHistory } from "react-router-dom";
|
import { Sigil } from '~/logic/lib/sigil';
|
||||||
import {useTutorialModal} from "~/views/components/useTutorialModal";
|
import { ViewProfile } from './ViewProfile';
|
||||||
|
import { EditProfile } from './EditProfile';
|
||||||
|
import { SetStatusBarModal } from '~/views/components/SetStatusBarModal';
|
||||||
|
import { uxToHex } from '~/logic/lib/util';
|
||||||
|
import { useTutorialModal } from '~/views/components/useTutorialModal';
|
||||||
|
|
||||||
|
export function Profile(props: any): ReactElement {
|
||||||
export function Profile(props: any) {
|
|
||||||
const { hideAvatars } = useLocalState(({ hideAvatars }) => ({
|
const { hideAvatars } = useLocalState(({ hideAvatars }) => ({
|
||||||
hideAvatars
|
hideAvatars
|
||||||
}));
|
}));
|
||||||
@ -32,17 +30,13 @@ export function Profile(props: any) {
|
|||||||
const { contact, nackedContacts, hasLoaded, isPublic, isEdit, ship } = props;
|
const { contact, nackedContacts, hasLoaded, isPublic, isEdit, ship } = props;
|
||||||
const nacked = nackedContacts.has(ship);
|
const nacked = nackedContacts.has(ship);
|
||||||
|
|
||||||
const [statusModal, showStatusModal] = useState(false);
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(hasLoaded && !contact && !nacked) {
|
if(hasLoaded && !contact && !nacked) {
|
||||||
props.api.contacts.retrieve(ship);
|
props.api.contacts.retrieve(ship);
|
||||||
}
|
}
|
||||||
}, [hasLoaded, contact])
|
}, [hasLoaded, contact]);
|
||||||
|
|
||||||
|
const hexColor = contact?.color ? `#${uxToHex(contact.color)}` : '#000000';
|
||||||
const hexColor = contact?.color ? `#${uxToHex(contact.color)}` : "#000000";
|
|
||||||
const cover = (contact?.cover)
|
const cover = (contact?.cover)
|
||||||
? <BaseImage src={contact.cover} width='100%' height='100%' style={{ objectFit: 'cover' }} />
|
? <BaseImage src={contact.cover} width='100%' height='100%' style={{ objectFit: 'cover' }} />
|
||||||
: <Box display="block" width='100%' height='100%' backgroundColor='washedGray' />;
|
: <Box display="block" width='100%' height='100%' backgroundColor='washedGray' />;
|
||||||
@ -59,12 +53,14 @@ export function Profile(props: any) {
|
|||||||
<Center
|
<Center
|
||||||
p={[0,4]}
|
p={[0,4]}
|
||||||
height="100%"
|
height="100%"
|
||||||
width="100%">
|
width="100%"
|
||||||
|
>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
ref={anchorRef}
|
ref={anchorRef}
|
||||||
maxWidth="600px"
|
maxWidth="600px"
|
||||||
width="100%">
|
width="100%"
|
||||||
|
>
|
||||||
<Row alignItems="center" justifyContent="space-between">
|
<Row alignItems="center" justifyContent="space-between">
|
||||||
<Row>
|
<Row>
|
||||||
{ship === `~${window.ship}` ? (
|
{ship === `~${window.ship}` ? (
|
||||||
@ -72,7 +68,10 @@ export function Profile(props: any) {
|
|||||||
<Text
|
<Text
|
||||||
py='2'
|
py='2'
|
||||||
cursor='pointer'
|
cursor='pointer'
|
||||||
onClick={() => { history.push(`/~profile/${ship}/edit`) }}>
|
onClick={() => {
|
||||||
|
history.push(`/~profile/${ship}/edit`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
Edit Profile
|
Edit Profile
|
||||||
</Text>
|
</Text>
|
||||||
<SetStatusBarModal
|
<SetStatusBarModal
|
||||||
@ -111,7 +110,8 @@ export function Profile(props: any) {
|
|||||||
api={props.api}
|
api={props.api}
|
||||||
groups={props.groups}
|
groups={props.groups}
|
||||||
associations={props.associations}
|
associations={props.associations}
|
||||||
isPublic={isPublic}/>
|
isPublic={isPublic}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ViewProfile
|
<ViewProfile
|
||||||
api={props.api}
|
api={props.api}
|
||||||
|
@ -3,14 +3,13 @@ import React, {
|
|||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
ChangeEvent
|
ChangeEvent
|
||||||
} from "react";
|
} from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Row,
|
Row,
|
||||||
Button,
|
Button,
|
||||||
StatelessTextInput as Input,
|
StatelessTextInput as Input
|
||||||
} from "@tlon/indigo-react";
|
} from '@tlon/indigo-react';
|
||||||
|
|
||||||
|
|
||||||
export function SetStatus(props: any) {
|
export function SetStatus(props: any) {
|
||||||
const { contact, ship, api, callback } = props;
|
const { contact, ship, api, callback } = props;
|
||||||
@ -23,7 +22,7 @@ export function SetStatus(props: any) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setStatus(!!contact ? contact.status : '');
|
setStatus(contact ? contact.status : '');
|
||||||
}, [contact]);
|
}, [contact]);
|
||||||
|
|
||||||
const editStatus = () => {
|
const editStatus = () => {
|
||||||
@ -53,7 +52,8 @@ export function SetStatus(props: any) {
|
|||||||
color="white"
|
color="white"
|
||||||
ml={2}
|
ml={2}
|
||||||
width="25%"
|
width="25%"
|
||||||
onClick={editStatus}>
|
onClick={editStatus}
|
||||||
|
>
|
||||||
Set Status
|
Set Status
|
||||||
</Button>
|
</Button>
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -1,28 +1,21 @@
|
|||||||
import React, {useEffect, useState} from "react";
|
import React from 'react';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { Sigil } from "~/logic/lib/sigil";
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Center,
|
Center,
|
||||||
Box,
|
Box,
|
||||||
Text,
|
Text,
|
||||||
Row,
|
Row,
|
||||||
Button,
|
|
||||||
Col,
|
Col,
|
||||||
LoadingSpinner
|
} from '@tlon/indigo-react';
|
||||||
} from "@tlon/indigo-react";
|
|
||||||
import { AsyncButton } from "~/views/components/AsyncButton";
|
|
||||||
import RichText from "~/views/components/RichText";
|
|
||||||
import { useHistory } from "react-router-dom";
|
|
||||||
import {GroupSummary} from "~/views/landscape/components/GroupSummary";
|
|
||||||
import {MetadataUpdatePreview} from "~/types";
|
|
||||||
import {GroupLink} from "~/views/components/GroupLink";
|
|
||||||
import {lengthOrder} from "~/logic/lib/util";
|
|
||||||
import useLocalState from "~/logic/state/local";
|
|
||||||
|
|
||||||
|
import RichText from '~/views/components/RichText';
|
||||||
|
import { GroupLink } from '~/views/components/GroupLink';
|
||||||
|
import { lengthOrder } from '~/logic/lib/util';
|
||||||
|
import useLocalState from '~/logic/state/local';
|
||||||
|
|
||||||
export function ViewProfile(props: any) {
|
export function ViewProfile(props: any): ReactElement {
|
||||||
const history = useHistory();
|
|
||||||
const { hideNicknames } = useLocalState(({ hideNicknames }) => ({
|
const { hideNicknames } = useLocalState(({ hideNicknames }) => ({
|
||||||
hideNicknames
|
hideNicknames
|
||||||
}));
|
}));
|
||||||
@ -33,17 +26,19 @@ export function ViewProfile(props: any) {
|
|||||||
<Row
|
<Row
|
||||||
pb={2}
|
pb={2}
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
width="100%">
|
width="100%"
|
||||||
|
>
|
||||||
<Center width="100%">
|
<Center width="100%">
|
||||||
<Text>
|
<Text>
|
||||||
{((!hideNicknames && contact?.nickname) ? contact.nickname : "")}
|
{((!hideNicknames && contact?.nickname) ? contact.nickname : '')}
|
||||||
</Text>
|
</Text>
|
||||||
</Center>
|
</Center>
|
||||||
</Row>
|
</Row>
|
||||||
<Row
|
<Row
|
||||||
pb={2}
|
pb={2}
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
width="100%">
|
width="100%"
|
||||||
|
>
|
||||||
<Center width="100%">
|
<Center width="100%">
|
||||||
<Text mono color="darkGray">{ship}</Text>
|
<Text mono color="darkGray">{ship}</Text>
|
||||||
</Center>
|
</Center>
|
||||||
@ -52,10 +47,11 @@ export function ViewProfile(props: any) {
|
|||||||
pb={2}
|
pb={2}
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
width="100%">
|
width="100%"
|
||||||
|
>
|
||||||
<Center flexDirection="column" maxWidth='32rem'>
|
<Center flexDirection="column" maxWidth='32rem'>
|
||||||
<RichText width='100%' disableRemoteContent>
|
<RichText width='100%' disableRemoteContent>
|
||||||
{(contact?.bio ? contact.bio : "")}
|
{(contact?.bio ? contact.bio : '')}
|
||||||
</RichText>
|
</RichText>
|
||||||
</Center>
|
</Center>
|
||||||
</Col>
|
</Col>
|
||||||
@ -82,7 +78,8 @@ export function ViewProfile(props: any) {
|
|||||||
borderRadius={1}
|
borderRadius={1}
|
||||||
bg="white"
|
bg="white"
|
||||||
border={1}
|
border={1}
|
||||||
borderColor="washedGray">
|
borderColor="washedGray"
|
||||||
|
>
|
||||||
<Center height="100%">
|
<Center height="100%">
|
||||||
<Text mono pr={1} color="gray">{ship}</Text>
|
<Text mono pr={1} color="gray">{ship}</Text>
|
||||||
<Text color="gray">remains private</Text>
|
<Text color="gray">remains private</Text>
|
||||||
|
@ -1,34 +1,24 @@
|
|||||||
import React from "react";
|
import React from 'react';
|
||||||
import { Route, Link } from "react-router-dom";
|
import { Route, Link } from 'react-router-dom';
|
||||||
import Helmet from 'react-helmet';
|
import Helmet from 'react-helmet';
|
||||||
|
|
||||||
import { Box, Text, Row, Col, Icon, BaseImage } from "@tlon/indigo-react";
|
import { Box } from '@tlon/indigo-react';
|
||||||
|
|
||||||
import { uxToHex } from "~/logic/lib/util";
|
import { Profile } from './components/Profile';
|
||||||
|
|
||||||
import { Profile } from "./components/Profile";
|
|
||||||
import useLocalState from "~/logic/state/local";
|
|
||||||
|
|
||||||
export default function ProfileScreen(props: any) {
|
export default function ProfileScreen(props: any) {
|
||||||
const { dark } = props;
|
|
||||||
const hideAvatars = useLocalState(state => state.hideAvatars);
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Helmet defer={false}>
|
<Helmet defer={false}>
|
||||||
<title>{ props.notificationsCount ? `(${String(props.notificationsCount) }) `: '' }Landscape - Profile</title>
|
<title>{ props.notificationsCount ? `(${String(props.notificationsCount) }) `: '' }Landscape - Profile</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<Route
|
<Route
|
||||||
path={"/~profile/:ship/:edit?"}
|
path={'/~profile/:ship/:edit?'}
|
||||||
render={({ match, history }) => {
|
render={({ match }) => {
|
||||||
const ship = match.params.ship;
|
const ship = match.params.ship;
|
||||||
const isEdit = match.url.includes('edit');
|
const isEdit = match.url.includes('edit');
|
||||||
const isPublic = props.isContactPublic;
|
const isPublic = props.isContactPublic;
|
||||||
const contact = props.contacts?.[ship];
|
const contact = props.contacts?.[ship];
|
||||||
const sigilColor = contact?.color
|
|
||||||
? `#${uxToHex(contact.color)}`
|
|
||||||
: dark
|
|
||||||
? "#FFFFFF"
|
|
||||||
: "#000000";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box height="100%" px={[0, 3]} pb={[0, 3]} borderRadius={1}>
|
<Box height="100%" px={[0, 3]} pb={[0, 3]} borderRadius={1}>
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import React from "react";
|
import React from 'react';
|
||||||
import { Box } from "@tlon/indigo-react";
|
import { Box } from '@tlon/indigo-react';
|
||||||
|
|
||||||
import GlobalApi from "~/logic/api/global";
|
import GlobalApi from '~/logic/api/global';
|
||||||
import { StoreState } from "~/logic/store/type";
|
import { StoreState } from '~/logic/store/type';
|
||||||
import { Association } from "~/types";
|
import { Association } from '@urbit/api';
|
||||||
import { RouteComponentProps } from "react-router-dom";
|
import { RouteComponentProps } from 'react-router-dom';
|
||||||
import { NotebookRoutes } from "./components/NotebookRoutes";
|
import { NotebookRoutes } from './components/NotebookRoutes';
|
||||||
|
|
||||||
type PublishResourceProps = StoreState & {
|
type PublishResourceProps = StoreState & {
|
||||||
association: Association;
|
association: Association;
|
||||||
@ -16,7 +16,7 @@ type PublishResourceProps = StoreState & {
|
|||||||
export function PublishResource(props: PublishResourceProps) {
|
export function PublishResource(props: PublishResourceProps) {
|
||||||
const { association, api, baseUrl, notebooks } = props;
|
const { association, api, baseUrl, notebooks } = props;
|
||||||
const rid = association.resource;
|
const rid = association.resource;
|
||||||
const [, , ship, book] = rid.split("/");
|
const [, , ship, book] = rid.split('/');
|
||||||
const notebookContacts = props.contacts[association.group];
|
const notebookContacts = props.contacts[association.group];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
import React from "react";
|
import React, { ReactElement } from 'react';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { PostFormSchema, PostForm } from "./NoteForm";
|
import { FormikHelpers } from 'formik';
|
||||||
import { FormikHelpers } from "formik";
|
import { RouteComponentProps, useLocation } from 'react-router-dom';
|
||||||
import GlobalApi from "~/logic/api/global";
|
|
||||||
import { RouteComponentProps, useLocation } from "react-router-dom";
|
import { GraphNode } from '@urbit/api';
|
||||||
import { GraphNode, TextContent, Association, S3State } from "~/types";
|
|
||||||
import { getLatestRevision, editPost } from "~/logic/lib/publish";
|
import { PostFormSchema, PostForm } from './NoteForm';
|
||||||
import {useWaitForProps} from "~/logic/lib/useWaitForProps";
|
import GlobalApi from '~/logic/api/global';
|
||||||
|
import { getLatestRevision, editPost } from '~/logic/lib/publish';
|
||||||
|
import { useWaitForProps } from '~/logic/lib/useWaitForProps';
|
||||||
|
import { S3State } from '~/types';
|
||||||
|
|
||||||
interface EditPostProps {
|
interface EditPostProps {
|
||||||
ship: string;
|
ship: string;
|
||||||
noteId: number;
|
noteId: number;
|
||||||
@ -16,7 +20,7 @@ interface EditPostProps {
|
|||||||
s3: S3State;
|
s3: S3State;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EditPost(props: EditPostProps & RouteComponentProps) {
|
export function EditPost(props: EditPostProps & RouteComponentProps): ReactElement {
|
||||||
const { note, book, noteId, api, ship, history, s3 } = props;
|
const { note, book, noteId, api, ship, history, s3 } = props;
|
||||||
const [revNum, title, body] = getLatestRevision(note);
|
const [revNum, title, body] = getLatestRevision(note);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@ -24,19 +28,19 @@ export function EditPost(props: EditPostProps & RouteComponentProps) {
|
|||||||
const waiter = useWaitForProps(props);
|
const waiter = useWaitForProps(props);
|
||||||
const initial: PostFormSchema = {
|
const initial: PostFormSchema = {
|
||||||
title,
|
title,
|
||||||
body,
|
body
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = async (
|
const onSubmit = async (
|
||||||
values: PostFormSchema,
|
values: PostFormSchema,
|
||||||
actions: FormikHelpers<PostFormSchema>
|
actions: FormikHelpers<PostFormSchema>
|
||||||
) => {
|
): Promise<void> => {
|
||||||
const { title, body } = values;
|
const { title, body } = values;
|
||||||
try {
|
try {
|
||||||
const newRev = revNum + 1;
|
const newRev = revNum + 1;
|
||||||
const nodes = editPost(newRev, noteId, title, body);
|
const nodes = editPost(newRev, noteId, title, body);
|
||||||
await api.graph.addNodes(ship, book, nodes);
|
await api.graph.addNodes(ship, book, nodes);
|
||||||
await waiter(p => {
|
await waiter((p) => {
|
||||||
const [rev] = getLatestRevision(p.note);
|
const [rev] = getLatestRevision(p.note);
|
||||||
return rev === newRev;
|
return rev === newRev;
|
||||||
});
|
});
|
||||||
@ -44,7 +48,7 @@ export function EditPost(props: EditPostProps & RouteComponentProps) {
|
|||||||
history.push(noteUrl);
|
history.push(noteUrl);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
actions.setStatus({ error: "Failed to edit notebook" });
|
actions.setStatus({ error: 'Failed to edit notebook' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,26 +1,26 @@
|
|||||||
import React, { createRef, useCallback, useRef } from "react";
|
import React, { createRef, useCallback, useRef } from 'react';
|
||||||
import { IUnControlledCodeMirror, UnControlled as CodeEditor } from "react-codemirror2";
|
import { IUnControlledCodeMirror, UnControlled as CodeEditor } from 'react-codemirror2';
|
||||||
import { useFormikContext } from 'formik';
|
import { useFormikContext } from 'formik';
|
||||||
import { Prompt } from 'react-router-dom';
|
import { Prompt } from 'react-router-dom';
|
||||||
import { Editor } from 'codemirror';
|
import { Editor } from 'codemirror';
|
||||||
|
|
||||||
import { MOBILE_BROWSER_REGEX, usePreventWindowUnload } from "~/logic/lib/util";
|
import { MOBILE_BROWSER_REGEX, usePreventWindowUnload } from '~/logic/lib/util';
|
||||||
import { PropFunc } from "~/types/util";
|
import { PropFunc } from '~/types/util';
|
||||||
import CodeMirror from "codemirror";
|
import CodeMirror from 'codemirror';
|
||||||
|
|
||||||
import "codemirror/mode/markdown/markdown";
|
import 'codemirror/mode/markdown/markdown';
|
||||||
import "codemirror/addon/display/placeholder";
|
import 'codemirror/addon/display/placeholder';
|
||||||
import "codemirror/addon/edit/continuelist";
|
import 'codemirror/addon/edit/continuelist';
|
||||||
|
|
||||||
import "codemirror/lib/codemirror.css";
|
import 'codemirror/lib/codemirror.css';
|
||||||
import { Box } from "@tlon/indigo-react";
|
import { Box } from '@tlon/indigo-react';
|
||||||
import { useFileDrag } from "~/logic/lib/useDrag";
|
import { useFileDrag } from '~/logic/lib/useDrag';
|
||||||
import SubmitDragger from "~/views/components/SubmitDragger";
|
import SubmitDragger from '~/views/components/SubmitDragger';
|
||||||
import useS3 from "~/logic/lib/useS3";
|
import useS3 from '~/logic/lib/useS3';
|
||||||
import { S3State } from "~/types";
|
import { S3State } from '@urbit/api';
|
||||||
|
|
||||||
const MARKDOWN_CONFIG = {
|
const MARKDOWN_CONFIG = {
|
||||||
name: "markdown",
|
name: 'markdown'
|
||||||
};
|
};
|
||||||
|
|
||||||
interface MarkdownEditorProps {
|
interface MarkdownEditorProps {
|
||||||
@ -49,12 +49,12 @@ export function MarkdownEditor(
|
|||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
mode: MARKDOWN_CONFIG,
|
mode: MARKDOWN_CONFIG,
|
||||||
theme: "tlon",
|
theme: 'tlon',
|
||||||
lineNumbers: false,
|
lineNumbers: false,
|
||||||
lineWrapping: true,
|
lineWrapping: true,
|
||||||
scrollbarStyle: "native",
|
scrollbarStyle: 'native',
|
||||||
// cursorHeight: 0.85,
|
// cursorHeight: 0.85,
|
||||||
placeholder: placeholder || "",
|
placeholder: placeholder || '',
|
||||||
extraKeys: { 'Enter': 'newlineAndIndentContinueMarkdownList' }
|
extraKeys: { 'Enter': 'newlineAndIndentContinueMarkdownList' }
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -84,7 +84,7 @@ export function MarkdownEditor(
|
|||||||
const codeMirror: Editor = editor.current.editor;
|
const codeMirror: Editor = editor.current.editor;
|
||||||
const doc = codeMirror.getDoc();
|
const doc = codeMirror.getDoc();
|
||||||
|
|
||||||
Array.from(files).forEach(async file => {
|
Array.from(files).forEach(async (file) => {
|
||||||
const placeholder = `![Uploading ${file.name}](...)`;
|
const placeholder = `![Uploading ${file.name}](...)`;
|
||||||
doc.setValue(doc.getValue() + placeholder);
|
doc.setValue(doc.getValue() + placeholder);
|
||||||
const url = await uploadDefault(file);
|
const url = await uploadDefault(file);
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user