mirror of
https://github.com/urbit/shrub.git
synced 2025-01-03 01:54:43 +03:00
chat: address design critique
This commit is contained in:
parent
3b38a22bc9
commit
cc49b6cee3
64
pkg/interface/src/logic/lib/useDrag.ts
Normal file
64
pkg/interface/src/logic/lib/useDrag.ts
Normal 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 };
|
||||
}
|
@ -100,5 +100,6 @@ export default class ChatReducer<S extends ChatState> {
|
||||
mailbox.splice(index, 1);
|
||||
}
|
||||
}
|
||||
state.pendingMessages.set(msg.path, mailbox);
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
14
pkg/interface/src/views/components/Loading.tsx
Normal file
14
pkg/interface/src/views/components/Loading.tsx
Normal 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>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user