mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-10-23 19:07:56 -05:00
Don't replay SQ/tournament sounds when switching tabs
This commit is contained in:
@@ -43,6 +43,7 @@ const queued = (overrides: Partial<GlobalStatus> = {}): GlobalStatus => ({
|
||||
describe("useGlobalStatusSounds", () => {
|
||||
beforeEach(() => {
|
||||
mocks.playSound.mockClear();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
test.each([
|
||||
@@ -99,4 +100,40 @@ describe("useGlobalStatusSounds", () => {
|
||||
|
||||
expect(mocks.playSound).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("stays silent for a moment another tab already announced", async () => {
|
||||
const match: GlobalStatus = { state: "SQ_MATCH", url: sendouQMatchPage(1) };
|
||||
const staleTab = await render(<Sounds status={queued()} />);
|
||||
await transitionTo(queued(), match);
|
||||
mocks.playSound.mockClear();
|
||||
|
||||
await staleTab.rerender(<Sounds status={match} />);
|
||||
|
||||
expect(mocks.playSound).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("stays silent for a like another tab already announced", async () => {
|
||||
const staleTab = await render(<Sounds status={queued({ count: 1 })} />);
|
||||
await transitionTo(queued({ count: 1 }), queued({ count: 2 }));
|
||||
mocks.playSound.mockClear();
|
||||
|
||||
await staleTab.rerender(<Sounds status={queued({ count: 2 })} />);
|
||||
|
||||
expect(mocks.playSound).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("plays a repeated ready check after the queued status was seen in between", async () => {
|
||||
const readyCheck: GlobalStatus = {
|
||||
state: "SQ_READY_CHECK",
|
||||
url: SENDOUQ_READY_PAGE,
|
||||
};
|
||||
const screen = await render(<Sounds status={queued()} />);
|
||||
await screen.rerender(<Sounds status={readyCheck} />);
|
||||
await screen.rerender(<Sounds status={queued()} />);
|
||||
mocks.playSound.mockClear();
|
||||
|
||||
await screen.rerender(<Sounds status={{ ...readyCheck }} />);
|
||||
|
||||
expect(mocks.playSound).toHaveBeenCalledWith("sq_ready-check");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import * as React from "react";
|
||||
import * as v from "valibot";
|
||||
import { playSound } from "~/features/chat/chat-utils";
|
||||
import * as PersistedState from "~/modules/persisted-state/persisted-state";
|
||||
import type { GlobalStatus, GlobalStatusState } from "./global-status-types";
|
||||
|
||||
const SOUND_BY_STATE: Partial<Record<GlobalStatusState, string>> = {
|
||||
@@ -8,6 +10,13 @@ const SOUND_BY_STATE: Partial<Record<GlobalStatusState, string>> = {
|
||||
TO_MATCH: "tournament_match",
|
||||
};
|
||||
|
||||
const lastSeenStatusPersisted = PersistedState.define({
|
||||
key: "global-status-last-seen",
|
||||
storage: "local",
|
||||
schema: v.nullable(v.string()),
|
||||
default: null,
|
||||
});
|
||||
|
||||
/**
|
||||
* Plays the alert sound of a status the user has just moved into (a ready
|
||||
* check starting, a match being ready to play) as well as of a like landing
|
||||
@@ -15,7 +24,8 @@ const SOUND_BY_STATE: Partial<Record<GlobalStatusState, string>> = {
|
||||
* anywhere on the site rather than only on the page the moment belongs to.
|
||||
*
|
||||
* The first status seen never plays: a sound announces a moment arriving, not
|
||||
* one already underway when the page was opened.
|
||||
* one already underway when the page was opened. Neither does a status another
|
||||
* tab has already seen, so a backgrounded tab catching up stays silent.
|
||||
*/
|
||||
export function useGlobalStatusSounds(status: GlobalStatus | null) {
|
||||
const previousRef = React.useRef<GlobalStatus | null | undefined>(undefined);
|
||||
@@ -24,13 +34,32 @@ export function useGlobalStatusSounds(status: GlobalStatus | null) {
|
||||
const previous = previousRef.current;
|
||||
previousRef.current = status;
|
||||
|
||||
if (previous === undefined || !status) return;
|
||||
const seenByAnotherTab = markSeenAcrossTabs(status);
|
||||
|
||||
if (previous === undefined || !status || seenByAnotherTab) return;
|
||||
|
||||
const sound = soundForTransition(previous, status);
|
||||
if (sound) playSound(sound);
|
||||
}, [status]);
|
||||
}
|
||||
|
||||
/** Records the status as the latest seen by any tab, returning whether it already was. */
|
||||
function markSeenAcrossTabs(status: GlobalStatus | null) {
|
||||
const key = status ? statusKey(status) : null;
|
||||
if (PersistedState.read(lastSeenStatusPersisted) === key) return true;
|
||||
|
||||
PersistedState.write(lastSeenStatusPersisted, key);
|
||||
return false;
|
||||
}
|
||||
|
||||
function statusKey(status: GlobalStatus) {
|
||||
if (status.state === "SQ_QUEUED") {
|
||||
return `${status.state}:${status.groupId}:${status.count ?? 0}`;
|
||||
}
|
||||
|
||||
return `${status.state}:${status.url}`;
|
||||
}
|
||||
|
||||
function soundForTransition(
|
||||
previous: GlobalStatus | null,
|
||||
next: GlobalStatus,
|
||||
|
||||
5
changelog/2026-10-03-sq-sound-multiple-tabs.md
Normal file
5
changelog/2026-10-03-sq-sound-multiple-tabs.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
navItem: [sendouq, medal]
|
||||
type: bug
|
||||
---
|
||||
SendouQ and tournament alert sounds no longer replay when switching to another sendou.ink tab that's open
|
||||
Reference in New Issue
Block a user