301 lines
		
	
	
	
		
			7.8 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			301 lines
		
	
	
	
		
			7.8 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import { useCallback, useState } from 'react';
 | |
| 
 | |
| import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
 | |
| 
 | |
| import classNames from 'classnames';
 | |
| 
 | |
| import AddIcon from '@/material-icons/400-24px/add.svg?react';
 | |
| import ArrowBackIcon from '@/material-icons/400-24px/arrow_back.svg?react';
 | |
| import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
 | |
| import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
 | |
| import CloseIcon from '@/material-icons/400-24px/close.svg?react';
 | |
| import SettingsIcon from '@/material-icons/400-24px/settings.svg?react';
 | |
| import type { IconProp } from 'mastodon/components/icon';
 | |
| import { Icon } from 'mastodon/components/icon';
 | |
| import { ButtonInTabsBar } from 'mastodon/features/ui/util/columns_context';
 | |
| import { useIdentity } from 'mastodon/identity_context';
 | |
| 
 | |
| import { useAppHistory } from './router';
 | |
| 
 | |
| const messages = defineMessages({
 | |
|   show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
 | |
|   hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' },
 | |
|   moveLeft: {
 | |
|     id: 'column_header.moveLeft_settings',
 | |
|     defaultMessage: 'Move column to the left',
 | |
|   },
 | |
|   moveRight: {
 | |
|     id: 'column_header.moveRight_settings',
 | |
|     defaultMessage: 'Move column to the right',
 | |
|   },
 | |
|   back: { id: 'column_back_button.label', defaultMessage: 'Back' },
 | |
| });
 | |
| 
 | |
| const BackButton: React.FC<{
 | |
|   onlyIcon: boolean;
 | |
| }> = ({ onlyIcon }) => {
 | |
|   const history = useAppHistory();
 | |
|   const intl = useIntl();
 | |
| 
 | |
|   const handleBackClick = useCallback(() => {
 | |
|     if (history.location.state?.fromMastodon) {
 | |
|       history.goBack();
 | |
|     } else {
 | |
|       history.push('/');
 | |
|     }
 | |
|   }, [history]);
 | |
| 
 | |
|   return (
 | |
|     <button
 | |
|       onClick={handleBackClick}
 | |
|       className={classNames('column-header__back-button', {
 | |
|         compact: onlyIcon,
 | |
|       })}
 | |
|       aria-label={intl.formatMessage(messages.back)}
 | |
|     >
 | |
|       <Icon
 | |
|         id='chevron-left'
 | |
|         icon={ArrowBackIcon}
 | |
|         className='column-back-button__icon'
 | |
|       />
 | |
|       {!onlyIcon && (
 | |
|         <FormattedMessage id='column_back_button.label' defaultMessage='Back' />
 | |
|       )}
 | |
|     </button>
 | |
|   );
 | |
| };
 | |
| 
 | |
| export interface Props {
 | |
|   title?: string;
 | |
|   icon?: string;
 | |
|   iconComponent?: IconProp;
 | |
|   active?: boolean;
 | |
|   children?: React.ReactNode;
 | |
|   pinned?: boolean;
 | |
|   multiColumn?: boolean;
 | |
|   extraButton?: React.ReactNode;
 | |
|   showBackButton?: boolean;
 | |
|   placeholder?: boolean;
 | |
|   appendContent?: React.ReactNode;
 | |
|   collapseIssues?: boolean;
 | |
|   onClick?: () => void;
 | |
|   onMove?: (arg0: number) => void;
 | |
|   onPin?: () => void;
 | |
| }
 | |
| 
 | |
