Sfoglia il codice sorgente

feat: require user interaction prior to loading the UI (so that sound can play)

Fela Maslen 5 anni fa
parent
commit
6b4b7fff49

+ 15 - 10
gmus-web/src/components/app.tsx

@@ -10,18 +10,20 @@ import { useCurrentlyPlayingSongInfo } from '../hooks/status';
 import { isMaster } from '../selectors';
 import { getSongUrl } from '../utils/url';
 import { LoadingWrapper } from './identify';
+import { Interact, Props as InteractProps } from './interact';
 import { Player } from './player';
 import { uiProviders } from './ui';
 import { UIProvider } from './ui/types';
 
 export type Props = {
   socket: WebSocket;
-};
+  interacted: boolean;
+} & InteractProps;
 
 const uiProvider = UIProvider.Cmus;
 const UI = uiProviders[uiProvider];
 
-export const App: React.FC<Props> = ({ socket }) => {
+export const App: React.FC<Props> = ({ socket, interacted, setInteracted }) => {
   useKeepalive(socket);
   useMaster();
   useCurrentlyPlayingSongInfo();
@@ -70,14 +72,17 @@ export const App: React.FC<Props> = ({ socket }) => {
         />
       )}
       <StateInspector name="ui">
-        <Suspense fallback={<LoadingWrapper />}>
-          <UI
-            isMaster={isMaster(state)}
-            currentSong={state.songInfo}
-            nextSong={nextSong}
-            prevSong={prevSong}
-          />
-        </Suspense>
+        {!interacted && <Interact setInteracted={setInteracted} />}
+        {interacted && (
+          <Suspense fallback={<LoadingWrapper />}>
+            <UI
+              isMaster={isMaster(state)}
+              currentSong={state.songInfo}
+              nextSong={nextSong}
+              prevSong={prevSong}
+            />
+          </Suspense>
+        )}
       </StateInspector>
     </>
   );

+ 21 - 6
gmus-web/src/components/identify.spec.tsx

@@ -1,4 +1,4 @@
-import { act, fireEvent, render } from '@testing-library/react';
+import { act, fireEvent, render, RenderResult } from '@testing-library/react';
 import React from 'react';
 
 import { Identify, Props } from './identify';
@@ -7,6 +7,7 @@ describe(Identify.name, () => {
   const props: Props = {
     connecting: false,
     onIdentify: jest.fn(),
+    setInteracted: jest.fn(),
   };
 
   it('should render an input', () => {
@@ -22,11 +23,10 @@ describe(Identify.name, () => {
   });
 
   describe('when pressing the connect button', () => {
-    it('should call the onIdentify prop', () => {
-      expect.assertions(2);
-      const { getByDisplayValue, getByText } = render(<Identify {...props} />);
-      const input = getByDisplayValue('');
-      const button = getByText('Connect');
+    const setupConnect = (): RenderResult => {
+      const renderResult = render(<Identify {...props} />);
+      const input = renderResult.getByDisplayValue('');
+      const button = renderResult.getByText('Connect');
 
       act(() => {
         fireEvent.change(input, { target: { value: 'my-computer' } });
@@ -35,9 +35,24 @@ describe(Identify.name, () => {
         fireEvent.click(button);
       });
 
+      return renderResult;
+    };
+
+    it('should call the onIdentify prop', () => {
+      expect.assertions(2);
+      setupConnect();
+
       expect(props.onIdentify).toHaveBeenCalledTimes(1);
       expect(props.onIdentify).toHaveBeenCalledWith('my-computer');
     });
+
+    it('should set interacted to true', () => {
+      expect.assertions(2);
+      setupConnect();
+
+      expect(props.setInteracted).toHaveBeenCalledTimes(1);
+      expect(props.setInteracted).toHaveBeenCalledWith(true);
+    });
   });
 
   describe('when connecting', () => {

+ 4 - 2
gmus-web/src/components/identify.tsx

@@ -7,6 +7,7 @@ import * as Styled from './identify.styles';
 export type Props = {
   connecting: boolean;
   onIdentify: (name: string) => void;
+  setInteracted: (interacted: boolean) => void;
 };
 
 export const LoadingWrapper: React.FC = () => (
@@ -15,7 +16,7 @@ export const LoadingWrapper: React.FC = () => (
   </Styled.Container>
 );
 
-export const Identify: React.FC<Props> = ({ connecting, onIdentify }) => {
+export const Identify: React.FC<Props> = ({ connecting, onIdentify, setInteracted }) => {
   const [name, setName] = useState<string>('');
   const onChange = useCallback(
     (event: React.ChangeEvent<HTMLInputElement>) => setName(event.target.value),
@@ -23,7 +24,8 @@ export const Identify: React.FC<Props> = ({ connecting, onIdentify }) => {
   );
   const onConnect = useCallback(() => {
     onIdentify(name);
-  }, [name, onIdentify]);
+    setInteracted(true);
+  }, [name, onIdentify, setInteracted]);
 
   const input = useRef<HTMLInputElement>(null);
   useEffect(() => {

+ 25 - 0
gmus-web/src/components/interact.tsx

@@ -0,0 +1,25 @@
+import React, { useCallback, useEffect, useRef } from 'react';
+import { useCTA } from '../hooks/cta';
+import { Container } from './identify.styles';
+
+export type Props = {
+  setInteracted: (interacted: boolean) => void;
+};
+
+export const Interact: React.FC<Props> = ({ setInteracted }) => {
+  const onInteract = useCallback(() => setInteracted(true), [setInteracted]);
+  const ctaProps = useCTA(onInteract);
+
+  const button = useRef<HTMLButtonElement>(null);
+  useEffect(() => {
+    button.current?.focus();
+  }, []);
+
+  return (
+    <Container>
+      <button {...ctaProps} ref={button}>
+        Continue
+      </button>
+    </Container>
+  );
+};

+ 7 - 3
gmus-web/src/components/root.tsx

@@ -1,4 +1,4 @@
-import React, { Reducer, useCallback, useReducer } from 'react';
+import React, { Reducer, useCallback, useReducer, useState } from 'react';
 
 import { AnyAction, nameSet } from '../actions';
 import { DispatchContext, StateContext } from '../context/state';
@@ -28,14 +28,18 @@ export const Root: React.FC = () => {
 
   const dispatchWithEffects = useDispatchWithEffects(state, dispatch, socket);
 
+  const [interacted, setInteracted] = useState<boolean>(false);
+
   if (!(socket && connected && name) || error) {
-    return <Identify connecting={connecting} onIdentify={onIdentify} />;
+    return (
+      <Identify connecting={connecting} onIdentify={onIdentify} setInteracted={setInteracted} />
+    );
   }
 
   return (
     <StateContext.Provider value={state}>
       <DispatchContext.Provider value={dispatchWithEffects}>
-        <App socket={socket} />
+        <App socket={socket} interacted={interacted} setInteracted={setInteracted} />
       </DispatchContext.Provider>
     </StateContext.Provider>
   );