import './compose.css';
import '@github/text-expander-element';

import { msg, plural, t, Trans } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { MenuItem } from '@szhsin/react-menu';
import { deepEqual } from 'fast-equals';
import Fuse from 'fuse.js';
import { forwardRef, memo } from 'preact/compat';
import {
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'preact/hooks';
import { useHotkeys } from 'react-hotkeys-hook';
import stringLength from 'string-length';
// import { detectAll } from 'tinyld/light';
import { uid } from 'uid/single';
import { useDebouncedCallback, useThrottledCallback } from 'use-debounce';
import { useSnapshot } from 'valtio';

import poweredByGiphyURL from '../assets/powered-by-giphy.svg';

import Menu2 from '../components/menu2';
import supportedLanguages from '../data/status-supported-languages';
import urlRegex from '../data/url-regex';
import { api } from '../utils/api';
import db from '../utils/db';
import emojifyText from '../utils/emojify-text';
import i18nDuration from '../utils/i18n-duration';
import isRTL from '../utils/is-rtl';
import localeMatch from '../utils/locale-match';
import localeCode2Text from '../utils/localeCode2Text';
import mem from '../utils/mem';
import openCompose from '../utils/open-compose';
import pmem from '../utils/pmem';
import prettyBytes from '../utils/pretty-bytes';
import { fetchRelationships } from '../utils/relationships';
import shortenNumber from '../utils/shorten-number';
import showToast from '../utils/show-toast';
import states, { saveStatus } from '../utils/states';
import store from '../utils/store';
import {
  getCurrentAccount,
  getCurrentAccountNS,
  getCurrentInstance,
  getCurrentInstanceConfiguration,
} from '../utils/store-utils';
import supports from '../utils/supports';
import useCloseWatcher from '../utils/useCloseWatcher';
import useInterval from '../utils/useInterval';
import visibilityIconsMap from '../utils/visibility-icons-map';

import AccountBlock from './account-block';
// import Avatar from './avatar';
import Icon from './icon';
import Loader from './loader';
import Modal from './modal';
import Status from './status';

const {
  PHANPY_IMG_ALT_API_URL: IMG_ALT_API_URL,
  PHANPY_GIPHY_API_KEY: GIPHY_API_KEY,
} = import.meta.env;

const supportedLanguagesMap = supportedLanguages.reduce((acc, l) => {
  const [code, common, native] = l;
  acc[code] = {
    common,
    native,
  };
  return acc;
}, {});

/* NOTES:
  - Max character limit includes BOTH status text and Content Warning text
*/

const expiryOptions = {
  300: i18nDuration(5, 'minute'),
  1_800: i18nDuration(30, 'minute'),
  3_600: i18nDuration(1, 'hour'),
  21_600: i18nDuration(6, 'hour'),
  86_400: i18nDuration(1, 'day'),
  259_200: i18nDuration(3, 'day'),
  604_800: i18nDuration(1, 'week'),
};
const expirySeconds = Object.keys(expiryOptions);
const oneDay = 24 * 60 * 60;

const expiresInFromExpiresAt = (expiresAt) => {
  if (!expiresAt) return oneDay;
  const delta = (new Date(expiresAt).getTime() - Date.now()) / 1000;
  return expirySeconds.find((s) => s >= delta) || oneDay;
};

const menu = document.createElement('ul');
menu.role = 'listbox';
menu.className = 'text-expander-menu';

// Set IntersectionObserver on menu, reposition it because text-expander doesn't handle it
const windowMargin = 16;
const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      const { left, width } = entry.boundingClientRect;
      const { innerWidth } = window;
      if (left + width > innerWidth) {
        const insetInlineStart = isRTL() ? 'right' : 'left';
        menu.style[insetInlineStart] = innerWidth - width - windowMargin + 'px';
      }
    }
  });
});
observer.observe(menu);

const DEFAULT_LANG = localeMatch(
  [new Intl.DateTimeFormat().resolvedOptions().locale, ...navigator.languages],
  supportedLanguages.map((l) => l[0]),
  'en',
);

// https://github.com/mastodon/mastodon/blob/c4a429ed47e85a6bbf0d470a41cc2f64cf120c19/app/javascript/mastodon/features/compose/util/counter.js
const urlRegexObj = new RegExp(urlRegex.source, urlRegex.flags);
const usernameRegex = /(^|[^\/\w])@(([a-z0-9_]+)@[a-z0-9\.\-]+[a-z0-9]+)/gi;
const urlPlaceholder = '$2xxxxxxxxxxxxxxxxxxxxxxx';
function countableText(inputText) {
  return inputText
    .replace(urlRegexObj, urlPlaceholder)
    .replace(usernameRegex, '$1@$3');
}

// https://github.com/mastodon/mastodon/blob/c03bd2a238741a012aa4b98dc4902d6cf948ab63/app/models/account.rb#L69
const USERNAME_RE = /[a-z0-9_]+([a-z0-9_.-]+[a-z0-9_]+)?/i;
const MENTION_RE = new RegExp(
  `(^|[^=\\/\\w])(@${USERNAME_RE.source}(?:@[\\p{L}\\w.-]+[\\w]+)?)`,
  'uig',
);

// AI-generated, all other regexes are too complicated
const HASHTAG_RE = new RegExp(
  `(^|[^=\\/\\w])(#[a-z0-9_]+([a-z0-9_.]+[a-z0-9_]+)?)(?![\\/\\w])`,
  'ig',
);

// https://github.com/mastodon/mastodon/blob/23e32a4b3031d1da8b911e0145d61b4dd47c4f96/app/models/custom_emoji.rb#L31
const SHORTCODE_RE_FRAGMENT = '[a-zA-Z0-9_]{2,}';
const SCAN_RE = new RegExp(
  `(^|[^=\\/\\w])(:${SHORTCODE_RE_FRAGMENT}:)(?=[^A-Za-z0-9_:]|$)`,
  'g',
);

const segmenter = new Intl.Segmenter();
function escapeHTML(text) {
  return text
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&apos;');
}
function highlightText(text, { maxCharacters = Infinity }) {
  // Exceeded characters limit
  const { composerCharacterCount } = states;
  if (composerCharacterCount > maxCharacters) {
    // Highlight exceeded characters
    let withinLimitHTML = '',
      exceedLimitHTML = '';
    const htmlSegments = segmenter.segment(text);
    for (const { segment, index } of htmlSegments) {
      if (index < maxCharacters) {
        withinLimitHTML += segment;
      } else {
        exceedLimitHTML += segment;
      }
    }
    if (exceedLimitHTML) {
      exceedLimitHTML =
        '<mark class="compose-highlight-exceeded">' +
        escapeHTML(exceedLimitHTML) +
        '</mark>';
    }
    return escapeHTML(withinLimitHTML) + exceedLimitHTML;
  }

  return escapeHTML(text)
    .replace(urlRegexObj, '$2<mark class="compose-highlight-url">$3</mark>') // URLs
    .replace(MENTION_RE, '$1<mark class="compose-highlight-mention">$2</mark>') // Mentions
    .replace(HASHTAG_RE, '$1<mark class="compose-highlight-hashtag">$2</mark>') // Hashtags
    .replace(
      SCAN_RE,
      '$1<mark class="compose-highlight-emoji-shortcode">$2</mark>',
    ); // Emoji shortcodes
}

// const rtf = new Intl.RelativeTimeFormat();
const RTF = mem((locale) => new Intl.RelativeTimeFormat(locale || undefined));

const CUSTOM_EMOJIS_COUNT = 100;

