Skip to content

Commit

Permalink
feat(spa): initial in game state
Browse files Browse the repository at this point in the history
  • Loading branch information
uonr committed May 4, 2024
1 parent f09d135 commit 1a54c69
Show file tree
Hide file tree
Showing 11 changed files with 95 additions and 63 deletions.
5 changes: 4 additions & 1 deletion apps/spa/components/compose/Compose.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ export const Compose = ({ member, channelAtoms }: Props) => {
return <EditMessageBanner currentUser={member.user} />;
}, [isEditing, member.user]);
const fileButton = useMemo(() => <FileButton />, []);
const inGameSwitchButton = useMemo(() => <InGameSwitchButton />, []);
const inGameSwitchButton = useMemo(
() => <InGameSwitchButton channelId={member.channel.channelId} />,
[member.channel.channelId],
);
const addDiceButton = useMemo(() => <AddDiceButton />, []);
const sendButton = useMemo(
() => <SendButton send={send} currentUser={member.user} isEditing={isEditing} />,
Expand Down
21 changes: 18 additions & 3 deletions apps/spa/components/compose/InGameSwitchButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,32 @@ import { FC } from 'react';
import { useIntl } from 'react-intl';
import { useChannelAtoms } from '../../hooks/useChannelAtoms';
import { InComposeButton } from './InComposeButton';
import { useQueryChannel } from '../../hooks/useQueryChannel';

interface Props {}
interface Props {
channelId: string;
}

export const InGameSwitchButton: FC<Props> = () => {
export const InGameSwitchButton: FC<Props> = ({ channelId }) => {
const { data: channel } = useQueryChannel(channelId);
const { inGameAtom, composeAtom } = useChannelAtoms();
const intl = useIntl();
const inGame = useAtomValue(inGameAtom);
const dispatch = useSetAtom(composeAtom);
const title = intl.formatMessage({ defaultMessage: 'Toggle In Game' });
return (
<InComposeButton pressed={inGame} onClick={() => dispatch({ type: 'toggleInGame', payload: {} })} title={title}>
<InComposeButton
pressed={inGame}
onClick={() =>
dispatch({
type: 'toggleInGame',
payload: {
defaultInGame: channel?.type === 'IN_GAME',
},
})
}
title={title}
>
<Mask className={inGame ? '' : 'text-text-lighter'} />
</InComposeButton>
);
Expand Down
19 changes: 15 additions & 4 deletions apps/spa/components/compose/useSendPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,14 @@ const sendPreview = (
parsed: ParseResult,
connection: WebSocket,
sendTimeoutRef: MutableRefObject<number | undefined>,
defaultInGame: boolean,
): void => {
window.clearTimeout(sendTimeoutRef.current);

sendTimeoutRef.current = window.setTimeout(() => {
const { defaultInGame: composeInGame, previewId, inputedName, editFor } = compose;
const { previewId, inputedName, editFor } = compose;
const { isAction, broadcast, whisperToUsernames, inGame: parsedInGame } = parsed;
const inGame = parsedInGame ?? composeInGame;
const inGame = parsedInGame ?? defaultInGame;
const inGameName = inputedName || characterName;
if (!previewId) {
return;
Expand Down Expand Up @@ -59,6 +60,7 @@ export const useSendPreview = (
characterName: string,
composeAtom: ComposeAtom,
parsedAtom: Atom<ParseResult>,
defaultInGame: boolean,
) => {
const store = useStore();
const sendTimoutRef = useRef<number | undefined>(undefined);
Expand All @@ -74,7 +76,16 @@ export const useSendPreview = (
}
const composeState = store.get(composeAtom);
const parsed = store.get(parsedAtom);
sendPreview(channelId, nickname, characterName, composeState, parsed, connectionState.connection, sendTimoutRef);
sendPreview(
channelId,
nickname,
characterName,
composeState,
parsed,
connectionState.connection,
sendTimoutRef,
defaultInGame,
);
});
}, [channelId, characterName, composeAtom, connectionState, isFocused, nickname, parsedAtom, store]);
}, [channelId, characterName, composeAtom, connectionState, defaultInGame, isFocused, nickname, parsedAtom, store]);
};
4 changes: 3 additions & 1 deletion apps/spa/components/pane-channel/ChannelPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,17 @@ const SecretChannelInfo: FC<{ className?: string }> = ({ className }) => {
export const ChatPaneChannel: FC<Props> = memo(({ channelId }) => {
const { data: currentUser } = useQueryUser();
const member = useMyChannelMember(channelId);
const atoms: ChannelAtoms = useMakeChannelAtoms(channelId, member.isOk ? member.some.channel : null);
const nickname = currentUser?.nickname ?? undefined;
const { data: channel, isLoading, error } = useQueryChannel(channelId);
const defaultInGame = channel?.type === 'IN_GAME';
const atoms: ChannelAtoms = useMakeChannelAtoms(channelId, member.isOk ? member.some.channel : null, defaultInGame);
useSendPreview(
channelId,
nickname,
member.isOk ? member.some.channel.characterName : '',
atoms.composeAtom,
atoms.parsedAtom,
defaultInGame,
);
const memberListState = useAtomValue(atoms.memberListStateAtom);
let errorNode = null;
Expand Down
4 changes: 2 additions & 2 deletions apps/spa/components/pane-channel/NameEditContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,10 @@ export const NameEditContent: FC<Props> = ({ member }) => {
inGame: baseId + 'in-game',
};
const switchToInGame = () => {
dispatch({ type: 'toggleInGame', payload: { inGame: true } });
dispatch({ type: 'setInGame', payload: { inGame: true } });
};
const switchToOutOfGame = () => {
dispatch({ type: 'toggleInGame', payload: { inGame: false } });
dispatch({ type: 'setInGame', payload: { inGame: false } });
};
return (
<div className="grid w-52 grid-cols-[auto_auto] gap-x-1 gap-y-2">
Expand Down
2 changes: 1 addition & 1 deletion apps/spa/components/pane-channel/SelfPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ type ComposeDrived = Pick<ComposeState, 'media'> & {
const isEqual = (a: ComposeDrived, b: ComposeDrived) =>
a.editMode === b.editMode && a.name === b.name && a.media === b.media;

const selector = ({ defaultInGame: inGame, inputedName, source, editFor, media }: ComposeState): ComposeDrived => {
const selector = ({ inputedName, source, editFor, media }: ComposeState): ComposeDrived => {
const editMode = editFor !== null;
return { name: inputedName.trim(), editMode, media };
};
Expand Down
6 changes: 5 additions & 1 deletion apps/spa/components/pane-channel/useSend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@ import { useQueryChannelMembers } from '../../hooks/useQueryChannelMembers';
import { parse } from '../../interpreter/parser';
import { upload } from '../../media';
import { ComposeActionUnion } from '../../state/compose.actions';
import { useQueryChannel } from '../../hooks/useQueryChannel';

export const useSend = (me: User) => {
const channelId = useChannelId();
const { data: channel } = useQueryChannel(channelId);
const defaultInGame = channel?.type === 'IN_GAME';
const { composeAtom, parsedAtom, checkComposeAtom } = useChannelAtoms();
const store = useStore();
const setBanner = useSetBanner();
Expand Down Expand Up @@ -56,7 +59,7 @@ export const useSend = (me: User) => {
const { text, entities, whisperToUsernames } = parse(compose.source);
let result: Result<Message, ApiError>;
let name = nickname;
const inGame = parsed.inGame ?? compose.defaultInGame;
const inGame = parsed.inGame ?? defaultInGame;
if (inGame) {
const inputedName = compose.inputedName.trim();
if (inputedName === '') {
Expand Down Expand Up @@ -131,6 +134,7 @@ export const useSend = (me: User) => {
channelId,
checkComposeAtom,
composeAtom,
defaultInGame,
myMember,
nickname,
parsedAtom,
Expand Down
13 changes: 8 additions & 5 deletions apps/spa/hooks/useChannelAtoms.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,15 @@ export interface ChannelAtoms {

export const ChannelAtomsContext = createContext<ChannelAtoms | null>(null);

export const useMakeChannelAtoms = (channelId: string, member: ChannelMember | null): ChannelAtoms => {
export const useMakeChannelAtoms = (
channelId: string,
member: ChannelMember | null,
defaultInGame: boolean,
): ChannelAtoms => {
const composeAtom = useMemo(() => atomWithReducer(makeInitialComposeState(), composeReducer), []);
const checkComposeAtom: Atom<ComposeError | null> = useMemo(
() => selectAtom(composeAtom, checkCompose(member?.characterName ?? '')),
[composeAtom, member?.characterName],
() => selectAtom(composeAtom, checkCompose(member?.characterName ?? '', defaultInGame)),
[composeAtom, defaultInGame, member?.characterName],
);
return useMemo(() => {
const loadableParsedAtom = loadable(
Expand All @@ -62,7 +66,6 @@ export const useMakeChannelAtoms = (channelId: string, member: ChannelMember | n
const isWhisperAtom = selectAtom(parsedAtom, ({ whisperToUsernames }) => whisperToUsernames !== null);
const inGameAtom = atom((read) => {
const { inGame } = read(parsedAtom);
const { defaultInGame } = read(composeAtom);
if (inGame == null) {
return defaultInGame;
} else {
Expand All @@ -83,7 +86,7 @@ export const useMakeChannelAtoms = (channelId: string, member: ChannelMember | n
showArchivedAtom: atomWithStorage(`${channelId}:show-archived`, false),
memberListStateAtom: atom<ChannelMemberListState>('CLOSED'),
};
}, [channelId, checkComposeAtom, composeAtom]);
}, [channelId, checkComposeAtom, composeAtom, defaultInGame]);
};

export const useChannelAtoms = (): ChannelAtoms => {
Expand Down
11 changes: 4 additions & 7 deletions apps/spa/hooks/useChatList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,10 @@ export const START_INDEX = 100000000;

type ComposeSlice = Pick<ComposeState, 'previewId' | 'editFor'> & {
prevPreviewId: string | null;
inGame: boolean;
};

const selectComposeSlice = (
{ previewId, editFor, defaultInGame }: ComposeState,
{ previewId, editFor }: ComposeState,
prevSlice: ComposeSlice | null | undefined,
): ComposeSlice => {
let prevPreviewId: string | null = null;
Expand All @@ -43,11 +42,10 @@ const selectComposeSlice = (
}
}

return { previewId, editFor, prevPreviewId, inGame: defaultInGame };
return { previewId, editFor, prevPreviewId };
};

const isComposeSliceEq = (a: ComposeSlice, b: ComposeSlice) =>
a.previewId === b.previewId && a.inGame === b.inGame && a.editFor === b.editFor;
const isComposeSliceEq = (a: ComposeSlice, b: ComposeSlice) => a.previewId === b.previewId && a.editFor === b.editFor;

const filter = (type: ChannelFilter, item: ChatItem) => {
if (type === 'OOC' && item.inGame) return false;
Expand Down Expand Up @@ -174,7 +172,7 @@ export const useChatList = (channelId: string, myId?: string): UseChatListReturn
composeSlice.previewId,
myId,
channelId,
composeSlice.inGame,
true,
composeSlice.editFor,
pos,
posP,
Expand Down Expand Up @@ -228,7 +226,6 @@ export const useChatList = (channelId: string, myId?: string): UseChatListReturn
}, [
channelId,
composeSlice.editFor,
composeSlice.inGame,
composeSlice.prevPreviewId,
composeSlice.previewId,
filterType,
Expand Down
3 changes: 2 additions & 1 deletion apps/spa/state/compose.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import type { ComposeState } from './compose.reducer';
export type ComposeActionMap = {
setSource: { channelId: string; source: string };
setInputedName: { inputedName: string; setInGame?: boolean };
toggleInGame: { inGame?: boolean };
toggleInGame: { defaultInGame: boolean };
setInGame: { inGame: boolean };
toggleBroadcast: Empty;
addDice: Empty;
link: { text: string; href: string };
Expand Down
70 changes: 33 additions & 37 deletions apps/spa/state/compose.reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export interface ComposeState {
editFor: string | null;
inputedName: string;
previewId: string;
defaultInGame: boolean;
source: string;
media: File | string | null;
whisperTo: // Represents whisper to the Game Master
Expand All @@ -30,7 +29,6 @@ export const makeInitialComposeState = (): ComposeState => ({
editFor: null,
inputedName: '',
previewId: makeId(),
defaultInGame: true,
source: DEFAULT_COMPOSE_SOURCE,
media: null,
range: [DEFAULT_COMPOSE_SOURCE.length, DEFAULT_COMPOSE_SOURCE.length],
Expand All @@ -45,42 +43,46 @@ const QUICK_CHECK_REGEX = /[.。](in|out)\b/i;

const handleSetComposeSource = (state: ComposeState, action: ComposeAction<'setSource'>): ComposeState => {
const { source } = action.payload;
let { previewId, defaultInGame } = state;
if (QUICK_CHECK_REGEX.exec(source)) {
const modifiersResult = parseModifiers(source);
if (modifiersResult.inGame) {
// Flip the default in-game state if the source has a explicit in-game modifier
// So that users can flip the state by deleting the modifier
defaultInGame = !modifiersResult.inGame.inGame;
}
}
let { previewId } = state;
if ((source === '' || state.source === '') && state.editFor === null) {
previewId = makeId();
}
return { ...state, source: action.payload.source, previewId, defaultInGame };
return { ...state, source: action.payload.source, previewId };
};

const handleToggleInGame = (state: ComposeState, action: ComposeAction<'toggleInGame'>): ComposeState => {
const { inGame: modifier } = parseModifiers(state.source);
if (action.payload.inGame != null) {
// Do nothing if the payload is the same as the current state
if (!modifier && state.defaultInGame === action.payload.inGame) {
return state;
}
if (modifier !== false && modifier.inGame === action.payload.inGame) {
return state;
}
const handleToggleInGame = (state: ComposeState, { payload }: ComposeAction<'toggleInGame'>): ComposeState => {
const { source } = state;
const { inGame: modifier } = parseModifiers(source);
let nextSource;
if (!modifier) {
const startsWithSpace = source.startsWith(' ');
const command = payload.defaultInGame ? '.out' : '.in';
nextSource = (startsWithSpace ? command : `${command} `) + source;
} else {
const before = source.substring(0, modifier.start);
const after = source.substring(modifier.start + modifier.len);
const command = modifier.inGame ? '.out ' : '.in ';
nextSource = command + (before + after).trimStart();
}
return { ...state, source: nextSource, range: [nextSource.length, nextSource.length] };
};

const handleSetInGame = (state: ComposeState, { payload }: ComposeAction<'setInGame'>): ComposeState => {
const { source } = state;
let nextSource = source;
const { inGame: modifier } = parseModifiers(source);
if (modifier !== false && modifier.inGame === payload.inGame) {
return state;
}
let nextSource;
if (!modifier) {
const startsWithSpace = source.startsWith(' ');
const command = state.defaultInGame ? '.out' : '.in';
const command = payload.inGame ? '.in' : '.out';
nextSource = (startsWithSpace ? command : `${command} `) + source;
} else {
const before = source.substring(0, modifier.start);
const after = source.substring(modifier.start + modifier.len);
nextSource = (modifier.inGame ? '.out ' : '.in ') + (before + after).trimStart();
const command = payload.inGame ? '.in ' : '.out ';
nextSource = command + (before + after).trimStart();
}
return { ...state, source: nextSource, range: [nextSource.length, nextSource.length] };
};
Expand Down Expand Up @@ -143,7 +145,7 @@ const handleSetInputedName = (state: ComposeState, { payload }: ComposeAction<'s
const inputedName = payload.inputedName.trim().slice(0, 32);
const nextState = { ...state, inputedName };
if (payload.setInGame) {
return handleToggleInGame(nextState, { type: 'toggleInGame', payload: { inGame: true } });
return handleSetInGame(nextState, { type: 'setInGame', payload: { inGame: true } });
} else {
return nextState;
}
Expand Down Expand Up @@ -182,7 +184,6 @@ const handleEditMessage = (
editFor,
media: mediaId,
source,
defaultInGame: inGame,
inputedName,
range,
backup: clearBackup(state),
Expand Down Expand Up @@ -242,8 +243,7 @@ const handleSent = (state: ComposeState, { payload: { edit = false } }: ComposeA
return state.backup;
}
const modifiersParseResult = parseModifiers(state.source);
const nextDefaultInGame = !(modifiersParseResult.inGame ? modifiersParseResult.inGame.inGame : state.defaultInGame);
let source = nextDefaultInGame ? '.out ' : '.in ';
let source = '';
if (modifiersParseResult.mute) {
source = '.mute ';
}
Expand All @@ -252,7 +252,6 @@ const handleSent = (state: ComposeState, { payload: { edit = false } }: ComposeA
}
return {
...state,
defaultInGame: nextDefaultInGame,
previewId: makeId(),
editFor: null,
range: [source.length, source.length],
Expand Down Expand Up @@ -327,6 +326,8 @@ export const composeReducer = (state: ComposeState, action: ComposeActionUnion):
return handleSetInputedName(state, action);
case 'toggleInGame':
return handleToggleInGame(state, action);
case 'setInGame':
return handleSetInGame(state, action);
case 'recoverState':
return handleRecoverState(state, action);
case 'addDice':
Expand Down Expand Up @@ -363,13 +364,8 @@ export const composeReducer = (state: ComposeState, action: ComposeActionUnion):
};

export const checkCompose =
(characterName: string) =>
({
source,
inputedName,
defaultInGame,
media,
}: Pick<ComposeState, 'source' | 'inputedName' | 'defaultInGame' | 'media'>): ComposeError | null => {
(characterName: string, defaultInGame: boolean) =>
({ source, inputedName, media }: Pick<ComposeState, 'source' | 'inputedName' | 'media'>): ComposeError | null => {
const { inGame, rest } = parseModifiers(source);
if (inGame ? inGame.inGame : defaultInGame) {
if (inputedName.trim() === '' && characterName === '') {
Expand Down

0 comments on commit 1a54c69

Please sign in to comment.