From 1565af1caf1def5d15e77d8bebd8924deda50d53 Mon Sep 17 00:00:00 2001 From: Plastikmensch Date: Wed, 26 Apr 2023 21:30:41 +0200 Subject: [PATCH] [Glitch] [Proposal] Make able to write React in Typescript (#2190) Port 4520e6473afe901005e7ac532317f4b4f1af9ead to glitch-soc Signed-off-by: Plastikmensch --- .../glitch/actions/picture_in_picture.js | 1 + .../flavours/glitch/actions/streaming.js | 12 +++ app/javascript/flavours/glitch/api.js | 4 +- .../flavours/glitch/components/avatar.jsx | 79 ------------------- .../flavours/glitch/components/avatar.tsx | 48 +++++++++++ .../flavours/glitch/components/blurhash.jsx | 1 + .../flavours/glitch/components/hashtag.jsx | 3 + .../features/emoji/emoji_mart_data_light.js | 2 +- .../emoji/emoji_unicode_mapping_light.js | 6 +- .../flavours/glitch/initial_state.js | 9 ++- app/javascript/flavours/glitch/is_mobile.js | 3 +- app/javascript/flavours/glitch/stream.js | 10 +++ app/javascript/flavours/glitch/uuid.js | 3 - app/javascript/flavours/glitch/uuid.ts | 3 + app/javascript/mastodon/initial_state.js | 1 + tsconfig.json | 12 ++- 16 files changed, 105 insertions(+), 92 deletions(-) delete mode 100644 app/javascript/flavours/glitch/components/avatar.jsx create mode 100644 app/javascript/flavours/glitch/components/avatar.tsx delete mode 100644 app/javascript/flavours/glitch/uuid.js create mode 100644 app/javascript/flavours/glitch/uuid.ts diff --git a/app/javascript/flavours/glitch/actions/picture_in_picture.js b/app/javascript/flavours/glitch/actions/picture_in_picture.js index 33d8d57d47..6b9ff7709e 100644 --- a/app/javascript/flavours/glitch/actions/picture_in_picture.js +++ b/app/javascript/flavours/glitch/actions/picture_in_picture.js @@ -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])) { diff --git a/app/javascript/flavours/glitch/actions/streaming.js b/app/javascript/flavours/glitch/actions/streaming.js index ffac1b2582..8c161ea83c 100644 --- a/app/javascript/flavours/glitch/actions/streaming.js +++ b/app/javascript/flavours/glitch/actions/streaming.js @@ -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,6 +62,7 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti onConnect() { dispatch(connectTimeline(timelineId)); + // @ts-expect-error if (pollingId) { clearTimeout(pollingId); pollingId = null; @@ -75,6 +77,7 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti dispatch(disconnectTimeline(timelineId)); if (options.fallback) { + // @ts-expect-error pollingId = setTimeout(() => useFallback(options.fallback), randomUpTo(40000)); } }, @@ -82,24 +85,30 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti 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 }); /** diff --git a/app/javascript/flavours/glitch/api.js b/app/javascript/flavours/glitch/api.js index 6bbddbef66..42b64d6cc5 100644 --- a/app/javascript/flavours/glitch/api.js +++ b/app/javascript/flavours/glitch/api.js @@ -36,7 +36,7 @@ const setCSRFHeader = () => { ready(setCSRFHeader); /** - * @param {() => import('immutable').Map} getState + * @param {() => import('immutable').Map} getState * @returns {import('axios').RawAxiosRequestHeaders} */ const authorizationHeaderFromState = getState => { @@ -52,7 +52,7 @@ const authorizationHeaderFromState = getState => { }; /** - * @param {() => import('immutable').Map} getState + * @param {() => import('immutable').Map} getState * @returns {import('axios').AxiosInstance} */ export default function api(getState) { diff --git a/app/javascript/flavours/glitch/components/avatar.jsx b/app/javascript/flavours/glitch/components/avatar.jsx deleted file mode 100644 index f30b33e700..0000000000 --- a/app/javascript/flavours/glitch/components/avatar.jsx +++ /dev/null @@ -1,79 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { autoPlayGif } from 'flavours/glitch/initial_state'; -import classNames from 'classnames'; - -export default class Avatar extends React.PureComponent { - - static propTypes = { - account: ImmutablePropTypes.map, - className: PropTypes.string, - 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, - animate, - className, - inline, - size, - } = this.props; - const { hovering } = this.state; - - const style = { - ...this.props.style, - width: `${size}px`, - height: `${size}px`, - backgroundSize: `${size}px ${size}px`, - }; - - if (account) { - const src = account.get('avatar'); - const staticSrc = account.get('avatar_static'); - - if (hovering || animate) { - style.backgroundImage = `url(${src})`; - } else { - style.backgroundImage = `url(${staticSrc})`; - } - } - - return ( -
- ); - } - -} diff --git a/app/javascript/flavours/glitch/components/avatar.tsx b/app/javascript/flavours/glitch/components/avatar.tsx new file mode 100644 index 0000000000..a21ffc9888 --- /dev/null +++ b/app/javascript/flavours/glitch/components/avatar.tsx @@ -0,0 +1,48 @@ +import * as React from 'react'; +import classNames from 'classnames'; +import { autoPlayGif } from 'flavours/glitch/initial_state'; +import { useHovering } from 'hooks/useHovering'; +import type { Account } from 'types/resources'; + +type Props = { + account: Account | undefined; + className?: string; + size: number; + style?: React.CSSProperties; + inline?: boolean; +} + +export const Avatar: React.FC = ({ + account, + className, + size = 20, + inline = false, + style: styleFromParent, +}) => { + const { hovering, handleMouseEnter, handleMouseLeave } = useHovering(autoPlayGif); + + const style = { + ...styleFromParent, + width: `${size}px`, + height: `${size}px`, + backgroundSize: `${size}px ${size}px`, + }; + + if (account) { + style.backgroundImage = `url(${account.get(hovering ? 'avatar' : 'avatar_static')})`; + } + + return ( +
+ ); +}; + +export default Avatar; diff --git a/app/javascript/flavours/glitch/components/blurhash.jsx b/app/javascript/flavours/glitch/components/blurhash.jsx index 2af5cfc568..07cd31b6ca 100644 --- a/app/javascript/flavours/glitch/components/blurhash.jsx +++ b/app/javascript/flavours/glitch/components/blurhash.jsx @@ -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 }); diff --git a/app/javascript/flavours/glitch/components/hashtag.jsx b/app/javascript/flavours/glitch/components/hashtag.jsx index 422b9a8fa1..bc4d7c7bdc 100644 --- a/app/javascript/flavours/glitch/components/hashtag.jsx +++ b/app/javascript/flavours/glitch/components/hashtag.jsx @@ -50,12 +50,14 @@ export const accountsCountRenderer = (displayNumber, pluralReady) => ( /> ); +// @ts-expect-error export const ImmutableHashtag = ({ hashtag }) => ( day.get('uses')).toArray()} /> ); @@ -64,6 +66,7 @@ ImmutableHashtag.propTypes = { hashtag: ImmutablePropTypes.map.isRequired, }; +// @ts-expect-error const Hashtag = ({ name, href, to, people, uses, history, className, description, withGraph }) => (
diff --git a/app/javascript/flavours/glitch/features/emoji/emoji_mart_data_light.js b/app/javascript/flavours/glitch/features/emoji/emoji_mart_data_light.js index 45086fc4cc..49813537d7 100644 --- a/app/javascript/flavours/glitch/features/emoji/emoji_mart_data_light.js +++ b/app/javascript/flavours/glitch/features/emoji/emoji_mart_data_light.js @@ -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 [ diff --git a/app/javascript/flavours/glitch/features/emoji/emoji_unicode_mapping_light.js b/app/javascript/flavours/glitch/features/emoji/emoji_unicode_mapping_light.js index 918684c310..1a38fde234 100644 --- a/app/javascript/flavours/glitch/features/emoji/emoji_unicode_mapping_light.js +++ b/app/javascript/flavours/glitch/features/emoji/emoji_unicode_mapping_light.js @@ -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'); diff --git a/app/javascript/flavours/glitch/initial_state.js b/app/javascript/flavours/glitch/initial_state.js index c3b27687df..e230af1c7e 100644 --- a/app/javascript/flavours/glitch/initial_state.js +++ b/app/javascript/flavours/glitch/initial_state.js @@ -51,6 +51,7 @@ * @property {boolean} activity_api_enabled * @property {string} admin * @property {boolean=} boost_modal + * @property {boolean=} favourite_modal * @property {boolean} crop_images * @property {boolean=} delete_modal * @property {boolean=} disable_swiping @@ -81,7 +82,9 @@ * @property {boolean=} use_pending_items * @property {string} version * @property {boolean} translation_enabled - * @property {object} local_settings + * @property {string} status_page_url + * @property {boolean} system_emoji_font + * @property {string} default_content_type */ /** @@ -89,6 +92,9 @@ * @property {Record} accounts * @property {InitialStateLanguage[]} languages * @property {InitialStateMeta} meta + * @property {object} local_settings + * @property {number} max_toot_chars + * @property {number} poll_limits */ const element = document.getElementById('initial-state'); @@ -98,6 +104,7 @@ const initialState = element?.textContent && JSON.parse(element.textContent); // Glitch-soc-specific “local settings” if (initialState) { try { + // @ts-expect-error initialState.local_settings = JSON.parse(localStorage.getItem('mastodon-settings')); } catch (e) { initialState.local_settings = {}; diff --git a/app/javascript/flavours/glitch/is_mobile.js b/app/javascript/flavours/glitch/is_mobile.js index 31944d89b3..f87da2c5de 100644 --- a/app/javascript/flavours/glitch/is_mobile.js +++ b/app/javascript/flavours/glitch/is_mobile.js @@ -36,6 +36,7 @@ export const layoutFromWindow = (layout_local_setting) => { } }; +// @ts-expect-error const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; const listenerOptions = supportsPassiveEvents ? { passive: true } : false; @@ -45,7 +46,7 @@ let userTouching = false; const touchListener = () => { userTouching = true; - window.removeEventListener('touchstart', touchListener, listenerOptions); + window.removeEventListener('touchstart', touchListener); }; window.addEventListener('touchstart', touchListener, listenerOptions); diff --git a/app/javascript/flavours/glitch/stream.js b/app/javascript/flavours/glitch/stream.js index c6d12cd6ff..8a60f12b48 100644 --- a/app/javascript/flavours/glitch/stream.js +++ b/app/javascript/flavours/glitch/stream.js @@ -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 })); } @@ -87,6 +90,7 @@ const sharedCallbacks = { subscriptions.forEach(subscription => subscribe(subscription)); }, + // @ts-expect-error received (data) { const { stream } = data; @@ -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']); @@ -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); + // @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; diff --git a/app/javascript/flavours/glitch/uuid.js b/app/javascript/flavours/glitch/uuid.js deleted file mode 100644 index 0d2cfaa776..0000000000 --- a/app/javascript/flavours/glitch/uuid.js +++ /dev/null @@ -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); -} diff --git a/app/javascript/flavours/glitch/uuid.ts b/app/javascript/flavours/glitch/uuid.ts new file mode 100644 index 0000000000..655bcf78c6 --- /dev/null +++ b/app/javascript/flavours/glitch/uuid.ts @@ -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); +} diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js index 56b2f4eb2d..6a220f37da 100644 --- a/app/javascript/mastodon/initial_state.js +++ b/app/javascript/mastodon/initial_state.js @@ -87,6 +87,7 @@ * @property {Record} accounts * @property {InitialStateLanguage[]} languages * @property {InitialStateMeta} meta + * @property {number} max_toot_chars */ const element = document.getElementById('initial-state'); diff --git a/tsconfig.json b/tsconfig.json index 505b19d89b..7c6529d48c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,15 @@ "noEmit": true, "strict": true, "esModuleInterop": true, - "skipLibCheck": true + "skipLibCheck": true, + "baseUrl": ".", + "paths": { + "*": ["app/javascript/*"] + } }, - "include": ["app/javascript/mastodon", "app/javascript/packs"] + "include": [ + "app/javascript/mastodon", + "app/javascript/flavours/glitch", + "app/javascript/packs" + ] }