function Compose({
  onClose,
  replyToStatus,
  editStatus,
  draftStatus,
  standalone,
  hasOpener,
}) {
  const { i18n } = useLingui();
  const rtf = RTF(i18n.locale);

  console.warn('RENDER COMPOSER');
  const { masto, instance } = api();
  const [uiState, setUIState] = useState('default');
  const UID = useRef(draftStatus?.uid || uid());
  console.log('Compose UID', UID.current);

  const currentAccount = getCurrentAccount();
  const currentAccountInfo = currentAccount.info;

  const configuration = getCurrentInstanceConfiguration();
  console.log('⚙️ Configuration', configuration);

  const {
    statuses: {
      maxCharacters,
      maxMediaAttachments,
      charactersReservedPerUrl,
    } = {},
    mediaAttachments: {
      supportedMimeTypes = [],
      imageSizeLimit,
      imageMatrixLimit,
      videoSizeLimit,
      videoMatrixLimit,
      videoFrameRateLimit,
    } = {},
    polls: {
      maxOptions,
      maxCharactersPerOption,
      maxExpiration,
      minExpiration,
    } = {},
  } = configuration || {};

  const textareaRef = useRef();
  const spoilerTextRef = useRef();
  const [visibility, setVisibility] = useState('public');
  const [sensitive, setSensitive] = useState(false);
  const [language, setLanguage] = useState(
    store.session.get('currentLanguage') || DEFAULT_LANG,
  );
  const prevLanguage = useRef(language);
  const [mediaAttachments, setMediaAttachments] = useState([]);
  const [poll, setPoll] = useState(null);

  const prefs = store.account.get('preferences') || {};

  const oninputTextarea = () => {
    if (!textareaRef.current) return;
    textareaRef.current.dispatchEvent(new Event('input'));
  };
  const focusTextarea = () => {
    setTimeout(() => {
      if (!textareaRef.current) return;
      // status starts with newline, focus on first position
      if (draftStatus?.status?.startsWith?.('\n')) {
        textareaRef.current.selectionStart = 0;
        textareaRef.current.selectionEnd = 0;
      }
      console.debug('FOCUS textarea');
      textareaRef.current?.focus();
    }, 300);
  };

  useEffect(() => {
    if (replyToStatus) {
      const { spoilerText, visibility, language, sensitive } = replyToStatus;
      if (spoilerText && spoilerTextRef.current) {
        spoilerTextRef.current.value = spoilerText;
      }
      const mentions = new Set([
        replyToStatus.account.acct,
        ...replyToStatus.mentions.map((m) => m.acct),
      ]);
      const allMentions = [...mentions].filter(
        (m) => m !== currentAccountInfo.acct,
      );
      if (allMentions.length > 0) {
        textareaRef.current.value = `${allMentions
          .map((m) => `@${m}`)
          .join(' ')} `;
        oninputTextarea();
      }
      focusTextarea();
      setVisibility(
        visibility === 'public' && prefs['posting:default:visibility']
          ? prefs['posting:default:visibility'].toLowerCase()
          : visibility,
      );
      setLanguage(
        language ||
          prefs['posting:default:language']?.toLowerCase() ||
          DEFAULT_LANG,
      );
      setSensitive(sensitive && !!spoilerText);
    } else if (editStatus) {
      const { visibility, language, sensitive, poll, mediaAttachments } =
        editStatus;
      const composablePoll = !!poll?.options && {
        ...poll,
        options: poll.options.map((o) => o?.title || o),
        expiresIn: poll?.expiresIn || expiresInFromExpiresAt(poll.expiresAt),
      };
      setUIState('loading');
      (async () => {
        try {
          const statusSource = await masto.v1.statuses
            .$select(editStatus.id)
            .source.fetch();
          console.log({ statusSource });
          const { text, spoilerText } = statusSource;
          textareaRef.current.value = text;
          textareaRef.current.dataset.source = text;
          oninputTextarea();
          focusTextarea();
          spoilerTextRef.current.value = spoilerText;
          setVisibility(visibility);
          setLanguage(
            language ||
              prefs['posting:default:language']?.toLowerCase() ||
              DEFAULT_LANG,
          );
          setSensitive(sensitive);
          if (composablePoll) setPoll(composablePoll);
          setMediaAttachments(mediaAttachments);
          setUIState('default');
        } catch (e) {
          console.error(e);
          alert(e?.reason || e);
          setUIState('error');
        }
      })();
    } else {
      focusTextarea();
      console.log('Apply prefs', prefs);
      if (prefs['posting:default:visibility']) {
        setVisibility(prefs['posting:default:visibility'].toLowerCase());
      }
      if (prefs['posting:default:language']) {
        setLanguage(prefs['posting:default:language'].toLowerCase());
      }
      if (prefs['posting:default:sensitive']) {
        setSensitive(!!prefs['posting:default:sensitive']);
      }
    }
    if (draftStatus) {
      const {
        status,
        spoilerText,
        visibility,
        language,
        sensitive,
        poll,
        mediaAttachments,
      } = draftStatus;
      const composablePoll = !!poll?.options && {
        ...poll,
        options: poll.options.map((o) => o?.title || o),
        expiresIn: poll?.expiresIn || expiresInFromExpiresAt(poll.expiresAt),
      };
      textareaRef.current.value = status;
      oninputTextarea();
      focusTextarea();
      if (spoilerText) spoilerTextRef.current.value = spoilerText;
      if (visibility) setVisibility(visibility);
      setLanguage(
        language ||
          prefs['posting:default:language']?.toLowerCase() ||
          DEFAULT_LANG,
      );
      if (sensitive !== null) setSensitive(sensitive);
      if (composablePoll) setPoll(composablePoll);
      if (mediaAttachments) setMediaAttachments(mediaAttachments);
    }
  }, [draftStatus, editStatus, replyToStatus]);

  const formRef = useRef();

  const beforeUnloadCopy = t`You have unsaved changes. Discard this post?`;
  const canClose = () => {
    const { value, dataset } = textareaRef.current;

    // check if loading
    if (uiState === 'loading') {
      console.log('canClose', { uiState });
      return false;
    }

    // check for status and media attachments
    const hasValue = (value || '')
      .trim()
      .replace(/^\p{White_Space}+|\p{White_Space}+$/gu, '');
    const hasMediaAttachments = mediaAttachments.length > 0;
    if (!hasValue && !hasMediaAttachments) {
      console.log('canClose', { value, mediaAttachments });
      return true;
    }

    // check if all media attachments have IDs
    const hasIDMediaAttachments =
      mediaAttachments.length > 0 &&
      mediaAttachments.every((media) => media.id);
    if (hasIDMediaAttachments) {
      console.log('canClose', { hasIDMediaAttachments });
      return true;
    }

    // check if status contains only "@acct", if replying
    const isSelf = replyToStatus?.account.id === currentAccountInfo.id;
    const hasOnlyAcct =
      replyToStatus && value.trim() === `@${replyToStatus.account.acct}`;
    // TODO: check for mentions, or maybe just generic "@username<space>", including multiple mentions like "@username1<space>@username2<space>"
    if (!isSelf && hasOnlyAcct) {
      console.log('canClose', { isSelf, hasOnlyAcct });
      return true;
    }

    // check if status is same with source
    const sameWithSource = value === dataset?.source;
    if (sameWithSource) {
      console.log('canClose', { sameWithSource });
      return true;
    }

    console.log('canClose', {
      value,
      hasMediaAttachments,
      hasIDMediaAttachments,
      poll,
      isSelf,
      hasOnlyAcct,
      sameWithSource,
      uiState,
    });

    return false;
  };

  const confirmClose = () => {
    if (!canClose()) {
      const yes = confirm(beforeUnloadCopy);
      return yes;
    }
    return true;
  };

  useEffect(() => {
    // Show warning if user tries to close window with unsaved changes
    const handleBeforeUnload = (e) => {
      if (!canClose()) {
        e.preventDefault();
        e.returnValue = beforeUnloadCopy;
      }
    };
    window.addEventListener('beforeunload', handleBeforeUnload, {
      capture: true,
    });
    return () =>
      window.removeEventListener('beforeunload', handleBeforeUnload, {
        capture: true,
      });
  }, []);

  const getCharCount = () => {
    const { value } = textareaRef.current;
    const { value: spoilerText } = spoilerTextRef.current;
    return stringLength(countableText(value)) + stringLength(spoilerText);
  };
  const updateCharCount = () => {
    const count = getCharCount();
    states.composerCharacterCount = count;
  };
  useEffect(updateCharCount, []);

  const supportsCloseWatcher = window.CloseWatcher;
  const escDownRef = useRef(false);
  useHotkeys(
    'esc',
    () => {
      escDownRef.current = true;
      // This won't be true if this event is already handled and not propagated 🤞
    },
    {
      enabled: !supportsCloseWatcher,
      enableOnFormTags: true,
    },
  );
  useHotkeys(
    'esc',
    () => {
      if (!standalone && escDownRef.current && confirmClose()) {
        onClose();
      }
      escDownRef.current = false;
    },
    {
      enabled: !supportsCloseWatcher,
      enableOnFormTags: true,
      // Use keyup because Esc keydown will close the confirm dialog on Safari
      keyup: true,
      ignoreEventWhen: (e) => {
        const modals = document.querySelectorAll('#modal-container > *');
        const hasModal = !!modals;
        const hasOnlyComposer =
          modals.length === 1 && modals[0].querySelector('#compose-container');
        return hasModal && !hasOnlyComposer;
      },
    },
  );
  useCloseWatcher(() => {
    if (!standalone && confirmClose()) {
      onClose();
    }
  }, [standalone, confirmClose, onClose]);

  const prevBackgroundDraft = useRef({});
  const draftKey = () => {
    const ns = getCurrentAccountNS();
    return `${ns}#${UID.current}`;
  };
  const saveUnsavedDraft = () => {
    // Not enabling this for editing status
    // I don't think this warrant a draft mode for a status that's already posted
    // Maybe it could be a big edit change but it should be rare
    if (editStatus) return;
    if (states.composerState.minimized) return;
    const key = draftKey();
    const backgroundDraft = {
      key,
      replyTo: replyToStatus
        ? {
            /* Smaller payload of replyToStatus. Reasons:
              - No point storing whole thing
              - Could have media attachments
              - Could be deleted/edited later
            */
            id: replyToStatus.id,
            account: {
              id: replyToStatus.account.id,
              username: replyToStatus.account.username,
              acct: replyToStatus.account.acct,
            },
          }
        : null,
      draftStatus: {
        uid: UID.current,
        status: textareaRef.current.value,
        spoilerText: spoilerTextRef.current.value,
        visibility,
        language,
        sensitive,
        poll,
        mediaAttachments,
      },
    };
    if (
      !deepEqual(backgroundDraft, prevBackgroundDraft.current) &&
      !canClose()
    ) {
      console.debug('not equal', backgroundDraft, prevBackgroundDraft.current);
      db.drafts
        .set(key, {
          ...backgroundDraft,
          state: 'unsaved',
          updatedAt: Date.now(),
        })
        .then(() => {
          console.debug('DRAFT saved', key, backgroundDraft);
        })
        .catch((e) => {
          console.error('DRAFT failed', key, e);
        });
      prevBackgroundDraft.current = structuredClone(backgroundDraft);
    }
  };
  useInterval(saveUnsavedDraft, 5000); // background save every 5s
  useEffect(() => {
    saveUnsavedDraft();
    // If unmounted, means user discarded the draft
    // Also means pop-out 🙈, but it's okay because the pop-out will persist the ID and re-create the draft
    return () => {
      db.drafts.del(draftKey());
    };
  }, []);

  useEffect(() => {
    const handleItems = (e) => {
      const { items } = e.clipboardData || e.dataTransfer;
      const files = [];
      for (let i = 0; i < items.length; i++) {
        const item = items[i];
        if (item.kind === 'file') {
          const file = item.getAsFile();
          if (file && supportedMimeTypes.includes(file.type)) {
            files.push(file);
          }
        }
      }
      if (files.length > 0 && mediaAttachments.length >= maxMediaAttachments) {
        alert(
          plural(maxMediaAttachments, {
            one: 'You can only attach up to 1 file.',
            other: 'You can only attach up to # files.',
          }),
        );
        return;
      }
      console.log({ files });
      if (files.length > 0) {
        e.preventDefault();
        e.stopPropagation();
        // Auto-cut-off files to avoid exceeding maxMediaAttachments
        const max = maxMediaAttachments - mediaAttachments.length;
        const allowedFiles = files.slice(0, max);
        if (allowedFiles.length <= 0) {
          alert(
            plural(maxMediaAttachments, {
              one: 'You can only attach up to 1 file.',
              other: 'You can only attach up to # files.',
            }),
          );
          return;
        }
        const mediaFiles = allowedFiles.map((file) => ({
          file,
          type: file.type,
          size: file.size,
          url: URL.createObjectURL(file),
          id: null,
          description: null,
        }));
        setMediaAttachments([...mediaAttachments, ...mediaFiles]);
      }
    };
    window.addEventListener('paste', handleItems);
    const handleDragover = (e) => {
      // Prevent default if there's files
      if (e.dataTransfer.items.length > 0) {
        e.preventDefault();
        e.stopPropagation();
      }
    };
    window.addEventListener('dragover', handleDragover);
    window.addEventListener('drop', handleItems);
    return () => {
      window.removeEventListener('paste', handleItems);
      window.removeEventListener('dragover', handleDragover);
      window.removeEventListener('drop', handleItems);
    };
  }, [mediaAttachments]);

  const [showMentionPicker, setShowMentionPicker] = useState(false);
  const [showEmoji2Picker, setShowEmoji2Picker] = useState(false);
  const [showGIFPicker, setShowGIFPicker] = useState(false);

  const [autoDetectedLanguages, setAutoDetectedLanguages] = useState(null);
  const [topSupportedLanguages, restSupportedLanguages] = useMemo(() => {
    const topLanguages = [];
    const restLanguages = [];
    const { contentTranslationHideLanguages = [] } = states.settings;
    supportedLanguages.forEach((l) => {
      const [code] = l;
      if (
        code === language ||
        code === prevLanguage.current ||
        code === DEFAULT_LANG ||
        contentTranslationHideLanguages.includes(code) ||
        (autoDetectedLanguages?.length && autoDetectedLanguages.includes(code))
      ) {
        topLanguages.push(l);
      } else {
        restLanguages.push(l);
      }
    });
    topLanguages.sort(([codeA, commonA], [codeB, commonB]) => {
      if (codeA === language) return -1;
      if (codeB === language) return 1;
      return commonA.localeCompare(commonB);
    });
    restLanguages.sort(([codeA, commonA], [codeB, commonB]) =>
      commonA.localeCompare(commonB),
    );
    return [topLanguages, restLanguages];
  }, [language, autoDetectedLanguages]);

  const replyToStatusMonthsAgo = useMemo(
    () =>
      !!replyToStatus?.createdAt &&
      Math.floor(
        (Date.now() - new Date(replyToStatus.createdAt)) /
          (1000 * 60 * 60 * 24 * 30),
      ),
    [replyToStatus],
  );

  const onMinimize = () => {
    saveUnsavedDraft();
    states.composerState.minimized = true;
  };

  return (
    <div id="compose-container-outer">
      <div id="compose-container" class={standalone ? 'standalone' : ''}>
        <div class="compose-top">
          {currentAccountInfo?.avatarStatic && (
            // <Avatar
            //   url={currentAccountInfo.avatarStatic}
            //   size="xl"
            //   alt={currentAccountInfo.username}
            //   squircle={currentAccountInfo?.bot}
            // />
            <AccountBlock
              account={currentAccountInfo}
              accountInstance={currentAccount.instanceURL}
              hideDisplayName
              useAvatarStatic
            />
          )}
          {!standalone ? (
            <span class="compose-controls">
              <button
                type="button"
                class="plain4 pop-button"
                disabled={uiState === 'loading'}
                onClick={() => {
                  // If there are non-ID media attachments (not yet uploaded), show confirmation dialog because they are not going to be passed to the new window
                  // const containNonIDMediaAttachments =
                  //   mediaAttachments.length > 0 &&
                  //   mediaAttachments.some((media) => !media.id);
                  // if (containNonIDMediaAttachments) {
                  //   const yes = confirm(
                  //     'You have media attachments that are not yet uploaded. Opening a new window will discard them and you will need to re-attach them. Are you sure you want to continue?',
                  //   );
                  //   if (!yes) {
                  //     return;
                  //   }
                  // }

                  // const mediaAttachmentsWithIDs = mediaAttachments.filter(
                  //   (media) => media.id,
                  // );

                  const newWin = openCompose({
                    editStatus,
                    replyToStatus,
                    draftStatus: {
                      uid: UID.current,
                      status: textareaRef.current.value,
                      spoilerText: spoilerTextRef.current.value,
                      visibility,
                      language,
                      sensitive,
                      poll,
                      mediaAttachments,
                    },
                  });

                  if (!newWin) {
                    return;
                  }

                  onClose();
                }}
              >
                <Icon icon="popout" alt={t`Pop out`} />
              </button>
              <button
                type="button"
                class="plain4 min-button"
                onClick={onMinimize}
              >
                <Icon icon="minimize" alt={t`Minimize`} />
              </button>{' '}
              <button
                type="button"
                class="light close-button"
                disabled={uiState === 'loading'}
                onClick={() => {
                  if (confirmClose()) {
                    onClose();
                  }
                }}
              >
                <Icon icon="x" alt={t`Close`} />
              </button>
            </span>
          ) : (
            hasOpener && (
              <button
                type="button"
                class="light pop-button"
                disabled={uiState === 'loading'}
                onClick={() => {
                  // If there are non-ID media attachments (not yet uploaded), show confirmation dialog because they are not going to be passed to the new window
                  // const containNonIDMediaAttachments =
                  //   mediaAttachments.length > 0 &&
                  //   mediaAttachments.some((media) => !media.id);
                  // if (containNonIDMediaAttachments) {
                  //   const yes = confirm(
                  //     'You have media attachments that are not yet uploaded. Opening a new window will discard them and you will need to re-attach them. Are you sure you want to continue?',
                  //   );
                  //   if (!yes) {
                  //     return;
                  //   }
                  // }

                  if (!window.opener) {
                    alert(t`Looks like you closed the parent window.`);
                    return;
                  }

                  if (window.opener.__STATES__.showCompose) {
                    if (window.opener.__STATES__.composerState?.publishing) {
                      alert(
                        t`Looks like you already have a compose field open in the parent window and currently publishing. Please wait for it to be done and try again later.`,
                      );
                      return;
                    }

                    let confirmText = t`Looks like you already have a compose field open in the parent window. Popping in this window will discard the changes you made in the parent window. Continue?`;
                    const yes = confirm(confirmText);
                    if (!yes) return;
                  }

                  // const mediaAttachmentsWithIDs = mediaAttachments.filter(
                  //   (media) => media.id,
                  // );

                  onClose({
                    fn: () => {
                      const passData = {
                        editStatus,
                        replyToStatus,
                        draftStatus: {
                          uid: UID.current,
                          status: textareaRef.current.value,
                          spoilerText: spoilerTextRef.current.value,
                          visibility,
                          language,
                          sensitive,
                          poll,
                          mediaAttachments,
                        },
                      };
                      window.opener.__COMPOSE__ = passData; // Pass it here instead of `showCompose` due to some weird proxy issue again
                      if (window.opener.__STATES__.showCompose) {
                        window.opener.__STATES__.showCompose = false;
                        setTimeout(() => {
                          window.opener.__STATES__.showCompose = true;
                        }, 10);
                      } else {
                        window.opener.__STATES__.showCompose = true;
                      }
                      if (window.opener.__STATES__.composerState.minimized) {
                        // Maximize it
                        window.opener.__STATES__.composerState.minimized = false;
                      }
                    },
                  });
                }}
              >
                <Icon icon="popin" alt={t`Pop in`} />
              </button>
            )
          )}
        </div>
        {!!replyToStatus && (
          <div class="status-preview">
            <Status status={replyToStatus} size="s" previewMode />
            <div class="status-preview-legend reply-to">
              {replyToStatusMonthsAgo > 0 ? (
                <Trans>
                  Replying to @
                  {replyToStatus.account.acct || replyToStatus.account.username}
                  &rsquo;s post (
                  <strong>
                    {rtf.format(-replyToStatusMonthsAgo, 'month')}
                  </strong>
                  )
                </Trans>
              ) : (
                <Trans>
                  Replying to @
                  {replyToStatus.account.acct || replyToStatus.account.username}
                  &rsquo;s post
                </Trans>
              )}
            </div>
          </div>
        )}
        {!!editStatus && (
          <div class="status-preview">
            <Status status={editStatus} size="s" previewMode />
            <div class="status-preview-legend">
              <Trans>Editing source post</Trans>
            </div>
          </div>
        )}
        <form
          ref={formRef}
          class={`form-visibility-${visibility}`}
          style={{
            pointerEvents: uiState === 'loading' ? 'none' : 'auto',
            opacity: uiState === 'loading' ? 0.5 : 1,
          }}
          onKeyDown={(e) => {
            if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
              formRef.current.dispatchEvent(
                new Event('submit', { cancelable: true }),
              );
            }
          }}
          onSubmit={(e) => {
            e.preventDefault();

            const formData = new FormData(e.target);
            const entries = Object.fromEntries(formData.entries());
            console.log('ENTRIES', entries);
            let { status, visibility, sensitive, spoilerText } = entries;

            // Pre-cleanup
            sensitive = sensitive === 'on'; // checkboxes return "on" if checked

            // Validation
            /* Let the backend validate this
          if (stringLength(status) > maxCharacters) {
            alert(`Status is too long! Max characters: ${maxCharacters}`);
            return;
          }
          if (
            sensitive &&
            stringLength(status) + stringLength(spoilerText) > maxCharacters
          ) {
            alert(
              `Status and content warning is too long! Max characters: ${maxCharacters}`,
            );
            return;
          }
          */
            if (poll) {
              if (poll.options.length < 2) {
                alert(t`Poll must have at least 2 options`);
                return;
              }
              if (poll.options.some((option) => option === '')) {
                alert(t`Some poll choices are empty`);
                return;
              }
            }
            // TODO: check for URLs and use `charactersReservedPerUrl` to calculate max characters

            if (mediaAttachments.length > 0) {
              // If there are media attachments, check if they have no descriptions
              const hasNoDescriptions = mediaAttachments.some(
                (media) => !media.description?.trim?.(),
              );
              if (hasNoDescriptions) {
                const yes = confirm(
                  t`Some media have no descriptions. Continue?`,
                );
                if (!yes) return;
              }
            }

            // Post-cleanup
            spoilerText = (sensitive && spoilerText) || undefined;
            status = status === '' ? undefined : status;

            // states.composerState.minimized = true;
            states.composerState.publishing = true;
            setUIState('loading');
            (async () => {
              try {
                console.log('MEDIA ATTACHMENTS', mediaAttachments);
                if (mediaAttachments.length > 0) {
                  // Upload media attachments first
                  const mediaPromises = mediaAttachments.map((attachment) => {
                    const { file, description, id } = attachment;
                    console.log('UPLOADING', attachment);
                    if (id) {
                      // If already uploaded
                      return attachment;
                    } else {
                      const params = removeNullUndefined({
                        file,
                        description,
                      });
                      return masto.v2.media.create(params).then((res) => {
                        if (res.id) {
                          attachment.id = res.id;
                        }
                        return res;
                      });
                    }
                  });
                  const results = await Promise.allSettled(mediaPromises);

                  // If any failed, return
                  if (
                    results.some((result) => {
                      return result.status === 'rejected' || !result.value?.id;
                    })
                  ) {
                    states.composerState.publishing = false;
                    states.composerState.publishingError = true;
                    setUIState('error');
                    // Alert all the reasons
                    results.forEach((result) => {
                      if (result.status === 'rejected') {
                        console.error(result);
                        alert(result.reason || t`Attachment #${i} failed`);
                      }
                    });
                    return;
                  }

                  console.log({ results, mediaAttachments });
                }

                /* NOTE:
                Using snakecase here because masto.js's `isObject` returns false for `params`, ONLY happens when opening in pop-out window. This is maybe due to `window.masto` variable being passed from the parent window. The check that failed is `x.constructor === Object`, so maybe the `Object` in new window is different than parent window's?
                Code: https://github.com/neet/masto.js/blob/dd0d649067b6a2b6e60fbb0a96597c373a255b00/src/serializers/is-object.ts#L2

                // TODO: Note above is no longer true in Masto.js v6. Revisit this.
              */
                let params = {
                  status,
                  // spoilerText,
                  spoiler_text: spoilerText,
                  language,
                  sensitive,
                  poll,
                  // mediaIds: mediaAttachments.map((attachment) => attachment.id),
                  media_ids: mediaAttachments.map(
                    (attachment) => attachment.id,
                  ),
                };
                if (editStatus && supports('@mastodon/edit-media-attributes')) {
                  params.media_attributes = mediaAttachments.map(
                    (attachment) => {
                      return {
                        id: attachment.id,
                        description: attachment.description,
                        // focus
                        // thumbnail
                      };
                    },
                  );
                } else if (!editStatus) {
                  params.visibility = visibility;
                  // params.inReplyToId = replyToStatus?.id || undefined;
                  params.in_reply_to_id = replyToStatus?.id || undefined;
                }
                params = removeNullUndefined(params);
                console.log('POST', params);

                let newStatus;
                if (editStatus) {
                  newStatus = await masto.v1.statuses
                    .$select(editStatus.id)
                    .update(params);
                  saveStatus(newStatus, instance, {
                    skipThreading: true,
                  });
                } else {
                  try {
                    newStatus = await masto.v1.statuses.create(params, {
                      requestInit: {
                        headers: {
                          'Idempotency-Key': UID.current,
                        },
                      },
                    });
                  } catch (_) {
                    // If idempotency key fails, try again without it
                    newStatus = await masto.v1.statuses.create(params);
                  }
                }
                states.composerState.minimized = false;
                states.composerState.publishing = false;
                setUIState('default');

                // Close
                onClose({
                  // type: post, reply, edit
                  type: editStatus ? 'edit' : replyToStatus ? 'reply' : 'post',
                  newStatus,
                  instance,
                });
              } catch (e) {
                states.composerState.publishing = false;
                states.composerState.publishingError = true;
                console.error(e);
                alert(e?.reason || e);
                setUIState('error');
              }
            })();
          }}
        >
          <div class="toolbar stretch">
            <input
              ref={spoilerTextRef}
              type="text"
              name="spoilerText"
              placeholder={t`Content warning`}
              disabled={uiState === 'loading'}
              class="spoiler-text-field"
              lang={language}
              spellCheck="true"
              dir="auto"
              style={{
                opacity: sensitive ? 1 : 0,
                pointerEvents: sensitive ? 'auto' : 'none',
              }}
              onInput={() => {
                updateCharCount();
              }}
            />
            <label
              class={`toolbar-button ${sensitive ? 'highlight' : ''}`}
              title={t`Content warning or sensitive media`}
            >
              <input
                name="sensitive"
                type="checkbox"
                checked={sensitive}
                disabled={uiState === 'loading'}
                onChange={(e) => {
                  const sensitive = e.target.checked;
                  setSensitive(sensitive);
                  if (sensitive) {
                    spoilerTextRef.current?.focus();
                  } else {
                    textareaRef.current?.focus();
                  }
                }}
              />
              <Icon icon={`eye-${sensitive ? 'close' : 'open'}`} />
            </label>{' '}
            <label
              class={`toolbar-button ${
                visibility !== 'public' && !sensitive ? 'show-field' : ''
              } ${visibility !== 'public' ? 'highlight' : ''}`}
              title={visibility}
            >
              <Icon icon={visibilityIconsMap[visibility]} alt={visibility} />
              <select
                name="visibility"
                value={visibility}
                onChange={(e) => {
                  setVisibility(e.target.value);
                }}
                disabled={uiState === 'loading' || !!editStatus}
                dir="auto"
              >
                <option value="public">
                  <Trans>Public</Trans>
                </option>
                {(supports('@pleroma/local-visibility-post') ||
                  supports('@akkoma/local-visibility-post')) && (
                  <option value="local">
                    <Trans>Local</Trans>
                  </option>
                )}
                <option value="unlisted">
                  <Trans>Unlisted</Trans>
                </option>
                <option value="private">
                  <Trans>Followers only</Trans>
                </option>
                <option value="direct">
                  <Trans>Private mention</Trans>
                </option>
              </select>
            </label>{' '}
          </div>
          <Textarea
            ref={textareaRef}
            placeholder={
              replyToStatus
                ? t`Post your reply`
                : editStatus
                ? t`Edit your post`
                : t`What are you doing?`
            }
            required={mediaAttachments?.length === 0}
            disabled={uiState === 'loading'}
            lang={language}
            onInput={() => {
              updateCharCount();
            }}
            maxCharacters={maxCharacters}
            performSearch={(params) => {
              const { type, q, limit } = params;
              if (type === 'accounts') {
                return masto.v1.accounts.search.list({
                  q,
                  limit,
                  resolve: false,
                });
              }
              return masto.v2.search.fetch(params);
            }}
            onTrigger={(action) => {
              if (action?.name === 'custom-emojis') {
                setShowEmoji2Picker({
                  defaultSearchTerm: action?.defaultSearchTerm || null,
                });
              } else if (action?.name === 'mention') {
                setShowMentionPicker({
                  defaultSearchTerm: action?.defaultSearchTerm || null,
                });
              } else if (
                action?.name === 'auto-detect-language' &&
                action?.languages
              ) {
                setAutoDetectedLanguages(action.languages);
              }
            }}
          />
          {mediaAttachments?.length > 0 && (
            <div class="media-attachments">
              {mediaAttachments.map((attachment, i) => {
                const { id, file } = attachment;
                const fileID = file?.size + file?.type + file?.name;
                return (
                  <MediaAttachment
                    key={id || fileID || i}
                    attachment={attachment}
                    disabled={uiState === 'loading'}
                    lang={language}
                    onDescriptionChange={(value) => {
                      setMediaAttachments((attachments) => {
                        const newAttachments = [...attachments];
                        newAttachments[i] = {
                          ...newAttachments[i],
                          description: value,
                        };
                        return newAttachments;
                      });
                    }}
                    onRemove={() => {
                      setMediaAttachments((attachments) => {
                        return attachments.filter((_, j) => j !== i);
                      });
                    }}
                  />
                );
              })}
              <label class="media-sensitive">
                <input
                  name="sensitive"
                  type="checkbox"
                  checked={sensitive}
                  disabled={uiState === 'loading'}
                  onChange={(e) => {
                    const sensitive = e.target.checked;
                    setSensitive(sensitive);
                  }}
                />{' '}
                <span>
                  <Trans>Mark media as sensitive</Trans>
                </span>{' '}
                <Icon icon={`eye-${sensitive ? 'close' : 'open'}`} />
              </label>
            </div>
          )}
          {!!poll && (
            <Poll
              lang={language}
              maxOptions={maxOptions}
              maxExpiration={maxExpiration}
              minExpiration={minExpiration}
              maxCharactersPerOption={maxCharactersPerOption}
              poll={poll}
              disabled={uiState === 'loading'}
              onInput={(poll) => {
                if (poll) {
                  const newPoll = { ...poll };
                  setPoll(newPoll);
                } else {
                  setPoll(null);
                }
              }}
            />
          )}
          <div
            class="toolbar wrap"
            style={{
              justifyContent: 'flex-end',
            }}
          >
            <span>
              <label class="toolbar-button">
                <input
                  type="file"
                  accept={supportedMimeTypes.join(',')}
                  multiple={mediaAttachments.length < maxMediaAttachments - 1}
                  disabled={
                    uiState === 'loading' ||
                    mediaAttachments.length >= maxMediaAttachments ||
                    !!poll
                  }
                  onChange={(e) => {
                    const files = e.target.files;
                    if (!files) return;

                    const mediaFiles = Array.from(files).map((file) => ({
                      file,
                      type: file.type,
                      size: file.size,
                      url: URL.createObjectURL(file),
                      id: null, // indicate uploaded state
                      description: null,
                    }));
                    console.log('MEDIA ATTACHMENTS', files, mediaFiles);

                    // Validate max media attachments
                    if (
                      mediaAttachments.length + mediaFiles.length >
                      maxMediaAttachments
                    ) {
                      alert(
                        plural(maxMediaAttachments, {
                          one: 'You can only attach up to 1 file.',
                          other: 'You can only attach up to # files.',
                        }),
                      );
                    } else {
                      setMediaAttachments((attachments) => {
                        return attachments.concat(mediaFiles);
                      });
                    }
                    // Reset
                    e.target.value = '';
                  }}
                />
                <Icon icon="attachment" />
              </label>
              {/* If maxOptions is not defined or defined and is greater than 1, show poll button */}
              {maxOptions == null ||
                (maxOptions > 1 && (
                  <>
                    <button
                      type="button"
                      class="toolbar-button"
                      disabled={
                        uiState === 'loading' ||
                        !!poll ||
                        !!mediaAttachments.length
                      }
                      onClick={() => {
                        setPoll({
                          options: ['', ''],
                          expiresIn: 24 * 60 * 60, // 1 day
                          multiple: false,
                        });
                      }}
                    >
                      <Icon icon="poll" alt={t`Add poll`} />
                    </button>
                  </>
                ))}
              {/* <button
                type="button"
                class="toolbar-button"
                disabled={uiState === 'loading'}
                onClick={() => {
                  setShowMentionPicker(true);
                }}
              >
                <Icon icon="at" />
              </button> */}
              <button
                type="button"
                class="toolbar-button"
                disabled={uiState === 'loading'}
                onClick={() => {
                  setShowEmoji2Picker(true);
                }}
              >
                <Icon icon="emoji2" alt={t`Add custom emoji`} />
              </button>
              {!!states.settings.composerGIFPicker && (
                <button
                  type="button"
                  class="toolbar-button gif-picker-button"
                  disabled={
                    uiState === 'loading' ||
                    mediaAttachments.length >= maxMediaAttachments ||
                    !!poll
                  }
                  onClick={() => {
                    setShowGIFPicker(true);
                  }}
                >
                  <span>GIF</span>
                </button>
              )}
            </span>
            <div class="spacer" />
            {uiState === 'loading' ? (
              <Loader abrupt />
            ) : (
              <CharCountMeter
                maxCharacters={maxCharacters}
                hidden={uiState === 'loading'}
              />
            )}
            <label
              class={`toolbar-button ${
                language !== prevLanguage.current ||
                (autoDetectedLanguages?.length &&
                  !autoDetectedLanguages.includes(language))
                  ? 'highlight'
                  : ''
              }`}
            >
              <span class="icon-text">
                {supportedLanguagesMap[language]?.native}
              </span>
              <select
                name="language"
                value={language}
                onChange={(e) => {
                  const { value } = e.target;
                  setLanguage(value || DEFAULT_LANG);
                  store.session.set('currentLanguage', value || DEFAULT_LANG);
                }}
                disabled={uiState === 'loading'}
                dir="auto"
              >
                {topSupportedLanguages.map(([code, common, native]) => {
                  const commonText = localeCode2Text({
                    code,
                    fallback: common,
                  });
                  const showCommon = commonText !== native;
                  return (
                    <option value={code} key={code}>
                      {showCommon ? `${native} - ${commonText}` : commonText}
                    </option>
                  );
                })}
                <hr />
                {restSupportedLanguages.map(([code, common, native]) => {
                  const commonText = localeCode2Text({
                    code,
                    fallback: common,
                  });
                  const showCommon = commonText !== native;
                  return (
                    <option value={code} key={code}>
                      {showCommon ? `${native} - ${commonText}` : commonText}
                    </option>
                  );
                })}
              </select>
            </label>{' '}
            <button
              type="submit"
              class="large"
              disabled={uiState === 'loading'}
            >
              {replyToStatus
                ? t`Reply`
                : editStatus
                ? t`Update`
                : t({
                    message: 'Post',
                    context: 'Submit button in composer',
                  })}
            </button>
          </div>
        </form>
      </div>
      {showMentionPicker && (
        <Modal
          onClick={(e) => {
            if (e.target === e.currentTarget) {
              setShowMentionPicker(false);
            }
          }}
        >
          <MentionModal
            masto={masto}
            instance={instance}
            onClose={() => {
              setShowMentionPicker(false);
            }}
            defaultSearchTerm={showMentionPicker?.defaultSearchTerm}
            onSelect={(socialAddress) => {
              const textarea = textareaRef.current;
              if (!textarea) return;
              const { selectionStart, selectionEnd } = textarea;
              const text = textarea.value;
              const textBeforeMention = text.slice(0, selectionStart);
              const spaceBeforeMention = textBeforeMention
                ? /[\s\t\n\r]$/.test(textBeforeMention)
                  ? ''
                  : ' '
                : '';
              const textAfterMention = text.slice(selectionEnd);
              const spaceAfterMention = /^[\s\t\n\r]/.test(textAfterMention)
                ? ''
                : ' ';
              const newText =
                textBeforeMention +
                spaceBeforeMention +
                '@' +
                socialAddress +
                spaceAfterMention +
                textAfterMention;
              textarea.value = newText;
              textarea.selectionStart = textarea.selectionEnd =
                selectionEnd +
                1 +
                socialAddress.length +
                spaceAfterMention.length;
              textarea.focus();
              textarea.dispatchEvent(new Event('input'));
            }}
          />
        </Modal>
      )}
      {showEmoji2Picker && (
        <Modal
          onClick={(e) => {
            if (e.target === e.currentTarget) {
              setShowEmoji2Picker(false);
            }
          }}
        >
          <CustomEmojisModal
            masto={masto}
            instance={instance}
            onClose={() => {
              setShowEmoji2Picker(false);
            }}
            defaultSearchTerm={showEmoji2Picker?.defaultSearchTerm}
            onSelect={(emojiShortcode) => {
              const textarea = textareaRef.current;
              if (!textarea) return;
              const { selectionStart, selectionEnd } = textarea;
              const text = textarea.value;
              const textBeforeEmoji = text.slice(0, selectionStart);
              const spaceBeforeEmoji = textBeforeEmoji
                ? /[\s\t\n\r]$/.test(textBeforeEmoji)
                  ? ''
                  : ' '
                : '';
              const textAfterEmoji = text.slice(selectionEnd);
              const spaceAfterEmoji = /^[\s\t\n\r]/.test(textAfterEmoji)
                ? ''
                : ' ';
              const newText =
                textBeforeEmoji +
                spaceBeforeEmoji +
                emojiShortcode +
                spaceAfterEmoji +
                textAfterEmoji;
              textarea.value = newText;
              textarea.selectionStart = textarea.selectionEnd =
                selectionEnd + emojiShortcode.length + spaceAfterEmoji.length;
              textarea.focus();
              textarea.dispatchEvent(new Event('input'));
            }}
          />
        </Modal>
      )}
      {showGIFPicker && (
        <Modal
          onClick={(e) => {
            if (e.target === e.currentTarget) {
              setShowGIFPicker(false);
            }
          }}
        >
          <GIFPickerModal
            onClose={() => setShowGIFPicker(false)}
            onSelect={({ url, type, alt_text }) => {
              console.log('GIF URL', url);
              if (mediaAttachments.length >= maxMediaAttachments) {
                alert(
                  plural(maxMediaAttachments, {
                    one: 'You can only attach up to 1 file.',
                    other: 'You can only attach up to # files.',
                  }),
                );
                return;
              }
              // Download the GIF and insert it as media attachment
              (async () => {
                let theToast;
                try {
                  theToast = showToast({
                    text: t`Downloading GIF…`,
                    duration: -1,
                  });
                  const blob = await fetch(url, {
                    referrerPolicy: 'no-referrer',
                  }).then((res) => res.blob());
                  const file = new File(
                    [blob],
                    type === 'video/mp4' ? 'video.mp4' : 'image.gif',
                    {
                      type,
                    },
                  );
                  const newMediaAttachments = [
                    ...mediaAttachments,
                    {
                      file,
                      type,
                      size: file.size,
                      id: null,
                      description: alt_text || '',
                    },
                  ];
                  setMediaAttachments(newMediaAttachments);
                  theToast?.hideToast?.();
                } catch (err) {
                  console.error(err);
                  theToast?.hideToast?.();
                  showToast(t`Failed to download GIF`);
                }
              })();
            }}
          />
        </Modal>
      )}
    </div>
  );
}

