diff --git a/client/src/App.tsx b/client/src/App.tsx index 00ffb55..e996972 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,5 +1,5 @@ import React, {ReactElement} from 'react'; -import {BrowserRouter as Router, Switch} from 'react-router-dom'; +import {Switch} from 'react-router-dom'; import './App.css'; import './DarkMode.css'; import Groups from './pages/groups'; @@ -8,21 +8,21 @@ import useSubscription from '@logux/redux/use-subscription'; import {GroupsLoad} from '@doko/common'; import HandleInvitation from './pages/HandleInvitation'; import Simulation from './pages/Simulation'; +import {useFollowLatestGame} from './store/Ui'; export default function App(): ReactElement | null { useSubscription(['groups/load']); + useFollowLatestGame(); - return - - - - - - - - - - - - ; + return + + + + + + + + + + ; } diff --git a/client/src/DarkMode.css b/client/src/DarkMode.css index ed41896..94dff64 100644 --- a/client/src/DarkMode.css +++ b/client/src/DarkMode.css @@ -85,6 +85,10 @@ body, color: var(--std-color); } +.ui.inverted.icon.menu .item { + --std-color: white; +} + .ui.button, .ui.dropdown .menu > .item, .ui.dropdown .menu .selected.item, diff --git a/client/src/PageMenu.tsx b/client/src/PageMenu.tsx index cf66a46..05c4c80 100644 --- a/client/src/PageMenu.tsx +++ b/client/src/PageMenu.tsx @@ -1,10 +1,12 @@ import React, {ReactElement, useCallback} from 'react'; -import {Icon, Menu} from 'semantic-ui-react'; +import {Checkbox, Icon, Menu} from 'semantic-ui-react'; import {asLink} from './AsLink'; import {SemanticICONS} from 'semantic-ui-react/dist/commonjs/generic'; import {usePageContext} from './Page'; import {useLatestGroupGame} from './store/Games'; import {useLatestGroupRound} from './store/Rounds'; +import {useSelector} from 'react-redux'; +import {followLastGameSelector, useSetUi} from './store/Ui'; export interface PageMenuItemConfig { route: string; @@ -21,6 +23,17 @@ function isConfig(item: PageMenuItemConfig | PageMenuItemComp): item is PageMenu return item.hasOwnProperty('route'); } +function FollowLastGameMenuItem(): ReactElement { + const checked = useSelector(followLastGameSelector); + const setUi = useSetUi(); + return + + setUi({followLastGame: checked})}/> + ; +} + export default function PageMenu({closeMenu}: { closeMenu: () => void }): ReactElement | null { const {groupId, menuItems} = usePageContext<{ groupId?: string }>(); const lastRound = useLatestGroupRound(); @@ -59,6 +72,8 @@ export default function PageMenu({closeMenu}: { closeMenu: () => void }): ReactE } } + + {menuItems.map((MenuItem, idx) => { if (isConfig(MenuItem)) { return diff --git a/client/src/index.tsx b/client/src/index.tsx index 1a92506..5a47189 100644 --- a/client/src/index.tsx +++ b/client/src/index.tsx @@ -16,6 +16,7 @@ import badgeMessages from '@logux/client/badge/en'; import log from '@logux/client/log'; import {storeReducer} from './store/Store'; import {getAuth} from './Auth'; +import {BrowserRouter as Router} from 'react-router-dom'; const isDev = process.env.NODE_ENV === 'development'; const wsProto = window.location.protocol === 'https:' ? 'wss' : 'ws'; @@ -35,7 +36,11 @@ if (isDev) { store.client.start(); ReactDOM.render( - , + + + + + , document.getElementById('root'), ); diff --git a/client/src/store/Rounds.ts b/client/src/store/Rounds.ts index 7b4bc34..71925d8 100644 --- a/client/src/store/Rounds.ts +++ b/client/src/store/Rounds.ts @@ -19,6 +19,8 @@ import useSubscription from '@logux/redux/use-subscription'; import {usePageContext} from '../Page'; import {LoguxDispatch} from './Logux'; import {groupsSelector} from './Groups'; +import {createSelector} from 'reselect'; +import {memoize} from 'lodash'; const {addReducer, combinedReducer} = createReducer({}, 'rounds'); @@ -63,6 +65,18 @@ export const roundsReducer = combinedReducer; export const roundsSelector = (state: State) => state.rounds; +export const getRoundByIdSelector = createSelector( + roundsSelector, + (rounds) => memoize((id: string): Round | null => { + for (const round of Object.values(rounds)) { + if (round[id]) { + return round[id]; + } + } + return null; + }), +); + export function useLoadRounds() { const {groupId} = usePageContext<{ groupId: string }>(); const group = useSelector(groupsSelector)[groupId]; diff --git a/client/src/store/Ui.ts b/client/src/store/Ui.ts index a6ccacd..5d9826d 100644 --- a/client/src/store/Ui.ts +++ b/client/src/store/Ui.ts @@ -1,19 +1,25 @@ import { DeepPartial, + GamesAdd, GroupMembersInvitationAccepted, GroupMembersInvitationRejected, GroupMembersInvitationUsed, mergeStates, SubType, } from '@doko/common'; -import {createReducer} from './Reducer'; +import {createReducer, isAction} from './Reducer'; import {State} from './Store'; -import {useDispatch} from 'react-redux'; +import {useDispatch, useSelector, useStore} from 'react-redux'; import {LoguxDispatch} from './Logux'; -import {useCallback} from 'react'; +import {useCallback, useEffect} from 'react'; +import * as LocalStorage from '../LocalStorage'; +import {useHistory} from 'react-router-dom'; +import {getRoundByIdSelector} from './Rounds'; +import Log from '@logux/core/log'; export interface Ui { acceptedInvitations: { [token: string]: string }, //token => groupId for the invitee + followLastGame: boolean; rejectedInvitations: string[]; //for the invitee usedInvitationTokens: string[]; //for the inviter statistics: { @@ -30,6 +36,7 @@ export interface UiSet { const initial: Ui = { acceptedInvitations: {}, + followLastGame: false, rejectedInvitations: [], usedInvitationTokens: [], statistics: { @@ -52,9 +59,30 @@ function addUniqueToken(key: keyof SubType) { }; } -const {addReducer, combinedReducer} = createReducer(initial); +const localStorageSync: Array = ['followLastGame']; -addReducer('ui/set', (state, action) => mergeStates(state, action.ui)); +function getInitialState(): Ui { + const state = {...initial}; + localStorageSync.forEach((key) => { + const parsed = LocalStorage.get(`ui.${key}`); + if (parsed !== null) { + state[key] = parsed; + } + }); + return state; +} + +const {addReducer, combinedReducer} = createReducer(getInitialState()); + +addReducer('ui/set', (state, action) => { + const newState = mergeStates(state, action.ui); + localStorageSync.forEach((key) => { + if (state[key] !== newState[key]) { + LocalStorage.set(`ui.${key}`, newState[key]); + } + }); + return newState; +}); addReducer('groupMembers/invitationAccepted', (state, {groupId, token}) => mergeStates(state, {acceptedInvitations: {[token]: groupId}})); @@ -66,6 +94,7 @@ addReducer('groupMembers/invitationUsed', addUniqueT export const uiReducer = combinedReducer; export const acceptedInvitationsSelector = (state: State) => state.ui.acceptedInvitations; +export const followLastGameSelector = (state: State) => state.ui.followLastGame; export const rejectedInvitationsSelector = (state: State) => state.ui.rejectedInvitations; export const usedInvitationTokensSelector = (state: State) => state.ui.usedInvitationTokens; export const statisticsSelector = (state: State) => state.ui.statistics; @@ -76,3 +105,23 @@ export function useSetUi() { dispatch({type: 'ui/set', ui} as UiSet); }, [dispatch]); } + +export function useFollowLatestGame() { + const getRoundById = useSelector(getRoundByIdSelector); + const followLastGame = useSelector(followLastGameSelector); + const history = useHistory(); + const store = useStore() as unknown as { log: Log }; + useEffect(() => { + if (followLastGame) { + return store.log.on('add', (action) => { + if (isAction(action, 'games/add')) { + const {game} = action; + const round = getRoundById(game.roundId); + if (round) { + history.push(`/group/${round.groupId}/rounds/round/${game.roundId}/games/game/${game.id}`); + } + } + }); + } + }, [followLastGame, history, store, getRoundById]); +} diff --git a/common/@types/logux.d.ts b/common/@types/logux.d.ts index 5cbc37c..b4e635c 100644 --- a/common/@types/logux.d.ts +++ b/common/@types/logux.d.ts @@ -37,7 +37,7 @@ declare module '@logux/core/log' { lastTime: number; sequence: number; - on(event: 'preadd' | 'add' | 'clean', listener: () => void): () => void; + on(event: 'preadd' | 'add' | 'clean', listener: (action: A) => void): () => void; add(action: A, meta: AddMeta): () => void;