Skip to content

Commit

Permalink
Complete typescript migration (#3055)
Browse files Browse the repository at this point in the history
* Port Overlay components to Typescript

* Port VideoToolbar components to TypeScript

Currently unused, not sure what to do with these

* Port Confirm/PromptDialogs to TypeScript

* Remove unused component

* Port password reset components to TypeScript

* Port useUserCard hook to TypeScript

* Port miscellaneous components to TypeScript

* Port login dialog to TypeScript

* Remove obsolete file

* Port miscellaneous files to TypeScript

* Port chat command definitions to TypeScript

* Fix file extensions

* Port DialogCloseAnimation to TypeScript

* Delete mobile-specific reducers

* Port remaining legacy action creators to TypeScript

* Delete obsolete files

* Port legacy selectors to TypeScript

* Port mentions resolvers to TypeScript
  • Loading branch information
goto-bus-stop authored Sep 29, 2024
1 parent 2d69277 commit b0233bb
Show file tree
Hide file tree
Showing 98 changed files with 898 additions and 1,002 deletions.
6 changes: 4 additions & 2 deletions src/Uwave.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Provider } from 'react-redux';
import { StyledEngineProvider } from '@mui/material/styles';
import { CacheProvider } from '@emotion/react';
import createCache from '@emotion/cache';
import type { EmptyObject } from 'type-fest';
import AppContainer from './containers/App';
import { get as readSession } from './utils/Session';
import configureStore from './redux/configureStore';
Expand All @@ -27,7 +28,7 @@ export default class Uwave {

#renderTarget: Element | null = null;

#aboutPageComponent: React.ComponentType | null = null;
#aboutPageComponent: React.ComponentType<EmptyObject> | null = null;

#emotionCache = createCache({
key: 'emc',
Expand Down Expand Up @@ -70,7 +71,7 @@ export default class Uwave {
return source;
}

setAboutPageComponent(AboutPageComponent: React.ComponentType) {
setAboutPageComponent(AboutPageComponent: React.ComponentType<EmptyObject>) {
this.#aboutPageComponent = AboutPageComponent;
}

Expand All @@ -92,6 +93,7 @@ export default class Uwave {
this.store.dispatch(socketConnect());
const [initResult] = await Promise.all([
this.store.dispatch(initState()),
// @ts-expect-error TS2569: not sure why this is failing, but it's correct I promise
this.store.dispatch(loadCurrentLanguage()),
]);
this.#resolveReady?.();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,23 @@ import {
import { loadedLanguagesSelector } from '../reducers/locales';
import { languageSelector as currentLanguageSelector } from '../reducers/settings';
import { resources } from '../locales';
import type { Thunk } from '../redux/api';

const inFlight = {};
const inFlight: Record<string, undefined | Promise<void>> = {};

function setLanguage(language) {
function setLanguage(language: string) {
return {
type: CHANGE_LANGUAGE,
payload: language,
};
}

function loadLanguage(language) {
return (dispatch) => {
function loadLanguage(language: string): Thunk<Promise<void>> {
return async (dispatch) => {
if (!Object.hasOwn(resources, language) || typeof resources[language] !== 'function') {
return;
}

dispatch({
type: LOAD_LANGUAGE_START,
payload: language,
Expand All @@ -32,29 +37,28 @@ function loadLanguage(language) {
delete inFlight[language];
});

return inFlight[language];
await inFlight[language];
};
}

export function loadCurrentLanguage() {
return (dispatch, getState) => {
export function loadCurrentLanguage(): Thunk<Promise<void>> {
return async (dispatch, getState) => {
const loadedLanguages = loadedLanguagesSelector(getState());
const currentLanguage = currentLanguageSelector(getState());
if (loadedLanguages.has(currentLanguage)) {
return Promise.resolve();
return;
}

return dispatch(loadLanguage(currentLanguage));
await dispatch(loadLanguage(currentLanguage));
};
}

export function changeLanguage(language) {
return (dispatch, getState) => {
export function changeLanguage(language: string): Thunk<Promise<void>> {
return async (dispatch, getState) => {
const loadedLanguages = loadedLanguagesSelector(getState());
if (loadedLanguages.has(language)) {
return dispatch(setLanguage(language));
if (!loadedLanguages.has(language)) {
await dispatch(loadLanguage(language));
}
return dispatch(loadLanguage(language))
.then(() => dispatch(setLanguage(language)));
dispatch(setLanguage(language));
};
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { SOCKET_CONNECT, SOCKET_RECONNECT } from '../constants/ActionTypes';
import { initState } from '../reducers/auth';
import { openLoginDialog } from '../reducers/dialogs';
import type { Thunk } from '../redux/api';

export function socketConnect() {
return { type: SOCKET_CONNECT };
Expand All @@ -10,8 +11,14 @@ export function socketReconnect() {
return { type: SOCKET_RECONNECT };
}

function whenWindowClosed(window) {
return new Promise((resolve) => {
type CreateCallbackData = {
type: string,
id: string,
suggestedName: string,
avatars: string[],
};
function whenWindowClosed(window: Window) {
return new Promise<void>((resolve) => {
const i = setInterval(() => {
if (window.closed) {
clearInterval(i);
Expand All @@ -20,17 +27,17 @@ function whenWindowClosed(window) {
}, 50);
});
}
function socialLogin(service) {
return (dispatch, getState) => {
function socialLogin(service: string): Thunk<Promise<void>> {
return async (dispatch, getState) => {
const { apiUrl } = getState().config;
let messageHandlerCalled = false;
let promise;
let promise: Promise<void> | undefined;

function onlogin() {
// Check login state after the window closed.
promise = dispatch(initState());
promise = dispatch(initState()).then(() => {});
}
function oncreate(data) {
function oncreate(data: CreateCallbackData) {
promise = Promise.resolve();
dispatch(openLoginDialog({
show: 'social',
Expand All @@ -41,7 +48,7 @@ function socialLogin(service) {
}));
}

const apiOrigin = new URL(apiUrl, window.location.href).origin;
const apiOrigin = new URL(apiUrl!, window.location.href).origin;
const clientOrigin = window.location.origin;

window.addEventListener('message', (event) => {
Expand All @@ -62,10 +69,16 @@ function socialLogin(service) {
});

const loginWindow = window.open(`${apiUrl}/auth/service/${service}?origin=${clientOrigin}`);
return whenWindowClosed(loginWindow).then(() => {
if (messageHandlerCalled) return promise;
return onlogin();
});
if (loginWindow == null) {
throw new Error('Could not open OAuth window');
}

await whenWindowClosed(loginWindow);
if (messageHandlerCalled) {
await promise;
} else {
onlogin();
}
};
}
export function loginWithGoogle() {
Expand Down
3 changes: 2 additions & 1 deletion src/app.js → src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ const uw = new Uwave(clientOptions);
uw.use(experimentalThemePlugin);

// Configure the Media sources to be used by this üWave client instance.
// @ts-expect-error: TS2345 - TODO
uw.source(youTubeSource());
uw.source(soundCloudSource());

window.uw = uw;
Object.defineProperty(window, 'uw', { value: uw });

load(uw).catch((err) => {
setTimeout(() => {
Expand Down
27 changes: 11 additions & 16 deletions src/components/About/index.jsx → src/components/About/index.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useState } from 'react';
import { useTranslator } from '@u-wave/react-translate';
import type { EmptyObject } from 'type-fest';
import List, { ListItem, ListItemText } from '../List';
import OverlayHeader from '../Overlay/Header';
import OverlayContent from '../Overlay/Content';
import ServerList from '../ServerList';

const { useState } = React;

type AboutProps = {
onCloseOverlay: () => void,
aboutComponent?: React.ComponentType<EmptyObject> | undefined,
};
function About({
onCloseOverlay,
hasAboutPage,
render: AboutPanel,
}) {
aboutComponent: AboutPanel,
}: AboutProps) {
const { t } = useTranslator();
const [active, setActive] = useState(hasAboutPage ? 'about' : 'servers');
const [active, setActive] = useState(AboutPanel != null ? 'about' : 'servers');

return (
<div className="About">
Expand All @@ -25,7 +26,7 @@ function About({
/>
<OverlayContent className="AboutPanel">
<List className="AboutPanel-menu">
{hasAboutPage && (
{AboutPanel != null && (
<ListItem
className="AboutPanel-menuItem"
selected={active === 'about'}
Expand All @@ -43,18 +44,12 @@ function About({
</ListItem>
</List>
<div className="AboutPanel-content">
{active === 'about' && <AboutPanel />}
{active === 'about' && AboutPanel != null ? <AboutPanel /> : null}
{active === 'servers' && <ServerList />}
</div>
</OverlayContent>
</div>
);
}

About.propTypes = {
onCloseOverlay: PropTypes.func.isRequired,
hasAboutPage: PropTypes.bool.isRequired,
render: PropTypes.func.isRequired,
};

export default About;
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import React from 'react';
import PropTypes from 'prop-types';
import { CSSTransition, TransitionGroup } from 'react-transition-group';

// Use the css transitionend event to mark the finish of a transition.
// Using this instead of a timeout prop so that we don't have to define
// the timeout in multiple places, and it also fixes a small visual
// glitch in Firefox where a scrollbar would appear for a split second
// when the enter transition was almost complete.
function addTransitionEndListener(node, done) {
function addTransitionEndListener(node: HTMLElement, done: () => void) {
node.addEventListener('transitionend', done, false);
}

const Overlays = ({ children, active }) => {
type OverlaysProps = {
children: React.ReactElement | React.ReactElement[],
active: string | null,
};
function Overlays({ children, active }: OverlaysProps) {
let view;
if (Array.isArray(children)) {
view = children.find((child) => child.key === active);
Expand All @@ -38,11 +40,6 @@ const Overlays = ({ children, active }) => {
{view}
</TransitionGroup>
);
};

Overlays.propTypes = {
children: PropTypes.node,
active: PropTypes.string,
};
}

export default Overlays;
18 changes: 8 additions & 10 deletions src/components/App/index.jsx → src/components/App/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import PropTypes from 'prop-types';
import FooterBar from '../FooterBar';
import HeaderBar from '../../containers/HeaderBar';
import Video from '../../containers/Video';
Expand All @@ -14,11 +13,16 @@ import SidePanels from '../SidePanels';
import Dialogs from '../Dialogs';
import AddToPlaylistMenu from '../../containers/AddToPlaylistMenu';

type AppProps = {
isConnected: boolean,
activeOverlay: string | null,
onCloseOverlay: () => void,
};
function App({
activeOverlay,
isConnected,
activeOverlay,
onCloseOverlay,
}) {
}: AppProps) {
return (
<div className="App">
<div className="AppColumn AppColumn--left">
Expand All @@ -33,7 +37,7 @@ function App({
<ErrorArea />
<ConnectionIndicator isConnected={isConnected} />
</div>
<Overlays transitionName="Overlay" active={activeOverlay}>
<Overlays active={activeOverlay}>
<About key="about" onCloseOverlay={onCloseOverlay} />
<AdminProxy key="admin" onCloseOverlay={onCloseOverlay} />
<PlaylistManager key="playlistManager" onCloseOverlay={onCloseOverlay} />
Expand All @@ -54,10 +58,4 @@ function App({
);
}

App.propTypes = {
activeOverlay: PropTypes.string,
isConnected: PropTypes.bool.isRequired,
onCloseOverlay: PropTypes.func.isRequired,
};

export default App;
17 changes: 9 additions & 8 deletions src/components/Chat/Message.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import cx from 'clsx';
import { memo, useCallback, useMemo } from 'react';
import {
memo, useCallback, useMemo, useRef,
} from 'react';
import CircularProgress from '@mui/material/CircularProgress';
import type { MarkupNode } from 'u-wave-parse-chat-markup';
import type { User } from '../../reducers/users';
Expand Down Expand Up @@ -66,13 +68,12 @@ function ChatMessage({
deletable,
onDelete,
}: ChatMessageProps) {
const userCard = useUserCard(user);
const rootRef = useRef<HTMLDivElement>(null);
const { card, open: openCard } = useUserCard(user, rootRef);
const onUsernameClick = useCallback((event: React.MouseEvent) => {
event.preventDefault();
userCard.open();
// The `userCard.open` reference never changes.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
openCard();
}, [openCard]);

let avatar: React.ReactNode;
if (inFlight) {
Expand Down Expand Up @@ -100,8 +101,8 @@ function ChatMessage({
'ChatMessage--mention': isMention,
});
return (
<div className={className} ref={userCard.refAnchor}>
{userCard.card}
<div className={className} ref={rootRef}>
{card}
{avatar}
<div className="ChatMessage-content">
<div className="ChatMessage-hover">
Expand Down
Loading

0 comments on commit b0233bb

Please sign in to comment.