import { ChangeEvent, Dispatch, SetStateAction, useCallback, useEffect, useRef, useState } from 'react' import { FormattedMessage, useIntl } from 'react-intl' import { FaEnvelope, FaFaceLaughBeam, FaGlobe, FaListCheck, FaLock, FaLockOpen, FaPaperPlane, FaPaperclip, FaPencil, FaXmark } from 'react-icons/fa6' import { Entity, MegalodonInterface } from 'megalodon' import { useToast } from '@/provider/toast' import Picker from '@emoji-mart/react' import { data } from '@/utils/emojiData' import EditMedia from './EditMedia' import { Button, Checkbox, IconButton, Input, List, ListItem, Option, Popover, PopoverContent, PopoverHandler, Radio, Select, Switch, Textarea } from '@material-tailwind/react' type Props = { client: MegalodonInterface in_reply_to?: Entity.Status } type Poll = { options: Array expires_in: number multiple: boolean } export default function Compose(props: Props) { const [body, setBody] = useState('') const [visibility, setVisibility] = useState<'public' | 'unlisted' | 'private' | 'direct'>('public') const [cw, setCW] = useState(false) const [spoiler, setSpoiler] = useState('') const [attachments, setAttachments] = useState>([]) const [poll, setPoll] = useState(null) const [loading, setLoading] = useState(false) const [editMedia, setEditMedia] = useState() const [maxCharacters, setMaxCharacters] = useState(null) const [remaining, setRemaining] = useState(null) const [popoverVisibility, setPopoverVisibility] = useState(false) const [popoverEmoji, setPopoverEmoji] = useState(false) const { formatMessage } = useIntl() const uploaderRef = useRef(null) const showToast = useToast() const textareaRef = useRef(null) useEffect(() => { if (!props.client) return const f = async () => { const instance = await props.client.getInstance() if (instance.data.configuration.statuses.max_characters) { setMaxCharacters(instance.data.configuration.statuses.max_characters) } } f() }, [props.client]) useEffect(() => { if (!cw) { setSpoiler('') } }, [cw]) useEffect(() => { if (props.in_reply_to) { const f = async () => { const myself = await props.client.verifyAccountCredentials() const mentionAccounts = [props.in_reply_to.account.acct, ...props.in_reply_to.mentions.map(a => a.acct)] .filter((a, i, self) => self.indexOf(a) === i) .filter(a => a !== myself.data.username) setBody(`${mentionAccounts.map(m => `@${m}`).join(' ')} `) setVisibility(props.in_reply_to.visibility) } f() } }, [props.in_reply_to]) useEffect(() => { if (maxCharacters) { setRemaining(maxCharacters - body.length - spoiler.length) } }, [maxCharacters, body, spoiler]) const post = async () => { if (body.length === 0) return let options = { visibility: visibility } if (props.in_reply_to) { options = Object.assign({}, options, { in_reply_to_id: props.in_reply_to.id }) } if (cw) { options = Object.assign({}, options, { spoiler_text: spoiler }) } if (attachments.length > 0) { options = Object.assign({}, options, { media_ids: attachments.map(m => m.id) }) } const sensitive = document.getElementById('sensitive') as HTMLInputElement if (sensitive && sensitive.checked) { options = Object.assign({}, options, { sensitive: sensitive.checked }) } if (poll && poll.options.length > 0) { options = Object.assign({}, options, { poll: poll }) } setLoading(true) try { await props.client.postStatus(body, options) reset() } catch (err) { console.error(err) showToast({ text: formatMessage({ id: 'alert.compose.post_failed' }), type: 'failure' }) } finally { setLoading(false) } } const reset = () => { setBody('') setSpoiler('') setCW(false) setAttachments([]) setPoll(null) setMaxCharacters(null) } const handleKeyPress = useCallback( (event: KeyboardEvent) => { if ((event.ctrlKey === true && event.key === 'Enter') || (event.metaKey === true && event.key === 'Enter')) { post() } }, [post] ) useEffect(() => { textareaRef.current?.addEventListener('keydown', handleKeyPress) return () => { textareaRef.current?.removeEventListener('keydown', handleKeyPress) } }, [handleKeyPress]) const selectFile = () => { if (uploaderRef.current) { uploaderRef.current.click() } } const fileChanged = async (event: ChangeEvent) => { const file = event.target.files?.item(0) if (file === null || file === undefined) { return } if (attachments.length >= 4) { showToast({ text: formatMessage({ id: 'alert.validation.attachment_length' }, { limit: 4 }), type: 'failure' }) return } if (!file.type.includes('image') && !file.type.includes('video')) { showToast({ text: formatMessage({ id: 'alert.validation.attachment_type' }), type: 'failure' }) return } setLoading(true) try { const res = await props.client.uploadMedia(file) setAttachments(current => [...current, res.data]) } catch { showToast({ text: formatMessage({ id: 'alert.upload_error' }), type: 'failure' }) } finally { setLoading(false) } } const removeFile = (index: number) => { setAttachments(current => current.filter((_, i) => i !== index)) } const openDescription = (index: number) => { setEditMedia(attachments[index]) } const closeDescription = () => { setEditMedia(undefined) } const togglePoll = () => { if (poll) { setPoll(null) } else { setPoll(defaultPoll()) } } const onEmojiSelect = emoji => { const textarea = document.getElementById('body') as HTMLTextAreaElement const cursor = textarea.selectionStart if (emoji.native) { setBody(current => `${current.slice(0, cursor)}${emoji.native} ${current.slice(cursor)}`) } else if (emoji.shortcodes) { setBody(current => `${current.slice(0, cursor)}${emoji.shortcodes} ${current.slice(cursor)}`) } setPopoverEmoji(false) } return (
{cw && ( setSpoiler(ev.target.value)} placeholder={formatMessage({ id: 'compose.spoiler.placeholder' })} /> )}