flow: good work

This commit is contained in:
cha0s 2020-12-08 02:24:47 -06:00
parent 96dd0fce9c
commit ddec5d2283
84 changed files with 36577 additions and 66 deletions

0
TODO.txt Normal file
View File

View File

@ -2,6 +2,7 @@ const config = {
globals: {
__non_webpack_require__: true,
process: true,
window: true,
},
rules: {
'babel/object-curly-spacing': 'off',

View File

@ -1,6 +1,6 @@
require('dotenv/config');
const airbnbBase = require('@neutrinojs/airbnb-base');
const airbnb = require('@neutrinojs/airbnb');
const clean = require('@neutrinojs/clean');
const mocha = require('@neutrinojs/mocha');
const node = require('@neutrinojs/node');
@ -12,7 +12,7 @@ module.exports = {
root: __dirname,
},
use: [
airbnbBase({
airbnb({
eslint: {
cache: false,
baseConfig: require('./.eslint.defaults'),
@ -24,10 +24,6 @@ module.exports = {
mocha(),
node(),
(neutrino) => {
neutrino.config.resolve.modules
.add(neutrino.options.source);
neutrino.config.resolve.modules
.add(`${neutrino.options.source}/../node_modules`);
if (process.env.LATUS_LINTING) {
return;
}

View File

@ -6,6 +6,7 @@
"build": "webpack --mode production",
"dev": "webpack --mode development",
"forcelatus": "pkgs=$(find node_modules/@latus -maxdepth 1 -mindepth 1 -printf '@latus/%f '); yarn upgrade $pkgs",
"forcereddichat": "pkgs=$(find node_modules/@reddichat -maxdepth 1 -mindepth 1 -printf '@reddichat/%f '); yarn upgrade $pkgs",
"lint": "eslint --cache --format codeframe --ext mjs,jsx,js src",
"repl": "rlwrap -C qmp socat STDIO UNIX:$(ls /tmp/latus-*.sock | tail -n 1)",
"start": "NODE_ENV=production node build/index.js",
@ -22,14 +23,23 @@
"@latus/repl": "^1.0.0",
"@latus/socket": "^1.0.0",
"@latus/user": "^1.0.0",
"@reddichat/app": "^1.0.0",
"@reddichat/chat": "^1.0.0",
"@reddichat/state": "^1.0.0",
"@reddichat/user": "^1.0.0",
"@reduxjs/toolkit": "^1.5.0",
"classnames": "^2.2.6",
"connected-react-router": "^6.8.0",
"deepmerge": "^4.2.2",
"dotenv": "8.2.0",
"history": "^4.7.2",
"react": "^17.0.1",
"react-hot-loader": "4.13.0",
"react-redux": "^7.2.2",
"react-router-dom": "^5.2.0"
},
"devDependencies": {
"@neutrinojs/airbnb-base": "^9.1.0",
"@neutrinojs/airbnb": "^9.1.0",
"@neutrinojs/clean": "^9.1.0",
"@neutrinojs/mocha": "^9.1.0",
"@neutrinojs/node": "^9.1.0",

View File

@ -7,7 +7,9 @@ const About = () => (
<div className="about">
<h1 className="about__title">
<span className="about__titleMessage">
<span class="about__nowrap">Hi!</span> <span class="about__nowrap about__smile">^_^</span>
<span className="about__nowrap">Hi!</span>
{' '}
<span className="about__nowrap about__smile">^_^</span>
{' '}
<img
alt="cha0s's icon"

View File

@ -1,6 +1,8 @@
import './index.scss';
import {isAnonymousSelector} from '@reddichat/user/client';
import React from 'react';
import {useSelector} from 'react-redux';
import {hot} from 'react-hot-loader';
import {Route, Router, Switch} from 'react-router-dom';
import {createBrowserHistory} from 'history';
@ -13,9 +15,11 @@ import Right from 'components/right';
const history = createBrowserHistory();
const App = () => (
const App = () => {
const isAnonymous = useSelector(isAnonymousSelector);
return (
<div className="app">
<Left />
{!isAnonymous && <Left />}
<Router history={history}>
<Switch>
<Route
@ -33,8 +37,9 @@ const App = () => (
/>
</Switch>
</Router>
<Right />
{!isAnonymous && <Right />}
</div>
);
)
};
export default hot(module)(App);

3
app/src/react/history.js Normal file
View File

@ -0,0 +1,3 @@
import {createBrowserHistory} from 'history';
export default createBrowserHistory();

View File

@ -1,9 +1,32 @@
import './index.scss';
import React from 'react';
import {ConnectedRouter} from 'connected-react-router';
import {Provider} from 'react-redux';
import App from 'components/app';
import {configureStore} from '@reddichat/state/client';
import history from './history';
const Index = async (latus) => {
const store = await configureStore(latus, {history});
return () => (
<Provider store={store}>
<ConnectedRouter history={history}>
{/* <Dispatcher> */}
<App />
{/* <Unread /> */}
{/* </Dispatcher> */}
</ConnectedRouter>
</Provider>
);
};
export default {
hooks: {
'@latus/react/components': () => App,
'@latus/react/components': (latus) => Index(latus),
},
};

View File

@ -16,11 +16,12 @@ else {
const config = readConfig();
const paths = Object.entries(config).map(([plugin]) => {
try {
require.resolve(plugin);
return plugin;
const local = join(process.cwd(), 'src', plugin);
require.resolve(local);
return local;
}
catch (error) {
return join(process.cwd(), plugin);
return plugin;
}
});
const latus = new Latus({

View File

@ -871,7 +871,15 @@
pirates "^4.0.0"
source-map-support "^0.5.16"
"@babel/runtime@^7.1.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.8.4":
"@babel/runtime-corejs3@^7.10.2":
version "7.12.5"
resolved "https://npm.i12e.cha0s.io/@babel%2fruntime-corejs3/-/runtime-corejs3-7.12.5.tgz#ffee91da0eb4c6dae080774e94ba606368e414f4"
integrity sha512-roGr54CsTmNPPzZoCP1AmDXuBoNao7tnSA83TXTwt+UK5QVyh1DIJnrgYRPWKCF2flqZQXwa7Yr8v7VmLzF0YQ==
dependencies:
core-js-pure "^3.0.0"
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.8.4":
version "7.12.5"
resolved "https://npm.i12e.cha0s.io/@babel%2fruntime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e"
integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==
@ -913,8 +921,8 @@
"@latus/core@1.0.0", "@latus/core@^1.0.0":
version "1.0.0"
resolved "https://npm.i12e.cha0s.io/@latus%2fcore/-/core-1.0.0.tgz#919a3e6d8ddbe7da80c512f11c2d0e183616c994"
integrity sha512-yowxOdYxAfnme4PCxrUoIpkDd32RbAmG72FJlxYcTsd3Vntjz2IEH6WlqOj+1rO+0OfyffrB7ORu7FGt51TqRQ==
resolved "https://npm.i12e.cha0s.io/@latus%2fcore/-/core-1.0.0.tgz#01f0121bb91579b8bd1bdadff8dabf8673f44a88"
integrity sha512-NK7DuYoVIXmFEjazKi44vW335DFcE1pr62DXf9+9bZ8iyexptFBdfX/5sYFMGaeJlN/opFp0qFx94VPdVCsChg==
dependencies:
debug "4.3.1"
js-yaml "3.14.0"
@ -933,8 +941,8 @@
"@latus/governor@^1.0.0":
version "1.0.0"
resolved "https://npm.i12e.cha0s.io/@latus%2fgovernor/-/governor-1.0.0.tgz#e0b2f45349bda2a652755f80056960647fe090c2"
integrity sha512-bpdCFSW+73cqEWJZf91q8c8s5Nq32bqhlrZRRlDjQ3asqspzlrU9RAxLyS1kvSJ1NrM0S7s53AMuFksV4cMgBQ==
resolved "https://npm.i12e.cha0s.io/@latus%2fgovernor/-/governor-1.0.0.tgz#7cbcc3ae04aaf7d26e78a94dd3306a3dc16b18a1"
integrity sha512-gZB30h/HgvyS0PWtAJaW4OpnlRJUpMcyDzDnCmuWi+zdr8EWYq7cwUOpWhD4TKIIviFYuEwvhaRzmg3c8rQM8A==
dependencies:
"@latus/redis" "^1.0.0"
"@latus/socket" "^1.0.0"
@ -943,8 +951,8 @@
"@latus/http@^1.0.0":
version "1.0.0"
resolved "https://npm.i12e.cha0s.io/@latus%2fhttp/-/http-1.0.0.tgz#4131aca03a7d7e4f66e54deaf4cf5edf94503137"
integrity sha512-BYGQwIglwo9/BTaU1DNF1ALxhxYbjhXtd1qPmNdqrLZXJVHg4MhpRLEl/L5SS3g8AkLO9e3CjXvseQBZMHBh8g==
resolved "https://npm.i12e.cha0s.io/@latus%2fhttp/-/http-1.0.0.tgz#ed08c6c85112ce4d66a652f5810a220a66a0a1b3"
integrity sha512-1qSwE1vX0bg4nFJFrPjFZT1PepDuDT9RIZRt3SzpvT5OsmE+6LjxtqTbMsgpljnHuhwjWPzDnR6An7sUhR6A0A==
dependencies:
"@latus/core" "1.0.0"
"@neutrinojs/web" "^9.1.0"
@ -960,8 +968,8 @@
"@latus/react@^1.0.0":
version "1.0.0"
resolved "https://npm.i12e.cha0s.io/@latus%2freact/-/react-1.0.0.tgz#dea047ecac755f95d503e552163c25cb9e138f7f"
integrity sha512-FK2P7L9H6OfwmS7yci9PbFbhon9m4+jHkcSsxAqLm3+5CgmMQmmVFqhkLBKPzumqwwV49579Fl2cooEH6G7k3A==
resolved "https://npm.i12e.cha0s.io/@latus%2freact/-/react-1.0.0.tgz#e68c8cd7015725c17a2aa18aebff52b39bbecca5"
integrity sha512-ywXWx21xcJvVfp67Mj6rIaw4p9wepDDmRF0zzhyaJbhAu3weJ5oK9yd3IwWEYONtc/lzblak+3/fEaK+GFM01Q==
dependencies:
"@neutrinojs/react" "^9.4.0"
debug "4.3.1"
@ -993,8 +1001,8 @@
"@latus/socket@^1.0.0":
version "1.0.0"
resolved "https://npm.i12e.cha0s.io/@latus%2fsocket/-/socket-1.0.0.tgz#751037f2a2a2ea51618060ad3d15b86e9399ecdb"
integrity sha512-/EiO7DVuYM0Iw0RqKP/sYpvyBA7Z8nrrToALtpqSWZaZ3gHrsP/9u6sflDgbL8RUlFrRPZcpJBDVmOMF1mUAVw==
resolved "https://npm.i12e.cha0s.io/@latus%2fsocket/-/socket-1.0.0.tgz#75e95bb522fbec52d94d72ae4a33dab75e47cb5e"
integrity sha512-UGmRxwkL1ZKT3oZUnQlsJSJOKAlJeaxdVS0VDTo1HBMneYP0HwoMeh7cJ7wWzV34Uj7/FhG6PWwwTz3wFz/c3A==
dependencies:
"@latus/core" "^1.0.0"
"@latus/http" "^1.0.0"
@ -1019,14 +1027,18 @@
passport "0.4.1"
passport-local "^1.0.0"
"@neutrinojs/airbnb-base@^9.1.0":
"@neutrinojs/airbnb@^9.1.0":
version "9.4.0"
resolved "https://npm.i12e.cha0s.io/@neutrinojs%2fairbnb-base/-/airbnb-base-9.4.0.tgz#20af4c27ee7b8ec520b7bb781ca79d1ee118034f"
integrity sha512-CYZ1dNhIzjEcje3cIHS0zrZ+2kjoDViuUhFOBPn6eGrOQp1G3l5E+QwhNhJii66a009dtUXuUesxfrlgr9H8vw==
resolved "https://npm.i12e.cha0s.io/@neutrinojs%2fairbnb/-/airbnb-9.4.0.tgz#de7dc8413e4332e95ae4ba7506b81661191f2493"
integrity sha512-EjV/B906d1yRHKu1DJilS6cNejNjCvSf3dfHxYqe5tldMVZTlgE57+PVSG57RDvdoxq8zRMQwmBAbgn7keoh6w==
dependencies:
"@neutrinojs/eslint" "9.4.0"
eslint-config-airbnb "^18.2.0"
eslint-config-airbnb-base "^14.2.0"
eslint-plugin-import "^2.22.0"
eslint-plugin-jsx-a11y "^6.3.1"
eslint-plugin-react "^7.20.6"
eslint-plugin-react-hooks "^4.1.0"
"@neutrinojs/banner@9.4.0":
version "9.4.0"
@ -1170,6 +1182,69 @@
babel-merge "^3.0.0"
deepmerge "^1.5.2"
"@reddichat/app@^1.0.0":
version "1.0.0"
resolved "https://npm.i12e.cha0s.io/@reddichat%2fapp/-/app-1.0.0.tgz#594d87b240e9635b7503c0f665d36d0c9c55cee6"
integrity sha512-QEPIw4WSOfsspHlXeZSRbf1vKSazkySn8RWlhZh+wvyRgK+5RLyYCQdYn1VizL3mykKAsHCbXEkjHAt0B/TV3Q==
dependencies:
"@reddichat/state" "^1.0.0"
"@reduxjs/toolkit" "^1.5.0"
connected-react-router "^6.8.0"
debug "4.3.1"
"@reddichat/chat@^1.0.0":
version "1.0.0"
resolved "https://npm.i12e.cha0s.io/@reddichat%2fchat/-/chat-1.0.0.tgz#007e210ef28021c4c5846f7d686ba83ad875f248"
integrity sha512-WR32qj/o/TwFcGiqFxIGzxwjRTfReTUneu5aPkGwxVomUg/fjUhxhfEVgtEFR/FMPooF6eB+yr2YlCneHHvtcg==
dependencies:
"@latus/core" "^1.0.0"
"@latus/db" "^1.0.0"
"@latus/governor" "^1.0.0"
"@latus/socket" "^1.0.0"
"@reddichat/core" "^1.0.0"
"@reddichat/state" "^1.0.0"
"@reduxjs/toolkit" "^1.5.0"
debug "4.3.1"
uuid "^8.3.1"
"@reddichat/core@^1.0.0":
version "1.0.0"
resolved "https://npm.i12e.cha0s.io/@reddichat%2fcore/-/core-1.0.0.tgz#f8f7b5350bbfb985697144681e6f8509545be745"
integrity sha512-1f03IIacxnzYrkCJZlQRrBX2BPiHjGdeo+PCOxgfqbmgp3xUANdfGY4G2hXzNy+WNE8+pH4xdZ6UEXqtFq9cmw==
dependencies:
debug "4.3.1"
"@reddichat/state@^1.0.0":
version "1.0.0"
resolved "https://npm.i12e.cha0s.io/@reddichat%2fstate/-/state-1.0.0.tgz#88695971edef8996dc243cec96dad2793b82a961"
integrity sha512-jtIGiO71Cqt/ZZtrdmCEHf/6k2EMjB2baklC7HC7MafbQKxk014ZikwvVtm74OAFapgv4qayRqOvw1gOjM91nw==
dependencies:
"@latus/core" "^1.0.0"
"@latus/db" "^1.0.0"
"@latus/redis" "^1.0.0"
"@reddichat/core" "^1.0.0"
"@reduxjs/toolkit" "^1.5.0"
connected-react-router "^6.8.0"
debug "4.3.1"
deepmerge "^4.2.2"
lodash.throttle "^4.1.1"
redux "^4.0.5"
"@reddichat/user@^1.0.0":
version "1.0.0"
resolved "https://npm.i12e.cha0s.io/@reddichat%2fuser/-/user-1.0.0.tgz#c13b89d14797b49cdc076d4dcc0a31b055a7e52b"
integrity sha512-Hik217AJ7Vu26I+ehLifHmLCN46W1yolKXB/FJFUfbP8Nz2ZtUpGrZNYGIgmND/4ZmuW1B4r7v6njBQPGioYgg==
dependencies:
"@latus/db" "^1.0.0"
"@latus/socket" "^1.0.0"
"@reddichat/chat" "^1.0.0"
"@reddichat/core" "^1.0.0"
"@reddichat/state" "^1.0.0"
"@reduxjs/toolkit" "^1.5.0"
connected-react-router "^6.8.0"
debug "4.3.1"
deepmerge "^4.2.2"
"@reduxjs/toolkit@^1.5.0":
version "1.5.0"
resolved "https://npm.i12e.cha0s.io/@reduxjs%2ftoolkit/-/toolkit-1.5.0.tgz#1025c1ccb224d1fc06d8d98a61f6717d57e6d477"
@ -1566,6 +1641,14 @@ argparse@^1.0.7:
dependencies:
sprintf-js "~1.0.2"
aria-query@^4.2.2:
version "4.2.2"
resolved "https://npm.i12e.cha0s.io/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b"
integrity sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==
dependencies:
"@babel/runtime" "^7.10.2"
"@babel/runtime-corejs3" "^7.10.2"
arr-diff@^4.0.0:
version "4.0.0"
resolved "https://npm.i12e.cha0s.io/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
@ -1688,6 +1771,11 @@ assign-symbols@^1.0.0:
resolved "https://npm.i12e.cha0s.io/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=
ast-types-flow@^0.0.7:
version "0.0.7"
resolved "https://npm.i12e.cha0s.io/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad"
integrity sha1-9wtzXGvKGlycItmCw+Oef+ujva0=
ast-types@0.9.6:
version "0.9.6"
resolved "https://npm.i12e.cha0s.io/ast-types/-/ast-types-0.9.6.tgz#102c9e9e9005d3e7e3829bf0c4fa24ee862ee9b9"
@ -1740,6 +1828,16 @@ aws4@^1.8.0:
resolved "https://npm.i12e.cha0s.io/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59"
integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==
axe-core@^4.0.2:
version "4.1.1"
resolved "https://npm.i12e.cha0s.io/axe-core/-/axe-core-4.1.1.tgz#70a7855888e287f7add66002211a423937063eaf"
integrity sha512-5Kgy8Cz6LPC9DJcNb3yjAXTu3XihQgEdnIg50c//zOC/MyLP0Clg+Y8Sh9ZjjnvBrDZU4DgXS9C3T9r4/scGZQ==
axobject-query@^2.2.0:
version "2.2.0"
resolved "https://npm.i12e.cha0s.io/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be"
integrity sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==
babel-eslint@^10.1.0:
version "10.1.0"
resolved "https://npm.i12e.cha0s.io/babel-eslint/-/babel-eslint-10.1.0.tgz#6968e568a910b78fb3779cdd8b6ac2f479943232"
@ -2514,6 +2612,13 @@ connect-redis@^5.0.0:
resolved "https://npm.i12e.cha0s.io/connect-redis/-/connect-redis-5.0.0.tgz#68fe890117e761ee98e13a14b835338bd6bf044c"
integrity sha512-R4nTW5uXeG5s6zr/q4abmtcdloglZrL/A3cpa0JU0RLFJU4mTR553HUY8OZ0ngeySkGDclwQ5xmCcjjKkxdOSg==
connected-react-router@^6.8.0:
version "6.8.0"
resolved "https://npm.i12e.cha0s.io/connected-react-router/-/connected-react-router-6.8.0.tgz#ddc687b31d498322445d235d660798489fa56cae"
integrity sha512-E64/6krdJM3Ag3MMmh2nKPtMbH15s3JQDuaYJvOVXzu6MbHbDyIvuwLOyhQIuP4Om9zqEfZYiVyflROibSsONg==
dependencies:
prop-types "^15.7.2"
console-browserify@^1.1.0:
version "1.2.0"
resolved "https://npm.i12e.cha0s.io/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336"
@ -2593,6 +2698,11 @@ core-js-compat@^3.7.0:
browserslist "^4.15.0"
semver "7.0.0"
core-js-pure@^3.0.0:
version "3.8.1"
resolved "https://npm.i12e.cha0s.io/core-js-pure/-/core-js-pure-3.8.1.tgz#23f84048f366fdfcf52d3fd1c68fec349177d119"
integrity sha512-Se+LaxqXlVXGvmexKGPvnUIYC1jwXu1H6Pkyb3uBM5d8/NELMYCHs/4/roD7721NxrTLyv7e5nXd5/QLBO+10g==
core-js@^2.4.0:
version "2.6.12"
resolved "https://npm.i12e.cha0s.io/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
@ -2732,6 +2842,11 @@ cyclist@^1.0.1:
resolved "https://npm.i12e.cha0s.io/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=
damerau-levenshtein@^1.0.6:
version "1.0.6"
resolved "https://npm.i12e.cha0s.io/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz#143c1641cb3d85c60c32329e26899adea8701791"
integrity sha512-JVrozIeElnj3QzfUIt8tB8YMluBJom4Vw9qTPpjGYQ9fYlB3D/rb6OordUxf3xeFB35LKWs0xqcO5U6ySvBtug==
dashdash@^1.12.0:
version "1.14.1"
resolved "https://npm.i12e.cha0s.io/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
@ -2816,6 +2931,11 @@ deepmerge@^2.2.1:
resolved "https://npm.i12e.cha0s.io/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170"
integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==
deepmerge@^4.2.2:
version "4.2.2"
resolved "https://npm.i12e.cha0s.io/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==
default-gateway@^4.2.0:
version "4.2.0"
resolved "https://npm.i12e.cha0s.io/default-gateway/-/default-gateway-4.2.0.tgz#167104c7500c2115f6dd69b0a536bb8ed720552b"
@ -3070,9 +3190,9 @@ ee-first@1.1.1:
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
electron-to-chromium@^1.3.612:
version "1.3.616"
resolved "https://npm.i12e.cha0s.io/electron-to-chromium/-/electron-to-chromium-1.3.616.tgz#de63d1c79bb8eb61168774df0c11c9e1af69f9e8"
integrity sha512-CI8L38UN2BEnqXw3/oRIQTmde0LiSeqWSRlPA42ZTYgJQ8fYenzAM2Z3ni+jtILTcrs5aiXZCGJ96Pm+3/yGyQ==
version "1.3.619"
resolved "https://npm.i12e.cha0s.io/electron-to-chromium/-/electron-to-chromium-1.3.619.tgz#4dc529ae802f5c9c31e7eea830144340539b62b4"
integrity sha512-WFGatwtk7Fw0QcKCZzfGD72hvbcXV8kLY8aFuj0Ip0QRnOtyLYMsc+wXbSjb2w4lk1gcAeNU1/lQ20A+tvuypQ==
elliptic@^6.5.3:
version "6.5.3"
@ -3097,6 +3217,11 @@ emoji-regex@^8.0.0:
resolved "https://npm.i12e.cha0s.io/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
emoji-regex@^9.0.0:
version "9.2.0"
resolved "https://npm.i12e.cha0s.io/emoji-regex/-/emoji-regex-9.2.0.tgz#a26da8e832b16a9753309f25e35e3c0efb9a066a"
integrity sha512-DNc3KFPK18bPdElMJnf/Pkv5TXhxFU3YFDEuGLDRtPmV4rkmCjBkCSEp22u6rBHdSN9Vlp/GK7k98prmE1Jgug==
emojis-list@^3.0.0:
version "3.0.0"
resolved "https://npm.i12e.cha0s.io/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
@ -3268,7 +3393,7 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
resolved "https://npm.i12e.cha0s.io/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
eslint-config-airbnb-base@^14.2.0:
eslint-config-airbnb-base@^14.2.0, eslint-config-airbnb-base@^14.2.1:
version "14.2.1"
resolved "https://npm.i12e.cha0s.io/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.2.1.tgz#8a2eb38455dc5a312550193b319cdaeef042cd1e"
integrity sha512-GOrQyDtVEc1Xy20U7vsB2yAoB4nBlfH5HZJeatRXHleO+OS5Ot+MWij4Dpltw4/DyIkqUfqz1epfhVR5XWWQPA==
@ -3277,6 +3402,15 @@ eslint-config-airbnb-base@^14.2.0:
object.assign "^4.1.2"
object.entries "^1.1.2"
eslint-config-airbnb@^18.2.0:
version "18.2.1"
resolved "https://npm.i12e.cha0s.io/eslint-config-airbnb/-/eslint-config-airbnb-18.2.1.tgz#b7fe2b42f9f8173e825b73c8014b592e449c98d9"
integrity sha512-glZNDEZ36VdlZWoxn/bUR1r/sdFKPd1mHPbqUtkctgNG4yT2DLLtJ3D+yCV+jzZCc2V1nBVkmdknOJBZ5Hc0fg==
dependencies:
eslint-config-airbnb-base "^14.2.1"
object.assign "^4.1.2"
object.entries "^1.1.2"
eslint-import-resolver-node@^0.3.4:
version "0.3.4"
resolved "https://npm.i12e.cha0s.io/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717"
@ -3346,6 +3480,23 @@ eslint-plugin-import@^2.22.0:
resolve "^1.17.0"
tsconfig-paths "^3.9.0"
eslint-plugin-jsx-a11y@^6.3.1:
version "6.4.1"
resolved "https://npm.i12e.cha0s.io/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.4.1.tgz#a2d84caa49756942f42f1ffab9002436391718fd"
integrity sha512-0rGPJBbwHoGNPU73/QCLP/vveMlM1b1Z9PponxO87jfr6tuH5ligXbDT6nHSSzBC8ovX2Z+BQu7Bk5D/Xgq9zg==
dependencies:
"@babel/runtime" "^7.11.2"
aria-query "^4.2.2"
array-includes "^3.1.1"
ast-types-flow "^0.0.7"
axe-core "^4.0.2"
axobject-query "^2.2.0"
damerau-levenshtein "^1.0.6"
emoji-regex "^9.0.0"
has "^1.0.3"
jsx-ast-utils "^3.1.0"
language-tags "^1.0.5"
eslint-plugin-react-hooks@^4.1.0:
version "4.2.0"
resolved "https://npm.i12e.cha0s.io/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.2.0.tgz#8c229c268d468956334c943bb45fc860280f5556"
@ -4250,7 +4401,7 @@ he@1.2.x, he@^1.2.0:
resolved "https://npm.i12e.cha0s.io/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
history@^4.9.0:
history@^4.7.2, history@^4.9.0:
version "4.10.1"
resolved "https://npm.i12e.cha0s.io/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3"
integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==
@ -4271,7 +4422,7 @@ hmac-drbg@^1.0.0:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0:
hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
version "3.3.2"
resolved "https://npm.i12e.cha0s.io/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@ -4952,7 +5103,7 @@ js-base64@^2.1.8:
resolved "https://npm.i12e.cha0s.io/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
js-yaml@3.14.0, js-yaml@^3.13.1:
js-yaml@3.14.0:
version "3.14.0"
resolved "https://npm.i12e.cha0s.io/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482"
integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==
@ -4960,6 +5111,14 @@ js-yaml@3.14.0, js-yaml@^3.13.1:
argparse "^1.0.7"
esprima "^4.0.0"
js-yaml@^3.13.1:
version "3.14.1"
resolved "https://npm.i12e.cha0s.io/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537"
integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==
dependencies:
argparse "^1.0.7"
esprima "^4.0.0"
jsbn@~0.1.0:
version "0.1.1"
resolved "https://npm.i12e.cha0s.io/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
@ -5036,7 +5195,7 @@ jsprim@^1.2.2:
json-schema "0.2.3"
verror "1.10.0"
"jsx-ast-utils@^2.4.1 || ^3.0.0":
"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.1.0:
version "3.1.0"
resolved "https://npm.i12e.cha0s.io/jsx-ast-utils/-/jsx-ast-utils-3.1.0.tgz#642f1d7b88aa6d7eb9d8f2210e166478444fa891"
integrity sha512-d4/UOjg+mxAWxCiF0c5UTSwyqbchkbqCvK87aBovhnh8GtysTjWmgC63tY0cJx/HzGgm9qnA147jVBdpOiQ2RA==
@ -5078,6 +5237,18 @@ klona@^2.0.4:
resolved "https://npm.i12e.cha0s.io/klona/-/klona-2.0.4.tgz#7bb1e3affb0cb8624547ef7e8f6708ea2e39dfc0"
integrity sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA==
language-subtag-registry@~0.3.2:
version "0.3.21"
resolved "https://npm.i12e.cha0s.io/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz#04ac218bea46f04cb039084602c6da9e788dd45a"
integrity sha512-L0IqwlIXjilBVVYKFT37X9Ih11Um5NEl9cbJIuU/SwP/zEEAbBPOnEeeuxVMf45ydWQRDQN3Nqc96OgbH1K+Pg==
language-tags@^1.0.5:
version "1.0.5"
resolved "https://npm.i12e.cha0s.io/language-tags/-/language-tags-1.0.5.tgz#d321dbc4da30ba8bf3024e040fa5c14661f9193a"
integrity sha1-0yHbxNowuovzAk4ED6XBRmH5GTo=
dependencies:
language-subtag-registry "~0.3.2"
levn@^0.3.0, levn@~0.3.0:
version "0.3.0"
resolved "https://npm.i12e.cha0s.io/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
@ -5188,6 +5359,11 @@ lodash.templatesettings@^4.0.0:
dependencies:
lodash._reinterpolate "^3.0.0"
lodash.throttle@^4.1.1:
version "4.1.1"
resolved "https://npm.i12e.cha0s.io/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4"
integrity sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ=
lodash@^4.0.0, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.4, lodash@~4.17.10:
version "4.17.20"
resolved "https://npm.i12e.cha0s.io/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
@ -6774,7 +6950,7 @@ react-hot-loader@4.13.0, react-hot-loader@^4.13.0:
shallowequal "^1.1.0"
source-map "^0.7.3"
react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1:
react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1:
version "16.13.1"
resolved "https://npm.i12e.cha0s.io/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@ -6784,6 +6960,17 @@ react-lifecycles-compat@^3.0.4:
resolved "https://npm.i12e.cha0s.io/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
react-redux@^7.2.2:
version "7.2.2"
resolved "https://npm.i12e.cha0s.io/react-redux/-/react-redux-7.2.2.tgz#03862e803a30b6b9ef8582dadcc810947f74b736"
integrity sha512-8+CQ1EvIVFkYL/vu6Olo7JFLWop1qRUeb46sGtIMDCSpgwPQq8fPLpirIB0iTqFe9XYEFPHssdX8/UwN6pAkEA==
dependencies:
"@babel/runtime" "^7.12.1"
hoist-non-react-statics "^3.3.2"
loose-envify "^1.4.0"
prop-types "^15.7.2"
react-is "^16.13.1"
react-router-dom@^5.2.0:
version "5.2.0"
resolved "https://npm.i12e.cha0s.io/react-router-dom/-/react-router-dom-5.2.0.tgz#9e65a4d0c45e13289e66c7b17c7e175d0ea15662"
@ -6943,7 +7130,7 @@ redux-thunk@^2.3.0:
resolved "https://npm.i12e.cha0s.io/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==
redux@^4.0.0:
redux@^4.0.0, redux@^4.0.5:
version "4.0.5"
resolved "https://npm.i12e.cha0s.io/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f"
integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==
@ -8485,7 +8672,7 @@ uuid@^3.3.2, uuid@^3.4.0:
resolved "https://npm.i12e.cha0s.io/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
uuid@^8.1.0:
uuid@^8.1.0, uuid@^8.3.1:
version "8.3.1"
resolved "https://npm.i12e.cha0s.io/uuid/-/uuid-8.3.1.tgz#2ba2e6ca000da60fce5a196954ab241131e05a31"
integrity sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==

View File

@ -3,4 +3,6 @@
// Neutrino's inspect feature can be used to view/export the generated configuration.
const neutrino = require('neutrino');
module.exports = neutrino(require(`${__dirname}/.neutrinorc`)).webpack();
const configOfConfigs = require(`${__dirname}/.neutrinorc`);
const configs = Array.isArray(configOfConfigs) ? configOfConfigs : [configOfConfigs];
module.exports = configs.map((config) => neutrino(config).webpack());

3
packages/app/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/*.js
/*.js.map
!/webpack.config.js

41
packages/app/package.json Normal file
View File

@ -0,0 +1,41 @@
{
"name": "@reddichat/app",
"version": "1.0.0",
"main": "index.js",
"author": "cha0s",
"license": "MIT",
"scripts": {
"build": "NODE_PATH=./node_modules webpack --mode production",
"clean": "rm -f yarn.lock && yarn",
"dev": "NODE_PATH=./node_modules webpack --mode development",
"forcepub": "npm unpublish --force $(node -e 'process.stdout.write(require(`./package.json`).name)') && npm publish",
"lint": "NODE_PATH=./node_modules eslint --format codeframe --ext mjs,js .",
"test": "NODE_PATH=./node_modules mocha --config ../../config/.mocharc.js",
"watch": "NODE_PATH=./node_modules webpack --watch --mode development"
},
"files": [
"client.js",
"client.js.map",
"index.js",
"index.js.map"
],
"dependencies": {
"@reddichat/state": "^1.0.0",
"@reduxjs/toolkit": "^1.5.0",
"connected-react-router": "^6.8.0",
"debug": "4.3.1"
},
"devDependencies": {
"@neutrinojs/airbnb-base": "^9.4.0",
"@neutrinojs/copy": "9.4.0",
"@neutrinojs/library": "^9.4.0",
"@neutrinojs/mocha": "^9.4.0",
"chai": "4.2.0",
"eslint": "^7",
"eslint-import-resolver-webpack": "0.13.0",
"mocha": "^8",
"neutrino": "^9.4.0",
"webpack": "^4",
"webpack-cli": "^3"
}
}

View File

@ -0,0 +1,16 @@
import {connectRouter} from 'connected-react-router';
import app from './state/app';
import historyState from './state/history';
export * from './state';
export default {
hooks: {
'@reddichat/state/reducers': ({history}) => ({
app,
history: historyState(history),
router: connectRouter(history),
}),
},
};

View File

@ -0,0 +1,63 @@
import {storage} from '@reddichat/state/client';
import {createSlice, createSelector} from '@reduxjs/toolkit';
export const appSelector = (state) => state.app;
export const leftActiveIndexSelector = createSelector(
appSelector,
(app) => app.leftActiveIndex,
);
export const rightActiveIndexSelector = createSelector(
appSelector,
(app) => app.rightActiveIndex,
);
export const leftIsOpenSelector = createSelector(
appSelector,
(app) => app.leftIsOpen,
);
export const rightIsOpenSelector = createSelector(
appSelector,
(app) => app.rightIsOpen,
);
const slice = createSlice({
name: 'reddichat/app',
initialState: {
leftActiveIndex: 0,
leftIsOpen: true,
rightActiveIndex: 0,
rightIsOpen: true,
...(storage(appSelector) || {}),
},
/* eslint-disable no-param-reassign */
extraReducers: {},
reducers: {
setLeftActiveIndex: (state, {payload: activeItem}) => {
state.leftActiveIndex = activeItem;
},
setRightActiveIndex: (state, {payload: activeItem}) => {
state.rightActiveIndex = activeItem;
},
toggleLeftIsOpen: (state) => {
state.leftIsOpen = !state.leftIsOpen;
},
toggleRightIsOpen: (state) => {
state.rightIsOpen = !state.rightIsOpen;
},
},
/* eslint-enable no-param-reassign */
});
slice.reducer.subscription = slice.reducer;
export const {
setLeftActiveIndex,
setRightActiveIndex,
toggleLeftIsOpen,
toggleRightIsOpen,
} = slice.actions;
export default slice.reducer;

View File

@ -0,0 +1,15 @@
/* eslint-disable no-param-reassign */
import {
createSelector,
} from '@reduxjs/toolkit';
export const historySelector = (state) => state.history;
export const historyLengthSelector = createSelector(
[historySelector],
({length}) => length,
);
export default (history) => () => ({
length: history.length,
});

View File

@ -0,0 +1,2 @@
export * from './app';
export * from './history';

View File

@ -0,0 +1,8 @@
export default {
hooks: {
'@reddichat/state/defaultState': async () => ({
app: undefined,
history: undefined,
}),
},
};

View File

@ -0,0 +1,8 @@
// Whilst the configuration object can be modified here, the recommended way of making
// changes is via the presets' options or Neutrino's API in `.neutrinorc.js` instead.
// Neutrino's inspect feature can be used to view/export the generated configuration.
const neutrino = require('neutrino');
const configOfConfigs = require(`${__dirname}/.neutrinorc`);
const configs = Array.isArray(configOfConfigs) ? configOfConfigs : [configOfConfigs];
module.exports = configs.map((config) => neutrino(config).webpack());

5769
packages/app/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -20,7 +20,15 @@
"index.js.map"
],
"dependencies": {
"debug": "4.3.1"
"@latus/core": "^1.0.0",
"@latus/db": "^1.0.0",
"@latus/governor": "^1.0.0",
"@latus/socket": "^1.0.0",
"@reddichat/core": "^1.0.0",
"@reddichat/state": "^1.0.0",
"@reduxjs/toolkit": "^1.5.0",
"debug": "4.3.1",
"uuid": "^8.3.1"
},
"devDependencies": {
"@neutrinojs/airbnb-base": "^9.4.0",

View File

@ -0,0 +1,20 @@
import Join from '../packets/join';
import Leave from '../packets/leave';
import Message from '../packets/message';
import chat from './state';
export * from './state';
export default {
hooks: {
'@latus/socket/packets': (latus) => ({
Join: Join(latus),
Leave: Leave(latus),
Message: Message(latus),
}),
'@reddichat/state/reducers': () => ({
chat,
}),
},
};

View File

@ -0,0 +1,145 @@
import {
createSelector,
createSlice,
} from '@reduxjs/toolkit';
import {renderChannel} from '@reddichat/core';
import {localStorage, storage} from '@reddichat/state/client';
export const chatSelector = (state) => state.chat;
export const channelsSelector = createSelector(
chatSelector,
(chat) => chat.channels,
);
export const channelSelector = createSelector(
[channelsSelector, (_, channel) => channel],
(channels, channel) => channel && channels[renderChannel(channel)],
);
export const channelUsersSelector = createSelector(
[channelSelector],
(channel) => (channel ? channel.users : []),
);
export const inputSelector = createSelector(
chatSelector,
(chat) => chat.input,
);
export const inputChannelSelector = createSelector(
[inputSelector, (_, channel) => channel],
(input, channel) => input[renderChannel(channel)],
);
export const messagesSelector = createSelector(
chatSelector,
(chat) => chat.messages,
);
export const channelMessagesSelector = createSelector(
[channelSelector, messagesSelector],
(channel, messages) => (!channel ? [] : channel.messages.map((uuid) => messages[uuid])),
);
const slice = createSlice({
name: 'reddichat/chat',
initialState: {
channels: {},
input: {},
messages: {},
...(storage(chatSelector) || {}),
},
/* eslint-disable no-param-reassign */
extraReducers: {
[localStorage]: ({input}) => ({input}),
},
reducers: {
addMessage: ({
channels,
messages,
}, {payload}) => {
const {channel, uuid} = payload;
messages[uuid] = payload;
channels[renderChannel(channel)].messages.push(uuid);
},
confirmMessage: ({channels, messages}, {payload: {previous, current, timestamp}}) => {
const {pending, ...previousMessage} = messages[previous];
messages[current] = {
...previousMessage,
timestamp,
uuid: current,
};
delete messages[previous];
const {[renderChannel(messages[current].channel)]: channel} = channels;
const index = channel.messages.findIndex((uuid) => uuid === previous);
channel.messages[index] = current;
},
editMessage: ({messages}, {payload: {uuid, message}}) => {
messages[uuid].message = message;
},
inputText: ({input}, {payload: {channel, text}}) => {
const rendered = renderChannel(channel);
if (!text) {
delete input[rendered];
}
else {
input[rendered] = text;
}
},
join: ({channels, messages}, {payload: {channel, messages: channelMessages, users}}) => {
channelMessages.forEach((message) => {
messages[message.uuid] = message;
});
channels[renderChannel(channel)] = {
messages: channelMessages.map((message) => message.uuid),
users,
};
},
joined: ({channels}, {payload: {channel, id}}) => {
channels[renderChannel(channel)].users.push(id);
},
leave: ({channels}, {payload: {channel}}) => {
delete channels[renderChannel(channel)];
},
left: ({channels}, {payload: {channel, id}}) => {
const {users} = channels[renderChannel(channel)];
users.splice(users.indexOf(id), 1);
},
rejectMessage: ({messages}, {payload: uuid}) => {
delete messages[uuid].pending;
messages[uuid].rejected = true;
},
removeMessage: (state, {payload: {channel, uuid}}) => {
delete state.messages[uuid];
const {messages} = state.channels[renderChannel(channel)];
messages.splice(messages.indexOf(uuid), 1);
},
submitJoin: () => {},
submitLeave: () => {},
submitMessage: () => {},
},
/* eslint-enable no-param-reassign */
});
export const {
addMessage,
confirmMessage,
editMessage,
inputText,
join,
joined,
leave,
left,
rejectMessage,
removeMessage,
submitJoin,
submitLeave,
submitMessage,
} = slice.actions;
slice.reducer.subscription = slice.reducer;
export default slice.reducer;

View File

@ -0,0 +1,28 @@
import Join from './packets/join.server';
import Leave from './packets/leave.server';
import Message from './packets/message.server';
import defaultState, {channelsToHydrate} from './state';
import joinChannel from './join-channel';
export * from './state';
export default {
hooks: {
'@latus/socket/packets': (latus) => ({
Join: Join(latus),
Leave: Leave(latus),
Message: Message(latus),
}),
'@latus/socket/connect': async (socket) => {
const channels = await channelsToHydrate(socket.req);
const joins = channels
.filter(({type}) => 'r' === type)
.map((channel) => joinChannel(channel, socket));
return Promise.all(joins);
},
'@reddichat/state/defaultState': async (req, latus) => ({
chat: await defaultState(req, latus),
}),
},
};

View File

@ -0,0 +1,17 @@
import {promisify} from 'util';
import {channelIsAnonymous, renderChannel} from '@reddichat/core';
import {channelUsers} from '@reddichat/state';
export default async (latus, channel, socket) => {
const {req} = socket;
const [id, username] = channelIsAnonymous(channel)
? [0, 'anonymous']
: [req.user.id, req.user.redditUsername];
const users = await channelUsers(req, channel);
const rendered = renderChannel(channel);
if (-1 === users.indexOf(id)) {
socket.constructor.send(socket.to(rendered), ['Join', {channel, id, username}]);
}
await promisify(socket.join.bind(socket))(rendered);
};

View File

@ -0,0 +1,15 @@
import {promisify} from 'util';
import {channelIsAnonymous, renderChannel} from '@reddichat/core';
import {channelUserCounts} from '@reddichat/state';
export default async (channel, socket) => {
const {req} = socket;
const rendered = renderChannel(channel);
const userId = channelIsAnonymous(channel) ? 0 : req.userId;
await promisify(socket.leave.bind(socket))(rendered);
const userCounts = await channelUserCounts(req, channel);
if (!userCounts[userId]) {
socket.to(rendered, ['Leave', {channel, id: userId}]);
}
};

View File

@ -0,0 +1,23 @@
import {Packet, ValidationError} from '@latus/socket/packets';
import {validateChannel} from '@reddichat/core/client';
export default () => class Join extends Packet {
static get data() {
return {
id: 'uint32',
channel: {
type: 'string',
name: 'string',
},
username: 'string',
};
}
static async validate({data: {channel}}) {
if (!validateChannel(channel)) {
throw new ValidationError({code: 400, reason: 'Malformed channel'});
}
}
};

View File

@ -0,0 +1,24 @@
import {ModelMap} from '@latus/db';
import {channelState} from '@reddichat/state';
import joinChannel from '../join-channel';
import Join from './join';
export default (latus) => class JoinServer extends Join(latus) {
static async respond({data: {channel}}, socket) {
const {req} = socket;
const {User} = ModelMap(latus);
await joinChannel(channel, socket);
const state = await channelState(req, channel);
const usernames = Object.fromEntries(await Promise.all(
state.users.map(async (id) => [
id,
0 === id ? 'anonymous' : (await User.findByPk(id)).redditUsername,
]),
));
return {...state, usernames};
}
};

View File

@ -0,0 +1,22 @@
import {validateChannel} from '@reddichat/core/client';
import {Packet, ValidationError} from '@latus/socket/packets';
export default () => class Leave extends Packet {
static get data() {
return {
id: 'uint32',
channel: {
type: 'string',
name: 'string',
},
};
}
static async validate({data: channel}) {
if (!validateChannel(channel)) {
throw new ValidationError({code: 400, reason: 'Malformed channel'});
}
}
};

View File

@ -0,0 +1,10 @@
import leaveChannel from '../leave-channel';
import Leave from './leave';
export default () => class LeaveServer extends Leave() {
static async respond({data: channel}, socket) {
return leaveChannel(channel, socket);
}
};

View File

@ -0,0 +1,41 @@
import {validateChannel} from '@reddichat/core/client';
import {createLimiter} from '@latus/governor/client';
import {Packet, ValidationError} from '@latus/socket/packets';
export default () => class Message extends Packet {
static characterLimiter = createLimiter({
keyPrefix: 'characterLimiter',
points: 2048,
duration: 2,
});
static get data() {
return {
channel: {
type: 'string',
name: 'string',
},
message: 'string',
owner: 'uint32',
timestamp: 'float64',
uuid: 'string',
};
}
static limit = {
points: 10,
duration: 15,
};
static async validate({data: {channel, message}}, socket) {
await this.characterLimiter.consume(socket.id, message.length);
if (!validateChannel(channel)) {
throw new ValidationError({code: 400, reason: 'Malformed channel'});
}
if (message.length > 512) {
throw new ValidationError({code: 400, reason: 'Message larger than 512 bytes'});
}
}
};

View File

@ -0,0 +1,92 @@
import {v4 as uuidv4} from 'uuid';
import {ModelMap} from '@latus/db';
import {createLimiter} from '@latus/governor';
import {ValidationError} from '@latus/socket';
import {channelIsAnonymous, renderChannel, validateChannel} from '@reddichat/core';
import Message from './message';
export default (latus) => class MessageServer extends Message(latus) {
static characterLimiter = createLimiter(latus, {
keyPrefix: 'characterLimiter',
points: 2048,
duration: 2,
});
static async respond({data}, socket) {
const {req} = socket;
const {pubClient} = req.adapter;
const {User} = ModelMap(latus);
const {channel, message} = data;
const {name, type} = channel;
const isAnonymous = channelIsAnonymous(channel);
const owner = isAnonymous ? 0 : req.userId;
const rendered = renderChannel(channel);
const serverChannel = 'r' === type
? rendered
: `/u/${[name, req.user.redditUsername].sort().join('$')}`;
const timestamp = Date.now();
const uuid = uuidv4();
const key = `${serverChannel}:messages:${uuid}`;
let destinations = [];
if ('u' === type) {
const other = await User.findOne({where: {redditUsername: name}});
destinations = [
{
type: 'u',
name: other.id,
username: name,
},
{
type: 'u',
name: req.userId,
username: req.user.redditUsername,
},
];
}
else {
destinations = [
channel,
];
}
destinations.forEach((room, i) => (
socket.to(renderChannel(room), new Message({
...data,
channel: 'r' === type
? room
: {
type: 'u',
name: destinations[1 - i].username,
},
owner,
timestamp,
uuid,
}))
));
return new Promise((resolve, reject) => {
pubClient
.multi()
.set(key, JSON.stringify({
message,
owner,
socket: socket.id,
timestamp,
}))
.expire(key, channelIsAnonymous(channel) ? 60 : 600)
.exec((error) => (error ? reject(error) : resolve([timestamp, uuid])));
});
}
static async validate({data: {channel, message}}, socket) {
await this.characterLimiter.consume(socket.id, message.length);
if (!validateChannel(channel)) {
throw new ValidationError({code: 400, reason: 'Malformed channel'});
}
if (message.length > 512) {
throw new ValidationError({code: 400, reason: 'Message larger than 512 bytes'});
}
}
};

118
packages/chat/src/state.js Normal file
View File

@ -0,0 +1,118 @@
/* eslint-disable import/no-extraneous-dependencies */
import {promisify} from 'util';
import {ModelMap} from '@latus/db';
import {createClient, keys} from '@latus/redis';
import {channelIsAnonymous, parseChannel, renderChannel} from '@reddichat/core';
export const channelUserCounts = async (req, channel) => {
const clients = promisify(req.adapter.clients.bind(req.adapter));
const rendered = renderChannel(channel);
const socketKeys = await clients([rendered]);
const customRequest = promisify(req.adapter.customRequest.bind(req.adapter));
// eslint-disable-next-line no-nested-ternary
const replies = channelIsAnonymous(channel)
? (
socketKeys.length > 0
? [socketKeys.reduce((r, socketKey) => ({...r, [socketKey]: 0}), {})]
: []
)
: await customRequest({type: 'socketUsers', payload: socketKeys});
const socketUsers = replies.reduce((r, m) => ({...r, ...m}), {});
return 0 === socketKeys.length
? []
: Object.values(socketUsers).reduce((r, uid) => ({...r, [uid]: 1 + (r[uid] || 0)}), {});
};
export const channelUsers = async (req, channel) => (
Object.keys(await channelUserCounts(req, channel)).map((id) => parseInt(id, 10))
);
export const channelState = async (req, latus, channel) => {
const {name, type} = channel;
const redisClient = createClient(latus);
const mget = promisify(redisClient.mget.bind(redisClient));
const realName = 'r' === type
? name
: `${[name, req.user.redditUsername].sort().join('$')}`;
const messagesKey = `${renderChannel({type, name: realName})}:messages:*`;
const messageKeys = await keys(redisClient, messagesKey);
const messages = 0 === messageKeys.length
? []
: (await mget(messageKeys))
.map((reply, i) => ({
...JSON.parse(reply),
uuid: messageKeys[i].split(':')[2],
}))
.sort((l, r) => l.timestamp - r.timestamp);
const users = new Set(await channelUsers(req, channel));
users.add(channelIsAnonymous(channel) ? 0 : req.userId);
return {
messages,
users: Array.from(users.values()),
};
};
export const channelsToHydrate = async (req, latus) => {
const {channel, user} = req;
if (!user) {
return channel ? [renderChannel(channel)] : [];
}
const {User} = ModelMap(latus);
const channels = await Promise.all(
[]
.concat(channel ? [channel] : [])
.concat(await req.user.favorites())
.concat(await Promise.all((await user.friendships()).map(
async ({adderId, addeeId}) => ({
type: 'u',
name: (await User.findByPk(adderId !== user.id ? adderId : addeeId)).redditUsername,
}),
))),
);
return Array.from((new Set(channels.map(renderChannel))).values()).map(parseChannel);
};
export const chatUserIds = async (req, latus) => {
const toHydrate = await channelsToHydrate(req, latus);
if (0 === toHydrate.length) {
return [];
}
const entries = await Promise.all(
toHydrate.map((channel) => channelState(req, latus, channel)),
);
const chatUserIds = new Set();
for (let i = 0; i < toHydrate.length; i++) {
const {messages, users} = entries[i];
Object.values(messages).map((message) => message.owner).forEach((id) => chatUserIds.add(id));
users.forEach((id) => chatUserIds.add(id));
}
return Array.from(chatUserIds.keys());
};
export default async (req, latus) => {
const toHydrate = await channelsToHydrate(req, latus);
const chat = {
channels: {},
messages: {},
users: {},
};
if (0 === toHydrate.length) {
return undefined;
}
const entries = await Promise.all(
toHydrate.map((favorite) => channelState(req, latus, favorite)),
);
for (let i = 0; i < toHydrate.length; i++) {
const channel = renderChannel(toHydrate[i]);
const {messages, users} = entries[i];
chat.channels[channel] = {
messages: messages.map((message) => message.uuid),
users,
};
messages.forEach((message) => {
chat.messages[message.uuid] = message;
});
}
return chat;
};

View File

@ -3,4 +3,6 @@
// Neutrino's inspect feature can be used to view/export the generated configuration.
const neutrino = require('neutrino');
module.exports = neutrino(require(`${__dirname}/.neutrinorc`)).webpack();
const configOfConfigs = require(`${__dirname}/.neutrinorc`);
const configs = Array.isArray(configOfConfigs) ? configOfConfigs : [configOfConfigs];
module.exports = configs.map((config) => neutrino(config).webpack());

8704
packages/chat/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

3
packages/core/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/*.js
/*.js.map
!/webpack.config.js

View File

@ -0,0 +1,38 @@
{
"name": "@reddichat/core",
"version": "1.0.0",
"main": "index.js",
"author": "cha0s",
"license": "MIT",
"scripts": {
"build": "NODE_PATH=./node_modules webpack --mode production",
"clean": "rm -f yarn.lock && yarn",
"dev": "NODE_PATH=./node_modules webpack --mode development",
"forcepub": "npm unpublish --force $(node -e 'process.stdout.write(require(`./package.json`).name)') && npm publish",
"lint": "NODE_PATH=./node_modules eslint --format codeframe --ext mjs,js .",
"test": "NODE_PATH=./node_modules mocha --config ../../config/.mocharc.js",
"watch": "NODE_PATH=./node_modules webpack --watch --mode development"
},
"files": [
"client.js",
"client.js.map",
"index.js",
"index.js.map"
],
"dependencies": {
"debug": "4.3.1"
},
"devDependencies": {
"@neutrinojs/airbnb-base": "^9.4.0",
"@neutrinojs/copy": "9.4.0",
"@neutrinojs/library": "^9.4.0",
"@neutrinojs/mocha": "^9.4.0",
"chai": "4.2.0",
"eslint": "^7",
"eslint-import-resolver-webpack": "0.13.0",
"mocha": "^8",
"neutrino": "^9.4.0",
"webpack": "^4",
"webpack-cli": "^3"
}
}

View File

@ -0,0 +1,40 @@
export const channelIsAnonymous = ({type, name}) => 'r' === type && 'anonymous' === name;
export const parseChannel = (channel) => {
const matches = channel.match(/^\/([^/]+)\/([^/]+)/i);
if (matches) {
const [, type, name] = matches;
return {name, type};
}
return undefined;
};
export const parseChatChannel = (url) => {
if (0 !== url.indexOf('/chat/')) {
return undefined;
}
return parseChannel(url.slice(5));
};
export const renderChannel = (channel) => (channel ? `/${channel.type}/${channel.name}` : '');
const countryExceptions = ['de', 'es', 'it'];
export const validateSubreddit = (name) => {
if (-1 !== countryExceptions.indexOf(name)) {
return true;
}
return !!name.match(/^[A-Za-z0-9][A-Za-z0-9_]{2,20}$/i);
};
export const validateUsername = (name) => !!name.trim().match(/^[\w-]{3,20}$/);
export const validateChannel = (channel) => {
if (!channel) {
return false;
}
const {type, name} = channel;
if (-1 === ['r', 'u'].indexOf(type)) {
return false;
}
return ('r' === type ? validateSubreddit : validateUsername)(name);
};

View File

@ -0,0 +1,9 @@
export {
channelIsAnonymous,
parseChannel,
parseChatChannel,
renderChannel,
validateSubreddit,
validateUsername,
validateChannel,
} from './channel';

View File

@ -0,0 +1,9 @@
export {
channelIsAnonymous,
parseChannel,
parseChatChannel,
renderChannel,
validateSubreddit,
validateUsername,
validateChannel,
} from './channel';

View File

@ -0,0 +1,8 @@
// Whilst the configuration object can be modified here, the recommended way of making
// changes is via the presets' options or Neutrino's API in `.neutrinorc.js` instead.
// Neutrino's inspect feature can be used to view/export the generated configuration.
const neutrino = require('neutrino');
const configOfConfigs = require(`${__dirname}/.neutrinorc`);
const configs = Array.isArray(configOfConfigs) ? configOfConfigs : [configOfConfigs];
module.exports = configs.map((config) => neutrino(config).webpack());

5333
packages/core/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -3,4 +3,6 @@
// Neutrino's inspect feature can be used to view/export the generated configuration.
const neutrino = require('neutrino');
module.exports = neutrino(require(`${__dirname}/.neutrinorc`)).webpack();
const configOfConfigs = require(`${__dirname}/.neutrinorc`);
const configs = Array.isArray(configOfConfigs) ? configOfConfigs : [configOfConfigs];
module.exports = configs.map((config) => neutrino(config).webpack());

3
packages/state/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/*.js
/*.js.map
!/webpack.config.js

View File

@ -0,0 +1,47 @@
{
"name": "@reddichat/state",
"version": "1.0.0",
"main": "index.js",
"author": "cha0s",
"license": "MIT",
"scripts": {
"build": "NODE_PATH=./node_modules webpack --mode production",
"clean": "rm -f yarn.lock && yarn",
"dev": "NODE_PATH=./node_modules webpack --mode development",
"forcepub": "npm unpublish --force $(node -e 'process.stdout.write(require(`./package.json`).name)') && npm publish",
"lint": "NODE_PATH=./node_modules eslint --format codeframe --ext mjs,js .",
"test": "NODE_PATH=./node_modules mocha --config ../../config/.mocharc.js",
"watch": "NODE_PATH=./node_modules webpack --watch --mode development"
},
"files": [
"client.js",
"client.js.map",
"index.js",
"index.js.map"
],
"dependencies": {
"@latus/core": "^1.0.0",
"@latus/db": "^1.0.0",
"@latus/redis": "^1.0.0",
"@reddichat/core": "^1.0.0",
"@reduxjs/toolkit": "^1.5.0",
"connected-react-router": "^6.8.0",
"debug": "4.3.1",
"deepmerge": "^4.2.2",
"lodash.throttle": "^4.1.1",
"redux": "^4.0.5"
},
"devDependencies": {
"@neutrinojs/airbnb-base": "^9.4.0",
"@neutrinojs/copy": "9.4.0",
"@neutrinojs/library": "^9.4.0",
"@neutrinojs/mocha": "^9.4.0",
"chai": "4.2.0",
"eslint": "^7",
"eslint-import-resolver-webpack": "0.13.0",
"mocha": "^8",
"neutrino": "^9.4.0",
"webpack": "^4",
"webpack-cli": "^3"
}
}

135
packages/state/src/client/effects.js vendored Normal file
View File

@ -0,0 +1,135 @@
// import RateLimiterMemory from 'rate-limiter-flexible/lib/RateLimiterMemory';
// import {v4 as uuidv4} from 'uuid';
// import AddFavorite from '~/common/packets/add-favorite.packet';
// import AddFriend from '~/common/packets/add-friend.packet';
// import Block from '~/common/packets/block.packet';
// import ConfirmFriend from '~/common/packets/confirm-friend.packet';
// import Join from '~/common/packets/join.packet';
// import Leave from '~/common/packets/leave.packet';
// import Message from '~/common/packets/message.packet';
// import RemoveFavorite from '~/common/packets/remove-favorite.packet';
// import RemoveFriend from '~/common/packets/remove-friend.packet';
// import Unblock from '~/common/packets/unblock.packet';
// import {
// addMessage,
// confirmMessage,
// join,
// leave,
// rejectMessage,
// submitJoin,
// submitLeave,
// submitMessage,
// } from '~/common/state/chat';
// import {
// addFriendship,
// confirmFriendship,
// idSelector,
// submitAddFavorite,
// submitAddFriend,
// submitBlock,
// submitConfirmFriend,
// submitRemoveFavorite,
// submitRemoveFriend,
// submitUnblock,
// } from '~/common/state/user';
// import {
// setUsernames,
// } from '@reddichat/state/client';
// import {socket} from '~/client/hooks/useSocket';
// const characterLimiter = new RateLimiterMemory({points: 2048, duration: 2});
// const messageLimiter = new RateLimiterMemory({points: 10, duration: 15});
const effects = {
// [submitAddFavorite]: (store, {payload}) => socket.send(new AddFavorite(payload)),
// [submitAddFriend]: ({dispatch, getState}, {payload: name}) => {
// const state = getState();
// const userId = idSelector(state);
// socket.send(new AddFriend({name}), (error, id) => {
// if (error) {
// return;
// }
// dispatch(addFriendship({
// addeeId: id,
// adderId: userId,
// status: 'pending',
// }));
// dispatch(setUsernames({[id]: name}));
// });
// },
// [submitBlock]: (store, {payload: id}) => socket.send(new Block(id)),
// [submitConfirmFriend]: ({dispatch}, {payload: adderId}) => {
// socket.send(new ConfirmFriend(adderId), (error) => {
// if (error) {
// return;
// }
// dispatch(confirmFriendship(adderId));
// });
// },
// [submitJoin]: ({dispatch}, {payload}) => {
// const {channel} = payload;
// socket.send(new Join(payload), (error, result) => {
// if (error) {
// return;
// }
// const {messages, users} = result;
// dispatch(join({channel, messages, users}));
// });
// },
// [submitLeave]: ({dispatch}, {payload}) => {
// const {channel} = payload;
// socket.send(new Leave(payload), () => dispatch(leave({channel})));
// },
// [submitMessage]: async ({dispatch}, {payload}) => {
// const reject = (ttr) => {
// setTimeout(() => {
// dispatch(addMessage({
// ...payload,
// message: [
// 'You are sending too much.',
// `Try again in ${ttr} second${1 === ttr ? '' : 's'}.`,
// ].join(' '),
// owner: -1,
// uuid: uuidv4(),
// }));
// dispatch(rejectMessage(payload.uuid));
// }, 0);
// };
// try {
// await characterLimiter.consume('', payload.message.length);
// await messageLimiter.consume('');
// socket.send(new Message(payload), (error, result) => {
// if (error) {
// switch (error.code) {
// case 429: {
// reject(error.ttr);
// break;
// }
// default:
// }
// return;
// }
// const [timestamp, current] = result;
// dispatch(confirmMessage({current, previous: payload.uuid, timestamp}));
// });
// }
// catch (error) {
// reject(Math.round(Math.max(0, error.msBeforeNext) / 1000) || 1);
// }
// },
// [submitRemoveFavorite]: (store, {payload}) => socket.send(new RemoveFavorite(payload)),
// [submitRemoveFriend]: (store, {payload: id}) => socket.send(new RemoveFriend(id)),
// [submitUnblock]: (store, {payload}) => socket.send(new Unblock(payload)),
};
export const middleware = (store) => (next) => (action) => {
const result = next(action);
if (effects[action.type]) {
setTimeout(() => effects[action.type](store, action), 0);
}
return result;
};
export default effects;

View File

@ -0,0 +1,6 @@
export * from './storage';
export {default as storage} from './storage';
export {default as configureStore} from './store';
export default {};

View File

@ -0,0 +1,35 @@
import throttle from 'lodash.throttle';
import {createAction} from '@reduxjs/toolkit';
export const localStorage = createAction('reddichat/localStorage');
const hasStorage = (() => {
try {
window.localStorage.setItem('__redux-test', true);
window.localStorage.removeItem('__redux-test');
return true;
}
catch (error) {
return false;
}
})();
export const storageSubscription = (store, reducer) => (
throttle(
!hasStorage ? (() => {}) : () => (
window.localStorage.setItem(
'redux-state',
JSON.stringify(reducer(store.getState(), localStorage())),
)
),
1000,
)
);
export default (selector) => {
const state = hasStorage && window.localStorage.getItem('redux-state');
if (!state) {
return undefined;
}
return selector(JSON.parse(state));
};

View File

@ -0,0 +1,37 @@
import {ensureUniqueReduction} from '@latus/core/client';
import {configureStore as configureStoreR, getDefaultMiddleware} from '@reduxjs/toolkit';
import merge from 'deepmerge';
import {routerMiddleware} from 'connected-react-router';
import {combineReducers} from 'redux';
import {storageSubscription} from './storage';
import {middleware as effectsMiddleware} from './effects';
export default async function configureStore(latus, options = {}) {
const {history} = options;
const reducers = await ensureUniqueReduction(latus, '@reddichat/state/reducers', options);
const {defaultState} = latus.config['@reddichat/state/client'];
const reducer = combineReducers(reducers);
const store = configureStoreR(
merge(
options,
{
middleware: [
...getDefaultMiddleware(),
routerMiddleware(history),
effectsMiddleware,
],
preloadedState: reducer(defaultState, {type: null}),
reducer,
},
),
);
const subscriptionReducers = Object.entries(reducers).map(([key, reducer]) => [
key,
reducer.subscription || (() => null),
]);
const subscriptionReducer = combineReducers(subscriptionReducers);
store.subscribe((store) => storageSubscription(store, subscriptionReducer));
return store;
}

View File

@ -0,0 +1,11 @@
import {ensureUniqueReduction} from '@latus/core';
export default {
hooks: {
'@latus/http/plugins': async (req, latus) => ({
'@reddichat/state/client': {
defaultState: await ensureUniqueReduction(latus, '@reddichat/state/defaultState', req),
},
}),
},
};

View File

@ -0,0 +1,8 @@
// Whilst the configuration object can be modified here, the recommended way of making
// changes is via the presets' options or Neutrino's API in `.neutrinorc.js` instead.
// Neutrino's inspect feature can be used to view/export the generated configuration.
const neutrino = require('neutrino');
const configOfConfigs = require(`${__dirname}/.neutrinorc`);
const configs = Array.isArray(configOfConfigs) ? configOfConfigs : [configOfConfigs];
module.exports = configs.map((config) => neutrino(config).webpack());

5746
packages/state/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -20,7 +20,15 @@
"index.js.map"
],
"dependencies": {
"debug": "4.3.1"
"@latus/db": "^1.0.0",
"@latus/socket": "^1.0.0",
"@reddichat/chat": "^1.0.0",
"@reddichat/core": "^1.0.0",
"@reddichat/state": "^1.0.0",
"@reduxjs/toolkit": "^1.5.0",
"connected-react-router": "^6.8.0",
"debug": "4.3.1",
"deepmerge": "^4.2.2"
},
"devDependencies": {
"@neutrinojs/airbnb-base": "^9.4.0",

View File

@ -0,0 +1,32 @@
import AddFavorite from '../packets/add-favorite';
import AddFriend from '../packets/add-friend';
import Block from '../packets/block';
import ConfirmFriend from '../packets/confirm-friend';
import RemoveFavorite from '../packets/remove-favorite';
import RemoveFriend from '../packets/remove-friend';
import Unblock from '../packets/unblock';
import Usernames from '../packets/usernames';
import user from './state/user';
import usernames from './state/usernames';
export * from './state';
export default {
hooks: {
'@latus/socket/packets': (latus) => ({
AddFavorite: AddFavorite(latus),
AddFriend: AddFriend(latus),
Block: Block(latus),
ConfirmFriend: ConfirmFriend(latus),
RemoveFavorite: RemoveFavorite(latus),
RemoveFriend: RemoveFriend(latus),
Unblock: Unblock(latus),
Usernames: Usernames(latus),
}),
'@reddichat/state/reducers': () => ({
user,
usernames,
}),
},
};

View File

@ -0,0 +1,2 @@
export * from './user';
export * from './usernames';

View File

@ -0,0 +1,245 @@
import {LOCATION_CHANGE} from 'connected-react-router';
import merge from 'deepmerge';
import {addMessage, join, leave} from '@reddichat/chat/client';
import {renderChannel} from '@reddichat/core/client';
import {localStorage, storage} from '@reddichat/state/client';
import {
createSelector,
createSlice,
} from '@reduxjs/toolkit';
export const userSelector = (state) => state.user;
export const blockedSelector = createSelector(
userSelector,
(user) => user.blocked,
);
export const blurredSelector = createSelector(
userSelector,
(user) => user.blurred,
);
export const favoritesSelector = createSelector(
[userSelector],
({favorites}) => favorites,
);
const createFavoriteSelector = (type) => (
createSelector(
[favoritesSelector],
(favorites) => (
favorites
.filter((favorite) => 0 === favorite.indexOf(`/${type}/`))
.map((favorite) => favorite.substr(3))
),
)
);
export const favoriteChannelsSelector = createFavoriteSelector('r');
export const favoriteUsersSelector = createFavoriteSelector('u');
export const friendshipIndex = (friendship, test) => (
friendship
.findIndex(({addeeId, adderId}) => test.adderId === adderId && test.addeeId === addeeId)
);
export const friendshipIdIndex = (friendship, id) => (
friendship
.findIndex(({addeeId, adderId}) => id === adderId || id === addeeId)
);
export const friendshipSelector = createSelector(
userSelector,
(user) => user.friendship,
);
export const activeFriendshipSelector = createSelector(
friendshipSelector,
(friendship) => friendship.filter(({status}) => 'active' === status),
);
export const idSelector = createSelector(
[userSelector],
({id}) => id,
);
export const isAnonymousSelector = createSelector(
[userSelector],
({isAnonymous}) => isAnonymous,
);
export const pendingFriendshipSelector = createSelector(
friendshipSelector,
(friendship) => friendship.filter(({status}) => 'pending' === status),
);
export const recentSelector = createSelector(
[userSelector],
({recent}) => recent,
);
export const redditUsernameSelector = createSelector(
userSelector,
(user) => user.redditUsername,
);
export const unreadSelector = createSelector(
[userSelector],
(user) => user.unread,
);
const createUnreadSelector = (type) => createSelector(
[unreadSelector],
(unread) => (
Object.entries(unread)
.filter(([name]) => name.charCodeAt(1) === type.charCodeAt(0))
.reduce((r, [, count]) => r + count, 0)
),
);
export const unreadChannelSelector = createUnreadSelector('r');
export const unreadUserSelector = createUnreadSelector('u');
export const unreadForChannelSelector = createSelector(
[unreadSelector, (_, channel) => channel],
(unread, channel) => unread[channel] || 0,
);
const slice = createSlice({
name: 'reddichat/user',
initialState: merge.all([
{
activeChannel: '',
blocked: [],
blurred: false,
favorites: [],
friendship: [],
id: 0,
isAnonymous: true,
recent: [],
redditUsername: 'anonymous',
unread: {},
},
storage(userSelector) || {},
], {arrayMerge: (target, source) => Array.from(new Set(source.concat(target)).values())}),
/* eslint-disable no-param-reassign */
extraReducers: {
[addMessage]: ({activeChannel, blurred, unread}, {payload: {channel}}) => {
const rendered = renderChannel(channel);
if (blurred || activeChannel !== rendered) {
unread[rendered] = (unread[rendered] || 0) + 1;
}
},
[join]: ({recent}, {payload: {channel}}) => {
const {type, name} = channel;
if ('r' === type && -1 === recent.indexOf(name)) {
recent.push(name);
}
},
[leave]: ({unread}, {payload: {channel}}) => {
delete unread[renderChannel(channel)];
},
[localStorage]: ({recent}) => ({recent}),
[LOCATION_CHANGE]: (state, {payload: {location: {pathname}}}) => {
const {unread} = state;
state.activeChannel = pathname.match(/^\/chat\//) ? pathname.substr('/chat'.length) : '';
if (unread[state.activeChannel]) {
delete unread[state.activeChannel];
}
},
},
reducers: {
addFriendship: ({friendship}, {payload}) => {
friendship.push(payload);
},
addToFavorites: ({favorites}, {payload: favorite}) => {
favorites.push(favorite);
},
block: ({blocked}, {payload}) => {
blocked.push(payload);
},
confirmFriendship: ({friendship, id}, {payload: otherId}) => {
const otherIndex = friendshipIndex(friendship, {adderId: otherId, addeeId: id});
const selfIndex = friendshipIndex(friendship, {adderId: id, addeeId: otherId});
const index = -1 === otherIndex ? selfIndex : otherIndex;
friendship[index].status = 'active';
},
logOut: () => ({
blocked: [],
blurred: false,
favorites: [],
activeChannel: '',
friendship: [],
id: 0,
isAnonymous: true,
recent: [],
redditUsername: 'anonymous',
unread: {},
}),
removeFriendship: ({friendship, id}, {payload: otherId}) => {
const otherIndex = friendshipIndex(friendship, {adderId: otherId, addeeId: id});
const selfIndex = friendshipIndex(friendship, {adderId: id, addeeId: otherId});
const index = -1 === otherIndex ? selfIndex : otherIndex;
friendship.splice(index, 1);
},
removeFromFavorites: ({favorites}, {payload: favorite}) => {
favorites.splice(favorites.indexOf(favorite), 1);
},
removeRecent: ({recent}, {payload: channel}) => {
recent.splice(recent.indexOf(channel), 1);
},
setBlurred: (state, {payload: blurred}) => {
const {pathname} = window.location;
const {unread} = state;
state.blurred = blurred;
const activeChannel = pathname.match(/^\/chat\//) ? pathname.substr('/chat'.length) : '';
if (
false === blurred
&& state.activeChannel === activeChannel
&& unread[state.activeChannel]
) {
delete unread[state.activeChannel];
}
},
submitAddFavorite: () => {},
submitAddFriend: () => {},
submitBlock: () => {},
submitConfirmFriend: () => {},
submitRemoveFavorite: () => {},
submitRemoveFriend: () => {},
submitUnblock: () => {},
unblock: ({blocked}, {payload}) => {
blocked.splice(blocked.indexOf(payload), 1);
},
},
/* eslint-enable no-param-reassign */
});
slice.reducer.subscription = slice.reducer;
export const {
addFriend,
addFriendship,
confirmFriendship,
addToFavorites,
block,
logOut,
removeFriend,
removeFriendship,
removeFromFavorites,
removeRecent,
setBlurred,
submitAddFavorite,
submitAddFriend,
submitBlock,
submitConfirmFriend,
submitRemoveFriend,
submitRemoveFavorite,
submitUnblock,
unblock,
} = slice.actions;
export default slice.reducer;

View File

@ -0,0 +1,37 @@
import {
join,
joined,
} from '@reddichat/chat/client';
import {
createSelector,
createSlice,
} from '@reduxjs/toolkit';
export const usernamesSelector = (state) => state.usernames;
export const usernameSelector = createSelector(
[usernamesSelector, (_, id) => id],
(usernames, id) => usernames[id],
);
const slice = createSlice({
name: 'reddichat/usernames',
initialState: {},
/* eslint-disable no-param-reassign */
extraReducers: {
[join]: (state, {payload: {usernames}}) => ({...state, ...usernames}),
[joined]: (state, {payload: {id, username}}) => ({...state, [id]: username}),
},
reducers: {
setUsernames: (state, {payload}) => ({...state, ...payload}),
},
/* eslint-enable no-param-reassign */
});
slice.reducer.subscription = slice.reducer;
export const {
setUsernames,
} = slice.actions;
export default slice.reducer;

View File

@ -0,0 +1,35 @@
import UserReddichat from './models/user-reddichat';
import AddFavorite from './packets/add-favorite.server';
import AddFriend from './packets/add-friend.server';
import Block from './packets/block.server';
import ConfirmFriend from './packets/confirm-friend.server';
import RemoveFavorite from './packets/remove-favorite.server';
import RemoveFriend from './packets/remove-friend.server';
import Unblock from './packets/unblock.server';
import Usernames from './packets/usernames.server';
import userDefaultState from './state/user';
import usernamesDefaultState from './state/usernames';
export default {
hooks: {
'@latus/db/models.decorate': (Models, latus) => ({
...Models,
User: UserReddichat(Models.User, latus),
}),
'@latus/socket/packets': (latus) => ({
AddFavorite: AddFavorite(latus),
AddFriend: AddFriend(latus),
Block: Block(latus),
ConfirmFriend: ConfirmFriend(latus),
RemoveFavorite: RemoveFavorite(latus),
RemoveFriend: RemoveFriend(latus),
Unblock: Unblock(latus),
Usernames: Usernames(latus),
}),
'@reddichat/state/defaultState': async (req, latus) => ({
user: await userDefaultState(req, latus),
usernames: await usernamesDefaultState(req, latus),
}),
},
};

View File

@ -0,0 +1,22 @@
import {Model, Types} from '@latus/db';
class Block extends Model {
static get attributes() {
return {
blocked: Types.INTEGER,
};
}
static associate({User}) {
User.hasMany(this);
this.belongsTo(User);
}
static get name() {
return 'Block';
}
}
export default Block;

View File

@ -0,0 +1,22 @@
import {Model, Types} from '@latus/db';
class Favorite extends Model {
static get attributes() {
return {
channel: Types.STRING,
};
}
static associate({User}) {
User.hasMany(this);
this.belongsTo(User);
}
static get name() {
return 'Favorite';
}
}
export default Favorite;

View File

@ -0,0 +1,33 @@
import {Model, Types} from '@latus/db';
class Friendship extends Model {
static get attributes() {
return {
status: {
type: Types.ENUM(['pending', 'active']),
defaultValue: 'pending',
},
};
}
static associate({User}) {
User.hasMany(this, {
as: 'adder',
foreignKey: 'adderId',
onDelete: 'CASCADE',
});
User.hasMany(this, {
as: 'addee',
foreignKey: 'addeeId',
onDelete: 'CASCADE',
});
}
static get name() {
return 'Friendship';
}
}
export default Friendship;

View File

@ -0,0 +1,36 @@
import {ModelMap, Op, Types} from '@latus/db';
import {parseChannel} from '@reddichat/core/client';
export default (User, latus) => class UserReddichat extends User {
static get attributes() {
return {
...super.atributes,
redditAccessToken: Types.STRING,
redditUsername: Types.STRING,
};
}
async blocks() {
return (await this.getBlocks()).map(({blocked}) => blocked);
}
async favorites() {
return (await this.getFavorites()).map(({channel}) => parseChannel(channel));
}
async friendship() {
const {Friendship} = ModelMap(latus);
const friendship = await Friendship.findAll({
where: {
[Op.or]: [
{adderId: this.id},
{addeeId: this.id},
],
},
});
return friendship.map(({adderId, addeeId, status}) => ({adderId, addeeId, status}));
}
};

View File

@ -0,0 +1,22 @@
import {Packet, ValidationError} from '@latus/socket/packets';
import {validateChannel} from '@reddichat/core/client';
export default () => class AddFavorite extends Packet {
static get data() {
return {
type: 'string',
name: 'string',
};
}
static async validate({data: channel}, {req: {user}}) {
if (!user) {
throw new ValidationError({code: 401, reason: 'Unauthorized'});
}
if (!validateChannel(channel)) {
throw new ValidationError({code: 400, reason: 'Malformed channel'});
}
}
};

View File

@ -0,0 +1,11 @@
import AddFavorite from './add-favorite';
export default () => class AddFavoriteServer extends AddFavorite() {
static async respond(packet, socket) {
const {req: {user}} = socket;
await user.createFavorite({channel: packet.data});
socket.to(`/u/${user.id}`, packet);
}
};

View File

@ -0,0 +1,42 @@
import {Packet, ValidationError} from '@latus/socket/packets';
import {validateUsername} from '@reddichat/core/client';
export default () => class AddFriend extends Packet {
constructor(...args) {
super(...args);
this.data = {
addeeId: 0,
adderId: 0,
...this.data,
};
}
static get data() {
return {
addeeId: 'uint32',
adderId: 'uint32',
name: 'string',
};
}
static limit = {
points: 20,
duration: 60,
};
static async validate({data: {addeeId, name}}, {req: {user}}) {
if (!user) {
throw new ValidationError({code: 401, reason: 'Unauthorized'});
}
if (0 === addeeId && '' === name) {
throw new ValidationError({code: 400, reason: 'Malformed'});
}
else if ('' !== name) {
if (!validateUsername(name)) {
throw new ValidationError({code: 400, reason: 'Malformed'});
}
}
}
};

View File

@ -0,0 +1,40 @@
import {ModelMap} from '@latus/db';
import AddFriend from './add-friend';
export default (latus) => class AddFriendServer extends AddFriend(latus) {
static async respond({data: {name}}, socket) {
const {req} = socket;
const {
Friendship,
User,
} = ModelMap(latus);
const adderId = req.user.id;
const adderName = req.user.redditUsername;
const user = (
await User.findOne({where: {redditUsername: name}})
|| await User.create({redditUsername: name})
);
const addeeId = user.id;
const addeeName = user.redditUsername;
const friendship = await Friendship.findOne(
{where: {addeeId: adderId, adderId: addeeId}},
);
if (friendship) {
friendship.status = 'active';
await friendship.save();
}
else {
await Friendship.create({addeeId, adderId});
}
[addeeId, adderId].forEach((id) => {
const packet = friendship
? ['ConfirmFriend', id === adderId ? addeeId : adderId]
: ['AddFriend', {addeeId, adderId, name: id === adderId ? addeeName : adderName}];
socket.to(`/u/${id}`, packet);
});
return addeeId;
}
};

View File

@ -0,0 +1,9 @@
import {Packet} from '@latus/socket/packets';
export default () => class Block extends Packet {
static get data() {
return 'uint32';
}
};

View File

@ -0,0 +1,39 @@
import {ModelMap, Op, ValidationError} from '@latus/db';
import removeFavoritedUser from '../remove-favorited-user';
import Block from './block';
export default (latus) => class BlockServer extends Block(latus) {
static async respond(packet, socket) {
const {req} = socket;
const id = packet.data;
const {Friendship, User} = ModelMap(latus);
await req.user.createBlock({blocked: id});
await Friendship.destroy({
where: {
[Op.or]: [
{[Op.and]: [{addeeId: req.userId}, {adderId: id}]},
{[Op.and]: [{addeeId: id}, {adderId: req.userId}]},
],
},
});
const user = await User.findByPk(id);
removeFavoritedUser(latus, socket, user, req.user);
removeFavoritedUser(latus, socket, req.user, user);
socket.to(`/u/${req.userId}`, packet);
socket.to(`/u/${req.userId}`, ['RemoveFriend', id]);
socket.to(`/u/${id}`, ['RemoveFriend', req.userId]);
}
static async validate({data: id}, {req: {user}}) {
if (!user) {
throw new ValidationError({code: 401, reason: 'Unauthorized'});
}
const {User} = ModelMap(latus);
if (!await User.count({where: {id}})) {
throw new ValidationError({code: 400, reason: 'No such user'});
}
}
};

View File

@ -0,0 +1,14 @@
import {Packet} from '@latus/socket/packets';
export default () => class ConfirmFriend extends Packet {
static get data() {
return 'uint32';
}
static limit = {
points: 20,
duration: 60,
};
};

View File

@ -0,0 +1,31 @@
import {ModelMap, ValidationError} from '@latus/db';
import ConfirmFriend from './confirm-friend';
export default (latus) => class ConfirmFriendServer extends ConfirmFriend(latus) {
static async respond({data: adderId}, socket) {
const {req} = socket;
const {Friendship} = ModelMap(latus);
const addeeId = req.user.id;
const friendship = await Friendship.findOne({where: {adderId, addeeId}});
friendship.status = 'active';
await friendship.save();
[addeeId, adderId].forEach((id) => {
socket.to(`/u/${id}`, ['ConfirmFriend', id === adderId ? addeeId : adderId]);
});
}
static async validate({data: adderId}, {req: {user}}) {
if (!user) {
throw new ValidationError({code: 401, reason: 'Unauthorized'});
}
const addeeId = user.id;
const {Friendship} = ModelMap(latus);
const friendship = await Friendship.findOne({where: {adderId, addeeId}});
if (!friendship) {
throw new ValidationError({code: 400, reason: 'Malformed'});
}
}
};

View File

@ -0,0 +1,9 @@
import {Packet} from '@latus/socket/packets';
export default () => class RemoveFavorite extends Packet {
static get data() {
return 'string';
}
};

View File

@ -0,0 +1,35 @@
import {ModelMap, ValidationError} from '@latus/db';
import {parseChannel, validateChannel} from '@reddichat/core';
import RemoveFavorite from './remove-favorite';
export default (latus) => class RemoveFavoriteServer extends RemoveFavorite(latus) {
static async respond(packet, socket) {
const {data: {channel}} = packet;
const {req} = socket;
const {Favorite} = ModelMap(latus);
const favorite = await Favorite.findOne({
where: {
channel: parseChannel(channel),
user_id: req.user.id,
},
});
await Favorite.destroy({where: {id: favorite.id}});
socket.to(`/u/${req.userId}`, packet);
}
static async validate({data: channel}, {req: {user}}) {
if (!user) {
throw new ValidationError({code: 401, reason: 'Unauthorized'});
}
const {Favorite} = ModelMap(latus);
if (!validateChannel(channel)) {
throw new ValidationError({code: 400, reason: 'Malformed channel.'});
}
if (0 === await Favorite.count({where: {user_id: user.id, channel: parseChannel(channel)}})) {
throw new ValidationError({code: 400, reason: 'No such favorite existed.'});
}
}
};

View File

@ -0,0 +1,9 @@
import {Packet} from '@latus/socket/packets';
export default () => class RemoveFriend extends Packet {
static get data() {
return 'uint32';
}
};

View File

@ -0,0 +1,46 @@
import {ModelMap, Op, ValidationError} from '@latus/db';
import removeFavoritedUser from '../remove-favorited-user';
import RemoveFriend from './remove-friend';
export default (latus) => class RemoveFriendServer extends RemoveFriend(latus) {
static async respond({data: id}, socket) {
const {req} = socket;
const {Friendship, User} = ModelMap(latus);
await Friendship.destroy({
where: {
[Op.or]: [
{[Op.and]: [{addeeId: req.userId}, {adderId: id}]},
{[Op.and]: [{addeeId: id}, {adderId: req.userId}]},
],
},
});
socket.to(`/u/${id}`, ['RemoveFriend', req.userId]);
socket.to(`/u/${req.userId}`, ['RemoveFriend', id]);
const user = await User.findByPk(id);
return Promise.all([
removeFavoritedUser(socket, user, req.user),
removeFavoritedUser(socket, req.user, user),
]);
}
static async validate({data: id}, {req: {user, userId}}) {
if (!user) {
throw new ValidationError({code: 401, reason: 'Unauthorized'});
}
const {Friendship} = ModelMap(latus);
const hasFriendship = !!await Friendship.count({
where: {
[Op.or]: [
{[Op.and]: [{addeeId: userId}, {adderId: id}]},
{[Op.and]: [{addeeId: id}, {adderId: userId}]},
],
},
});
if (!hasFriendship) {
throw new ValidationError({code: 400, reason: 'Malformed friendship.'});
}
}
};

View File

@ -0,0 +1,9 @@
import {Packet} from '@latus/socket/packets';
export default () => class Unblock extends Packet {
static get data() {
return 'uint32';
}
};

View File

@ -0,0 +1,36 @@
import {ModelMap, ValidationError} from '@latus/db';
import Unblock from './unblock';
export default (latus) => class UnblockServer extends Unblock(latus) {
static async respond(packet, socket) {
const {data: blocked} = packet;
const {req} = socket;
const {Block: BlockModel} = ModelMap(latus);
await BlockModel.destroy({
where: {
blocked,
user_id: req.userId,
},
});
socket.to(`/u/${req.userId}`, packet);
}
static async validate({data: blocked}, {req: {user}}) {
if (!user) {
throw new ValidationError({code: 401, reason: 'Unauthorized'});
}
const {Block: BlockModel} = ModelMap(latus);
const hasBlock = !!await BlockModel.count({
where: {
blocked,
user_id: user.id,
},
});
if (!hasBlock) {
throw new ValidationError({code: 400, reason: "Wasn't blocking."});
}
}
};

View File

@ -0,0 +1,11 @@
import {Packet} from '@latus/socket/packets';
export default () => class Usernames extends Packet {
static get data() {
return [
'uint32',
];
}
};

View File

@ -0,0 +1,14 @@
import {ModelMap} from '@latus/db';
import Usernames from './usernames';
export default (latus) => class UsernamesServer extends Usernames(latus) {
static async respond(packet) {
const {User} = ModelMap(latus);
return Promise.all(packet.data.map(
async (id) => (await User.findByPk(id)).redditUsername,
));
}
};

View File

@ -0,0 +1,12 @@
import {ModelMap} from '@latus/db';
export default async (latus, socket, user, other) => {
const {Favorite} = ModelMap(latus);
const favorite = await Favorite.findOne(
{where: {channel: `/u/${other.redditUsername}`, user_id: user.id}},
);
if (favorite) {
await Favorite.destroy({where: {id: favorite.id}});
socket.to(`/u/${user.id}`, ['RemoveFavorite', `/u/${other.redditUsername}`]);
}
};

View File

View File

@ -0,0 +1,20 @@
import {renderChannel} from '@reddichat/core';
import {channelsToHydrate} from '@reddichat/chat';
export default async (req, latus) => {
const {/* channel, */user} = req;
if (!user) {
return undefined;
}
const toHydrate = await channelsToHydrate(req, latus);
return {
// activeChannel: channel ? renderChannel(channel) : '',
blocked: user ? await user.blocks() : [],
favorites: (await user.favorites()).map(renderChannel),
friendship: user ? await user.friendships() : [],
id: user.id,
redditUsername: user.redditUsername,
recent: toHydrate.filter(({type}) => 'r' === type).map(({name}) => name),
};
};

View File

@ -0,0 +1,24 @@
import {ModelMap} from '@latus/db';
import {chatUserIds} from '@reddichat/chat';
export default async (req, latus) => {
const {user} = req;
const {User} = ModelMap(latus);
const allIds = Array.from(new Set(
[]
.concat(user ? await user.blocks() : [])
.concat(await chatUserIds(latus, req))
.concat(
(user ? await user.friendships() : [])
.reduce((r, {addeeId, adderId}) => ([...r, addeeId, adderId]), []),
),
).values());
return Object.fromEntries(await Promise.all(
allIds
.map(async (id) => [
id,
0 === id ? 'anonymous' : (await User.findByPk(id)).redditUsername,
]),
));
};

View File

@ -3,4 +3,6 @@
// Neutrino's inspect feature can be used to view/export the generated configuration.
const neutrino = require('neutrino');
module.exports = neutrino(require(`${__dirname}/.neutrinorc`)).webpack();
const configOfConfigs = require(`${__dirname}/.neutrinorc`);
const configs = Array.isArray(configOfConfigs) ? configOfConfigs : [configOfConfigs];
module.exports = configs.map((config) => neutrino(config).webpack());

8710
packages/user/yarn.lock Normal file

File diff suppressed because it is too large Load Diff