chat: address design critique

This commit is contained in:
Liam Fitzgerald 2020-10-02 16:31:05 +10:00
parent 3b38a22bc9
commit cc49b6cee3
9 changed files with 156 additions and 53 deletions

View File

@ -0,0 +1,64 @@
import { useState, useCallback, useMemo, useEffect } from "react";
function validateDragEvent(e: DragEvent): FileList | null {
const files = e.dataTransfer?.files;
console.log(files);
if(!files?.length) {
return null;
}
return files || null;
}
export function useFileDrag(dragged: (f: FileList) => void) {
const [dragging, setDragging] = useState(false);
const onDragEnter = useCallback(
(e: DragEvent) => {
if (!validateDragEvent(e)) {
return;
}
setDragging(true);
},
[setDragging]
);
const onDrop = useCallback(
(e: DragEvent) => {
setDragging(false);
e.preventDefault();
const files = validateDragEvent(e);
if (!files) {
return;
}
dragged(files);
},
[setDragging, dragged]
);
const onDragOver = useCallback(
(e: DragEvent) => {
e.preventDefault();
setDragging(true);
},
[setDragging]
);
const onDragLeave = useCallback(
(e: DragEvent) => {
const over = document.elementFromPoint(e.clientX, e.clientY);
if (!over || !(e.currentTarget as any)?.contains(over)) {
setDragging(false);
}
},
[setDragging]
);
const bind = {
onDragLeave,
onDragOver,
onDrop,
onDragEnter,
};
return { bind, dragging };
}

View File

@ -100,5 +100,6 @@ export default class ChatReducer<S extends ChatState> {
mailbox.splice(index, 1);
}
}
state.pendingMessages.set(msg.path, mailbox);
}
}

View File

