[Proposal] Make able to write React in Typescript (#16210)
Co-authored-by: berlysia <berlysia@gmail.com> Co-authored-by: fusagiko / takayamaki <takayamaki@users.noreply.github.com>
This commit is contained in:
		
				
					committed by
					
						
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							2f7c3cb628
						
					
				
				
					commit
					4520e6473a
				
			
							
								
								
									
										19
									
								
								.eslintrc.js
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								.eslintrc.js
									
									
									
									
									
								
							@@ -20,13 +20,14 @@ module.exports = {
 | 
			
		||||
    ATTACHMENT_HOST: false,
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  parser: '@babel/eslint-parser',
 | 
			
		||||
  parser: '@typescript-eslint/parser',
 | 
			
		||||
 | 
			
		||||
  plugins: [
 | 
			
		||||
    'react',
 | 
			
		||||
    'jsx-a11y',
 | 
			
		||||
    'import',
 | 
			
		||||
    'promise',
 | 
			
		||||
    '@typescript-eslint',
 | 
			
		||||
  ],
 | 
			
		||||
 | 
			
		||||
  parserOptions: {
 | 
			
		||||
@@ -41,14 +42,13 @@ module.exports = {
 | 
			
		||||
      presets: ['@babel/react', '@babel/env'],
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  extends: [
 | 
			
		||||
    'plugin:import/typescript',
 | 
			
		||||
  ],
 | 
			
		||||
  settings: {
 | 
			
		||||
    react: {
 | 
			
		||||
      version: 'detect',
 | 
			
		||||
    },
 | 
			
		||||
    'import/extensions': [
 | 
			
		||||
      '.js', '.jsx',
 | 
			
		||||
    ],
 | 
			
		||||
    'import/ignore': [
 | 
			
		||||
      'node_modules',
 | 
			
		||||
      '\\.(css|scss|json)$',
 | 
			
		||||
@@ -56,7 +56,7 @@ module.exports = {
 | 
			
		||||
    'import/resolver': {
 | 
			
		||||
      node: {
 | 
			
		||||
        paths: ['app/javascript'],
 | 
			
		||||
        extensions: ['.js', '.jsx'],
 | 
			
		||||
        extensions: ['.js', '.jsx', '.ts', '.tsx'],
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
@@ -97,7 +97,8 @@ module.exports = {
 | 
			
		||||
    'no-self-assign': 'off',
 | 
			
		||||
    'no-trailing-spaces': 'warn',
 | 
			
		||||
    'no-unused-expressions': 'error',
 | 
			
		||||
    'no-unused-vars': [
 | 
			
		||||
    'no-unused-vars': 'off',
 | 
			
		||||
    '@typescript-eslint/no-unused-vars': [
 | 
			
		||||
      'error',
 | 
			
		||||
      {
 | 
			
		||||
        vars: 'all',
 | 
			
		||||
@@ -116,7 +117,7 @@ module.exports = {
 | 
			
		||||
    semi: 'error',
 | 
			
		||||
    'valid-typeof': 'error',
 | 
			
		||||
 | 
			
		||||
    'react/jsx-filename-extension': ['error', { 'allow': 'as-needed' }],
 | 
			
		||||
    'react/jsx-filename-extension': ['error', { extensions: ['.jsx', 'tsx'] }],
 | 
			
		||||
    'react/jsx-boolean-value': 'error',
 | 
			
		||||
    'react/jsx-closing-bracket-location': ['error', 'line-aligned'],
 | 
			
		||||
    'react/jsx-curly-spacing': 'error',
 | 
			
		||||
@@ -192,6 +193,8 @@ module.exports = {
 | 
			
		||||
      {
 | 
			
		||||
        js: 'never',
 | 
			
		||||
        jsx: 'never',
 | 
			
		||||
        ts: 'never',
 | 
			
		||||
        tsx: 'never',
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
    'import/newline-after-import': 'error',
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										17
									
								
								app/javascript/hooks/useHovering.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								app/javascript/hooks/useHovering.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
import { useCallback, useState } from 'react';
 | 
			
		||||
 | 
			
		||||
export const useHovering = (animate?: boolean) => {
 | 
			
		||||
  const [hovering, setHovering] = useState<boolean>(animate ?? false);
 | 
			
		||||
 | 
			
		||||
  const handleMouseEnter = useCallback(() => {
 | 
			
		||||
    if (animate) return;
 | 
			
		||||
    setHovering(true);
 | 
			
		||||
  }, [animate]);
 | 
			
		||||
 | 
			
		||||
  const handleMouseLeave = useCallback(() => {
 | 
			
		||||
    if (animate) return;
 | 
			
		||||
    setHovering(false);
 | 
			
		||||
  }, [animate]);
 | 
			
		||||
 | 
			
		||||
  return { hovering, handleMouseEnter, handleMouseLeave };
 | 
			
		||||
};
 | 
			
		||||
@@ -23,6 +23,7 @@ export const PICTURE_IN_PICTURE_REMOVE = 'PICTURE_IN_PICTURE_REMOVE';
 | 
			
		||||
 * @return {object}
 | 
			
		||||
 */
 | 
			
		||||
export const deployPictureInPicture = (statusId, accountId, playerType, props) => {
 | 
			
		||||
  // @ts-expect-error
 | 
			
		||||
  return (dispatch, getState) => {
 | 
			
		||||
    // Do not open a player for a toot that does not exist
 | 
			
		||||
    if (getState().hasIn(['statuses', statusId])) {
 | 
			
		||||
 
 | 
			
		||||
@@ -46,6 +46,7 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
 | 
			
		||||
  connectStream(channelName, params, (dispatch, getState) => {
 | 
			
		||||
    const locale = getState().getIn(['meta', 'locale']);
 | 
			
		||||
 | 
			
		||||
    // @ts-expect-error
 | 
			
		||||
    let pollingId;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -61,9 +62,10 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
 | 
			
		||||
      onConnect() {
 | 
			
		||||
        dispatch(connectTimeline(timelineId));
 | 
			
		||||
 | 
			
		||||
        // @ts-expect-error
 | 
			
		||||
        if (pollingId) {
 | 
			
		||||
          clearTimeout(pollingId);
 | 
			
		||||
          pollingId = null;
 | 
			
		||||
          // @ts-ignore
 | 
			
		||||
          clearTimeout(pollingId); pollingId = null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (options.fillGaps) {
 | 
			
		||||
@@ -75,31 +77,38 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
 | 
			
		||||
        dispatch(disconnectTimeline(timelineId));
 | 
			
		||||
 | 
			
		||||
        if (options.fallback) {
 | 
			
		||||
          // @ts-expect-error
 | 
			
		||||
          pollingId = setTimeout(() => useFallback(options.fallback), randomUpTo(40000));
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      onReceive (data) {
 | 
			
		||||
        switch(data.event) {
 | 
			
		||||
      onReceive(data) {
 | 
			
		||||
        switch (data.event) {
 | 
			
		||||
        case 'update':
 | 
			
		||||
          // @ts-expect-error
 | 
			
		||||
          dispatch(updateTimeline(timelineId, JSON.parse(data.payload), options.accept));
 | 
			
		||||
          break;
 | 
			
		||||
        case 'status.update':
 | 
			
		||||
          // @ts-expect-error
 | 
			
		||||
          dispatch(updateStatus(JSON.parse(data.payload)));
 | 
			
		||||
          break;
 | 
			
		||||
        case 'delete':
 | 
			
		||||
          dispatch(deleteFromTimelines(data.payload));
 | 
			
		||||
          break;
 | 
			
		||||
        case 'notification':
 | 
			
		||||
          // @ts-expect-error
 | 
			
		||||
          dispatch(updateNotifications(JSON.parse(data.payload), messages, locale));
 | 
			
		||||
          break;
 | 
			
		||||
        case 'conversation':
 | 
			
		||||
          // @ts-expect-error
 | 
			
		||||
          dispatch(updateConversations(JSON.parse(data.payload)));
 | 
			
		||||
          break;
 | 
			
		||||
        case 'announcement':
 | 
			
		||||
          // @ts-expect-error
 | 
			
		||||
          dispatch(updateAnnouncements(JSON.parse(data.payload)));
 | 
			
		||||
          break;
 | 
			
		||||
        case 'announcement.reaction':
 | 
			
		||||
          // @ts-expect-error
 | 
			
		||||
          dispatch(updateAnnouncementsReaction(JSON.parse(data.payload)));
 | 
			
		||||
          break;
 | 
			
		||||
        case 'announcement.delete':
 | 
			
		||||
@@ -115,7 +124,9 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
 | 
			
		||||
 * @param {function(): void} done
 | 
			
		||||
 */
 | 
			
		||||
const refreshHomeTimelineAndNotification = (dispatch, done) => {
 | 
			
		||||
  // @ts-expect-error
 | 
			
		||||
  dispatch(expandHomeTimeline({}, () =>
 | 
			
		||||
    // @ts-expect-error
 | 
			
		||||
    dispatch(expandNotifications({}, () =>
 | 
			
		||||
      dispatch(fetchAnnouncements(done))))));
 | 
			
		||||
};
 | 
			
		||||
@@ -124,6 +135,7 @@ const refreshHomeTimelineAndNotification = (dispatch, done) => {
 | 
			
		||||
 * @return {function(): void}
 | 
			
		||||
 */
 | 
			
		||||
export const connectUserStream = () =>
 | 
			
		||||
  // @ts-expect-error
 | 
			
		||||
  connectTimelineStream('home', 'user', {}, { fallback: refreshHomeTimelineAndNotification, fillGaps: fillHomeTimelineGaps });
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 
 | 
			
		||||
@@ -36,7 +36,7 @@ const setCSRFHeader = () => {
 | 
			
		||||
ready(setCSRFHeader);
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {() => import('immutable').Map} getState
 | 
			
		||||
 * @param {() => import('immutable').Map<string,any>} getState
 | 
			
		||||
 * @returns {import('axios').RawAxiosRequestHeaders}
 | 
			
		||||
 */
 | 
			
		||||
const authorizationHeaderFromState = getState => {
 | 
			
		||||
@@ -52,7 +52,7 @@ const authorizationHeaderFromState = getState => {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {() => import('immutable').Map} getState
 | 
			
		||||
 * @param {() => import('immutable').Map<string,any>} getState
 | 
			
		||||
 * @returns {import('axios').AxiosInstance}
 | 
			
		||||
 */
 | 
			
		||||
export default function api(getState) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,62 +0,0 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
			
		||||
import { autoPlayGif } from '../initial_state';
 | 
			
		||||
import classNames from 'classnames';
 | 
			
		||||
 | 
			
		||||
export default class Avatar extends React.PureComponent {
 | 
			
		||||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    account: ImmutablePropTypes.map,
 | 
			
		||||
    size: PropTypes.number.isRequired,
 | 
			
		||||
    style: PropTypes.object,
 | 
			
		||||
    inline: PropTypes.bool,
 | 
			
		||||
    animate: PropTypes.bool,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  static defaultProps = {
 | 
			
		||||
    animate: autoPlayGif,
 | 
			
		||||
    size: 20,
 | 
			
		||||
    inline: false,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  state = {
 | 
			
		||||
    hovering: false,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  handleMouseEnter = () => {
 | 
			
		||||
    if (this.props.animate) return;
 | 
			
		||||
    this.setState({ hovering: true });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  handleMouseLeave = () => {
 | 
			
		||||
    if (this.props.animate) return;
 | 
			
		||||
    this.setState({ hovering: false });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { account, size, animate, inline } = this.props;
 | 
			
		||||
    const { hovering } = this.state;
 | 
			
		||||
 | 
			
		||||
    const style = {
 | 
			
		||||
      ...this.props.style,
 | 
			
		||||
      width: `${size}px`,
 | 
			
		||||
      height: `${size}px`,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let src;
 | 
			
		||||
 | 
			
		||||
    if (hovering || animate) {
 | 
			
		||||
      src = account?.get('avatar');
 | 
			
		||||
    } else {
 | 
			
		||||
      src = account?.get('avatar_static');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <div className={classNames('account__avatar', { 'account__avatar-inline': inline })} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} style={style}>
 | 
			
		||||
        {src && <img src={src} alt={account?.get('acct')} />}
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										40
									
								
								app/javascript/mastodon/components/avatar.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								app/javascript/mastodon/components/avatar.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
			
		||||
import * as React from 'react';
 | 
			
		||||
import classNames from 'classnames';
 | 
			
		||||
import { autoPlayGif } from '../initial_state';
 | 
			
		||||
import { useHovering } from '../../hooks/useHovering';
 | 
			
		||||
import type { Account } from '../../types/resources';
 | 
			
		||||
 | 
			
		||||
type Props = {
 | 
			
		||||
  account: Account;
 | 
			
		||||
  size: number;
 | 
			
		||||
  style?: React.CSSProperties;
 | 
			
		||||
  inline?: boolean;
 | 
			
		||||
  animate?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const Avatar: React.FC<Props> = ({
 | 
			
		||||
  account,
 | 
			
		||||
  animate = autoPlayGif,
 | 
			
		||||
  size = 20,
 | 
			
		||||
  inline = false,
 | 
			
		||||
  style: styleFromParent,
 | 
			
		||||
}) => {
 | 
			
		||||
 | 
			
		||||
  const { hovering, handleMouseEnter, handleMouseLeave } = useHovering(animate);
 | 
			
		||||
 | 
			
		||||
  const style = {
 | 
			
		||||
    ...styleFromParent,
 | 
			
		||||
    width: `${size}px`,
 | 
			
		||||
    height: `${size}px`,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const src = (hovering || animate) ? account?.get('avatar') : account?.get('avatar_static');
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className={classNames('account__avatar', { 'account__avatar-inline': inline })} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} style={style}>
 | 
			
		||||
      {src && <img src={src} alt={account?.get('acct')} />}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Avatar;
 | 
			
		||||
@@ -44,6 +44,7 @@ function Blurhash({
 | 
			
		||||
      const ctx = canvas.getContext('2d');
 | 
			
		||||
      const imageData = new ImageData(pixels, width, height);
 | 
			
		||||
 | 
			
		||||
      // @ts-expect-error
 | 
			
		||||
      ctx.putImageData(imageData, 0, 0);
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      console.error('Blurhash decoding failure', { err, hash });
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
// @ts-check
 | 
			
		||||
import React from 'react';
 | 
			
		||||
// @ts-expect-error
 | 
			
		||||
import { FormattedMessage } from 'react-intl';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,14 @@
 | 
			
		||||
// @ts-check
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { Sparklines, SparklinesCurve } from 'react-sparklines';
 | 
			
		||||
// @ts-expect-error
 | 
			
		||||
import { FormattedMessage } from 'react-intl';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
			
		||||
import { Link } from 'react-router-dom';
 | 
			
		||||
// @ts-expect-error
 | 
			
		||||
import ShortNumber from 'mastodon/components/short_number';
 | 
			
		||||
// @ts-expect-error
 | 
			
		||||
import Skeleton from 'mastodon/components/skeleton';
 | 
			
		||||
import classNames from 'classnames';
 | 
			
		||||
 | 
			
		||||
@@ -19,11 +22,11 @@ class SilentErrorBoundary extends React.Component {
 | 
			
		||||
    error: false,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  componentDidCatch () {
 | 
			
		||||
  componentDidCatch() {
 | 
			
		||||
    this.setState({ error: true });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
  render() {
 | 
			
		||||
    if (this.state.error) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
@@ -50,11 +53,13 @@ export const accountsCountRenderer = (displayNumber, pluralReady) => (
 | 
			
		||||
  />
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
// @ts-expect-error
 | 
			
		||||
export const ImmutableHashtag = ({ hashtag }) => (
 | 
			
		||||
  <Hashtag
 | 
			
		||||
    name={hashtag.get('name')}
 | 
			
		||||
    to={`/tags/${hashtag.get('name')}`}
 | 
			
		||||
    people={hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1}
 | 
			
		||||
    // @ts-expect-error
 | 
			
		||||
    history={hashtag.get('history').reverse().map((day) => day.get('uses')).toArray()}
 | 
			
		||||
  />
 | 
			
		||||
);
 | 
			
		||||
@@ -63,6 +68,7 @@ ImmutableHashtag.propTypes = {
 | 
			
		||||
  hashtag: ImmutablePropTypes.map.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// @ts-expect-error
 | 
			
		||||
const Hashtag = ({ name, to, people, uses, history, className, description, withGraph }) => (
 | 
			
		||||
  <div className={classNames('trends__item', className)}>
 | 
			
		||||
    <div className='trends__item__name'>
 | 
			
		||||
@@ -86,7 +92,9 @@ const Hashtag = ({ name, to, people, uses, history, className, description, with
 | 
			
		||||
    {withGraph && (
 | 
			
		||||
      <div className='trends__item__sparkline'>
 | 
			
		||||
        <SilentErrorBoundary>
 | 
			
		||||
          {/* @ts-expect-error */}
 | 
			
		||||
          <Sparklines width={50} height={28} data={history ? history : Array.from(Array(7)).map(() => 0)}>
 | 
			
		||||
            {/* @ts-expect-error */}
 | 
			
		||||
            <SparklinesCurve style={{ fill: 'none' }} />
 | 
			
		||||
          </Sparklines>
 | 
			
		||||
        </SilentErrorBoundary>
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ const emojis = {};
 | 
			
		||||
// decompress
 | 
			
		||||
Object.keys(shortCodesToEmojiData).forEach((shortCode) => {
 | 
			
		||||
  let [
 | 
			
		||||
    filenameData, // eslint-disable-line no-unused-vars
 | 
			
		||||
    filenameData, // eslint-disable-line @typescript-eslint/no-unused-vars
 | 
			
		||||
    searchData,
 | 
			
		||||
  ] = shortCodesToEmojiData[shortCode];
 | 
			
		||||
  let [
 | 
			
		||||
 
 | 
			
		||||
@@ -4,9 +4,9 @@
 | 
			
		||||
 | 
			
		||||
const [
 | 
			
		||||
  shortCodesToEmojiData,
 | 
			
		||||
  skins, // eslint-disable-line no-unused-vars
 | 
			
		||||
  categories, // eslint-disable-line no-unused-vars
 | 
			
		||||
  short_names, // eslint-disable-line no-unused-vars
 | 
			
		||||
  skins, // eslint-disable-line @typescript-eslint/no-unused-vars
 | 
			
		||||
  categories, // eslint-disable-line @typescript-eslint/no-unused-vars
 | 
			
		||||
  short_names, // eslint-disable-line @typescript-eslint/no-unused-vars
 | 
			
		||||
  emojisWithoutShortCodes,
 | 
			
		||||
] = require('./emoji_compressed');
 | 
			
		||||
const { unicodeToFilename } = require('./unicode_to_filename');
 | 
			
		||||
 
 | 
			
		||||
@@ -132,6 +132,7 @@ export const useBlurhash = getMeta('use_blurhash');
 | 
			
		||||
export const usePendingItems = getMeta('use_pending_items');
 | 
			
		||||
export const version = getMeta('version');
 | 
			
		||||
export const languages = initialState?.languages;
 | 
			
		||||
// @ts-expect-error
 | 
			
		||||
export const statusPageUrl = getMeta('status_page_url');
 | 
			
		||||
 | 
			
		||||
export default initialState;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
// @ts-check
 | 
			
		||||
 | 
			
		||||
import { supportsPassiveEvents } from 'detect-passive-events';
 | 
			
		||||
// @ts-expect-error
 | 
			
		||||
import { forceSingleColumn } from 'mastodon/initial_state';
 | 
			
		||||
 | 
			
		||||
const LAYOUT_BREAKPOINT = 630;
 | 
			
		||||
@@ -24,6 +25,7 @@ export const layoutFromWindow = () => {
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// @ts-expect-error
 | 
			
		||||
const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
 | 
			
		||||
 | 
			
		||||
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
 | 
			
		||||
@@ -33,7 +35,7 @@ let userTouching = false;
 | 
			
		||||
const touchListener = () => {
 | 
			
		||||
  userTouching = true;
 | 
			
		||||
 | 
			
		||||
  window.removeEventListener('touchstart', touchListener, listenerOptions);
 | 
			
		||||
  window.removeEventListener('touchstart', touchListener);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
window.addEventListener('touchstart', touchListener, listenerOptions);
 | 
			
		||||
 
 | 
			
		||||
@@ -59,6 +59,7 @@ const subscribe = ({ channelName, params, onConnect }) => {
 | 
			
		||||
  subscriptionCounters[key] = subscriptionCounters[key] || 0;
 | 
			
		||||
 | 
			
		||||
  if (subscriptionCounters[key] === 0) {
 | 
			
		||||
    // @ts-expect-error
 | 
			
		||||
    sharedConnection.send(JSON.stringify({ type: 'subscribe', stream: channelName, ...params }));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -74,7 +75,9 @@ const unsubscribe = ({ channelName, params, onDisconnect }) => {
 | 
			
		||||
 | 
			
		||||
  subscriptionCounters[key] = subscriptionCounters[key] || 1;
 | 
			
		||||
 | 
			
		||||
  // @ts-expect-error
 | 
			
		||||
  if (subscriptionCounters[key] === 1 && sharedConnection.readyState === WebSocketClient.OPEN) {
 | 
			
		||||
    // @ts-expect-error
 | 
			
		||||
    sharedConnection.send(JSON.stringify({ type: 'unsubscribe', stream: channelName, ...params }));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -83,11 +86,12 @@ const unsubscribe = ({ channelName, params, onDisconnect }) => {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const sharedCallbacks = {
 | 
			
		||||
  connected () {
 | 
			
		||||
  connected() {
 | 
			
		||||
    subscriptions.forEach(subscription => subscribe(subscription));
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  received (data) {
 | 
			
		||||
  // @ts-expect-error
 | 
			
		||||
  received(data) {
 | 
			
		||||
    const { stream } = data;
 | 
			
		||||
 | 
			
		||||
    subscriptions.filter(({ channelName, params }) => {
 | 
			
		||||
@@ -111,11 +115,11 @@ const sharedCallbacks = {
 | 
			
		||||
    });
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  disconnected () {
 | 
			
		||||
  disconnected() {
 | 
			
		||||
    subscriptions.forEach(subscription => unsubscribe(subscription));
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  reconnected () {
 | 
			
		||||
  reconnected() {
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -138,6 +142,7 @@ const channelNameWithInlineParams = (channelName, params) => {
 | 
			
		||||
 * @param {function(Function, Function): { onConnect: (function(): void), onReceive: (function(StreamEvent): void), onDisconnect: (function(): void) }} callbacks
 | 
			
		||||
 * @return {function(): void}
 | 
			
		||||
 */
 | 
			
		||||
// @ts-expect-error
 | 
			
		||||
export const connectStream = (channelName, params, callbacks) => (dispatch, getState) => {
 | 
			
		||||
  const streamingAPIBaseURL = getState().getIn(['meta', 'streaming_api_base_url']);
 | 
			
		||||
  const accessToken = getState().getIn(['meta', 'access_token']);
 | 
			
		||||
@@ -147,19 +152,19 @@ export const connectStream = (channelName, params, callbacks) => (dispatch, getS
 | 
			
		||||
  // to using individual connections for each channel
 | 
			
		||||
  if (!streamingAPIBaseURL.startsWith('ws')) {
 | 
			
		||||
    const connection = createConnection(streamingAPIBaseURL, accessToken, channelNameWithInlineParams(channelName, params), {
 | 
			
		||||
      connected () {
 | 
			
		||||
      connected() {
 | 
			
		||||
        onConnect();
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      received (data) {
 | 
			
		||||
      received(data) {
 | 
			
		||||
        onReceive(data);
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      disconnected () {
 | 
			
		||||
      disconnected() {
 | 
			
		||||
        onDisconnect();
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      reconnected () {
 | 
			
		||||
      reconnected() {
 | 
			
		||||
        onConnect();
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
@@ -227,14 +232,19 @@ const handleEventSourceMessage = (e, received) => {
 | 
			
		||||
const createConnection = (streamingAPIBaseURL, accessToken, channelName, { connected, received, disconnected, reconnected }) => {
 | 
			
		||||
  const params = channelName.split('&');
 | 
			
		||||
 | 
			
		||||
  // @ts-expect-error
 | 
			
		||||
  channelName = params.shift();
 | 
			
		||||
 | 
			
		||||
  if (streamingAPIBaseURL.startsWith('ws')) {
 | 
			
		||||
    // @ts-expect-error
 | 
			
		||||
    const ws = new WebSocketClient(`${streamingAPIBaseURL}/api/v1/streaming/?${params.join('&')}`, accessToken);
 | 
			
		||||
 | 
			
		||||
    ws.onopen      = connected;
 | 
			
		||||
    ws.onmessage   = e => received(JSON.parse(e.data));
 | 
			
		||||
    ws.onclose     = disconnected;
 | 
			
		||||
    // @ts-expect-error
 | 
			
		||||
    ws.onopen = connected;
 | 
			
		||||
    ws.onmessage = e => received(JSON.parse(e.data));
 | 
			
		||||
    // @ts-expect-error
 | 
			
		||||
    ws.onclose = disconnected;
 | 
			
		||||
    // @ts-expect-error
 | 
			
		||||
    ws.onreconnect = reconnected;
 | 
			
		||||
 | 
			
		||||
    return ws;
 | 
			
		||||
@@ -256,7 +266,7 @@ const createConnection = (streamingAPIBaseURL, accessToken, channelName, { conne
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  KNOWN_EVENT_TYPES.forEach(type => {
 | 
			
		||||
    es.addEventListener(type, e => handleEventSourceMessage(/** @type {MessageEvent} */ (e), received));
 | 
			
		||||
    es.addEventListener(type, e => handleEventSourceMessage(/** @type {MessageEvent} */(e), received));
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  es.onerror = /** @type {function(): void} */ (disconnected);
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@
 | 
			
		||||
 | 
			
		||||
const checkNotificationPromise = () => {
 | 
			
		||||
  try {
 | 
			
		||||
    // eslint-disable-next-line promise/catch-or-return, promise/valid-params
 | 
			
		||||
    // eslint-disable-next-line promise/catch-or-return
 | 
			
		||||
    Notification.requestPermission().then();
 | 
			
		||||
  } catch(e) {
 | 
			
		||||
    return false;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +0,0 @@
 | 
			
		||||
export default function uuid(a) {
 | 
			
		||||
  return a ? (a^Math.random() * 16 >> a / 4).toString(16) : ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, uuid);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										3
									
								
								app/javascript/mastodon/uuid.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								app/javascript/mastodon/uuid.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
export default function uuid(a?: string): string {
 | 
			
		||||
  return a ? ((a as any as number) ^ Math.random() * 16 >> (a as any as number) / 4).toString(16) : ('' + 1e7 + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, uuid);
 | 
			
		||||
}
 | 
			
		||||
@@ -17,5 +17,4 @@ function formatPublicPath(host = '', path = '') {
 | 
			
		||||
 | 
			
		||||
const cdnHost = document.querySelector('meta[name=cdn-host]');
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line no-undef
 | 
			
		||||
__webpack_public_path__ = formatPublicPath(cdnHost ? cdnHost.content : '', process.env.PUBLIC_OUTPUT_PATH);
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										13
									
								
								app/javascript/types/resources.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								app/javascript/types/resources.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
interface MastodonMap<T> {
 | 
			
		||||
  get<K extends keyof T>(key: K): T[K];
 | 
			
		||||
  has<K extends keyof T>(key: K): boolean;
 | 
			
		||||
  set<K extends keyof T>(key: K, value: T[K]): this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type AccountValues = {
 | 
			
		||||
  id: number;
 | 
			
		||||
  avatar: string;
 | 
			
		||||
  avatar_static: string;
 | 
			
		||||
  [key: string]: any;
 | 
			
		||||
}
 | 
			
		||||
export type Account = MastodonMap<AccountValues>
 | 
			
		||||
@@ -13,6 +13,7 @@ module.exports = (api) => {
 | 
			
		||||
 | 
			
		||||
  const config = {
 | 
			
		||||
    presets: [
 | 
			
		||||
      '@babel/preset-typescript',
 | 
			
		||||
      ['@babel/react', reactOptions],
 | 
			
		||||
      ['@babel/env', envOptions],
 | 
			
		||||
    ],
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@ const { join, resolve } = require('path');
 | 
			
		||||
const { env, settings } = require('../configuration');
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
  test: /\.(js|jsx|mjs)$/,
 | 
			
		||||
  test: /\.(js|jsx|mjs|ts|tsx)$/,
 | 
			
		||||
  include: [
 | 
			
		||||
    settings.source_path,
 | 
			
		||||
    ...settings.resolved_paths,
 | 
			
		||||
 
 | 
			
		||||
@@ -36,6 +36,8 @@ default: &default
 | 
			
		||||
    - .mjs
 | 
			
		||||
    - .js
 | 
			
		||||
    - .jsx
 | 
			
		||||
    - .ts
 | 
			
		||||
    - .tsx
 | 
			
		||||
    - .sass
 | 
			
		||||
    - .scss
 | 
			
		||||
    - .css
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										44
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										44
									
								
								package.json
									
									
									
									
									
								
							@@ -10,10 +10,11 @@
 | 
			
		||||
    "build:production": "cross-env RAILS_ENV=production NODE_ENV=production ./bin/webpack",
 | 
			
		||||
    "manage:translations": "node ./config/webpack/translationRunner.js",
 | 
			
		||||
    "start": "node ./streaming/index.js",
 | 
			
		||||
    "test": "${npm_execpath} run test:lint:js && ${npm_execpath} run test:jest",
 | 
			
		||||
    "test": "${npm_execpath} run test:lint:js && ${npm_execpath} run test:typecheck && ${npm_execpath} run test:jest",
 | 
			
		||||
    "test:lint": "${npm_execpath} run test:lint:js && ${npm_execpath} run test:lint:sass",
 | 
			
		||||
    "test:lint:js": "eslint --ext=.js,.jsx . --cache --report-unused-disable-directives",
 | 
			
		||||
    "test:lint:js": "eslint --ext=.js,.jsx,.ts,.tsx . --cache --report-unused-disable-directives",
 | 
			
		||||
    "test:lint:sass": "stylelint \"**/*.{css,scss}\" && prettier --check \"**/*.{css,scss}\"",
 | 
			
		||||
    "test:typecheck": "tsc --noEmit",
 | 
			
		||||
    "test:jest": "cross-env NODE_ENV=test jest",
 | 
			
		||||
    "format": "prettier --write .",
 | 
			
		||||
    "format-check": "prettier --check .",
 | 
			
		||||
@@ -139,9 +140,45 @@
 | 
			
		||||
    "ws": "^8.12.1"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@babel/eslint-parser": "^7.21.3",
 | 
			
		||||
    "@babel/preset-typescript": "^7.21.0",
 | 
			
		||||
    "@testing-library/jest-dom": "^5.16.5",
 | 
			
		||||
    "@testing-library/react": "^12.1.5",
 | 
			
		||||
    "@types/babel__core": "^7.20.0",
 | 
			
		||||
    "@types/emoji-mart": "^3.0.9",
 | 
			
		||||
    "@types/escape-html": "^1.0.2",
 | 
			
		||||
    "@types/eslint": "^8.21.2",
 | 
			
		||||
    "@types/express": "^4.17.17",
 | 
			
		||||
    "@types/glob": "^8.1.0",
 | 
			
		||||
    "@types/http-link-header": "^1.0.3",
 | 
			
		||||
    "@types/intl": "^1.2.0",
 | 
			
		||||
    "@types/jest": "^29.4.2",
 | 
			
		||||
    "@types/js-yaml": "^4.0.5",
 | 
			
		||||
    "@types/lodash": "^4.14.191",
 | 
			
		||||
    "@types/npmlog": "^4.1.4",
 | 
			
		||||
    "@types/object-assign": "^4.0.30",
 | 
			
		||||
    "@types/pg": "^8.6.6",
 | 
			
		||||
    "@types/prop-types": "^15.7.5",
 | 
			
		||||
    "@types/punycode": "^2.1.0",
 | 
			
		||||
    "@types/raf": "^3.4.0",
 | 
			
		||||
    "@types/react": "^18.0.28",
 | 
			
		||||
    "@types/react-dom": "^18.0.11",
 | 
			
		||||
    "@types/react-intl": "2.3.18",
 | 
			
		||||
    "@types/react-motion": "^0.0.33",
 | 
			
		||||
    "@types/react-redux": "^7.1.25",
 | 
			
		||||
    "@types/react-router-dom": "^5.3.3",
 | 
			
		||||
    "@types/react-sparklines": "^1.7.2",
 | 
			
		||||
    "@types/react-swipeable-views": "^0.13.1",
 | 
			
		||||
    "@types/react-test-renderer": "^18.0.0",
 | 
			
		||||
    "@types/react-toggle": "^4.0.3",
 | 
			
		||||
    "@types/redux-immutable": "^4.0.3",
 | 
			
		||||
    "@types/requestidlecallback": "^0.3.5",
 | 
			
		||||
    "@types/throng": "^5.0.4",
 | 
			
		||||
    "@types/uuid": "^9.0.1",
 | 
			
		||||
    "@types/webpack": "^5.28.0",
 | 
			
		||||
    "@types/webpack-bundle-analyzer": "^4.6.0",
 | 
			
		||||
    "@types/yargs": "^17.0.22",
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": "^5.55.0",
 | 
			
		||||
    "@typescript-eslint/parser": "^5.55.0",
 | 
			
		||||
    "babel-jest": "^29.5.0",
 | 
			
		||||
    "eslint": "^8.36.0",
 | 
			
		||||
    "eslint-plugin-import": "~2.27.5",
 | 
			
		||||
@@ -160,6 +197,7 @@
 | 
			
		||||
    "react-test-renderer": "^16.14.0",
 | 
			
		||||
    "stylelint": "^15.3.0",
 | 
			
		||||
    "stylelint-config-standard-scss": "^7.0.1",
 | 
			
		||||
    "typescript": "^4.9.5",
 | 
			
		||||
    "webpack-dev-server": "^3.11.3",
 | 
			
		||||
    "yargs": "^17.7.1"
 | 
			
		||||
  },
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										13
									
								
								tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								tsconfig.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
{
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "jsx": "react",
 | 
			
		||||
    "target": "esnext",
 | 
			
		||||
    "moduleResolution": "node",
 | 
			
		||||
    "allowJs": true,
 | 
			
		||||
    "noEmit": true,
 | 
			
		||||
    "strict": true,
 | 
			
		||||
    "esModuleInterop": true,
 | 
			
		||||
    "skipLibCheck": true
 | 
			
		||||
  },
 | 
			
		||||
  "include": ["app/javascript/mastodon", "app/javascript/packs"]
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user