Newer
Older
import { Send, ShieldX } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite";
import { ChannelPermission } from "revolt.js/dist/api/permissions";
import { ulid } from "ulid";
import { Text } from "preact-i18n";
import { useCallback, useContext, useEffect, useState } from "preact/hooks";
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
import { defer } from "../../../lib/defer";
import { internalEmit, internalSubscribe } from "../../../lib/eventEmitter";
import { useTranslation } from "../../../lib/i18n";
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
import {
import { dispatch, getState } from "../../../redux";
import { SoundContext } from "../../../context/Settings";
import { useIntermediate } from "../../../context/intermediate/Intermediate";
import {
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import { takeError } from "../../../context/revoltjs/util";
import IconButton from "../../ui/IconButton";
import AutoComplete, { useAutoComplete } from "../AutoComplete";
import { PermissionTooltip } from "../Tooltip";
import FilePreview from "./bars/FilePreview";
import { ExperimentOptions } from "../../../redux/reducers/experiments";
export type UploadState =
| { type: "none" }
| { type: "attached"; files: File[] }
| {
type: "uploading";
files: File[];
percent: number;
cancel: CancelTokenSource;
}
| { type: "sending"; files: File[] }
| { type: "failed"; files: File[]; error: string };
const Base = styled.div`
display: flex;
background: var(--message-box);
textarea {
font-size: var(--text-size);
&::placeholder {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
font-size: var(--text-size);
.text {
padding: 14px 14px 14px 0;
}
height: 48px;
width: 48px;
padding: 12px;
}
.mobile {
@media (pointer: fine) {
display: none;
}
// ! FIXME: add to app config and load from app config
export const CAN_UPLOAD_AT_ONCE = 4;
export default observer(({ channel }: Props) => {
const [draft, setDraft] = useState(getState().drafts[channel._id] ?? "");
const [uploadState, setUploadState] = useState<UploadState>({
type: "none",
});
const [typing, setTyping] = useState<boolean | number>(false);
const [replies, setReplies] = useState<Reply[]>([]);
const playSound = useContext(SoundContext);
if (!(channel.permission & ChannelPermission.SendMessage)) {
<PermissionTooltip
permission="SendMessages"
placement="top">
<ShieldX size={22} />
</PermissionTooltip>
</Action>
<div className="text">
<Text id="app.main.channel.misc.no_sending" />
</div>
const setMessage = useCallback(
(content?: string) => {
setDraft(content ?? "");
if (content) {
dispatch({
type: "SET_DRAFT",
channel: channel._id,
content,
});
} else {
dispatch({
type: "CLEAR_DRAFT",
channel: channel._id,
});
}
},
[channel._id],
);
function append(content: string, action: "quote" | "mention") {
const text =
action === "quote"
? `${content
.split("\n")
.join("\n")}\n\n`
: `${content} `;
if (!draft || draft.length === 0) {
setMessage(text);
} else {
setMessage(`${draft}\n${text}`);
}
}
return internalSubscribe(
"MessageBox",
"append",
append as (...args: unknown[]) => void,
);
}, [draft, setMessage]);
if (uploadState.type === "uploading" || uploadState.type === "sending")
return;
let content = draft?.trim() ?? "";
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
content = content.replace(/(?<=(^| )[A-z])([eyuioa])/g, "\\*");
}
function gayify(): string {
const cycle =
[
"F66", "FC6", "CF6", "6F6", "6FC", "6CF", "66F", "C6F"
];
let res = "$\\textsf{";
let i = 0;
for (let ci = 0; ci < content.length; ci++) {
let str = content[ci];
if (str == " ") {
res += str;
continue;
}
if (str == "{" || str == "}" || str == "\\" || str == "&" || str == "%" || str == "$" || str == "#" || str == "_")
str = "\\" + str;
res += `\\color{#${cycle[i]}}${str}`;
i++;
if (i == cycle.length)
i = 0;
}
res += "}$";
return res;
}
if (hhhh == "owo" || hhhh == "uwu" || hhhh == "uvu")
content = owoify(content, hhhh);
console.log(hhhh);
});
if (experiments.enabled?.includes("rainbow") && !content.includes("\n")) {
const gay = gayify();
if (gay.length <= 2000)
content = gay;
}
if (uploadState.type === "attached") return sendFile(content);
type: "QUEUE_ADD",
nonce,
channel: channel._id,
message: {
_id: nonce,
channel: channel._id,
author: client.user!._id,
defer(() => renderer.jumpToBottom(SMOOTH_SCROLL_ON_RECEIVE));
if (uploadState.type !== "attached") return;
const cancel = Axios.CancelToken.source();
const files = uploadState.files;
stopTyping();
setUploadState({ type: "uploading", files, percent: 0, cancel });
try {
for (let i = 0; i < files.length && i < CAN_UPLOAD_AT_ONCE; i++) {
const file = files[i];
attachments.push(
await uploadFile(
client.configuration!.features.autumn.url,
"attachments",
file,
{
onUploadProgress: (e) =>
setUploadState({
type: "uploading",
files,
percent: Math.round(
(i * 100 + (100 * e.loaded) / e.total) /
Math.min(
files.length,
CAN_UPLOAD_AT_ONCE,
),
),
cancel,
}),
cancelToken: cancel.token,
},
),
);
}
} catch (err) {
if (err?.message === "cancel") {
setUploadState({
type: "attached",
});
} else {
setUploadState({
type: "failed",
files,
});
}
return;
}
setUploadState({
type: "sending",
});
} catch (err) {
setUploadState({
type: "failed",
files,
});
return;
}
setMessage();
if (files.length > CAN_UPLOAD_AT_ONCE) {
setUploadState({
type: "attached",
});
} else {
setUploadState({ type: "none" });
}
if (typeof typing === "number" && +new Date() < typing) return;
const ws = client.websocket;
if (ws.connected) {
setTyping(+new Date() + 2500);
});
}
}
function stopTyping(force?: boolean) {
if (force || typing) {
const ws = client.websocket;
if (ws.connected) {
setTyping(false);
ws.send({
type: "EndTyping",
// eslint-disable-next-line
const debouncedStopTyping = useCallback(
debounce(stopTyping as (...args: unknown[]) => void, 1000),
[channel._id],
);
const {
onChange,
onKeyUp,
onKeyDown,
onFocus,
onBlur,
...autoCompleteProps
} = useAutoComplete(setMessage, {
users: { type: "channel", id: channel._id },
channels:
channel.channel_type === "TextChannel"
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
const messageTextArea = <TextAreaAutoSize
autoFocus
hideBorder
maxRows={20}
id="message"
onKeyUp={onKeyUp}
value={messageText}
padding="var(--message-box-padding)"
onKeyDown={(e) => {
if (onKeyDown(e)) return;
if (
e.key === "ArrowUp" &&
(!draft || draft.length === 0)
) {
e.preventDefault();
internalEmit("MessageRenderer", "edit_last");
return;
}
if (
!e.shiftKey &&
e.key === "Enter" &&
!isTouchscreenDevice
) {
e.preventDefault();
return send();
}
debouncedStopTyping(true);
}}
placeholder={
channel.channel_type === "DirectMessage"
? translate("app.main.channel.message_who", {
person: channel.recipient?.username,
})
: channel.channel_type === "SavedMessages"
? translate("app.main.channel.message_saved")
: translate("app.main.channel.message_where", {
channel_name: channel.name,
})
}
disabled={
uploadState.type === "uploading" ||
uploadState.type === "sending"
}
onChange={(e) => {
setMessage(e.currentTarget.value);
startTyping();
onChange(e);
}}
onFocus={onFocus}
onBlur={onBlur}
/>;
<FilePreview
state={uploadState}
addFile={() =>
uploadState.type === "attached" &&
grabFiles(
20_000_000,
(files) =>
setUploadState({
type: "attached",
files: [...uploadState.files, ...files],
}),
() =>
openScreen({ id: "error", error: "FileTooLarge" }),
true,
)
}
removeFile={(index) => {
if (uploadState.type !== "attached") return;
setUploadState({
type: "attached",
files: uploadState.files.filter(
(_, i) => index !== i,
),
});
replies={replies}
setReplies={setReplies}
/>
<Action>
<FileUploader
size={24}
behaviour="multi"
style="attachment"
fileType="attachments"
maxFileSize={20_000_000}
attached={uploadState.type !== "none"}
uploading={
uploadState.type === "uploading" ||
uploadState.type === "sending"
}
remove={async () =>
setUploadState({ type: "none" })
onChange={(files) =>
setUploadState({ type: "attached", files })
}
cancel={() =>
uploadState.type === "uploading" &&
uploadState.cancel.cancel("cancel")
}
append={(files) => {
if (files.length === 0) return;
if (uploadState.type === "none") {
setUploadState({ type: "attached", files });
} else if (uploadState.type === "attached") {
setUploadState({
type: "attached",
files: [...uploadState.files, ...files],
});
}
}}
/>
</Action>
) : undefined}
{/*<Button onClick={() =>*/}
{/* openScreen({*/}
{/* id: "szuru_pop",*/}
{/* callback: (value) => {*/}
{/* setMessage(value)*/}
{/* console.log("callback called")*/}
{/* if (*/}
{/* !e.shiftKey &&*/}
{/* e.key === "Enter" &&*/}
{/* !isTouchscreenDevice*/}
{/* ) {*/}
{/* e.preventDefault();*/}
{/* return send();*/}
{/* }*/}
{/* debouncedStopTyping(true);*/}
{/*}}*/}
padding="var(--message-box-padding)"
if (
e.key === "ArrowUp" &&
(!draft || draft.length === 0)
) {
e.preventDefault();
internalEmit("MessageRenderer", "edit_last");
return;
}
if (
!e.shiftKey &&
e.key === "Enter" &&
!isTouchscreenDevice
) {
e.preventDefault();
return send();
}
if (e.key === "Escape") {
if (replies.length > 0) {
setReplies(replies.slice(0, -1));
} else if (
uploadState.type === "attached" &&
uploadState.files.length > 0
) {
setUploadState({
type:
uploadState.files.length > 1
? "attached"
: "none",
files: uploadState.files.slice(0, -1),
});
}
debouncedStopTyping(true);
}}
placeholder={
channel.channel_type === "DirectMessage"
? translate("app.main.channel.message_who", {
: channel.channel_type === "SavedMessages"
? translate("app.main.channel.message_saved")
: translate("app.main.channel.message_where", {
}
disabled={
uploadState.type === "uploading" ||
uploadState.type === "sending"
setMessage(e.currentTarget.value);
startTyping();
<Action>
{/*<IconButton onClick={emojiPicker}>
<HappyAlt size={20} />
</IconButton>*/}
className="mobile"
onClick={send}
onMouseDown={(e) => e.preventDefault()}>