diff --git a/locale/en.yaml b/locale/en.yaml
index 8db687f4a..da1b6aafb 100644
--- a/locale/en.yaml
+++ b/locale/en.yaml
@@ -132,6 +132,7 @@ uwave:
join: Join Waitlist
leave: Leave Waitlist
leaveBooth: Stop playing
+ autoLeave: Stop playing after this
add: Add to waitlist
remove: Remove from waitlist
diff --git a/src/components/FooterBar/UserFooterContent.tsx b/src/components/FooterBar/UserFooterContent.tsx
index afba915bd..1c0c51839 100644
--- a/src/components/FooterBar/UserFooterContent.tsx
+++ b/src/components/FooterBar/UserFooterContent.tsx
@@ -16,13 +16,7 @@ import {
canSkipSelector,
} from '../../reducers/booth';
import { activePlaylistSelector, nextMediaSelector } from '../../reducers/playlists';
-import {
- joinWaitlist,
- leaveWaitlist,
- baseEtaSelector,
- userInWaitlistSelector,
- isLockedSelector,
-} from '../../reducers/waitlist';
+import { baseEtaSelector, userInWaitlistSelector } from '../../reducers/waitlist';
import NextMedia from './NextMedia';
import UserInfo from './UserInfo';
import SkipButton from './SkipButton';
@@ -43,7 +37,6 @@ function UserFooterContent({ user: currentUser }: UserFooterContentProps) {
const currentDJ = useSelector(djSelector);
const historyID = useSelector(historyIDSelector);
const showSkip = useSelector(canSkipSelector);
- const waitlistIsLocked = useSelector(isLockedSelector);
const voteStats = useSelector(currentVoteStatsSelector);
const dispatch = useDispatch();
const handleTogglePlaylistManager = useCallback(() => {
@@ -75,18 +68,6 @@ function UserFooterContent({ user: currentUser }: UserFooterContentProps) {
return dispatch(skipSelf({ remove: false }));
}, [userIsDJ, dispatch]);
- const handleJoinWaitlist = useCallback(() => {
- dispatch(joinWaitlist()).catch(() => {
- // error is already reported
- });
- }, [dispatch]);
- const handleLeaveWaitlist = useCallback(() => {
- if (userIsDJ) {
- return dispatch(skipSelf({ remove: true }));
- }
- return dispatch(leaveWaitlist());
- }, [userIsDJ, dispatch]);
-
const canVote = !userIsDJ && !!currentDJ;
return (
@@ -135,12 +116,7 @@ function UserFooterContent({ user: currentUser }: UserFooterContentProps) {
)}
-
+
>
);
diff --git a/src/components/FooterBar/WaitlistButton.css b/src/components/FooterBar/WaitlistButton.css
index d6722b210..164a5107d 100644
--- a/src/components/FooterBar/WaitlistButton.css
+++ b/src/components/FooterBar/WaitlistButton.css
@@ -7,7 +7,6 @@
width: 125px;
background: var(--highlight-color);
color: var(--text-color);
- cursor: pointer;
font-size: 11pt;
text-transform: uppercase;
@@ -20,6 +19,13 @@
background: var(--highbright-color);
}
}
+.WaitlistButton--split {
+ background: var(--highlight-color);
+ color: var(--text-color);
+ &:hover {
+ background: var(--highbright-color);
+ }
+}
.WaitlistButton--locked {
width: 140px;
diff --git a/src/components/FooterBar/WaitlistButton.tsx b/src/components/FooterBar/WaitlistButton.tsx
index 955adddfb..a8cdfb867 100644
--- a/src/components/FooterBar/WaitlistButton.tsx
+++ b/src/components/FooterBar/WaitlistButton.tsx
@@ -1,23 +1,49 @@
import cx from 'clsx';
-import { memo } from 'react';
+import { useRef, useState } from 'react';
+import { useAsyncCallback } from 'react-async-hook';
import { useTranslator } from '@u-wave/react-translate';
+import Popover from '@mui/material/Popover';
+import MenuList from '@mui/material/MenuList';
+import MenuItem from '@mui/material/MenuItem';
+import ListItemIcon from '@mui/material/ListItemIcon';
import Button from '@mui/material/Button';
-import { mdiLock } from '@mdi/js';
+import ButtonGroup from '@mui/material/ButtonGroup';
+import { mdiCheck, mdiLock, mdiMenuUp } from '@mdi/js';
import SvgIcon from '../SvgIcon';
+import { useDispatch, useSelector } from '../../hooks/useRedux';
+import {
+ joinWaitlist,
+ leaveWaitlist,
+ userInWaitlistSelector,
+ waitlistIsLockedSelector,
+} from '../../reducers/waitlist';
+import { isCurrentDJSelector, setAutoLeave, skipSelf } from '../../reducers/booth';
-type WaitlistButtonProps = {
- userIsDJ: boolean,
- userInWaitlist: boolean,
- isLocked: boolean,
- onClick: () => void,
-};
-function WaitlistButton({
- userIsDJ,
- userInWaitlist,
- isLocked,
- onClick,
-}: WaitlistButtonProps) {
+function WaitlistButton() {
const { t } = useTranslator();
+ const isDJ = useSelector(isCurrentDJSelector);
+ const isInWaitlist = useSelector(userInWaitlistSelector);
+ const isLocked = useSelector(waitlistIsLockedSelector);
+ const autoLeave = useSelector((state) => state.booth.autoLeave ?? false);
+ const dispatch = useDispatch();
+ const anchorRef = useRef(null);
+ const [open, setOpen] = useState(false);
+
+ const handleSkipRemove = useAsyncCallback(async () => {
+ await dispatch(skipSelf({ remove: true }));
+ });
+
+ const handleAutoLeave = useAsyncCallback(async () => {
+ await dispatch(setAutoLeave({ autoLeave: !autoLeave }));
+ });
+
+ const handleLeave = useAsyncCallback(async () => {
+ await dispatch(leaveWaitlist());
+ });
+
+ const handleJoin = useAsyncCallback(async () => {
+ await dispatch(joinWaitlist());
+ });
let icon;
if (isLocked) {
@@ -28,19 +54,71 @@ function WaitlistButton({
'WaitlistButton-icon',
// The user can still leave the waitlist, if it's locked,
// but cannot join the waitlist.
- !userInWaitlist && 'WaitlistButton-icon--locked',
+ !isInWaitlist && 'WaitlistButton-icon--locked',
)}
/>
);
}
- let label;
- if (userIsDJ) {
- label = t('waitlist.leaveBooth');
- } else if (userInWaitlist) {
- label = t('waitlist.leave');
- } else {
- label = t('waitlist.join');
+ if (isDJ) {
+ return (
+ <>
+
+
+
+
+ setOpen(false)}
+ >
+
+
+
+
+ >
+ );
+ }
+
+ if (isInWaitlist) {
+ return (
+
+ );
}
return (
@@ -49,14 +127,14 @@ function WaitlistButton({
root: 'WaitlistButton',
disabled: 'WaitlistButton--locked',
}}
- disabled={isLocked && !userInWaitlist}
- onClick={() => onClick()}
+ disabled={isLocked || handleJoin.loading}
+ onClick={handleJoin.execute}
>
{icon}
- {isLocked && ' '}
- {label}
+ {isLocked ? ' ' : null}
+ {t('waitlist.join')}
);
}
-export default memo(WaitlistButton);
+export default WaitlistButton;
diff --git a/src/mobile/containers/UsersDrawer.js b/src/mobile/containers/UsersDrawer.js
index 28eb203da..d671de7bf 100644
--- a/src/mobile/containers/UsersDrawer.js
+++ b/src/mobile/containers/UsersDrawer.js
@@ -5,7 +5,7 @@ import {
leaveWaitlist,
waitlistUsersSelector,
userInWaitlistSelector,
- isLockedSelector,
+ waitlistIsLockedSelector,
} from '../../reducers/waitlist';
import { isLoggedInSelector } from '../../reducers/auth';
import { djSelector } from '../../reducers/booth';
@@ -21,7 +21,7 @@ const mapStateToProps = createStructuredSelector({
open: usersDrawerIsOpenSelector,
userIsLoggedIn: isLoggedInSelector,
userInWaitlist: userInWaitlistSelector,
- isLockedWaitlist: isLockedSelector,
+ isLockedWaitlist: waitlistIsLockedSelector,
});
const mapDispatchToProps = {
diff --git a/src/reducers/__tests__/booth.js b/src/reducers/__tests__/booth.js
index 7673c3687..8be0c06ca 100644
--- a/src/reducers/__tests__/booth.js
+++ b/src/reducers/__tests__/booth.js
@@ -11,6 +11,7 @@ describe('reducers/booth', () => {
it('should default to an empty DJ booth', () => {
const state = booth(undefined, { type: '@@redux/INIT' });
expect(state).toEqual({
+ autoLeave: null,
historyID: null,
djID: null,
media: null,
@@ -32,6 +33,7 @@ describe('reducers/booth', () => {
timestamp: 1449767164107,
}));
expect(state).toEqual({
+ autoLeave: null,
historyID: 'someRandomID',
djID: 'seventeen',
media: { artist: 'about tess', title: 'Imaginedit' },
@@ -48,6 +50,7 @@ describe('reducers/booth', () => {
it('should stop playing if there is no next song', () => {
const state = booth(initialState(), advanceInner(null));
expect(state).toEqual({
+ autoLeave: null,
historyID: null,
djID: null,
media: null,
diff --git a/src/reducers/auth.ts b/src/reducers/auth.ts
index 9ff8bfda6..7a2de7661 100644
--- a/src/reducers/auth.ts
+++ b/src/reducers/auth.ts
@@ -67,6 +67,7 @@ export type InitialStatePayload = {
favorites: string[],
},
} | null,
+ autoLeave?: boolean, // Only on recent u-wave-core
waitlist: string[],
waitlistLocked: boolean,
activePlaylist: string | null,
diff --git a/src/reducers/booth.ts b/src/reducers/booth.ts
index 3f7bf2472..680431757 100644
--- a/src/reducers/booth.ts
+++ b/src/reducers/booth.ts
@@ -11,7 +11,7 @@ import {
currentUserHasRoleSelector,
} from './users';
import { currentTimeSelector } from './server';
-import { initState } from './auth';
+import { currentUserIDSelector, initState } from './auth';
import uwFetch from '../utils/fetch';
import { createAsyncThunk, type Thunk } from '../redux/api';
import type { StoreState } from '../redux/configureStore';
@@ -38,6 +38,7 @@ interface PlayingState {
media: Media,
startTime: number,
stats: { upvotes: string[], downvotes: string[], favorites: string[] },
+ autoLeave?: boolean | null,
}
interface EmptyState {
@@ -46,6 +47,7 @@ interface EmptyState {
media: null,
startTime: null,
stats: null,
+ autoLeave?: null,
}
type State = (PlayingState | EmptyState) & {
@@ -58,6 +60,7 @@ const initialState = {
djID: null,
startTime: null,
stats: null,
+ autoLeave: null,
isFullscreen: false,
} as State;
@@ -94,6 +97,21 @@ export const skipCurrentDJ = createAsyncThunk('booth/skip', async (options: { re
}]);
});
+// This action doesn't need further handling because it will cause updates
+// over WebSocket on the server.
+export const setAutoLeave = createAsyncThunk('booth/setAutoLeave', async (
+ options: { autoLeave: boolean },
+ api,
+) => {
+ const userID = currentUserIDSelector(api.getState());
+ const { data } = await uwFetch<{ data: { autoLeave: boolean } }>(['/booth/leave', {
+ method: 'put',
+ data: { userID, autoLeave: options.autoLeave },
+ }]);
+
+ return data;
+});
+
export const upvote = createAsyncThunk('booth/upvote', async ({ historyID }: { historyID: string }) => {
await uwFetch([`/booth/${historyID}/vote`, { method: 'put', data: { direction: 1 } }]);
});
@@ -148,6 +166,7 @@ const slice = createSlice({
downvotes: [],
favorites: [],
},
+ autoLeave: null,
};
}
return {
@@ -157,6 +176,7 @@ const slice = createSlice({
djID: null,
startTime: null,
stats: null,
+ autoLeave: null,
};
},
prepare(payload: AdvancePayload | null, previous: PreviousBooth | null = null) {
@@ -237,6 +257,13 @@ const slice = createSlice({
stats: null,
});
}
+
+ state.autoLeave = payload.autoLeave ?? false;
+ });
+ builder.addCase(setAutoLeave.fulfilled, (state, { payload }) => {
+ if (state.djID != null) {
+ state.autoLeave = payload.autoLeave;
+ }
});
},
selectors: {
diff --git a/src/reducers/waitlist.ts b/src/reducers/waitlist.ts
index 4987d5310..2cccb0b5d 100644
--- a/src/reducers/waitlist.ts
+++ b/src/reducers/waitlist.ts
@@ -118,7 +118,7 @@ export const {
waitlistUpdated,
} = slice.actions;
-export function isLockedSelector(state: StoreState) {
+export function waitlistIsLockedSelector(state: StoreState) {
return state.waitlist.locked;
}
@@ -158,7 +158,7 @@ export function userInWaitlistSelector(state: StoreState) {
}
export const waitlistSelector = createStructuredSelector({
- locked: isLockedSelector,
+ locked: waitlistIsLockedSelector,
users: waitlistUsersSelector,
});