Skip to content

Commit

Permalink
Type most chat actions?
Browse files Browse the repository at this point in the history
  • Loading branch information
goto-bus-stop committed Mar 25, 2023
1 parent 184cc06 commit fb9e564
Show file tree
Hide file tree
Showing 9 changed files with 106 additions and 122 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { AnyAction } from 'redux';
import type { ThunkAction } from 'redux-thunk';
import ms from 'ms';
import parseChatMarkup from 'u-wave-parse-chat-markup';
import flashDocumentTitle from 'flash-document-title';
Expand All @@ -7,18 +9,10 @@ import {
RECEIVE_MOTD,
SET_MOTD_START,
SET_MOTD_COMPLETE,
SEND_MESSAGE,
LOG,
REMOVE_MESSAGE,
REMOVE_USER_MESSAGES,
REMOVE_ALL_MESSAGES,
MUTE_USER,
UNMUTE_USER,
LOAD_EMOTES,
} from '../constants/ActionTypes';
import { get, put } from './RequestActionCreators';
import {
muteTimeoutsSelector,
mutedUserIDsSelector,
currentUserMuteSelector,
} from '../selectors/chatSelectors';
Expand All @@ -29,32 +23,32 @@ import {
userHasRoleSelector,
currentUserHasRoleSelector,
} from '../selectors/userSelectors';
import { currentTimeSelector } from '../selectors/timeSelectors';
import {
getAvailableGroupMentions,
resolveMentions,
hasMention,
} from '../utils/chatMentions';
import * as actions from '../reducers/chat';
import { StoreState } from '../redux/configureStore';
import { User } from '../reducers/users';

