Merge branch 'release/next-userspace' into lf/hark-redux

This commit is contained in:
Matilde Park 2020-11-10 15:04:16 -05:00
commit e986298ee9
44 changed files with 642 additions and 503 deletions

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d39205183f34081949acf9fa4abb9017a8764ee36526009cd3a26a4f121faf01
size 6278799
oid sha256:b5d3225b222544eae6f78d18a4a2343ff4c746fbcb72914760b79598ff90813d
size 6278785

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 453 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 611 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 951 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1010 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 679 B

View File

@ -0,0 +1,222 @@
:: observe-hook:
::
:: helper that observes an app at a particular path and forwards all facts
:: to a particular thread. kills the subscription if the thread crashes
::
/- sur=observe-hook
/+ default-agent, dbug
::
|%
+$ card card:agent:gall
+$ versioned-state
$% state-0
==
::
+$ serial @uv
+$ state-0 [%0 observers=(map serial observer:sur)]
++ got-by-val
|= [a=(map serial observer:sur) b=observer:sur]
^- serial
%- need
%+ roll ~(tap by a)
|= [[key=serial val=observer:sur] output=(unit serial)]
?:(=(val b) `key output)
--
::
%- agent:dbug
=| state-0
=* state -
::
^- agent:gall
|_ =bowl:gall
+* this .
def ~(. (default-agent this %|) bowl)
::
++ on-init
|^ ^- (quip card _this)
:_ this
:_ ~
(act /inv-gra [%watch %invite-store /invitatory/graph %invite-accepted-graph])
::
++ act
|= [=wire =action:sur]
^- card
:* %pass
wire
%agent
[our.bowl %observe-hook]
%poke
%observe-action
!> ^- action:sur
action
==
--
::
++ on-save !>(state)
++ on-load
|= old-vase=vase
^- (quip card _this)
`this(state !<(state-0 old-vase))
::
++ on-poke
|= [=mark =vase]
^- (quip card _this)
?> (team:title our.bowl src.bowl)
?. ?=(%observe-action mark)
(on-poke:def mark vase)
=/ =action:sur !<(action:sur vase)
=* observer observer.action
=/ vals (silt ~(val by observers))
?- -.action
%watch
?: ?|(=(app.observer %spider) =(app.observer %observe-hook))
~|('we avoid infinite loops' !!)
?: (~(has in vals) observer)
~|('duplicate observer' !!)
:_ this(observers (~(put by observers) (sham eny.bowl) observer))
:_ ~
:* %pass
/observer/(scot %uv (sham eny.bowl))
%agent
[our.bowl app.observer]
%watch
path.observer
==
::
%ignore
?. (~(has in vals) observer)
~|('cannot remove nonexistent observer' !!)
=/ key (got-by-val observers observer)
:_ this(observers (~(del by observers) key))
:_ ~
:* %pass
/observer/(scot %uv key)
%agent
[our.bowl app.observer]
%leave
~
==
==
::
++ on-agent
|= [=wire =sign:agent:gall]
^- (quip card _this)
|^
?+ wire (on-agent:def wire sign)
[%observer @ ~] on-observer
[%thread-result @ ~] on-thread-result
[%thread-start @ @ ~] on-thread-start
==
::
++ on-observer
?> ?=([%observer @ ~] wire)
?+ -.sign (on-agent:def wire sign)
%watch-ack
?~ p.sign [~ this]
=/ =serial (slav %uv i.t.wire)
~& watch-ack-deleting-observer+(~(got by observers) serial)
[~ this(observers (~(del by observers) serial))]
::
%kick
=/ =serial (slav %uv i.t.wire)
=/ =observer:sur (~(got by observers) serial)
:_ this
:_ ~
:* %pass
wire
%agent
[our.bowl app.observer]
%watch
path.observer
==
::
%fact
=/ =serial (slav %uv i.t.wire)
=/ =observer:sur (~(got by observers) serial)
=/ tid (scot %uv (sham eny.bowl))
:_ this
:~ :* %pass
[%thread-result i.t.wire ~]
%agent
[our.bowl %spider]
%watch
[%thread-result tid ~]
==
:* %pass
[%thread-start i.t.wire tid ~]
%agent
[our.bowl %spider]
%poke
%spider-start
!>([~ `tid thread.observer (slop q.cage.sign !>(~))])
== ==
==
::
++ on-thread-result
?> ?=([%thread-result @ ~] wire)
?+ -.sign (on-agent:def wire sign)
%kick [~ this]
%watch-ack [~ this]
::
%fact
?. =(p.cage.sign %thread-fail)
:_ this
:_ ~
:* %pass
wire
%agent
[our.bowl %spider]
%leave
~
==
=/ =serial (slav %uv i.t.wire)
=/ =observer:sur (~(got by observers) serial)
~& observer-failed+observer
:_ this(observers (~(del by observers) serial))
:~ :* %pass
[%observer i.t.wire ~]
%agent
[our.bowl app.observer]
%leave
~
==
:* %pass
wire
%agent
[our.bowl %spider]
%leave
~
==
==
==
::
++ on-thread-start
?> ?=([%thread-start @ @ ~] wire)
?. ?=(%poke-ack -.sign) (on-agent:def wire sign)
?~ p.sign [~ this]
=/ =serial (slav %uv i.t.wire)
=/ =observer:sur (~(got by observers) serial)
~& added-invalid-observer+observer
:_ this(observers (~(del by observers) serial))
:~ :* %pass
[%observer i.t.wire ~]
%agent
[our.bowl app.observer]
%leave
~
==
:* %pass
wire
%agent
[our.bowl app.observer]
%leave
~
== ==
--
::
++ on-watch on-watch:def
++ on-leave on-leave:def
++ on-peek on-peek:def
++ on-arvo on-arvo:def
++ on-fail on-fail:def
--

View File

@ -111,6 +111,7 @@
%hark-graph-hook
%hark-group-hook
%hark-chat-hook
%observe-hook
==
::
++ deft-fish :: default connects
@ -250,6 +251,7 @@
=> (se-born | %home %hark-group-hook)
=> (se-born | %home %hark-chat-hook)
(se-born | %home %hark-store)
(se-born | %home %observe-hook)
..on-load
::
++ reap-phat :: ack connect

View File

@ -12,26 +12,26 @@
::
++ on-init
^- (quip card:agent:gall agent:gall)
%- (print bowl "{<dap.bowl>}: on-init")
%- (print bowl |.("{<dap.bowl>}: on-init"))
=^ cards agent on-init:ag
[[(emit-event %on-init ~) cards] this]
::
++ on-save
^- vase
%- (print bowl "{<dap.bowl>}: on-save")
%- (print bowl |.("{<dap.bowl>}: on-save"))
on-save:ag
::
++ on-load
|= old-state=vase
^- (quip card:agent:gall agent:gall)
%- (print bowl "{<dap.bowl>}: on-load")
%- (print bowl |.("{<dap.bowl>}: on-load"))
=^ cards agent (on-load:ag old-state)
[[(emit-event %on-load ~) cards] this]
::
++ on-poke
|= [=mark =vase]
^- (quip card:agent:gall agent:gall)
%- (print bowl "{<dap.bowl>}: on-poke with mark {<mark>}")
%- (print bowl |.("{<dap.bowl>}: on-poke with mark {<mark>}"))
?: ?=(%verb mark)
?- !<(?(%loud %bowl) vase)
%loud `this(loud !loud)
@ -43,7 +43,7 @@
++ on-watch
|= =path
^- (quip card:agent:gall agent:gall)
%- (print bowl "{<dap.bowl>}: on-watch on path {<path>}")
%- (print bowl |.("{<dap.bowl>}: on-watch on path {<path>}"))
=^ cards agent
?: ?=([%verb %events ~] path)
[~ agent]
@ -53,7 +53,7 @@
++ on-leave
|= =path
^- (quip card:agent:gall agent:gall)
%- (print bowl "{<dap.bowl>}: on-leave on path {<path>}")
%- (print bowl |.("{<dap.bowl>}: on-leave on path {<path>}"))
?: ?=([%verb %event ~] path)
[~ this]
=^ cards agent (on-leave:ag path)
@ -62,39 +62,40 @@
++ on-peek
|= =path
^- (unit (unit cage))
%- (print bowl "{<dap.bowl>}: on-peek on path {<path>}")
%- (print bowl |.("{<dap.bowl>}: on-peek on path {<path>}"))
(on-peek:ag path)
::
++ on-agent
|= [=wire =sign:agent:gall]
^- (quip card:agent:gall agent:gall)
%- (print bowl "{<dap.bowl>}: on-agent on wire {<wire>}, {<-.sign>}")
%- (print bowl |.("{<dap.bowl>}: on-agent on wire {<wire>}, {<-.sign>}"))
=^ cards agent (on-agent:ag wire sign)
[[(emit-event %on-agent wire -.sign) cards] this]
::
++ on-arvo
|= [=wire =sign-arvo]
^- (quip card:agent:gall agent:gall)
%- (print bowl "{<dap.bowl>}: on-arvo on wire {<wire>}, {<[- +<]:sign-arvo>}")
%- %+ print bowl |.
"{<dap.bowl>}: on-arvo on wire {<wire>}, {<[- +<]:sign-arvo>}"
=^ cards agent (on-arvo:ag wire sign-arvo)
[[(emit-event %on-arvo wire [- +<]:sign-arvo) cards] this]
::
++ on-fail
|= [=term =tang]
^- (quip card:agent:gall agent:gall)
%- (print bowl "{<dap.bowl>}: on-fail with term {<term>}")
%- (print bowl |.("{<dap.bowl>}: on-fail with term {<term>}"))
=^ cards agent (on-fail:ag term tang)
[[(emit-event %on-fail term) cards] this]
--
::
++ print
|= [=bowl:gall =tape]
|= [=bowl:gall render=(trap tape)]
^+ same
=? . bowl-print
%- (slog >bowl< ~)
.
?. loud same
%- (slog leaf+tape ~)
%- (slog [%leaf $:render] ~)
same
::
++ emit-event

View File

@ -0,0 +1,13 @@
/- sur=observe-hook
|_ =action:sur
++ grad %noun
++ grow
|%
++ noun action
--
::
++ grab
|%
++ noun action:sur
--
--

View File

@ -0,0 +1,7 @@
|%
+$ observer [app=term =path thread=term]
+$ action
$% [%watch =observer]
[%ignore =observer]
==
--

View File

@ -0,0 +1,28 @@
/- spider, inv=invite-store, graph-view
/+ strandio
::
=* strand strand:spider
=* fail strand-fail:strand
=* poke-our poke-our:strandio
=* flog-text flog-text:strandio
::
^- thread:spider
|= arg=vase
=/ m (strand ,vase)
^- form:m
=+ !<([=update:inv ~] arg)
?. ?=(%accepted -.update)
(pure:m !>(~))
;< =bowl:spider bind:m get-bowl:strandio
=* invite invite.update
?: =(our.bowl entity.resource.invite)
:: do not crash because that will kill the invitatory subscription
(pure:m !>(~))
;< ~ bind:m
%+ poke-our %spider
=- spider-start+!>([`tid.bowl ~ %graph-join -])
%+ slop
!> ^- action:graph-view
[%join resource.invite ship.invite]
!>(~)
(pure:m !>(~))

View File

@ -1,17 +1,16 @@
import React, { useRef, useCallback, useEffect } from "react";
import { RouteComponentProps } from "react-router-dom";
import { Col } from "@tlon/indigo-react";
import React, { useRef, useCallback, useEffect } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { Col } from '@tlon/indigo-react';
import _ from 'lodash';
import { Association } from "~/types/metadata-update";
import { StoreState } from "~/logic/store/type";
import { useFileDrag } from "~/logic/lib/useDrag";
import ChatWindow from "./components/ChatWindow";
import ChatInput from "./components/ChatInput";
import GlobalApi from "~/logic/api/global";
import { deSig } from "~/logic/lib/util";
import { SubmitDragger } from "~/views/components/s3-upload";
import { useLocalStorageState } from "~/logic/lib/useLocalStorageState";
import { Association } from '~/types/metadata-update';
import { StoreState } from '~/logic/store/type';
import { useFileDrag } from '~/logic/lib/useDrag';
import ChatWindow from './components/ChatWindow';
import ChatInput from './components/ChatInput';
import GlobalApi from '~/logic/api/global';
import { SubmitDragger } from '~/views/components/s3-upload';
import { useLocalStorageState } from '~/logic/lib/useLocalStorageState';
type ChatResourceProps = StoreState & {
association: Association;
@ -20,22 +19,22 @@ type ChatResourceProps = StoreState & {
} & RouteComponentProps;
export function ChatResource(props: ChatResourceProps) {
const station = props.association["app-path"];
const station = props.association['app-path'];
if (!props.chatInitialized) {
return null;
}
const { envelopes, config } = (props.inbox?.[station]) ? props.inbox[station] : {envelopes: [], config: {}};
const { envelopes, config } = (props.inbox?.[station]) ? props.inbox[station] : { envelopes: [], config: {} };
const { read, length } = (config) ? config : undefined;
const groupPath = props.association["group-path"];
const groupPath = props.association['group-path'];
const group = props.groups[groupPath];
const contacts = props.contacts[groupPath] || {};
const pendingMessages = (props.pendingMessages.get(station) || []).map(
(value) => ({
value => ({
...value,
pending: true,
pending: true
})
);
@ -62,7 +61,7 @@ export function ChatResource(props: ChatResourceProps) {
const unreadCount = length - read;
const unreadMsg = unreadCount > 0 && envelopes[unreadCount - 1];
const [, owner, name] = station.split("/");
const [, owner, name] = station.split('/');
const ourContact = contacts?.[window.ship];
const lastMsgNum = envelopes.length || 0;
@ -81,24 +80,24 @@ export function ChatResource(props: ChatResourceProps) {
const { bind, dragging } = useFileDrag(onFileDrag);
const [unsent, setUnsent] = useLocalStorageState<Record<string, string>>(
"chat-unsent",
'chat-unsent',
{}
);
const appendUnsent = useCallback(
(u: string) => setUnsent((s) => ({ ...s, [station]: u })),
(u: string) => setUnsent(s => ({ ...s, [station]: u })),
[station]
);
const clearUnsent = useCallback(() => setUnsent((s) => _.omit(s, station)), [
station,
const clearUnsent = useCallback(() => setUnsent(s => _.omit(s, station)), [
station
]);
const scrollTo = new URLSearchParams(location.search).get('msg');
useEffect(() => {
const clear = () => {
props.history.replace(location.pathname);
}
};
setTimeout(clear, 10000);
return clear;
}, [station]);
@ -141,7 +140,7 @@ export function ChatResource(props: ChatResourceProps) {
s3={props.s3}
hideAvatars={props.hideAvatars}
placeholder="Message..."
message={unsent[station] || ""}
message={unsent[station] || ''}
deleteMessage={clearUnsent}
/>
</Col>

View File

@ -1,13 +1,13 @@
import React, { Component } from 'react';
import ChatEditor from './chat-editor';
import { S3Upload, SubmitDragger } from '~/views/components/s3-upload' ;
import { S3Upload } from '~/views/components/s3-upload' ;
import { uxToHex } from '~/logic/lib/util';
import { Sigil } from '~/logic/lib/sigil';
import tokenizeMessage, { isUrl } from '~/logic/lib/tokenizeMessage';
import GlobalApi from '~/logic/api/global';
import { Envelope } from '~/types/chat-update';
import { Contacts, S3Configuration } from '~/types';
import { Row } from '@tlon/indigo-react';
import { Contacts } from '~/types';
import { Row, BaseImage, Box, Icon } from '@tlon/indigo-react';
interface ChatInputProps {
api: GlobalApi;
@ -31,7 +31,6 @@ interface ChatInputState {
uploadingPaste: boolean;
}
export default class ChatInput extends Component<ChatInputProps, ChatInputState> {
public s3Uploader: React.RefObject<S3Upload>;
private chatEditor: React.RefObject<ChatEditor>;
@ -42,7 +41,7 @@ export default class ChatInput extends Component<ChatInputProps, ChatInputState>
this.state = {
inCodeMode: false,
submitFocus: false,
uploadingPaste: false,
uploadingPaste: false
};
this.s3Uploader = React.createRef();
@ -50,7 +49,6 @@ export default class ChatInput extends Component<ChatInputProps, ChatInputState>
this.submit = this.submit.bind(this);
this.toggleCode = this.toggleCode.bind(this);
}
toggleCode() {
@ -82,8 +80,6 @@ export default class ChatInput extends Component<ChatInputProps, ChatInputState>
}
}
submit(text) {
const { props, state } = this;
if (state.inCodeMode) {
@ -134,7 +130,6 @@ export default class ChatInput extends Component<ChatInputProps, ChatInputState>
{ url }
);
}
}
uploadError(error) {
@ -159,10 +154,11 @@ export default class ChatInput extends Component<ChatInputProps, ChatInputState>
if (!this.readyToUpload()) {
return;
}
if (!this.s3Uploader.current || !this.s3Uploader.current.inputRef.current) return;
if (!this.s3Uploader.current || !this.s3Uploader.current.inputRef.current)
return;
this.s3Uploader.current.inputRef.current.files = files;
const fire = document.createEvent("HTMLEvents");
fire.initEvent("change", true, true);
const fire = document.createEvent('HTMLEvents');
fire.initEvent('change', true, true);
this.s3Uploader.current?.inputRef.current?.dispatchEvent(fire);
}
@ -179,7 +175,7 @@ export default class ChatInput extends Component<ChatInputProps, ChatInputState>
props.ourContact &&
((props.ourContact.avatar !== null) && !props.hideAvatars)
)
? <img src={props.ourContact.avatar} height={16} width={16} className="dib" />
? <BaseImage src={props.ourContact.avatar} height={16} width={16} className="dib" />
: <Sigil
ship={window.ship}
size={16}
@ -201,9 +197,9 @@ export default class ChatInput extends Component<ChatInputProps, ChatInputState>
className='cf'
zIndex='0'
>
<div className="pa2 flex items-center">
<Row p='2' alignItems='center'>
{avatar}
</div>
</Row>
<ChatEditor
ref={this.chatEditor}
inCodeMode={state.inCodeMode}
@ -213,12 +209,13 @@ export default class ChatInput extends Component<ChatInputProps, ChatInputState>
onPaste={this.onPaste.bind(this)}
placeholder='Message...'
/>
<div className="ml2 mr2 flex-shrink-0"
style={{
height: '16px',
width: '16px',
flexBasis: 16,
}}>
<Box
mx='2'
flexShrink='0'
height='16px'
width='16px'
flexBasis='16px'
>
<S3Upload
ref={this.s3Uploader}
configuration={props.s3.configuration}
@ -227,28 +224,25 @@ export default class ChatInput extends Component<ChatInputProps, ChatInputState>
uploadError={this.uploadError.bind(this)}
accept="*"
>
<img
className="invert-d"
src="/~landscape/img/ImageUpload.png"
<Icon icon='Links'
width="16"
height="16"
/>
</S3Upload>
</div>
<div className="mr2 flex-shrink-0" style={{
height: '16px',
width: '16px',
flexBasis: 16,
}}>
<img style={{
filter: state.inCodeMode ? 'invert(100%)' : '',
height: '14px',
width: '14px',
}}
</Box>
<Box
mr='2'
flexShrink='0'
height='16px'
width='16px'
flexBasis='16px'
>
<Icon
icon='Dojo'
onClick={this.toggleCode}
src="/~landscape/img/CodeEval.png"
className="contrast-10-d bg-white bg-none-d ba b--gray1-d br1" />
</div>
color={state.inCodeMode ? 'blue' : 'black'}
/>
</Box>
</Row>
);
}

View File

@ -295,27 +295,89 @@ export const MessageContent = ({ content, remoteContentPolicy, measure }) => {
};
export const MessagePlaceholder = ({ height, index, className = '', style = {}, ...props }) => (
<div className={`w-100 f7 pl3 pt4 pr3 cf flex lh-copy ${className}`} style={{ height, ...style }} {...props}>
<div className="fl pr3 v-top bg-white bg-gray0-d">
<span
className="db bg-gray2 bg-white-d"
<Box
width='100%'
fontSize='2'
pl='3' pt='4'
pr='3'
display='flex'
lineHeight='tall'
className={className}
style={{ height, ...style }}
{...props}
>
<Box pr='3' verticalAlign='top' backgroundColor='white' style={{ float: 'left' }}>
<Text
display='block'
background='gray'
width='24px'
height='24px'
borderRadius='50%'
style={{
width: "24px",
height: "24px",
borderRadius: "50%",
visibility: (index % 5 == 0) ? "initial" : "hidden",
}}
></span>
</div>
<div className="fr clamp-message white-d" style={{ flexGrow: 1, marginTop: -8 }}>
<div className="hide-child" style={{paddingTop: "6px", visibility: (index % 5 == 0) ? "initial" : "hidden" }}>
<p className={`v-mid f9 gray2 dib mr3 c-default`}>
<span className="mw5 db"><span className="bg-gray5 bg-gray1-d db w-100 h-100"></span></span>
</p>
<p className="v-mid mono f9 gray2 dib"><span className="bg-gray5 bg-gray1-d db w-100 h-100" style={{height: "1em", width: `${(index % 3 + 1) * 3}em`}}></span></p>
<p className="v-mid mono f9 ml2 gray2 dib child dn-s"><span className="bg-gray5 bg-gray1-d db w-100 h-100"></span></p>
</div>
<span className="bg-gray5 bg-gray1-d db w-100 h-100 db" style={{height: `1em`, width: `${(index % 5) * 20}%`}}></span>
</div>
</div>
></Text>
</Box>
<Box
style={{ float: 'right', flexGrow: 1 }}
color='black'
className="clamp-message"
>
<Box
className="hide-child"
paddingTop='4'
style={{visibility: (index % 5 == 0) ? "initial" : "hidden" }}
>
<Text
display='inline-block'
verticalAlign='middle'
fontSize='0'
gray
cursor='default'
>
<Text maxWidth='32rem' display='block'>
<Text
backgroundColor='gray'
display='block'
width='100%'
height='100%'></Text>
</Text>
</Text>
<Text
display='inline-block'
mono
verticalAlign='middle'
fontSize='0'
gray
>
<Text
background='gray'
display='block'
height='1em'
style={{ width: `${(index % 3 + 1) * 3}em` }}
></Text>
</Text>
<Text
mono
verticalAlign='middle'
fontSize='0'
ml='2'
gray
display={['none', 'inline-block']}
className="child">
<Text
backgroundColor='gray'
display='block'
width='100%'
height='100%'
></Text>
</Text>
</Box>
<Text
display='block'
backgroundColor='gray'
height='1em'
style={{ width: `${(index % 5) * 20}%` }}></Text>
</Box>
</Box>
);

View File

@ -1,22 +1,34 @@
import React, { Component } from 'react';
import React from 'react';
import { Box, LoadingSpinner, Text } from '@tlon/indigo-react';
export const BacklogElement = (props) => {
if (!props.isChatLoading) {
return null;
}
return (
<div className="center mw6 absolute z-9999" style={{ left: 0, right: 0, top: 48}}>
<div className={
"db pa3 ma3 ba b--gray4 bg-gray5 b--gray2-d bg-gray1-d " +
"white-d flex items-center"
}>
<img className="invert-d spin-active v-mid"
src="/~landscape/img/Spinner.png"
width={16}
height={16}
<Box
marginLeft='auto'
marginRight='auto'
maxWidth='32rem'
position='absolute'
zIndex='9999'
style={{ left: 0, right: 0, top: 0 }}
>
<Box
display='flex'
justifyContent='center'
p='3'
m='3'
border='1px solid'
borderColor='washedGray'
backgroundColor='white'
>
<LoadingSpinner
foreground='black'
background='gray'
/>
<p className="lh-copy db ml3">Past messages are being restored</p>
</div>
</div>
<Text display='block' ml='3' lineHeight='tall'>Past messages are being restored</Text>
</Box>
</Box>
);
}
};

View File

@ -4,6 +4,7 @@ import {
ProfileOverlay,
OVERLAY_HEIGHT
} from './profile-overlay';
import { Box, BaseImage } from '@tlon/indigo-react';
export class OverlaySigil extends PureComponent {
constructor() {
@ -58,7 +59,7 @@ export class OverlaySigil extends PureComponent {
const { hideAvatars } = props;
const img = (props.contact && (props.contact.avatar !== null) && !hideAvatars)
? <img src={props.contact.avatar} height={16} width={16} className="dib" />
? <BaseImage display='inline-block' src={props.contact.avatar} height={16} width={16} />
: <Sigil
ship={props.ship}
size={16}
@ -69,9 +70,11 @@ export class OverlaySigil extends PureComponent {
/>;
return (
<div
<Box
cursor='pointer'
position='relative'
onClick={this.profileShow}
className={props.className + ' pointer relative'}
className={props.className}
ref={this.containerRef}
>
{state.profileClicked && (
@ -91,7 +94,7 @@ export class OverlaySigil extends PureComponent {
/>
)}
{img}
</div>
</Box>
);
}
}

View File

@ -2,7 +2,7 @@ import React, { PureComponent } from 'react';
import { cite } from '~/logic/lib/util';
import { Sigil } from '~/logic/lib/sigil';
import { Box, Col, Button, Text } from "@tlon/indigo-react";
import { Box, Col, Button, Text, BaseImage } from '@tlon/indigo-react';
export const OVERLAY_HEIGHT = 250;
@ -51,8 +51,8 @@ export class ProfileOverlay extends PureComponent {
const isOwn = window.ship === ship;
let img = contact?.avatar && !hideAvatars
? <img src={contact.avatar} height={160} width={160} className="brt2 dib" />
const img = contact?.avatar && !hideAvatars
? <BaseImage display='inline-block' src={contact.avatar} height={160} width={160} className="brt2" />
: <Sigil
ship={ship}
size={160}
@ -63,7 +63,7 @@ export class ProfileOverlay extends PureComponent {
const showNickname = contact?.nickname && !hideNicknames;
// TODO: we need to rethink this "top-level profile view" of other ships
/*if (!group.hidden) {
/* if (!group.hidden) {
}*/
const isHidden = group.hidden;
@ -103,7 +103,7 @@ export class ProfileOverlay extends PureComponent {
<Button
mt='2'
width='100%'
style={{ cursor: 'pointer '}}
style={{ cursor: 'pointer ' }}
onClick={() => (isHidden) ? history.push('/~profile/identity') : history.push(`${history.location.pathname}/popover/profile`)}
>
Edit Identity

View File

@ -1,4 +1,5 @@
import React, { Component } from 'react';
import { Box, Text, Button } from '@tlon/indigo-react';
export class ResubscribeElement extends Component {
onClickResubscribe() {
@ -9,21 +10,23 @@ export class ResubscribeElement extends Component {
}
render() {
const { props } = this;
const { props } = this;
if (props.isChatUnsynced) {
return (
<div className="db pa3 ma3 ba b--yellow2 bg-yellow0">
<p className="lh-copy db">
<Box p='3' m='3' border='1px solid' borderColor='yellow' backgroundColor='lightYellow'>
<Text lineHeight='tall' display='block'>
Your ship has been disconnected from the chat's host.
This may be due to a bad connection, going offline, lack of permission,
or an over-the-air update.
</p>
<a onClick={this.onClickResubscribe.bind(this)}
className="db underline black pointer mt3"
</Text>
<Button
primary
mt='3'
onClick={this.onClickResubscribe.bind(this)}
>
Reconnect to this chat
</a>
</div>
</Button>
</Box>
);
} else {
return null;

View File

@ -95,20 +95,6 @@ h2 {
font-family: "Inter", sans-serif;
}
/* spinner */
.spin-active {
animation: spin 2s infinite;
}
@keyframes spin {
0% {transform: rotate(0deg);}
25% {transform: rotate(90deg);}
50% {transform: rotate(180deg);}
75% {transform: rotate(270deg);}
100% {transform: rotate(360deg);}
}
.embed-container iframe {
max-width: 100%;
}

View File

@ -15,18 +15,13 @@ import Groups from './components/Groups';
const ScrollbarLessBox = styled(Box)`
scrollbar-width: none !important;
::-webkit-scrollbar {
display: none;
}
`;
export default class LaunchApp extends React.Component {
componentDidMount() {
// preload spinner asset
new Image().src = '/~landscape/img/Spinner.png';
}
render() {
const { props } = this;

View File

@ -12,8 +12,7 @@ import { RouteComponentProps } from "react-router-dom";
import { LinkItem } from "./components/link-item";
import { LinkSubmit } from "./components/link-submit";
import { LinkPreview } from "./components/link-preview";
import { CommentSubmit } from "./components/comment-submit";
import { Comments } from "./components/comments";
import { Comments } from "~/views/components/comments";
import "./css/custom.css";
@ -70,9 +69,9 @@ export function LinkResource(props: LinkResourceProps) {
render={(props) => {
return (
<Col width="100%" p={4} alignItems="center" maxWidth="768px">
<Row width="100%" flexShrink='0'>
<Col width="100%" flexShrink='0'>
<LinkSubmit s3={s3} name={name} ship={ship.slice(1)} api={api} />
</Row>
</Col>
{Array.from(graph).map(([date, node]) => {
const contact = contactDetails[node.post.author];
return (
@ -123,16 +122,10 @@ export function LinkResource(props: LinkResourceProps) {
commentNumber={node.children.size}
remoteContentPolicy={remoteContentPolicy}
/>
<Row flexShrink='0'>
<CommentSubmit
name={name}
ship={ship}
api={api}
parentIndex={node.post.index}
/>
</Row>
<Comments
comments={node.children}
ship={ship}
name={name}
comments={node}
resource={resourcePath}
contacts={contactDetails}
api={api}

View File

@ -1,63 +0,0 @@
import React, { Component } from 'react';
import { Sigil } from '~/logic/lib/sigil';
import { cite } from '~/logic/lib/util';
import moment from 'moment';
import { Box, Text, Row } from '@tlon/indigo-react';
import RichText from '~/views/components/RichText';
import { MentionText } from "~/views/components/MentionText";
export const CommentItem = (props) => {
const content = props.post.contents;
const timeSent =
moment.unix(props.post['time-sent'] / 1000).format('hh:mm a');
const showAvatar = props.avatar && !props.hideAvatars;
const showNickname = props.nickname && !props.hideNicknames;
const img = showAvatar
? <img src={props.avatar} height={36} width={36} className="dib" />
: <Sigil
ship={`~${props.post.author}`}
size={36}
color={`#${props.color}`}
classes={(!!props.member ? 'mix-blend-diff' : '')}
/>;
const disabled = props.pending || props.post.author !== window.ship;
const onDelete = async () => {
const [ship,name] = props.resource.split('/');
await props.api.graph.removeNodes(`~${ship}`, name, [props.post.index]);
};
return (
<Box width="100%" py={3} opacity={props.pending ? '0.6' : '1'}>
<Row backgroundColor='white'>
{img}
<Row fontSize={0} alignItems="center" ml={2}>
<Text mono={!props.hasNickname} title={props.post.author}>
{showNickname ? props.nickname : cite(props.post.author)}
</Text>
<Text gray ml={2}>{timeSent}</Text>
{!disabled && (
<>
<Box cursor="pointer" pl="2" color="red" onClick={onDelete}>
Delete
</Box>
</>
)}
</Row>
</Row>
<Row>
<Text py={3} fontSize={1}>
<MentionText
remoteContentPolicy={props.remoteContentPolicy}
contacts={props.contacts}
content={content}
/>
</Text>
</Row>
</Box>
);
}

View File

@ -1,84 +0,0 @@
import React, { Component } from 'react';
import { Spinner } from '~/views/components/Spinner';
import { createPost } from '~/logic/api/graph';
import { deSig } from "~/logic/lib/util";
import { scanForMentions } from "~/logic/lib/graph";
export class CommentSubmit extends Component {
constructor(props) {
super(props);
this.state = {
comment: '',
commentFocus: false,
disabled: false
};
}
onClickPost() {
const parentIndex = this.props.parentIndex || '';
const content = scanForMentions(this.state.comment);
let post = createPost(content, parentIndex);
this.setState({ disabled: true }, () => {
this.props.api.graph.addPost(
`~${deSig(this.props.ship)}`,
this.props.name,
post
).then((r) => {
this.setState({
disabled: false,
comment: ''
});
});
});
}
setComment(event) {
this.setState({ comment: event.target.value });
}
render() {
const { state, props } = this;
const focus = (state.commentFocus)
? 'b--black b--white-d'
: 'b--gray4 b--gray2-d';
const activeClasses = state.comment
? 'black white-d pointer'
: 'gray2 b--gray2';
return (
<div className={'w-100 relative ba br1 mt6 mb6 ' + focus}>
<textarea
className="w-100 bg-gray0-d white-d f8 pa2 pr8"
style={{ resize: 'none', height: 75 }}
placeholder="Leave a comment on this link"
onChange={this.setComment.bind(this)}
onKeyDown={(e) => {
if (
(e.getModifierState('Control') || e.metaKey) &&
e.key === 'Enter'
) {
this.onClickPost();
}
}}
onFocus={() => this.setState({ commentFocus: true })}
onBlur={() => this.setState({ commentFocus: false })}
value={state.comment}
/>
<button
className={
'f8 bg-gray0-d ml2 absolute ' + activeClasses
}
disabled={state.disabled}
onClick={this.onClickPost.bind(this)}
style={{ bottom: 12, right: 8 }}>
Post
</button>
</div>
);
}
}

View File

@ -1,45 +0,0 @@
import React from 'react';
import { CommentItem } from './comment-item';
import { getContactDetails } from '~/logic/lib/util';
export const Comments = (props) => {
const {
hideNicknames,
hideAvatars,
remoteContentPolicy
} = props;
const contacts = props.contacts ? props.contacts : {};
return (
<div>
{ Array.from(props.comments).map(([date, comment]) => {
const { nickname, color, member, avatar } =
getContactDetails(contacts[comment.post.author]);
const nameClass = nickname && !hideNicknames ? 'inter' : 'mono';
return (
<CommentItem
api={props.api}
resource={props.resource}
key={comment.post.index}
contacts={contacts}
post={comment.post}
nickname={nickname}
hasNickname={Boolean(nickname)}
color={color}
avatar={avatar}
member={member}
hideNicknames={hideNicknames}
hideAvatars={hideAvatars}
remoteContentPolicy={remoteContentPolicy}
/>
);
})
}
</div>
);
}

View File

@ -1,17 +1,16 @@
import React from 'react';
import { Row, Col, Anchor, Box, Text } from '@tlon/indigo-react';
import { Row, Col, Anchor, Box, Text, BaseImage } from '@tlon/indigo-react';
import { Sigil } from '~/logic/lib/sigil';
import { Link } from 'react-router-dom';
import { cite } from '~/logic/lib/util';
import { roleForShip } from "~/logic/lib/group";
import { roleForShip } from '~/logic/lib/group';
export const LinkItem = (props) => {
const {
node,
nickname,
color,
avatar,
resource,
hideAvatars,
@ -34,13 +33,13 @@ export const LinkItem = (props) => {
const showNickname = nickname && !hideNicknames;
const img = showAvatar
? <img src={props.avatar} height={36} width={36} className="dib" />
? <BaseImage display='inline-block' src={props.avatar} height={36} width={36} />
: <Sigil ship={`~${author}`} size={36} color={'#' + props.color} />;
const baseUrl = props.baseUrl || `/~404/${resource}`;
const ourRole = group ? roleForShip(group, window.ship) : undefined;
const [ship, name] = resource.split("/");
const [ship, name] = resource.split('/');
return (
<Row minWidth='0' flexShrink='0' width="100%" alignItems="center" py={3} bg="white">
@ -53,19 +52,21 @@ export const LinkItem = (props) => {
href={contents[1].url}
width="100%"
target="_blank"
rel="noopener noreferrer">
<Text display='inline-block' overflow='hidden' style={{ textOverflow: 'ellipsis', whiteSpace: 'pre'}}> {contents[0].text}</Text>
rel="noopener noreferrer"
>
<Text display='inline-block' overflow='hidden' style={{ textOverflow: 'ellipsis', whiteSpace: 'pre' }}>{contents[0].text}</Text>
<Text ml="2" color="gray" display='inline-block' flexShrink='0'>{hostname} </Text>
</Anchor>
<Box width="100%">
<Text
fontFamily={showNickname ? 'sans' : 'mono'} pr={2}>
fontFamily={showNickname ? 'sans' : 'mono'} pr={2}
>
{showNickname ? nickname : cite(author) }
</Text>
<Link to={`${baseUrl}/${index}`}>
<Text color="gray">{size} comments</Text>
</Link>
{(ourRole === "admin" || node.post.author === window.ship)
{(ourRole === 'admin' || node.post.author === window.ship)
&& (<Text color='red' ml='2' cursor='pointer' onClick={() => api.graph.removeNodes(`~${ship}`, name, [node.post.index])}>Delete</Text>)}
</Box>
</Col>

View File

@ -1,18 +1,17 @@
import React from 'react';
import { cite } from '~/logic/lib/util';
import RemoteContent from "~/views/components/RemoteContent";
import RemoteContent from '~/views/components/RemoteContent';
import { Box, Col, Anchor, Text } from '@tlon/indigo-react';
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;
@ -32,29 +31,36 @@ export const LinkPreview = (props) => {
);
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"
target="_blank"
rel="noopener noreferrer">
<p className="f8 truncate">{title}</p>
<span className="gray2 ml2 f8 dib v-btm flex-shrink-0" style={{ whiteSpace: 'nowrap' }}>
{hostname}
</span>
</a>
<div className="w-100 pt1">
<span className={'f9 pr2 white-d dib ' + nameClass} title={author}>
<Box pb='6' width='100%'>
<Box width='100%' textAlign='center'>{embed}</Box>
<Col flex='1 1 auto' minWidth='0' minHeight='0' pt='6'>
<Anchor href={url}
lineHeight="tall"
display='flex'
style={{ textDecoration: 'none' }}
width='100%'
target="_blank"
rel="noopener noreferrer">
<Text
display='inline-block'
overflow='hidden'
style={{ textOverflow: 'ellipsis', whiteSpace: 'pre' }}
>
{title}
</Text>
<Text ml="2" color="gray" display='inline-block' flexShrink='0'>{hostname} </Text>
</Anchor>
<Box width='100%' pt='1'>
<Text fontSize='0' pr='2' display='inline-block' mono={!showNickname} title={author}>
{showNickname ? props.nickname : cite(`~${author}`)}
</span>
<span className="f9 inter gray2 pr3 dib">{timeSent}</span>
<span className="f9 inter gray2 dib">
</Text>
<Text fontSize='0' gray pr='3' display='inline-block'>{timeSent}</Text>
<Text gray fontSize='0' display='inline-block'>
{props.commentNumber} comments
</span>
</div>
</div>
</div>
</Text>
</Box>
</Col>
</Box>
);
}
};

View File

@ -2,14 +2,12 @@ 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 { Box, Text, BaseInput, Button } 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;
@ -58,7 +56,7 @@ export class LinkSubmit extends Component<LinkSubmitProps, LinkSubmitState> {
this.setState({ disabled: true });
const parentIndex = this.props.parentIndex || '';
let post = createPost([
const post = createPost([
{ text: title },
{ url: link }
], parentIndex);
@ -80,7 +78,7 @@ export class LinkSubmit extends Component<LinkSubmitProps, LinkSubmitState> {
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);
@ -101,7 +99,7 @@ export class LinkSubmit extends Component<LinkSubmitProps, LinkSubmitState> {
if (result.title && !this.state.linkTitle) {
this.setState({ linkTitle: result.title });
}
}).catch((error) => {/*noop*/});
}).catch((error) => { /* noop*/ });
} else if (!this.state.linkTitle) {
this.setState({
linkTitle: decodeURIComponent(linkValue
@ -113,7 +111,7 @@ export class LinkSubmit extends Component<LinkSubmitProps, LinkSubmitState> {
.replace('_', ' ')
.replace(/\d{4}\.\d{1,2}\.\d{2}\.\.\d{2}\.\d{2}\.\d{2}-/, '')
)
})
});
}
}
}
@ -170,19 +168,12 @@ export class LinkSubmit extends Component<LinkSubmitProps, LinkSubmitState> {
return;
}
this.s3Uploader.current.inputRef.current.files = files;
const fire = document.createEvent("HTMLEvents");
fire.initEvent("change", true, true);
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 &&
@ -190,44 +181,57 @@ export class LinkSubmit extends Component<LinkSubmitProps, LinkSubmitState> {
);
return (
<div
className={`flex-shrink-0 relative ba br1 w-100 mb6 ${focus}`}
<>
<Box
flexShrink='0'
position='relative'
border='1px solid'
borderColor={this.state.submitFocus ? 'black' : 'washedGray'}
width='100%'
borderRadius='2'
onDragEnter={this.onDragEnter.bind(this)}
onDragOver={e => {
onDragOver={(e) => {
e.preventDefault();
if (isS3Ready) {
this.setState({ dragover: true})
this.setState({ dragover: true });
}
}}
onDragLeave={() => this.setState({ dragover: false })}
onDrop={this.onDrop}
>
{this.state.dragover ? <SubmitDragger /> : null}
<div className="relative">
<Box position='relative'>
{
( this.state.linkValue ||
this.state.urlFocus ||
this.state.disabled
) ? null : (
isS3Ready ? (
<span className="gray2 absolute pl2 pt3 pb2 f8"
style={{pointerEvents: 'none'}}>
<Text gray position='absolute'
pl='2' pt='2'
pb='2' fontSize='0'
style={{ pointerEvents: 'none' }}
>
Drop or
<span className="pointer green2"
style={{pointerEvents: 'all'}}
<Text cursor='pointer' color='blue'
style={{ pointerEvents: 'all' }}
onClick={(event) => {
if (!this.readyToUpload()) {
return;
}
this.s3Uploader.current.inputRef.current.click();
}}> upload </span>
}}
> upload </Text>
a file, or paste a link here
</span>
</Text>
) : (
<span className="gray2 absolute pl2 pt3 pb2 f8"
style={{pointerEvents: 'none'}}>
<Text gray position='absolute'
pl='2' pt='2'
pb='2' fontSize='0'
style={{ pointerEvents: 'none' }}
>
Paste a link here
</span>
</Text>
)
)
}
@ -238,10 +242,16 @@ export class LinkSubmit extends Component<LinkSubmitProps, LinkSubmitState> {
uploadSuccess={this.uploadSuccess.bind(this)}
uploadError={this.uploadError.bind(this)}
className="dn absolute pt3 pb2 pl2 w-100"
></S3Upload> : null}
<input
></S3Upload> : null}
<BaseInput
type="url"
className="pl2 w-100 f8 pt3 pb2 white-d bg-transparent"
pl='2'
width='100%'
fontSize='0'
pt='2'
pb='2'
color='black'
backgroundColor='transparent'
onChange={this.setLinkValue}
onBlur={() => this.setState({ submitFocus: false, urlFocus: false })}
onFocus={() => this.setState({ submitFocus: true, urlFocus: true })}
@ -255,10 +265,15 @@ export class LinkSubmit extends Component<LinkSubmitProps, LinkSubmitState> {
}}
value={this.state.linkValue}
/>
</div>
<input
</Box>
<BaseInput
pl='2'
backgroundColor='transparent'
width='100%'
fontSize='0'
color='black'
type="text"
className="pl2 bg-transparent w-100 f8 white-d linkTitle"
className="linkTitle"
style={{
resize: 'none',
height: 40
@ -276,23 +291,18 @@ export class LinkSubmit extends Component<LinkSubmitProps, LinkSubmitState> {
}}
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
}}
>
</Box>
<Box mt='2' mb='4'>
<Button
flexShrink='0'
primary
disabled={!this.state.linkValid || this.state.disabled}
onClick={this.onClickPost.bind(this)}
>
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>
</Button>
</Box>
</>
) ;
}
}

