{Object.entries(reactionMap).map(([key, count]) => (
-
dispatch.methods.messages.addReaction(id, key)}>
+ dispatch.methods.messages.addReaction({id, text: key})}>
{count > 1 ? `${count} ` : ''}
diff --git a/packages/app/src/js/components/molecules/ThreadLink.tsx b/packages/app/src/js/components/molecules/ThreadLink.tsx
index 7cd41f18..f6eb4acc 100644
--- a/packages/app/src/js/components/molecules/ThreadLink.tsx
+++ b/packages/app/src/js/components/molecules/ThreadLink.tsx
@@ -1,5 +1,5 @@
import styled from 'styled-components';
-import { useDispatch } from 'react-redux';
+import { useDispatch } from '../../store';
const Link = styled.span`
color: ${(props) => props.theme.linkColor};
diff --git a/packages/app/src/js/components/molecules/UserMention.tsx b/packages/app/src/js/components/molecules/UserMention.tsx
index 602a93c5..fbd03c6d 100644
--- a/packages/app/src/js/components/molecules/UserMention.tsx
+++ b/packages/app/src/js/components/molecules/UserMention.tsx
@@ -1,4 +1,4 @@
-import { useSelector, useDispatch } from 'react-redux';
+import { useSelector, useDispatch } from '../../store';
import styled from 'styled-components';
import { gotoDirectChannel } from '../../services/channels';
diff --git a/packages/app/src/js/components/organisms/Conversation.tsx b/packages/app/src/js/components/organisms/Conversation.tsx
index 584bb6d7..7d4c24ea 100644
--- a/packages/app/src/js/components/organisms/Conversation.tsx
+++ b/packages/app/src/js/components/organisms/Conversation.tsx
@@ -1,5 +1,5 @@
import { useEffect, useCallback} from 'react';
-import { useDispatch, useSelector } from 'react-redux';
+import { useDispatch, useSelector } from '../../store';
import { MessageList } from './MessageListScroller';
import { uploadMany } from '../../services/file';
import { Input } from '../organisms/Input';
diff --git a/packages/app/src/js/components/organisms/EmojiSearch.tsx b/packages/app/src/js/components/organisms/EmojiSearch.tsx
index 0e1f9df2..d4df4549 100644
--- a/packages/app/src/js/components/organisms/EmojiSearch.tsx
+++ b/packages/app/src/js/components/organisms/EmojiSearch.tsx
@@ -1,5 +1,5 @@
import { useState, useEffect} from 'react';
-import { useSelector } from 'react-redux';
+import { useSelector } from '../../store';
import { Tooltip } from '../atoms/Tooltip';
import { SearchBox } from '../atoms/SearchBox';
import { useEmojiFuse } from '../../hooks';
diff --git a/packages/app/src/js/components/organisms/Input.tsx b/packages/app/src/js/components/organisms/Input.tsx
index 6300863b..ba5f3289 100644
--- a/packages/app/src/js/components/organisms/Input.tsx
+++ b/packages/app/src/js/components/organisms/Input.tsx
@@ -1,5 +1,5 @@
import { useCallback, useState, useEffect } from 'react';
-import { useDispatch } from 'react-redux';
+import { useDispatch } from '../../store';
import styled from 'styled-components';
import { EmojiDescriptor } from '../../types';
diff --git a/packages/app/src/js/components/organisms/MainConversaion.tsx b/packages/app/src/js/components/organisms/MainConversaion.tsx
index cb7fe6a7..6f607336 100644
--- a/packages/app/src/js/components/organisms/MainConversaion.tsx
+++ b/packages/app/src/js/components/organisms/MainConversaion.tsx
@@ -1,6 +1,6 @@
import styled from 'styled-components';
import { Conversation } from './Conversation';
-import { useDispatch } from 'react-redux';
+import { useDispatch } from '../../store';
import { Channel } from '../molecules/NavChannel';
import { init } from '../../services/init';
import { useStream } from '../contexts/useStream';
diff --git a/packages/app/src/js/components/organisms/Message.tsx b/packages/app/src/js/components/organisms/Message.tsx
index 4815f389..af053312 100644
--- a/packages/app/src/js/components/organisms/Message.tsx
+++ b/packages/app/src/js/components/organisms/Message.tsx
@@ -1,5 +1,5 @@
import { useCallback } from 'react';
-import { useDispatch } from 'react-redux';
+import { useDispatch } from '../../store';
import styled from 'styled-components';
import { resend } from '../../services/messages';
diff --git a/packages/app/src/js/components/organisms/SideConversation.tsx b/packages/app/src/js/components/organisms/SideConversation.tsx
index fb09a4bc..a5baa38f 100644
--- a/packages/app/src/js/components/organisms/SideConversation.tsx
+++ b/packages/app/src/js/components/organisms/SideConversation.tsx
@@ -1,6 +1,6 @@
import styled from 'styled-components';
import { Conversation } from './Conversation';
-import { useDispatch } from 'react-redux';
+import { useDispatch } from '../../store';
import { Channel } from '../molecules/NavChannel';
import { useStream } from '../contexts/useStream';
import { useMessage } from '../../hooks';
diff --git a/packages/app/src/js/components/organisms/Sidebar.tsx b/packages/app/src/js/components/organisms/Sidebar.tsx
index 6beebdf7..a31609e8 100644
--- a/packages/app/src/js/components/organisms/Sidebar.tsx
+++ b/packages/app/src/js/components/organisms/Sidebar.tsx
@@ -5,7 +5,7 @@ import { NavUsers } from '../molecules/NavUsers';
import { NavButton } from '../molecules/NavButton';
import plugins from '../../core/plugins';
import { logout } from '../../services/session';
-import { useDispatch } from 'react-redux';
+import { useDispatch } from '../../store';
export const SideMenu = styled.div`
diff --git a/packages/app/src/js/components/pages/Pins.tsx b/packages/app/src/js/components/pages/Pins.tsx
index 03f1ef81..eaca89c2 100644
--- a/packages/app/src/js/components/pages/Pins.tsx
+++ b/packages/app/src/js/components/pages/Pins.tsx
@@ -2,7 +2,7 @@ import styled from 'styled-components';
import { Channel } from '../molecules/NavChannel';
import { useStream } from '../contexts/useStream';
import { useCallback } from 'react';
-import { useDispatch, useSelector } from 'react-redux';
+import { useDispatch, useSelector } from '../../store';
import { HoverProvider } from '../contexts/hover';
import { MessageList } from '../organisms/MessageListScroller'
import { Message as MessageType } from '../../types';
diff --git a/packages/app/src/js/components/pages/Search.tsx b/packages/app/src/js/components/pages/Search.tsx
index d3ef8d9b..25c1abe8 100644
--- a/packages/app/src/js/components/pages/Search.tsx
+++ b/packages/app/src/js/components/pages/Search.tsx
@@ -2,7 +2,7 @@ import styled from 'styled-components';
import { useCallback, useState } from 'react';
import { useStream } from '../contexts/useStream';
import { HoverProvider } from '../contexts/hover';
-import { useSelector, useDispatch } from 'react-redux';
+import { useSelector, useDispatch } from '../../store';
import { formatTime, formatDate } from '../../utils';
import { SearchBox } from '../atoms/SearchBox';
@@ -108,7 +108,7 @@ export const Header = () => {
const [value, setValue] = useState('');
const submit = useCallback(async () => {
- dispatch.methods.search.find(stream.channelId, value);
+ dispatch.methods.search.find({channelId: stream.channelId, text: value});
}, [dispatch, stream, value]);
const onKeyDown= useCallback(async (e: React.KeyboardEvent) => {
diff --git a/packages/app/src/js/components/pages/Workspace.tsx b/packages/app/src/js/components/pages/Workspace.tsx
index e8f60f40..2d6f04f1 100644
--- a/packages/app/src/js/components/pages/Workspace.tsx
+++ b/packages/app/src/js/components/pages/Workspace.tsx
@@ -1,4 +1,4 @@
-import { useDispatch, useSelector } from 'react-redux';
+import { useDispatch, useSelector } from '../../store';
import { MainConversation } from '../organisms/MainConversaion';
import { SideConversation } from '../organisms/SideConversation';
diff --git a/packages/app/src/js/hooks/index.js b/packages/app/src/js/hooks/index.ts
similarity index 100%
rename from packages/app/src/js/hooks/index.js
rename to packages/app/src/js/hooks/index.ts
diff --git a/packages/app/src/js/hooks/useEmoji.ts b/packages/app/src/js/hooks/useEmoji.ts
index f9240e26..40084d86 100644
--- a/packages/app/src/js/hooks/useEmoji.ts
+++ b/packages/app/src/js/hooks/useEmoji.ts
@@ -1,5 +1,5 @@
import { useMemo, useEffect } from 'react';
-import { useSelector, useDispatch } from 'react-redux';
+import { useDispatch, useSelector } from '../store';
type Emoji = {
shortname: string;
@@ -10,16 +10,16 @@ type Emoji = {
export const useEmoji = (shortname: string): Emoji => {
const dispatch = useDispatch();
- const emojis = useSelector((state: any) => state.emojis);
+ const emojis = useSelector((state) => state.emojis);
const emoji = useMemo(
() => emojis.data.find((emoji: Emoji) => emoji.shortname === shortname),
[emojis, shortname],
);
useEffect(() => {
if (!emoji && emojis.ready) {
- (dispatch as any).methods.emojis.find(shortname);
+ dispatch.methods.emojis.find(shortname);
}
}, [dispatch, emoji, shortname, emojis]);
- return emoji;
+ return emoji ?? { shortname, empty: true};
};
diff --git a/packages/app/src/js/services/file.js b/packages/app/src/js/services/file.js
index 77b7430b..e61bb81c 100644
--- a/packages/app/src/js/services/file.js
+++ b/packages/app/src/js/services/file.js
@@ -6,12 +6,12 @@ const tempId = createCounter(`file:${(Math.random() + 1).toString(36)}`);
const FILES_URL = `${API_URL}/files`;
-export const uploadMany = (streamId, files) => async (dispatch) => {
+export const uploadMany = (streamId, files) => ({type: 'async', handler: async (dispatch) => {
for (let i = 0, file; i < files.length; i++) {
file = files.item(i);
dispatch(upload(streamId, file));
}
-};
+}});
export const upload = (streamId, file) => async (dispatch) => {
const local = {
diff --git a/packages/app/src/js/services/messages.js b/packages/app/src/js/services/messages.ts
similarity index 96%
rename from packages/app/src/js/services/messages.js
rename to packages/app/src/js/services/messages.ts
index eb43287b..efe24167 100644
--- a/packages/app/src/js/services/messages.js
+++ b/packages/app/src/js/services/messages.ts
@@ -1,16 +1,17 @@
import { client } from '../core';
import { createCounter } from '../utils';
+import { createAsyncAction } from '../store';
const tempId = createCounter(`temp:${(Math.random() + 1).toString(36)}`);
-const loading = (dispatch) => {
- dispatch.actions.messages.loading();
- const timer = setTimeout(() => dispatch.actions.messages.loadingDone(), 1000);
+const loading = createAsyncAction(async (dispatch) => {
+ dispatch.actions.messages.loading({});
+ const timer = setTimeout(() => dispatch.actions.messages.loadingDone({}), 1000);
return () => {
- dispatch.actions.messages.loadingDone();
+ dispatch.actions.messages.loadingDone({});
clearTimeout(timer);
};
-};
+});
const getStreamMessages = (stream, messages) => messages
.filter((m) => m.channelId === stream.channelId
@@ -119,9 +120,9 @@ export const sendFromDom = (stream, dom) => async (dispatch, getState) => {
}
};
-export const send = (stream, msg) => (dispatch) => dispatch(msg.type === 'command:execute' ? sendCommand(stream, msg) : sendMessage(msg));
+export const send = (stream, msg) => createAsyncAction((dispatch) => dispatch(msg.type === 'command:execute' ? sendCommand(stream, msg) : sendMessage(msg)));
-export const sendShareMessage = (data) => async (dispatch, getState) => {
+export const sendShareMessage = (data) => createAsyncAction(async (dispatch, getState) => {
const { channelId, parentId } = getState().stream.main;
const info = { links: [] };
const msg = build({
@@ -147,7 +148,7 @@ export const sendShareMessage = (data) => async (dispatch, getState) => {
},
});
}
-};
+});
const buildShareMessage = (data, info) => {
const lines = [];
diff --git a/packages/app/src/js/store/components/command.tsx b/packages/app/src/js/store/components/command.tsx
new file mode 100644
index 00000000..616f5c01
--- /dev/null
+++ b/packages/app/src/js/store/components/command.tsx
@@ -0,0 +1,18 @@
+import {
+ createContext,
+} from 'react';
+
+export const CommandContext = createContext(undefined);
+
+type CommandContextProps = {
+ children: React.ReactNode;
+ value: any;
+};
+
+export const CommandProvider = ({ children, value }: CommandContextProps) => {
+ return (
+
+ {children}
+
+ );
+};
diff --git a/packages/app/src/js/store/components/provider.tsx b/packages/app/src/js/store/components/provider.tsx
new file mode 100644
index 00000000..81df3859
--- /dev/null
+++ b/packages/app/src/js/store/components/provider.tsx
@@ -0,0 +1,15 @@
+import { Provider } from 'react-redux';
+import { store } from '../store';
+
+type ProviderProps = {
+ children: React.ReactNode;
+};
+
+const StoreProvider = ({children}: ProviderProps) => (
+
+ {children}
+
+);
+
+export default StoreProvider;
+
diff --git a/packages/app/src/js/store/hooks/useCommand.ts b/packages/app/src/js/store/hooks/useCommand.ts
new file mode 100644
index 00000000..c8bc503d
--- /dev/null
+++ b/packages/app/src/js/store/hooks/useCommand.ts
@@ -0,0 +1,6 @@
+import { useContext } from 'react';
+import {CommandContext} from '../components/command';
+
+export const useCommand = () => {
+ return useContext(CommandContext);
+};
diff --git a/packages/app/src/js/store/hooks/useDispatch.ts b/packages/app/src/js/store/hooks/useDispatch.ts
new file mode 100644
index 00000000..24c57bf9
--- /dev/null
+++ b/packages/app/src/js/store/hooks/useDispatch.ts
@@ -0,0 +1,4 @@
+import * as reactRedux from 'react-redux';
+import {DispatchType} from '../store';
+
+export const useDispatch = reactRedux.useDispatch.withTypes();
diff --git a/packages/app/src/js/store/hooks/useSelector.ts b/packages/app/src/js/store/hooks/useSelector.ts
new file mode 100644
index 00000000..75cec550
--- /dev/null
+++ b/packages/app/src/js/store/hooks/useSelector.ts
@@ -0,0 +1,4 @@
+import * as reactRedux from 'react-redux';
+import {StateType} from '../store';
+
+export const useSelector = reactRedux.useSelector.withTypes();
diff --git a/packages/app/src/js/store/hooks/useStore.ts b/packages/app/src/js/store/hooks/useStore.ts
new file mode 100644
index 00000000..14fd607e
--- /dev/null
+++ b/packages/app/src/js/store/hooks/useStore.ts
@@ -0,0 +1,4 @@
+import * as reactRedux from 'react-redux';
+import {StoreType} from '../store';
+
+export const useStore = () => reactRedux.useStore();
diff --git a/packages/app/src/js/store/index.js b/packages/app/src/js/store/index.js
deleted file mode 100644
index ee2b7760..00000000
--- a/packages/app/src/js/store/index.js
+++ /dev/null
@@ -1,52 +0,0 @@
-import {
- createStore, applyMiddleware, compose,
-} from 'redux';
-import { client } from '../core';
-import * as modules from './modules';
-
-const middleware = ({ dispatch, getState }) => (next) => async (action) => {
- if (typeof action === 'function') {
- dispatch.actions = actions;
- dispatch.methods = methods;
- try {
- return await action(dispatch, getState, {client});
- } catch (err) {
- // eslint-disable-next-line no-console
- console.error(err);
- return;
- }
- }
-
- return next(action);
-}
-
-export const store = createStore(
- modules.reducer,
- window.__REDUX_DEVTOOLS_EXTENSION__
- ? compose(
- applyMiddleware(middleware),
- window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(),
- )
- : applyMiddleware(middleware),
-);
-
-window.store = store;
-
-export const actions = Object.keys(modules.actions).reduce((acc, module) => {
- acc[module] = Object.keys(modules.actions[module]).reduce((acc2, action) => {
- acc2[action] = (data) => store.dispatch(modules.actions[module][action](data));
- return acc2;
- }, {});
- return acc;
-}, {});
-
-export const methods = Object.keys(modules.methods).reduce((acc, module) => {
- acc[module] = Object.keys(modules.methods[module]).reduce((acc2, action) => {
- acc2[action] = (...props) => store.dispatch(modules.methods[module][action](...props));
- return acc2;
- }, {});
- return acc;
-}, {});
-
-store.dispatch.actions = actions;
-store.dispatch.methods = methods;
diff --git a/packages/app/src/js/store/index.ts b/packages/app/src/js/store/index.ts
new file mode 100644
index 00000000..0ff383e1
--- /dev/null
+++ b/packages/app/src/js/store/index.ts
@@ -0,0 +1,9 @@
+export * from './store';
+export * from './hooks/useCommand';
+export * from './hooks/useDispatch';
+export * from './hooks/useSelector';
+export * from './hooks/useStore';
+
+
+
+
diff --git a/packages/app/src/js/store/modules/channels.js b/packages/app/src/js/store/modules/channels.js
deleted file mode 100644
index ced755e1..00000000
--- a/packages/app/src/js/store/modules/channels.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import {createModule} from '../tools';
-
-export default createModule({
- name: 'channels',
- initialState: {},
- reducers: {
- add: (state, action) => {
- const newState = {...state};
- [action.payload].flat().forEach((channel) => {
- newState[channel.id] = Object.assign(newState[channel.id] || {}, channel);
- });
- return newState;
- },
- remove: (state, action) => {
- const id = action.payload;
- const newState = {...state};
- delete newState[id];
- return newState;
- },
- },
- methods: {
- load: () => async ({actions}, getState, {client}) => {
- const res = await client.req({ type: 'channel:getAll' });
- actions.channels.add(res.data);
- },
- create: ({channelType, name, users}) => async ({actions}, getState, {client}) => {
- const res = await client.req({
- type: 'channel:create', channelType, name, users,
- });
- actions.channels.add(res.data);
- },
- find: (id) => async ({actions}, getState, {client}) => {
- const res = await client.req({ type: 'channel:get', id });
- actions.channels.add(res.data);
- return res.data;
- },
- },
-});
diff --git a/packages/app/src/js/store/modules/config.js b/packages/app/src/js/store/modules/config.js
deleted file mode 100644
index 8b87c2a1..00000000
--- a/packages/app/src/js/store/modules/config.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import {createModule} from '../tools';
-
-export default createModule({
- name: 'config',
- initialState: {},
- reducers: {
- setAppVersion: (state, action) => ({...state, appVersion: action.payload }),
- },
- methods: {
- load: () => async ({ actions }, getState, { client }) => {
- const { data: [config] } = await client.req({ type: 'user:config' });
- actions.config.setAppVersion(config.appVersion);
- return config;
- },
- },
-});
diff --git a/packages/app/src/js/store/modules/connection.js b/packages/app/src/js/store/modules/connection.js
deleted file mode 100644
index 8dc6fc78..00000000
--- a/packages/app/src/js/store/modules/connection.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import {createModule} from '../tools';
-
-export default createModule({
- name: 'connection',
- initialState: false,
- reducers: {
- connected: () => true,
- disconnected: () => false,
- },
-});
diff --git a/packages/app/src/js/store/modules/files.js b/packages/app/src/js/store/modules/files.js
deleted file mode 100644
index 1aaf51e6..00000000
--- a/packages/app/src/js/store/modules/files.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import {createModule} from '../tools';
-
-export const findIdx = (list, id) => list.findIndex((f) => (f.id && f.id === id)
- || (f.clientId && f.clientId === id));
-
-export default createModule({
- name: 'files',
- initialState: [],
- reducers: {
- add: (state, action) => ([...state, ...[action.payload].flat()]),
- update: (state, action) => {
- const idx = findIdx(state, action.payload.id);
- if (idx === -1) return state;
- const newState = [...state];
- newState[idx] = { ...newState[idx], ...action.payload.file};
- return newState;
- },
- remove: (state, action) => {
- const idx = findIdx(state, action.payload);
- if (idx === -1) return;
- const newState = [...state];
- newState.splice(idx, 1);
- return newState;
- },
- clear: (state, action) => state.filter((f) => f.streamId !== action.payload),
- },
-});
diff --git a/packages/app/src/js/store/modules/index.js b/packages/app/src/js/store/modules/index.js
deleted file mode 100644
index d6c6bbc2..00000000
--- a/packages/app/src/js/store/modules/index.js
+++ /dev/null
@@ -1,51 +0,0 @@
-import {combineReducers} from 'redux';
-import channels from './channels';
-import me from './me';
-import users from './users';
-import stream from './stream';
-import messages from './messages';
-import emojis from './emojis';
-import config from './config';
-import connection from './connection';
-import system from './system';
-import info from './info';
-import progress from './progress';
-import view from './view';
-import files from './files';
-import typing from './typing';
-import pins from './pins';
-import search from './search';
-
-const MODULES = {
- channels,
- me,
- users,
- stream,
- messages,
- emojis,
- config,
- connection,
- system,
- info,
- progress,
- view,
- files,
- typing,
- pins,
- search,
-};
-
-export const reducer = combineReducers(Object.entries(MODULES).reduce((acc, [name, value]) => ({
- ...acc,
- [name]: value.reducer,
-}), {}));
-
-export const actions = Object.entries(MODULES).reduce((acc, [name, value]) => ({
- ...acc,
- [name]: value.actions,
-}), {});
-
-export const methods = Object.entries(MODULES).reduce((acc, [name, value]) => ({
- ...acc,
- [name]: value.methods,
-}), {});
diff --git a/packages/app/src/js/store/modules/info.js b/packages/app/src/js/store/modules/info.js
deleted file mode 100644
index e9d04b94..00000000
--- a/packages/app/src/js/store/modules/info.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import {createModule} from '../tools';
-
-export default createModule({
- name: 'info',
- initialState: { type: null, message: '' },
- reducers: {
- show: (state, action) => action.payload,
- },
-});
diff --git a/packages/app/src/js/store/modules/me.js b/packages/app/src/js/store/modules/me.js
deleted file mode 100644
index c4a63c63..00000000
--- a/packages/app/src/js/store/modules/me.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import {createModule} from '../tools';
-
-export default createModule({
- name: 'me',
- initialState: null,
- reducers: {
- set: (state, action) => action.payload,
- },
-});
diff --git a/packages/app/src/js/store/modules/system.js b/packages/app/src/js/store/modules/system.js
deleted file mode 100644
index c696ca4c..00000000
--- a/packages/app/src/js/store/modules/system.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import {createModule} from '../tools';
-
-export default createModule({
- name: 'system',
- initialState: { initFailed: false },
- reducers: {
- initFailed: (state, action) => ({ ...state, initFailed: action.payload }),
- },
-});
diff --git a/packages/app/src/js/store/modules/users.js b/packages/app/src/js/store/modules/users.js
deleted file mode 100644
index 4fcd58ab..00000000
--- a/packages/app/src/js/store/modules/users.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import {createModule} from '../tools';
-
-export default createModule({
- name: 'users',
- initialState: {},
- reducers: {
- add: (state, action) => {
- const newState = {...state};
- [action.payload].flat().forEach((user) => {
- newState[user.id] = Object.assign(newState[user.id] || {}, user);
- });
- return newState;
- },
- },
- methods: {
- load: () => async ({actions}, getState, {client}) => {
- const res = await client.req({ type: 'user:getAll' });
- actions.users.add(res.data);
- },
- },
-});
diff --git a/packages/app/src/js/store/modules/view.js b/packages/app/src/js/store/modules/view.js
deleted file mode 100644
index 2ebed615..00000000
--- a/packages/app/src/js/store/modules/view.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import {createModule} from '../tools';
-
-export default createModule({
- name: 'view',
- initialState: {current: null},
- reducers: {
- set: (state, action) => {
- const view = action.payload;
- if (state.current === view) {
- return {current: null};
- }
- return {current: view};
- },
- },
-});
diff --git a/packages/app/src/js/store/slices/channels.ts b/packages/app/src/js/store/slices/channels.ts
new file mode 100644
index 00000000..843209f4
--- /dev/null
+++ b/packages/app/src/js/store/slices/channels.ts
@@ -0,0 +1,53 @@
+import { PayloadAction, createSlice } from '@reduxjs/toolkit';
+import { createMethods } from '../tools';
+
+type Channel = {
+ id: string;
+ name: string;
+ users: string[];
+ priv?: boolean;
+};
+
+const slice = createSlice({
+ name: 'channels',
+ initialState: {} as {[id: string]: Channel},
+ reducers: {
+ add: (state, action: PayloadAction) => {
+ const payload = action.payload;
+ const newState = {...state};
+ [payload as Channel | Channel[]].flat().forEach((channel) => {
+ newState[channel.id] = Object.assign(newState[channel.id] || {}, channel);
+ });
+ return newState;
+ },
+ remove: (state, action: PayloadAction) => {
+ const id = action.payload;
+ const newState = {...state};
+ delete newState[id];
+ return newState;
+ },
+ },
+});
+
+export const methods = createMethods({
+ module_name: 'channels',
+ methods: {
+ load: async (_arg, {dispatch: {actions}},{client}) => {
+ const res = await client.req({ type: 'channel:getAll' });
+ actions.channels.add(res.data);
+ },
+ create: async ({channelType, name, users}, {dispatch: {actions}}, {client}) => {
+ const res = await client.req({
+ type: 'channel:create', channelType, name, users,
+ });
+ actions.channels.add(res.data);
+ },
+ find: async (id, {dispatch: {actions}},{client}) => {
+ const res = await client.req({ type: 'channel:get', id });
+ actions.channels.add(res.data);
+ return res.data;
+ },
+ },
+});
+
+export const { reducer, actions } = slice;
diff --git a/packages/app/src/js/store/slices/config.ts b/packages/app/src/js/store/slices/config.ts
new file mode 100644
index 00000000..3fb33e51
--- /dev/null
+++ b/packages/app/src/js/store/slices/config.ts
@@ -0,0 +1,27 @@
+import { createSlice } from '@reduxjs/toolkit';
+import {createMethods} from '../tools';
+
+type ConfigState = {
+ appVersion: string,
+};
+
+const slice = createSlice({
+ name: 'config',
+ initialState: {} as ConfigState,
+ reducers: {
+ setAppVersion: (state, action) => ({...state, appVersion: action.payload }),
+ },
+});
+
+export const methods = createMethods({
+ module_name: 'config',
+ methods: {
+ load: async (_arg, {dispatch: { actions }}, { client }) => {
+ const { data: [config] } = await client.req({ type: 'user:config' });
+ actions.config.setAppVersion(config.appVersion);
+ return config;
+ },
+ },
+});
+
+export const { reducer, actions } = slice;
diff --git a/packages/app/src/js/store/slices/connection.ts b/packages/app/src/js/store/slices/connection.ts
new file mode 100644
index 00000000..cda63760
--- /dev/null
+++ b/packages/app/src/js/store/slices/connection.ts
@@ -0,0 +1,15 @@
+import { createSlice } from '@reduxjs/toolkit';
+
+type ConnectionState = boolean;
+
+const slice = createSlice({
+ name: 'connection',
+ initialState: false as ConnectionState,
+ reducers: {
+ connected: () => true,
+ disconnected: () => false,
+ },
+});
+
+export const methods = {};
+export const { reducer, actions } = slice;
diff --git a/packages/app/src/js/store/modules/emojis.js b/packages/app/src/js/store/slices/emojis.ts
similarity index 62%
rename from packages/app/src/js/store/modules/emojis.js
rename to packages/app/src/js/store/slices/emojis.ts
index 511da82b..03b73d64 100644
--- a/packages/app/src/js/store/modules/emojis.js
+++ b/packages/app/src/js/store/slices/emojis.ts
@@ -1,8 +1,15 @@
-import {createModule} from '../tools';
+import { createSlice } from '@reduxjs/toolkit';
+import { createMethods } from '../tools';
+import { EmojiDescriptor } from '../../types';
-export default createModule({
+type EmojiState = {
+ ready: boolean;
+ data: EmojiDescriptor[];
+};
+
+const slice = createSlice({
name: 'emojis',
- initialState: {ready: false, data: []},
+ initialState: {ready: false, data: []} as EmojiState,
reducers: {
ready: (state) => ({...state, ready: true}),
add: (state, action) => {
@@ -18,18 +25,22 @@ export default createModule({
return newState;
},
},
+});
+
+export const methods = createMethods({
+ module_name: 'emojis',
methods: {
- load: () => async ({actions}, getState, {client}) => {
+ load: async (_arg, {dispatch: {actions}}, {client}) => {
const [baseEmojis, { data: emojis }] = await Promise.all([
import('../../../assets/emoji_list.json'),
client.req({ type: 'emoji:getAll' }),
]);
actions.emojis.add(baseEmojis.default);
- actions.emojis.add(emojis.map((e) => ({...e, category: 'c'})));
+ actions.emojis.add(emojis.map((e: any) => ({...e, category: 'c'})));
actions.emojis.ready({});
},
- find: (shortname) => async ({actions}, getState, {client}) => {
+ find: async (shortname, {dispatch: {actions}}, {client}) => {
try {
const { data: [emoji] } = await client.req({ type: 'emoji:get', shortname });
if (emoji) actions.emojis.add(emoji);
@@ -39,3 +50,6 @@ export default createModule({
},
},
});
+
+
+export const { actions, reducer } = slice;
diff --git a/packages/app/src/js/store/slices/files.ts b/packages/app/src/js/store/slices/files.ts
new file mode 100644
index 00000000..93245765
--- /dev/null
+++ b/packages/app/src/js/store/slices/files.ts
@@ -0,0 +1,44 @@
+import { createSlice } from '@reduxjs/toolkit';
+
+type File = {
+ id: string;
+ clientId: string;
+ streamId: string;
+ status: string;
+ file: {
+ name: string;
+ type: string;
+ size: number;
+ };
+};
+
+export const findIdx = (list: File[], id: string) => list.findIndex((f) => (f.id && f.id === id)
+ || (f.clientId && f.clientId === id));
+
+const slice = createSlice({
+ name: 'files',
+ initialState: [] as File[],
+ reducers: {
+ add: (state, action) => ([...state, ...[action.payload].flat()] as File[]),
+ update: (state, action) => {
+ const payload = (action.payload as File);
+ const idx = findIdx(state, payload.id);
+ if (idx === -1) return state;
+ const newState = [...state];
+ newState[idx] = { ...newState[idx], ...payload.file};
+ return newState;
+ },
+ remove: (state, action) => {
+ const id = (action.payload as string);
+ const idx = findIdx(state, id);
+ if (idx === -1) return state;
+ const newState = [...state];
+ newState.splice(idx, 1);
+ return newState;
+ },
+ clear: (state, action) => state.filter((f) => f.streamId !== action.payload),
+ },
+});
+
+export const methods = {};
+export const { reducer, actions } = slice;
diff --git a/packages/app/src/js/store/slices/index.ts b/packages/app/src/js/store/slices/index.ts
new file mode 100644
index 00000000..37ea3a91
--- /dev/null
+++ b/packages/app/src/js/store/slices/index.ts
@@ -0,0 +1,44 @@
+import * as channels from './channels';
+import * as me from './me';
+import * as users from './users';
+import * as stream from './stream';
+import * as messages from './messages';
+import * as emojis from './emojis';
+import * as config from './config';
+import * as connection from './connection';
+import * as system from './system';
+import * as info from './info';
+import * as progress from './progress';
+import * as view from './view';
+import * as files from './files';
+import * as typing from './typing';
+import * as pins from './pins';
+import * as search from './search';
+
+const modules = {
+ channels,
+ me,
+ users,
+ stream,
+ messages,
+ emojis,
+ config,
+ connection,
+ system,
+ info,
+ progress,
+ view,
+ files,
+ typing,
+ pins,
+ search,
+};
+type Modules = typeof modules;
+
+type Reducers = { [K in keyof T]: T[K]['reducer'] };
+type Methods = { [K in keyof T]: T[K]['methods'] };
+type Actions = { [K in keyof T]: T[K]['actions'] };
+
+export const reducers = Object.fromEntries(Object.entries(modules).map(([name, {reducer}]) => [name, reducer])) as Reducers;
+export const methods = Object.fromEntries(Object.entries(modules).map(([name, {methods}]) => [name, methods])) as Methods;
+export const actions = Object.fromEntries(Object.entries(modules).map(([name, {actions}]) => [name, actions])) as Actions;
diff --git a/packages/app/src/js/store/slices/info.ts b/packages/app/src/js/store/slices/info.ts
new file mode 100644
index 00000000..ba8277e1
--- /dev/null
+++ b/packages/app/src/js/store/slices/info.ts
@@ -0,0 +1,17 @@
+import { PayloadAction, createSlice } from '@reduxjs/toolkit';
+
+type InfoState = {
+ type: string | null;
+ message: string;
+};
+
+const slice = createSlice({
+ name: 'info',
+ initialState: { type: null, message: '' } as InfoState,
+ reducers: {
+ show: (_state, action: PayloadAction) => action.payload,
+ },
+});
+
+export const methods = {};
+export const { reducer, actions } = slice;
diff --git a/packages/app/src/js/store/slices/me.ts b/packages/app/src/js/store/slices/me.ts
new file mode 100644
index 00000000..47e904fc
--- /dev/null
+++ b/packages/app/src/js/store/slices/me.ts
@@ -0,0 +1,14 @@
+import { PayloadAction, createSlice } from '@reduxjs/toolkit';
+
+type MeState = string | null;
+
+const slice = createSlice({
+ name: 'me',
+ initialState: null as MeState,
+ reducers: {
+ set: (_state, action: PayloadAction) => action.payload,
+ },
+});
+
+export const methods = {};
+export const { reducer, actions } = slice;
diff --git a/packages/app/src/js/store/modules/messages.js b/packages/app/src/js/store/slices/messages.ts
similarity index 82%
rename from packages/app/src/js/store/modules/messages.js
rename to packages/app/src/js/store/slices/messages.ts
index f18f1f95..c2f3713f 100644
--- a/packages/app/src/js/store/modules/messages.js
+++ b/packages/app/src/js/store/slices/messages.ts
@@ -1,16 +1,26 @@
-import {createModule} from '../tools';
+import { Message as MessageType } from '../../types';
+import { PayloadAction } from '../types';
+import { createSlice } from '@reduxjs/toolkit';
+import { createMethods } from '../tools';
-export default createModule({
+type MessagesState = {
+ data: MessageType[];
+ loading: boolean;
+ status: 'live' | 'archived';
+ hovered: string | null;
+};
+
+const slice = createSlice({
name: 'messages',
initialState: {
data: [],
loading: false,
status: 'live',
hovered: null,
- },
+ } as MessagesState,
reducers: {
- hover: (state, action) => ({...state, hovered: action.payload}),
- setStatus: (state, action) => ({...state, status: action.payload}),
+ hover: (state, action: PayloadAction) => ({...state, hovered: action.payload ?? null}),
+ setStatus: (state, action) => ({...state, status: action.payload as 'live' | 'archived'}),
loadingFailed: (state, action) => ({...state, loadingFailed: action.payload}),
loading: (state) => ({...state, loading: true}),
loadingDone: (state) => ({...state, loading: false}),
@@ -95,9 +105,12 @@ export default createModule({
return state;
},
},
+});
+export const methods = createMethods({
+ module_name: 'messages',
methods: {
- load: (query) => async ({actions}, getState, { client }) => {
+ load: async (query, {dispatch: {actions}}, { client }) => {
const req = await client.req({
limit: 50,
...query,
@@ -106,13 +119,15 @@ export default createModule({
actions.messages.add(req.data);
return req.data;
},
- addReaction: (id, text) => async ({actions}, getState, { client }) => {
+ addReaction: async (args, {dispatch: {actions}}, { client }) => {
const req = await client.req({
type: 'message:react',
- id,
- reaction: text.trim(),
+ id: args.id,
+ reaction: args.text.trim(),
});
actions.messages.add(req.data);
},
- },
+ }
});
+
+export const { reducer, actions } = slice;
diff --git a/packages/app/src/js/store/modules/pins.js b/packages/app/src/js/store/slices/pins.ts
similarity index 74%
rename from packages/app/src/js/store/modules/pins.js
rename to packages/app/src/js/store/slices/pins.ts
index 0dc954af..93352647 100644
--- a/packages/app/src/js/store/modules/pins.js
+++ b/packages/app/src/js/store/slices/pins.ts
@@ -1,8 +1,14 @@
-import {createModule} from '../tools';
+import { createSlice } from '@reduxjs/toolkit';
+import { Message } from '../../types';
+import { createMethods } from '../tools';
-export default createModule({
+type PinsState = {
+ [channelId: string]: Message[];
+};
+
+const slice = createSlice({
name: 'pins',
- initialState: {},
+ initialState: {} as PinsState,
reducers: {
add: (state, action) => {
const newState = { ...state };
@@ -32,8 +38,12 @@ export default createModule({
return newState;
},
},
+});
+
+export const methods = createMethods({
+ module_name: 'pins',
methods: {
- load: (channelId) => async ({actions}, getState, {client}) => {
+ load: async (channelId, {dispatch: {actions}}, {client}) => {
actions.pins.clear(channelId);
const req = await client.req({
type: 'message:pins',
@@ -42,7 +52,7 @@ export default createModule({
});
actions.pins.add(req.data);
},
- pin: (id, channelId) => async ({actions, methods}, getState, {client}) => {
+ pin: async ({id, channelId}, {dispatch: {actions, methods}}, {client}) => {
const req = await client.req({
type: 'message:pin',
channelId,
@@ -53,7 +63,7 @@ export default createModule({
await methods.pins.load(channelId);
},
- unpin: (id, channelId) => async ({actions, methods}, getState, {client}) => {
+ unpin: async ({id, channelId}, {dispatch: {actions, methods}}, {client}) => {
const req = await client.req({
type: 'message:pin',
channelId,
@@ -65,3 +75,6 @@ export default createModule({
},
},
});
+
+export const { reducer, actions } = slice;
+
diff --git a/packages/app/src/js/store/modules/progress.js b/packages/app/src/js/store/slices/progress.ts
similarity index 68%
rename from packages/app/src/js/store/modules/progress.js
rename to packages/app/src/js/store/slices/progress.ts
index 1c9a9a7f..968fd550 100644
--- a/packages/app/src/js/store/modules/progress.js
+++ b/packages/app/src/js/store/slices/progress.ts
@@ -1,8 +1,15 @@
-import {createModule} from '../tools';
+import { createSlice } from "@reduxjs/toolkit";
+import { createMethods } from "../tools";
-export default createModule({
+type ProgressState = {
+ channelId: string;
+ userId: string;
+ parentId: string;
+};
+
+const slice = createSlice({
name: 'progress',
- initialState: [],
+ initialState: [] as ProgressState[],
reducers: {
add: (state, action) => {
const newState = [...state];
@@ -18,15 +25,19 @@ export default createModule({
return newState;
},
},
+});
+
+export const methods = createMethods({
+ module_name: 'progress',
methods: {
- loadBadges: () => async ({actions}, getState, {client}) => {
+ loadBadges: async (_arg, {dispatch: {actions}}, {client}) => {
const { data } = await client.req({
type: 'readReceipt:getOwn',
});
actions.progress.add(data);
},
- loadProgress: (stream) => async ({actions}, getState, {client}) => {
+ loadProgress: async (stream, {dispatch: {actions}}, {client}) => {
try{
if (!stream.channelId) return;
const { data } = await client.req({
@@ -41,7 +52,7 @@ export default createModule({
}
},
- update: (messageId) => async ({actions}, getState, {client}) => {
+ update: async (messageId, {dispatch: {actions}}, {client}) => {
const { data } = await client.req({
type: 'readReceipt:update',
messageId,
@@ -50,3 +61,5 @@ export default createModule({
},
},
});
+
+export const { reducer, actions } = slice;
diff --git a/packages/app/src/js/store/modules/search.js b/packages/app/src/js/store/slices/search.ts
similarity index 50%
rename from packages/app/src/js/store/modules/search.js
rename to packages/app/src/js/store/slices/search.ts
index cae6a079..edc83ac3 100644
--- a/packages/app/src/js/store/modules/search.js
+++ b/packages/app/src/js/store/slices/search.ts
@@ -1,17 +1,30 @@
-import {createModule} from '../tools';
+import { createSlice } from "@reduxjs/toolkit";
+import { createMethods } from "../tools";
+import { Message } from "../../types";
-export default createModule({
+type SearchState = {
+ results: Message[];
+ text: string;
+};
+
+const slice = createSlice({
name: 'search',
- initialState: {results: [], text: ''},
+ initialState: {results: [], text: ''} as SearchState,
reducers: {
push: (state, action) => ({...state, results: [action.payload, ...state.results.slice(0, 5)]}),
set: (state, action) => ({...state, text: action.payload}),
clear: () => ({results: [], text: ''}),
},
+});
+
+export const methods = createMethods({
+ module_name: 'search',
methods: {
- find: (channelId, text) => async ({actions}, getState, {client}) => {
+ find: async ({channelId, text}, {dispatch: {actions}}, {client}) => {
const data = await client.req({ type: 'message:search', channelId, text });
actions.search.push({ text, data: data.data, searchedAt: new Date().toISOString() });
},
},
});
+
+export const { reducer, actions } = slice;
diff --git a/packages/app/src/js/store/modules/stream.js b/packages/app/src/js/store/slices/stream.ts
similarity index 84%
rename from packages/app/src/js/store/modules/stream.js
rename to packages/app/src/js/store/slices/stream.ts
index aa74b4f5..c6a1eed6 100644
--- a/packages/app/src/js/store/modules/stream.js
+++ b/packages/app/src/js/store/slices/stream.ts
@@ -1,7 +1,6 @@
-import {createModule} from '../tools';
+import { createSlice } from '@reduxjs/toolkit';
import { omitUndefined } from '../../utils';
-/*
type Stream = {
channelId: string,
parentId: string,
@@ -10,7 +9,12 @@ type Stream = {
selected: string,
date: Date,
};
-*/
+
+type StreamState = {
+ main: Stream,
+ side: Stream,
+ mainChannelId: string,
+};
const loadStream = () => {
const { hash } = document.location;
@@ -32,7 +36,7 @@ const loadStream = () => {
};
};
-const saveStream = (stream) => {
+const saveStream = (stream: Stream) => {
const query = new URLSearchParams(omitUndefined({
type: stream.type,
date: stream.date,
@@ -45,9 +49,9 @@ const saveStream = (stream) => {
+ (querystring ? `?${querystring}` : '');
};
-export default createModule({
+const slice = createSlice({
name: 'stream',
- initialState: { main: loadStream(), side: null, mainChannelId: null },
+ initialState: { main: loadStream(), side: null, mainChannelId: null } as StreamState,
reducers: {
open: (state, action) => {
const { id, value } = action.payload;
@@ -64,3 +68,6 @@ export default createModule({
},
},
});
+
+export const methods = {};
+export const { actions, reducer } = slice;
diff --git a/packages/app/src/js/store/slices/system.ts b/packages/app/src/js/store/slices/system.ts
new file mode 100644
index 00000000..8c420793
--- /dev/null
+++ b/packages/app/src/js/store/slices/system.ts
@@ -0,0 +1,16 @@
+import { createSlice } from "@reduxjs/toolkit";
+
+type SystemState = {
+ initFailed: boolean;
+};
+
+const slice = createSlice({
+ name: 'system',
+ initialState: { initFailed: false } as SystemState,
+ reducers: {
+ initFailed: (state, action) => ({ ...state, initFailed: action.payload }),
+ },
+});
+
+export const methods = {};
+export const { actions, reducer } = slice;
diff --git a/packages/app/src/js/store/modules/typing.js b/packages/app/src/js/store/slices/typing.ts
similarity index 69%
rename from packages/app/src/js/store/modules/typing.js
rename to packages/app/src/js/store/slices/typing.ts
index 32723892..165768eb 100644
--- a/packages/app/src/js/store/modules/typing.js
+++ b/packages/app/src/js/store/slices/typing.ts
@@ -1,8 +1,18 @@
-import {createModule} from '../tools';
+import { createSlice } from "@reduxjs/toolkit";
+import { createMethods } from "../tools";
-export default createModule({
+type TypingState = {
+ [channelId: string]: {
+ [userId: string]: string;
+ };
+} & {
+ cooldown: boolean;
+ queue: boolean;
+};
+
+const slice = createSlice({
name: 'typing',
- initialState: { cooldown: false, queue: false },
+ initialState: { cooldown: false, queue: false } as TypingState,
reducers: {
add: (state, action) => {
const newState = {...state};
@@ -12,7 +22,7 @@ export default createModule({
});
return newState;
},
- clear: (state) => ({
+ clear: (state, _action) => ({
...Object.fromEntries(
Object.entries(state)
.map(([channelId, users]) => [
@@ -28,14 +38,18 @@ export default createModule({
}),
set: (state, action) => ({...state, ...action.payload}),
},
+});
+
+export const methods = createMethods({
+ module_name: 'typing',
methods: {
- ack: (msg) => (dispatch, getState) => {
+ ack: async (msg, {dispatch, getState}) => {
const meId = getState().me;
if (msg.userId === meId) return;
dispatch.actions.typing.add(msg);
setTimeout(() => dispatch.actions.typing.clear(), 1100);
},
- notify: ({channelId, parentId}) => ({ actions, methods }, getState, {client}) => {
+ notify: () => ({channelId, parentId}, {dispatch: { actions, methods }, getState}, {client}) => {
const {cooldown} = getState().typing;
if (cooldown) {
actions.typing.set({ queue: true });
@@ -52,3 +66,5 @@ export default createModule({
},
},
});
+
+export const { reducer, actions } = slice;
diff --git a/packages/app/src/js/store/slices/users.ts b/packages/app/src/js/store/slices/users.ts
new file mode 100644
index 00000000..3209c6b0
--- /dev/null
+++ b/packages/app/src/js/store/slices/users.ts
@@ -0,0 +1,35 @@
+import { createSlice } from '@reduxjs/toolkit';
+import { createMethods } from '../tools';
+
+type User = {
+ id: string;
+ name: string;
+ email: string;
+ avatarUrl: string;
+};
+
+const slice = createSlice({
+ name: 'users',
+ initialState: {} as Record,
+ reducers: {
+ add: (state, action) => {
+ const newState = {...state};
+ ([action.payload] as User[]).flat().forEach((user) => {
+ newState[user.id] = Object.assign(newState[user.id] || {}, user);
+ });
+ return newState;
+ },
+ },
+});
+
+export const methods = createMethods({
+ module_name: 'users',
+ methods: {
+ load: async (_arg, {dispatch: {actions}}, {client}) => {
+ const res = await client.req({ type: 'user:getAll' });
+ actions.users.add(res.data);
+ },
+ },
+});
+
+export const { reducer, actions } = slice;
diff --git a/packages/app/src/js/store/slices/view.ts b/packages/app/src/js/store/slices/view.ts
new file mode 100644
index 00000000..f75467e3
--- /dev/null
+++ b/packages/app/src/js/store/slices/view.ts
@@ -0,0 +1,23 @@
+import { createSlice } from "@reduxjs/toolkit";
+
+type ViewState = {
+ current: string | null;
+};
+
+const slice = createSlice({
+ name: 'view',
+ initialState: {current: null} as ViewState,
+ reducers: {
+ set: (state, action) => {
+ const view = action.payload;
+ if (state.current === view) {
+ return {current: null};
+ }
+ return {current: view};
+ },
+ },
+});
+
+
+export const methods = {};
+export const { actions, reducer } = slice;
diff --git a/packages/app/src/js/store/store.ts b/packages/app/src/js/store/store.ts
new file mode 100644
index 00000000..ed4b6338
--- /dev/null
+++ b/packages/app/src/js/store/store.ts
@@ -0,0 +1,70 @@
+import {
+ Middleware, Dispatch, Action,
+} from 'redux';
+import { configureStore } from '@reduxjs/toolkit'
+import { client } from '../core';
+import * as slices from './slices';
+import { ActionCreator, AsyncAction, AsyncHandler, PayloadAction, Reducer } from './types';
+console.log(slices);
+
+
+const isAsyncAction = (action: unknown): action is AsyncAction => (action as Action)?.type === 'async';
+
+const middleware: Middleware = ({ dispatch, getState }) => (next) => async (action) => {
+ // FIXME: remove this after migrating
+ if (typeof action == 'function') {
+ (dispatch as any).actions = actions;
+ (dispatch as any).methods = methods;
+ try {
+ return await action(dispatch, getState, {client});
+ } catch (err) {
+ // eslint-disable-next-line no-console
+ console.error(err);
+ return;
+ }
+ }
+ if (isAsyncAction(action)) {
+ const { handler } = action;
+ (dispatch as any).actions = actions;
+ (dispatch as any).methods = methods;
+ try {
+ return await handler(dispatch, getState, {client});
+ } catch (err) {
+ // eslint-disable-next-line no-console
+ console.error(err);
+ return;
+ }
+ }
+
+ return next(action);
+};
+
+export const store = configureStore({
+ reducer: slices.reducers,
+ middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(middleware),
+});
+
+function remap(input: T): {[module: string]: {[key: string]: (data: unknown) => void}} {
+ return Object.keys(input)
+ .reduce void}}>>((acc, module: string) => {
+ const mod = input[module];
+ acc[module] = Object.keys(mod)
+ .reduce<{[key: string]: (data: unknown) => void}>((acc2, action: string) => {
+ acc2[action] = async (data) => await store.dispatch(mod[action](data)).unwrap();
+ return acc2;
+ }, {});
+ return acc;
+ }, {}) as {[module: string]: {[key: string]: (data: unknown) => void}};
+}
+
+export const actions = remap(slices.actions);
+export const methods = remap(slices.methods);
+console.log(actions, methods);
+
+export type StateType = typeof slices.reducer extends Reducer ? S : never;
+export type DispatchType = Dispatch
+ & { methods: typeof methods, actions: typeof actions };
+export type StoreType = typeof store & { dispatch: DispatchType };
+
+
+export const createAsyncAction = (handler: AsyncHandler) => ({ type: 'async', handler });
diff --git a/packages/app/src/js/store/tools.js b/packages/app/src/js/store/tools.js
deleted file mode 100644
index 5d1a7463..00000000
--- a/packages/app/src/js/store/tools.js
+++ /dev/null
@@ -1,16 +0,0 @@
-
-export const createModule = (def) => {
- def.reducers = { ...def.reducers, reset: () => def.initialState };
- return ({
- actions: Object.keys(def.reducers).reduce((acc, key) => {
- acc[key] = (data) => ({ type: `${def.name}/${key}`, payload: data });
- return acc;
- }, {}),
- methods: def.methods || {},
- reducer: (state = def.initialState, action) => {
- if (!action.type.startsWith(def.name)) return state;
- const reducer = def.reducers[action.type.split('/').slice(-1)[0]];
- return reducer ? reducer(state, action) : state;
- },
- });
-}
diff --git a/packages/app/src/js/store/tools.ts b/packages/app/src/js/store/tools.ts
new file mode 100644
index 00000000..1559db31
--- /dev/null
+++ b/packages/app/src/js/store/tools.ts
@@ -0,0 +1,15 @@
+import { createAsyncThunk } from '@reduxjs/toolkit';
+
+type MethodsDef = {
+ module_name: string;
+ methods: {
+ [key: string]: (args: any, { dispatch, getState }: { dispatch: D, getState: () => S }, api: API) => Promise;
+ }
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export const createMethods = >(def: T) => {
+ return Object.fromEntries(Object.entries(def.methods).map(([key, val]) => {
+ return [key, createAsyncThunk(`${def.module_name}/${key}`, (arg: any, thunkApi: any) => val(arg, thunkApi, {}))];
+ }));
+}
diff --git a/packages/app/src/js/store/types.ts b/packages/app/src/js/store/types.ts
new file mode 100644
index 00000000..61b1a59f
--- /dev/null
+++ b/packages/app/src/js/store/types.ts
@@ -0,0 +1,38 @@
+import { Action, ActionCreator as ActionCreatorPrev, Reducer } from 'redux';
+
+export type { Action, Reducer };
+
+export type AsyncHandler< S = unknown, D = unknown, API = unknown, R = unknown> = (dispatch: D, getState: () => S, api: API) => Promise;
+export type AsyncAction = {type: 'async', handler: AsyncHandler};
+export type PayloadAction = { type: string, payload?: T };
+
+export type ActionCreator = (args?: P) => PayloadAction;
+
+export type Method< S = unknown, D = unknown, API = unknown> = (...args: any) => (dispatch: D, getState: () => S, api: API) => Promise;
+
+// eslint-disable-next-line @typescript-eslint/ban-types
+export type Module; } = {}> = {
+ reducer: Reducer;
+ actions: Actions,
+ methods: M;
+};
+
+export type Reducers = {
+ [key: string]: (state: T, action: PayloadAction) => T
+};
+export type Methods = { [key: string]: (...args: any) => (dispatch: any, getState: () => any, api: any) => Promise };
+
+
+export type Actions = { [K in keyof T]: ActionCreator };
+
+export type StateFromReducer = T extends Reducer ? S : never;
+
+export type CombinedState> = { [K in keyof T]: (T[K]['reducer'] extends Reducer ? S : never)};
+
+export type CombinedModule> = {
+ actions: { [K in keyof T]: T[K]['actions'] };
+ methods: { [K in keyof T]: T[K]['methods'] };
+ reducer: Reducer, PayloadAction>;
+};
+
+
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index dfe13c7a..cfe80fb7 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -72,6 +72,9 @@ importers:
'@quack/rpc':
specifier: workspace:*
version: link:../rpc
+ '@reduxjs/toolkit':
+ specifier: 2.2.3
+ version: 2.2.3(react-redux@9.1.0)(react@18.2.0)
fuse.js:
specifier: 7.0.0
version: 7.0.0
@@ -85,8 +88,8 @@ importers:
specifier: 18.2.0
version: 18.2.0(react@18.2.0)
react-redux:
- specifier: 9.0.4
- version: 9.0.4(@types/react@18.2.48)(react@18.2.0)(redux@5.0.0)
+ specifier: 9.1.0
+ version: 9.1.0(@types/react@18.2.48)(react@18.2.0)(redux@5.0.0)
redux:
specifier: 5.0.0
version: 5.0.0
@@ -2608,6 +2611,25 @@ packages:
resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==}
dev: false
+ /@reduxjs/toolkit@2.2.3(react-redux@9.1.0)(react@18.2.0):
+ resolution: {integrity: sha512-76dll9EnJXg4EVcI5YNxZA/9hSAmZsFqzMmNRHvIlzw2WS/twfcVX3ysYrWGJMClwEmChQFC4yRq74tn6fdzRA==}
+ peerDependencies:
+ react: ^16.9.0 || ^17.0.0 || ^18
+ react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0
+ peerDependenciesMeta:
+ react:
+ optional: true
+ react-redux:
+ optional: true
+ dependencies:
+ immer: 10.0.4
+ react: 18.2.0
+ react-redux: 9.1.0(@types/react@18.2.48)(react@18.2.0)(redux@5.0.0)
+ redux: 5.0.1
+ redux-thunk: 3.1.0(redux@5.0.1)
+ reselect: 5.1.0
+ dev: false
+
/@rollup/plugin-babel@5.3.1(@babel/core@7.23.6)(rollup@2.79.1):
resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==}
engines: {node: '>= 10.0.0'}
@@ -6062,6 +6084,10 @@ packages:
engines: {node: '>= 4'}
dev: true
+ /immer@10.0.4:
+ resolution: {integrity: sha512-cuBuGK40P/sk5IzWa9QPUaAdvPHjkk1c+xYsd9oZw+YQQEV+10G0P5uMpGctZZKnyQ+ibRO08bD25nWLmYi2pw==}
+ dev: false
+
/import-fresh@3.3.0:
resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
engines: {node: '>=6'}
@@ -7775,8 +7801,8 @@ packages:
/react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
- /react-redux@9.0.4(@types/react@18.2.48)(react@18.2.0)(redux@5.0.0):
- resolution: {integrity: sha512-9J1xh8sWO0vYq2sCxK2My/QO7MzUMRi3rpiILP/+tDr8krBHixC6JMM17fMK88+Oh3e4Ae6/sHIhNBgkUivwFA==}
+ /react-redux@9.1.0(@types/react@18.2.48)(react@18.2.0)(redux@5.0.0):
+ resolution: {integrity: sha512-6qoDzIO+gbrza8h3hjMA9aq4nwVFCKFtY2iLxCtVT38Swyy2C/dJCGBXHeHLtx6qlg/8qzc2MrhOeduf5K32wQ==}
peerDependencies:
'@types/react': ^18.2.25
react: ^18.0
@@ -7860,10 +7886,22 @@ packages:
dependencies:
resolve: 1.22.8
+ /redux-thunk@3.1.0(redux@5.0.1):
+ resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==}
+ peerDependencies:
+ redux: ^5.0.0
+ dependencies:
+ redux: 5.0.1
+ dev: false
+
/redux@5.0.0:
resolution: {integrity: sha512-blLIYmYetpZMET6Q6uCY7Jtl/Im5OBldy+vNPauA8vvsdqyt66oep4EUpAMWNHauTC6xa9JuRPhRB72rY82QGA==}
dev: false
+ /redux@5.0.1:
+ resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==}
+ dev: false
+
/reflect.getprototypeof@1.0.4:
resolution: {integrity: sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==}
engines: {node: '>= 0.4'}
@@ -7953,6 +7991,10 @@ packages:
resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
dev: false
+ /reselect@5.1.0:
+ resolution: {integrity: sha512-aw7jcGLDpSgNDyWBQLv2cedml85qd95/iszJjN988zX1t7AVRJi19d9kto5+W7oCfQ94gyo40dVbT6g2k4/kXg==}
+ dev: false
+
/resolve-cwd@3.0.0:
resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==}
engines: {node: '>=8'}