mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
feat(settings): add expanded video auto-unmute preference (#1104)
* feat(settings): add expanded video auto-unmute preference * fix(translations): add missing unmute labels * fix(translations): localize unmute label
This commit is contained in:
@@ -15,6 +15,7 @@ const testState = vi.hoisted(() => ({
|
||||
gifFrameUrl: null as string | null,
|
||||
hostname: 'example.com',
|
||||
isMobile: false,
|
||||
unmuteExpandedVideoSound: false,
|
||||
}));
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
@@ -36,6 +37,7 @@ vi.mock('../../../lib/utils/url-utils', () => ({
|
||||
vi.mock('../../../stores/use-expanded-media-store', () => ({
|
||||
default: () => ({
|
||||
fitExpandedImagesToScreen: testState.fitExpandedImagesToScreen,
|
||||
unmuteExpandedVideoSound: testState.unmuteExpandedVideoSound,
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -76,6 +78,7 @@ describe('CommentMedia', () => {
|
||||
testState.gifFrameUrl = null;
|
||||
testState.hostname = 'example.com';
|
||||
testState.isMobile = false;
|
||||
testState.unmuteExpandedVideoSound = false;
|
||||
setShowThumbnailMock = vi.fn();
|
||||
|
||||
container = document.createElement('div');
|
||||
@@ -252,4 +255,36 @@ describe('CommentMedia', () => {
|
||||
|
||||
expect(setShowThumbnailMock).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('unmutes expanded videos when the preference is enabled', async () => {
|
||||
testState.unmuteExpandedVideoSound = true;
|
||||
|
||||
await renderMedia({
|
||||
commentMediaInfo: {
|
||||
type: 'video',
|
||||
url: 'https://cdn.example.com/video.mp4',
|
||||
},
|
||||
setShowThumbnail: setShowThumbnailMock,
|
||||
showThumbnail: false,
|
||||
});
|
||||
|
||||
const video = container.querySelector<HTMLVideoElement>('video[src="https://cdn.example.com/video.mp4"]');
|
||||
expect(video).toBeTruthy();
|
||||
expect(video?.muted).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps expanded videos muted when the preference is disabled', async () => {
|
||||
await renderMedia({
|
||||
commentMediaInfo: {
|
||||
type: 'video',
|
||||
url: 'https://cdn.example.com/video.mp4',
|
||||
},
|
||||
setShowThumbnail: setShowThumbnailMock,
|
||||
showThumbnail: false,
|
||||
});
|
||||
|
||||
const video = container.querySelector<HTMLVideoElement>('video[src="https://cdn.example.com/video.mp4"]');
|
||||
expect(video).toBeTruthy();
|
||||
expect(video?.muted).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -199,7 +199,7 @@ const Media = ({ commentMediaInfo, disableToggle, isReply, setShowThumbnail }: M
|
||||
const { t } = useTranslation();
|
||||
const { thumbnail, type, url } = commentMediaInfo || {};
|
||||
const isMobile = useIsMobile();
|
||||
const { fitExpandedImagesToScreen } = useExpandedMediaStore();
|
||||
const { fitExpandedImagesToScreen, unmuteExpandedVideoSound } = useExpandedMediaStore();
|
||||
const mediaClass = `${isMobile ? styles.mediaMobile : isReply ? styles.mediaDesktopReply : styles.mediaDesktopOp} ${
|
||||
fitExpandedImagesToScreen ? styles.fitToScreen : ''
|
||||
}`;
|
||||
@@ -228,7 +228,7 @@ const Media = ({ commentMediaInfo, disableToggle, isReply, setShowThumbnail }: M
|
||||
onClick={disableToggle ? undefined : () => setShowThumbnail(true)}
|
||||
/>
|
||||
) : type === 'video' ? (
|
||||
<video src={url} controls autoPlay loop muted />
|
||||
<video src={url} controls autoPlay loop muted={!unmuteExpandedVideoSound} />
|
||||
) : type === 'webpage' ? (
|
||||
<img
|
||||
src={thumbnail}
|
||||
|
||||
+23
-1
@@ -19,6 +19,8 @@ const testState = vi.hoisted(() => ({
|
||||
fetchMock: vi.fn(),
|
||||
fitExpandedImagesToScreen: false,
|
||||
setFitExpandedImagesToScreenMock: vi.fn(),
|
||||
setUnmuteExpandedVideoSoundMock: vi.fn(),
|
||||
unmuteExpandedVideoSound: false,
|
||||
}));
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
@@ -32,6 +34,8 @@ vi.mock('../../../../stores/use-expanded-media-store', () => ({
|
||||
default: () => ({
|
||||
fitExpandedImagesToScreen: testState.fitExpandedImagesToScreen,
|
||||
setFitExpandedImagesToScreen: testState.setFitExpandedImagesToScreenMock,
|
||||
setUnmuteExpandedVideoSound: testState.setUnmuteExpandedVideoSoundMock,
|
||||
unmuteExpandedVideoSound: testState.unmuteExpandedVideoSound,
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -86,6 +90,8 @@ describe('InterfaceSettings', () => {
|
||||
testState.fetchMock.mockReset();
|
||||
testState.fitExpandedImagesToScreen = false;
|
||||
testState.setFitExpandedImagesToScreenMock.mockReset();
|
||||
testState.setUnmuteExpandedVideoSoundMock.mockReset();
|
||||
testState.unmuteExpandedVideoSound = false;
|
||||
useFeedViewSettingsStore.getState().setEnableInfiniteScroll(false);
|
||||
useAppUpdateStore.setState({
|
||||
needRefresh: false,
|
||||
@@ -131,12 +137,14 @@ describe('InterfaceSettings', () => {
|
||||
|
||||
await act(async () => {
|
||||
checkbox?.click();
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(useFeedViewSettingsStore.getState().enableInfiniteScroll).toBe(true);
|
||||
expect(checkbox?.checked).toBe(true);
|
||||
expect(container.querySelector('[data-testid="board-mode"]')?.textContent).toBe('infinite');
|
||||
expect(setItemSpy).toHaveBeenCalledWith(STORAGE_KEY, expect.stringContaining('"enableInfiniteScroll":true'));
|
||||
expect(localStorage.getItem(STORAGE_KEY)).toContain('"enableInfiniteScroll":true');
|
||||
});
|
||||
|
||||
it('toggles fit expanded images through the media store', async () => {
|
||||
@@ -153,6 +161,20 @@ describe('InterfaceSettings', () => {
|
||||
expect(testState.setFitExpandedImagesToScreenMock).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('toggles unmute video sound through the media store', async () => {
|
||||
render(createElement(InterfaceSettings));
|
||||
|
||||
const label = Array.from(container.querySelectorAll('label')).find((candidate) => candidate.textContent?.toLowerCase().includes('unmute_video_sound'));
|
||||
const checkbox = label?.querySelector<HTMLInputElement>('input[type="checkbox"]');
|
||||
expect(checkbox).toBeTruthy();
|
||||
|
||||
await act(async () => {
|
||||
checkbox?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
});
|
||||
|
||||
expect(testState.setUnmuteExpandedVideoSoundMock).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('changes the interface language from the language selector', async () => {
|
||||
render(createElement(InterfaceSettings));
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
}
|
||||
|
||||
.setting input[type="checkbox"] {
|
||||
margin-right: 2px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.setting input[type="radio"] {
|
||||
|
||||
@@ -108,7 +108,7 @@ const InterfaceLanguage = () => {
|
||||
|
||||
const InterfaceSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
const { fitExpandedImagesToScreen, setFitExpandedImagesToScreen } = useExpandedMediaStore();
|
||||
const { fitExpandedImagesToScreen, setFitExpandedImagesToScreen, setUnmuteExpandedVideoSound, unmuteExpandedVideoSound } = useExpandedMediaStore();
|
||||
const { enableInfiniteScroll, setEnableInfiniteScroll } = useFeedViewSettingsStore();
|
||||
|
||||
return (
|
||||
@@ -132,6 +132,13 @@ const InterfaceSettings = () => {
|
||||
</label>
|
||||
<div className={styles.settingTip}>{capitalize(t('fit_expanded_images_to_screen_tip'))}</div>
|
||||
</div>
|
||||
<div className={styles.setting}>
|
||||
<label>
|
||||
<input type='checkbox' checked={unmuteExpandedVideoSound} onChange={(e) => setUnmuteExpandedVideoSound(e.target.checked)} />
|
||||
{capitalize(t('unmute_video_sound'))}
|
||||
</label>
|
||||
<div className={styles.settingTip}>{capitalize(t('unmute_video_sound_tip'))}</div>
|
||||
</div>
|
||||
<div className={styles.setting}>
|
||||
<label>
|
||||
<input type='checkbox' checked={enableInfiniteScroll} onChange={(e) => setEnableInfiniteScroll(e.target.checked)} />
|
||||
|
||||
@@ -32,15 +32,19 @@ describe('ui state stores', () => {
|
||||
expect(store.getState().selectedText).toBe('');
|
||||
});
|
||||
|
||||
it('useExpandedMediaStore persists fitExpandedImagesToScreen', async () => {
|
||||
it('useExpandedMediaStore persists expanded media preferences', async () => {
|
||||
const store = (await import('../use-expanded-media-store')).default;
|
||||
|
||||
expect(store.getState().fitExpandedImagesToScreen).toBe(false);
|
||||
expect(store.getState().unmuteExpandedVideoSound).toBe(false);
|
||||
|
||||
store.getState().setFitExpandedImagesToScreen(true);
|
||||
store.getState().setUnmuteExpandedVideoSound(true);
|
||||
|
||||
expect(store.getState().fitExpandedImagesToScreen).toBe(true);
|
||||
expect(store.getState().unmuteExpandedVideoSound).toBe(true);
|
||||
expect(localStorage.getItem('expanded-media-store')).toContain('fitExpandedImagesToScreen');
|
||||
expect(localStorage.getItem('expanded-media-store')).toContain('unmuteExpandedVideoSound');
|
||||
});
|
||||
|
||||
it('useSubplebbitOfflineStore merges updates and clears initialLoad after the timeout', async () => {
|
||||
|
||||
@@ -3,14 +3,18 @@ import { persist } from 'zustand/middleware';
|
||||
|
||||
interface ExpandedMediaState {
|
||||
fitExpandedImagesToScreen: boolean;
|
||||
unmuteExpandedVideoSound: boolean;
|
||||
setFitExpandedImagesToScreen: (fit: boolean) => void;
|
||||
setUnmuteExpandedVideoSound: (unmute: boolean) => void;
|
||||
}
|
||||
|
||||
const useExpandedMediaStore = create<ExpandedMediaState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
fitExpandedImagesToScreen: false,
|
||||
unmuteExpandedVideoSound: false,
|
||||
setFitExpandedImagesToScreen: (fit) => set({ fitExpandedImagesToScreen: fit }),
|
||||
setUnmuteExpandedVideoSound: (unmute) => set({ unmuteExpandedVideoSound: unmute }),
|
||||
}),
|
||||
{
|
||||
name: 'expanded-media-store',
|
||||
|
||||
Reference in New Issue
Block a user