View File

@ -32,11 +32,13 @@ export function Author(props: AuthorProps) {
{showImage && (
<Box>
{showAvatar ? (
<img src={contact?.avatar} height={24} width={24} className="dib" />
<img src={contact?.avatar} height={16} width={16} className="dib" />
) : (
<Sigil
ship={ship}
size={24}
size={16}
icon
padded
color={color}
classes={contact?.color ? '' : "mix-blend-diff"}
/>
@ -45,8 +47,10 @@ export function Author(props: AuthorProps) {
)}
<Box
ml={showImage ? 2 : 0}
color="gray"
color="black"
lineHeight='tall'
fontFamily={showNickname ? "sans" : "mono"}
fontWeight={showNickname ? '500' : '400'}
>
{name}
</Box>

View File

@ -5,7 +5,7 @@ import bigInt from 'big-integer';
import { Link, RouteComponentProps } from "react-router-dom";
import { Spinner } from "~/views/components/Spinner";
import { Comments } from "./Comments";
import { Comments } from "~/views/components/Comments";
import { NoteNavigation } from "./NoteNavigation";
import GlobalApi from "~/logic/api/global";
import { getLatestRevision, getComments } from '~/logic/lib/publish';
@ -34,13 +34,13 @@ export function Note(props: NoteProps & RouteComponentProps) {
const deletePost = async () => {
setDeleting(true);
const indices = [note.post.index]
await api.graph.removeNodes(ship, book, indices);
await api.graph.removeNodes(ship, book, indices);
props.history.push(rootUrl);
};
const comments = getComments(note);
const [revNum, title, body, post] = getLatestRevision(note);
const noteId = bigInt(note.post.index.split('/')[1]);
let adminLinks: JSX.Element | null = null;
@ -106,8 +106,7 @@ export function Note(props: NoteProps & RouteComponentProps) {
/>
<Comments
ship={ship}
book={props.book}
note={props.note}
name={props.book}
comments={comments}
contacts={props.contacts}
api={props.api}

View File

@ -1,7 +1,7 @@
import React from "react";
import * as Yup from "yup";
import { Formik, FormikHelpers, Form, useFormikContext } from "formik";
import { AsyncButton } from "../../../components/AsyncButton";
import { AsyncButton } from "./AsyncButton";
import { ManagedTextAreaField as TextArea } from "@tlon/indigo-react";
interface FormSchema {

View File

@ -1,27 +1,24 @@
import React, { useState } from "react";
import CommentInput from "./CommentInput";
import { Comment, NoteId } from "~/types/publish-update";
import { Contacts } from "~/types/contact-update";
import GlobalApi from "~/logic/api/global";
import { Box, Row } from "@tlon/indigo-react";
import styled from "styled-components";
import { Author } from "./Author";
import { GraphNode, TextContent } from "~/types/graph-update";
import tokenizeMessage from "~/logic/lib/tokenizeMessage";
import RichText from "~/views/components/RichText";
import { LocalUpdateRemoteContentPolicy } from "~/types";
import { MentionText } from "~/views/components/MentionText";
import React from 'react';
import { Contacts } from '~/types/contact-update';
import GlobalApi from '~/logic/api/global';
import { Box, Row } from '@tlon/indigo-react';
import styled from 'styled-components';
import { Author } from '~/views/apps/publish/components/Author';
import { GraphNode, TextContent } from '~/types/graph-update';
import tokenizeMessage from '~/logic/lib/tokenizeMessage';
import { LocalUpdateRemoteContentPolicy } from '~/types';
import { MentionText } from '~/views/components/MentionText';
const ClickBox = styled(Box)`
cursor: pointer;
padding-left: ${(p) => p.theme.space[2]}px;
padding-left: ${p => p.theme.space[2]}px;
`;
interface CommentItemProps {
pending?: boolean;
comment: GraphNode;
contacts: Contacts;
book: string;
name: string;
ship: string;
api: GlobalApi;
hideNicknames: boolean;
@ -30,24 +27,24 @@ interface CommentItemProps {
}
export function CommentItem(props: CommentItemProps) {
const { ship, contacts, book, api, remoteContentPolicy } = props;
const { ship, contacts, name, api, remoteContentPolicy } = props;
const commentData = props.comment?.post;
const comment = commentData.contents;
const disabled = props.pending || window.ship !== commentData.author;
const onDelete = async () => {
await api.graph.removeNodes(ship, book, [commentData?.index]);
await api.graph.removeNodes(ship, name, [commentData?.index]);
};
return (
<Box mb={4} opacity={commentData?.pending ? "60%" : "100%"}>
<Box mb={4} opacity={commentData?.pending ? '60%' : '100%'}>
<Row bg="white" my={3}>
<Author
showImage
contacts={contacts}
ship={commentData?.author}
date={commentData?.["time-sent"]}
date={commentData?.['time-sent']}
hideAvatars={props.hideAvatars}
hideNicknames={props.hideNicknames}
>

View File

@ -1,22 +1,18 @@
import React, { useState, useEffect, useCallback } from "react";
import { Col } from "@tlon/indigo-react";
import { CommentItem } from "./CommentItem";
import CommentInput from "./CommentInput";
import { dateToDa } from "~/logic/lib/util";
import { Comment, Note, NoteId } from "~/types/publish-update";
import { Contacts } from "~/types/contact-update";
import _ from "lodash";
import GlobalApi from "~/logic/api/global";
import { FormikHelpers } from "formik";
import {GraphNode, Graph} from "~/types/graph-update";
import {createPost} from "~/logic/api/graph";
import { LocalUpdateRemoteContentPolicy } from "~/types";
import {scanForMentions} from "~/logic/lib/graph";
import React from 'react';
import { Col } from '@tlon/indigo-react';
import { CommentItem } from './CommentItem';
import CommentInput from './CommentInput';
import { Contacts } from '~/types/contact-update';
import GlobalApi from '~/logic/api/global';
import { FormikHelpers } from 'formik';
import { GraphNode } from '~/types/graph-update';
import { createPost } from '~/logic/api/graph';
import { LocalUpdateRemoteContentPolicy } from '~/types';
import { scanForMentions } from '~/logic/lib/graph';
interface CommentsProps {
comments: GraphNode;
book: string;
note: GraphNode;
name: string;
ship: string;
contacts: Contacts;
api: GlobalApi;
@ -26,16 +22,16 @@ interface CommentsProps {
}
export function Comments(props: CommentsProps) {
const { comments, ship, book, note, api } = props;
const { comments, ship, name, api } = props;
const onSubmit = async (
{ comment },
actions: FormikHelpers<{ comment: string }>
) => {
try {
const content = scanForMentions(comment)
const content = scanForMentions(comment);
const post = createPost(content, comments?.post?.index);
await api.graph.addPost(ship, book, post)
await api.graph.addPost(ship, name, post);
actions.resetForm();
actions.setStatus({ success: null });
} catch (e) {
@ -53,7 +49,7 @@ export function Comments(props: CommentsProps) {
key={idx.toString()}
contacts={props.contacts}
api={api}
book={book}
name={name}
ship={ship}
hideNicknames={props.hideNicknames}
hideAvatars={props.hideAvatars}

View File

@ -1,18 +1,19 @@
import React, { Component } from 'react';
import React from 'react';
import { LoadingSpinner, Text } from '@tlon/indigo-react';
const Spinner = ({
classes = '',
text = '',
awaiting = false
}) => awaiting ? (
<div className={classes + ' z-2 bg-white bg-gray0-d white-d flex'}>
<img className="invert-d spin-active v-mid"
src="/~landscape/img/Spinner.png"
width={16}
height={16}
<Text zIndex='2' display='flex' className={classes}>
<LoadingSpinner
foreground='black'
background='gray'
style={{ flexShrink: 0 }}
/>
<p className="dib f9 ml2 v-mid inter">{text}</p>
</div>
<Text display='inline-block' ml='2' verticalAlign='middle' flexShrink='0'>{text}</Text>
</Text>
) : null;
export { Spinner as default, Spinner };
export { Spinner as default, Spinner };

View File

@ -1,8 +1,7 @@
import React, { Component } from 'react'
import { BaseInput, Box, Text, Icon } from "@tlon/indigo-react";
import { BaseInput, Box, Text, Icon, LoadingSpinner } from "@tlon/indigo-react";
import S3Client from '~/logic/lib/s3';
import { Spinner } from './Spinner';
import { S3Credentials, S3Configuration } from '~/types';
import { dateToDa, deSig } from '~/logic/lib/util';
@ -148,7 +147,7 @@ export class S3Upload extends Component<S3UploadProps, S3UploadState> {
accept={accept}
onChange={this.onChange.bind(this)} />
{this.state.isUploading
? <Spinner awaiting={true} classes={className} />
? <LoadingSpinner background="gray" foreground="black" />
: <Text cursor='pointer' className={className} onClick={this.onClick.bind(this)}>{display}</Text>
}
</>

View File

@ -23,11 +23,9 @@ type LandscapeProps = StoreState & {
export default class Landscape extends Component<LandscapeProps, {}> {
componentDidMount() {
document.title = 'OS1 - Landscape';
// preload spinner asset
new Image().src = '/~landscape/img/Spinner.png';
this.props.subscription.startApp('groups')
this.props.subscription.startApp('chat')
this.props.subscription.startApp('groups');
this.props.subscription.startApp('chat');
this.props.subscription.startApp('graph');
}