Don't replay SQ/tournament sounds when switching tabs

This commit is contained in:
Kalle
2026-10-03 18:22:35 +03:00
parent 03bd9d23e3
commit 2ab43b3fde
3 changed files with 73 additions and 2 deletions

View File

@@ -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");
});
});

View File

@@ -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,

View 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