Skip to content

Commit a9e8ede

Browse files
committed
feat: show overlay when character is playing or stopping
1 parent 567e171 commit a9e8ede

File tree

5 files changed

+84
-12
lines changed

5 files changed

+84
-12
lines changed

electron/renderer/components/sidebar/sidebar.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export const Sidebar: React.FC = (): ReactNode => {
2020
setShowSettings(false);
2121
}, []);
2222

23-
useSubscribe('sidebar:show', (sidebarId: SidebarId) => {
23+
useSubscribe(['sidebar:show'], (sidebarId: SidebarId) => {
2424
closeSidebar();
2525
switch (sidebarId) {
2626
case SidebarId.Characters:

electron/renderer/context/game.tsx

+48-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import type { IpcRendererEvent } from 'electron';
2+
import { EuiLoadingSpinner, EuiOverlayMask } from '@elastic/eui';
3+
import { useRouter } from 'next/router.js';
24
import type { ReactNode } from 'react';
3-
import { createContext, useEffect } from 'react';
5+
import { createContext, useEffect, useState } from 'react';
46
import type {
57
GameConnectMessage,
68
GameDisconnectMessage,
79
GameErrorMessage,
810
} from '../../common/game/types.js';
911
import { useQuitCharacter } from '../hooks/characters.jsx';
1012
import { useLogger } from '../hooks/logger.jsx';
13+
import { useSubscribe } from '../hooks/pubsub.jsx';
1114
import { runInBackground } from '../lib/async/run-in-background.js';
1215

1316
/**
@@ -34,9 +37,41 @@ export const GameProvider: React.FC<GameProviderProps> = (
3437
const { children } = props;
3538

3639
const logger = useLogger('context:game');
40+
const router = useRouter();
3741

3842
const quitCharacter = useQuitCharacter();
3943

44+
// To protect against a user pressing play/stop while the app
45+
// is transitioning between characters, show a loading spinner.
46+
const [showPlayStartingOverlay, setShowPlayStartingOverlay] =
47+
useState<boolean>(false);
48+
49+
const [showPlayStoppingOverlay, setShowPlayStoppingOverlay] =
50+
useState<boolean>(false);
51+
52+
// You may be lured into subscribing to multiple events
53+
// to set a single overlay state as true/false, but don't do that.
54+
// The start/stop events fire back-to-back when you play
55+
// a second character and one is already playing. What you see
56+
// is a quick flicker of the overlay then no overlay at all.
57+
// Instead, use two variables to drive the overlay.
58+
useSubscribe(['character:play:starting'], async () => {
59+
setShowPlayStartingOverlay(true);
60+
});
61+
62+
useSubscribe(['character:play:started'], async () => {
63+
setShowPlayStartingOverlay(false);
64+
await router.push('/grid');
65+
});
66+
67+
useSubscribe(['character:play:stopping'], async () => {
68+
setShowPlayStoppingOverlay(true);
69+
});
70+
71+
useSubscribe(['character:play:stopped'], async () => {
72+
setShowPlayStoppingOverlay(false);
73+
});
74+
4075
useEffect(() => {
4176
const unsubscribe = window.api.onMessage(
4277
'game:connect',
@@ -90,5 +125,16 @@ export const GameProvider: React.FC<GameProviderProps> = (
90125
};
91126
}, [logger]);
92127

93-
return <GameContext.Provider value={{}}>{children}</GameContext.Provider>;
128+
return (
129+
<GameContext.Provider value={{}}>
130+
<>
131+
{(showPlayStartingOverlay || showPlayStoppingOverlay) && (
132+
<EuiOverlayMask>
133+
<EuiLoadingSpinner size="l" />
134+
</EuiOverlayMask>
135+
)}
136+
{children}
137+
</>
138+
</GameContext.Provider>
139+
);
94140
};

electron/renderer/hooks/accounts.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export const useListAccounts = (): Array<Account> => {
3434
}, []);
3535

3636
// Reload when told to.
37-
useSubscribe('accounts:reload', async () => {
37+
useSubscribe(['accounts:reload'], async () => {
3838
await loadAccounts();
3939
});
4040

@@ -62,6 +62,7 @@ export const useSaveAccount = (): SaveAccountFn => {
6262
const fn = useCallback<SaveAccountFn>(
6363
async (options): Promise<void> => {
6464
const { accountName, accountPassword } = options;
65+
publish('account:saving', { accountName });
6566
await window.api.saveAccount({ accountName, accountPassword });
6667
publish('account:saved', { accountName });
6768
publish('accounts:reload');
@@ -83,6 +84,7 @@ export const useRemoveAccount = (): RemoveAccountFn => {
8384
const fn = useCallback<RemoveAccountFn>(
8485
async (options): Promise<void> => {
8586
const { accountName } = options;
87+
publish('account:removing', { accountName });
8688
await window.api.removeAccount({ accountName });
8789
publish('account:removed', { accountName });
8890
publish('accounts:reload');

electron/renderer/hooks/characters.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export const useListCharacters = (options?: {
2929
}, [options?.accountName]);
3030

3131
// Reload when told to.
32-
useSubscribe('characters:reload', async () => {
32+
useSubscribe(['characters:reload'], async () => {
3333
await loadCharacters();
3434
});
3535

@@ -53,6 +53,7 @@ export const useSaveCharacter = (): SaveCharacterFn => {
5353

5454
const fn = useCallback<SaveCharacterFn>(
5555
async (character): Promise<void> => {
56+
publish('character:saving', character);
5657
await window.api.saveCharacter(character);
5758
publish('character:saved', character);
5859
publish('characters:reload');
@@ -77,6 +78,7 @@ export const useRemoveCharacter = (): RemoveCharacterFn => {
7778

7879
const fn = useCallback<RemoveCharacterFn>(
7980
async (character): Promise<void> => {
81+
publish('character:removing', character);
8082
if (isEqual(playingCharacter, character)) {
8183
await quitCharacter();
8284
}
@@ -104,6 +106,7 @@ export const usePlayCharacter = (): PlayCharacterFn => {
104106

105107
const fn = useCallback<PlayCharacterFn>(
106108
async (character): Promise<void> => {
109+
publish('character:play:starting', character);
107110
await quitCharacter(); // quit any currently playing character, if any
108111
await window.api.playCharacter(character);
109112
setPlayingCharacter(character);
@@ -129,6 +132,7 @@ export const useQuitCharacter = (): QuitCharacterFn => {
129132

130133
const fn = useCallback<QuitCharacterFn>(async (): Promise<void> => {
131134
if (playingCharacter) {
135+
publish('character:play:stopping', playingCharacter);
132136
await window.api.quitCharacter();
133137
setPlayingCharacter(undefined);
134138
publish('character:play:stopped', playingCharacter);

electron/renderer/hooks/pubsub.tsx

+27-7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { useEffect, useMemo } from 'react';
22
import { create } from 'zustand';
33
import { useShallow } from 'zustand/react/shallow';
4+
import type { Logger } from '../../common/logger/types.js';
45
import { runInBackground } from '../lib/async/run-in-background.js';
6+
import { createLogger } from '../lib/logger/create-logger.js';
57

68
export type PubSubSubscriber = (data?: any) => Promise<void> | void;
79

@@ -37,23 +39,27 @@ interface PubSub {
3739
}
3840

3941
/**
40-
* Hook that subscribes to an event.
42+
* Hook that subscribes to one or more events.
4143
* Automatically unsubscribes when the component unmounts.
4244
*
4345
* For more granular control, use `usePubSub()`.
4446
*/
4547
export const useSubscribe = (
46-
event: string,
48+
events: Array<string>,
4749
subscriber: PubSubSubscriber
4850
): void => {
4951
const subscribe = usePubSubStore((state) => state.subscribe);
5052

5153
useEffect(() => {
52-
const unsubscribe = subscribe({ event, subscriber });
54+
const unsubscribes = events.map((event) => {
55+
return subscribe({ event, subscriber });
56+
});
5357
return () => {
54-
unsubscribe();
58+
unsubscribes.forEach((unsubscribe) => {
59+
unsubscribe();
60+
});
5561
};
56-
}, [event, subscriber, subscribe]);
62+
}, [events, subscriber, subscribe]);
5763
};
5864

5965
/**
@@ -99,6 +105,11 @@ export const usePubSub = (): PubSub => {
99105
};
100106

101107
interface PubSubStoreData {
108+
/**
109+
* Private logger for the pubsub store.
110+
*/
111+
logger: Logger;
112+
102113
/**
103114
* Map of event names to subscribers.
104115
*/
@@ -132,6 +143,8 @@ interface PubSubStoreData {
132143
* An implementation of the PubSub pattern.
133144
*/
134145
const usePubSubStore = create<PubSubStoreData>((set, get) => ({
146+
logger: createLogger('hooks:pubsub'),
147+
135148
subscribers: {},
136149

137150
subscribe: (options: { event: string; subscriber: PubSubSubscriber }) => {
@@ -188,8 +201,15 @@ const usePubSubStore = create<PubSubStoreData>((set, get) => ({
188201
// so that a slow subscriber doesn't block the others.
189202
runInBackground(async () => {
190203
await Promise.allSettled(
191-
subscribers.map((subscriber) => {
192-
return subscriber(data);
204+
subscribers.map(async (subscriber) => {
205+
try {
206+
await subscriber(data);
207+
} catch (error) {
208+
get().logger.error('error in pubsub subscriber', {
209+
event,
210+
error,
211+
});
212+
}
193213
})
194214
);
195215
});

0 commit comments

Comments
 (0)