@ -1,13 +1,16 @@
import React from "react";
import React, { useRef, useCallback } from "react";
import { RouteComponentProps } from "react-router-dom";
import { Col } from "@tlon/indigo-react";
import { Association } from "~/types/metadata-update";
import { StoreState } from "~/logic/store/type";
import { useFileDrag } from "~/logic/lib/useDrag";
import ChatWindow from "./components/lib/ChatWindow";
import ChatInput from "./components/lib/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";
type ChatResourceProps = StoreState & {
association: Association;
@ -28,7 +31,7 @@ export function ChatResource(props: ChatResourceProps) {
const group = props.groups[groupPath];
const contacts = props.contacts[groupPath] || {};
const pendingMessages = (props.pendingMessages.get(props.station) || []).map(
const pendingMessages = (props.pendingMessages.get(station) || []).map(
(value) => ({
...value,
pending: true,
@ -36,37 +39,68 @@ export function ChatResource(props: ChatResourceProps) {
);
const isChatMissing =
props.chatInitialized &&
!(station in props.inbox) &&
props.chatSynced &&
!(station in props.chatSynced) || false;
(props.chatInitialized &&
!(station in props.inbox) &&
props.chatSynced &&
!(station in props.chatSynced)) ||
false;
const isChatLoading =
props.chatInitialized &&
!(station in props.inbox) &&
props.chatSynced &&
station in props.chatSynced || false;
(props.chatInitialized &&
!(station in props.inbox) &&
props.chatSynced &&
station in props.chatSynced) ||
false;
const isChatUnsynced =
props.chatSynced && !(station in props.chatSynced) && envelopes.length > 0 || false;
(props.chatSynced &&
!(station in props.chatSynced) &&
envelopes.length > 0) ||
false;
const unreadCount = length - read;
const unreadMsg = unreadCount > 0 && envelopes[unreadCount - 1];
const [, owner, name] = station.split("/");
const ownerContact = contacts?.[deSig(owner)];
const lastMsgNum = 0;
const ourContact = contacts?.[window.ship];
const lastMsgNum = envelopes.length || 0;
const chatInput = useRef<ChatInput>();
const onFileDrag = useCallback(
(files: FileList) => {
if (!chatInput.current) {
return;
}
chatInput.current?.uploadFiles(files);
},
[chatInput?.current]
);
const { bind, dragging } = useFileDrag(onFileDrag);
const [unsent, setUnsent] = useLocalStorageState<Record<string, string>>(
"chat-unsent",
{}
);
const appendUnsent = useCallback(
(u: string) => setUnsent((s) => ({ ...s, [station]: u })),
[station]
);
const clearUnsent = useCallback(() => setUnsent((s) => _.omit(s, station)), [
station,
]);
return (
<Col height="100%" overflow="hidden" position="relative">
<Col {...bind} height="100%" overflow="hidden" position="relative">
{dragging && <SubmitDragger />}
<ChatWindow
remoteContentPolicy={props.remoteContentPolicy}
mailboxSize={length}
match={props.match as any}
stationPendingMessages={[]}
stationPendingMessages={pendingMessages}
history={props.history}
isChatMissing={isChatMissing}
isChatLoading={isChatLoading}
@ -85,23 +119,19 @@ export function ChatResource(props: ChatResourceProps) {
location={props.location}
/>
<ChatInput
ref={chatInput}
api={props.api}
numMsgs={lastMsgNum}
station={station}
owner={deSig(owner)}
ownerContact={ownerContact}
ourContact={ourContact}
envelopes={envelopes || []}
contacts={contacts}
onUnmount={(msg: string) => {
/*this.setState({
messages: this.state.messages.set(props.station, msg),
}) */
}}
onUnmount={appendUnsent}
s3={props.s3}
hideAvatars={props.hideAvatars}
placeholder="Message..."
message={"" || ""}
deleteMessage={() => {}}
message={unsent[station] || ""}
deleteMessage={clearUnsent}
/>
</Col>
);

View File

@ -1,7 +1,6 @@
import React, { Component } from 'react';
import ChatEditor from './chat-editor';
import { S3Upload, SubmitDragger } from '~/views/components/s3-upload'
;
import { S3Upload, SubmitDragger } from '~/views/components/s3-upload' ;
import { uxToHex } from '~/logic/lib/util';
import { Sigil } from '~/logic/lib/sigil';
import tokenizeMessage, { isUrl } from '~/logic/lib/tokenizeMessage';
@ -13,8 +12,7 @@ interface ChatInputProps {
api: GlobalApi;
numMsgs: number;
station: any;
owner: string;
ownerContact: any;
ourContact: any;
envelopes: Envelope[];
contacts: Contacts;
onUnmount(msg: string): void;
@ -173,17 +171,17 @@ export default class ChatInput extends Component<ChatInputProps, ChatInputState>
render() {
const { props, state } = this;
const color = props.ownerContact
? uxToHex(props.ownerContact.color) : '000000';
const color = props.ourContact
? uxToHex(props.ourContact.color) : '000000';
const sigilClass = props.ownerContact
const sigilClass = props.ourContact
? '' : 'mix-blend-diff';
const avatar = (
props.ownerContact &&
((props.ownerContact.avatar !== null) && !props.hideAvatars)
props.ourContact &&
((props.ourContact.avatar !== null) && !props.hideAvatars)
)
? <img src={props.ownerContact.avatar} height={16} width={16} className="dib" />
? <img src={props.ourContact.avatar} height={16} width={16} className="dib" />
: <Sigil
ship={window.ship}
size={16}
@ -193,17 +191,12 @@ export default class ChatInput extends Component<ChatInputProps, ChatInputState>
return (
<div className={
"cf items-center flex black white-d bt b--gray4 b--gray1-d bg-white " +
"cf items-center flex black white-d bt b--gray4 b--gray1-d bg-white" +
"bg-gray0-d relative"
}
style={{ flexGrow: 1 }}
>
<div className="ml2 flex items-center"
style={{
flexBasis: 16,
height: 16,
width: 16
}}>
<div className="pa2 flex items-center">
{avatar}
</div>
<ChatEditor

View File

@ -91,7 +91,7 @@ export default class ChatMessage extends Component<ChatMessageProps> {
const containerClass = `${renderSigil
? `f9 w-100 flex flex-wrap cf pr3 pt4 pl3 lh-copy`
: `f9 w-100 flex flex-wrap cf pr3 hide-child`} ${isPending ? 'o-40' : ''} ${isLastMessage ? 'pb3' : ''} ${className}`
: `f9 w-100 flex flex-wrap items-center cf pr3 hide-child`} ${isPending ? 'o-40' : ''} ${isLastMessage ? 'pb3' : ''} ${className}`
const timestamp = moment.unix(msg.when / 1000).format(renderSigil ? 'hh:mm a' : 'hh:mm');
@ -197,7 +197,7 @@ export class MessageWithSigil extends PureComponent<MessageProps> {
className="fl pr3 v-top bg-white bg-gray0-d"
/>
<div className="fr clamp-message white-d" style={{ flexGrow: 1, marginTop: -8 }}>
<div className="hide-child" style={{ paddingTop: '6px' }}>
<div className="hide-child" style={{ paddingTop: '4px' }}>
<p className="v-mid f9 gray2 dib mr3 c-default">
<span
className={`mw5 db truncate pointer ${showNickname ? '' : 'mono'}`}
@ -221,7 +221,7 @@ export class MessageWithSigil extends PureComponent<MessageProps> {
export const MessageWithoutSigil = ({ timestamp, msg, remoteContentPolicy, measure }) => (
<>
<p className="child pt2 pl2 pr1 mono f9 gray2 dib">{timestamp}</p>
<p className="child ph1 mono f9 gray2 dib">{timestamp}</p>
<div className="fr clamp-message white-d pr3 lh-copy" style={{ flexGrow: 1 }}>
<MessageContent content={msg.letter} remoteContentPolicy={remoteContentPolicy} measure={measure}/>
</div>

View File

@ -118,7 +118,7 @@ export default class ChatEditor extends Component {
lineNumbers: false,
lineWrapping: true,
scrollbarStyle: 'native',
cursorHeight: 1,
cursorHeight: 0.85,
placeholder: inCodeMode ? 'Code...' : placeholder,
extraKeys: {
'Enter': () => {
@ -133,10 +133,10 @@ export default class ChatEditor extends Component {
return (
<div
className={
'chat fr h-100 flex bg-gray0-d lh-copy pl2 w-100 items-center pv2' +
'chat fr h-100 flex bg-gray0-d lh-copy w-100 items-center ' +
(inCodeMode ? ' code' : '')
}
style={{ flexGrow: 1, maxHeight: '224px', width: 'calc(100% - 72px)' }}>
style={{ flexGrow: 1, paddingBottom: '3px', maxHeight: '224px', width: 'calc(100% - 72px)' }}>
<CodeEditor
value={message}
options={options}

View File

@ -58,10 +58,10 @@ export class OverlaySigil extends PureComponent {
const { hideAvatars } = props;
const img = (props.contact && (props.contact.avatar !== null) && !hideAvatars)
? <img src={props.contact.avatar} height={24} width={24} className="dib" />
? <img src={props.contact.avatar} height={16} width={16} className="dib" />
: <Sigil
ship={props.ship}
size={24}
size={16}
color={props.color}
classes={props.sigilClass}
/>;
@ -71,7 +71,7 @@ export class OverlaySigil extends PureComponent {
onClick={this.profileShow}
className={props.className + ' pointer relative'}
ref={this.containerRef}
style={{ height: '24px' }}
style={{ height: '16px' }}
>
{state.profileClicked && (
<ProfileOverlay

View File

@ -380,6 +380,7 @@ pre.CodeMirror-placeholder.CodeMirror-line-like { color: var(--gray); }
.chat .cm-s-tlon.CodeMirror {
background: #333;
color: #fff;
font-size: 12px;
}
.chat .cm-s-tlon span.cm-def {

View File

@ -0,0 +1,14 @@
import React from "react";
import { Center, LoadingSpinner } from "@tlon/indigo-react";
import { Body } from "./Body";
export function Loading() {
return (
<Body>
<Center height="100%">
<LoadingSpinner />
</Center>
</Body>
);
}