chat: migrate to indigo-react

Fixes urbit/landscape#41.
This commit is contained in:
Matilde Park 2020-11-09 21:13:17 -05:00
parent 0934c52e83
commit e9c129b1e3
8 changed files with 185 additions and 126 deletions

View File

@ -1,17 +1,16 @@
import React, { useRef, useCallback } from "react";
import { RouteComponentProps } from "react-router-dom";
import { Col } from "@tlon/indigo-react";
import React, { useRef, useCallback } 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,17 +80,17 @@ 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
]);
return (
@ -131,7 +130,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

@ -289,27 +289,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

@ -91,20 +91,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%;
}