Merge branch 'release/next-userspace' into lf/global-skeleton-links

This commit is contained in:
Liam Fitzgerald 2020-09-29 11:02:49 +10:00
commit 8acabefcc5
85 changed files with 1290 additions and 716 deletions

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:06808af2c089441d2cb497fc95e3292b6229b3dfa034272d46c7c41f34eb6a3b
size 6268465
oid sha256:eab360913b845f8775002cfe1830defcd252b490ac90e8dfa093297b56531392
size 19090656

View File

@ -22,8 +22,10 @@
state-5
state-6
state-7
state-8
==
::
+$ state-8 [%8 state-base]
+$ state-7 [%7 state-base]
+$ state-6 [%6 state-base]
+$ state-5 [%5 state-base]
@ -54,7 +56,7 @@
$% [%chat-update update:store]
==
--
=| state-7
=| state-8
=* state -
::
%- agent:dbug
@ -83,8 +85,33 @@
=/ old !<(versioned-state old-vase)
=| cards=(list card)
|-
?: ?=(%7 -.old)
?: ?=(%8 -.old)
[cards this(state old)]
?: ?=(%7 -.old)
=/ subscribers=(jug path ship)
%+ roll ~(val by sup.bol)
|= [[=ship =path] out=(jug path ship)]
:: /(mailbox|backlog)/~ship/resource.name
::
?. ?=([@ @ @ *] path) out
=/ pax=^path [i.t.path i.t.t.path ~]
(~(put ju out) pax ship)
=/ group ~(. grpl bol)
=. cards
%+ weld cards
^- (list card)
%+ murn ~(tap in ~(key by synced.old))
|= =path
^- (unit card)
?> ?=([@ @ ~] path)
=/ group-path (group-from-chat:cc path)
=/ members (members-from-path:group group-path)
?: (is-managed-path:group group-path) ~
=/ ships=(set ship) (~(get ju subscribers) path)
%- some
=+ [%invite path (~(dif in members) ships)]
[%pass /inv %agent [our.bol %chat-view] %poke %chat-view-action !>(-)]
$(-.old %8)
?: ?=(%6 -.old)
=. cards
%+ weld cards

View File

@ -218,7 +218,6 @@
;~(plug (cold %ur lus) parse-url)
;~(plug (cold %ge lus) parse-model)
;~(plug (cold %te hep) sym (star ;~(pfix ace parse-source)))
;~(plug (cold %as pad) sym ;~(pfix ace parse-source))
;~(plug (cold %do cab) parse-hoon ;~(pfix ace parse-source))
parse-value
==
@ -284,6 +283,7 @@
==
++ parse-value
;~ pose
;~(plug (cold %as pad) sym ;~(pfix ace parse-source))
(stag %sa ;~(pfix tar pad sym))
(stag %ex parse-hoon)
(stag %tu (ifix [lac rac] (most ace parse-source)))

View File

@ -5,7 +5,7 @@
/- glob
/+ default-agent, verb, dbug
|%
++ hash 0v3.u1ets.ipgbo.eo23m.md70h.djpj0
++ hash 0v5.6e3d0.3hm4q.iib09.rb2jb.9h4k4
+$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))]
+$ all-states
$% state-0

View File

@ -473,6 +473,22 @@
:+ %0
now.bowl
[%add-graph [ship term] `graph:store`p.u.result q.u.result]
::
:: note: near-duplicate of /x/graph
::
[%x %archive @ @ ~]
=/ =ship (slav %p i.t.t.path)
=/ =term i.t.t.t.path
=/ result=(unit marked-graph:store)
(~(get by archive) [ship term])
?~ result
~& no-archived-graph+[ship term]
[~ ~]
:- ~ :- ~ :- %graph-update
!> ^- update:store
:+ %0
now.bowl
[%add-graph [ship term] `graph:store`p.u.result q.u.result]
::
[%x %graph-subset @ @ @ @ ~]
=/ =ship (slav %p i.t.t.path)

View File

@ -24,7 +24,7 @@
<div id="portal-root"></div>
<script src="/~landscape/js/channel.js"></script>
<script src="/~landscape/js/session.js"></script>
<script src="/~landscape/js/bundle/index.df81e597349a655b83f2.js"></script>
<script src="/~landscape/js/bundle/index.9f00eb9b1c58d2b1bd3c.js"></script>
<script src="https://sdk.amazonaws.com/js/aws-sdk-2.1.12.min.js"></script>
</body>
</html>

View File

@ -36,9 +36,15 @@
^- card
[%pass wire %agent [ship term] %leave ~]
::
++ on-arvo
|= [=wire =sign-arvo]
^- (quip card _this)
?+ sign-arvo (on-arvo:def wire sign-arvo)
[%b *] [~ this]
==
::
++ on-agent on-agent:def
++ on-poke on-poke:def
++ on-arvo on-arvo:def
++ on-peek on-peek:def
++ on-watch on-watch:def
++ on-leave on-leave:def

View File

@ -1,63 +1,11 @@
:: link [landscape]:
::
:: social bookmarking
::
:: the paths under which links are submitted are generally expected to
:: correspond to existing group paths. for strictly-local collections of
:: links, arbitrary paths are probably fair game, but could trip up
:: primitive ui implementations.
::
:: urls in paths are expected to be encoded using +wood, for @ta sanity.
:: generally, use /lib/link's +build-discussion-path.
::
:: see link-listen-hook to see what's synced in, and similarly
:: see link-proxy-hook to see what's exposed.
::
:: scry and subscription paths:
::
:: (map path pages) %local-pages
:: /local-pages our saved pages
:: /local-pages/some-path our saved pages on path
::
:: (map path submissions) %submissions
:: /submissions all submissions we've seen
:: /submissions/some-path all submissions we've seen on path
::
:: (map path (map url notes)) %annotations
:: /annotations our comments
:: /annotations/wood-url our comments on url
:: /annotations/wood-url/some-path our comments on url on path
:: /annotations//some-path our comments on path
::
:: (map path (map url comments)) %discussions
:: /discussions all comments
:: /discussions/wood-url all comments on url
:: /discussions/wood-url/some-path all comments on url on path
:: /discussions//some-path all comments on path
::
:: subscription-only paths:
::
:: [path url] %observation
:: /seen updates whenever an item is seen
::
:: scry-only paths:
::
::
:: (map path (set url))
:: /unseen the ones we haven't seen yet
::
:: (set url)
:: /unseen/some-path the ones we haven't seen here yet
::
:: ?
:: /seen/wood-url/some-path have we seen this here
::
/- *link, gra=graph-store, *resource
/+ store=link-store, graph-store, default-agent, verb, dbug
::
|%
+$ state-any $%(state-1 state-0)
+$ state-1 [%1 ~]
+$ state-1 [%1 cards=(list card)]
+$ state-0
$: %0
by-group=(map path links)
@ -96,9 +44,15 @@
++ on-load
|= old=vase
^- (quip card _this)
::
=/ s !<(state-any old)
?: ?=(%1 -.s)
[~ this(state s)]
:: defer card emission to later event
::
=; [cards=(list card) that=_this]
:_ that(state [%1 cards])
[%pass /load %arvo %b %wait now.bowl]~
::
:_ this(state *state-1)
=/ orm orm:graph-store
@ -107,9 +61,9 @@
%+ turn ~(tap by by-group.s)
|= [=path =links]
^- (list card)
?. ?=([@ @ *] path)
?. ?=([@ ~] path)
(on-bad-path path links)
=/ =resource [(slav %p i.path) i.t.path]
=/ =resource [our.bowl i.path]
:_ [(archive-graph resource)]~
%+ add-graph resource
^- graph:gra
@ -153,7 +107,7 @@
++ on-bad-path
|= [=path =links]
^- (list card)
~| discarding-malformed-links+[path links]
~& discarding-malformed-links+[path links]
~
::
++ add-graph
@ -181,6 +135,12 @@
++ on-watch on-watch:def
++ on-leave on-leave:def
++ on-agent on-agent:def
++ on-arvo on-arvo:def
++ on-arvo
|= [=wire =sign-arvo]
^- (quip card _this)
?+ sign-arvo (on-arvo:def wire sign-arvo)
[%b %wake *]
[cards.state this]
==
++ on-fail on-fail:def
--

View File

@ -55,15 +55,17 @@
+$ state-1 [%1 base-state-0]
+$ state-2 [%2 base-state-0]
+$ state-3 [%3 base-state-1]
+$ state-4 [%4 base-state-1]
+$ versioned-state
$% state-0
state-1
state-2
state-3
state-4
==
--
::
=| state-3
=| state-4
=* state -
%+ verb |
%- agent:dbug
@ -82,8 +84,21 @@
=/ old !<(versioned-state vase)
=| cards=(list card)
|^
?: ?=(%3 -.old)
?: ?=(%4 -.old)
[cards this(state old)]
?: ?=(%3 -.old)
%_ $
-.old %4
::
resource-indices.old
(rebuild-resource-indices associations.old)
::
app-indices.old
(rebuild-app-indices associations.old)
::
group-indices.old
(rebuild-group-indices associations.old)
==
?: ?=(%2 -.old)
=/ new-state=state-3
%* . *state-3
@ -119,6 +134,41 @@
==
$(old new-state-1)
::
++ rebuild-app-indices
=| app-indices=(jug app-name [group-path app-path])
|= =^associations
^- (jug app-name [group-path app-path])
?~ associations app-indices
=. app-indices
%+ ~(put ju app-indices) app-name.p.n.associations
[-.p.n.associations app-path.p.n.associations]
%- ~(uni by $(associations l.associations))
$(associations r.associations)
::
++ rebuild-group-indices
=| group-indices=(jug group-path md-resource)
|= =^associations
^- (jug group-path md-resource)
?~ associations group-indices
=. group-indices
%+ ~(put ju group-indices)
-.p.n.associations
+.p.n.associations
%- ~(uni by $(associations l.associations))
$(associations r.associations)
::
++ rebuild-resource-indices
=| resource-indices=(jug md-resource group-path)
|= =^associations
^- (jug md-resource group-path)
?~ associations resource-indices
=. resource-indices
%+ ~(put ju resource-indices)
+.p.n.associations
-.p.n.associations
%- ~(uni by $(associations l.associations))
$(associations r.associations)
::
++ poke-md-hook
|= act=metadata-hook-action
^- card

View File

@ -1,8 +0,0 @@
:- ~[comments+&]
;>
# Static
You can put static files in here to serve them to the web. Actually, you can put static files anywhere in `/web` and see them in a browser.
Docs on static publishing with urbit are forthcoming — but feel free to drop markdown files in `/web` to try it out.

View File

@ -0,0 +1,14 @@
/+ graph-store
::
:- %say
|= $: [now=@da eny=@uvJ bec=beak]
[[=ship graph=term ~] ~]
==
:- %graph-update
=/ our (scot %p p.bec)
=/ wen (scot %da now)
=/ who (scot %p ship)
::
.^ update:graph-store
/gx/[our]/graph-store/[wen]/archive/[who]/[graph]/graph-update
==

View File

@ -0,0 +1,9 @@
/+ graph-store
::
:- %say
|= $: [now=@da eny=@uvJ bec=beak]
[[graph=term =path ~] ~]
==
:- %graph-update
=- ~& update=- -
.^(=update:graph-store %cx path)

View File