| export const ColumnHeader: React.FC<Props> = ({
 | |
|   title,
 | |
|   icon,
 | |
|   iconComponent,
 | |
|   active,
 | |
|   children,
 | |
|   pinned,
 | |
|   multiColumn,
 | |
|   extraButton,
 | |
|   showBackButton,
 | |
|   placeholder,
 | |
|   appendContent,
 | |
|   collapseIssues,
 | |
|   onClick,
 | |
|   onMove,
 | |
|   onPin,
 | |
| }) => {
 | |
|   const intl = useIntl();
 | |
|   const { signedIn } = useIdentity();
 | |
|   const history = useAppHistory();
 | |
|   const [collapsed, setCollapsed] = useState(true);
 | |
|   const [animating, setAnimating] = useState(false);
 | |
| 
 | |
|   const handleToggleClick = useCallback(
 | |
|     (e: React.MouseEvent) => {
 | |
|       e.stopPropagation();
 | |
|       setCollapsed((value) => !value);
 | |
|       setAnimating(true);
 | |
|     },
 | |
|     [setCollapsed, setAnimating],
 | |
|   );
 | |
| 
 | |
|   const handleTitleClick = useCallback(() => {
 | |
|     onClick?.();
 | |
|   }, [onClick]);
 | |
| 
 | |
|   const handleMoveLeft = useCallback(() => {
 | |
|     onMove?.(-1);
 | |
|   }, [onMove]);
 | |
| 
 | |
|   const handleMoveRight = useCallback(() => {
 | |
|     onMove?.(1);
 | |
|   }, [onMove]);
 | |
| 
 | |
|   const handleTransitionEnd = useCallback(() => {
 | |
|     setAnimating(false);
 | |
|   }, [setAnimating]);
 | |
| 
 | |
|   const handlePin = useCallback(() => {
 | |
|     if (!pinned) {
 | |
|       history.replace('/');
 | |
|     }
 | |
| 
 | |
|     onPin?.();
 | |
|   }, [history, pinned, onPin]);
 | |
| 
 | |
|   const wrapperClassName = classNames('column-header__wrapper', {
 | |
|     active,
 | |
|   });
 | |
| 
 | |
|   const buttonClassName = classNames('column-header', {
 | |
|     active,
 | |
|   });
 | |
| 
 | |
|   const collapsibleClassName = classNames('column-header__collapsible', {
 | |
|     collapsed,
 | |
|     animating,
 | |
|   });
 | |
| 
 | |
|   const collapsibleButtonClassName = classNames('column-header__button', {
 | |
|     active: !collapsed,
 | |
|   });
 | |
| 
 | |
|   let extraContent, pinButton, moveButtons, backButton, collapseButton;
 | |
| 
 | |
|   if (children) {
 | |
|     extraContent = (
 | |
|       <div key='extra-content' className='column-header__collapsible__extra'>
 | |
|         {children}
 | |
|       </div>
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   if (multiColumn && pinned) {
 | |
|     pinButton = (
 | |
|       <button
 | |
|         className='text-btn column-header__setting-btn'
 | |
|         onClick={handlePin}
 | |
|       >
 | |
|         <Icon id='times' icon={CloseIcon} />{' '}
 | |
|         <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' />
 | |
|       </button>
 | |
|     );
 | |
| 
 | |
|     moveButtons = (
 | |
|       <div className='column-header__setting-arrows'>
 | |
|         <button
 | |
|           title={intl.formatMessage(messages.moveLeft)}
 | |
|           aria-label={intl.formatMessage(messages.moveLeft)}
 | |
|           className='icon-button column-header__setting-btn'
 | |
|           onClick={handleMoveLeft}
 | |
|         >
 | |
|           <Icon id='chevron-left' icon={ChevronLeftIcon} />
 | |
|         </button>
 | |
|         <button
 | |
|           title={intl.formatMessage(messages.moveRight)}
 | |
|           aria-label={intl.formatMessage(messages.moveRight)}
 | |
|           className='icon-button column-header__setting-btn'
 | |
|           onClick={handleMoveRight}
 | |
|         >
 | |
|           <Icon id='chevron-right' icon={ChevronRightIcon} />
 | |
|         </button>
 | |
|       </div>
 | |
|     );
 | |
|   } else if (multiColumn && onPin) {
 | |
|     pinButton = (
 | |
|       <button
 | |
|         className='text-btn column-header__setting-btn'
 | |
|         onClick={handlePin}
 | |
|       >
 | |
|         <Icon id='plus' icon={AddIcon} />{' '}
 | |
|         <FormattedMessage id='column_header.pin' defaultMessage='Pin' />
 | |
|       </button>
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   if (
 | |
|     !pinned &&
 | |
|     ((multiColumn && history.location.state?.fromMastodon) || showBackButton)
 | |
|   ) {
 | |
|     backButton = <BackButton onlyIcon={!!title} />;
 | |
|   }
 | |
| 
 | |
|   const collapsedContent = [extraContent];
 | |
| 
 | |
|   if (multiColumn) {
 | |
|     collapsedContent.push(
 | |
|       <div key='buttons' className='column-header__advanced-buttons'>
 | |
|         {pinButton}
 | |
|         {moveButtons}
 | |
|       </div>,
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   if (signedIn && (children || (multiColumn && onPin))) {
 | |
|     collapseButton = (
 | |
|       <button
 | |
|         className={collapsibleButtonClassName}
 | |
|         title={intl.formatMessage(collapsed ? messages.show : messages.hide)}
 | |
|         aria-label={intl.formatMessage(
 | |
|           collapsed ? messages.show : messages.hide,
 | |
|         )}
 | |
|         onClick={handleToggleClick}
 | |
|       >
 | |
|         <i className='icon-with-badge'>
 | |
|           <Icon id='sliders' icon={SettingsIcon} />
 | |
|           {collapseIssues && <i className='icon-with-badge__issue-badge' />}
 | |
|         </i>
 | |
|       </button>
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   const hasIcon = icon && iconComponent;
 | |
|   const hasTitle = hasIcon && title;
 | |
| 
 | |
|   const component = (
 | |
|     <div className={wrapperClassName}>
 | |
|       <h1 className={buttonClassName}>
 | |
|         {hasTitle && (
 | |
|           <>
 | |
|             {backButton}
 | |
| 
 | |
|             <button onClick={handleTitleClick} className='column-header__title'>
 | |
|               {!backButton && (
 | |
|                 <Icon
 | |
|                   id={icon}
 | |
|                   icon={iconComponent}
 | |
|                   className='column-header__icon'
 | |
|                 />
 | |
|               )}
 | |
|               {title}
 | |
|             </button>
 | |
|           </>
 | |
|         )}
 | |
| 
 | |
|         {!hasTitle && backButton}
 | |
| 
 | |
|         <div className='column-header__buttons'>
 | |
|           {extraButton}
 | |
|           {collapseButton}
 | |
|         </div>
 | |
|       </h1>
 | |
| 
 | |
|       <div
 | |
|         className={collapsibleClassName}
 | |
|         tabIndex={collapsed ? -1 : undefined}
 | |
|         onTransitionEnd={handleTransitionEnd}
 | |
|       >
 | |
|         <div className='column-header__collapsible-inner'>
 | |
|           {(!collapsed || animating) && collapsedContent}
 | |
|         </div>
 | |
|       </div>
 | |
| 
 | |
|       {appendContent}
 | |
|     </div>
 | |
|   );
 | |
| 
 | |
|   if (placeholder) {
 | |
|     return component;
 | |
|   } else {
 | |
|     return <ButtonInTabsBar>{component}</ButtonInTabsBar>;
 | |
|   }
 | |
| };
 | |
| 
 | |
| // eslint-disable-next-line import/no-default-export
 | |
| export default ColumnHeader;
 |