blowater/UI/editor.tsx

287 lines
11 KiB
TypeScript
Raw Normal View History

2023-06-30 14:05:57 +00:00
/** @jsx h */
import { createRef, h } from "https://esm.sh/preact@10.17.1";
import { CenterClass, LinearGradientsClass, NoOutlineClass } from "./components/tw.ts";
import { emitFunc } from "../event-bus.ts";
2023-06-30 14:05:57 +00:00
2023-08-28 17:58:05 +00:00
import { PublicKey } from "../lib/nostr-ts/key.ts";
2023-11-11 11:19:21 +00:00
import { ImageIcon } from "./icons/image-icon.tsx";
2023-12-18 10:23:15 +00:00
import { DividerBackgroundColor, PrimaryBackgroundColor, PrimaryTextColor } from "./style/colors.ts";
2023-11-11 11:19:21 +00:00
import { SendIcon } from "./icons/send-icon.tsx";
import { Component } from "https://esm.sh/preact@10.17.1";
2023-11-11 11:19:21 +00:00
import { RemoveIcon } from "./icons/remove-icon.tsx";
import { isMobile } from "./_helper.ts";
2023-06-30 14:05:57 +00:00
export type EditorModel = {
readonly pubkey: PublicKey;
2023-06-30 14:05:57 +00:00
text: string;
files: Blob[];
};
export function new_DM_EditorModel(
pubkey: PublicKey,
): EditorModel {
2023-06-30 14:05:57 +00:00
return {
pubkey: pubkey,
2023-06-30 14:05:57 +00:00
text: "",
files: [],
};
}
export type EditorEvent = SendMessage | UpdateEditorText | UpdateMessageFiles;
2023-06-30 14:05:57 +00:00
export type SendMessage = {
readonly type: "SendMessage";
readonly pubkey: PublicKey;
2023-06-30 14:05:57 +00:00
text: string;
files: Blob[];
isGroupChat: boolean;
2023-06-30 14:05:57 +00:00
};
export type UpdateEditorText = {
readonly type: "UpdateEditorText";
readonly pubkey: PublicKey;
readonly isGroupChat: boolean;
2023-06-30 14:05:57 +00:00
readonly text: string;
};
export type UpdateMessageFiles = {
readonly type: "UpdateMessageFiles";
readonly pubkey: PublicKey;
readonly isGroupChat: boolean;
2023-06-30 14:05:57 +00:00
readonly files: Blob[];
};
type EditorProps = {
2023-06-30 14:05:57 +00:00
// UI
readonly placeholder: string;
readonly maxHeight: string;
// Logic
readonly targetNpub: PublicKey;
readonly text: string;
files: Blob[];
2023-06-30 14:05:57 +00:00
//
readonly emit: emitFunc<EditorEvent>;
readonly isGroupChat: boolean;
};
2023-06-30 14:05:57 +00:00
2023-12-22 06:55:15 +00:00
export type EditorState = {
text: string;
files: Blob[];
};
export class Editor extends Component<EditorProps, EditorState> {
state: Readonly<EditorState> = {
text: "",
files: [],
};
componentDidMount(): void {
this.setState({
text: this.props.text,
files: this.props.files,
});
}
componentWillReceiveProps(nextProps: Readonly<EditorProps>) {
if (!isMobile()) {
this.textareaElement.current.focus();
}
}
textareaElement = createRef();
2023-12-22 06:55:15 +00:00
sendMessage = async () => {
const props = this.props;
props.emit({
type: "SendMessage",
pubkey: props.targetNpub,
files: props.files,
text: props.text,
isGroupChat: props.isGroupChat,
});
this.textareaElement.current.setAttribute(
"rows",
"1",
);
this.setState({ text: "", files: [] });
};
2023-06-30 14:05:57 +00:00
2023-12-22 06:55:15 +00:00
removeFile = (index: number) => {
const files = this.state.files;
const newFiles = files.slice(0, index).concat(files.slice(index + 1));
this.props.emit({
type: "UpdateMessageFiles",
files: newFiles,
pubkey: this.props.targetNpub,
isGroupChat: this.props.isGroupChat,
});
this.setState({
files: newFiles,
});
};
2023-06-30 14:05:57 +00:00
2023-12-22 06:55:15 +00:00
render() {
const uploadFileInput = createRef();
return (
2023-12-22 10:48:44 +00:00
<div class={`flex mb-4 mx-4 items-center bg-[${DividerBackgroundColor}] rounded-lg`}>
<button
2023-12-22 06:55:15 +00:00
class={`min-w-[3rem] w-[3rem] h-[3rem] hover:bg-[${DividerBackgroundColor}] group ${CenterClass} rounded-[50%] ${NoOutlineClass}`}
onClick={() => {
if (uploadFileInput.current) {
uploadFileInput.current.click();
2023-06-30 14:05:57 +00:00
}
}}
>
<ImageIcon
2023-12-22 06:55:15 +00:00
class={`h-[2rem] w-[2rem] stroke-current text-[${PrimaryTextColor}4D] group-hover:text-[${PrimaryTextColor}]`}
style={{
fill: "none",
}}
/>
</button>
<input
ref={uploadFileInput}
type="file"
accept="image/*"
multiple
onChange={async (e) => {
2023-12-22 06:55:15 +00:00
let propsfiles = this.state.files;
const files = e.currentTarget.files;
if (!files) {
2023-06-30 14:05:57 +00:00
return;
}
for (let i = 0; i < files.length; i++) {
const file = files.item(i);
if (!file) {
continue;
2023-06-30 14:05:57 +00:00
}
propsfiles = propsfiles.concat([file]);
2023-06-30 14:05:57 +00:00
}
2023-12-22 06:55:15 +00:00
this.props.emit({
type: "UpdateMessageFiles",
files: propsfiles,
2023-12-22 06:55:15 +00:00
pubkey: this.props.targetNpub,
isGroupChat: this.props.isGroupChat,
});
this.setState({
files: propsfiles,
});
2023-06-30 14:05:57 +00:00
}}
2023-12-18 10:23:15 +00:00
class={`hidden`}
/>
<div
2023-12-22 10:48:44 +00:00
class={`py-[0.75rem] flex flex-col flex-1 overflow-hidden`}
2023-06-30 14:05:57 +00:00
>
2023-12-22 06:55:15 +00:00
{this.state.files.length > 0
? (
<ul
2023-12-18 10:23:15 +00:00
class={`flex overflow-auto list-none py-2 w-full border-b border-[#52525B] mb-[1rem]`}
>
2023-12-22 06:55:15 +00:00
{this.state.files.map((file, index) => {
return (
<li
2023-12-18 10:23:15 +00:00
class={`relative mx-2 min-w-[10rem] w-[10rem] h-[10rem] p-2 bg-[${PrimaryBackgroundColor}] rounded ${CenterClass}`}
>
<button
2023-12-18 10:23:15 +00:00
class={`w-[2rem] h-[2rem] absolute top-1 right-1 rounded-[50%] hover:bg-[${DividerBackgroundColor}] ${CenterClass} ${NoOutlineClass}`}
onClick={() => {
2023-12-22 06:55:15 +00:00
this.removeFile(index);
}}
>
<RemoveIcon
2023-12-18 10:23:15 +00:00
class={`w-[1.3rem] h-[1.3rem]`}
style={{
fill: "none",
stroke: PrimaryTextColor,
}}
/>
</button>
<img
2023-12-18 10:23:15 +00:00
class={`max-w-full max-h-full`}
src={URL.createObjectURL(file)}
alt=""
/>
</li>
);
})}
</ul>
)
: undefined}
2023-06-30 14:05:57 +00:00
<textarea
ref={this.textareaElement}
2023-07-03 13:42:11 +00:00
style={{
2023-12-22 06:55:15 +00:00
maxHeight: this.props.maxHeight,
2023-07-03 13:42:11 +00:00
}}
2023-12-22 06:55:15 +00:00
value={this.state.text}
rows={1}
2023-12-18 10:23:15 +00:00
class={`flex-1 bg-transparent focus-visible:outline-none placeholder-[${PrimaryTextColor}4D] text-[0.8rem] text-[#D2D3D5] whitespace-nowrap resize-none overflow-x-hidden overflow-y-auto`}
2023-12-22 06:55:15 +00:00
placeholder={this.props.placeholder}
onInput={(e) => {
2023-12-22 06:55:15 +00:00
this.props.emit({
type: "UpdateEditorText",
2023-12-22 06:55:15 +00:00
pubkey: this.props.targetNpub,
text: e.currentTarget.value,
2023-12-22 06:55:15 +00:00
isGroupChat: this.props.isGroupChat,
});
const lines = e.currentTarget.value.split("\n");
e.currentTarget.setAttribute(
"rows",
`${lines.length}`,
);
2023-12-22 06:55:15 +00:00
this.setState({ text: e.currentTarget.value });
}}
onKeyDown={async (e) => {
// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/metaKey
if (e.code === "Enter" && (e.ctrlKey || e.metaKey)) {
2023-12-22 06:55:15 +00:00
await this.sendMessage();
}
}}
onPaste={async (_) => {
let clipboardData: ClipboardItems = [];
try {
clipboardData = await window.navigator.clipboard.read();
} catch (e) {
2023-10-21 11:29:47 +00:00
console.error(e.message);
return;
}
for (const item of clipboardData) {
try {
const image = await item.getType(
"image/png",
);
2023-12-22 06:55:15 +00:00
this.props.emit({
2023-10-21 11:29:47 +00:00
type: "UpdateMessageFiles",
2023-12-22 06:55:15 +00:00
isGroupChat: this.props.isGroupChat,
pubkey: this.props.targetNpub,
files: this.props.files.concat([image]),
2023-10-21 11:29:47 +00:00
});
} catch (e) {
console.error(e);
}
}
}}
>
</textarea>
</div>
2023-11-15 11:35:02 +00:00
<button
2023-12-22 10:48:44 +00:00
class={`m-2 w-12 h-8 ${CenterClass} ${LinearGradientsClass} rounded`}
2023-11-15 11:35:02 +00:00
onClick={async () => {
2023-12-22 06:55:15 +00:00
await this.sendMessage();
this.textareaElement.current?.focus();
2023-11-15 11:35:02 +00:00
}}
>
<SendIcon
2023-12-18 10:23:15 +00:00
class={`h-4 w-4`}
2023-11-15 11:35:02 +00:00
style={{
stroke: PrimaryTextColor,
fill: "none",
}}
/>
</button>
2023-07-03 13:42:11 +00:00
</div>
);
}
}