@ -21,10 +21,11 @@
:: ## Pokes
::
:: %push-hook-action: Add/remove a resource from pushing.
:: [update-mark.config]: A poke to proxy to the local store
:: [update-mark.config]: A poke to proxy to the local store or a
:: foreign push-hook
::
/- *push-hook
/+ default-agent, resource
/+ default-agent, resource, verb
|%
+$ card card:agent:gall
::
@ -182,6 +183,9 @@
[cards this]
::
?: =(mark update-mark.config)
?: (team:title [our src]:bowl)
:_ this
(forward-update:hc vase)
=^ cards state
(poke-update:hc vase)
[cards this]
@ -201,7 +205,7 @@
=/ =resource
(de-path:resource t.path)
=/ =vase
(initial-watch:og t.t.t.path resource)
(initial-watch:og t.t.t.t.path resource)
:_ this
[%give %fact ~ update-mark.config vase]~
::
@ -333,5 +337,19 @@
=/ =path
resource+(en-path:resource u.rid)
[%give %fact ~[path] update-mark.config vase]~
::
++ forward-update
|= =vase
^- (list card:agent:gall)
=/ rid=(unit resource)
(resource-for-update:og vase)
?~ rid ~
=/ =path
resource+(en-path:resource u.rid)
=/ =wire
(make-wire resource+(en-path:resource u.rid))
=/ dap=term
?:(=(our.bowl entity.u.rid) store-name.config dap.bowl)
[%pass wire %agent [entity.u.rid dap] %poke update-mark.config vase]~
--
--

View File

@ -1,15 +1,19 @@
/+ *graph-store
=* as-octs as-octs:mimes:html
::
|_ upd=update
++ grad %noun
++ grow
|%
++ noun upd
++ json (update:enjs upd)
++ mime [/application/x-urb-graph-update (as-octs (jam upd))]
--
::
++ grab
|%
++ noun update
++ json update:dejs
++ mime |=([* =octs] ;;(update (cue q.octs)))
--
--

View File

@ -53,7 +53,7 @@
::
;< ~ bind:m
%+ poke-our %metadata-hook
metadata-hook-action+!>([%add-synced ship.action rid.action])
metadata-hook-action+!>([%add-synced ship.action (en-path:resource rid.action)])
::
;< ~ bind:m
%+ poke-our %graph-pull-hook

View File

@ -0,0 +1,37 @@
/- spider, graph=graph-store, *metadata-store, *group, group-store
/+ strandio, resource, graph-view
=>
|%
++ strand strand:spider
++ poke poke:strandio
++ poke-our poke-our:strandio
--
=, strand=strand:spider
^- thread:spider
|= arg=vase
=/ m (strand ,vase)
^- form:m
=+ !<([rid=resource title=@t description=@t group=resource module=@t ~] arg)
;< =bowl:spider bind:m get-bowl:strandio
:: unarchive graph and share it
;< ~ bind:m
(poke-our %graph-store %graph-update !>([%0 now.bowl %unarchive-graph rid]))
;< ~ bind:m
(poke-our %graph-push-hook %push-hook-action !>([%add rid]))
::
:: Setup metadata
::
=/ =metadata
%* . *metadata
title title
description description
date-created now.bowl
creator our.bowl
module module
==
=/ act=metadata-action
[%add (en-path:resource group) graph+(en-path:resource rid) metadata]
;< ~ bind:m (poke-our %metadata-hook %metadata-action !>(act))
;< ~ bind:m
(poke-our %metadata-hook %metadata-hook-action !>([%add-owned (en-path:resource group)]))
(pure:m !>(~))

View File

@ -44,6 +44,8 @@ let devServer = {
contentBase: path.join(__dirname, '../dist'),
hot: true,
port: 9000,
host: '0.0.0.0',
disableHostCheck: true,
historyApiFallback: true
};

Binary file not shown.

View File

@ -9,7 +9,7 @@
"@reach/menu-button": "^0.10.5",
"@reach/tabs": "^0.10.5",
"@tlon/indigo-light": "^1.0.3",
"@tlon/indigo-react": "github:liam-fitzgerald/indigo-react#lf/1.1.17",
"@tlon/indigo-react": "1.2.6",
"aws-sdk": "^2.726.0",
"classnames": "^2.2.6",
"codemirror": "^5.55.0",

View File