function autoResizeTextarea(textarea) {
  if (!textarea) return;
  const { value, offsetHeight, scrollHeight, clientHeight } = textarea;
  if (offsetHeight < window.innerHeight) {
    // NOTE: This check is needed because the offsetHeight return 50000 (really large number) on first render
    // No idea why it does that, will re-investigate in far future
    const offset = offsetHeight - clientHeight;
    const height = value ? scrollHeight + offset + 'px' : null;
    textarea.style.height = height;
  }
}

async function _getCustomEmojis(instance, masto) {
  const emojis = await masto.v1.customEmojis.list();
  const visibleEmojis = emojis.filter((e) => e.visibleInPicker);
  const searcher = new Fuse(visibleEmojis, {
    keys: ['shortcode'],
    findAllMatches: true,
  });
  return [visibleEmojis, searcher];
}
const getCustomEmojis = pmem(_getCustomEmojis, {
  // Limit by time to reduce memory usage
  // Cached by instance
  matchesArg: (cacheKeyArg, keyArg) => cacheKeyArg.instance === keyArg.instance,
  maxAge: 30 * 60 * 1000, // 30 minutes
});

const detectLangs = async (text) => {
  const { detectAll } = await import('tinyld/light');
  const langs = detectAll(text);
  if (langs?.length) {
    // return max 2
    return langs.slice(0, 2).map((lang) => lang.lang);
  }
  return null;
};

