mirror of
https://github.com/revoltchat/revite.git
synced 2024-12-24 06:32:08 -05:00
Show errors / queue on message.
Focus editor / box properly.
This commit is contained in:
parent
5db0854b42
commit
8fe1ce3450
8 changed files with 52 additions and 173 deletions
2
external/lang
vendored
2
external/lang
vendored
|
@ -1 +1 @@
|
||||||
Subproject commit 210172de724fcd5adeacec221bd9da30350afc06
|
Subproject commit a84270a2b941a51f4785e543c0882ce9f7f004a6
|
|
@ -6,18 +6,21 @@ import { Children } from "../../../types/Preact";
|
||||||
import Attachment from "./attachments/Attachment";
|
import Attachment from "./attachments/Attachment";
|
||||||
import { attachContextMenu } from "preact-context-menu";
|
import { attachContextMenu } from "preact-context-menu";
|
||||||
import { useUser } from "../../../context/revoltjs/hooks";
|
import { useUser } from "../../../context/revoltjs/hooks";
|
||||||
|
import { QueuedMessage } from "../../../redux/reducers/queue";
|
||||||
import { MessageObject } from "../../../context/revoltjs/util";
|
import { MessageObject } from "../../../context/revoltjs/util";
|
||||||
import MessageBase, { MessageContent, MessageDetail, MessageInfo } from "./MessageBase";
|
import MessageBase, { MessageContent, MessageDetail, MessageInfo } from "./MessageBase";
|
||||||
|
import Overline from "../../ui/Overline";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
attachContext?: boolean
|
attachContext?: boolean
|
||||||
|
queued?: QueuedMessage
|
||||||
message: MessageObject
|
message: MessageObject
|
||||||
contrast?: boolean
|
contrast?: boolean
|
||||||
content?: Children
|
content?: Children
|
||||||
head?: boolean
|
head?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Message({ attachContext, message, contrast, content: replacement, head }: Props) {
|
export default function Message({ attachContext, message, contrast, content: replacement, head, queued }: Props) {
|
||||||
// TODO: Can improve re-renders here by providing a list
|
// TODO: Can improve re-renders here by providing a list
|
||||||
// TODO: of dependencies. We only need to update on u/avatar.
|
// TODO: of dependencies. We only need to update on u/avatar.
|
||||||
let user = useUser(message.author);
|
let user = useUser(message.author);
|
||||||
|
@ -25,9 +28,11 @@ export default function Message({ attachContext, message, contrast, content: rep
|
||||||
const content = message.content as string;
|
const content = message.content as string;
|
||||||
return (
|
return (
|
||||||
<MessageBase id={message._id}
|
<MessageBase id={message._id}
|
||||||
contrast={contrast}
|
|
||||||
head={head}
|
head={head}
|
||||||
onContextMenu={attachContext ? attachContextMenu('Menu', { message, contextualChannel: message.channel }) : undefined}>
|
contrast={contrast}
|
||||||
|
sending={typeof queued !== 'undefined'}
|
||||||
|
failed={typeof queued?.error !== 'undefined'}
|
||||||
|
onContextMenu={attachContext ? attachContextMenu('Menu', { message, contextualChannel: message.channel, queued }) : undefined}>
|
||||||
<MessageInfo>
|
<MessageInfo>
|
||||||
{ head ?
|
{ head ?
|
||||||
<UserIcon target={user} size={36} /> :
|
<UserIcon target={user} size={36} /> :
|
||||||
|
@ -39,6 +44,7 @@ export default function Message({ attachContext, message, contrast, content: rep
|
||||||
<MessageDetail message={message} position="top" />
|
<MessageDetail message={message} position="top" />
|
||||||
</span> }
|
</span> }
|
||||||
{ replacement ?? <Markdown content={content} /> }
|
{ replacement ?? <Markdown content={content} /> }
|
||||||
|
{ queued?.error && <Overline type="error" error={queued.error} /> }
|
||||||
{ message.attachments?.map((attachment, index) =>
|
{ message.attachments?.map((attachment, index) =>
|
||||||
<Attachment key={index} attachment={attachment} hasContent={ index > 0 || content.length > 0 } />) }
|
<Attachment key={index} attachment={attachment} hasContent={ index > 0 || content.length > 0 } />) }
|
||||||
{ message.embeds?.map((embed, index) =>
|
{ message.embeds?.map((embed, index) =>
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { MessageObject } from "../../../context/revoltjs/util";
|
||||||
|
|
||||||
export interface BaseMessageProps {
|
export interface BaseMessageProps {
|
||||||
head?: boolean,
|
head?: boolean,
|
||||||
status?: boolean,
|
failed?: boolean,
|
||||||
mention?: boolean,
|
mention?: boolean,
|
||||||
blocked?: boolean,
|
blocked?: boolean,
|
||||||
sending?: boolean,
|
sending?: boolean,
|
||||||
|
@ -49,7 +49,7 @@ export default styled.div<BaseMessageProps>`
|
||||||
color: var(--tertiary-foreground);
|
color: var(--tertiary-foreground);
|
||||||
` }
|
` }
|
||||||
|
|
||||||
${ props => props.status && css`
|
${ props => props.failed && css`
|
||||||
color: var(--error);
|
color: var(--error);
|
||||||
` }
|
` }
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { SingletonMessageRenderer, SMOOTH_SCROLL_ON_RECEIVE } from "../../../lib
|
||||||
|
|
||||||
import FilePreview from './bars/FilePreview';
|
import FilePreview from './bars/FilePreview';
|
||||||
import { debounce } from "../../../lib/debounce";
|
import { debounce } from "../../../lib/debounce";
|
||||||
|
import { internalEmit } from "../../../lib/eventEmitter";
|
||||||
|
|
||||||
type Props = WithDispatcher & {
|
type Props = WithDispatcher & {
|
||||||
channel: Channel;
|
channel: Channel;
|
||||||
|
@ -237,11 +238,22 @@ function MessageBox({ channel, draft, dispatcher }: Props) {
|
||||||
/>
|
/>
|
||||||
</Action>
|
</Action>
|
||||||
<TextAreaAutoSize
|
<TextAreaAutoSize
|
||||||
|
autoFocus
|
||||||
hideBorder
|
hideBorder
|
||||||
maxRows={5}
|
maxRows={5}
|
||||||
padding={15}
|
padding={15}
|
||||||
|
id="message"
|
||||||
value={draft ?? ''}
|
value={draft ?? ''}
|
||||||
onKeyDown={e => {
|
onKeyDown={e => {
|
||||||
|
if (
|
||||||
|
e.key === "ArrowUp" &&
|
||||||
|
(!draft || draft.length === 0)
|
||||||
|
) {
|
||||||
|
e.preventDefault();
|
||||||
|
internalEmit("MessageRenderer", "edit_last");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!e.shiftKey && e.key === "Enter" && !isTouchscreenDevice) {
|
if (!e.shiftKey && e.key === "Enter" && !isTouchscreenDevice) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return send();
|
return send();
|
||||||
|
|
|
@ -48,142 +48,3 @@ export default styled.textarea<TextAreaProps>`
|
||||||
font-family: 'Open Sans', sans-serif;
|
font-family: 'Open Sans', sans-serif;
|
||||||
` }
|
` }
|
||||||
`;
|
`;
|
||||||
|
|
||||||
/*export interface TextAreaProps {
|
|
||||||
id?: string;
|
|
||||||
value: string;
|
|
||||||
maxRows?: number;
|
|
||||||
padding?: number;
|
|
||||||
minHeight?: number;
|
|
||||||
disabled?: boolean;
|
|
||||||
maxLength?: number;
|
|
||||||
className?: string;
|
|
||||||
autoFocus?: boolean;
|
|
||||||
forceFocus?: boolean;
|
|
||||||
placeholder?: string;
|
|
||||||
onKeyDown?: (ev: KeyboardEvent) => void;
|
|
||||||
onKeyUp?: (ev: KeyboardEvent) => void;
|
|
||||||
onChange: (
|
|
||||||
value: string,
|
|
||||||
ev: JSX.TargetedEvent<HTMLTextAreaElement, Event>
|
|
||||||
) => void;
|
|
||||||
onFocus?: (current: HTMLTextAreaElement) => void;
|
|
||||||
onBlur?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lineHeight = 20;
|
|
||||||
|
|
||||||
export const TextAreaB = memo((props: TextAreaProps) => {
|
|
||||||
const padding = props.padding ? props.padding * 2 : 0;
|
|
||||||
|
|
||||||
const [height, setHeightState] = useState(
|
|
||||||
props.minHeight ?? lineHeight + padding
|
|
||||||
);
|
|
||||||
const ghost = useRef<HTMLDivElement>();
|
|
||||||
const ref = useRef<HTMLTextAreaElement>();
|
|
||||||
|
|
||||||
function setHeight(h: number = lineHeight) {
|
|
||||||
let newHeight = Math.min(
|
|
||||||
Math.max(
|
|
||||||
lineHeight,
|
|
||||||
props.maxRows ? Math.min(h, props.maxRows * lineHeight) : h
|
|
||||||
),
|
|
||||||
props.minHeight ?? Infinity
|
|
||||||
);
|
|
||||||
|
|
||||||
if (props.padding) newHeight += padding;
|
|
||||||
if (height !== newHeight) {
|
|
||||||
setHeightState(newHeight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onChange(ev: JSX.TargetedEvent<HTMLTextAreaElement, Event>) {
|
|
||||||
props.onChange(ev.currentTarget.value, ev);
|
|
||||||
}
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
setHeight(ghost.current.clientHeight);
|
|
||||||
}, [ghost, props.value]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (props.autoFocus) ref.current.focus();
|
|
||||||
}, [props.value]);
|
|
||||||
|
|
||||||
const inputSelected = () =>
|
|
||||||
["TEXTAREA", "INPUT"].includes(document.activeElement?.nodeName ?? "");
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (props.forceFocus) {
|
|
||||||
ref.current.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.autoFocus && !inputSelected()) {
|
|
||||||
ref.current.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ? if you are wondering what this is
|
|
||||||
// ? it is a quick and dirty hack to fix
|
|
||||||
// ? value not setting correctly
|
|
||||||
// ? I have no clue what's going on
|
|
||||||
ref.current.value = props.value;
|
|
||||||
|
|
||||||
if (!props.autoFocus) return;
|
|
||||||
function keyDown(e: KeyboardEvent) {
|
|
||||||
if ((e.ctrlKey && e.key !== "v") || e.altKey || e.metaKey) return;
|
|
||||||
if (e.key.length !== 1) return;
|
|
||||||
if (ref && !inputSelected()) {
|
|
||||||
ref.current.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.body.addEventListener("keydown", keyDown);
|
|
||||||
return () => document.body.removeEventListener("keydown", keyDown);
|
|
||||||
}, [ref]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
function focus(textarea_id: string) {
|
|
||||||
if (props.id === textarea_id) {
|
|
||||||
ref.current.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// InternalEventEmitter.addListener("focus_textarea", focus);
|
|
||||||
// return () =>
|
|
||||||
// InternalEventEmitter.removeListener("focus_textarea", focus);
|
|
||||||
}, [ref]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classNames(styles.container, props.className)}>
|
|
||||||
<textarea
|
|
||||||
id={props.id}
|
|
||||||
name={props.id}
|
|
||||||
style={{ height }}
|
|
||||||
value={props.value}
|
|
||||||
onChange={onChange}
|
|
||||||
disabled={props.disabled}
|
|
||||||
maxLength={props.maxLength}
|
|
||||||
className={styles.textarea}
|
|
||||||
onKeyDown={props.onKeyDown}
|
|
||||||
placeholder={props.placeholder}
|
|
||||||
onContextMenu={e => e.stopPropagation()}
|
|
||||||
onKeyUp={ev => {
|
|
||||||
setHeight(ghost.current.clientHeight);
|
|
||||||
props.onKeyUp && props.onKeyUp(ev);
|
|
||||||
}}
|
|
||||||
ref={ref}
|
|
||||||
onFocus={() => props.onFocus && props.onFocus(ref.current)}
|
|
||||||
onBlur={props.onBlur}
|
|
||||||
/>
|
|
||||||
<div className={styles.hide}>
|
|
||||||
<div className={styles.ghost} ref={ghost}>
|
|
||||||
{props.value
|
|
||||||
? props.value
|
|
||||||
.split("\n")
|
|
||||||
.map(x => `${x}`)
|
|
||||||
.join("\n")
|
|
||||||
: undefined ?? "\n"}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});*/
|
|
||||||
|
|
|
@ -1,20 +1,24 @@
|
||||||
import TextArea, { DEFAULT_LINE_HEIGHT, DEFAULT_TEXT_AREA_PADDING, TextAreaProps, TEXT_AREA_BORDER_WIDTH } from "../components/ui/TextArea";
|
import TextArea, { DEFAULT_LINE_HEIGHT, DEFAULT_TEXT_AREA_PADDING, TextAreaProps, TEXT_AREA_BORDER_WIDTH } from "../components/ui/TextArea";
|
||||||
import { useEffect, useRef } from "preact/hooks";
|
import { useEffect, useRef } from "preact/hooks";
|
||||||
|
import { internalSubscribe } from "./eventEmitter";
|
||||||
|
|
||||||
type TextAreaAutoSizeProps = Omit<JSX.HTMLAttributes<HTMLTextAreaElement>, 'style' | 'value'> & TextAreaProps & {
|
type TextAreaAutoSizeProps = Omit<JSX.HTMLAttributes<HTMLTextAreaElement>, 'style' | 'value'> & TextAreaProps & {
|
||||||
autoFocus?: boolean,
|
forceFocus?: boolean
|
||||||
minHeight?: number,
|
autoFocus?: boolean
|
||||||
maxRows?: number,
|
minHeight?: number
|
||||||
|
maxRows?: number
|
||||||
value: string
|
value: string
|
||||||
|
|
||||||
|
id?: string
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) {
|
export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) {
|
||||||
const { autoFocus, minHeight, maxRows, value, padding, lineHeight, hideBorder, children, as, ...textAreaProps } = props;
|
const { autoFocus, minHeight, maxRows, value, padding, lineHeight, hideBorder, forceFocus, children, as, ...textAreaProps } = props;
|
||||||
const line = lineHeight ?? DEFAULT_LINE_HEIGHT;
|
const line = lineHeight ?? DEFAULT_LINE_HEIGHT;
|
||||||
|
|
||||||
const heightPadding = ((padding ?? DEFAULT_TEXT_AREA_PADDING) + (hideBorder ? 0 : TEXT_AREA_BORDER_WIDTH)) * 2;
|
const heightPadding = ((padding ?? DEFAULT_TEXT_AREA_PADDING) + (hideBorder ? 0 : TEXT_AREA_BORDER_WIDTH)) * 2;
|
||||||
const height = Math.max(Math.min(value.split('\n').length, maxRows ?? Infinity) * line + heightPadding, minHeight ?? 0);
|
const height = Math.max(Math.min(value.split('\n').length, maxRows ?? Infinity) * line + heightPadding, minHeight ?? 0);
|
||||||
console.log(value.split('\n').length, line, heightPadding, height);
|
|
||||||
const ref = useRef<HTMLTextAreaElement>();
|
const ref = useRef<HTMLTextAreaElement>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -25,9 +29,9 @@ export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) {
|
||||||
["TEXTAREA", "INPUT"].includes(document.activeElement?.nodeName ?? "");
|
["TEXTAREA", "INPUT"].includes(document.activeElement?.nodeName ?? "");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
/* if (props.forceFocus) { // figure out what needed force focus
|
if (forceFocus) {
|
||||||
ref.current.focus();
|
ref.current.focus();
|
||||||
} */
|
}
|
||||||
|
|
||||||
if (autoFocus && !inputSelected()) {
|
if (autoFocus && !inputSelected()) {
|
||||||
ref.current.focus();
|
ref.current.focus();
|
||||||
|
@ -52,6 +56,16 @@ export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) {
|
||||||
return () => document.body.removeEventListener("keydown", keyDown);
|
return () => document.body.removeEventListener("keydown", keyDown);
|
||||||
}, [ref]);
|
}, [ref]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function focus(id: string) {
|
||||||
|
if (id === props.id) {
|
||||||
|
ref.current.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return internalSubscribe("TextArea", "focus", focus);
|
||||||
|
}, [ref]);
|
||||||
|
|
||||||
return <TextArea
|
return <TextArea
|
||||||
ref={ref}
|
ref={ref}
|
||||||
value={value}
|
value={value}
|
||||||
|
|
|
@ -58,6 +58,7 @@ export default function MessageEditor({ message, finish }: Props) {
|
||||||
return (
|
return (
|
||||||
<EditorBase>
|
<EditorBase>
|
||||||
<TextAreaAutoSize
|
<TextAreaAutoSize
|
||||||
|
forceFocus
|
||||||
maxRows={3}
|
maxRows={3}
|
||||||
padding={12}
|
padding={12}
|
||||||
value={content}
|
value={content}
|
||||||
|
@ -73,7 +74,6 @@ export default function MessageEditor({ message, finish }: Props) {
|
||||||
save();
|
save();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
// forceFocus
|
|
||||||
/>
|
/>
|
||||||
<span className="caption">
|
<span className="caption">
|
||||||
escape to <a onClick={finish}>cancel</a> ·
|
escape to <a onClick={finish}>cancel</a> ·
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import { decodeTime } from "ulid";
|
import { decodeTime } from "ulid";
|
||||||
import MessageEditor from "./MessageEditor";
|
import MessageEditor from "./MessageEditor";
|
||||||
import { Children } from "../../../types/Preact";
|
import { Children } from "../../../types/Preact";
|
||||||
import { useEffect, useState } from "preact/hooks";
|
|
||||||
import ConversationStart from "./ConversationStart";
|
import ConversationStart from "./ConversationStart";
|
||||||
import { connectState } from "../../../redux/connector";
|
import { connectState } from "../../../redux/connector";
|
||||||
import Preloader from "../../../components/ui/Preloader";
|
import Preloader from "../../../components/ui/Preloader";
|
||||||
import { RenderState } from "../../../lib/renderer/types";
|
import { RenderState } from "../../../lib/renderer/types";
|
||||||
import DateDivider from "../../../components/ui/DateDivider";
|
import DateDivider from "../../../components/ui/DateDivider";
|
||||||
import { QueuedMessage } from "../../../redux/reducers/queue";
|
import { QueuedMessage } from "../../../redux/reducers/queue";
|
||||||
|
import { useContext, useEffect, useState } from "preact/hooks";
|
||||||
import { MessageObject } from "../../../context/revoltjs/util";
|
import { MessageObject } from "../../../context/revoltjs/util";
|
||||||
import Message from "../../../components/common/messaging/Message";
|
import Message from "../../../components/common/messaging/Message";
|
||||||
|
import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
||||||
import RequiresOnline from "../../../context/revoltjs/RequiresOnline";
|
import RequiresOnline from "../../../context/revoltjs/RequiresOnline";
|
||||||
import { useForceUpdate, useUsers } from "../../../context/revoltjs/hooks";
|
|
||||||
import { internalSubscribe, internalEmit } from "../../../lib/eventEmitter";
|
import { internalSubscribe, internalEmit } from "../../../lib/eventEmitter";
|
||||||
import { SystemMessage } from "../../../components/common/messaging/SystemMessage";
|
import { SystemMessage } from "../../../components/common/messaging/SystemMessage";
|
||||||
|
|
||||||
|
@ -24,17 +24,13 @@ interface Props {
|
||||||
function MessageRenderer({ id, state, queue }: Props) {
|
function MessageRenderer({ id, state, queue }: Props) {
|
||||||
if (state.type !== 'RENDER') return null;
|
if (state.type !== 'RENDER') return null;
|
||||||
|
|
||||||
const ctx = useForceUpdate();
|
const client = useContext(AppContext);
|
||||||
const users = useUsers();
|
const userId = client.user!._id;
|
||||||
const userId = ctx.client.user!._id;
|
|
||||||
|
|
||||||
/*
|
|
||||||
const view = useView(id);*/
|
|
||||||
|
|
||||||
const [editing, setEditing] = useState<string | undefined>(undefined);
|
const [editing, setEditing] = useState<string | undefined>(undefined);
|
||||||
const stopEditing = () => {
|
const stopEditing = () => {
|
||||||
setEditing(undefined);
|
setEditing(undefined);
|
||||||
internalEmit("MessageBox", "focus");
|
internalEmit("TextArea", "focus", "message");
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -141,24 +137,14 @@ function MessageRenderer({ id, state, queue }: Props) {
|
||||||
} as any;
|
} as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*render.push(
|
|
||||||
<Message
|
|
||||||
user={users.find(x => x?._id === userId)}
|
|
||||||
message={msg.data}
|
|
||||||
queued={msg}
|
|
||||||
key={msg.id}
|
|
||||||
head={head}
|
|
||||||
/>
|
|
||||||
);*/
|
|
||||||
render.push(
|
render.push(
|
||||||
<Message message={msg.data}
|
<Message message={msg.data}
|
||||||
key={msg.id}
|
key={msg.id}
|
||||||
|
queued={msg}
|
||||||
head={head}
|
head={head}
|
||||||
attachContext />
|
attachContext />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// render.push(<div>end</div>);
|
|
||||||
} else {
|
} else {
|
||||||
render.push(
|
render.push(
|
||||||
<RequiresOnline>
|
<RequiresOnline>
|
||||||
|
|
Loading…
Reference in a new issue