@ -28,7 +28,7 @@ export default class GraphApi extends BaseApi<StoreState> {
}
private hookAction(ship: Patp, action: any): Promise<any> {
return this.action('graph-push-hook', 'graph-update', action, deSig(ship));
return this.action('graph-push-hook', 'graph-update', action);
}
createManagedGraph(

View File

@ -5,7 +5,8 @@ import { Path, Patp } from '~/types/noun';
export default class MetadataApi extends BaseApi<StoreState> {
metadataAdd(appName: string, appPath: Path, groupPath: Path, title: string, description: string, dateCreated: string, color: string, module: string = '') {
metadataAdd(appName: string, appPath: Path, groupPath: Path, title: string, description: string, dateCreated: string, color: string, moduleName: string) {
const creator = `~${this.ship}`;
return this.metadataAction({
add: {
@ -20,7 +21,7 @@ export default class MetadataApi extends BaseApi<StoreState> {
color,
'date-created': dateCreated,
creator,
module
'module': moduleName
}
}
});

View File

@ -0,0 +1,22 @@
export class OrderedMap<V> extends Map<number, V>
implements Iterable<[number, V]> {
[Symbol.iterator](): IterableIterator<[number, V]> {
const sorted = Array.from(super[Symbol.iterator]()).sort(
([a], [b]) => b - a
);
let index = 0;
return {
[Symbol.iterator]: this[Symbol.iterator],
next: (): IteratorResult<[number, V]> => {
if (index < sorted.length) {
return { value: sorted[index++], done: false };
} else {
return { done: true, value: null };
}
},
};
}
}

View File

@ -103,6 +103,10 @@ export default function index(associations, apps) {
app = 'groups';
};
if (each['app-name'] === 'graph') {
app = each.metadata.module;
}
const shipStart = each['app-path'].substr(each['app-path'].indexOf('~'));
if (app === 'groups') {
@ -116,7 +120,8 @@ export default function index(associations, apps) {
} else {
const obj = result(
title,
`/~${each['app-name']}/join${each['app-path']}`,
`/~${each['app-name']}/join${each['app-path']}${
(each.metadata.module && '/' + each.metadata.module) || ''}`,
app.charAt(0).toUpperCase() + app.slice(1),
(associations?.contacts?.[each['group-path']]?.metadata?.title || null)
);

View File

@ -1,4 +1,5 @@
import _ from 'lodash';
import { OrderedMap } from "~/logic/lib/OrderedMap";
export const GraphReducer = (json, state) => {
@ -17,11 +18,6 @@ const keys = (json, state) => {
if (data) {
state.graphKeys = new Set(data.map((res) => {
let resource = res.ship + '/' + res.name;
if (!(resource in state.graphs)) {
state.graphs[resource] = new Map();
}
return resource;
}));
}
@ -32,12 +28,12 @@ const addGraph = (json, state) => {
const _processNode = (node) => {
// is empty
if (!node.children) {
node.children = new Map();
node.children = new OrderedMap();
return node;
}
// is graph
let converted = new Map();
let converted = new OrderedMap();
for (let i in node.children) {
let item = node.children[i];
let index = item[0].split('/').slice(1).map((ind) => {
@ -62,7 +58,7 @@ const addGraph = (json, state) => {
}
let resource = data.resource.ship + '/' + data.resource.name;
state.graphs[resource] = new Map();
state.graphs[resource] = new OrderedMap();
for (let i in data.graph) {
let item = data.graph[i];
@ -86,12 +82,19 @@ const removeGraph = (json, state) => {
if (!('graphs' in state)) {
state.graphs = {};
}
let resource = data.ship + '/' + data.name;
let resource = data.resource.ship + '/' + data.resource.name;
delete state.graphs[resource];
state.graphKeys.delete(resource);
}
};
const mapifyChildren = (children) => {
return new OrderedMap(
children.map(([idx, node]) => {
const nd = {...node, children: mapifyChildren(node.children || []) };
return [parseInt(idx.slice(1), 10), nd];
}));
};
const addNodes = (json, state) => {
const _addNode = (graph, index, node) => {
// set child of graph
@ -128,8 +131,8 @@ const addNodes = (json, state) => {
if (index.length === 0) { return; }
// TODO: support adding nodes with children
item[1].children = new Map();
item[1].children = mapifyChildren(item[1].children || []);
state.graphs[resource] = _addNode(
state.graphs[resource],
@ -167,4 +170,3 @@ const removeNodes = (json, state) => {
});
}
};

View File

@ -56,7 +56,6 @@ export default class GlobalStore extends BaseStore<StoreState> {
this.localReducer.dehydrate(this.state);
}
initialState(): StoreState {
return {
pendingMessages: new Map(),

View File

@ -28,7 +28,6 @@ const groupSubscriptions: AppSubscription[] = [
];
const graphSubscriptions: AppSubscription[] = [
['/keys', 'graph-store'],
['/updates', 'graph-store']
];
@ -58,6 +57,7 @@ export default class GlobalSubscription extends BaseSubscription<StoreState> {
this.subscribe('/all', 's3-store');
this.subscribe('/all', 'launch');
this.subscribe('/all', 'weather');
this.subscribe('/keys', 'graph-store');
}
restart() {

View File

@ -0,0 +1,2 @@
export type PropFunc<T extends (...args: any[]) => any> = Parameters<T>[0];

View File

@ -156,6 +156,7 @@ class App extends React.Component {
/>
</Router>
</Root>
<div id="portal-root" />
</ThemeProvider>
);
}

View File

@ -2,7 +2,7 @@ import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import { Spinner } from '../../../components/Spinner';
import urbitOb from 'urbit-ob';
import { Box, Text, Input, Button } from '@tlon/indigo-react';
import { Box, Text, ManagedTextInputField as Input, Button } from '@tlon/indigo-react';
import { Formik, Form } from 'formik'
import * as Yup from 'yup';

View File

@ -100,6 +100,9 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
handleWindowFocus() {
this.setState({ idle: false });
if (this.virtualList?.window?.scrollTop === 0) {
this.dismissUnread();
}
}
initialFetch() {
@ -121,7 +124,7 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
}
componentDidUpdate(prevProps: ChatWindowProps, prevState) {
const { isChatMissing, history, envelopes, mailboxSize, stationPendingMessages } = this.props;
const { isChatMissing, history, envelopes, mailboxSize, stationPendingMessages, unreadCount } = this.props;
if (isChatMissing) {
history.push("/~chat");
@ -134,6 +137,12 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
this.stayLockedIfActive();
}
if (unreadCount > prevProps.unreadCount && this.state.idle) {
this.setState({
lastRead: unreadCount ? mailboxSize - unreadCount : Infinity,
});
}
if (stationPendingMessages.length !== prevProps.stationPendingMessages.length) {
this.virtualList?.calculateVisibleItems();
}

View File

@ -98,6 +98,7 @@ export class SettingsScreen extends Component {
association={association}
resource="chat"
app="chat"
module=""
/>
<Spinner
awaiting={this.state.awaiting}

View File

@ -44,8 +44,12 @@ export default class GraphApp extends PureComponent {
setTimeout(autoJoin, 2000);
}
};
autoJoin();
if(!graphKeys.has(resource)) {
autoJoin();
} else if(props.match.params.module) {
props.history.push(`/~${props.match.params.module}/${resource}`);
}
return (
<Center width="100%" height="100%">
<Text fontSize={1}>Redirecting...</Text>

View File

@ -3,16 +3,11 @@ import { AsyncButton } from "../../../../components/AsyncButton";
import * as Yup from "yup";
import {
Box,
Input,
Checkbox,
ManagedTextInputField as Input,
Col,
InputLabel,
InputCaption,
Button,
Center,
Label,
Text,
} from "@tlon/indigo-react";
import { Formik, Form, useFormikContext, FormikHelpers } from "formik";
import GlobalApi from "~/logic/api/global";
import { FormError } from "~/views/components/FormError";
@ -79,9 +74,9 @@ export function ChannelSettings(props: ChannelSettingsProps) {
>
<Col mb={3}>
<Text fontWeight="bold">Channel Host Settings</Text>
<InputCaption>
<Label>
Adjust channel settings, only available for channel's hosts
</InputCaption>
</Label>
</Col>
<Input
id="title"

View File

@ -1,13 +1,17 @@
import React, { Component } from "react";
import React from "react";
import { Sigil } from "~/logic/lib/sigil";
import * as Yup from "yup";
import { Link } from "react-router-dom";
import { EditElement } from "./edit-element";
import { Spinner } from "~/views/components/Spinner";
import { uxToHex } from "~/logic/lib/util";
import { Col, Input, Box, Text, Row } from "@tlon/indigo-react";
import { Formik, Form, FormikHelpers } from "formik";
import {
ManagedForm as Form,
Col,
ManagedTextInputField as Input,
Box,
Text,
Row,
} from "@tlon/indigo-react";
import { Formik, FormikHelpers } from "formik";
import { Contact } from "~/types/contact-update";
import { AsyncButton } from "~/views/components/AsyncButton";
import { ColorInput } from "~/views/components/ColorInput";
@ -103,30 +107,34 @@ export function ContactCard(props: ContactCardProps) {
initialValues={contact || emptyContact}
onSubmit={onSubmit}
>
<Form>
<Col>
<Row
borderBottom={1}
borderBottomColor="washedGray"
pb={3}
alignItems="center"
>
<Sigil size={32} classes="" color={hexColor} ship={us} />
<Box ml={2}>
<Text fontFamily="mono">{us}</Text>
</Box>
</Row>
<ImageInput mt={3} id="avatar" label="Avatar" s3={props.s3} />
<ColorInput id="color" label="Sigil Color" />
<Input id="nickname" label="Nickname" />
<Input id="email" label="Email" />
<Input id="phone" label="Phone" />
<Input id="website" label="Website" />
<Input id="notes" label="Notes" />
<AsyncButton primary loadingText="Updating..." border>
Save
</AsyncButton>
</Col>
<Form
display="grid"
gridAutoRows="auto"
gridTemplateColumns="100%"
gridRowGap="5"
maxWidth="400px"
>
<Row
borderBottom={1}
borderBottomColor="washedGray"
pb={3}
alignItems="center"
>
<Sigil size={32} classes="" color={hexColor} ship={us} />
<Box ml={2}>
<Text fontFamily="mono">{us}</Text>
</Box>
</Row>
<ImageInput id="avatar" label="Avatar" s3={props.s3} />
<ColorInput id="color" label="Sigil Color" />
<Input id="nickname" label="Nickname" />
<Input id="email" label="Email" />
<Input id="phone" label="Phone" />
<Input id="website" label="Website" />
<Input id="notes" label="Notes" />
<AsyncButton primary loadingText="Updating..." border>
Save
</AsyncButton>
</Form>
</Formik>
</Box>

View File

@ -3,13 +3,11 @@ import { AsyncButton } from "../../../../components/AsyncButton";
import * as Yup from "yup";
import {
Box,
Input,
Checkbox,
ManagedTextInputField as Input,
ManagedCheckboxField as Checkbox,
Col,
InputLabel,
InputCaption,
Label,
Button,
Center,
} from "@tlon/indigo-react";
import { Formik, Form, useFormikContext, FormikHelpers } from "formik";
import { FormError } from "~/views/components/FormError";
@ -87,12 +85,12 @@ export function GroupSettings(props: GroupSettingsProps) {
mx={4}
>
<Col mb={4}>
<InputLabel>Delete Group</InputLabel>
<InputCaption>
<Label>Delete Group</Label>
<Label gray mt="2">
Permanently delete this group. (All current members will no
longer see this group.)
</InputCaption>
<Button onClick={onDelete} mt={1} border error>
</Label>
<Button onClick={onDelete} mt={1} destructive>
Delete this group
</Button>
</Col>

View File

@ -1,5 +1,11 @@
import React, { useCallback } from "react";
import { Box, Input, Col, InputLabel, Radio, Text } from "@tlon/indigo-react";
import {
Box,
ManagedTextInputField as Input,
Col,
ManagedRadioButtonField as Radio,
Text,
} from "@tlon/indigo-react";
import { Formik, Form } from "formik";
import * as Yup from "yup";
import GlobalApi from "~/logic/api/global";
@ -59,7 +65,7 @@ export function NewChannel(props: NewChannelProps & RouteComponentProps) {
appPath,
groupPath,
EMPTY_INVITE_POLICY,
ships.map(s => `~${s}`),
ships.map((s) => `~${s}`),
true,
false
);

View File

@ -281,7 +281,8 @@ export class GroupDetail extends Component {
this.state.title,
association.metadata.description,
association.metadata['date-created'],
uxToHex(association.metadata.color)
uxToHex(association.metadata.color),
''
).then(() => {
this.setState({ awaiting: false });
});

View File

@ -44,7 +44,7 @@ export default class LaunchApp extends React.Component {
<Icon
stroke="green"
fill="rgba(0,0,0,0)"
icon="CircleDot"
icon="Circle"
/>
<Text ml="1" color="green">Home</Text>
</Row>

View File

@ -137,9 +137,11 @@ export default class LinksApp extends Component {
render={ (props) => {
const resourcePath =
`${props.match.params.ship}/${props.match.params.name}`;
const metPath = `/ship/~${resourcePath}`;
const resource =
associations.graph[resourcePath] ?
associations.graph[resourcePath] : { metadata: {} };
associations.graph[metPath] ?
associations.graph[metPath] : { metadata: {} };
const contactDetails = contacts[resource['group-path']] || {};
const popout = props.match.url.includes('/popout/');
const graph = graphs[resourcePath] || null;
@ -158,8 +160,10 @@ export default class LinksApp extends Component {
<LinkList
{...props}
api={api}
s3={s3}
graph={graph}
graphResource={graphKeys.has(resourcePath)}
resourcePath={resourcePath}
popout={popout}
metadata={resource.metadata}
contacts={contactDetails}
@ -177,10 +181,12 @@ export default class LinksApp extends Component {
render={ (props) => {
const resourcePath =
`${props.match.params.ship}/${props.match.params.name}`;
const metPath = `/ship/~${resourcePath}`;
const resource =
associations.graph[resourcePath] ?
associations.graph[resourcePath] : { metadata: {} };
associations.graph[metPath] ?
associations.graph[metPath] : { metadata: {} };
const popout = props.match.url.includes('/popout/');
const contactDetails = contacts[resource['group-path']] || {};
const indexArr = props.match.params.index.split('-');

View File

@ -6,14 +6,15 @@ import { getContactDetails } from '~/logic/lib/util';
export const Comments = (props) => {
const {
hideNicknames,
hideAvatars
hideAvatars,
remoteContentPolicy
} = props;
const contacts = props.contacts ? props.contacts : {};
return (
<div>
{ Array.from(props.comments.values()).map((comment) => {
{ Array.from(props.comments).map(([date, comment]) => {
const { nickname, color, member, avatar } =
getContactDetails(contacts[comment.post.author]);
@ -30,6 +31,7 @@ export const Comments = (props) => {
member={member}
hideNicknames={hideNicknames}
hideAvatars={hideAvatars}
remoteContentPolicy={remoteContentPolicy}
/>
);
})

View File

@ -1,4 +1,4 @@
import React, { Component } from 'react';
import React from 'react';
import { Row, Col, Anchor, Box, Text } from '@tlon/indigo-react';
import { Sigil } from '~/logic/lib/sigil';
@ -9,34 +9,32 @@ export const LinkItem = (props) => {
const {
node,
nickname,
color,
avatar,
resource,
hideAvatars,
hideNicknames
} = props;
const URLparser = new RegExp(
/((?:([\w\d\.-]+)\:\/\/?){1}(?:(www)\.?){0,1}(((?:[\w\d-]+\.)*)([\w\d-]+\.[\w\d]+))){1}(?:\:(\d+)){0,1}((\/(?:(?:[^\/\s\?]+\/)*))(?:([^\?\/\s#]+?(?:.[^\?\s]+){0,1}){0,1}(?:\?([^\s#]+)){0,1})){0,1}(?:#([^#\s]+)){0,1}/
);
const author = node.post.author;
const index = node.post.index.split('/').join('-');
const size = node.children ? node.children.size : 0;
const contents = node.post.contents;
const hostname = URLparser.exec(contents[1].url) ? URLparser.exec(contents[1].url)[4] : null;
const showAvatar = props.avatar && !hideAvatars;
const showAvatar = avatar && !hideAvatars;
const showNickname = nickname && !hideNicknames;
const mono = showNickname ? 'inter white-d' : 'mono white-d';
const img = showAvatar
? <img src={props.avatar} height={36} width={36} className="dib" />
: <Sigil ship={`~${author}`} size={36} color={'#' + props.color} />;
const baseUrl = props.baseUrl || `/~link/${resource}`;
let hostname = '';
try {
const url = new URL(contents[1].url);
hostname = url.hostname;
} catch (e) {}
return (
<Row alignItems="center" py={3} bg="white">
{img}
@ -63,5 +61,5 @@ export const LinkItem = (props) => {
</Col>
</Row>
);
}
};

View File

@ -1,21 +1,39 @@
import React from 'react';
import { cite } from '~/logic/lib/util';
import RemoteContent from "~/views/components/RemoteContent";
import moment from 'moment';
const URLparser = new RegExp(
/((?:([\w\d\.-]+)\:\/\/?){1}(?:(www)\.?){0,1}(((?:[\w\d-]+\.)*)([\w\d-]+\.[\w\d]+))){1}(?:\:(\d+)){0,1}((\/(?:(?:[^\/\s\?]+\/)*))(?:([^\?\/\s#]+?(?:.[^\?\s]+){0,1}){0,1}(?:\?([^\s#]+)){0,1})){0,1}(?:#([^#\s]+)){0,1}/
);
export const LinkPreview = (props) => {
const showNickname = props.nickname && !props.hideNicknames;
const nameClass = showNickname ? 'inter' : 'mono';
const author = props.post.author;
const title = props.post.contents[0].text;
const url = props.post.contents[1].url;
const hostname = URLparser.exec(url) ? URLparser.exec(url)[4] : null;
const timeSent =
moment.unix(props.post['time-sent'] / 1000).format('hh:mm a');
const title = props.post.contents[0].text;
const url = props.post.contents[1].url;
const embed = (
<RemoteContent
unfold={true}
renderUrl={false}
url={url}
remoteContentPolicy={props.remoteContentPolicy}
className="mw-100"
/>
);
return (
<div className="pb6 w-100">
<div className='w-100 tc'>{embed}</div>
<div className="flex flex-column ml2 pt6 flex-auto">
<a href={url}
className="w-100 flex"
@ -23,7 +41,7 @@ export const LinkPreview = (props) => {
rel="noopener noreferrer">
<p className="f8 truncate">{title}</p>
<span className="gray2 ml2 f8 dib v-btm flex-shrink-0">
{url}
{hostname}
</span>
</a>
<div className="w-100 pt1">

View File

@ -1,126 +0,0 @@
import React, { Component } from 'react';
import { Spinner } from '~/views/components/Spinner';
import { createPost } from '~/logic/api/graph';
export class LinkSubmit extends Component {
constructor() {
super();
this.state = {
linkValue: '',
linkTitle: '',
submitFocus: false,
disabled: false
};
this.setLinkValue = this.setLinkValue.bind(this);
this.setLinkTitle = this.setLinkTitle.bind(this);
}
onClickPost() {
const link = this.state.linkValue;
const title = this.state.linkTitle
? this.state.linkTitle
: this.state.linkValue;
const parentIndex = this.props.parentIndex || '';
let post = createPost([
{ text: title },
{ url: link }
], parentIndex);
this.setState({ disabled: true }, () => {
this.props.api.graph.addPost(
`~${this.props.ship}`,
this.props.name,
post
).then((r) => {
this.setState({
disabled: false,
linkValue: '',
linkTitle: '',
});
});
});
}
setLinkValue(event) {
this.setState({ linkValue: event.target.value });
}
setLinkTitle(event) {
this.setState({ linkTitle: event.target.value });
}
render() {
const activeClasses = (!this.state.disabled) ? 'green2 pointer' : 'gray2';
const focus = (this.state.submitFocus)
? 'b--black b--white-d'
: 'b--gray4 b--gray2-d';
return (
<div className={'relative ba br1 w-100 mb6 ' + focus}>
<textarea
className="pl2 bg-gray0-d white-d w-100 f8"
style={{
resize: 'none',
height: 40,
paddingTop: 10
}}
placeholder="Paste link here"
onChange={this.setLinkValue}
onBlur={() => this.setState({ submitFocus: false })}
onFocus={() => this.setState({ submitFocus: true })}
spellCheck="false"
rows={1}
onKeyPress={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
this.onClickPost();
}
}}
value={this.state.linkValue}
/>
<textarea
className="pl2 bg-gray0-d white-d w-100 f8"
style={{
resize: 'none',
height: 40,
paddingTop: 16
}}
placeholder="Enter title"
onChange={this.setLinkTitle}
onBlur={() => this.setState({ submitFocus: false })}
onFocus={() => this.setState({ submitFocus: true })}
spellCheck="false"
rows={1}
onKeyPress={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
this.onClickPost();
}
}}
value={this.state.linkTitle}
/>
<button
className={
'absolute bg-gray0-d f8 ml2 flex-shrink-0 ' + activeClasses
}
disabled={this.state.disabled}
onClick={this.onClickPost.bind(this)}
style={{
bottom: 12,
right: 8
}}
>
Post
</button>
<Spinner
awaiting={this.state.disabled}
classes="mt3 absolute right-0"
text="Posting to collection..." />
</div>
);
}
}

View File

@ -0,0 +1,300 @@
import React, { Component } from 'react';
import { hasProvider } from 'oembed-parser';
import { S3Upload, SubmitDragger } from '~/views/components/s3-upload';
import { Spinner } from '~/views/components/Spinner';
import { Icon } from "@tlon/indigo-react";
import GlobalApi from '~/logic/api/global';
import { S3State } from '~/types';
import { createPost } from '~/logic/api/graph';
interface LinkSubmitProps {
api: GlobalApi;
s3: S3State;
name: string;
ship: string;
}
interface LinkSubmitState {
linkValue: string;
linkTitle: string;
linkValid: boolean;
submitFocus: boolean;
urlFocus: boolean;
disabled: boolean;
dragover: boolean;
}
export class LinkSubmit extends Component<LinkSubmitProps, LinkSubmitState> {
private s3Uploader: React.RefObject<S3Upload>;
constructor(props) {
super(props);
this.state = {
linkValue: '',
linkTitle: '',
linkValid: false,
submitFocus: false,
urlFocus: false,
disabled: false,
dragover: false
};
this.setLinkValue = this.setLinkValue.bind(this);
this.setLinkTitle = this.setLinkTitle.bind(this);
this.onDragEnter = this.onDragEnter.bind(this);
this.onDrop = this.onDrop.bind(this);
this.onPaste = this.onPaste.bind(this);
this.uploadFiles = this.uploadFiles.bind(this);
this.s3Uploader = React.createRef();
}
onClickPost() {
const link = this.state.linkValue;
const title = this.state.linkTitle
? this.state.linkTitle
: this.state.linkValue;
this.setState({ disabled: true });
const parentIndex = this.props.parentIndex || '';
let post = createPost([
{ text: title },
{ url: link }
], parentIndex);
this.props.api.graph.addPost(
`~${this.props.ship}`,
this.props.name,
post
).then((r) => {
this.setState({
disabled: false,
linkValue: '',
linkTitle: '',
linkValid: false
});
});
}
setLinkValid(linkValue) {
const URLparser = new RegExp(
/((?:([\w\d\.-]+)\:\/\/?){1}(?:(www)\.?){0,1}(((?:[\w\d-]+\.)*)([\w\d-]+\.[\w\d]+))){1}(?:\:(\d+)){0,1}((\/(?:(?:[^\/\s\?]+\/)*))(?:([^\?\/\s#]+?(?:.[^\?\s]+){0,1}){0,1}(?:\?([^\s#]+)){0,1})){0,1}(?:#([^#\s]+)){0,1}/
);;
let linkValid = URLparser.test(linkValue);
if (!linkValid) {
linkValid = URLparser.test(`http://${linkValue}`);
if (linkValid) {
linkValue = `http://${linkValue}`;
}
}
this.setState({ linkValid, linkValue });
if (linkValid) {
if (hasProvider(linkValue)) {
fetch(`https://noembed.com/embed?url=${linkValue}`)
.then(response => response.json())
.then((result) => {
if (result.title && !this.state.linkTitle) {
this.setState({ linkTitle: result.title });
}
}).catch((error) => {/*noop*/});
} else if (!this.state.linkTitle) {
this.setState({
linkTitle: decodeURIComponent(linkValue
.split('/')
.pop()
.split('.')
.slice(0, -1)
.join('.')
.replace('_', ' ')
.replace(/\d{4}\.\d{1,2}\.\d{2}\.\.\d{2}\.\d{2}\.\d{2}-/, '')
)
})
}
}
}
setLinkValue(event) {
this.setState({ linkValue: event.target.value });
this.setLinkValid(event.target.value);
}
setLinkTitle(event) {
this.setState({ linkTitle: event.target.value });
}
uploadSuccess(url) {
this.setState({ linkValue: url });
this.setLinkValid(url);
}
uploadError(error) {
// no-op for now
}
readyToUpload(): boolean {
return Boolean(this.s3Uploader.current && this.s3Uploader.current.inputRef.current);
}
onDragEnter() {
if (!this.readyToUpload()) {
return;
}
this.setState({ dragover: true });
}
onDrop(event: DragEvent) {
this.setState({ dragover: false });
if (!event.dataTransfer || !event.dataTransfer.files.length) {
return;
}
event.preventDefault();
this.uploadFiles(event.dataTransfer.files);
}
onPaste(event: ClipboardEvent) {
if (!event.clipboardData || !event.clipboardData.files.length) {
return;
}
event.preventDefault();
event.stopPropagation();
this.uploadFiles(event.clipboardData.files);
}
uploadFiles(files: FileList) {
if (!this.readyToUpload()) {
return;
}
this.s3Uploader.current.inputRef.current.files = files;
const fire = document.createEvent("HTMLEvents");
fire.initEvent("change", true, true);
this.s3Uploader.current?.inputRef.current?.dispatchEvent(fire);
}
render() {
const activeClasses = (this.state.linkValid && !this.state.disabled)
? 'green2 pointer' : 'gray2';
const focus = (this.state.submitFocus)
? 'b--black b--white-d'
: 'b--gray4 b--gray2-d';
const isS3Ready =
( this.props.s3.credentials.secretAccessKey &&
this.props.s3.credentials.endpoint &&
this.props.s3.credentials.accessKeyId
);
return (
<div
className={`relative ba br1 w-100 mb6 ${focus}`}
onDragEnter={this.onDragEnter.bind(this)}
onDragOver={e => {
e.preventDefault();
if (isS3Ready) {
this.setState({ dragover: true})
}
}}
onDragLeave={() => this.setState({ dragover: false })}
onDrop={this.onDrop}
>
{this.state.dragover ? <SubmitDragger /> : null}
<div className="relative">
{
( this.state.linkValue ||
this.state.urlFocus ||
this.state.disabled
) ? null : (
isS3Ready ? (
<span className="gray2 absolute pl2 pt3 pb2 f8"
style={{pointerEvents: 'none'}}>
Drop or
<span className="pointer green2"
style={{pointerEvents: 'all'}}
onClick={(event) => {
if (!this.readyToUpload()) {
return;
}
this.s3Uploader.current.inputRef.current.click();
}}> upload </span>
a file, or paste a link here
</span>
) : (
<span className="gray2 absolute pl2 pt3 pb2 f8"
style={{pointerEvents: 'none'}}>
Paste a link here
</span>
)
)
}
{!this.state.disabled && isS3Ready ? <S3Upload
ref={this.s3Uploader}
configuration={this.props.s3.configuration}
credentials={this.props.s3.credentials}
uploadSuccess={this.uploadSuccess.bind(this)}
uploadError={this.uploadError.bind(this)}
className="dn absolute pt3 pb2 pl2 w-100"
></S3Upload> : null}
<input
type="url"
className="pl2 w-100 f8 pt3 pb2 white-d bg-transparent"
onChange={this.setLinkValue}
onBlur={() => this.setState({ submitFocus: false, urlFocus: false })}
onFocus={() => this.setState({ submitFocus: true, urlFocus: true })}
spellCheck="false"
onPaste={this.onPaste}
onKeyPress={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
this.onClickPost();
}
}}
value={this.state.linkValue}
/>
</div>
<input
type="text"
className="pl2 bg-transparent w-100 f8 white-d linkTitle"
style={{
resize: 'none',
height: 40
}}
placeholder="Provide a title"
onChange={this.setLinkTitle}
onBlur={() => this.setState({ submitFocus: false })}
onFocus={() => this.setState({ submitFocus: true })}
spellCheck="false"
onKeyPress={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
this.onClickPost();
}
}}
value={this.state.linkTitle}
/>
{!this.state.disabled ? <button
className={
'bg-transparent f8 flex-shrink-0 pr2 pl2 pt2 pb3 ' + activeClasses
}
disabled={!this.state.linkValid || this.state.disabled}
onClick={this.onClickPost.bind(this)}
style={{
bottom: 12,
right: 8
}}
>
Post link
</button> : null}
<Spinner awaiting={this.state.disabled} classes="nowrap flex items-center pr2 pl2 pt2 pb4" style={{flex: '1 1 14rem'}} text="Posting to collection..." />
</div>
) ;
}
}
export default LinkSubmit;

View File

@ -48,8 +48,13 @@ export const LinkDetail = (props) => {
popout={props.popout}
api={props.api}
/>
<Link className="dib f9 fw4 pt2 gray2 lh-solid" to="/~link">
{`<- ${title}`}
<Link className="dib f9 fw4 pt2 gray2 lh-solid"
to={`/~link/${resourcePath}`}>
<h2
className="dib f9 fw4 lh-solid v-top black white-d"
style={{ width: 'max-content' }}>
{`<- ${title}`}
</h2>
</Link>
<TabBar
location={props.location}
@ -65,7 +70,9 @@ export const LinkDetail = (props) => {
post={props.node.post}
nickname={nickname}
hideNicknames={props.hideNicknames}
commentNumber={props.node.children.size} />
commentNumber={props.node.children.size}
remoteContentPolicy={props.remoteContentPolicy}
/>
<div className="flex">
<CommentSubmit
name={props.name}
@ -80,7 +87,8 @@ export const LinkDetail = (props) => {
popout={props.popout}
api={props.api}
hideAvatars={props.hideAvatars}
hideNicknames={props.hideNicknames} />
hideNicknames={props.hideNicknames}
remoteContentPolicy={props.remoteContentPolicy} />
</div>
</div>
</div>

View File

@ -1,10 +1,10 @@
import React, { Component, useEffect } from "react";
import { TabBar } from "~/views/components/chat-link-tabbar";
import { SidebarSwitcher } from "~/views/components/SidebarSwitch";
import { Link } from "react-router-dom";
import { LinkItem } from "./lib/link-item";
import { LinkSubmit } from "./lib/link-submit";
import { TabBar } from '~/views/components/chat-link-tabbar';
import { SidebarSwitcher } from '~/views/components/SidebarSwitch';
import { Link } from 'react-router-dom';
import { LinkItem } from './lib/link-item';
import LinkSubmit from './lib/link-submit';
import { getContactDetails } from "~/logic/lib/util";
@ -45,9 +45,12 @@ export const LinkList = (props) => {
<SidebarSwitcher
sidebarShown={props.sidebarShown}
popout={props.popout}
api={props.api}
/>
<h2 className="white-d dib f9 fw4 lh-solid v-top pt2">{title}</h2>
api={props.api} />
<h2
className="dib f9 fw4 pt2 lh-solid v-top black white-d"
style={{ width: 'max-content' }}>
{title}
</h2>
<TabBar
location={props.location}
popout={props.popout}
@ -58,19 +61,29 @@ export const LinkList = (props) => {
<div className="w-100 mt6 flex justify-center overflow-y-scroll ph4 pb4">
<div className="w-100 mw7">
<div className="flex">
<LinkSubmit name={props.name} ship={props.ship} api={props.api} />
<LinkSubmit
name={props.name}
ship={props.ship}
api={props.api}
s3={props.s3} />
</div>
{Array.from(props.graph.values()).map((node) => {
return (
<LinkItem
resource={resource}
node={node}
nickname={props.metadata.nickname}
hideAvatars={props.hideAvatars}
hideNicknames={props.hideNicknames}
/>
);
})}
{ Array.from(props.graph).map(([date, node]) => {
const { nickname, color, avatar } =
getContactDetails(props.contacts[ship]);
return (
<LinkItem
resource={resource}
node={node}
nickname={nickname}
color={color}
avatar={avatar}
hideAvatars={props.hideAvatars}
hideNicknames={props.hideNicknames}
/>
);
})
}
</div>
</div>
</div>

View File

@ -1,6 +1,6 @@
import React, { useCallback } from "react";
import { RouteComponentProps } from "react-router-dom";
import { Box, Input, Col } from "@tlon/indigo-react";
import { Box, ManagedTextInputField as Input, Col } from "@tlon/indigo-react";
import { Formik, Form } from "formik";
import * as Yup from "yup";
@ -69,7 +69,7 @@ export function NewScreen(props: object) {
<Box
display="grid"
gridTemplateRows="auto"
gridRowGap={2}
gridRowGap={4}
gridTemplateColumns="300px">
<Input
id="name"

View File

@ -143,7 +143,8 @@ export class SettingsScreen extends Component {
popout={this.props.popout}
api={this.props.api}
/>
<Link to="/~link" className="pt2">
<Link className="dib f9 fw4 pt2 gray2 lh-solid"
to={`/~link/${props.resourcePath}`}>
<h2
className="dib f9 fw4 lh-solid v-top"
style={{ width: 'max-content' }}>
@ -153,8 +154,8 @@ export class SettingsScreen extends Component {
<TabBar
location={props.location}
popout={props.popout}
popoutHref={`/~link/popout/${props.resource}/settings`}
settings={`/~link/${props.resource}/settings`}
popoutHref={`/~link/popout/${props.resourcePath}/settings`}
settings={`/~link/${props.resourcePath}/settings`}
/>
</div>
<div className="w-100 pl3 mt4 cf">
@ -168,6 +169,7 @@ export class SettingsScreen extends Component {
association={props.resource}
resource="collection"
app="graph"
module="link"
/>
<Spinner
awaiting={this.state.awaiting}

View File

@ -6,6 +6,10 @@
padding-bottom: 56.25%;
}
.linkTitle::placeholder {
color: #7f7f7f;
}
.links.embed-container iframe, .links.embed-container object, .links.embed-container embed {
position: absolute;
top: 0;

View File

@ -1,9 +1,16 @@
import React from 'react';
import { Box, InputLabel, Radio, Input } from '@tlon/indigo-react';
import React from "react";
import {
Box,
Row,
Label,
Col,
ManagedRadioButtonField as Radio,
ManagedTextInputField as Input,
} from "@tlon/indigo-react";
import GlobalApi from '~/logic/api/global';
import { S3State } from '~/types';
import { ImageInput } from '~/views/components/ImageInput';
import GlobalApi from "~/logic/api/global";
import { S3State } from "~/types";
import { ImageInput } from "~/views/components/ImageInput";
export type BgType = "none" | "url" | "color";
@ -18,37 +25,33 @@ export function BackgroundPicker({
api: GlobalApi;
s3: S3State;
}) {
const rowSpace = { my: 0, alignItems: 'center' };
const radioProps = { my: 4, mr: 4, name: 'bgType' };
return (
<Box>
<InputLabel>Landscape Background</InputLabel>
<Box display="flex" alignItems="center">
<Box mt={3} mr={7}>
<Radio label="Image" id="url" name="bgType" />
{bgType === "url" && (
<ImageInput
api={api}
s3={s3}
id="bgUrl"
name="bgUrl"
label="URL"
url={bgUrl || ""}
/>
)}
<Radio label="Color" id="color" name="bgType" />
{bgType === "color" && (
<Input
ml={4}
type="text"
label="Color"
id="bgColor"
name="bgColor"
/>
)}
<Radio label="None" id="none" name="bgType" />
</Box>
</Box>
</Box>
<Col>
<Label mb="2">Landscape Background</Label>
<Row {...rowSpace}>
<Radio {...radioProps} label="Image" id="url" />
{bgType === "url" && (
<ImageInput
ml="3"
api={api}
s3={s3}
id="bgUrl"
name="bgUrl"
label="URL"
url={bgUrl || ""}
/>
)}
</Row>
<Row {...rowSpace}>
<Radio label="Color" id="color" {...radioProps} />
{bgType === "color" && (
<Input ml={4} type="text" label="Color" id="bgColor" />
)}
</Row>
<Radio label="None" id="none" {...radioProps} />
</Col>
);
}

View File

@ -1,7 +1,8 @@
import React, { useCallback } from "react";
import {
Input,
ManagedTextInputField as Input,
ManagedForm as Form,
Box,
Button,
Col,
@ -11,9 +12,9 @@ import {
MenuList,
MenuItem,
} from "@tlon/indigo-react";
import { Formik, Form } from "formik";
import { Formik } from "formik";
import GlobalApi from "../../../../api/global";
import GlobalApi from "~/logic/api/global";
export function BucketList({
buckets,
@ -53,49 +54,48 @@ export function BucketList({
return (
<Formik initialValues={{ newBucket: "" }} onSubmit={onSubmit}>
<Form>
<Col alignItems="start">
{_buckets.map((bucket) => (
<Box
key={bucket}
display="flex"
justifyContent="space-between"
alignItems="center"
borderRadius={1}
border={1}
borderColor="washedGray"
fontSize={1}
pl={2}
mb={2}
width="100%"
>
<Text>{bucket}</Text>
{bucket === selected && (
<Text p={1} color="green">
Active
</Text>
)}
{bucket !== selected && (
<Menu>
<MenuButton sm>Options</MenuButton>
<MenuList>
<MenuItem onSelect={onSelect(bucket)}>Make Active</MenuItem>
<MenuItem onSelect={onDelete(bucket)}>Delete</MenuItem>
</MenuList>
</Menu>
)}
</Box>
))}
<Input
mt={2}
type="text"
label="New Bucket"
id="newBucket"
/>
<Button border borderColor="washedGrey" type="submit">
Add
</Button>
</Col>
<Form
display="grid"
gridTemplateColumns="100%"
gridAutoRows="auto"
gridRowGap={2}
>
{_buckets.map((bucket) => (
<Box
key={bucket}
display="flex"
justifyContent="space-between"
alignItems="center"
borderRadius={1}
border={1}
borderColor="washedGray"
fontSize={1}
pl={2}
mb={2}
>
<Text>{bucket}</Text>
{bucket === selected && (
<Text p={2} color="green">
Active
</Text>
)}
{bucket !== selected && (
<Menu>
<MenuButton border={0} cursor="pointer" width="auto">
Options
</MenuButton>
<MenuList>
<MenuItem onSelect={onSelect(bucket)}>Make Active</MenuItem>
<MenuItem onSelect={onDelete(bucket)}>Delete</MenuItem>
</MenuList>
</Menu>
)}
</Box>
))}
<Input mt="2" label="New Bucket" id="newBucket" />
<Button mt="2" borderColor="washedGrey" type="submit">
Add
</Button>
</Form>
</Formik>
);

View File

@ -2,8 +2,8 @@ import React from "react";
import {
Box,
InputLabel,
Checkbox,
Label,
ManagedCheckboxField as Checkbox,
Button,
} from "@tlon/indigo-react";
import { Formik, Form } from "formik";
@ -14,7 +14,7 @@ import GlobalApi from "../../../../api/global";
import { LaunchState } from "../../../../types/launch-update";
import { DropLaunchTiles } from "./DropLaunch";
import { S3State, BackgroundConfig } from "../../../../types";
import { BackgroundPicker, BgType } from './BackgroundPicker';
import { BackgroundPicker, BgType } from "./BackgroundPicker";
const formSchema = Yup.object().shape({
tileOrdering: Yup.array().of(Yup.string()),
@ -47,14 +47,7 @@ interface DisplayFormProps {
}
export default function DisplayForm(props: DisplayFormProps) {
const {
api,
launch,
background,
hideAvatars,
hideNicknames,
s3
} = props;
const { api, launch, background, hideAvatars, hideNicknames, s3 } = props;
let bgColor, bgUrl;
if (background?.type === "url") {
@ -99,17 +92,17 @@ export default function DisplayForm(props: DisplayFormProps) {
<Form>
<Box
display="grid"
gridTemplateColumns="1fr"
gridTemplateColumns="100%"
gridTemplateRows="auto"
gridRowGap={3}
gridRowGap={5}
>
<Box color="black" fontSize={1} mb={3} fontWeight={900}>
Display Preferences
</Box>
<Box mb={2}>
<InputLabel display="block" pb={2}>
<Label display="block" pb={2}>
Tile Order
</InputLabel>
</Label>
<DropLaunchTiles
id="tileOrdering"
name="tileOrdering"
@ -123,22 +116,20 @@ export default function DisplayForm(props: DisplayFormProps) {
api={api}
s3={s3}
/>
<Box>
<Checkbox
label="Disable avatars"
id="avatars"
caption="Do not show user-set avatars"
/>
<Checkbox
label="Disable nicknames"
id="nicknames"
caption="Do not show user-set nicknames"
/>
</Box>
<Checkbox
label="Disable avatars"
id="avatars"
caption="Do not show user-set avatars"
/>
<Checkbox
label="Disable nicknames"
id="nicknames"
caption="Do not show user-set nicknames"
/>
<Button border={1} borderColor="washedGray" type="submit">
Save
</Button>
</Box>
<Button border={1} borderColor="washedGray" type="submit">
Save
</Button>
</Form>
)}
</Formik>

View File

@ -4,11 +4,11 @@ import { usePreview } from "react-dnd-multi-backend";
import { capitalize } from "lodash";
import { TileTypeBasic, Tile } from "../../../../types/launch-update";
import { Box, Img as _Img, Text } from "@tlon/indigo-react";
import { Box, Image as _Image, Text } from "@tlon/indigo-react";
import styled from "styled-components";
// Need to change dojo image
const Img = styled(_Img)<{ invert?: boolean }>`
const Image = styled(_Image)<{ invert?: boolean }>`
${(p) =>
p.theme.colors.white !== "rgba(255,255,255,1)" ? `filter: invert(1);` : ``}
@ -83,7 +83,7 @@ function DragTileBasic(props: {
}
style={props.style}
>
<Img width="48px" height="48px" src={tile.iconUrl} invert={isDojo} />
<Image width="48px" height="48px" src={tile.iconUrl} invert={isDojo} />
<Text
color={
"black" // isDojo ? "white" : "black"

View File

@ -1,5 +1,9 @@
import React from "react";
import { Box, Button, Checkbox } from '@tlon/indigo-react';
import {
Box,
Button,
ManagedCheckboxField as Checkbox,
} from "@tlon/indigo-react";
import { Formik, Form } from "formik";
import * as Yup from "yup";
@ -10,7 +14,7 @@ const formSchema = Yup.object().shape({
imageShown: Yup.boolean(),
audioShown: Yup.boolean(),
videoShown: Yup.boolean(),
oembedShown: Yup.boolean()
oembedShown: Yup.boolean(),
});
interface FormSchema {
@ -39,7 +43,7 @@ export default function RemoteContentForm(props: RemoteContentFormProps) {
imageShown,
audioShown,
videoShown,
oembedShown
oembedShown,
} as FormSchema
}
onSubmit={(values, actions) => {
@ -47,7 +51,7 @@ export default function RemoteContentForm(props: RemoteContentFormProps) {
imageShown: values.imageShown,
audioShown: values.audioShown,
videoShown: values.videoShown,
oembedShown: values.oembedShown
oembedShown: values.oembedShown,
});
api.local.dehydrate();
actions.setSubmitting(false);
@ -59,36 +63,26 @@ export default function RemoteContentForm(props: RemoteContentFormProps) {
display="grid"
gridTemplateColumns="1fr"
gridTemplateRows="audio"
gridRowGap={3}
gridRowGap={5}
>
<Box color="black" fontSize={1} mb={3} fontWeight={900}>
<Box color="black" fontSize={1} fontWeight={900}>
Remote Content
</Box>
<Box>
<Checkbox
label="Load images"
id="imageShown"
/>
<Checkbox
label="Load audio files"
id="audioShown"
/>
<Checkbox
label="Load video files"
id="videoShown"
/>
<Checkbox
label="Load embedded content"
id="oembedShown"
caption="Embedded content may contain scripts"
/>
</Box>
<Checkbox label="Load images" id="imageShown" />
<Checkbox label="Load audio files" id="audioShown" />
<Checkbox label="Load video files" id="videoShown" />
<Checkbox
label="Load embedded content"
id="oembedShown"
caption="Embedded content may contain scripts"
/>
<Button border={1} borderColor="washedGray" type="submit">
Save
</Button>
</Box>
<Button border={1} borderColor="washedGray" type="submit">
Save
</Button>
</Form>
)}
</Formik>
);
}
}

View File

@ -1,17 +1,18 @@
import React, { useCallback } from "react";
import {
Input,
ManagedTextInputField as Input,
ManagedForm as Form,
Box,
Button,
Col,
Text,
Menu
Menu,
} from "@tlon/indigo-react";
import { Formik, Form } from "formik";
import { Formik } from "formik";
import GlobalApi from "../../../../api/global";
import { BucketList } from './BucketList';
import { BucketList } from "./BucketList";
import { S3State } from "../../../../types";
interface FormSchema {
@ -49,9 +50,6 @@ export default function S3Form(props: S3FormProps) {
return (
<>
<Col>
<Box color="black" mb={4} fontSize={1} fontWeight={900}>
S3 Credentials
</Box>
<Formik
initialValues={
{
@ -64,23 +62,23 @@ export default function S3Form(props: S3FormProps) {
}
onSubmit={onSubmit}
>
<Form>
<Input width="256px" type="text" label="Endpoint" id="s3endpoint" />
<Form
display="grid"
gridTemplateColumns="100%"
gridAutoRows="auto"
gridRowGap={5}
>
<Box color="black" fontSize={1} fontWeight={900}>
S3 Credentials
</Box>
<Input label="Endpoint" id="s3endpoint" />
<Input label="Access Key ID" id="s3accessKeyId" />
<Input
width="256px"
type="text"
label="Access Key ID"
id="s3accessKeyId"
/>
<Input
width="256px"
type="password"
label="Secret Access Key"
id="s3secretAccessKey"
/>
<Button border={1} type="submit">
Submit
</Button>
<Button type="submit">Submit</Button>
</Form>
</Formik>
</Col>

View File

@ -36,7 +36,6 @@ export default function Settings({
return (
<Box
backgroundColor="white"
fontSize={2}
display="grid"
gridTemplateRows="auto"
gridTemplateColumns="1fr"

View File

@ -22,7 +22,7 @@ const SidebarItem = ({ children, view, current }) => {
px={3}
backgroundColor={selected ? "washedBlue" : "white"}
>
<Icon mr={2} display="inline-block" icon="Circle" fill={color} />
<Icon mr={2} display="inline-block" icon="Circle" color={color} />
<Text color={color} fontSize={0}>
{children}
</Text>

View File

@ -2,7 +2,7 @@ import React from "react";
import * as Yup from "yup";
import { Formik, FormikHelpers, Form, useFormikContext } from "formik";
import { AsyncButton } from "../../../../components/AsyncButton";
import { TextArea } from "@tlon/indigo-react";
import { ManagedTextAreaField as TextArea } from "@tlon/indigo-react";
interface FormSchema {
comment: string;
@ -48,7 +48,7 @@ export default function CommentInput(props: CommentInputProps) {
id="comment"
placeholder={props.placeholder || ""}
/>
<AsyncButton loadingText={loading} border type="submit">
<AsyncButton mt={2} loadingText={loading} border type="submit">
{label}
</AsyncButton>
</Form>

View File

@ -38,7 +38,7 @@ export function EditPost(props: EditPostProps & RouteComponentProps) {
<PostForm
initial={initial}
onSubmit={onSubmit}
submitLabel={`Update ${note.title}`}
submitLabel="Update"
loadingText="Updating..."
/>
);

View File

@ -1,5 +1,5 @@
import React, { useCallback, useState, useRef, useEffect } from "react";
import { Col, Text, ErrorMessage } from "@tlon/indigo-react";
import { Col, Text, ErrorLabel } from "@tlon/indigo-react";
import { Spinner } from "~/views/components/Spinner";
import { Notebooks } from "~/types/publish-update";
import { useWaitForProps } from "~/logic/lib/useWaitForProps";
@ -46,7 +46,7 @@ export function JoinScreen(props: JoinScreenProps & RouteComponentProps) {
<Col p={4}>
<Text fontSize={1}>Joining Notebook</Text>
<Spinner awaiting text="Joining..." />
{error && <ErrorMessage>Unable to join notebook</ErrorMessage>}
{error && <ErrorLabel>Unable to join notebook</ErrorLabel>}
</Col>
);
}

View File

@ -0,0 +1,73 @@
import React, { useCallback } from "react";
import { UnControlled as CodeEditor } from "react-codemirror2";
import { MOBILE_BROWSER_REGEX } from "~/logic/lib/util";
import { PropFunc } from "~/types/util";
import CodeMirror from "codemirror";
import "codemirror/mode/markdown/markdown";
import "codemirror/addon/display/placeholder";
import "codemirror/lib/codemirror.css";
import { Box } from "@tlon/indigo-react";
const MARKDOWN_CONFIG = {
name: "markdown",
};
interface MarkdownEditorProps {
placeholder?: string;
value: string;
onChange: (s: string) => void;
onBlur?: (e: any) => void;
}
export function MarkdownEditor(
props: MarkdownEditorProps & PropFunc<typeof Box>
) {
const { onBlur, placeholder, value, onChange, ...boxProps } = props;
const options = {
mode: MARKDOWN_CONFIG,
theme: "tlon",
lineNumbers: false,
lineWrapping: true,
scrollbarStyle: "native",
// cursorHeight: 0.85,
placeholder: placeholder || "",
};
const handleChange = useCallback(
(_e, _d, v: string) => {
onChange(v);
},
[onChange]
);
const handleBlur = useCallback(
(_i, e: any) => {
onBlur && onBlur(e);
},
[onBlur]
);
return (
<Box
flexGrow={1}
position="static"
className="publish"
p={1}
border={1}
borderColor="lightGray"
borderRadius={2}
{...boxProps}
>
<CodeEditor
onBlur={onBlur}
value={value}
options={options}
onChange={handleChange}
/>
</Box>
);
}

View File

@ -1,27 +1,43 @@
import React from 'react';
import styled from 'styled-components';
import { MarkdownEditor as _MarkdownEditor, Box, ErrorMessage } from '@tlon/indigo-react';
import { useField } from 'formik';
import React, { useCallback } from "react";
import _ from "lodash";
import { Box, ErrorLabel } from "@tlon/indigo-react";
import { useField } from "formik";
import { MarkdownEditor } from "./MarkdownEditor";
const MarkdownEditor = styled(_MarkdownEditor)`
border: 1px solid ${(p) => p.theme.colors.lightGray};
border-radius: ${(p) => p.theme.radii[2]}px;
`;
export const MarkdownField = ({
id,
...rest
}: { id: string } & Parameters<typeof Box>[0]) => {
const [{ value, onBlur }, { error, touched }, { setValue }] = useField(id);
export const MarkdownField = ({ id, ...rest }: { id: string; } & Parameters<typeof Box>[0]) => {
const [{ value }, { error, touched }, { setValue, setTouched }] = useField(id);
const handleBlur = useCallback(
(e: any) => {
_.set(e, "target.id", id);
console.log(e);
onBlur && onBlur(e);
},
[onBlur, id]
);
const hasError = !!(error && touched);
return (
<Box overflowY="hidden" width="100%" display="flex" flexDirection="column" {...rest}>
<Box
overflowY="hidden"
width="100%"
display="flex"
flexDirection="column"
{...rest}
>
<MarkdownEditor
onFocus={() => setTouched(true)}
onBlur={() => setTouched(false)}
borderColor={hasError ? "red" : "lightGray"}
onBlur={handleBlur}
value={value}
onBeforeChange={(e, d, v) => setValue(v)}
onChange={setValue}
/>
<ErrorMessage>{touched && error}</ErrorMessage>
<ErrorLabel mt="2" hasError={!!(error && touched)}>
{error}
</ErrorLabel>
</Box>
);
};

View File

@ -3,11 +3,9 @@ import { AsyncButton } from "../../../../components/AsyncButton";
import * as Yup from "yup";
import {
Box,
Input,
Checkbox,
ManagedTextInputField as Input,
ManagedCheckboxField as Checkbox,
Col,
InputLabel,
InputCaption,
Button,
Center,
} from "@tlon/indigo-react";

View File

@ -1,6 +1,11 @@
import React from "react";
import * as Yup from "yup";
import { Box, Input } from "@tlon/indigo-react";
import {
Box,
ManagedTextInputField as Input,
Row,
Col,
} from "@tlon/indigo-react";
import { AsyncButton } from "../../../../components/AsyncButton";
import { Formik, Form, FormikHelpers } from "formik";
import { MarkdownField } from "./MarkdownField";
@ -29,32 +34,29 @@ export function PostForm(props: PostFormProps) {
const { initial, onSubmit, submitLabel, loadingText } = props;
return (
<Box
width="100%"
height="100%"
p={[2, 4]}
display="grid"
justifyItems="start"
gridTemplateRows={["64px 64px 1fr", "64px 1fr"]}
gridTemplateColumns={["100%", "1fr 1fr"]}
gridColumnGap={2}
gridRowGap={2}
>
<Col width="100%" height="100%" p={[2, 4]}>
<Formik
validationSchema={formSchema}
initialValues={initial}
onSubmit={onSubmit}
validateOnBlur
>
<Form style={{ display: "contents" }}>
<Input width="100%" placeholder="Post Title" id="title" />
<Box gridRow={["1/2", "auto"]} mt={1} justifySelf={["start", "end"]}>
<AsyncButton primary loadingText={loadingText}>
<Row flexDirection={["column-reverse", "row"]} mb={4} gapX={4} justifyContent='space-between'>
<Input maxWidth='40rem' flexGrow={1} placeholder="Post Title" id="title" />
<AsyncButton
ml={[0,2]}
mb={[4,0]}
flexShrink={1}
primary
loadingText={loadingText}
>
{submitLabel}
</AsyncButton>
</Box>
<MarkdownField gridColumn={["1/2", "1/3"]} id="body" />
</Row>
<MarkdownField flexGrow={1} id="body" />
</Form>
</Formik>
</Box>
</Col>
);
}

View File

@ -4,18 +4,9 @@ import { NotebookPosts } from "./NotebookPosts";
import { Subscribers } from "./Subscribers";
import { Settings } from "./Settings";
import { Spinner } from "~/views/components/Spinner";
import { Tabs, Tab } from "~/views/components/Tab";
import { roleForShip } from "~/logic/lib/group";
import {
Box,
Button,
Text,
Tab as _Tab,
Tabs,
TabList as _TabList,
TabPanels,
TabPanel,
Row,
} from "@tlon/indigo-react";
import { Box, Button, Text, Row } from "@tlon/indigo-react";
import { Notebook as INotebook } from "~/types/publish-update";
import { Groups } from "~/types/group-update";
import { Contacts, Rolodex } from "~/types/contact-update";
@ -24,14 +15,6 @@ import styled from "styled-components";
import { Associations } from "~/types";
import { deSig } from "~/logic/lib/util";
const TabList = styled(_TabList)`
margin-bottom: ${(p) => p.theme.space[4]}px;
`;
const Tab = styled(_Tab)`
flex-grow: 1;
`;
interface NotebookProps {
api: GlobalApi;
ship: string;
@ -48,17 +31,24 @@ interface NotebookProps {
interface NotebookState {
isUnsubscribing: boolean;
tab: string;
}
export class Notebook extends PureComponent<
NotebookProps & RouteComponentProps,
NotebookState
> {
constructor(props: NotebookProps & RouteComponentProps) {
constructor(props) {
super(props);
this.state = {
isUnsubscribing: false,
tab: "all",
};
this.setTab = this.setTab.bind(this);
}
setTab(tab: string) {
this.setState({ tab });
}
render() {
@ -73,6 +63,7 @@ export class Notebook extends PureComponent<
hideNicknames,
associations,
} = this.props;
const { state } = this;
const group = groups[notebook?.["writers-group-path"]];
if (!group) return null; // Waitin on groups to populate
@ -131,8 +122,7 @@ export class Notebook extends PureComponent<
) : (
<Button
ml={isWriter ? 2 : 0}
error
border
destructive
onClick={() => {
this.setState({ isUnsubscribing: true });
api.publish
@ -149,56 +139,74 @@ export class Notebook extends PureComponent<
</Button>
)
) : null}
{!isOwn && (
<Button ml={isWriter ? 2 : 0} error border>
Unsubscribe
</Button>
)}
</Row>
<Box gridColumn={["1/2", "1/3"]}>
<Tabs>
<TabList>
<Tab>All Posts</Tab>
<Tab>About</Tab>
{isAdmin && <Tab>Subscribers</Tab>}
{isOwn && <Tab>Settings</Tab>}
</TabList>
<TabPanels>
<TabPanel>
<NotebookPosts
notes={notes}
list={notesList}
host={ship}
book={book}
contacts={notebookContacts}
hideNicknames={hideNicknames}
<Tab
selected={state.tab}
setSelected={this.setTab}
label="All Posts"
id="all"
/>
<Tab
selected={state.tab}
setSelected={this.setTab}
label="About"
id="about"
/>
{isAdmin && (
<>
<Tab
selected={state.tab}
setSelected={this.setTab}
label="Subscribers"
id="subscribers"
/>
</TabPanel>
<TabPanel>
<Box color="black">{notebook?.about}</Box>
</TabPanel>
<TabPanel>
<Subscribers
host={ship}
book={book}
notebook={notebook}
api={api}
groups={groups}
<Tab
selected={state.tab}
setSelected={this.setTab}
label="Settings"
id="settings"
/>
</TabPanel>
<TabPanel>
<Settings
host={ship}
book={book}
api={api}
notebook={notebook}
contacts={notebookContacts}
associations={associations}
groups={groups}
/>
</TabPanel>
</TabPanels>
</>
)}
</Tabs>
{state.tab === "all" && (
<NotebookPosts
notes={notes}
list={notesList}
host={ship}
book={book}
contacts={notebookContacts}
hideNicknames={hideNicknames}
/>
)}
{state.tab === "about" && (
<Box mt="3" color="black">
{notebook?.about}
</Box>
)}
{state.tab === "subscribers" && (
<Subscribers
host={ship}
book={book}
notebook={notebook}
api={api}
groups={groups}
/>
)}
{state.tab === "settings" && (
<Settings
host={ship}
book={book}
api={api}
notebook={notebook}
contacts={notebookContacts}
associations={associations}
groups={groups}
/>
)}
</Box>
</Box>
);

View File

@ -16,7 +16,7 @@ interface NotebookPostsProps {
export function NotebookPosts(props: NotebookPostsProps) {
return (
<Col>
<Col mt="3">
{props.list.map((noteId: NoteId) => {
const note = props.notes[noteId];
if (!note) {

View File

@ -1,5 +1,5 @@
import React, { useEffect } from "react";
import { Box, Col, Button, InputLabel, InputCaption } from "@tlon/indigo-react";
import { Box, Col, Button, Label } from "@tlon/indigo-react";
import GlobalApi from "~/logic/api/global";
import { Notebook } from "~/types/publish-update";
import { Contacts } from "~/types/contact-update";
@ -20,7 +20,7 @@ interface SettingsProps {
}
const Divider = (props) => (
<Box {...props} mb={4} borderBottom={1} borderBottomColor="lightGray" />
<Box {...props} borderBottom={1} borderBottomColor="lightGray" />
);
export function Settings(props: SettingsProps) {
const history = useHistory();
@ -36,10 +36,11 @@ export function Settings(props: SettingsProps) {
<Box
mx="auto"
maxWidth="300px"
mb={4}
my={4}
gridTemplateColumns="1fr"
gridAutoRows="auto"
display="grid"
gridRowGap={5}
>
{isUnmanaged && (
<>
@ -50,12 +51,12 @@ export function Settings(props: SettingsProps) {
<MetadataForm {...props} />
<Divider />
<Col mb={4}>
<InputLabel>Delete Notebook</InputLabel>
<InputCaption>
<Label>Delete Notebook</Label>
<Label gray mt="2">
Permanently delete this notebook. (All current members will no longer
see this notebook.)
</InputCaption>
<Button onClick={onDelete} mt={1} border error>
</Label>
<Button mt="2" onClick={onDelete} destructive>
Delete this notebook
</Button>
</Col>

View File

@ -96,7 +96,7 @@ export function Sidebar(props: any) {
);
}
const display = props.path ? ['none', 'block'] : 'block';
const display = props.hidden ? ['none', 'block'] : 'block';
return (
<Col
@ -106,7 +106,8 @@ export function Sidebar(props: any) {
pt={[3, 0]}
overflowY="auto"
display={display}
maxWidth={["none", "250px"]}
flexShrink={0}
width={["auto", "250px"]}
>
<Box>
<Link to="/~publish/new" className="green2 pa4 f9 dib">

View File

@ -80,7 +80,7 @@ export class Subscribers extends Component<SubscribersProps> {
const role = roleForShip(group, window.ship)
return (
<Box>
<Box mt="3">
{ role === 'admin' && (
<Button mb={3} border onClick={this.addAll}>
Add all members as writers

View File

@ -59,7 +59,7 @@ export default function NewPost(props: NewPostProps & RouteComponentProps) {
<PostForm
initial={initialValues}
onSubmit={onSubmit}
submitLabel={`Publish to ${notebook?.title}`}
submitLabel="Publish"
loadingText="Posting..."
/>
);

View File

@ -1,5 +1,5 @@
import React, { useCallback } from "react";
import { Box, Input, Col } from "@tlon/indigo-react";
import { Box, ManagedTextInputField as Input, Col } from "@tlon/indigo-react";
import { Formik, Form } from "formik";
import * as Yup from "yup";
import GlobalApi from "~/logic/api/global";

View File

@ -89,7 +89,9 @@ export function Skeleton(props: SkeletonProps) {
}, 1500);
}, [noteId, notebook, ship, notebooks])
const panelDisplay = !path ? ["none", "block"] : "block";
const hideSidebar = path || props.location.pathname.endsWith('/new')
const panelDisplay = !hideSidebar ? ["none", "block"] : "block";
return (
<Box height="100%" width="100%" px={[0, 3]} pb={[0, 3]}>
<Box
@ -105,6 +107,7 @@ export function Skeleton(props: SkeletonProps) {
notebooks={props.notebooks}
contacts={props.contacts}
path={path}
hidden={hideSidebar}
invites={props.invites}
associations={props.associations}
api={props.api}

View File

@ -65,12 +65,13 @@
.publish .react-codemirror2 {
width: 100%;
height: 100%;
}
.publish .CodeMirror {
padding: 12px;
height: 100% !important;
max-width: 700px;
overflow-y: hidden;
width: 100% !important;
cursor: text;
font-size: 12px;

View File

@ -1,8 +1,7 @@
import React, { ReactNode, useState, useEffect } from "react";
import { Button } from "@tlon/indigo-react";
import { Button, LoadingSpinner } from "@tlon/indigo-react";
import { Spinner } from "./Spinner";
import { useFormikContext } from "formik";
interface AsyncButtonProps {
@ -37,7 +36,12 @@ export function AsyncButton({
return (
<Button disabled={!isValid} type="submit" {...rest}>
{isSubmitting ? (
<Spinner awaiting text={loadingText} />
<LoadingSpinner
foreground={rest.primary ? "white" : 'black'}
background="transparent"
awaiting
text={loadingText}
/>
) : success === true ? (
"Done"
) : success === false ? (

View File

@ -1,41 +1,34 @@
import React from "react";
import { useField } from "formik";
import styled from "styled-components";
import { Col, InputLabel, Row, Box, ErrorMessage } from "@tlon/indigo-react";
import {
Col,
Label,
Row,
Box,
ErrorLabel,
StatelessTextInput as Input,
} from "@tlon/indigo-react";
import { uxToHex, hexToUx } from "~/logic/lib/util";
const Input = styled.input`
background-color: ${ p => p.theme.colors.white };
color: ${ p => p.theme.colors.black };
box-sizing: border-box;
border: 1px solid;
border-right: none;
border-color: ${(p) => p.theme.colors.lightGray};
border-top-left-radius: ${(p) => p.theme.radii[2]}px;
border-bottom-left-radius: ${(p) => p.theme.radii[2]}px;
padding: ${(p) => p.theme.space[2]}px;
font-size: 12px;
line-height: 1.2;
`;
type ColorInputProps = Parameters<typeof Col>[0] & {
id: string;
label: string;
}
};
export function ColorInput(props: ColorInputProps) {
const { id, label, ...rest } = props;
const [{ value }, { error }, { setValue }] = useField(id);
const { id, label, caption, ...rest } = props;
const [{ value, onBlur }, meta, { setValue }] = useField(id);
const hex = value.substr(2).replace('.', '');
const padded = hex.padStart(6, '0');
const hex = value.substr(2).replace(".", "");
const padded = hex.padStart(6, "0");
const onChange = (e: any) => {
const { value: newValue } = e.target as HTMLInputElement;
const valid = newValue.match(/^(\d|[a-f]|[A-F]){0,6}$/);
if(!valid) {
if (!valid) {
return;
}
const result = hexToUx(newValue);
@ -43,10 +36,21 @@ export function ColorInput(props: ColorInputProps) {
};
return (
<Col {...rest}>
<InputLabel htmlFor={id}>{label}</InputLabel>
<Row mt={2}>
<Input onChange={onChange} value={hex} />
<Box display="flex" flexDirection="column" {...props}>
<Label htmlFor={id}>{label}</Label>
{caption ? (
<Label mt="2" gray>
{caption}
</Label>
) : null}
<Row mt="2" alignItems="flex-end">
<Input
borderTopRightRadius={0}
borderBottomRightRadius={0}
onBlur={onBlur}
onChange={onChange}
value={hex}
/>
<Box
borderBottomRightRadius={1}
borderTopRightRadius={1}
@ -58,7 +62,9 @@ export function ColorInput(props: ColorInputProps) {
bg={`#${padded}`}
/>
</Row>
<ErrorMessage mt="2">{error}</ErrorMessage>
</Col>
<ErrorLabel mt="2" hasError={!!(meta.touched && meta.error)}>
{meta.error}
</ErrorLabel>
</Box>
);
}

View File

@ -10,9 +10,8 @@ import _ from "lodash";
import Mousetrap from "mousetrap";
import {
Box,
InputLabel,
ErrorMessage,
InputCaption,
Label,
ErrorLabel,
} from "@tlon/indigo-react";
import { useDropdown } from "~/logic/lib/useDropdown";
import styled from "styled-components";
@ -130,9 +129,9 @@ export function DropdownSearch<C>(props: DropdownSearchProps<C>) {
}, [options, props.getKey, props.renderCandidate, selected]);
return (
<Box position="relative">
<InputLabel htmlFor={props.id}>{props.label}</InputLabel>
{caption ? <InputCaption>{caption}</InputCaption> : null}
<Box position="relative" zIndex={9}>
<Label htmlFor={props.id}>{props.label}</Label>
{caption ? <Label mt="2" gray>{caption}</Label> : null}
{!props.disabled && (
<TextArea
ref={textarea}
@ -166,7 +165,7 @@ export function DropdownSearch<C>(props: DropdownSearchProps<C>) {
})}
</Box>
)}
<ErrorMessage>{props.error}</ErrorMessage>
<ErrorLabel>{props.error}</ErrorLabel>
</Box>
);
}

View File

@ -1,6 +1,6 @@
import React from "react";
import { useFormikContext } from "formik";
import { ErrorMessage } from "@tlon/indigo-react";
import { ErrorLabel } from "@tlon/indigo-react";
export function FormError(props: { message: string }) {
const { status } = useFormikContext();
@ -8,6 +8,6 @@ export function FormError(props: { message: string }) {
let s = status || {};
return (
<ErrorMessage>{"error" in s ? props.message : null}</ErrorMessage>
<ErrorLabel>{"error" in s ? props.message : null}</ErrorLabel>
);
}

View File

@ -25,16 +25,16 @@ class GroupMember extends Component<{ ship: Patp; options: any[] }, {}> {
return (
<div className='flex justify-between f9 items-center'>
<div className='flex flex-column'>
<div className='flex flex-column flex-shrink-0'>
<Text mono mr='2'>{`${cite(ship)}`}</Text>
{children}
</div>
{options.length > 0 && (
<Menu>
<MenuButton sm>Options</MenuButton>
<MenuButton width='min-content'>Options</MenuButton>
<MenuList>
{options.map(({ onSelect, text }) => (
<MenuItem onSelect={onSelect}>{text}</MenuItem>
<MenuItem onSelect={onSelect}><Text fontsize='0'>{text}</Text></MenuItem>
))}
</MenuList>
</Menu>

View File

@ -1,7 +1,13 @@
import React, { useRef, useCallback, useState } from "react";
import { Box, Input, Img, Button } from "@tlon/indigo-react";
import GlobalApi from "~/api/global";
import {
Box,
StatelessTextInput as Input,
Row,
Button,
Label,
ErrorLabel,
} from "@tlon/indigo-react";
import { useField } from "formik";
import { S3State } from "~/types/s3-update";
import { useS3 } from "~/logic/lib/useS3";
@ -10,16 +16,17 @@ type ImageInputProps = Parameters<typeof Box>[0] & {
id: string;
label: string;
s3: S3State;
placeholder?: string;
};
export function ImageInput(props: ImageInputProps) {
const { id, label, s3, ...rest } = props;
const { id, label, s3, caption, placeholder, ...rest } = props;
const { uploadDefault, canUpload } = useS3(s3);
const [uploading, setUploading] = useState(false);
const [, , { setValue, setError }] = useField(id);
const [field, meta, { setValue, setError }] = useField(id);
const ref = useRef<HTMLInputElement | null>(null);
@ -44,29 +51,44 @@ export function ImageInput(props: ImageInputProps) {
}, [ref]);
return (
<Box {...rest} display="flex">
<Input disabled={uploading} type="text" label={label} id={id} />
{canUpload && (
<>
<Button
ml={1}
border={3}
borderColor="washedGray"
style={{ marginTop: "18px" }}
onClick={onClick}
>
{uploading ? "Uploading" : "Upload"}
</Button>
<input
style={{ display: "none" }}
type="file"
id="fileElement"
ref={ref}
accept="image/*"
onChange={onImageUpload}
/>
</>
)}
<Box display="flex" flexDirection="column" {...props}>
<Label htmlFor={id}>{label}</Label>
{caption ? (
<Label mt="2" gray>
{caption}
</Label>
) : null}
<Row mt="2" alignItems="flex-end">
<Input
type={"text"}
hasError={meta.touched && meta.error !== undefined}
placeholder={placeholder}
{...field}
/>
{canUpload && (
<>
<Button
ml={1}
border={1}
borderColor="lightGray"
onClick={onClick}
>
{uploading ? "Uploading" : "Upload"}
</Button>
<input
style={{ display: "none" }}
type="file"
id="fileElement"
ref={ref}
accept="image/*"
onChange={onImageUpload}
/>
</>
)}
</Row>
<ErrorLabel mt="2" hasError={!!(meta.touched && meta.error)}>
{meta.error}
</ErrorLabel>
</Box>
);
}

View File

@ -48,7 +48,7 @@ export function SidebarItem(props: {
const appPath = association?.["app-path"];
const groupPath = association?.["group-path"];
const app = apps[module];
const isUnmanaged = groups[groupPath]?.hidden || false;
const isUnmanaged = groups?.[groupPath]?.hidden || false;
if (!app) {
return null;
}

View File

@ -1,6 +1,13 @@
import React, { useCallback } from "react";
import * as Yup from "yup";
import { Row, Box, Icon, Radio, Col, Checkbox } from "@tlon/indigo-react";
import {
Row,
Box,
Icon,
ManagedRadioButtonField as Radio,
ManagedCheckboxField as Checkbox,
Col,
} from "@tlon/indigo-react";
import { FormikOnBlur } from "./FormikOnBlur";
import { Dropdown } from "./Dropdown";
import { FormikHelpers } from "formik";
@ -53,7 +60,7 @@ export function SidebarListHeader(props: {
</FormikOnBlur>
}
>
<Icon stroke="gray" icon="Menu" />
<Icon stroke="gray" icon="Circle" />
</Dropdown>
</Row>
);

View File

@ -0,0 +1,32 @@
import React from "react";
import { Box, Text, Row } from "@tlon/indigo-react";
export const Tab = ({ selected, id, label, setSelected }) => (
<Box
py={2}
borderBottom={1}
borderBottomColor={selected === id ? "black" : "washedGray"}
px={2}
cursor='pointer'
flexGrow={1}
display="flex"
alignItems="center"
justifyContent="center"
onClick={() => setSelected(id)}
>
<Text color={selected === id ? "black" : "gray"}>{label}</Text>
</Box>
);
export const Tabs = ({ children, ...rest }: Parameters<typeof Row>[0]) => (
<Row
bg="white"
mb={2}
justifyContent="stretch"
alignItems="flex-end"
{...rest}
>
{children}
</Row>
);

View File

@ -117,7 +117,7 @@ export class Omnibox extends Component {
const { props } = this;
this.setState({ results: this.initialResults(), query: '' }, () => {
props.api.local.setOmnibox();
if (defaultApps.includes(app.toLowerCase()) || app === 'profile') {
if (defaultApps.includes(app.toLowerCase()) || app === 'profile' || app === 'Links') {
props.history.push(link);
} else {
window.location.href = link;
@ -179,18 +179,15 @@ export class Omnibox extends Component {
})
);
if (currentIndex > 0) {
const nextApp = flattenedResults[currentIndex - 1].app;
const nextLink = flattenedResults[currentIndex - 1].link;
this.setState({ selected: [nextApp, nextLink] });
const { app, link } = flattenedResults[currentIndex - 1];
this.setState({ selected: [app, link] });
} else {
const nextApp = flattenedResults[totalLength - 1].app;
const nextLink = flattenedResults[totalLength - 1].link;
this.setState({ selected: [nextApp, nextLink] });
const { app, link } = flattenedResults[totalLength - 1];
this.setState({ selected: [app, link] });
}
} else {
const nextApp = flattenedResults[totalLength - 1].app;
const nextLink = flattenedResults[totalLength - 1].link;
this.setState({ selected: [nextApp, nextLink] });
const { app, link } = flattenedResults[totalLength - 1];
this.setState({ selected: [app, link] });
}
}
@ -204,18 +201,15 @@ export class Omnibox extends Component {
})
);
if (currentIndex < flattenedResults.length - 1) {
const nextApp = flattenedResults[currentIndex + 1].app;
const nextLink = flattenedResults[currentIndex + 1].link;
this.setState({ selected: [nextApp, nextLink] });
const { app, link } = flattenedResults[currentIndex + 1];
this.setState({ selected: [app, link] });
} else {
const nextApp = flattenedResults[0].app;
const nextLink = flattenedResults[0].link;
this.setState({ selected: [nextApp, nextLink] });
const { app, link } = flattenedResults[0];
this.setState({ selected: [app, link] });
}
} else {
const nextApp = flattenedResults[0].app;
const nextLink = flattenedResults[0].link;
this.setState({ selected: [nextApp, nextLink] });
const { app, link } = flattenedResults[0];
this.setState({ selected: [app, link] });
}
}

View File

@ -8,6 +8,10 @@ export class OmniboxInput extends Component {
<input
ref={(el) => {
this.input = el;
if (el && document.activeElement.isSameNode(el)) {
el.blur();
el.focus();
}
}
}
className='ba b--transparent w-100 br2 white-d bg-gray0-d inter f9 pa2'

View File

@ -12,7 +12,8 @@ export const MetadataSettings = (props) => {
changeLoading,
api,
resource,
app
app,
module
} = props;
const title =
@ -41,6 +42,7 @@ export const MetadataSettings = (props) => {
val,
association.metadata.description,
association.metadata['date-created'],
module,
uxToHex(association.metadata.color)
).then(() => {
changeLoading(false, false, '', () => {});
@ -61,7 +63,8 @@ export const MetadataSettings = (props) => {
association.metadata.title,
val,
association.metadata['date-created'],
uxToHex(association.metadata.color)
uxToHex(association.metadata.color),
module
).then(() => {
changeLoading(false, false, '', () => {});
});
@ -80,7 +83,8 @@ export const MetadataSettings = (props) => {
association.metadata.title,
association.metadata.description,
association.metadata['date-created'],
val
val,
module
).then(() => {
changeLoading(false, false, '', () => {});
});