const Textarea = forwardRef((props, ref) => {
  const { masto, instance } = api();
  const [text, setText] = useState(ref.current?.value || '');
  const {
    maxCharacters,
    performSearch = () => {},
    onTrigger = () => {},
    ...textareaProps
  } = props;
  // const snapStates = useSnapshot(states);
  // const charCount = snapStates.composerCharacterCount;

  // const customEmojis = useRef();
  const searcherRef = useRef();
  useEffect(() => {
    getCustomEmojis(instance, masto)
      .then((r) => {
        const [emojis, searcher] = r;
        searcherRef.current = searcher;
      })
      .catch((e) => {
        console.error(e);
      });
  }, []);

  const textExpanderRef = useRef();
  const textExpanderTextRef = useRef('');
  useEffect(() => {
    let handleChange, handleValue, handleCommited;
    if (textExpanderRef.current) {
      handleChange = (e) => {
        // console.log('text-expander-change', e);
        const { key, provide, text } = e.detail;
        textExpanderTextRef.current = text;

        if (text === '') {
          provide(
            Promise.resolve({
              matched: false,
            }),
          );
          return;
        }

        if (key === ':') {
          // const emojis = customEmojis.current.filter((emoji) =>
          //   emoji.shortcode.startsWith(text),
          // );
          // const emojis = filterShortcodes(customEmojis.current, text);
          const results = searcherRef.current?.search(text, {
            limit: 5,
          });
          let html = '';
          results.forEach(({ item: emoji }) => {
            const { shortcode, url } = emoji;
            html += `
                <li role="option" data-value="${encodeHTML(shortcode)}">
                <img src="${encodeHTML(
                  url,
                )}" width="16" height="16" alt="" loading="lazy" /> 
                ${encodeHTML(shortcode)}
              </li>`;
          });
          html += `<li role="option" data-value="" data-more="${text}">${t`More…`}</li>`;
          // console.log({ emojis, html });
          menu.innerHTML = html;
          provide(
            Promise.resolve({
              matched: results.length > 0,
              fragment: menu,
            }),
          );
          return;
        }

        const type = {
          '@': 'accounts',
          '#': 'hashtags',
        }[key];
        provide(
          new Promise((resolve) => {
            const searchResults = performSearch({
              type,
              q: text,
              limit: 5,
            });
            searchResults.then((value) => {
              if (text !== textExpanderTextRef.current) {
                return;
              }
              console.log({ value, type, v: value[type] });
              const results = value[type] || value;
              console.log('RESULTS', value, results);
              let html = '';
              results.forEach((result) => {
                const {
                  name,
                  avatarStatic,
                  displayName,
                  username,
                  acct,
                  emojis,
                  history,
                } = result;
                const displayNameWithEmoji = emojifyText(displayName, emojis);
                // const item = menuItem.cloneNode();
                if (acct) {
                  html += `
                    <li role="option" data-value="${encodeHTML(acct)}">
                      <span class="avatar">
                        <img src="${encodeHTML(
                          avatarStatic,
                        )}" width="16" height="16" alt="" loading="lazy" />
                      </span>
                      <span>
                        <b>${displayNameWithEmoji || username}</b>
                        <br><span class="bidi-isolate">@${encodeHTML(
                          acct,
                        )}</span>
                      </span>
                    </li>
                  `;
                } else {
                  const total = history?.reduce?.(
                    (acc, cur) => acc + +cur.uses,
                    0,
                  );
                  html += `
                    <li role="option" data-value="${encodeHTML(name)}">
                      <span class="grow">#<b>${encodeHTML(name)}</b></span>
                      ${
                        total
                          ? `<span class="count">${shortenNumber(total)}</span>`
                          : ''
                      }
                    </li>
                  `;
                }
              });
              if (type === 'accounts') {
                html += `<li role="option" data-value="" data-more="${text}">${t`More…`}</li>`;
              }
              menu.innerHTML = html;
              console.log('MENU', results, menu);
              resolve({
                matched: results.length > 0,
                fragment: menu,
              });
            });
          }),
        );
      };

      textExpanderRef.current.addEventListener(
        'text-expander-change',
        handleChange,
      );

      handleValue = (e) => {
        const { key, item } = e.detail;
        const { value, more } = item.dataset;
        if (key === ':') {
          e.detail.value = value ? `:${value}:` : '​'; // zero-width space
          if (more) {
            // Prevent adding space after the above value
            e.detail.continue = true;

            setTimeout(() => {
              onTrigger?.({
                name: 'custom-emojis',
                defaultSearchTerm: more,
              });
            }, 300);
          }
        } else if (key === '@') {
          e.detail.value = value ? `@${value} ` : '​'; // zero-width space
          if (more) {
            e.detail.continue = true;
            setTimeout(() => {
              onTrigger?.({
                name: 'mention',
                defaultSearchTerm: more,
              });
            }, 300);
          }
        } else {
          e.detail.value = `${key}${value}`;
        }
      };

      textExpanderRef.current.addEventListener(
        'text-expander-value',
        handleValue,
      );

      handleCommited = (e) => {
        const { input } = e.detail;
        setText(input.value);
        // fire input event
        if (ref.current) {
          const event = new Event('input', { bubbles: true });
          ref.current.dispatchEvent(event);
        }
      };

      textExpanderRef.current.addEventListener(
        'text-expander-committed',
        handleCommited,
      );
    }

    return () => {
      if (textExpanderRef.current) {
        textExpanderRef.current.removeEventListener(
          'text-expander-change',
          handleChange,
        );
        textExpanderRef.current.removeEventListener(
          'text-expander-value',
          handleValue,
        );
        textExpanderRef.current.removeEventListener(
          'text-expander-committed',
          handleCommited,
        );
      }
    };
  }, []);

  useEffect(() => {
    // Resize observer for textarea
    const textarea = ref.current;
    if (!textarea) return;
    const resizeObserver = new ResizeObserver(() => {
      // Get height of textarea, set height to textExpander
      if (textExpanderRef.current) {
        const { height } = textarea.getBoundingClientRect();
        textExpanderRef.current.style.height = height + 'px';
      }
    });
    resizeObserver.observe(textarea);
  }, []);

  const slowHighlightPerf = useRef(0); // increment if slow
  const composeHighlightRef = useRef();
  const throttleHighlightText = useThrottledCallback((text) => {
    if (!composeHighlightRef.current) return;
    if (slowHighlightPerf.current > 3) {
      // After 3 times of lag, disable highlighting
      composeHighlightRef.current.innerHTML = '';
      composeHighlightRef.current = null; // Destroy the whole thing
      throttleHighlightText?.cancel?.();
      return;
    }
    let start;
    let end;
    if (slowHighlightPerf.current <= 3) start = Date.now();
    composeHighlightRef.current.innerHTML =
      highlightText(text, {
        maxCharacters,
      }) + '\n';
    if (slowHighlightPerf.current <= 3) end = Date.now();
    console.debug('HIGHLIGHT PERF', { start, end, diff: end - start });
    if (start && end && end - start > 50) {
      // if slow, increment
      slowHighlightPerf.current++;
    }
    // Newline to prevent multiple line breaks at the end from being collapsed, no idea why
  }, 500);

  const debouncedAutoDetectLanguage = useDebouncedCallback(() => {
    // Make use of the highlightRef to get the DOM
    // Clone the dom
    const dom = composeHighlightRef.current?.cloneNode(true);
    if (!dom) return;
    // Remove mark
    dom.querySelectorAll('mark').forEach((mark) => {
      mark.remove();
    });
    const text = dom.innerText?.trim();
    if (!text) return;
    (async () => {
      const langs = await detectLangs(text);
      if (langs?.length) {
        onTrigger?.({
          name: 'auto-detect-language',
          languages: langs,
        });
      }
    })();
  }, 2000);

  return (
    <text-expander
      ref={textExpanderRef}
      keys="@ # :"
      class="compose-field-container"
    >
      <textarea
        class="compose-field"
        autoCapitalize="sentences"
        autoComplete="on"
        autoCorrect="on"
        spellCheck="true"
        dir="auto"
        rows="6"
        cols="50"
        {...textareaProps}
        ref={ref}
        name="status"
        value={text}
        onKeyDown={(e) => {
          // Get line before cursor position after pressing 'Enter'
          const { key, target } = e;
          if (key === 'Enter' && !(e.ctrlKey || e.metaKey)) {
            try {
              const { value, selectionStart } = target;
              const textBeforeCursor = value.slice(0, selectionStart);
              const lastLine = textBeforeCursor.split('\n').slice(-1)[0];
              if (lastLine) {
                // If line starts with "- " or "12. "
                if (/^\s*(-|\d+\.)\s/.test(lastLine)) {
                  // insert "- " at cursor position
                  const [_, preSpaces, bullet, postSpaces, anything] =
                    lastLine.match(/^(\s*)(-|\d+\.)(\s+)(.+)?/) || [];
                  if (anything) {
                    e.preventDefault();
                    const [number] = bullet.match(/\d+/) || [];
                    const newBullet = number ? `${+number + 1}.` : '-';
                    const text = `\n${preSpaces}${newBullet}${postSpaces}`;
                    target.setRangeText(text, selectionStart, selectionStart);
                    const pos = selectionStart + text.length;
                    target.setSelectionRange(pos, pos);
                  } else {
                    // trim the line before the cursor, then insert new line
                    const pos = selectionStart - lastLine.length;
                    target.setRangeText('', pos, selectionStart);
                  }
                  autoResizeTextarea(target);
                  target.dispatchEvent(new Event('input'));
                }
              }
            } catch (e) {
              // silent fail
              console.error(e);
            }
          }
          if (composeHighlightRef.current) {
            composeHighlightRef.current.scrollTop = target.scrollTop;
          }
        }}
        onInput={(e) => {
          const { target } = e;
          // Replace zero-width space
          const text = target.value.replace(/\u200b/g, '');
          setText(text);
          autoResizeTextarea(target);
          props.onInput?.(e);
          throttleHighlightText(text);
          debouncedAutoDetectLanguage();
        }}
        style={{
          width: '100%',
          height: '4em',
          // '--text-weight': (1 + charCount / 140).toFixed(1) || 1,
        }}
        onScroll={(e) => {
          if (composeHighlightRef.current) {
            const { scrollTop } = e.target;
            composeHighlightRef.current.scrollTop = scrollTop;
          }
        }}
      />
      <div
        ref={composeHighlightRef}
        class="compose-highlight"
        aria-hidden="true"
      />
    </text-expander>
  );
});

