flow: lots of UI, limiting
This commit is contained in:
parent
7b3986657c
commit
050899b407
|
@ -2,47 +2,42 @@ import './bar.scss';
|
||||||
|
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, {useEffect, useState} from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
export default function Bar(props) {
|
export default function Bar(props) {
|
||||||
const {
|
const {
|
||||||
|
active,
|
||||||
branding,
|
branding,
|
||||||
buttons,
|
buttons,
|
||||||
isHorizontal,
|
className,
|
||||||
onActive,
|
onActive,
|
||||||
} = props;
|
} = props;
|
||||||
const [active, setActive] = useState(0);
|
|
||||||
useEffect(() => {
|
|
||||||
const clampedActive = Math.max(0, Math.min(buttons.length - 1, active));
|
|
||||||
if (active !== clampedActive) {
|
|
||||||
onActive(clampedActive, -1);
|
|
||||||
}
|
|
||||||
setActive(clampedActive);
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [active, buttons.length]);
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classnames(['bar', isHorizontal ? 'bar--horizontal' : 'bar--vertical'])}
|
className={classnames(['bar', className])}
|
||||||
>
|
>
|
||||||
<ul className="bar__buttons">
|
<ul className="bar__buttons">
|
||||||
{branding}
|
{branding}
|
||||||
{
|
{
|
||||||
buttons.map(({count, icon, label}, i) => (
|
buttons.map(({
|
||||||
// eslint-disable-next-line react/no-array-index-key
|
count,
|
||||||
<li className="bar__buttonItem" key={i}>
|
icon,
|
||||||
|
label,
|
||||||
|
}, i) => (
|
||||||
|
<li
|
||||||
|
className="bar__buttonItem"
|
||||||
|
key={label}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
className={classnames('bar__button', {active: i === active})}
|
className={classnames('bar__button', {active: i === active})}
|
||||||
onClick={() => {
|
onClick={() => onActive(i, active)}
|
||||||
onActive(i, active);
|
title={label}
|
||||||
setActive(i);
|
|
||||||
}}
|
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="bar__buttonIcon"
|
className="bar__buttonIcon"
|
||||||
role="img"
|
role="img"
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
title={label}
|
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
</span>
|
</span>
|
||||||
|
@ -57,17 +52,19 @@ export default function Bar(props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
Bar.defaultProps = {
|
Bar.defaultProps = {
|
||||||
|
active: 0,
|
||||||
branding: null,
|
branding: null,
|
||||||
isHorizontal: true,
|
className: '',
|
||||||
onActive: () => {},
|
onActive: () => {},
|
||||||
};
|
};
|
||||||
|
|
||||||
Bar.propTypes = {
|
Bar.propTypes = {
|
||||||
|
active: PropTypes.number,
|
||||||
branding: PropTypes.node,
|
branding: PropTypes.node,
|
||||||
buttons: PropTypes.arrayOf(PropTypes.shape({
|
buttons: PropTypes.arrayOf(PropTypes.shape({
|
||||||
label: PropTypes.string,
|
label: PropTypes.string,
|
||||||
icon: PropTypes.string,
|
icon: PropTypes.string,
|
||||||
})).isRequired,
|
})).isRequired,
|
||||||
isHorizontal: PropTypes.bool,
|
className: PropTypes.string,
|
||||||
onActive: PropTypes.func,
|
onActive: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,13 +1,7 @@
|
||||||
@import '~/client/scss/colors.scss';
|
@import '~/client/scss/colors.scss';
|
||||||
|
|
||||||
.bar--horizontal {
|
.bar {
|
||||||
height: 4em;
|
height: 3em;
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bar--vertical {
|
|
||||||
height: 100%;
|
|
||||||
width: 4em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bar__items {
|
.bar__items {
|
||||||
|
@ -55,10 +49,10 @@
|
||||||
.bar__button {
|
.bar__button {
|
||||||
background-color: #303030;
|
background-color: #303030;
|
||||||
border: none;
|
border: none;
|
||||||
height: 3em;
|
height: 2em;
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: 0.1s background-color;
|
transition: 0.1s background-color;
|
||||||
width: 3em;
|
width: 2em;
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #2a2a2a;
|
background-color: #2a2a2a;
|
||||||
}
|
}
|
||||||
|
@ -82,7 +76,7 @@
|
||||||
|
|
||||||
.bar__buttonIcon {
|
.bar__buttonIcon {
|
||||||
display: flow-root;
|
display: flow-root;
|
||||||
font-size: 1.5em;
|
font-size: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bar__buttonText {
|
.bar__buttonText {
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
|
|
||||||
.channel {
|
.channel {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.channel__link {
|
.channel__link {
|
||||||
|
|
|
@ -1,14 +1,105 @@
|
||||||
import './chat--center.scss';
|
import './chat--center.scss';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import {useDispatch, useSelector} from 'react-redux';
|
||||||
|
|
||||||
|
import {
|
||||||
|
leftActiveIndexSelector,
|
||||||
|
rightActiveIndexSelector,
|
||||||
|
setLeftActiveIndex,
|
||||||
|
setRightActiveIndex,
|
||||||
|
toggleLeftIsOpen,
|
||||||
|
toggleRightIsOpen,
|
||||||
|
} from '~/common/state/app';
|
||||||
|
import {
|
||||||
|
channelUsersSelector,
|
||||||
|
} from '~/common/state/chat';
|
||||||
|
import {
|
||||||
|
blockedSelector,
|
||||||
|
idSelector,
|
||||||
|
pendingFriendshipSelector,
|
||||||
|
unreadChannelSelector,
|
||||||
|
unreadUserSelector,
|
||||||
|
} from '~/common/state/user';
|
||||||
|
|
||||||
|
import useChannel from '~/client/hooks/useChannel';
|
||||||
|
|
||||||
|
import Bar from './bar';
|
||||||
import ChatMessages from './chat--messages';
|
import ChatMessages from './chat--messages';
|
||||||
|
|
||||||
export default function ChatCenter() {
|
export default function ChatCenter() {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const leftActiveIndex = useSelector(leftActiveIndexSelector);
|
||||||
|
const rightActiveIndex = useSelector(rightActiveIndexSelector);
|
||||||
|
const blockedIds = useSelector(blockedSelector);
|
||||||
|
const channel = useChannel();
|
||||||
|
const channelUsers = useSelector((state) => channelUsersSelector(state, channel));
|
||||||
|
const id = useSelector(idSelector);
|
||||||
|
const pendingFriendship = useSelector(pendingFriendshipSelector);
|
||||||
|
const unreadChannel = useSelector(unreadChannelSelector);
|
||||||
|
const unreadUser = useSelector(unreadUserSelector);
|
||||||
|
const leftButtons = [
|
||||||
|
{
|
||||||
|
count: unreadChannel,
|
||||||
|
icon: '💬',
|
||||||
|
label: 'Chat',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
count: unreadUser + pendingFriendship.filter(({addeeId}) => addeeId === id).length,
|
||||||
|
icon: '😁',
|
||||||
|
label: 'Friends',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const rightButtons = []
|
||||||
|
.concat(channelUsers.length > 0 ? [{icon: '🙃', label: 'Present'}] : [])
|
||||||
|
.concat(blockedIds.length > 0 ? [{icon: '☢️', label: 'Blocked'}] : []);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="center flexed"
|
className="center flexed"
|
||||||
>
|
>
|
||||||
|
<div className="center__header">
|
||||||
|
<Bar
|
||||||
|
active={leftActiveIndex}
|
||||||
|
branding={(
|
||||||
|
<li className="bar__brandItem">
|
||||||
|
<div>
|
||||||
|
reddi
|
||||||
|
<span className="muted">?</span>
|
||||||
|
</div>
|
||||||
|
{' '}
|
||||||
|
<div>
|
||||||
|
chat
|
||||||
|
<span className="muted">!</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
buttons={leftButtons}
|
||||||
|
className="bar--left"
|
||||||
|
isHorizontal
|
||||||
|
onActive={(active, i) => {
|
||||||
|
if (i === active) {
|
||||||
|
dispatch(toggleLeftIsOpen());
|
||||||
|
}
|
||||||
|
dispatch(setLeftActiveIndex(active));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="center__headerChannel">
|
||||||
|
<span className="tiny">chatting in</span>
|
||||||
|
<span>/r/reddichat</span>
|
||||||
|
</div>
|
||||||
|
<Bar
|
||||||
|
active={rightActiveIndex}
|
||||||
|
buttons={rightButtons}
|
||||||
|
className="bar--right"
|
||||||
|
isHorizontal
|
||||||
|
onActive={(active, i) => {
|
||||||
|
if (i === active) {
|
||||||
|
dispatch(toggleRightIsOpen());
|
||||||
|
}
|
||||||
|
dispatch(setRightActiveIndex(active));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<ChatMessages />
|
<ChatMessages />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -9,3 +9,25 @@
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.center__header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
height: 3em;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center__headerChannel {
|
||||||
|
align-self: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-family: var(--thick-title-font-family);
|
||||||
|
font-weight: bold;
|
||||||
|
user-select: none;
|
||||||
|
.tiny {
|
||||||
|
font-family: var(--title-font-family);
|
||||||
|
font-size: 0.7em;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,70 +1,27 @@
|
||||||
import './chat--left.scss';
|
import './chat--left.scss';
|
||||||
|
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import React, {useState} from 'react';
|
import React from 'react';
|
||||||
import {useSelector} from 'react-redux';
|
import {useSelector} from 'react-redux';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
idSelector,
|
leftActiveIndexSelector,
|
||||||
pendingFriendshipSelector,
|
leftIsOpenSelector,
|
||||||
unreadChannelSelector,
|
} from '~/common/state/app';
|
||||||
unreadUserSelector,
|
|
||||||
} from '~/common/state/user';
|
|
||||||
|
|
||||||
import useBreakpoints from './hooks/useBreakpoints';
|
import useBreakpoints from './hooks/useBreakpoints';
|
||||||
import Bar from './bar';
|
|
||||||
import ChatLeftFriends from './chat--leftFriends';
|
import ChatLeftFriends from './chat--leftFriends';
|
||||||
import ChatLeftRooms from './chat--leftRooms';
|
import ChatLeftRooms from './chat--leftRooms';
|
||||||
|
|
||||||
export default function ChatLeft() {
|
export default function ChatLeft() {
|
||||||
const {desktop, tablet} = useBreakpoints();
|
const active = useSelector(leftActiveIndexSelector);
|
||||||
const [active, setActive] = useState(0);
|
const isOpen = useSelector(leftIsOpenSelector);
|
||||||
const [isOpen, setIsOpen] = useState(true);
|
const {tablet} = useBreakpoints();
|
||||||
const toggleOpen = () => setIsOpen(!isOpen);
|
const showsAsOpen = isOpen || !tablet;
|
||||||
const id = useSelector(idSelector);
|
|
||||||
const pendingFriendship = useSelector(pendingFriendshipSelector);
|
|
||||||
const showsAsOpen = isOpen || desktop || !tablet;
|
|
||||||
const unreadChannel = useSelector(unreadChannelSelector);
|
|
||||||
const unreadUser = useSelector(unreadUserSelector);
|
|
||||||
const buttons = [
|
|
||||||
{
|
|
||||||
count: unreadChannel,
|
|
||||||
icon: '💬',
|
|
||||||
label: 'Chat',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
count: unreadUser + pendingFriendship.filter(({addeeId}) => addeeId === id).length,
|
|
||||||
icon: '😁',
|
|
||||||
label: 'Friends',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classnames('left', 'flexed', showsAsOpen ? 'open' : 'closed')}
|
className={classnames('left', 'flexed', showsAsOpen ? 'open' : 'closed')}
|
||||||
>
|
>
|
||||||
<Bar
|
|
||||||
branding={(
|
|
||||||
<li className="bar__brandItem">
|
|
||||||
<div>
|
|
||||||
reddi
|
|
||||||
<span className="muted">?</span>
|
|
||||||
</div>
|
|
||||||
{' '}
|
|
||||||
<div>
|
|
||||||
chat
|
|
||||||
<span className="muted">!</span>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
buttons={buttons}
|
|
||||||
isHorizontal={showsAsOpen}
|
|
||||||
onActive={(active, i) => {
|
|
||||||
if (i === active || !isOpen) {
|
|
||||||
toggleOpen();
|
|
||||||
}
|
|
||||||
setActive(active);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{0 === active && <ChatLeftRooms />}
|
{0 === active && <ChatLeftRooms />}
|
||||||
{1 === active && <ChatLeftFriends />}
|
{1 === active && <ChatLeftFriends />}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
.left {
|
.left {
|
||||||
background-color: #373737;
|
background-color: #373737;
|
||||||
|
flex-shrink: 0;
|
||||||
transform: translateX(-100%);
|
transform: translateX(-100%);
|
||||||
transition: 0.2s width;
|
transition: 0.2s width;
|
||||||
@include breakpoint(tablet) {
|
@include breakpoint(tablet) {
|
||||||
|
|
|
@ -47,7 +47,7 @@ header {
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat--messageOwner {
|
.chat--messageOwner {
|
||||||
color: #d68030aa;
|
color: lighten(#d68030aa, 15%);
|
||||||
font-family: Caladea, 'Times New Roman', Times, serif;
|
font-family: Caladea, 'Times New Roman', Times, serif;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-bottom: 0.25rem;
|
margin-bottom: 0.25rem;
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 100%;
|
height: calc(100% - 3em);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat--messagesSmoosh {
|
.chat--messagesSmoosh {
|
||||||
|
|
|
@ -1,28 +1,30 @@
|
||||||
import './chat--right.scss';
|
import './chat--right.scss';
|
||||||
|
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import React, {useState} from 'react';
|
import React from 'react';
|
||||||
import {useSelector} from 'react-redux';
|
import {useSelector} from 'react-redux';
|
||||||
|
|
||||||
|
import {
|
||||||
|
rightActiveIndexSelector,
|
||||||
|
rightIsOpenSelector,
|
||||||
|
} from '~/common/state/app';
|
||||||
import {channelUsersSelector} from '~/common/state/chat';
|
import {channelUsersSelector} from '~/common/state/chat';
|
||||||
import {blockedSelector} from '~/common/state/user';
|
import {blockedSelector} from '~/common/state/user';
|
||||||
|
|
||||||
import useBreakpoints from '~/client/hooks/useBreakpoints';
|
import useBreakpoints from './hooks/useBreakpoints';
|
||||||
import useChannel from '~/client/hooks/useChannel';
|
import useChannel from '~/client/hooks/useChannel';
|
||||||
|
|
||||||
import Bar from './bar';
|
|
||||||
import ChatRightBlocked from './chat--rightBlocked';
|
import ChatRightBlocked from './chat--rightBlocked';
|
||||||
import ChatRightUsers from './chat--rightUsers';
|
import ChatRightUsers from './chat--rightUsers';
|
||||||
|
|
||||||
export default function ChatRight() {
|
export default function ChatRight() {
|
||||||
const {desktop, tablet} = useBreakpoints();
|
const active = useSelector(rightActiveIndexSelector);
|
||||||
const [active, setActive] = useState(0);
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const channel = useChannel();
|
const channel = useChannel();
|
||||||
const blockedIds = useSelector(blockedSelector);
|
const blockedIds = useSelector(blockedSelector);
|
||||||
const channelUsers = useSelector((state) => channelUsersSelector(state, channel));
|
const channelUsers = useSelector((state) => channelUsersSelector(state, channel));
|
||||||
const toggleOpen = () => setIsOpen(!isOpen);
|
const isOpen = useSelector(rightIsOpenSelector);
|
||||||
const showsAsOpen = isOpen || desktop || !tablet;
|
const {tablet} = useBreakpoints();
|
||||||
|
const showsAsOpen = isOpen || !tablet;
|
||||||
const buttons = []
|
const buttons = []
|
||||||
.concat(channelUsers.length > 0 ? [{icon: '🙃', label: 'Present'}] : [])
|
.concat(channelUsers.length > 0 ? [{icon: '🙃', label: 'Present'}] : [])
|
||||||
.concat(blockedIds.length > 0 ? [{icon: '☢️', label: 'Blocked'}] : []);
|
.concat(blockedIds.length > 0 ? [{icon: '☢️', label: 'Blocked'}] : []);
|
||||||
|
@ -33,16 +35,6 @@ export default function ChatRight() {
|
||||||
<div
|
<div
|
||||||
className={classnames('right', 'flexed', showsAsOpen ? 'open' : 'closed')}
|
className={classnames('right', 'flexed', showsAsOpen ? 'open' : 'closed')}
|
||||||
>
|
>
|
||||||
<Bar
|
|
||||||
buttons={buttons}
|
|
||||||
isHorizontal={showsAsOpen}
|
|
||||||
onActive={(active, i) => {
|
|
||||||
if (i === active || !isOpen) {
|
|
||||||
toggleOpen();
|
|
||||||
}
|
|
||||||
setActive(active);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{0 === active && channelUsers.length > 0 && <ChatRightUsers />}
|
{0 === active && channelUsers.length > 0 && <ChatRightUsers />}
|
||||||
{1 === active && blockedIds.length > 0 && <ChatRightBlocked ids={blockedIds} />}
|
{1 === active && blockedIds.length > 0 && <ChatRightBlocked ids={blockedIds} />}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
.right {
|
.right {
|
||||||
background-color: #373737;
|
background-color: #373737;
|
||||||
|
flex-shrink: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
transform: translateX(100%);
|
transform: translateX(100%);
|
||||||
transition: 0.2s width;
|
transition: 0.2s width;
|
||||||
|
|
|
@ -20,15 +20,19 @@ export default function ChatSubmitMessage() {
|
||||||
<form
|
<form
|
||||||
onSubmit={(event) => {
|
onSubmit={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const message = text.slice(0, 1000).trim();
|
let caret = 0;
|
||||||
if (message) {
|
const message = text.trim();
|
||||||
|
let chunk;
|
||||||
|
// eslint-disable-next-line no-cond-assign
|
||||||
|
while (chunk = message.substr(caret, 512)) {
|
||||||
dispatch(submitMessage({
|
dispatch(submitMessage({
|
||||||
channel,
|
channel,
|
||||||
message,
|
message: chunk,
|
||||||
owner: '/r/anonymous' === channel ? 0 : user.id,
|
owner: '/r/anonymous' === channel ? 0 : user.id,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
uuid: uuidv4(),
|
uuid: uuidv4(),
|
||||||
}));
|
}));
|
||||||
|
caret += 512;
|
||||||
}
|
}
|
||||||
setText('');
|
setText('');
|
||||||
}}
|
}}
|
||||||
|
@ -38,7 +42,6 @@ export default function ChatSubmitMessage() {
|
||||||
className="chat--messagesTextarea"
|
className="chat--messagesTextarea"
|
||||||
name="message"
|
name="message"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
maxLength="1000"
|
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setText(event.target.value);
|
setText(event.target.value);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
.flexed {
|
.flexed {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 4em;
|
width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
&:not(.center) {
|
&:not(.center) {
|
||||||
max-width: calc(100% - 2em);
|
max-width: calc(100% - 2em);
|
||||||
|
|
58
src/client/store/effects.js
vendored
58
src/client/store/effects.js
vendored
|
@ -1,3 +1,4 @@
|
||||||
|
import RateLimiterMemory from 'rate-limiter-flexible/lib/RateLimiterMemory';
|
||||||
import {v4 as uuidv4} from 'uuid';
|
import {v4 as uuidv4} from 'uuid';
|
||||||
|
|
||||||
import AddFavorite from '../../common/packets/add-favorite.packet';
|
import AddFavorite from '../../common/packets/add-favorite.packet';
|
||||||
|
@ -43,6 +44,9 @@ import {
|
||||||
|
|
||||||
import {socket} from '~/client/hooks/useSocket';
|
import {socket} from '~/client/hooks/useSocket';
|
||||||
|
|
||||||
|
const characterLimiter = new RateLimiterMemory({points: 2048, duration: 2});
|
||||||
|
const messageLimiter = new RateLimiterMemory({points: 10, duration: 15});
|
||||||
|
|
||||||
const effects = {
|
const effects = {
|
||||||
[addFriendship]: ({dispatch}, {payload: {addeeId, adderId}}) => {
|
[addFriendship]: ({dispatch}, {payload: {addeeId, adderId}}) => {
|
||||||
dispatch(fetchUsernames([addeeId, adderId]));
|
dispatch(fetchUsernames([addeeId, adderId]));
|
||||||
|
@ -99,31 +103,41 @@ const effects = {
|
||||||
const {channel} = payload;
|
const {channel} = payload;
|
||||||
socket.send(new Leave(payload), () => dispatch(leave({channel})));
|
socket.send(new Leave(payload), () => dispatch(leave({channel})));
|
||||||
},
|
},
|
||||||
[submitMessage]: ({dispatch}, {payload}) => {
|
[submitMessage]: async ({dispatch}, {payload}) => {
|
||||||
dispatch(addMessage(payload));
|
dispatch(addMessage(payload));
|
||||||
socket.send(new Message(payload), (error, result) => {
|
const reject = (ttr) => {
|
||||||
if (error) {
|
dispatch(rejectMessage(payload.uuid));
|
||||||
switch (error.code) {
|
dispatch(addMessage({
|
||||||
case 429: {
|
...payload,
|
||||||
dispatch(rejectMessage(payload.uuid));
|
message: [
|
||||||
dispatch(addMessage({
|
'You are sending too much.',
|
||||||
...payload,
|
`Try again in ${ttr} second${1 === ttr ? '' : 's'}.`,
|
||||||
message: [
|
].join(' '),
|
||||||
'You are sending too many messages.',
|
owner: -1,
|
||||||
`Try again in ${error.ttr} second${1 === error.ttr ? '' : 's'}.`,
|
uuid: uuidv4(),
|
||||||
].join(' '),
|
}));
|
||||||
owner: -1,
|
};
|
||||||
uuid: uuidv4(),
|
try {
|
||||||
}));
|
await characterLimiter.consume('', payload.message.length);
|
||||||
break;
|
await messageLimiter.consume('');
|
||||||
|
socket.send(new Message(payload), (error, result) => {
|
||||||
|
if (error) {
|
||||||
|
switch (error.code) {
|
||||||
|
case 429: {
|
||||||
|
reject(error.ttr);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
}
|
}
|
||||||
default:
|
return;
|
||||||
}
|
}
|
||||||
return;
|
const [timestamp, current] = result;
|
||||||
}
|
dispatch(confirmMessage({current, previous: payload.uuid, timestamp}));
|
||||||
const [timestamp, current] = result;
|
});
|
||||||
dispatch(confirmMessage({current, previous: payload.uuid, timestamp}));
|
}
|
||||||
});
|
catch (error) {
|
||||||
|
reject(Math.round(Math.max(0, error.msBeforeNext) / 1000) || 1);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[submitRemoveFavorite]: ({dispatch}, {payload}) => {
|
[submitRemoveFavorite]: ({dispatch}, {payload}) => {
|
||||||
dispatch(removeFromFavorites(payload));
|
dispatch(removeFromFavorites(payload));
|
||||||
|
|
|
@ -2,8 +2,10 @@ import merge from 'deepmerge';
|
||||||
import {combineReducers} from 'redux';
|
import {combineReducers} from 'redux';
|
||||||
import {connectRouter, routerMiddleware} from 'connected-react-router';
|
import {connectRouter, routerMiddleware} from 'connected-react-router';
|
||||||
|
|
||||||
|
import app from '~/common/state/app';
|
||||||
import chat from '~/common/state/chat';
|
import chat from '~/common/state/chat';
|
||||||
import createHistory from '~/common/state/history';
|
import createHistory from '~/common/state/history';
|
||||||
|
import {storageSubscription} from '~/common/state/storage';
|
||||||
import user from '~/common/state/user';
|
import user from '~/common/state/user';
|
||||||
import usernames from '~/common/state/usernames';
|
import usernames from '~/common/state/usernames';
|
||||||
import createCommonStore from '~/common/store';
|
import createCommonStore from '~/common/store';
|
||||||
|
@ -13,13 +15,14 @@ import {middleware as effectsMiddleware} from './effects';
|
||||||
export default function createStore(options = {}) {
|
export default function createStore(options = {}) {
|
||||||
const {history} = options;
|
const {history} = options;
|
||||||
const reducer = combineReducers({
|
const reducer = combineReducers({
|
||||||
|
app,
|
||||||
chat,
|
chat,
|
||||||
history: createHistory(history),
|
history: createHistory(history),
|
||||||
router: connectRouter(history),
|
router: connectRouter(history),
|
||||||
user,
|
user,
|
||||||
usernames,
|
usernames,
|
||||||
});
|
});
|
||||||
return createCommonStore(
|
const store = createCommonStore(
|
||||||
merge(
|
merge(
|
||||||
options,
|
options,
|
||||||
{
|
{
|
||||||
|
@ -31,4 +34,6 @@ export default function createStore(options = {}) {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
store.subscribe(storageSubscription(store));
|
||||||
|
return store;
|
||||||
}
|
}
|
||||||
|
|
61
src/common/state/app.js
Normal file
61
src/common/state/app.js
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import {createSlice, createSelector} from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
import storage from './storage';
|
||||||
|
|
||||||
|
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: 'app',
|
||||||
|
initialState: storage(appSelector) || {
|
||||||
|
leftActiveIndex: 0,
|
||||||
|
leftIsOpen: true,
|
||||||
|
rightActiveIndex: 0,
|
||||||
|
rightIsOpen: true,
|
||||||
|
},
|
||||||
|
/* 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 */
|
||||||
|
});
|
||||||
|
|
||||||
|
export const {
|
||||||
|
setLeftActiveIndex,
|
||||||
|
setRightActiveIndex,
|
||||||
|
toggleLeftIsOpen,
|
||||||
|
toggleRightIsOpen,
|
||||||
|
} = slice.actions;
|
||||||
|
|
||||||
|
export default slice.reducer;
|
13
src/common/state/storage.js
Normal file
13
src/common/state/storage.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import throttle from 'lodash.throttle';
|
||||||
|
|
||||||
|
export const storageSubscription = (store) => (
|
||||||
|
throttle(() => localStorage.setItem('redux-state', JSON.stringify(store.getState())), 1000)
|
||||||
|
);
|
||||||
|
|
||||||
|
export default (selector) => {
|
||||||
|
const state = localStorage.getItem('redux-state');
|
||||||
|
if (!state) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return selector(JSON.parse(state));
|
||||||
|
};
|
|
@ -1,7 +1,7 @@
|
||||||
/* eslint-disable import/no-extraneous-dependencies */
|
/* eslint-disable import/no-extraneous-dependencies */
|
||||||
import {createReadStream} from 'fs';
|
import {createReadStream} from 'fs';
|
||||||
import http, {ServerResponse} from 'http';
|
import http, {ServerResponse} from 'http';
|
||||||
import {join, resolve} from 'path';
|
import {join} from 'path';
|
||||||
|
|
||||||
import concat from 'concat-stream-p';
|
import concat from 'concat-stream-p';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
|
|
|
@ -3,19 +3,23 @@ import {v4 as uuidv4} from 'uuid';
|
||||||
import {parseChannel, validateChannel} from '~/common/channel';
|
import {parseChannel, validateChannel} from '~/common/channel';
|
||||||
import Message from '~/common/packets/message.packet';
|
import Message from '~/common/packets/message.packet';
|
||||||
|
|
||||||
|
import createLimiter from '~/server/limiter';
|
||||||
import {allModels} from '~/server/models/registrar';
|
import {allModels} from '~/server/models/registrar';
|
||||||
|
|
||||||
import ValidationError from './validation-error';
|
import ValidationError from './validation-error';
|
||||||
|
|
||||||
|
const characterLimiter = createLimiter({keyPrefix: 'characterLimiter', points: 2048, duration: 2});
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Packet: Message,
|
Packet: Message,
|
||||||
limiter: {points: 10, duration: 15},
|
limiter: {points: 10, duration: 15},
|
||||||
validator: async ({data: {channel, message}}) => {
|
validator: async ({data: {channel, message}}, socket) => {
|
||||||
|
await characterLimiter.consume(socket.id, message.length);
|
||||||
if (!validateChannel(channel)) {
|
if (!validateChannel(channel)) {
|
||||||
throw new ValidationError({code: 400, reason: 'Malformed channel'});
|
throw new ValidationError({code: 400, reason: 'Malformed channel'});
|
||||||
}
|
}
|
||||||
if (message.length > 1024) {
|
if (message.length > 512) {
|
||||||
throw new ValidationError({code: 400, reason: 'Your message was a bit too long'});
|
throw new ValidationError({code: 400, reason: 'Message larger than 512 bytes'});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
responder: async ({data}, socket) => {
|
responder: async ({data}, socket) => {
|
||||||
|
|
|
@ -18,7 +18,8 @@ export default {
|
||||||
throw new ValidationError({code: 400, reason: "Wasn't blocking."});
|
throw new ValidationError({code: 400, reason: "Wasn't blocking."});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
responder: async ({data: blocked}, socket) => {
|
responder: async (packet, socket) => {
|
||||||
|
const {data: blocked} = packet;
|
||||||
const {req} = socket;
|
const {req} = socket;
|
||||||
const {Block: BlockModel} = allModels();
|
const {Block: BlockModel} = allModels();
|
||||||
await BlockModel.destroy({
|
await BlockModel.destroy({
|
||||||
|
|
Loading…
Reference in New Issue
Block a user