Skip to content

Commit

Permalink
Support auto-leave (#2989)
Browse files Browse the repository at this point in the history
* Implement auto-leave

* Support disabling auto-leave

* Remove unused props from WaitlistButton

* expected properties

* Get auto-leave state from initState (u-wave/core#642)
  • Loading branch information
goto-bus-stop authored Sep 28, 2024
1 parent c3ad716 commit 59cf76f
Show file tree
Hide file tree
Showing 9 changed files with 151 additions and 59 deletions.
1 change: 1 addition & 0 deletions locale/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
28 changes: 2 additions & 26 deletions src/components/FooterBar/UserFooterContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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(() => {
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -135,12 +116,7 @@ function UserFooterContent({ user: currentUser }: UserFooterContentProps) {
</div>
)}
<div className="FooterBar-join">
<WaitlistButton
isLocked={waitlistIsLocked}
userIsDJ={userIsDJ}
userInWaitlist={userInWaitlist}
onClick={userInWaitlist ? handleLeaveWaitlist : handleJoinWaitlist}
/>
<WaitlistButton />
</div>
</>
);
Expand Down
8 changes: 7 additions & 1 deletion src/components/FooterBar/WaitlistButton.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
width: 125px;
background: var(--highlight-color);
color: var(--text-color);
cursor: pointer;
font-size: 11pt;
text-transform: uppercase;

Expand All @@ -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;
Expand Down
132 changes: 105 additions & 27 deletions src/components/FooterBar/WaitlistButton.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>(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) {
Expand All @@ -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 (
<>
<ButtonGroup ref={anchorRef} style={{ height: '100%' }}>
<Button
classes={{ root: 'WaitlistButton' }}
onClick={handleSkipRemove.execute}
disabled={handleSkipRemove.loading}
>
{t('waitlist.leaveBooth')}
</Button>
<Button
classes={{ root: 'WaitlistButton--split' }}
onClick={() => setOpen((v) => !v)}
>
<SvgIcon path={mdiMenuUp} />
</Button>
</ButtonGroup>
<Popover
anchorEl={anchorRef.current}
open={open}
anchorOrigin={{
vertical: 'top',
horizontal: 'right',
}}
transformOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
onClose={() => setOpen(false)}
>
<MenuList>
<MenuItem onClick={handleAutoLeave.execute}>
<ListItemIcon>
{autoLeave ? <SvgIcon path={mdiCheck} /> : null}
</ListItemIcon>
{t('waitlist.autoLeave')}
</MenuItem>
</MenuList>
</Popover>
</>
);
}

if (isInWaitlist) {
return (
<Button
classes={{
root: 'WaitlistButton',
disabled: 'WaitlistButton--locked',
}}
onClick={handleLeave.execute}
disabled={handleLeave.loading}
>
{icon}
{isLocked ? ' ' : null}
{t('waitlist.leave')}
</Button>
);
}

return (
Expand All @@ -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')}
</Button>
);
}

export default memo(WaitlistButton);
export default WaitlistButton;
4 changes: 2 additions & 2 deletions src/mobile/containers/UsersDrawer.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
leaveWaitlist,
waitlistUsersSelector,
userInWaitlistSelector,
isLockedSelector,
waitlistIsLockedSelector,
} from '../../reducers/waitlist';
import { isLoggedInSelector } from '../../reducers/auth';
import { djSelector } from '../../reducers/booth';
Expand All @@ -21,7 +21,7 @@ const mapStateToProps = createStructuredSelector({
open: usersDrawerIsOpenSelector,
userIsLoggedIn: isLoggedInSelector,
userInWaitlist: userInWaitlistSelector,
isLockedWaitlist: isLockedSelector,
isLockedWaitlist: waitlistIsLockedSelector,
});

const mapDispatchToProps = {
Expand Down
3 changes: 3 additions & 0 deletions src/reducers/__tests__/booth.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -32,6 +33,7 @@ describe('reducers/booth', () => {
timestamp: 1449767164107,
}));
expect(state).toEqual({
autoLeave: null,
historyID: 'someRandomID',
djID: 'seventeen',
media: { artist: 'about tess', title: 'Imaginedit' },
Expand All @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/reducers/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
29 changes: 28 additions & 1 deletion src/reducers/booth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -38,6 +38,7 @@ interface PlayingState {
media: Media,
startTime: number,
stats: { upvotes: string[], downvotes: string[], favorites: string[] },
autoLeave?: boolean | null,
}

interface EmptyState {
Expand All @@ -46,6 +47,7 @@ interface EmptyState {
media: null,
startTime: null,
stats: null,
autoLeave?: null,
}

type State = (PlayingState | EmptyState) & {
Expand All @@ -58,6 +60,7 @@ const initialState = {
djID: null,
startTime: null,
stats: null,
autoLeave: null,
isFullscreen: false,
} as State;

Expand Down Expand Up @@ -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 } }]);
});
Expand Down Expand Up @@ -148,6 +166,7 @@ const slice = createSlice({
downvotes: [],
favorites: [],
},
autoLeave: null,
};
}
return {
Expand All @@ -157,6 +176,7 @@ const slice = createSlice({
djID: null,
startTime: null,
stats: null,
autoLeave: null,
};
},
prepare(payload: AdvancePayload | null, previous: PreviousBooth | null = null) {
Expand Down Expand Up @@ -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: {
Expand Down
Loading

0 comments on commit 59cf76f

Please sign in to comment.