function CharCountMeter({ maxCharacters = 500, hidden }) {
  const snapStates = useSnapshot(states);
  const charCount = snapStates.composerCharacterCount;
  const leftChars = maxCharacters - charCount;
  if (hidden) {
    return <span class="char-counter" hidden />;
  }
  return (
    <span
      class="char-counter"
      title={`${leftChars}/${maxCharacters}`}
      style={{
        '--percentage': (charCount / maxCharacters) * 100,
      }}
    >
      <meter
        class={`${
          leftChars <= -10
            ? 'explode'
            : leftChars <= 0
            ? 'danger'
            : leftChars <= 20
            ? 'warning'
            : ''
        }`}
        value={charCount}
        max={maxCharacters}
      />
      <span class="counter">{leftChars}</span>
    </span>
  );
}

function scaleDimension(matrix, matrixLimit, width, height) {
  // matrix = number of pixels
  // matrixLimit = max number of pixels
  // Calculate new width and height, downsize to within the limit, preserve aspect ratio, no decimals
  const scalingFactor = Math.sqrt(matrixLimit / matrix);
  const newWidth = Math.floor(width * scalingFactor);
  const newHeight = Math.floor(height * scalingFactor);
  return { newWidth, newHeight };
}

function MediaAttachment({
  attachment,
  disabled,
  lang,
  onDescriptionChange = () => {},
  onRemove = () => {},
}) {
  const { i18n } = useLingui();
  const [uiState, setUIState] = useState('default');
  const supportsEdit = supports('@mastodon/edit-media-attributes');
  const { type, id, file } = attachment;
  const url = useMemo(
    () => (file ? URL.createObjectURL(file) : attachment.url),
    [file, attachment.url],
  );
  console.log({ attachment });

  const checkMaxError = !!file?.size;
  const configuration = checkMaxError ? getCurrentInstanceConfiguration() : {};
  const {
    mediaAttachments: {
      imageSizeLimit,
      imageMatrixLimit,
      videoSizeLimit,
      videoMatrixLimit,
      videoFrameRateLimit,
    } = {},
  } = configuration || {};

  const [maxError, setMaxError] = useState(() => {
    if (!checkMaxError) return null;
    if (
      type.startsWith('image') &&
      imageSizeLimit &&
      file.size > imageSizeLimit
    ) {
      return {
        type: 'imageSizeLimit',
        details: {
          imageSize: file.size,
          imageSizeLimit,
        },
      };
    } else if (
      type.startsWith('video') &&
      videoSizeLimit &&
      file.size > videoSizeLimit
    ) {
      return {
        type: 'videoSizeLimit',
        details: {
          videoSize: file.size,
          videoSizeLimit,
        },
      };
    }
    return null;
  });

  const [imageMatrix, setImageMatrix] = useState({});
  useEffect(() => {
    if (!checkMaxError || !imageMatrixLimit) return;
    if (imageMatrix?.matrix > imageMatrixLimit) {
      setMaxError({
        type: 'imageMatrixLimit',
        details: {
          imageMatrix: imageMatrix?.matrix,
          imageMatrixLimit,
          width: imageMatrix?.width,
          height: imageMatrix?.height,
        },
      });
    }
  }, [imageMatrix, imageMatrixLimit, checkMaxError]);

  const [videoMatrix, setVideoMatrix] = useState({});
  useEffect(() => {
    if (!checkMaxError || !videoMatrixLimit) return;
    if (videoMatrix?.matrix > videoMatrixLimit) {
      setMaxError({
        type: 'videoMatrixLimit',
        details: {
          videoMatrix: videoMatrix?.matrix,
          videoMatrixLimit,
          width: videoMatrix?.width,
          height: videoMatrix?.height,
        },
      });
    }
  }, [videoMatrix, videoMatrixLimit, checkMaxError]);

  const [description, setDescription] = useState(attachment.description);
  const [suffixType, subtype] = type.split('/');
  const debouncedOnDescriptionChange = useDebouncedCallback(
    onDescriptionChange,
    250,
  );
  useEffect(() => {
    debouncedOnDescriptionChange(description);
  }, [description, debouncedOnDescriptionChange]);

  const [showModal, setShowModal] = useState(false);
  const textareaRef = useRef(null);
  useEffect(() => {
    let timer;
    if (showModal && textareaRef.current) {
      timer = setTimeout(() => {
        textareaRef.current.focus();
      }, 100);
    }
    return () => {
      clearTimeout(timer);
    };
  }, [showModal]);

  const descTextarea = (
    <>
      {!!id && !supportsEdit ? (
        <div class="media-desc">
          <span class="tag">
            <Trans>Uploaded</Trans>
          </span>
          <p title={description}>
            {attachment.description || <i>No description</i>}
          </p>
        </div>
      ) : (
        <textarea
          ref={textareaRef}
          value={description || ''}
          lang={lang}
          placeholder={
            {
              image: t`Image description`,
              video: t`Video description`,
              audio: t`Audio description`,
            }[suffixType]
          }
          autoCapitalize="sentences"
          autoComplete="on"
          autoCorrect="on"
          spellCheck="true"
          dir="auto"
          disabled={disabled || uiState === 'loading'}
          class={uiState === 'loading' ? 'loading' : ''}
          maxlength="1500" // Not unicode-aware :(
          // TODO: Un-hard-code this maxlength, ref: https://github.com/mastodon/mastodon/blob/b59fb28e90bc21d6fd1a6bafd13cfbd81ab5be54/app/models/media_attachment.rb#L39
          onInput={(e) => {
            const { value } = e.target;
            setDescription(value);
            // debouncedOnDescriptionChange(value);
          }}
        ></textarea>
      )}
    </>
  );

  const toastRef = useRef(null);
  useEffect(() => {
    return () => {
      toastRef.current?.hideToast?.();
    };
  }, []);

  const maxErrorToast = useRef(null);

  const maxErrorText = (err) => {
    const { type, details } = err;
    switch (type) {
      case 'imageSizeLimit': {
        const { imageSize, imageSizeLimit } = details;
        return t`File size too large. Uploading might encounter issues. Try reduce the file size from ${prettyBytes(
          imageSize,
        )} to ${prettyBytes(imageSizeLimit)} or lower.`;
      }
      case 'imageMatrixLimit': {
        const { imageMatrix, imageMatrixLimit, width, height } = details;
        const { newWidth, newHeight } = scaleDimension(
          imageMatrix,
          imageMatrixLimit,
          width,
          height,
        );
        return t`Dimension too large. Uploading might encounter issues. Try reduce dimension from ${i18n.number(
          width,
        )}×${i18n.number(height)}px to ${i18n.number(newWidth)}×${i18n.number(
          newHeight,
        )}px.`;
      }
      case 'videoSizeLimit': {
        const { videoSize, videoSizeLimit } = details;
        return t`File size too large. Uploading might encounter issues. Try reduce the file size from ${prettyBytes(
          videoSize,
        )} to ${prettyBytes(videoSizeLimit)} or lower.`;
      }
      case 'videoMatrixLimit': {
        const { videoMatrix, videoMatrixLimit, width, height } = details;
        const { newWidth, newHeight } = scaleDimension(
          videoMatrix,
          videoMatrixLimit,
          width,
          height,
        );
        return t`Dimension too large. Uploading might encounter issues. Try reduce dimension from ${i18n.number(
          width,
        )}×${i18n.number(height)}px to ${i18n.number(newWidth)}×${i18n.number(
          newHeight,
        )}px.`;
      }
      case 'videoFrameRateLimit': {
        // Not possible to detect this on client-side for now
        return t`Frame rate too high. Uploading might encounter issues.`;
      }
    }
  };

  return (
    <>
      <div class="media-attachment">
        <div
          class="media-preview"
          tabIndex="0"
          onClick={() => {
            setShowModal(true);
          }}
        >
          {suffixType === 'image' ? (
            <img
              src={url}
              alt=""
              onLoad={(e) => {
                if (!checkMaxError) return;
                const { naturalWidth, naturalHeight } = e.target;
                setImageMatrix({
                  matrix: naturalWidth * naturalHeight,
                  width: naturalWidth,
                  height: naturalHeight,
                });
              }}
            />
          ) : suffixType === 'video' || suffixType === 'gifv' ? (
            <video
              src={url + '#t=0.1'} // Make Safari show 1st-frame preview
              playsinline
              muted
              disablePictureInPicture
              preload="metadata"
              onLoadedMetadata={(e) => {
                if (!checkMaxError) return;
                const { videoWidth, videoHeight } = e.target;
                if (videoWidth && videoHeight) {
                  setVideoMatrix({
                    matrix: videoWidth * videoHeight,
                    width: videoWidth,
                    height: videoHeight,
                  });
                }
              }}
            />
          ) : suffixType === 'audio' ? (
            <audio src={url} controls />
          ) : null}
        </div>
        {descTextarea}
        <div class="media-aside">
          <button
            type="button"
            class="plain close-button"
            disabled={disabled}
            onClick={onRemove}
          >
            <Icon icon="x" alt={t`Remove`} />
          </button>
          {!!maxError && (
            <button
              type="button"
              class="media-error"
              title={maxErrorText(maxError)}
              onClick={() => {
                if (maxErrorToast.current) {
                  maxErrorToast.current.hideToast();
                }
                maxErrorToast.current = showToast({
                  text: maxErrorText(maxError),
                  duration: 10_000,
                });
              }}
            >
              <Icon icon="alert" alt={t`Error`} />
            </button>
          )}
        </div>
      </div>
      {showModal && (
        <Modal
          onClose={() => {
            setShowModal(false);
          }}
        >
          <div id="media-sheet" class="sheet sheet-max">
            <button
              type="button"
              class="sheet-close"
              onClick={() => {
                setShowModal(false);
              }}
            >
              <Icon icon="x" alt={t`Close`} />
            </button>
            <header>
              <h2>
                {
                  {
                    image: t`Edit image description`,
                    video: t`Edit video description`,
                    audio: t`Edit audio description`,
                  }[suffixType]
                }
              </h2>
            </header>
            <main tabIndex="-1">
              <div class="media-preview">
                {suffixType === 'image' ? (
                  <img src={url} alt="" />
                ) : suffixType === 'video' || suffixType === 'gifv' ? (
                  <video src={url} playsinline controls />
                ) : suffixType === 'audio' ? (
                  <audio src={url} controls />
                ) : null}
              </div>
              <div class="media-form">
                {descTextarea}
                <footer>
                  {suffixType === 'image' &&
                    /^(png|jpe?g|gif|webp)$/i.test(subtype) &&
                    !!states.settings.mediaAltGenerator &&
                    !!IMG_ALT_API_URL && (
                      <Menu2
                        portal={{
                          target: document.body,
                        }}
                        containerProps={{
                          style: {
                            zIndex: 1001,
                          },
                        }}
                        align="center"
                        position="anchor"
                        overflow="auto"
                        menuButton={
                          <button type="button" class="plain">
                            <Icon icon="more" size="l" alt={t`More`} />
                          </button>
                        }
                      >
                        <MenuItem
                          disabled={uiState === 'loading'}
                          onClick={() => {
                            setUIState('loading');
                            toastRef.current = showToast({
                              text: t`Generating description. Please wait…`,
                              duration: -1,
                            });
                            // POST with multipart
                            (async function () {
                              try {
                                const body = new FormData();
                                body.append('image', file);
                                const response = await fetch(IMG_ALT_API_URL, {
                                  method: 'POST',
                                  body,
                                }).then((r) => r.json());
                                if (response.error) {
                                  throw new Error(response.error);
                                }
                                setDescription(response.description);
                              } catch (e) {
                                console.error(e);
                                showToast(
                                  e.message
                                    ? t`Failed to generate description: ${e.message}`
                                    : t`Failed to generate description`,
                                );
                              } finally {
                                setUIState('default');
                                toastRef.current?.hideToast?.();
                              }
                            })();
                          }}
                        >
                          <Icon icon="sparkles2" />
                          {lang && lang !== 'en' ? (
                            <small>
                              <Trans>Generate description…</Trans>
                              <br />
                              (English)
                            </small>
                          ) : (
                            <span>
                              <Trans>Generate description…</Trans>
                            </span>
                          )}
                        </MenuItem>
                        {!!lang && lang !== 'en' && (
                          <MenuItem
                            disabled={uiState === 'loading'}
                            onClick={() => {
                              setUIState('loading');
                              toastRef.current = showToast({
                                text: t`Generating description. Please wait…`,
                                duration: -1,
                              });
                              // POST with multipart
                              (async function () {
                                try {
                                  const body = new FormData();
                                  body.append('image', file);
                                  const params = `?lang=${lang}`;
                                  const response = await fetch(
                                    IMG_ALT_API_URL + params,
                                    {
                                      method: 'POST',
                                      body,
                                    },
                                  ).then((r) => r.json());
                                  if (response.error) {
                                    throw new Error(response.error);
                                  }
                                  setDescription(response.description);
                                } catch (e) {
                                  console.error(e);
                                  showToast(
                                    t`Failed to generate description${
                                      e?.message ? `: ${e.message}` : ''
                                    }`,
                                  );
                                } finally {
                                  setUIState('default');
                                  toastRef.current?.hideToast?.();
                                }
                              })();
                            }}
                          >
                            <Icon icon="sparkles2" />
                            <small>
                              <Trans>Generate description…</Trans>
                              <br />
                              <Trans>
                                ({localeCode2Text(lang)}){' '}
                                <span class="more-insignificant">
                                  — experimental
                                </span>
                              </Trans>
                            </small>
                          </MenuItem>
                        )}
                      </Menu2>
                    )}
                  <button
                    type="button"
                    class="light block"
                    onClick={() => {
                      setShowModal(false);
                    }}
                    disabled={uiState === 'loading'}
                  >
                    <Trans>Done</Trans>
                  </button>
                </footer>
              </div>
            </main>
          </div>
        </Modal>
      )}
    </>
  );
}

