160 lines
		
	
	
		
			4.9 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			160 lines
		
	
	
		
			4.9 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import { useCallback, useRef } from 'react';
 | 
						|
 | 
						|
import { FormattedMessage } from 'react-intl';
 | 
						|
 | 
						|
import { useHistory } from 'react-router-dom';
 | 
						|
 | 
						|
import type { List as ImmutableList, RecordOf } from 'immutable';
 | 
						|
 | 
						|
import BarChart4BarsIcon from '@/material-icons/400-24px/bar_chart_4_bars.svg?react';
 | 
						|
import PhotoLibraryIcon from '@/material-icons/400-24px/photo_library.svg?react';
 | 
						|
import { Avatar } from 'mastodon/components/avatar';
 | 
						|
import { DisplayName } from 'mastodon/components/display_name';
 | 
						|
import { Icon } from 'mastodon/components/icon';
 | 
						|
import type { Status } from 'mastodon/models/status';
 | 
						|
import { useAppSelector } from 'mastodon/store';
 | 
						|
 | 
						|
import { EmbeddedStatusContent } from './embedded_status_content';
 | 
						|
 | 
						|
export type Mention = RecordOf<{ url: string; acct: string }>;
 | 
						|
 | 
						|
export const EmbeddedStatus: React.FC<{ statusId: string }> = ({
 | 
						|
  statusId,
 | 
						|
}) => {
 | 
						|
  const history = useHistory();
 | 
						|
  const clickCoordinatesRef = useRef<[number, number] | null>();
 | 
						|
 | 
						|
  const status = useAppSelector(
 | 
						|
    (state) => state.statuses.get(statusId) as Status | undefined,
 | 
						|
  );
 | 
						|
 | 
						|
  const account = useAppSelector((state) =>
 | 
						|
    state.accounts.get(status?.get('account') as string),
 | 
						|
  );
 | 
						|
 | 
						|
  const handleMouseDown = useCallback<React.MouseEventHandler<HTMLDivElement>>(
 | 
						|
    ({ clientX, clientY }) => {
 | 
						|
      clickCoordinatesRef.current = [clientX, clientY];
 | 
						|
    },
 | 
						|
    [clickCoordinatesRef],
 | 
						|
  );
 | 
						|
 | 
						|
  const handleMouseUp = useCallback<React.MouseEventHandler<HTMLDivElement>>(
 | 
						|
    ({ clientX, clientY, target, button }) => {
 | 
						|
      const [startX, startY] = clickCoordinatesRef.current ?? [0, 0];
 | 
						|
      const [deltaX, deltaY] = [
 | 
						|
        Math.abs(clientX - startX),
 | 
						|
        Math.abs(clientY - startY),
 | 
						|
      ];
 | 
						|
 | 
						|
      let element: HTMLDivElement | null = target as HTMLDivElement;
 | 
						|
 | 
						|
      while (element) {
 | 
						|
        if (
 | 
						|
          element.localName === 'button' ||
 | 
						|
          element.localName === 'a' ||
 | 
						|
          element.localName === 'label'
 | 
						|
        ) {
 | 
						|
          return;
 | 
						|
        }
 | 
						|
 | 
						|
        element = element.parentNode as HTMLDivElement | null;
 | 
						|
      }
 | 
						|
 | 
						|
      if (deltaX + deltaY < 5 && button === 0 && account) {
 | 
						|
        history.push(`/@${account.acct}/${statusId}`);
 | 
						|
      }
 | 
						|
 | 
						|
      clickCoordinatesRef.current = null;
 | 
						|
    },
 | 
						|
    [clickCoordinatesRef, statusId, account, history],
 | 
						|
  );
 | 
						|
 | 
						|
  const handleMouseEnter = useCallback<React.MouseEventHandler<HTMLDivElement>>(
 | 
						|
    ({ currentTarget }) => {
 | 
						|
      const emojis =
 | 
						|
        currentTarget.querySelectorAll<HTMLImageElement>('.custom-emoji');
 | 
						|
 | 
						|
      for (const emoji of emojis) {
 | 
						|
        const newSrc = emoji.getAttribute('data-original');
 | 
						|
        if (newSrc) emoji.src = newSrc;
 | 
						|
      }
 | 
						|
    },
 | 
						|
    [],
 | 
						|
  );
 | 
						|
 | 
						|
  const handleMouseLeave = useCallback<React.MouseEventHandler<HTMLDivElement>>(
 | 
						|
    ({ currentTarget }) => {
 | 
						|
      const emojis =
 | 
						|
        currentTarget.querySelectorAll<HTMLImageElement>('.custom-emoji');
 | 
						|
 | 
						|
      for (const emoji of emojis) {
 | 
						|
        const newSrc = emoji.getAttribute('data-static');
 | 
						|
        if (newSrc) emoji.src = newSrc;
 | 
						|
      }
 | 
						|
    },
 | 
						|
    [],
 | 
						|
  );
 | 
						|
 | 
						|
  if (!status) {
 | 
						|
    return null;
 | 
						|
  }
 | 
						|
 | 
						|
  // Assign status attributes to variables with a forced type, as status is not yet properly typed
 | 
						|
  const contentHtml = status.get('contentHtml') as string;
 | 
						|
  const poll = status.get('poll');
 | 
						|
  const language = status.get('language') as string;
 | 
						|
  const mentions = status.get('mentions') as ImmutableList<Mention>;
 | 
						|
  const mediaAttachmentsSize = (
 | 
						|
    status.get('media_attachments') as ImmutableList<unknown>
 | 
						|
  ).size;
 | 
						|
 | 
						|
  return (
 | 
						|
    <div
 | 
						|
      className='notification-group__embedded-status'
 | 
						|
      role='button'
 | 
						|
      tabIndex={-1}
 | 
						|
      onMouseDown={handleMouseDown}
 | 
						|
      onMouseUp={handleMouseUp}
 | 
						|
      onMouseEnter={handleMouseEnter}
 | 
						|
      onMouseLeave={handleMouseLeave}
 | 
						|
    >
 | 
						|
      <div className='notification-group__embedded-status__account'>
 | 
						|
        <Avatar account={account} size={16} />
 | 
						|
        <DisplayName account={account} />
 | 
						|
      </div>
 | 
						|
 | 
						|
      <EmbeddedStatusContent
 | 
						|
        className='notification-group__embedded-status__content reply-indicator__content translate'
 | 
						|
        content={contentHtml}
 | 
						|
        language={language}
 | 
						|
        mentions={mentions}
 | 
						|
      />
 | 
						|
 | 
						|
      {(poll || mediaAttachmentsSize > 0) && (
 | 
						|
        <div className='notification-group__embedded-status__attachments reply-indicator__attachments'>
 | 
						|
          {!!poll && (
 | 
						|
            <>
 | 
						|
              <Icon icon={BarChart4BarsIcon} id='bar-chart-4-bars' />
 | 
						|
              <FormattedMessage
 | 
						|
                id='reply_indicator.poll'
 | 
						|
                defaultMessage='Poll'
 | 
						|
              />
 | 
						|
            </>
 | 
						|
          )}
 | 
						|
          {mediaAttachmentsSize > 0 && (
 | 
						|
            <>
 | 
						|
              <Icon icon={PhotoLibraryIcon} id='photo-library' />
 | 
						|
              <FormattedMessage
 | 
						|
                id='reply_indicator.attachments'
 | 
						|
                defaultMessage='{count, plural, one {# attachment} other {# attachments}}'
 | 
						|
                values={{ count: mediaAttachmentsSize }}
 | 
						|
              />
 | 
						|
            </>
 | 
						|
          )}
 | 
						|
        </div>
 | 
						|
      )}
 | 
						|
    </div>
 | 
						|
  );
 | 
						|
};
 |