export function receiveMotd(text) {
type Thunk = ThunkAction<unknown, StoreState, never, AnyAction>;

export function receiveMotd(text: string) {
return {
type: RECEIVE_MOTD,
payload: text,
};
}

export function log(text) {
return {
type: LOG,
payload: {
_id: randomUUID(),
text,
},
};
export function log(text: string) {
return actions.log({
_id: randomUUID(),
text,
});
}

export function prepareMessage(state, user, text, parseOpts = {}) {
export function prepareMessage(state: StoreState, user: User, text: string, parseOpts = {}) {
const parsed = parseChatMarkup(text, parseOpts);
resolveMentions(parsed, state);
return actions.sendMessage({
Expand All @@ -64,7 +58,7 @@ export function prepareMessage(state, user, text, parseOpts = {}) {
});
}

export function sendChat(text) {
export function sendChat(text: string): Thunk {
return (dispatch, getState) => {
const state = getState();
const sender = currentUserSelector(state);
Expand All @@ -87,17 +81,26 @@ export function sendChat(text) {
};
}

function isMuted(state, userID) {
function isMuted(state: StoreState, userID: string) {
return mutedUserIDsSelector(state).includes(userID);
}

export function receive(message) {
export function receive(message: {
_id: string,
userID: string,
text: string,
timestamp: number,
}): Thunk {
return (dispatch, getState) => {
const state = getState();
const settings = settingsSelector(state);
const currentUser = currentUserSelector(state);
const users = userListSelector(state);
const sender = users.find((user) => user._id === message.userID);
if (!sender) {
// TODO we should find the user somehow?
return;
}
const senderHasRole = userHasRoleSelector(state)(sender);
const mentions = [
...users.map((user) => user.username),
Expand Down Expand Up @@ -131,82 +134,24 @@ export function receive(message) {
};
}

export function removeMessage(id) {
return {
type: REMOVE_MESSAGE,
payload: { _id: id },
};
}

export function removeMessagesByUser(userID) {
return {
type: REMOVE_USER_MESSAGES,
payload: { userID },
};
}

export function removeAllMessages() {
return {
type: REMOVE_ALL_MESSAGES,
};
}

function expireMute(userID) {
return {
type: UNMUTE_USER,
payload: { userID },
};
}

export function muteUser(userID, { moderatorID, expiresAt }) {
return (dispatch, getState) => {
const currentTime = currentTimeSelector(getState());
const expireIn = expiresAt - currentTime;

dispatch({
type: MUTE_USER,
payload: {
userID,
moderatorID,
expiresAt,
expirationTimer: expireIn > 0
? setTimeout(() => dispatch(expireMute(userID)), expireIn) : null,
},
});
};
}

export function unmuteUser(userID, { moderatorID }) {
return (dispatch, getState) => {
const muteTimeouts = muteTimeoutsSelector(getState());
if (muteTimeouts && muteTimeouts[userID]) {
clearTimeout(muteTimeouts[userID]);
}
dispatch({
type: UNMUTE_USER,
payload: { userID, moderatorID },
});
};
}

export function setMotdStart(motd) {
export function setMotdStart(motd: string) {
return {
type: SET_MOTD_START,
payload: motd,
};
}

export function setMotdComplete(motd) {
export function setMotdComplete(motd: string) {
return {
type: SET_MOTD_COMPLETE,
payload: motd,
};
}

export function setMotd(text) {
export function setMotd(text: string) {
return put('/motd', { motd: text }, {
onStart: () => setMotdStart(text),
onComplete: ({ data }) => (dispatch) => {
onComplete: ({ data }: { data: { motd: string } }): Thunk => (dispatch) => {
dispatch(setMotdComplete(data.motd));
dispatch(log(`Message of the Day is now: ${data.motd}`));
},
Expand All @@ -217,13 +162,3 @@ export function setMotd(text) {
}),
});
}

export function loadEmotes() {
return async (dispatch) => {
const emotes = await dispatch(get('/emotes'));
dispatch({
type: LOAD_EMOTES,
payload: { emotes: emotes.data },
});
};
}
2 changes: 0 additions & 2 deletions src/actions/LoginActionCreators.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import { get, post, del } from './RequestActionCreators';
import { setPlaylists } from './PlaylistActionCreators';
import { syncTimestamps } from './TickerActionCreators';
import { closeLoginDialog } from './DialogActionCreators';
import { loadEmotes } from './ChatActionCreators';
import { tokenSelector } from '../selectors/userSelectors';

export function socketConnect() {
Expand Down Expand Up @@ -77,7 +76,6 @@ export function initState() {
mutate('/booth/history');
dispatch(syncTimestamps(beforeTime, state.time));
dispatch(loadedState(state));
dispatch(loadEmotes());
return state;
},
});
Expand Down
4 changes: 0 additions & 4 deletions src/actions/ModerationActionCreators.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
ADD_USER_ROLES_START, ADD_USER_ROLES_COMPLETE,
REMOVE_USER_ROLES_START, REMOVE_USER_ROLES_COMPLETE,
} from '../constants/ActionTypes';
import { removeMessage, removeMessagesByUser, removeAllMessages } from './ChatActionCreators';

export function skipCurrentDJ(reason = '', shouldRemove = false) {
return (dispatch, getState) => {
Expand Down Expand Up @@ -145,7 +144,6 @@ export function removeUserRole(user, role) {

export function deleteChatMessage(id) {
return del(`/chat/${id}`, {}, {
onStart: () => removeMessage(id),
onError: (error) => ({
type: undefined,
error: true,
Expand All @@ -157,7 +155,6 @@ export function deleteChatMessage(id) {

export function deleteChatMessagesByUser(userID) {
return del(`/chat/user/${userID}`, {}, {
onComplete: () => removeMessagesByUser(userID),
onError: (error) => ({
type: undefined,
error: true,
Expand All @@ -169,7 +166,6 @@ export function deleteChatMessagesByUser(userID) {

export function deleteAllChatMessages() {
return del('/chat', {}, {
onComplete: removeAllMessages,
onError: (error) => ({
type: undefined,
error: true,
Expand Down
26 changes: 14 additions & 12 deletions src/containers/ChatInput.jsx → src/containers/ChatInput.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
import React from 'react';
import { useBus } from 'react-bus';
import splitargs from 'splitargs';
import { useStore, useSelector } from '../hooks/useRedux';
import { useDispatch, useStore, useSelector } from '../hooks/useRedux';
import { sendChat } from '../actions/ChatActionCreators';
import {
availableGroupMentionsSelector,
emojiCompletionsSelector,
} from '../selectors/chatSelectors';
import { availableGroupMentionsSelector } from '../selectors/chatSelectors';
import {
userListSelector,
isLoggedInSelector,
} from '../selectors/userSelectors';
import commandList from '../utils/commands';
import ChatCommands from '../utils/ChatCommands';
import useEmotes from '../hooks/useEmotes';

const ChatInput = React.lazy(() => (
import(/* webpackPreload: true */ '../components/Chat/Input')
));
const ChatInput = React.lazy(() => import('../components/Chat/Input'));

const {
useCallback,
Expand All @@ -34,11 +30,17 @@ function ChatInputContainer() {
const isLoggedIn = useSelector(isLoggedInSelector);
const mentionableUsers = useSelector(userListSelector);
const mentionableGroups = useSelector(availableGroupMentionsSelector);
const availableEmoji = useSelector(emojiCompletionsSelector);
const emotes = useEmotes();
const availableEmoji = useMemo(() => {
return Object.entries(emotes).map(([shortcode, url]) => ({
shortcode,
image: url,
}));
}, [emotes]);
const dispatch = useDispatch();
const store = useStore();
const { dispatch } = store;
const commander = useMemo(() => new ChatCommands(store, commandList), [store]);
const onSend = useCallback((message) => {
const onSend = useCallback((message: string) => {
if (message.startsWith('/')) {
const [command, ...params] = splitargs(message.slice(1));
if (command) {
Expand All @@ -53,7 +55,7 @@ function ChatInputContainer() {
}, [commander, dispatch]);

const bus = useBus();
const onScroll = useCallback((direction) => {
const onScroll = useCallback((direction: unknown) => {
bus.emit('chat:scroll', direction);
}, [bus]);

Expand Down
14 changes: 11 additions & 3 deletions src/containers/ChatMessages.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
import { useCallback } from 'react';
import { useCallback, useMemo } from 'react';
import { useDispatch, useSelector } from '../hooks/useRedux';
import {
motdSelector,
messagesSelector,
markupCompilerOptionsSelector,
canDeleteMessagesSelector,
} from '../selectors/chatSelectors';
import { customEmojiNamesSelector } from '../selectors/configSelectors';
import { deleteChatMessage } from '../actions/ModerationActionCreators';
import ChatMessages from '../components/Chat/ChatMessages';
import useEmotes from '../hooks/useEmotes';
import { CompileOptions } from '../components/Chat/Markup';

function ChatMessagesContainer() {
const dispatch = useDispatch();
const motd = useSelector(motdSelector);
const messages = useSelector(messagesSelector);
const compileOptions = useSelector(markupCompilerOptionsSelector);
const emotes = useEmotes();
const customEmojiNames = useSelector(customEmojiNamesSelector);
const compileOptions: CompileOptions = useMemo(() => ({
availableEmoji: new Set(Object.keys(emotes)),
emojiImages: emotes,
customEmojiNames,
}), [emotes, customEmojiNames]);
const canDeleteMessages = useSelector(canDeleteMessagesSelector);
const onDeleteMessage = useCallback((id: string) => {
dispatch(deleteChatMessage(id));
Expand Down
32 changes: 32 additions & 0 deletions src/hooks/useEmotes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import useSWR from 'swr';
import { useSelector } from './useRedux';
import defaultEmoji from '../utils/emojiShortcodes';
import { useMemo } from 'react';

type ServerEmote = {
name: string,
url: string,
};

function useEmotes() {
const { data: serverEmotes } = useSWR<ServerEmote[]>('/emotes', async (url: string) => {
const response = await fetch(url);
const { data } = await response.json();
return data;
}, {
revalidateOnFocus: false,
fallbackData: [],
});

const configEmoji = useSelector((state) => state.config.emoji);

const emotes: Record<string, string> = useMemo(() => ({
...defaultEmoji,
...configEmoji,
...Object.fromEntries(serverEmotes.map(({ name, url }) => [name, url])),
}), [configEmoji, serverEmotes]);

return emotes;
}

export default useEmotes;
Loading

0 comments on commit fb9e564

Please sign in to comment.