function Poll({
  lang,
  poll,
  disabled,
  onInput = () => {},
  maxOptions,
  maxExpiration,
  minExpiration,
  maxCharactersPerOption,
}) {
  const { _ } = useLingui();
  const { options, expiresIn, multiple } = poll;

  return (
    <div class={`poll ${multiple ? 'multiple' : ''}`}>
      <div class="poll-choices">
        {options.map((option, i) => (
          <div class="poll-choice" key={i}>
            <input
              required
              type="text"
              value={option}
              disabled={disabled}
              maxlength={maxCharactersPerOption}
              placeholder={t`Choice ${i + 1}`}
              lang={lang}
              spellCheck="true"
              dir="auto"
              onInput={(e) => {
                const { value } = e.target;
                options[i] = value;
                onInput(poll);
              }}
            />
            <button
              type="button"
              class="plain2 poll-button"
              disabled={disabled || options.length <= 1}
              onClick={() => {
                options.splice(i, 1);
                onInput(poll);
              }}
            >
              <Icon icon="x" size="s" alt={t`Remove`} />
            </button>
          </div>
        ))}
      </div>
      <div class="poll-toolbar">
        <button
          type="button"
          class="plain2 poll-button"
          disabled={disabled || options.length >= maxOptions}
          onClick={() => {
            options.push('');
            onInput(poll);
          }}
        >
          +
        </button>{' '}
        <label class="multiple-choices">
          <input
            type="checkbox"
            checked={multiple}
            disabled={disabled}
            onChange={(e) => {
              const { checked } = e.target;
              poll.multiple = checked;
              onInput(poll);
            }}
          />{' '}
          <Trans>Multiple choices</Trans>
        </label>
        <label class="expires-in">
          <Trans>Duration</Trans>{' '}
          <select
            value={expiresIn}
            disabled={disabled}
            onChange={(e) => {
              const { value } = e.target;
              poll.expiresIn = value;
              onInput(poll);
            }}
          >
            {Object.entries(expiryOptions)
              .filter(([value]) => {
                return value >= minExpiration && value <= maxExpiration;
              })
              .map(([value, label]) => (
                <option value={value} key={value}>
                  {label()}
                </option>
              ))}
          </select>
        </label>
      </div>
      <div class="poll-toolbar">
        <button
          type="button"
          class="plain remove-poll-button"
          disabled={disabled}
          onClick={() => {
            onInput(null);
          }}
        >
          <Trans>Remove poll</Trans>
        </button>
      </div>
    </div>
  );
}

