forked from fedi/mastodon
[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:
parent
2f7c3cb628
commit
4520e6473a
19
.eslintrc.js
19
.eslintrc.js
|
@ -20,13 +20,14 @@ module.exports = {
|
||||||
ATTACHMENT_HOST: false,
|
ATTACHMENT_HOST: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
parser: '@babel/eslint-parser',
|
parser: '@typescript-eslint/parser',
|
||||||
|
|
||||||
plugins: [
|
plugins: [
|
||||||
'react',
|
'react',
|
||||||
'jsx-a11y',
|
'jsx-a11y',
|
||||||
'import',
|
'import',
|
||||||
'promise',
|
'promise',
|
||||||
|
'@typescript-eslint',
|
||||||
],
|
],
|
||||||
|
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
|
@ -41,14 +42,13 @@ module.exports = {
|
||||||
presets: ['@babel/react', '@babel/env'],
|
presets: ['@babel/react', '@babel/env'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
extends: [
|
||||||
|
'plugin:import/typescript',
|
||||||
|
],
|
||||||
settings: {
|
settings: {
|
||||||
react: {
|
react: {
|
||||||
version: 'detect',
|
version: 'detect',
|
||||||
},
|
},
|
||||||
'import/extensions': [
|
|
||||||
'.js', '.jsx',
|
|
||||||
],
|
|
||||||
'import/ignore': [
|
'import/ignore': [
|
||||||
'node_modules',
|
'node_modules',
|
||||||
'\\.(css|scss|json)$',
|
'\\.(css|scss|json)$',
|
||||||
|
@ -56,7 +56,7 @@ module.exports = {
|
||||||
'import/resolver': {
|
'import/resolver': {
|
||||||
node: {
|
node: {
|
||||||
paths: ['app/javascript'],
|
paths: ['app/javascript'],
|
||||||
extensions: ['.js', '.jsx'],
|
extensions: ['.js', '.jsx', '.ts', '.tsx'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -97,7 +97,8 @@ module.exports = {
|
||||||
'no-self-assign': 'off',
|
'no-self-assign': 'off',
|
||||||
'no-trailing-spaces': 'warn',
|
'no-trailing-spaces': 'warn',
|
||||||
'no-unused-expressions': 'error',
|
'no-unused-expressions': 'error',
|
||||||
'no-unused-vars': [
|
'no-unused-vars': 'off',
|
||||||
|
'@typescript-eslint/no-unused-vars': [
|
||||||
'error',
|
'error',
|
||||||
{
|
{
|
||||||
vars: 'all',
|
vars: 'all',
|
||||||
|
@ -116,7 +117,7 @@ module.exports = {
|
||||||
semi: 'error',
|
semi: 'error',
|
||||||
'valid-typeof': '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-boolean-value': 'error',
|
||||||
'react/jsx-closing-bracket-location': ['error', 'line-aligned'],
|
'react/jsx-closing-bracket-location': ['error', 'line-aligned'],
|
||||||
'react/jsx-curly-spacing': 'error',
|
'react/jsx-curly-spacing': 'error',
|
||||||
|
@ -192,6 +193,8 @@ module.exports = {
|
||||||
{
|
{
|
||||||
js: 'never',
|
js: 'never',
|
||||||
jsx: 'never',
|
jsx: 'never',
|
||||||
|
ts: 'never',
|
||||||
|
tsx: 'never',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
'import/newline-after-import': 'error',
|
'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}
|
* @return {object}
|
||||||
*/
|
*/
|
||||||
export const deployPictureInPicture = (statusId, accountId, playerType, props) => {
|
export const deployPictureInPicture = (statusId, accountId, playerType, props) => {
|
||||||
|
// @ts-expect-error
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
// Do not open a player for a toot that does not exist
|
// Do not open a player for a toot that does not exist
|
||||||
if (getState().hasIn(['statuses', statusId])) {
|
if (getState().hasIn(['statuses', statusId])) {
|
||||||
|
|
|
@ -46,6 +46,7 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
|
||||||
connectStream(channelName, params, (dispatch, getState) => {
|
connectStream(channelName, params, (dispatch, getState) => {
|
||||||
const locale = getState().getIn(['meta', 'locale']);
|
const locale = getState().getIn(['meta', 'locale']);
|
||||||
|
|
||||||
|
// @ts-expect-error
|
||||||
let pollingId;
|
let pollingId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -61,9 +62,10 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
|
||||||
onConnect() {
|
onConnect() {
|
||||||
dispatch(connectTimeline(timelineId));
|
dispatch(connectTimeline(timelineId));
|
||||||
|
|
||||||
|
// @ts-expect-error
|
||||||
if (pollingId) {
|
if (pollingId) {
|
||||||
clearTimeout(pollingId);
|
// @ts-ignore
|
||||||
pollingId = null;
|
clearTimeout(pollingId); pollingId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.fillGaps) {
|
if (options.fillGaps) {
|
||||||
|
@ -75,31 +77,38 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
|
||||||
dispatch(disconnectTimeline(timelineId));
|
dispatch(disconnectTimeline(timelineId));
|
||||||
|
|
||||||
if (options.fallback) {
|
if (options.fallback) {
|
||||||
|
// @ts-expect-error
|
||||||
pollingId = setTimeout(() => useFallback(options.fallback), randomUpTo(40000));
|
pollingId = setTimeout(() => useFallback(options.fallback), randomUpTo(40000));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onReceive (data) {
|
onReceive(data) {
|
||||||
switch(data.event) {
|
switch (data.event) {
|
||||||
case 'update':
|
case 'update':
|
||||||
|
// @ts-expect-error
|
||||||
dispatch(updateTimeline(timelineId, JSON.parse(data.payload), options.accept));
|
dispatch(updateTimeline(timelineId, JSON.parse(data.payload), options.accept));
|
||||||
break;
|
break;
|
||||||
case 'status.update':
|
case 'status.update':
|
||||||
|
// @ts-expect-error
|
||||||
dispatch(updateStatus(JSON.parse(data.payload)));
|
dispatch(updateStatus(JSON.parse(data.payload)));
|
||||||
break;
|
break;
|
||||||
case 'delete':
|
case 'delete':
|
||||||
dispatch(deleteFromTimelines(data.payload));
|
dispatch(deleteFromTimelines(data.payload));
|
||||||
break;
|
break;
|
||||||
case 'notification':
|
case 'notification':
|
||||||
|
// @ts-expect-error
|
||||||
dispatch(updateNotifications(JSON.parse(data.payload), messages, locale));
|
dispatch(updateNotifications(JSON.parse(data.payload), messages, locale));
|
||||||
break;
|
break;
|
||||||
case 'conversation':
|
case 'conversation':
|
||||||
|
// @ts-expect-error
|
||||||
dispatch(updateConversations(JSON.parse(data.payload)));
|
dispatch(updateConversations(JSON.parse(data.payload)));
|
||||||
break;
|
break;
|
||||||
case 'announcement':
|
case 'announcement':
|
||||||
|
// @ts-expect-error
|
||||||
dispatch(updateAnnouncements(JSON.parse(data.payload)));
|
dispatch(updateAnnouncements(JSON.parse(data.payload)));
|
||||||
break;
|
break;
|
||||||
case 'announcement.reaction':
|
case 'announcement.reaction':
|
||||||
|
// @ts-expect-error
|
||||||
dispatch(updateAnnouncementsReaction(JSON.parse(data.payload)));
|
dispatch(updateAnnouncementsReaction(JSON.parse(data.payload)));
|
||||||
break;
|
break;
|
||||||
case 'announcement.delete':
|
case 'announcement.delete':
|
||||||
|
@ -115,7 +124,9 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
|
||||||
* @param {function(): void} done
|
* @param {function(): void} done
|
||||||
*/
|
*/
|
||||||
const refreshHomeTimelineAndNotification = (dispatch, done) => {
|
const refreshHomeTimelineAndNotification = (dispatch, done) => {
|
||||||
|
// @ts-expect-error
|
||||||
dispatch(expandHomeTimeline({}, () =>
|
dispatch(expandHomeTimeline({}, () =>
|
||||||
|
// @ts-expect-error
|
||||||
dispatch(expandNotifications({}, () =>
|
dispatch(expandNotifications({}, () =>
|
||||||
dispatch(fetchAnnouncements(done))))));
|
dispatch(fetchAnnouncements(done))))));
|
||||||
};
|
};
|
||||||
|
@ -124,6 +135,7 @@ const refreshHomeTimelineAndNotification = (dispatch, done) => {
|
||||||
* @return {function(): void}
|
* @return {function(): void}
|
||||||
*/
|
*/
|
||||||
export const connectUserStream = () =>
|
export const connectUserStream = () =>
|
||||||
|
// @ts-expect-error
|
||||||
connectTimelineStream('home', 'user', {}, { fallback: refreshHomeTimelineAndNotification, fillGaps: fillHomeTimelineGaps });
|
connectTimelineStream('home', 'user', {}, { fallback: refreshHomeTimelineAndNotification, fillGaps: fillHomeTimelineGaps });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -36,7 +36,7 @@ const setCSRFHeader = () => {
|
||||||
ready(setCSRFHeader);
|
ready(setCSRFHeader);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {() => import('immutable').Map} getState
|
* @param {() => import('immutable').Map<string,any>} getState
|
||||||
* @returns {import('axios').RawAxiosRequestHeaders}
|
* @returns {import('axios').RawAxiosRequestHeaders}
|
||||||
*/
|
*/
|
||||||
const authorizationHeaderFromState = getState => {
|
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}
|
* @returns {import('axios').AxiosInstance}
|
||||||
*/
|
*/
|
||||||
export default function api(getState) {
|
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 ctx = canvas.getContext('2d');
|
||||||
const imageData = new ImageData(pixels, width, height);
|
const imageData = new ImageData(pixels, width, height);
|
||||||
|
|
||||||
|
// @ts-expect-error
|
||||||
ctx.putImageData(imageData, 0, 0);
|
ctx.putImageData(imageData, 0, 0);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Blurhash decoding failure', { err, hash });
|
console.error('Blurhash decoding failure', { err, hash });
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
// @ts-expect-error
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Sparklines, SparklinesCurve } from 'react-sparklines';
|
import { Sparklines, SparklinesCurve } from 'react-sparklines';
|
||||||
|
// @ts-expect-error
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
// @ts-expect-error
|
||||||
import ShortNumber from 'mastodon/components/short_number';
|
import ShortNumber from 'mastodon/components/short_number';
|
||||||
|
// @ts-expect-error
|
||||||
import Skeleton from 'mastodon/components/skeleton';
|
import Skeleton from 'mastodon/components/skeleton';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
@ -19,11 +22,11 @@ class SilentErrorBoundary extends React.Component {
|
||||||
error: false,
|
error: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidCatch () {
|
componentDidCatch() {
|
||||||
this.setState({ error: true });
|
this.setState({ error: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
if (this.state.error) {
|
if (this.state.error) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -50,11 +53,13 @@ export const accountsCountRenderer = (displayNumber, pluralReady) => (
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// @ts-expect-error
|
||||||
export const ImmutableHashtag = ({ hashtag }) => (
|
export const ImmutableHashtag = ({ hashtag }) => (
|
||||||
<Hashtag
|
<Hashtag
|
||||||
name={hashtag.get('name')}
|
name={hashtag.get('name')}
|
||||||
to={`/tags/${hashtag.get('name')}`}
|
to={`/tags/${hashtag.get('name')}`}
|
||||||
people={hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1}
|
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()}
|
history={hashtag.get('history').reverse().map((day) => day.get('uses')).toArray()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -63,6 +68,7 @@ ImmutableHashtag.propTypes = {
|
||||||
hashtag: ImmutablePropTypes.map.isRequired,
|
hashtag: ImmutablePropTypes.map.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// @ts-expect-error
|
||||||
const Hashtag = ({ name, to, people, uses, history, className, description, withGraph }) => (
|
const Hashtag = ({ name, to, people, uses, history, className, description, withGraph }) => (
|
||||||
<div className={classNames('trends__item', className)}>
|
<div className={classNames('trends__item', className)}>
|
||||||
<div className='trends__item__name'>
|
<div className='trends__item__name'>
|
||||||
|
@ -86,7 +92,9 @@ const Hashtag = ({ name, to, people, uses, history, className, description, with
|
||||||
{withGraph && (
|
{withGraph && (
|
||||||
<div className='trends__item__sparkline'>
|
<div className='trends__item__sparkline'>
|
||||||
<SilentErrorBoundary>
|
<SilentErrorBoundary>
|
||||||
|
{/* @ts-expect-error */}
|
||||||
<Sparklines width={50} height={28} data={history ? history : Array.from(Array(7)).map(() => 0)}>
|
<Sparklines width={50} height={28} data={history ? history : Array.from(Array(7)).map(() => 0)}>
|
||||||
|
{/* @ts-expect-error */}
|
||||||
<SparklinesCurve style={{ fill: 'none' }} />
|
<SparklinesCurve style={{ fill: 'none' }} />
|
||||||
</Sparklines>
|
</Sparklines>
|
||||||
</SilentErrorBoundary>
|
</SilentErrorBoundary>
|
||||||
|
|
|
@ -9,7 +9,7 @@ const emojis = {};
|
||||||
// decompress
|
// decompress
|
||||||
Object.keys(shortCodesToEmojiData).forEach((shortCode) => {
|
Object.keys(shortCodesToEmojiData).forEach((shortCode) => {
|
||||||
let [
|
let [
|
||||||
filenameData, // eslint-disable-line no-unused-vars
|
filenameData, // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||||
searchData,
|
searchData,
|
||||||
] = shortCodesToEmojiData[shortCode];
|
] = shortCodesToEmojiData[shortCode];
|
||||||
let [
|
let [
|
||||||
|
|
|
@ -4,9 +4,9 @@
|
||||||
|
|
||||||
const [
|
const [
|
||||||
shortCodesToEmojiData,
|
shortCodesToEmojiData,
|
||||||
skins, // eslint-disable-line no-unused-vars
|
skins, // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||||
categories, // eslint-disable-line no-unused-vars
|
categories, // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||||
short_names, // eslint-disable-line no-unused-vars
|
short_names, // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||||
emojisWithoutShortCodes,
|
emojisWithoutShortCodes,
|
||||||
] = require('./emoji_compressed');
|
] = require('./emoji_compressed');
|
||||||
const { unicodeToFilename } = require('./unicode_to_filename');
|
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 usePendingItems = getMeta('use_pending_items');
|
||||||
export const version = getMeta('version');
|
export const version = getMeta('version');
|
||||||
export const languages = initialState?.languages;
|
export const languages = initialState?.languages;
|
||||||
|
// @ts-expect-error
|
||||||
export const statusPageUrl = getMeta('status_page_url');
|
export const statusPageUrl = getMeta('status_page_url');
|
||||||
|
|
||||||
export default initialState;
|
export default initialState;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||||
|
// @ts-expect-error
|
||||||
import { forceSingleColumn } from 'mastodon/initial_state';
|
import { forceSingleColumn } from 'mastodon/initial_state';
|
||||||
|
|
||||||
const LAYOUT_BREAKPOINT = 630;
|
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 iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
||||||
|
|
||||||
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
||||||
|
@ -33,7 +35,7 @@ let userTouching = false;
|
||||||
const touchListener = () => {
|
const touchListener = () => {
|
||||||
userTouching = true;
|
userTouching = true;
|
||||||
|
|
||||||
window.removeEventListener('touchstart', touchListener, listenerOptions);
|
window.removeEventListener('touchstart', touchListener);
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('touchstart', touchListener, listenerOptions);
|
window.addEventListener('touchstart', touchListener, listenerOptions);
|
||||||
|
|
|
@ -59,6 +59,7 @@ const subscribe = ({ channelName, params, onConnect }) => {
|
||||||
subscriptionCounters[key] = subscriptionCounters[key] || 0;
|
subscriptionCounters[key] = subscriptionCounters[key] || 0;
|
||||||
|
|
||||||
if (subscriptionCounters[key] === 0) {
|
if (subscriptionCounters[key] === 0) {
|
||||||
|
// @ts-expect-error
|
||||||
sharedConnection.send(JSON.stringify({ type: 'subscribe', stream: channelName, ...params }));
|
sharedConnection.send(JSON.stringify({ type: 'subscribe', stream: channelName, ...params }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,7 +75,9 @@ const unsubscribe = ({ channelName, params, onDisconnect }) => {
|
||||||
|
|
||||||
subscriptionCounters[key] = subscriptionCounters[key] || 1;
|
subscriptionCounters[key] = subscriptionCounters[key] || 1;
|
||||||
|
|
||||||
|
// @ts-expect-error
|
||||||
if (subscriptionCounters[key] === 1 && sharedConnection.readyState === WebSocketClient.OPEN) {
|
if (subscriptionCounters[key] === 1 && sharedConnection.readyState === WebSocketClient.OPEN) {
|
||||||
|
// @ts-expect-error
|
||||||
sharedConnection.send(JSON.stringify({ type: 'unsubscribe', stream: channelName, ...params }));
|
sharedConnection.send(JSON.stringify({ type: 'unsubscribe', stream: channelName, ...params }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,11 +86,12 @@ const unsubscribe = ({ channelName, params, onDisconnect }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const sharedCallbacks = {
|
const sharedCallbacks = {
|
||||||
connected () {
|
connected() {
|
||||||
subscriptions.forEach(subscription => subscribe(subscription));
|
subscriptions.forEach(subscription => subscribe(subscription));
|
||||||
},
|
},
|
||||||
|
|
||||||
received (data) {
|
// @ts-expect-error
|
||||||
|
received(data) {
|
||||||
const { stream } = data;
|
const { stream } = data;
|
||||||
|
|
||||||
subscriptions.filter(({ channelName, params }) => {
|
subscriptions.filter(({ channelName, params }) => {
|
||||||
|
@ -111,11 +115,11 @@ const sharedCallbacks = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
disconnected () {
|
disconnected() {
|
||||||
subscriptions.forEach(subscription => unsubscribe(subscription));
|
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
|
* @param {function(Function, Function): { onConnect: (function(): void), onReceive: (function(StreamEvent): void), onDisconnect: (function(): void) }} callbacks
|
||||||
* @return {function(): void}
|
* @return {function(): void}
|
||||||
*/
|
*/
|
||||||
|
// @ts-expect-error
|
||||||
export const connectStream = (channelName, params, callbacks) => (dispatch, getState) => {
|
export const connectStream = (channelName, params, callbacks) => (dispatch, getState) => {
|
||||||
const streamingAPIBaseURL = getState().getIn(['meta', 'streaming_api_base_url']);
|
const streamingAPIBaseURL = getState().getIn(['meta', 'streaming_api_base_url']);
|
||||||
const accessToken = getState().getIn(['meta', 'access_token']);
|
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
|
// to using individual connections for each channel
|
||||||
if (!streamingAPIBaseURL.startsWith('ws')) {
|
if (!streamingAPIBaseURL.startsWith('ws')) {
|
||||||
const connection = createConnection(streamingAPIBaseURL, accessToken, channelNameWithInlineParams(channelName, params), {
|
const connection = createConnection(streamingAPIBaseURL, accessToken, channelNameWithInlineParams(channelName, params), {
|
||||||
connected () {
|
connected() {
|
||||||
onConnect();
|
onConnect();
|
||||||
},
|
},
|
||||||
|
|
||||||
received (data) {
|
received(data) {
|
||||||
onReceive(data);
|
onReceive(data);
|
||||||
},
|
},
|
||||||
|
|
||||||
disconnected () {
|
disconnected() {
|
||||||
onDisconnect();
|
onDisconnect();
|
||||||
},
|
},
|
||||||
|
|
||||||
reconnected () {
|
reconnected() {
|
||||||
onConnect();
|
onConnect();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -227,14 +232,19 @@ const handleEventSourceMessage = (e, received) => {
|
||||||
const createConnection = (streamingAPIBaseURL, accessToken, channelName, { connected, received, disconnected, reconnected }) => {
|
const createConnection = (streamingAPIBaseURL, accessToken, channelName, { connected, received, disconnected, reconnected }) => {
|
||||||
const params = channelName.split('&');
|
const params = channelName.split('&');
|
||||||
|
|
||||||
|
// @ts-expect-error
|
||||||
channelName = params.shift();
|
channelName = params.shift();
|
||||||
|
|
||||||
if (streamingAPIBaseURL.startsWith('ws')) {
|
if (streamingAPIBaseURL.startsWith('ws')) {
|
||||||
|
// @ts-expect-error
|
||||||
const ws = new WebSocketClient(`${streamingAPIBaseURL}/api/v1/streaming/?${params.join('&')}`, accessToken);
|
const ws = new WebSocketClient(`${streamingAPIBaseURL}/api/v1/streaming/?${params.join('&')}`, accessToken);
|
||||||
|
|
||||||
ws.onopen = connected;
|
// @ts-expect-error
|
||||||
ws.onmessage = e => received(JSON.parse(e.data));
|
ws.onopen = connected;
|
||||||
ws.onclose = disconnected;
|
ws.onmessage = e => received(JSON.parse(e.data));
|
||||||
|
// @ts-expect-error
|
||||||
|
ws.onclose = disconnected;
|
||||||
|
// @ts-expect-error
|
||||||
ws.onreconnect = reconnected;
|
ws.onreconnect = reconnected;
|
||||||
|
|
||||||
return ws;
|
return ws;
|
||||||
|
@ -256,7 +266,7 @@ const createConnection = (streamingAPIBaseURL, accessToken, channelName, { conne
|
||||||
};
|
};
|
||||||
|
|
||||||
KNOWN_EVENT_TYPES.forEach(type => {
|
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);
|
es.onerror = /** @type {function(): void} */ (disconnected);
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
const checkNotificationPromise = () => {
|
const checkNotificationPromise = () => {
|
||||||
try {
|
try {
|
||||||
// eslint-disable-next-line promise/catch-or-return, promise/valid-params
|
// eslint-disable-next-line promise/catch-or-return
|
||||||
Notification.requestPermission().then();
|
Notification.requestPermission().then();
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
return false;
|
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]');
|
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);
|
__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 = {
|
const config = {
|
||||||
presets: [
|
presets: [
|
||||||
|
'@babel/preset-typescript',
|
||||||
['@babel/react', reactOptions],
|
['@babel/react', reactOptions],
|
||||||
['@babel/env', envOptions],
|
['@babel/env', envOptions],
|
||||||
],
|
],
|
||||||
|
|
|
@ -2,7 +2,7 @@ const { join, resolve } = require('path');
|
||||||
const { env, settings } = require('../configuration');
|
const { env, settings } = require('../configuration');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
test: /\.(js|jsx|mjs)$/,
|
test: /\.(js|jsx|mjs|ts|tsx)$/,
|
||||||
include: [
|
include: [
|
||||||
settings.source_path,
|
settings.source_path,
|
||||||
...settings.resolved_paths,
|
...settings.resolved_paths,
|
||||||
|
|
|
@ -36,6 +36,8 @@ default: &default
|
||||||
- .mjs
|
- .mjs
|
||||||
- .js
|
- .js
|
||||||
- .jsx
|
- .jsx
|
||||||
|
- .ts
|
||||||
|
- .tsx
|
||||||
- .sass
|
- .sass
|
||||||
- .scss
|
- .scss
|
||||||
- .css
|
- .css
|
||||||
|
|
44
package.json
44
package.json
|
@ -10,10 +10,11 @@
|
||||||
"build:production": "cross-env RAILS_ENV=production NODE_ENV=production ./bin/webpack",
|
"build:production": "cross-env RAILS_ENV=production NODE_ENV=production ./bin/webpack",
|
||||||
"manage:translations": "node ./config/webpack/translationRunner.js",
|
"manage:translations": "node ./config/webpack/translationRunner.js",
|
||||||
"start": "node ./streaming/index.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": "${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:lint:sass": "stylelint \"**/*.{css,scss}\" && prettier --check \"**/*.{css,scss}\"",
|
||||||
|
"test:typecheck": "tsc --noEmit",
|
||||||
"test:jest": "cross-env NODE_ENV=test jest",
|
"test:jest": "cross-env NODE_ENV=test jest",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"format-check": "prettier --check .",
|
"format-check": "prettier --check .",
|
||||||
|
@ -139,9 +140,45 @@
|
||||||
"ws": "^8.12.1"
|
"ws": "^8.12.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/eslint-parser": "^7.21.3",
|
"@babel/preset-typescript": "^7.21.0",
|
||||||
"@testing-library/jest-dom": "^5.16.5",
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
"@testing-library/react": "^12.1.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",
|
"babel-jest": "^29.5.0",
|
||||||
"eslint": "^8.36.0",
|
"eslint": "^8.36.0",
|
||||||
"eslint-plugin-import": "~2.27.5",
|
"eslint-plugin-import": "~2.27.5",
|
||||||
|
@ -160,6 +197,7 @@
|
||||||
"react-test-renderer": "^16.14.0",
|
"react-test-renderer": "^16.14.0",
|
||||||
"stylelint": "^15.3.0",
|
"stylelint": "^15.3.0",
|
||||||
"stylelint-config-standard-scss": "^7.0.1",
|
"stylelint-config-standard-scss": "^7.0.1",
|
||||||
|
"typescript": "^4.9.5",
|
||||||
"webpack-dev-server": "^3.11.3",
|
"webpack-dev-server": "^3.11.3",
|
||||||
"yargs": "^17.7.1"
|
"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"]
|
||||||
|
}
|
Loading…
Reference in a new issue