Skip to content

Commit

Permalink
[Proposal] Make able to write React in Typescript (mastodon#16210)
Browse files Browse the repository at this point in the history
Co-authored-by: berlysia <berlysia@gmail.com>
Co-authored-by: fusagiko / takayamaki <takayamaki@users.noreply.github.com>
  • Loading branch information
3 people committed Apr 3, 2023
1 parent 2f7c3cb commit 4520e64
Show file tree
Hide file tree
Showing 26 changed files with 1,100 additions and 212 deletions.
19 changes: 11 additions & 8 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -41,22 +42,21 @@ 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)$',
],
'import/resolver': {
node: {
paths: ['app/javascript'],
extensions: ['.js', '.jsx'],
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
},
},
Expand Down Expand Up @@ -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',
Expand 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',
Expand Down Expand Up @@ -192,6 +193,8 @@ module.exports = {
{
js: 'never',
jsx: 'never',
ts: 'never',
tsx: 'never',
},
],
'import/newline-after-import': 'error',
Expand Down
17 changes: 17 additions & 0 deletions app/javascript/hooks/useHovering.ts
Original file line number Diff line number Diff line change
@@ -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 };
};
1 change: 1 addition & 0 deletions app/javascript/mastodon/actions/picture_in_picture.js
Original file line number Diff line number Diff line change
Expand Up @@ -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])) {
Expand Down
20 changes: 16 additions & 4 deletions app/javascript/mastodon/actions/streaming.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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) {
Expand All @@ -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':
Expand All @@ -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))))));
};
Expand All @@ -124,6 +135,7 @@ const refreshHomeTimelineAndNotification = (dispatch, done) => {
* @return {function(): void}
*/
export const connectUserStream = () =>
// @ts-expect-error
connectTimelineStream('home', 'user', {}, { fallback: refreshHomeTimelineAndNotification, fillGaps: fillHomeTimelineGaps });

/**
Expand Down
4 changes: 2 additions & 2 deletions app/javascript/mastodon/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand All @@ -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) {
Expand Down
62 changes: 0 additions & 62 deletions app/javascript/mastodon/components/avatar.jsx

This file was deleted.

40 changes: 40 additions & 0 deletions app/javascript/mastodon/components/avatar.tsx
Original file line number Diff line number Diff line change
@@ -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;
1 change: 1 addition & 0 deletions app/javascript/mastodon/components/blurhash.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
1 change: 1 addition & 0 deletions app/javascript/mastodon/components/common_counter.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// @ts-check
import React from 'react';
// @ts-expect-error
import { FormattedMessage } from 'react-intl';

/**
Expand Down
12 changes: 10 additions & 2 deletions app/javascript/mastodon/components/hashtag.jsx
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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;
}
Expand All @@ -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()}
/>
);
Expand All @@ -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'>
Expand All @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
Loading

0 comments on commit 4520e64

Please sign in to comment.