Newer
Older
import { Send, HappyAlt, ShieldX } from "@styled-icons/boxicons-solid";
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 {
SingletonMessageRenderer,
SMOOTH_SCROLL_ON_RECEIVE,
import { dispatch, getState } from "../../../redux";
import { SoundContext } from "../../../context/Settings";
import { useIntermediate } from "../../../context/intermediate/Intermediate";
import {
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import { useChannelPermission } from "../../../context/revoltjs/hooks";
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";
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 function MessageBox({ channel, options }: 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);
const permissions = useChannelPermission(channel._id);
if (!(permissions & ChannelPermission.SendMessage)) {
return (
<Base>
<PermissionTooltip
permission="SendMessages"
placement="top">
<ShieldX size={22} />
</PermissionTooltip>
</Action>
<div className="text">
<Text id="app.main.channel.misc.no_sending" />
</div>
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);
if (uploadState.type === "uploading" || uploadState.type === "sending")
return;
let content = draft?.trim() ?? "";
190
191
192
193
194
195
196
197
198
199
200
201
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
if (options.enabled?.includes("censor")) {
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;
}
options.enabled?.forEach(hhhh => {
if (hhhh == "owo" || hhhh == "uwu" || hhhh == "uvu")
content = owoify(content, hhhh);
console.log(hhhh);
});
if (options.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(() =>
SingletonMessageRenderer.jumpToBottom(
channel._id,
SMOOTH_SCROLL_ON_RECEIVE,
),
);
try {
await client.channels.sendMessage(channel._id, {
content,
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",
});
const nonce = ulid();
try {
await client.channels.sendMessage(channel._id, {
content,
nonce,
});
} 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) {
});
}
}
function stopTyping(force?: boolean) {
if (force || typing) {
const ws = client.websocket;
if (ws.connected) {
setTyping(false);
ws.send({
type: "EndTyping",
const debouncedStopTyping = useCallback(debounce(stopTyping, 1000), [
channel._id,
]);
const {
onChange,
onKeyUp,
onKeyDown,
onFocus,
onBlur,
...autoCompleteProps
} = useAutoComplete(setMessage, {
users: { type: "channel", id: channel._id },
channels:
channel.channel_type === "TextChannel"
? { server: channel.server }
: undefined,
});
<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,
),
});
}}
/>
<ReplyBar
channel={channel._id}
replies={replies}
setReplies={setReplies}
/>
{permissions & ChannelPermission.UploadFiles ? (
<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}
value={draft ?? ""}
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();
}
debouncedStopTyping(true);
}}
placeholder={
channel.channel_type === "DirectMessage"
? translate("app.main.channel.message_who", {
person: client.users.get(
client.channels.getRecipient(channel._id),
)?.username,
})
: 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>*/}
<IconButton className="mobile" onClick={send}>