Refactor <ActionsModal> to TypeScript (#34559)
				
					
				
			This commit is contained in:
		@@ -26,11 +26,12 @@ import {
 | 
			
		||||
import { openModal, closeModal } from 'mastodon/actions/modal';
 | 
			
		||||
import { CircularProgress } from 'mastodon/components/circular_progress';
 | 
			
		||||
import { isUserTouching } from 'mastodon/is_mobile';
 | 
			
		||||
import type {
 | 
			
		||||
  MenuItem,
 | 
			
		||||
  ActionMenuItem,
 | 
			
		||||
  ExternalLinkMenuItem,
 | 
			
		||||
import {
 | 
			
		||||
  isMenuItem,
 | 
			
		||||
  isActionItem,
 | 
			
		||||
  isExternalLinkItem,
 | 
			
		||||
} from 'mastodon/models/dropdown_menu';
 | 
			
		||||
import type { MenuItem } from 'mastodon/models/dropdown_menu';
 | 
			
		||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
 | 
			
		||||
 | 
			
		||||
import type { IconProp } from './icon';
 | 
			
		||||
@@ -38,30 +39,6 @@ import { IconButton } from './icon_button';
 | 
			
		||||
 | 
			
		||||
let id = 0;
 | 
			
		||||
 | 
			
		||||
const isMenuItem = (item: unknown): item is MenuItem => {
 | 
			
		||||
  if (item === null) {
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return typeof item === 'object' && 'text' in item;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const isActionItem = (item: unknown): item is ActionMenuItem => {
 | 
			
		||||
  if (!item || !isMenuItem(item)) {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return 'action' in item;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const isExternalLinkItem = (item: unknown): item is ExternalLinkMenuItem => {
 | 
			
		||||
  if (!item || !isMenuItem(item)) {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return 'href' in item;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type RenderItemFn<Item = MenuItem> = (
 | 
			
		||||
  item: Item,
 | 
			
		||||
  index: number,
 | 
			
		||||
@@ -354,6 +331,9 @@ export const Dropdown = <Item = MenuItem,>({
 | 
			
		||||
  const open = currentId === openDropdownId;
 | 
			
		||||
  const activeElement = useRef<HTMLElement | null>(null);
 | 
			
		||||
  const targetRef = useRef<HTMLButtonElement | null>(null);
 | 
			
		||||
  const prefetchAccountId = status
 | 
			
		||||
    ? status.getIn(['account', 'id'])
 | 
			
		||||
    : undefined;
 | 
			
		||||
 | 
			
		||||
  const handleClose = useCallback(() => {
 | 
			
		||||
    if (activeElement.current) {
 | 
			
		||||
@@ -402,8 +382,8 @@ export const Dropdown = <Item = MenuItem,>({
 | 
			
		||||
      } else {
 | 
			
		||||
        onOpen?.();
 | 
			
		||||
 | 
			
		||||
        if (status) {
 | 
			
		||||
          dispatch(fetchRelationships([status.getIn(['account', 'id'])]));
 | 
			
		||||
        if (prefetchAccountId) {
 | 
			
		||||
          dispatch(fetchRelationships([prefetchAccountId]));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (isUserTouching()) {
 | 
			
		||||
@@ -411,7 +391,6 @@ export const Dropdown = <Item = MenuItem,>({
 | 
			
		||||
            openModal({
 | 
			
		||||
              modalType: 'ACTIONS',
 | 
			
		||||
              modalProps: {
 | 
			
		||||
                status,
 | 
			
		||||
                actions: items,
 | 
			
		||||
                onClick: handleItemClick,
 | 
			
		||||
              },
 | 
			
		||||
@@ -431,11 +410,11 @@ export const Dropdown = <Item = MenuItem,>({
 | 
			
		||||
    [
 | 
			
		||||
      dispatch,
 | 
			
		||||
      currentId,
 | 
			
		||||
      prefetchAccountId,
 | 
			
		||||
      scrollKey,
 | 
			
		||||
      onOpen,
 | 
			
		||||
      handleItemClick,
 | 
			
		||||
      open,
 | 
			
		||||
      status,
 | 
			
		||||
      items,
 | 
			
		||||
      handleClose,
 | 
			
		||||
    ],
 | 
			
		||||
 
 | 
			
		||||
@@ -30,9 +30,6 @@ const messages = defineMessages({
 | 
			
		||||
class PrivacyDropdown extends PureComponent {
 | 
			
		||||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    isUserTouching: PropTypes.func,
 | 
			
		||||
    onModalOpen: PropTypes.func,
 | 
			
		||||
    onModalClose: PropTypes.func,
 | 
			
		||||
    value: PropTypes.string.isRequired,
 | 
			
		||||
    onChange: PropTypes.func.isRequired,
 | 
			
		||||
    noDirect: PropTypes.bool,
 | 
			
		||||
 
 | 
			
		||||
@@ -15,16 +15,6 @@ const mapDispatchToProps = dispatch => ({
 | 
			
		||||
    dispatch(changeComposeVisibility(value));
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  isUserTouching,
 | 
			
		||||
  onModalOpen: props => dispatch(openModal({
 | 
			
		||||
    modalType: 'ACTIONS',
 | 
			
		||||
    modalProps: props,
 | 
			
		||||
  })),
 | 
			
		||||
  onModalClose: () => dispatch(closeModal({
 | 
			
		||||
    modalType: undefined,
 | 
			
		||||
    ignoreFocus: false,
 | 
			
		||||
  })),
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default connect(mapStateToProps, mapDispatchToProps)(PrivacyDropdown);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,48 +0,0 @@
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
 | 
			
		||||
import classNames from 'classnames';
 | 
			
		||||
 | 
			
		||||
import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
			
		||||
import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
			
		||||
 | 
			
		||||
import { IconButton } from '../../../components/icon_button';
 | 
			
		||||
 | 
			
		||||
export default class ActionsModal extends ImmutablePureComponent {
 | 
			
		||||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    status: ImmutablePropTypes.map,
 | 
			
		||||
    actions: PropTypes.array,
 | 
			
		||||
    onClick: PropTypes.func,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  renderAction = (action, i) => {
 | 
			
		||||
    if (action === null) {
 | 
			
		||||
      return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const { icon = null, iconComponent = null, text, meta = null, active = false, href = '#' } = action;
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <li key={`${text}-${i}`}>
 | 
			
		||||
        <a href={href} target='_blank' rel='noopener' onClick={this.props.onClick} data-index={i} className={classNames({ active })}>
 | 
			
		||||
          {icon && <IconButton title={text} icon={icon} iconComponent={iconComponent} role='presentation' tabIndex={-1} inverted />}
 | 
			
		||||
          <div>
 | 
			
		||||
            <div className={classNames({ 'actions-modal__item-label': !!meta })}>{text}</div>
 | 
			
		||||
            <div>{meta}</div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </a>
 | 
			
		||||
      </li>
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className='modal-root__modal actions-modal'>
 | 
			
		||||
        <ul className={classNames({ 'with-status': !!status })}>
 | 
			
		||||
          {this.props.actions.map(this.renderAction)}
 | 
			
		||||
        </ul>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,65 @@
 | 
			
		||||
import classNames from 'classnames';
 | 
			
		||||
import { Link } from 'react-router-dom';
 | 
			
		||||
 | 
			
		||||
import type { MenuItem } from 'mastodon/models/dropdown_menu';
 | 
			
		||||
import {
 | 
			
		||||
  isActionItem,
 | 
			
		||||
  isExternalLinkItem,
 | 
			
		||||
} from 'mastodon/models/dropdown_menu';
 | 
			
		||||
 | 
			
		||||
export const ActionsModal: React.FC<{
 | 
			
		||||
  actions: MenuItem[];
 | 
			
		||||
  onClick: React.MouseEventHandler;
 | 
			
		||||
}> = ({ actions, onClick }) => (
 | 
			
		||||
  <div className='modal-root__modal actions-modal'>
 | 
			
		||||
    <ul>
 | 
			
		||||
      {actions.map((option, i: number) => {
 | 
			
		||||
        if (option === null) {
 | 
			
		||||
          return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const { text, dangerous } = option;
 | 
			
		||||
 | 
			
		||||
        let element: React.ReactElement;
 | 
			
		||||
 | 
			
		||||
        if (isActionItem(option)) {
 | 
			
		||||
          element = (
 | 
			
		||||
            <button onClick={onClick} data-index={i}>
 | 
			
		||||
              {text}
 | 
			
		||||
            </button>
 | 
			
		||||
          );
 | 
			
		||||
        } else if (isExternalLinkItem(option)) {
 | 
			
		||||
          element = (
 | 
			
		||||
            <a
 | 
			
		||||
              href={option.href}
 | 
			
		||||
              target={option.target ?? '_target'}
 | 
			
		||||
              data-method={option.method}
 | 
			
		||||
              rel='noopener'
 | 
			
		||||
              onClick={onClick}
 | 
			
		||||
              data-index={i}
 | 
			
		||||
            >
 | 
			
		||||
              {text}
 | 
			
		||||
            </a>
 | 
			
		||||
          );
 | 
			
		||||
        } else {
 | 
			
		||||
          element = (
 | 
			
		||||
            <Link to={option.to} onClick={onClick} data-index={i}>
 | 
			
		||||
              {text}
 | 
			
		||||
            </Link>
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return (
 | 
			
		||||
          <li
 | 
			
		||||
            className={classNames({
 | 
			
		||||
              'dropdown-menu__item--dangerous': dangerous,
 | 
			
		||||
            })}
 | 
			
		||||
            key={`${text}-${i}`}
 | 
			
		||||
          >
 | 
			
		||||
            {element}
 | 
			
		||||
          </li>
 | 
			
		||||
        );
 | 
			
		||||
      })}
 | 
			
		||||
    </ul>
 | 
			
		||||
  </div>
 | 
			
		||||
);
 | 
			
		||||
@@ -24,7 +24,7 @@ import { getScrollbarWidth } from 'mastodon/utils/scrollbar';
 | 
			
		||||
 | 
			
		||||
import BundleContainer from '../containers/bundle_container';
 | 
			
		||||
 | 
			
		||||
import ActionsModal from './actions_modal';
 | 
			
		||||
import { ActionsModal } from './actions_modal';
 | 
			
		||||
import AudioModal from './audio_modal';
 | 
			
		||||
import { BoostModal } from './boost_modal';
 | 
			
		||||
import {
 | 
			
		||||
 
 | 
			
		||||
@@ -22,3 +22,29 @@ export type MenuItem =
 | 
			
		||||
  | LinkMenuItem
 | 
			
		||||
  | ExternalLinkMenuItem
 | 
			
		||||
  | null;
 | 
			
		||||
 | 
			
		||||
export const isMenuItem = (item: unknown): item is MenuItem => {
 | 
			
		||||
  if (item === null) {
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return typeof item === 'object' && 'text' in item;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const isActionItem = (item: unknown): item is ActionMenuItem => {
 | 
			
		||||
  if (!item || !isMenuItem(item)) {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return 'action' in item;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const isExternalLinkItem = (
 | 
			
		||||
  item: unknown,
 | 
			
		||||
): item is ExternalLinkMenuItem => {
 | 
			
		||||
  if (!item || !isMenuItem(item)) {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return 'href' in item;
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -6484,55 +6484,38 @@ a.status-card {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.actions-modal {
 | 
			
		||||
  border-radius: 8px 8px 0 0;
 | 
			
		||||
  background: var(--dropdown-background-color);
 | 
			
		||||
  backdrop-filter: var(--background-filter);
 | 
			
		||||
  border-color: var(--dropdown-border-color);
 | 
			
		||||
  box-shadow: var(--dropdown-shadow);
 | 
			
		||||
  max-height: 80vh;
 | 
			
		||||
  max-width: 80vw;
 | 
			
		||||
 | 
			
		||||
  .actions-modal__item-label {
 | 
			
		||||
    font-weight: 500;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ul {
 | 
			
		||||
    overflow-y: auto;
 | 
			
		||||
    flex-shrink: 0;
 | 
			
		||||
    max-height: 80vh;
 | 
			
		||||
    padding-bottom: 8px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    &.with-status {
 | 
			
		||||
      max-height: calc(80vh - 75px);
 | 
			
		||||
    }
 | 
			
		||||
  a,
 | 
			
		||||
  button {
 | 
			
		||||
    color: inherit;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    padding: 16px;
 | 
			
		||||
    font-size: 15px;
 | 
			
		||||
    line-height: 21px;
 | 
			
		||||
    background: transparent;
 | 
			
		||||
    border: none;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    text-decoration: none;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    box-sizing: border-box;
 | 
			
		||||
 | 
			
		||||
    li:empty {
 | 
			
		||||
      margin: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    li:not(:empty) {
 | 
			
		||||
      a {
 | 
			
		||||
        color: $primary-text-color;
 | 
			
		||||
        display: flex;
 | 
			
		||||
        padding: 12px 16px;
 | 
			
		||||
        font-size: 15px;
 | 
			
		||||
        align-items: center;
 | 
			
		||||
        text-decoration: none;
 | 
			
		||||
 | 
			
		||||
        &,
 | 
			
		||||
        button {
 | 
			
		||||
          transition: none;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        &.active,
 | 
			
		||||
        &:hover,
 | 
			
		||||
        &:active,
 | 
			
		||||
        &:focus {
 | 
			
		||||
          &,
 | 
			
		||||
          button {
 | 
			
		||||
            background: $ui-highlight-color;
 | 
			
		||||
            color: $primary-text-color;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        button:first-child {
 | 
			
		||||
          margin-inline-end: 10px;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    &:hover,
 | 
			
		||||
    &:active,
 | 
			
		||||
    &:focus {
 | 
			
		||||
      background: var(--dropdown-border-color);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user