function filterShortcodes(emojis, searchTerm) {
  searchTerm = searchTerm.toLowerCase();

  // Return an array of shortcodes that start with or contain the search term, sorted by relevance and limited to the first 5
  return emojis
    .sort((a, b) => {
      let aLower = a.shortcode.toLowerCase();
      let bLower = b.shortcode.toLowerCase();

      let aStartsWith = aLower.startsWith(searchTerm);
      let bStartsWith = bLower.startsWith(searchTerm);
      let aContains = aLower.includes(searchTerm);
      let bContains = bLower.includes(searchTerm);
      let bothStartWith = aStartsWith && bStartsWith;
      let bothContain = aContains && bContains;

      return bothStartWith
        ? a.length - b.length
        : aStartsWith
        ? -1
        : bStartsWith
        ? 1
        : bothContain
        ? a.length - b.length
        : aContains
        ? -1
        : bContains
        ? 1
        : 0;
    })
    .slice(0, 5);
}

function encodeHTML(str) {
  return str.replace(/[&<>"']/g, function (char) {
    return '&#' + char.charCodeAt(0) + ';';
  });
}

function removeNullUndefined(obj) {
  for (let key in obj) {
    if (obj[key] === null || obj[key] === undefined) {
      delete obj[key];
    }
  }
  return obj;
}

function MentionModal({
  onClose = () => {},
  onSelect = () => {},
  defaultSearchTerm,
}) {
  const { masto } = api();
  const [uiState, setUIState] = useState('default');
  const [accounts, setAccounts] = useState([]);
  const [relationshipsMap, setRelationshipsMap] = useState({});

  const [selectedIndex, setSelectedIndex] = useState(0);

  const loadRelationships = async (accounts) => {
    if (!accounts?.length) return;
    const relationships = await fetchRelationships(accounts, relationshipsMap);
    if (relationships) {
      setRelationshipsMap({
        ...relationshipsMap,
        ...relationships,
      });
    }
  };

  const loadAccounts = (term) => {
    if (!term) return;
    setUIState('loading');
    (async () => {
      try {
        const accounts = await masto.v1.accounts.search.list({
          q: term,
          limit: 40,
          resolve: false,
        });
        setAccounts(accounts);
        loadRelationships(accounts);
        setUIState('default');
      } catch (e) {
        setUIState('error');
        console.error(e);
      }
    })();
  };

  const debouncedLoadAccounts = useDebouncedCallback(loadAccounts, 1000);

  useEffect(() => {
    loadAccounts();
  }, [loadAccounts]);

  const inputRef = useRef();
  useEffect(() => {
    if (inputRef.current) {
      inputRef.current.focus();
      // Put cursor at the end
      if (inputRef.current.value) {
        inputRef.current.selectionStart = inputRef.current.value.length;
        inputRef.current.selectionEnd = inputRef.current.value.length;
      }
    }
  }, []);

  useEffect(() => {
    if (defaultSearchTerm) {
      loadAccounts(defaultSearchTerm);
    }
  }, [defaultSearchTerm]);

  const selectAccount = (account) => {
    const socialAddress = account.acct;
    onSelect(socialAddress);
    onClose();
  };

  useHotkeys(
    'enter',
    () => {
      const selectedAccount = accounts[selectedIndex];
      if (selectedAccount) {
        selectAccount(selectedAccount);
      }
    },
    {
      preventDefault: true,
      enableOnFormTags: ['input'],
    },
  );

  const listRef = useRef();
  useHotkeys(
    'down',
    () => {
      if (selectedIndex < accounts.length - 1) {
        setSelectedIndex(selectedIndex + 1);
      } else {
        setSelectedIndex(0);
      }
      setTimeout(() => {
        const selectedItem = listRef.current.querySelector('.selected');
        if (selectedItem) {
          selectedItem.scrollIntoView({
            behavior: 'smooth',
            block: 'center',
            inline: 'center',
          });
        }
      }, 1);
    },
    {
      preventDefault: true,
      enableOnFormTags: ['input'],
    },
  );

  useHotkeys(
    'up',
    () => {
      if (selectedIndex > 0) {
        setSelectedIndex(selectedIndex - 1);
      } else {
        setSelectedIndex(accounts.length - 1);
      }
      setTimeout(() => {
        const selectedItem = listRef.current.querySelector('.selected');
        if (selectedItem) {
          selectedItem.scrollIntoView({
            behavior: 'smooth',
            block: 'center',
            inline: 'center',
          });
        }
      }, 1);
    },
    {
      preventDefault: true,
      enableOnFormTags: ['input'],
    },
  );

  return (
    <div id="mention-sheet" class="sheet">
      {!!onClose && (
        <button type="button" class="sheet-close" onClick={onClose}>
          <Icon icon="x" alt={t`Close`} />
        </button>
      )}
      <header>
        <form
          onSubmit={(e) => {
            e.preventDefault();
            debouncedLoadAccounts.flush?.();
            // const searchTerm = inputRef.current.value;
            // debouncedLoadAccounts(searchTerm);
          }}
        >
          <input
            ref={inputRef}
            required
            type="search"
            class="block"
            placeholder={t`Search accounts`}
            onInput={(e) => {
              const { value } = e.target;
              debouncedLoadAccounts(value);
            }}
            autocomplete="off"
            autocorrect="off"
            autocapitalize="off"
            spellCheck="false"
            dir="auto"
            defaultValue={defaultSearchTerm || ''}
          />
        </form>
      </header>
      <main>
        {accounts?.length > 0 ? (
          <ul
            ref={listRef}
            class={`accounts-list ${uiState === 'loading' ? 'loading' : ''}`}
          >
            {accounts.map((account, i) => {
              const relationship = relationshipsMap[account.id];
              return (
                <li
                  key={account.id}
                  class={i === selectedIndex ? 'selected' : ''}
                >
                  <AccountBlock
                    avatarSize="xxl"
                    account={account}
                    relationship={relationship}
                    showStats
                    showActivity
                  />
                  <button
                    type="button"
                    class="plain2"
                    onClick={() => {
                      selectAccount(account);
                    }}
                  >
                    <Icon icon="plus" size="xl" alt={t`Add`} />
                  </button>
                </li>
              );
            })}
          </ul>
        ) : uiState === 'loading' ? (
          <div class="ui-state">
            <Loader abrupt />
          </div>
        ) : uiState === 'error' ? (
          <div class="ui-state">
            <p>
              <Trans>Error loading accounts</Trans>
            </p>
          </div>
        ) : null}
      </main>
    </div>
  );
}

