import Input, { type InputController } from "@tensamin/markdown/input"; import { Card, CardHeader, Drawer, DrawerContent, DrawerTrigger, Popover, PopoverContent, PopoverTrigger, } from "@methanium/ui"; import { useStorage } from "@tensamin/storage/context"; import React, { useCallback, useEffect, useState, useRef } from "react"; import { Button } from "@methanium/ui"; import { Plus, Laugh, FileVideo, SendHorizonal } from "lucide-react"; import { useChat, useReplyMessage } from "../context"; import { requireRelaySuccess, useMTP } from "@tensamin/mtp"; import { log, toast } from "@tensamin/shared/log"; import { cn, useIsMobile } from "@methanium/ui"; import { encryptChatText } from "@tensamin/crypto/chatSecret"; import { useSession } from "@tensamin/identity/session"; import { useEmojiRanks, useRecordEmojiUse } from "./emoji/emojiRanks"; import ReplyBox from "./replyBox"; import { useHotkey } from "@tensamin/hotkeys"; import { editLastMessageHotkey } from "../hotkeys"; import { useUser } from "@tensamin/identity/context"; import EmojiPicker from "./emoji/emojiPicker"; import GifPicker from "./media/gifPicker"; export default function InputComponent({ value, setValue, onEditLastMessage, }: { value: string; setValue: (value: string) => void; onEditLastMessage: () => void; }) { const [invertEnterBehavior, setInvertEnterBehavior] = useState(false); const { sendSealedRelay } = useMTP(); const { getIota } = useUser(); const { addLiveMessage, chatSecret, userId, inputBoxRef, replyTo, setReplyTo, } = useChat(); const { load, save } = useStorage(); const { moveUserIdToTop } = useSession(); const gifPopoverRef = useRef(null); const [gifPopoverOpen, setGifPopoverOpen] = useState(false); const [emojiPopoverOpen, setEmojiPopoverOpen] = useState(false); const recordUse = useRecordEmojiUse(); const { ranks: emojiFrequencies } = useEmojiRanks(); const [gifPopoverSize, setGifPopoverSize] = useState<{ width: number; height: number; }>(); const composerRef = useRef(null); const setComposer = useCallback((controller: InputController | null) => { composerRef.current = controller; }, []); useEffect(() => { const frame = requestAnimationFrame(() => composerRef.current?.focus()); return () => cancelAnimationFrame(frame); }, [userId]); useEffect(() => { if (replyTo === undefined) return; const frame = requestAnimationFrame(() => composerRef.current?.focus()); return () => cancelAnimationFrame(frame); }, [replyTo]); useEffect(() => { const focusComposerOnType = (event: KeyboardEvent) => { const composer = composerRef.current; const target = event.target; if ( !composer || composer.hasFocus() || event.defaultPrevented || event.isComposing || event.ctrlKey || event.metaKey || event.altKey || event.key.length !== 1 ) { return; } if ( target instanceof HTMLElement && (target.isContentEditable || target.closest( 'button, a[href], input, textarea, select, summary, [contenteditable], [role], [tabindex]:not([tabindex="-1"])', )) ) { return; } event.preventDefault(); event.stopPropagation(); composer.focus(); composer.insertText(event.key); }; window.addEventListener("keydown", focusComposerOnType, true); return () => window.removeEventListener("keydown", focusComposerOnType, true); }, []); useHotkey(editLastMessageHotkey, onEditLastMessage, { enabled: value.length === 0, ignoreInputs: false, target: inputBoxRef, }); useEffect(() => { void load("settings.reverse_enter_behavior").then((shouldInvert) => { setInvertEnterBehavior(shouldInvert); }); }, [load]); useEffect(() => { void load("chat_picker_size").then((size) => { if (size) { setGifPopoverSize(size); } }); }, [load]); async function handleSubmit(content = value, preserveContent = false) { if (content.trim() === "") return; const time = new Date().getTime(); const currentValue = content; if (!Number.isSafeInteger(userId) || userId <= 0) { toast("error", "No conversation selected"); return; } if (!chatSecret) { toast("error", "Still getting chat secret..."); return; } log(3, "chat", "purple", "Message send init, adding live message ..."); const reference = addLiveMessage({ NotEncrypted: true, SendTime: time, Content: currentValue, SenderId: ownId, MessageState: "awaiting", ...(replyTo && { ReplyId: replyTo }), }); log(3, "chat", "purple", "Live message added, encrypting..."); const encryptedContent = await encryptChatText( chatSecret, currentValue, ).catch((err) => { toast("error", "Failed to encrypt message", String(err)); reference.setFailed(true); }); if (!encryptedContent) return; log(3, "chat", "purple", "Content encrypted, sending message..."); try { const [ownIota, peerIota] = await Promise.all([ getIota(ownId), getIota(userId), ]); const response = await sendSealedRelay( "MessageSend", { Content: encryptedContent, ReceiverId: userId, SendTime: time, ...(replyTo && { ReplyId: replyTo }), }, { nextHop: { kind: "iota", id: ownIota.IotaId }, finalRecipientId: userId, metadataRecipients: [ { value: ownIota.PublicKey, encoding: "base64" }, { value: peerIota.PublicKey, encoding: "base64" }, ], contentRecipients: [ { value: ownIota.PublicKey, encoding: "base64" }, { value: peerIota.PublicKey, encoding: "base64" }, ], }, ); requireRelaySuccess(response); reference.setMessageState("sent"); } catch (e) { log(0, "Chat", "red", "Failed to send message", e, { ReceiverId: userId, SendTime: time, }); reference.setFailed(true); toast("error", "Failed to send message"); return; } if (replyTo) { setReplyTo(undefined); } log(3, "chat", "purple", "Message sent"); moveUserIdToTop(userId); if (!preserveContent) { setValue(""); } } const isMobile = useIsMobile(); function handleGifPopoverResizeStart( event: React.PointerEvent, ) { const popover = gifPopoverRef.current; if (!popover) return; event.preventDefault(); event.stopPropagation(); const rect = popover.getBoundingClientRect(); const startX = event.clientX; const startY = event.clientY; const startWidth = rect.width; const startHeight = rect.height; const minSize = 40; const maxSize = 720; function clampSize(size: number) { return Math.min(Math.max(size, minSize), maxSize); } function handlePointerMove(moveEvent: PointerEvent) { const nextSize = { width: clampSize(startWidth + startX - moveEvent.clientX), height: clampSize(startHeight + startY - moveEvent.clientY), }; setGifPopoverSize(nextSize); } function handlePointerUp() { const popover = gifPopoverRef.current; if (popover) { const rect = popover.getBoundingClientRect(); void save("chat_picker_size", { width: clampSize(rect.width), height: clampSize(rect.height), }); } window.removeEventListener("pointermove", handlePointerMove); window.removeEventListener("pointerup", handlePointerUp); } window.addEventListener("pointermove", handlePointerMove); window.addEventListener("pointerup", handlePointerUp); } const { ownId, replyMessage, replyUserId } = useReplyMessage(); return (
{/* reply */} {replyTo !== undefined && ( setReplyTo(undefined)} userId={replyUserId ?? ownId} variant="composer" /> )}

Files list

( )} /> { setValue(`${value}${shortcode} `); recordUse(shortcode); setEmojiPopoverOpen(false); }} /> {isMobile ? (
{ void handleSubmit(url, true); setGifPopoverOpen(false); }} />
) : ( ( )} /> event.stopPropagation()} onWheelCapture={(event) => event.stopPropagation()} style={{ ...gifPopoverSize, maxHeight: gifPopoverSize?.height, }} >
{ void handleSubmit(url, true); setGifPopoverOpen(false); }} /> )}
); }