chat: adding localstorage sync and cleanup

This commit is contained in:
Hunter Miller 2021-07-01 11:58:21 -05:00
parent a00ab9e347
commit fa5e974ad4
4 changed files with 47 additions and 57 deletions

View File

@ -4,6 +4,9 @@ module.exports = {
'jest': true 'jest': true
}, },
rules: { rules: {
// Because we use styled system, and use
// the convention of each prop on a new line
// we probably shouldn't keep this on
'max-lines-per-function': ['off', {}] 'max-lines-per-function': ['off', {}]
} }
}; };

View File

@ -113,7 +113,6 @@ interface ChatEditorProps {
inCodeMode: boolean; inCodeMode: boolean;
placeholder: string; placeholder: string;
submit: (message: string) => void; submit: (message: string) => void;
onUnmount: (message: string) => void;
onPaste: (codemirrorInstance, event: ClipboardEvent) => void; onPaste: (codemirrorInstance, event: ClipboardEvent) => void;
} }

View File

@ -5,18 +5,16 @@ import tokenizeMessage from '~/logic/lib/tokenizeMessage';
import useStorage, { IuseStorage } from '~/logic/lib/useStorage'; import useStorage, { IuseStorage } from '~/logic/lib/useStorage';
import { MOBILE_BROWSER_REGEX } from '~/logic/lib/util'; import { MOBILE_BROWSER_REGEX } from '~/logic/lib/util';
import { withLocalState } from '~/logic/state/local'; import { withLocalState } from '~/logic/state/local';
import withStorage from '~/views/components/withStorage';
import ChatEditor, { CodeMirrorShim } from './ChatEditor'; import ChatEditor, { CodeMirrorShim } from './ChatEditor';
import airlock from '~/logic/api'; import airlock from '~/logic/api';
import { ChatAvatar } from './ChatAvatar'; import { ChatAvatar } from './ChatAvatar';
import { useChatStore } from './ChatPane'; import { useChatStore } from './ChatPane';
import { useImperativeHandle } from 'react';
type ChatInputProps = PropsWithChildren<IuseStorage & { type ChatInputProps = PropsWithChildren<IuseStorage & {
hideAvatars: boolean; hideAvatars: boolean;
ourContact?: Contact; ourContact?: Contact;
placeholder: string; placeholder: string;
onUnmount(msg: string): void;
deleteMessage(): void;
onSubmit: (contents: Content[]) => void; onSubmit: (contents: Content[]) => void;
}>; }>;
@ -69,8 +67,9 @@ const MobileSubmitButton = ({ enabled, onSubmit }) => (
</Box> </Box>
); );
export function ChatInput({ ourContact, hideAvatars, placeholder, onSubmit, onUnmount }: ChatInputProps) { export const ChatInput = React.forwardRef(({ ourContact, hideAvatars, placeholder, onSubmit }: ChatInputProps, ref) => {
const chatEditor = useRef<CodeMirrorShim>(null); const chatEditor = useRef<CodeMirrorShim>(null);
useImperativeHandle(ref, () => ({ uploadFiles }));
const { const {
message, message,
setMessage setMessage
@ -104,15 +103,6 @@ export function ChatInput({ ourContact, hideAvatars, placeholder, onSubmit, onUn
chatEditor.current.focus(); chatEditor.current.focus();
} }
function uploadSuccess(url: string) {
if (uploadingPaste) {
chatEditor.current.setValue(url);
setUploadingPaste(false);
} else {
onSubmit([{ url }]);
}
}
function onPaste(codemirrorInstance, event: React.ClipboardEvent<HTMLTextAreaElement>) { function onPaste(codemirrorInstance, event: React.ClipboardEvent<HTMLTextAreaElement>) {
if (!event.clipboardData || !event.clipboardData.files.length) { if (!event.clipboardData || !event.clipboardData.files.length) {
return; return;
@ -130,11 +120,24 @@ export function ChatInput({ ourContact, hideAvatars, placeholder, onSubmit, onUn
} }
Array.from(files).forEach((file) => { Array.from(files).forEach((file) => {
uploadDefault(file) uploadDefault(file)
.then(this.uploadSuccess) .then(uploadSuccess)
.catch(this.uploadError); .catch(uploadError);
}); });
} }
function uploadSuccess(url: string) {
if (uploadingPaste) {
chatEditor.current.setValue(url);
setUploadingPaste(false);
} else {
onSubmit([{ url }]);
}
}
function uploadError(error: Error) {
console.log(error);
}
return ( return (
<InputBox> <InputBox>
<Row p='12px 4px 12px 12px' flexShrink={0} alignItems='center'> <Row p='12px 4px 12px 12px' flexShrink={0} alignItems='center'>
@ -144,7 +147,6 @@ export function ChatInput({ ourContact, hideAvatars, placeholder, onSubmit, onUn
ref={chatEditor} ref={chatEditor}
inCodeMode={inCodeMode} inCodeMode={inCodeMode}
submit={submit} submit={submit}
onUnmount={onUnmount}
onPaste={onPaste} onPaste={onPaste}
placeholder={placeholder} placeholder={placeholder}
/> />
@ -181,11 +183,10 @@ export function ChatInput({ ourContact, hideAvatars, placeholder, onSubmit, onUn
)} )}
</InputBox> </InputBox>
); );
} });
// @ts-ignore withLocalState prop passing weirdness // @ts-ignore withLocalState prop passing weirdness
export default withLocalState<Omit<ChatInputProps, keyof IuseStorage>, 'hideAvatars', ChatInput>( export default withLocalState<Omit<ChatInputProps, keyof IuseStorage>, 'hideAvatars', ChatInput>(
// @ts-ignore withLocalState prop passing weirdness ChatInput,
withStorage<ChatInputProps, ChatInput>(ChatInput, { accept: 'image/*' }),
['hideAvatars'] ['hideAvatars']
); );

View File

@ -1,39 +1,40 @@
import { Col } from '@tlon/indigo-react'; import { Col } from '@tlon/indigo-react';
import { Content, Graph, Post } from '@urbit/api'; import { Content, Graph, Post } from '@urbit/api';
import bigInt, { BigInteger } from 'big-integer'; import bigInt, { BigInteger } from 'big-integer';
import _ from 'lodash';
import React, { ReactElement, useCallback, useEffect, useRef, useState } from 'react'; import React, { ReactElement, useCallback, useEffect, useRef, useState } from 'react';
import create from 'zustand'; import create from 'zustand';
import { useFileDrag } from '~/logic/lib/useDrag'; import { useFileDrag } from '~/logic/lib/useDrag';
import { useLocalStorageState } from '~/logic/lib/useLocalStorageState'; import { retrieve } from '~/logic/lib/useLocalStorageState';
import { useOurContact } from '~/logic/state/contact'; import { useOurContact } from '~/logic/state/contact';
import { useGraphTimesent } from '~/logic/state/graph'; import { useGraphTimesent } from '~/logic/state/graph';
import ShareProfile from '~/views/apps/chat/components/ShareProfile'; import ShareProfile from '~/views/apps/chat/components/ShareProfile';
import { Loading } from '~/views/components/Loading'; import { Loading } from '~/views/components/Loading';
import SubmitDragger from '~/views/components/SubmitDragger'; import SubmitDragger from '~/views/components/SubmitDragger';
import ChatInput, { ChatInput as NakedChatInput } from './ChatInput'; import ChatInput from './ChatInput';
import ChatWindow from './ChatWindow'; import ChatWindow from './ChatWindow';
interface useChatStoreType { interface useChatStoreType {
id: string;
message: string; message: string;
messageStore: Record<string, string>;
setMessage: (message: string) => void; setMessage: (message: string) => void;
} }
export const useChatStore = create<useChatStoreType>(set => ({ const unsentKey = 'chat-unsent';
export const useChatStore = create<useChatStoreType>((set, get) => ({
id: '',
message: '', message: '',
setMessage: (message: string) => set({ message }) messageStore: retrieve(unsentKey, {}),
setMessage: (message: string) => {
const store = get().messageStore;
store[get().id] = message;
localStorage.setItem(unsentKey, JSON.stringify(store));
set({ message });
}
})); }));
// const unsentKey = 'chat-unsent';
// export const useChatStore = create<useChatStoreType>(set => ({
// message: retrieve(unsentKey, ''),
// setMessage: (message: string) => {
// set({ message });
// localStorage.setItem(unsentKey, message);
// }
// }));
interface ChatPaneProps { interface ChatPaneProps {
/** /**
* A key to uniquely identify a ChatPane instance. Should be either the * A key to uniquely identify a ChatPane instance. Should be either the
@ -100,7 +101,7 @@ export function ChatPane(props: ChatPaneProps): ReactElement {
} = props; } = props;
const graphTimesentMap = useGraphTimesent(id); const graphTimesentMap = useGraphTimesent(id);
const ourContact = useOurContact(); const ourContact = useOurContact();
const chatInput = useRef<NakedChatInput>(); const chatInput = useRef<{ uploadFiles: (files: FileList | File[]) => void }>();
const setMessage = useChatStore(s => s.setMessage); const setMessage = useChatStore(s => s.setMessage);
const onFileDrag = useCallback( const onFileDrag = useCallback(
@ -108,29 +109,19 @@ export function ChatPane(props: ChatPaneProps): ReactElement {
if (!chatInput.current) { if (!chatInput.current) {
return; return;
} }
(chatInput.current as NakedChatInput)?.uploadFiles(files); chatInput.current?.uploadFiles(files);
}, },
[chatInput] [chatInput]
); );
const { bind, dragging } = useFileDrag(onFileDrag); const { bind, dragging } = useFileDrag(onFileDrag);
const [, setUnsent] = useLocalStorageState<Record<string, string>>( useEffect(() => {
'chat-unsent', const messageStore = retrieve(unsentKey, {});
{} useChatStore.setState({
); id,
messageStore,
const appendUnsent = useCallback( message: messageStore[id] || ''
(u: string) => setUnsent(s => ({ ...s, [id]: u })),
[id]
);
const clearUnsent = useCallback(() => {
setUnsent((s) => {
if (id in s) {
return _.omit(s, id);
}
return s;
}); });
}, [id]); }, [id]);
@ -145,8 +136,6 @@ export function ChatPane(props: ChatPaneProps): ReactElement {
const onReply = useCallback( const onReply = useCallback(
(msg: Post) => { (msg: Post) => {
const message = props.onReply(msg); const message = props.onReply(msg);
console.log(message);
// setUnsent(s => ({ ...s, [id]: message }));
setMessage(message); setMessage(message);
}, },
[id, props.onReply] [id, props.onReply]
@ -185,9 +174,7 @@ export function ChatPane(props: ChatPaneProps): ReactElement {
ref={chatInput} ref={chatInput}
onSubmit={onSubmit} onSubmit={onSubmit}
ourContact={(promptShare.length === 0 && ourContact) || undefined} ourContact={(promptShare.length === 0 && ourContact) || undefined}
onUnmount={appendUnsent}
placeholder="Message..." placeholder="Message..."
deleteMessage={clearUnsent}
/> />
)} )}
</Col> </Col>