function CustomEmojisModal({
  masto,
  instance,
  onClose = () => {},
  onSelect = () => {},
  defaultSearchTerm,
}) {
  const [uiState, setUIState] = useState('default');
  const customEmojisList = useRef([]);
  const [customEmojis, setCustomEmojis] = useState([]);
  const recentlyUsedCustomEmojis = useMemo(
    () => store.account.get('recentlyUsedCustomEmojis') || [],
  );
  const searcherRef = useRef();
  useEffect(() => {
    setUIState('loading');
    (async () => {
      try {
        const [emojis, searcher] = await getCustomEmojis(instance, masto);
        console.log('emojis', emojis);
        searcherRef.current = searcher;
        setCustomEmojis(emojis);
        setUIState('default');
      } catch (e) {
        setUIState('error');
        console.error(e);
      }
    })();
  }, []);

  const customEmojisCatList = useMemo(() => {
    // Group emojis by category
    const emojisCat = {
      '--recent--': recentlyUsedCustomEmojis.filter((emoji) =>
        customEmojis.find((e) => e.shortcode === emoji.shortcode),
      ),
    };
    const othersCat = [];
    customEmojis.forEach((emoji) => {
      customEmojisList.current?.push?.(emoji);
      if (!emoji.category) {
        othersCat.push(emoji);
        return;
      }
      if (!emojisCat[emoji.category]) {
        emojisCat[emoji.category] = [];
      }
      emojisCat[emoji.category].push(emoji);
    });
    if (othersCat.length) {
      emojisCat['--others--'] = othersCat;
    }
    return emojisCat;
  }, [customEmojis]);

  const scrollableRef = useRef();
  const [matches, setMatches] = useState(null);
  const onFind = useCallback(
    (e) => {
      const { value } = e.target;
      if (value) {
        const results = searcherRef.current?.search(value, {
          limit: CUSTOM_EMOJIS_COUNT,
        });
        setMatches(results.map((r) => r.item));
        scrollableRef.current?.scrollTo?.(0, 0);
      } else {
        setMatches(null);
      }
    },
    [customEmojis],
  );
  useEffect(() => {
    if (defaultSearchTerm && customEmojis?.length) {
      onFind({ target: { value: defaultSearchTerm } });
    }
  }, [defaultSearchTerm, onFind, customEmojis]);

  const onSelectEmoji = useCallback(
    (emoji) => {
      onSelect?.(emoji);
      onClose?.();

      queueMicrotask(() => {
        let recentlyUsedCustomEmojis =
          store.account.get('recentlyUsedCustomEmojis') || [];
        const recentlyUsedEmojiIndex = recentlyUsedCustomEmojis.findIndex(
          (e) => e.shortcode === emoji.shortcode,
        );
        if (recentlyUsedEmojiIndex !== -1) {
          // Move emoji to index 0
          recentlyUsedCustomEmojis.splice(recentlyUsedEmojiIndex, 1);
          recentlyUsedCustomEmojis.unshift(emoji);
        } else {
          recentlyUsedCustomEmojis.unshift(emoji);
          // Remove unavailable ones
          recentlyUsedCustomEmojis = recentlyUsedCustomEmojis.filter((e) =>
            customEmojisList.current?.find?.(
              (emoji) => emoji.shortcode === e.shortcode,
            ),
          );
          // Limit to 10
          recentlyUsedCustomEmojis = recentlyUsedCustomEmojis.slice(0, 10);
        }

        // Store back
        store.account.set('recentlyUsedCustomEmojis', recentlyUsedCustomEmojis);
      });
    },
    [onSelect],
  );

  const inputRef = useRef();
  useEffect(() => {
    if (inputRef.current) {
      inputRef.current.focus();
      // Put cursor at the end
      if (inputRef.current.value) {
        inputRef.current.selectionStart = inputRef.current.value.length;
        inputRef.current.selectionEnd = inputRef.current.value.length;
      }
    }
  }, []);

  return (
    <div id="custom-emojis-sheet" class="sheet">
      {!!onClose && (
        <button type="button" class="sheet-close" onClick={onClose}>
          <Icon icon="x" alt={t`Close`} />
        </button>
      )}
      <header>
        <div>
          <b>
            <Trans>Custom emojis</Trans>
          </b>{' '}
          {uiState === 'loading' ? (
            <Loader />
          ) : (
            <small class="insignificant"> • {instance}</small>
          )}
        </div>
        <form
          onSubmit={(e) => {
            e.preventDefault();
            const emoji = matches[0];
            if (emoji) {
              onSelectEmoji(`:${emoji.shortcode}:`);
            }
          }}
        >
          <input
            ref={inputRef}
            type="search"
            placeholder={t`Search emoji`}
            onInput={onFind}
            autocomplete="off"
            autocorrect="off"
            autocapitalize="off"
            spellCheck="false"
            dir="auto"
            defaultValue={defaultSearchTerm || ''}
          />
        </form>
      </header>
      <main ref={scrollableRef}>
        {matches !== null ? (
          <ul class="custom-emojis-matches custom-emojis-list">
            {matches.map((emoji) => (
              <li key={emoji.shortcode} class="custom-emojis-match">
                <CustomEmojiButton
                  emoji={emoji}
                  onClick={() => {
                    onSelectEmoji(`:${emoji.shortcode}:`);
                  }}
                  showCode
                />
              </li>
            ))}
          </ul>
        ) : (
          <div class="custom-emojis-list">
            {uiState === 'error' && (
              <div class="ui-state">
                <p>
                  <Trans>Error loading custom emojis</Trans>
                </p>
              </div>
            )}
            {uiState === 'default' &&
              Object.entries(customEmojisCatList).map(
                ([category, emojis]) =>
                  !!emojis?.length && (
                    <div class="section-container">
                      <div class="section-header">
                        {{
                          '--recent--': t`Recently used`,
                          '--others--': t`Others`,
                        }[category] || category}
                      </div>
                      <CustomEmojisList
                        emojis={emojis}
                        onSelect={onSelectEmoji}
                      />
                    </div>
                  ),
              )}
          </div>
        )}
      </main>
    </div>
  );
}

const CustomEmojisList = memo(({ emojis, onSelect }) => {
  const { i18n } = useLingui();
  const [max, setMax] = useState(CUSTOM_EMOJIS_COUNT);
  const showMore = emojis.length > max;
  return (
    <section>
      {emojis.slice(0, max).map((emoji) => (
        <CustomEmojiButton
          key={emoji.shortcode}
          emoji={emoji}
          onClick={() => {
            onSelect(`:${emoji.shortcode}:`);
          }}
        />
      ))}
      {showMore && (
        <button
          type="button"
          class="plain small"
          onClick={() => setMax(max + CUSTOM_EMOJIS_COUNT)}
        >
          <Trans>{i18n.number(emojis.length - max)} more…</Trans>
        </button>
      )}
    </section>
  );
});

const CustomEmojiButton = memo(({ emoji, onClick, showCode }) => {
  const addEdges = (e) => {
    // Add edge-left or edge-right class based on self position relative to scrollable parent
    // If near left edge, add edge-left, if near right edge, add edge-right
    const buffer = 88;
    const parent = e.currentTarget.closest('main');
    if (parent) {
      const rect = parent.getBoundingClientRect();
      const selfRect = e.currentTarget.getBoundingClientRect();
      const targetClassList = e.currentTarget.classList;
      if (selfRect.left < rect.left + buffer) {
        targetClassList.add('edge-left');
        targetClassList.remove('edge-right');
      } else if (selfRect.right > rect.right - buffer) {
        targetClassList.add('edge-right');
        targetClassList.remove('edge-left');
      } else {
        targetClassList.remove('edge-left', 'edge-right');
      }
    }
  };

  return (
    <button
      type="button"
      className="plain4"
      onClick={onClick}
      data-title={showCode ? undefined : emoji.shortcode}
      onPointerEnter={addEdges}
      onFocus={addEdges}
    >
      <picture>
        {!!emoji.staticUrl && (
          <source
            srcSet={emoji.staticUrl}
            media="(prefers-reduced-motion: reduce)"
          />
        )}
        <img
          className="shortcode-emoji"
          src={emoji.url || emoji.staticUrl}
          alt={emoji.shortcode}
          width="24"
          height="24"
          loading="lazy"
          decoding="async"
        />
      </picture>
      {showCode && (
        <>
          {' '}
          <code>{emoji.shortcode}</code>
        </>
      )}
    </button>
  );
});

const GIFS_PER_PAGE = 20;
function GIFPickerModal({ onClose = () => {}, onSelect = () => {} }) {
  const { i18n } = useLingui();
  const [uiState, setUIState] = useState('default');
  const [results, setResults] = useState([]);
  const formRef = useRef(null);
  const qRef = useRef(null);
  const currentOffset = useRef(0);
  const scrollableRef = useRef(null);

  function fetchGIFs({ offset }) {
    console.log('fetchGIFs', { offset });
    if (!qRef.current?.value) return;
    setUIState('loading');
    scrollableRef.current?.scrollTo?.({
      top: 0,
      left: 0,
      behavior: 'smooth',
    });
    (async () => {
      try {
        const query = {
          api_key: GIPHY_API_KEY,
          q: qRef.current.value,
          rating: 'g',
          limit: GIFS_PER_PAGE,
          bundle: 'messaging_non_clips',
          offset,
          lang: i18n.locale || 'en',
        };
        const response = await fetch(
          'https://api.giphy.com/v1/gifs/search?' + new URLSearchParams(query),
          {
            referrerPolicy: 'no-referrer',
          },
        ).then((r) => r.json());
        currentOffset.current = response.pagination?.offset || 0;
        setResults(response);
        setUIState('results');
      } catch (e) {
        setUIState('error');
        console.error(e);
      }
    })();
  }

  useEffect(() => {
    qRef.current?.focus();
  }, []);

  const debouncedOnInput = useDebouncedCallback(() => {
    fetchGIFs({ offset: 0 });
  }, 1000);

  return (
    <div id="gif-picker-sheet" class="sheet">
      {!!onClose && (
        <button type="button" class="sheet-close" onClick={onClose}>
          <Icon icon="x" alt={t`Close`} />
        </button>
      )}
      <header>
        <form
          ref={formRef}
          onSubmit={(e) => {
            e.preventDefault();
            fetchGIFs({ offset: 0 });
          }}
        >
          <input
            ref={qRef}
            type="search"
            name="q"
            placeholder={t`Search GIFs`}
            required
            autocomplete="off"
            autocorrect="off"
            autocapitalize="off"
            spellCheck="false"
            dir="auto"
            onInput={debouncedOnInput}
          />
          <input
            type="image"
            class="powered-button"
            src={poweredByGiphyURL}
            width="86"
            height="30"
            alt={t`Powered by GIPHY`}
          />
        </form>
      </header>
      <main ref={scrollableRef} class={uiState === 'loading' ? 'loading' : ''}>
        {uiState === 'default' && (
          <div class="ui-state">
            <p class="insignificant">
              <Trans>Type to search GIFs</Trans>
            </p>
          </div>
        )}
        {uiState === 'loading' && !results?.data?.length && (
          <div class="ui-state">
            <Loader abrupt />
          </div>
        )}
        {results?.data?.length > 0 ? (
          <>
            <ul>
              {results.data.map((gif) => {
                const { id, images, title, alt_text } = gif;
                const {
                  fixed_height_small,
                  fixed_height_downsampled,
                  fixed_height,
                  original,
                } = images;
                const theImage = fixed_height_small?.url
                  ? fixed_height_small
                  : fixed_height_downsampled?.url
                  ? fixed_height_downsampled
                  : fixed_height;
                let { url, webp, width, height } = theImage;
                if (+height > 100) {
                  width = (width / height) * 100;
                  height = 100;
                }
                const urlObj = URL.parse(url);
                const strippedURL = urlObj.origin + urlObj.pathname;
                let strippedWebP;
                if (webp) {
                  const webpObj = URL.parse(webp);
                  strippedWebP = webpObj.origin + webpObj.pathname;
                }
                return (
                  <li key={id}>
                    <button
                      type="button"
                      onClick={() => {
                        const { mp4, url } = original;
                        const theURL = mp4 || url;
                        const urlObj = URL.parse(theURL);
                        const strippedURL = urlObj.origin + urlObj.pathname;
                        onClose();
                        onSelect({
                          url: strippedURL,
                          type: mp4 ? 'video/mp4' : 'image/gif',
                          alt_text: alt_text || title,
                        });
                      }}
                    >
                      <figure
                        style={{
                          '--figure-width': width + 'px',
                          // width: width + 'px'
                        }}
                      >
                        <picture>
                          {strippedWebP && (
                            <source srcset={strippedWebP} type="image/webp" />
                          )}
                          <img
                            src={strippedURL}
                            width={width}
                            height={height}
                            loading="lazy"
                            decoding="async"
                            alt={alt_text}
                            referrerpolicy="no-referrer"
                            onLoad={(e) => {
                              e.target.style.backgroundColor = 'transparent';
                            }}
                          />
                        </picture>
                        <figcaption>{alt_text || title}</figcaption>
                      </figure>
                    </button>
                  </li>
                );
              })}
            </ul>
            <p class="pagination">
              {results.pagination?.offset > 0 && (
                <button
                  type="button"
                  class="light small"
                  disabled={uiState === 'loading'}
                  onClick={() => {
                    fetchGIFs({
                      offset: results.pagination?.offset - GIFS_PER_PAGE,
                    });
                  }}
                >
                  <Icon icon="chevron-left" />
                  <span>
                    <Trans>Previous</Trans>
                  </span>
                </button>
              )}
              <span />
              {results.pagination?.offset + results.pagination?.count <
                results.pagination?.total_count && (
                <button
                  type="button"
                  class="light small"
                  disabled={uiState === 'loading'}
                  onClick={() => {
                    fetchGIFs({
                      offset: results.pagination?.offset + GIFS_PER_PAGE,
                    });
                  }}
                >
                  <span>
                    <Trans>Next</Trans>
                  </span>{' '}
                  <Icon icon="chevron-right" />
                </button>
              )}
            </p>
          </>
        ) : (
          uiState === 'results' && (
            <div class="ui-state">
              <p>No results</p>
            </div>
          )
        )}
        {uiState === 'error' && (
          <div class="ui-state">
            <p>
              <Trans>Error loading GIFs</Trans>
            </p>
          </div>
        )}
      </main>
    </div>
  );
}

export default Compose;
