chore: components in
This commit is contained in:
parent
ddec5d2283
commit
9ffb5da0b5
57
app/src/react/components/actions/index.jsx
Normal file
57
app/src/react/components/actions/index.jsx
Normal file
|
@ -0,0 +1,57 @@
|
|||
import './index.scss';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
export default function Actions(props) {
|
||||
const {
|
||||
actions,
|
||||
} = props;
|
||||
return (
|
||||
<div
|
||||
className="actions"
|
||||
>
|
||||
{actions.map(([icon, label, onClick]) => (
|
||||
<button
|
||||
className="action"
|
||||
key={label}
|
||||
onClick={onClick}
|
||||
title={label}
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
className="action__icon"
|
||||
role="img"
|
||||
aria-label={label}
|
||||
>
|
||||
{icon}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const actionsPropValidator = (propValue, key, componentName, location, propFullName) => {
|
||||
const prop = propValue[key];
|
||||
const propError = (message) => (
|
||||
new Error(`Invalid prop '${propFullName}' passed to '${componentName}'. ${message}.`)
|
||||
);
|
||||
if (!Array.isArray(prop)) {
|
||||
return propError('Expected an array');
|
||||
}
|
||||
if ('string' !== typeof prop[0]) {
|
||||
return propError('First element must be a string');
|
||||
}
|
||||
if ('string' !== typeof prop[1]) {
|
||||
return propError('Second element must be a string');
|
||||
}
|
||||
if ('function' !== typeof prop[2]) {
|
||||
return propError('Third element must be a function');
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
Actions.propTypes = {
|
||||
actions: PropTypes.arrayOf(actionsPropValidator).isRequired,
|
||||
};
|
19
app/src/react/components/actions/index.scss
Normal file
19
app/src/react/components/actions/index.scss
Normal file
|
@ -0,0 +1,19 @@
|
|||
@import 'scss/breakpoints.scss';
|
||||
|
||||
.actions {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
padding-right: 0.5em;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
transition: 0.2s right;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.action {
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
color: inherit;
|
||||
opacity: 0;
|
||||
}
|
|
@ -19,7 +19,7 @@ const App = () => {
|
|||
const isAnonymous = useSelector(isAnonymousSelector);
|
||||
return (
|
||||
<div className="app">
|
||||
{!isAnonymous && <Left />}
|
||||
{<Left />}
|
||||
<Router history={history}>
|
||||
<Switch>
|
||||
<Route
|
||||
|
|
69
app/src/react/components/bar/index.jsx
Normal file
69
app/src/react/components/bar/index.jsx
Normal file
|
@ -0,0 +1,69 @@
|
|||
import './index.scss';
|
||||
|
||||
import classnames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
export default function Bar(props) {
|
||||
const {
|
||||
active,
|
||||
buttons,
|
||||
className,
|
||||
onActive,
|
||||
} = props;
|
||||
if (buttons.length < 2) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={classnames(['bar', className])}
|
||||
>
|
||||
<ul className="bar__buttons">
|
||||
{
|
||||
buttons.map(({
|
||||
count,
|
||||
icon,
|
||||
label,
|
||||
}, i) => (
|
||||
<li
|
||||
className="bar__buttonItem"
|
||||
key={label}
|
||||
>
|
||||
<button
|
||||
className={classnames('bar__button', {active: i === active})}
|
||||
onClick={() => onActive(i, active)}
|
||||
title={label}
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
className="bar__buttonIcon"
|
||||
role="img"
|
||||
aria-label={label}
|
||||
>
|
||||
{icon}
|
||||
</span>
|
||||
{count ? <span className="unread">{count}</span> : null}
|
||||
</button>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Bar.defaultProps = {
|
||||
active: 0,
|
||||
className: '',
|
||||
onActive: () => {},
|
||||
};
|
||||
|
||||
Bar.propTypes = {
|
||||
active: PropTypes.number,
|
||||
buttons: PropTypes.arrayOf(PropTypes.shape({
|
||||
label: PropTypes.string,
|
||||
icon: PropTypes.string,
|
||||
})).isRequired,
|
||||
className: PropTypes.string,
|
||||
onActive: PropTypes.func,
|
||||
};
|
62
app/src/react/components/bar/index.scss
Normal file
62
app/src/react/components/bar/index.scss
Normal file
|
@ -0,0 +1,62 @@
|
|||
@import 'scss/colors.scss';
|
||||
|
||||
.bar {
|
||||
height: 3em;
|
||||
}
|
||||
|
||||
.bar__items {
|
||||
display: flex;
|
||||
.channels--closed & {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.bar__buttons {
|
||||
display: flex;
|
||||
.closed & {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.bar__buttonItem {
|
||||
position: relative;
|
||||
padding: 0.5em;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.bar__button {
|
||||
background-color: #303030;
|
||||
border: none;
|
||||
height: 2em;
|
||||
position: relative;
|
||||
transition: 0.1s background-color;
|
||||
width: 2em;
|
||||
&:hover {
|
||||
background-color: #2a2a2a;
|
||||
}
|
||||
&.active {
|
||||
background-color: $color-active;
|
||||
}
|
||||
.closed &.active {
|
||||
background-color: #303030;
|
||||
transition: none;
|
||||
&:hover {
|
||||
background-color: #2a2a2a;
|
||||
}
|
||||
}
|
||||
.unread {
|
||||
bottom: -0.2em;
|
||||
position: absolute;
|
||||
right: -0.8em;
|
||||
z-index: 100;
|
||||
}
|
||||
}
|
||||
|
||||
.bar__buttonIcon {
|
||||
display: flow-root;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.bar__buttonText {
|
||||
font-size: 0.7em;
|
||||
}
|
22
app/src/react/components/branding/index.jsx
Normal file
22
app/src/react/components/branding/index.jsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
import './index.scss';
|
||||
|
||||
import React from 'react';
|
||||
import {Link} from 'react-router-dom';
|
||||
|
||||
export default function Branding() {
|
||||
return (
|
||||
<Link className="branding" to="/">
|
||||
<div>
|
||||
reddi
|
||||
<span className="muted">?</span>
|
||||
</div>
|
||||
{' '}
|
||||
<div>
|
||||
chat
|
||||
<span className="muted">!</span>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
Branding.propTypes = {};
|
26
app/src/react/components/branding/index.scss
Normal file
26
app/src/react/components/branding/index.scss
Normal file
|
@ -0,0 +1,26 @@
|
|||
@import 'scss/colors.scss';
|
||||
|
||||
.branding {
|
||||
align-items: center;
|
||||
color: $color-muted;
|
||||
display: inline-block;
|
||||
font-family: var(--thick-title-font-family);
|
||||
font-size: 0.95em;
|
||||
justify-content: space-around;
|
||||
padding: 1em;
|
||||
text-decoration: none;
|
||||
user-select: none;
|
||||
.closed & {
|
||||
flex-direction: column;
|
||||
font-size: 0.9em;
|
||||
text-align: center;
|
||||
padding: 1em 0 0.5em;
|
||||
}
|
||||
.muted {
|
||||
opacity: 0.5;
|
||||
}
|
||||
div {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
64
app/src/react/components/channel/index.jsx
Normal file
64
app/src/react/components/channel/index.jsx
Normal file
|
@ -0,0 +1,64 @@
|
|||
import './index.scss';
|
||||
|
||||
import classnames from 'classnames';
|
||||
import {getLocation} from 'connected-react-router';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import {Link} from 'react-router-dom';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import {unreadForChannelSelector} from '@reddichat/user/client';
|
||||
|
||||
import Actions, {actionsPropValidator} from 'components/actions';
|
||||
|
||||
export default function Channel(props) {
|
||||
const {
|
||||
actions,
|
||||
href,
|
||||
name,
|
||||
type,
|
||||
} = props;
|
||||
const location = useSelector(getLocation);
|
||||
const unread = useSelector((state) => unreadForChannelSelector(state, `${type}${name}`));
|
||||
const content = (
|
||||
<>
|
||||
{
|
||||
unread
|
||||
? (
|
||||
<span className="unread">
|
||||
{unread}
|
||||
{' '}
|
||||
</span>
|
||||
)
|
||||
: null
|
||||
}
|
||||
<span className="muted">{type}</span>
|
||||
<span className="channel__name">{name}</span>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<li
|
||||
className={classnames('channel__item', {active: location.pathname === href})}
|
||||
>
|
||||
<div className="channel">
|
||||
{React.cloneElement(
|
||||
href ? <Link to={href} /> : <div />,
|
||||
{className: 'channel__link', title: name},
|
||||
[React.cloneElement(content, {key: name})],
|
||||
)}
|
||||
<Actions actions={actions} />
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
Channel.defaultProps = {
|
||||
href: null,
|
||||
};
|
||||
|
||||
Channel.propTypes = {
|
||||
actions: PropTypes.arrayOf(actionsPropValidator).isRequired,
|
||||
href: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
};
|
66
app/src/react/components/channel/index.scss
Normal file
66
app/src/react/components/channel/index.scss
Normal file
|
@ -0,0 +1,66 @@
|
|||
@import 'scss/breakpoints.scss';
|
||||
@import 'scss/colors.scss';
|
||||
|
||||
.channel__item {
|
||||
box-shadow: 0px 0px 2px rgba(0, 0, 0, 0.1) inset;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
&:nth-of-type(even) {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
&:nth-of-type(odd) {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
&.active[class] {
|
||||
background-color: $color-active;
|
||||
&:hover {
|
||||
background-color: lighten($color-active, 5%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.channel {
|
||||
position: relative;
|
||||
user-select: none;
|
||||
&:hover .action {
|
||||
opacity: 0.5;
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.action {
|
||||
padding: 0.5em;
|
||||
@include breakpoint(desktop) {
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
.actions {
|
||||
height: 3em;
|
||||
}
|
||||
}
|
||||
|
||||
.channel__link {
|
||||
color: #ffffff;
|
||||
display: block;
|
||||
font-family: var(--title-font-family);
|
||||
height: 3em;
|
||||
max-width: calc(100% - 6em);
|
||||
overflow: hidden;
|
||||
padding: 1em;
|
||||
padding-right: 0;
|
||||
text-decoration: none;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
.muted {
|
||||
font-size: 0.8em;
|
||||
padding-top: 0.15em;
|
||||
margin-right: 0.125em;
|
||||
}
|
||||
}
|
48
app/src/react/components/channels/index.jsx
Normal file
48
app/src/react/components/channels/index.jsx
Normal file
|
@ -0,0 +1,48 @@
|
|||
import './index.scss';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
import {actionsPropValidator} from 'components/actions';
|
||||
import Channel from 'components/channel';
|
||||
|
||||
export default function Channels(props) {
|
||||
const {channels, type, title} = props;
|
||||
return (
|
||||
<div
|
||||
className="channels"
|
||||
>
|
||||
{channels.length > 0 && (
|
||||
<>
|
||||
<h2 className="channels__title">{title}</h2>
|
||||
<ul className="channels__list">
|
||||
{(
|
||||
channels
|
||||
.map(({actions, name}) => (
|
||||
<Channel
|
||||
key={name}
|
||||
actions={actions}
|
||||
href={`/chat${type}${name}`}
|
||||
name={name}
|
||||
type={type}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Channels.propTypes = {
|
||||
channels: PropTypes.arrayOf(PropTypes.shape({
|
||||
actions: PropTypes.arrayOf(actionsPropValidator),
|
||||
name: PropTypes.string,
|
||||
})).isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
title: PropTypes.oneOfType([
|
||||
PropTypes.node,
|
||||
PropTypes.string,
|
||||
]).isRequired,
|
||||
};
|
12
app/src/react/components/channels/index.scss
Normal file
12
app/src/react/components/channels/index.scss
Normal file
|
@ -0,0 +1,12 @@
|
|||
@import 'scss/colors.scss';
|
||||
|
||||
.channels__title {
|
||||
color: $color-muted;
|
||||
font-family: var(--thick-title-font-family);
|
||||
font-size: 0.7em;
|
||||
font-weight: bold;
|
||||
padding: 2.5em 1em 0.625em 1.375em;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
51
app/src/react/components/friends/active/index.jsx
Normal file
51
app/src/react/components/friends/active/index.jsx
Normal file
|
@ -0,0 +1,51 @@
|
|||
import './index.scss';
|
||||
|
||||
import React from 'react';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
|
||||
import {
|
||||
activeFriendshipSelector,
|
||||
favoriteUsersSelector,
|
||||
idSelector,
|
||||
submitAddFavorite,
|
||||
submitBlock,
|
||||
submitRemoveFriend,
|
||||
usernamesSelector,
|
||||
} from '@reddichat/user/client';
|
||||
|
||||
import Channels from 'components/channels';
|
||||
|
||||
export default function ChatLeftFriendsActive() {
|
||||
const dispatch = useDispatch();
|
||||
const id = useSelector(idSelector);
|
||||
const favorites = useSelector(favoriteUsersSelector);
|
||||
const usernames = useSelector(usernamesSelector);
|
||||
const activeFriendship = useSelector(activeFriendshipSelector)
|
||||
.filter(({adderId, addeeId}) => (
|
||||
[adderId, addeeId].every((id) => -1 === favorites.indexOf(usernames[id]))
|
||||
));
|
||||
const channels = activeFriendship
|
||||
.map(({addeeId, adderId}) => {
|
||||
const otherId = id === adderId ? addeeId : adderId;
|
||||
const name = usernames[otherId] || '?';
|
||||
const actions = [
|
||||
['☢️', 'Block', () => dispatch(submitBlock(otherId))],
|
||||
['🚫', 'Unfriend', () => dispatch(submitRemoveFriend(otherId))],
|
||||
['❤️', 'Favorite', () => dispatch(submitAddFavorite(`/u/${name}`))],
|
||||
];
|
||||
return {actions, name};
|
||||
});
|
||||
return (
|
||||
<div
|
||||
className="chat--leftFriendsActive"
|
||||
>
|
||||
<Channels
|
||||
channels={channels}
|
||||
type="/u/"
|
||||
title="Friends"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ChatLeftFriendsActive.propTypes = {};
|
0
app/src/react/components/friends/active/index.scss
Normal file
0
app/src/react/components/friends/active/index.scss
Normal file
54
app/src/react/components/friends/add/index.jsx
Normal file
54
app/src/react/components/friends/add/index.jsx
Normal file
|
@ -0,0 +1,54 @@
|
|||
import './index.scss';
|
||||
|
||||
import React, {useRef, useState} from 'react';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import {validateUsername} from '@reddichat/core/client';
|
||||
import {submitAddFriend} from '@reddichat/user/client';
|
||||
|
||||
export default function ChatLeftFriendsAdd() {
|
||||
const dispatch = useDispatch();
|
||||
const [text, setText] = useState('');
|
||||
const $form = useRef(null);
|
||||
return (
|
||||
<div
|
||||
className="chat--leftFriendsAdd"
|
||||
>
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
if (validateUsername(text)) {
|
||||
dispatch(submitAddFriend(text));
|
||||
}
|
||||
setText('');
|
||||
}}
|
||||
ref={$form}
|
||||
>
|
||||
<label className="channels__join">
|
||||
Make friends with
|
||||
<span className="friends__addTextWrapper">
|
||||
<input
|
||||
className="friends__addText"
|
||||
onChange={(event) => {
|
||||
setText(event.target.value);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if ('Enter' === event.key && !event.shiftKey) {
|
||||
if ($form.current) {
|
||||
$form.current.dispatchEvent(new Event('submit', {cancelable: true}));
|
||||
}
|
||||
event.preventDefault();
|
||||
}
|
||||
}}
|
||||
placeholder="cha0s"
|
||||
type="text"
|
||||
value={text}
|
||||
/>
|
||||
</span>
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ChatLeftFriendsAdd.propTypes = {};
|
29
app/src/react/components/friends/add/index.scss
Normal file
29
app/src/react/components/friends/add/index.scss
Normal file
|
@ -0,0 +1,29 @@
|
|||
@import 'scss/colors.scss';
|
||||
|
||||
.friends__add[class] {
|
||||
align-items: center;
|
||||
background-color: #272727;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.friends__addTextWrapper {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.friends__addTextWrapper::before {
|
||||
content: '/u/';
|
||||
font-size: 0.8em;
|
||||
padding: 0 0.25em 0 0.5em;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
.friends__addText {
|
||||
flex-grow: 1;
|
||||
padding-left: 0.25em;
|
||||
width: 0%;
|
||||
}
|
36
app/src/react/components/friends/favorites/index.jsx
Normal file
36
app/src/react/components/friends/favorites/index.jsx
Normal file
|
@ -0,0 +1,36 @@
|
|||
import './index.scss';
|
||||
|
||||
import React from 'react';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
|
||||
import {
|
||||
favoriteUsersSelector,
|
||||
submitRemoveFavorite,
|
||||
} from '@reddichat/user/client';
|
||||
|
||||
import Channels from 'components/channels';
|
||||
|
||||
export default function ChatLeftFriendsFavorites() {
|
||||
const dispatch = useDispatch();
|
||||
const favorites = useSelector(favoriteUsersSelector);
|
||||
const channels = favorites
|
||||
.map((channel) => ({
|
||||
actions: [
|
||||
['💔', 'Unfavorite', () => dispatch(submitRemoveFavorite(`/u/${channel}`))],
|
||||
],
|
||||
name: channel,
|
||||
}));
|
||||
return (
|
||||
<div
|
||||
className="chat--leftFriendsFavorites"
|
||||
>
|
||||
<Channels
|
||||
channels={channels}
|
||||
type="/u/"
|
||||
title="Favorites"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ChatLeftFriendsFavorites.propTypes = {};
|
25
app/src/react/components/friends/index.jsx
Normal file
25
app/src/react/components/friends/index.jsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import './index.scss';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import FriendsActive from './active';
|
||||
import FriendsAdd from './add';
|
||||
import FriendsFavorites from './favorites';
|
||||
import FriendsPendingIncoming from './pending/incoming';
|
||||
import FriendsPendingOutgoing from './pending/outgoing';
|
||||
|
||||
export default function ChatLeftFriends() {
|
||||
return (
|
||||
<div
|
||||
className="friends"
|
||||
>
|
||||
<FriendsAdd />
|
||||
<FriendsActive />
|
||||
<FriendsFavorites />
|
||||
<FriendsPendingIncoming />
|
||||
<FriendsPendingOutgoing />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ChatLeftFriends.propTypes = {};
|
0
app/src/react/components/friends/index.scss
Normal file
0
app/src/react/components/friends/index.scss
Normal file
49
app/src/react/components/friends/pending/incoming/index.jsx
Normal file
49
app/src/react/components/friends/pending/incoming/index.jsx
Normal file
|
@ -0,0 +1,49 @@
|
|||
import './index.scss';
|
||||
|
||||
import React from 'react';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
|
||||
import {
|
||||
idSelector,
|
||||
pendingFriendshipSelector,
|
||||
submitConfirmFriend,
|
||||
submitRemoveFriend,
|
||||
usernamesSelector,
|
||||
} from '@reddichat/user/client';
|
||||
|
||||
import Channels from 'components/channels';
|
||||
|
||||
export default function ChatLeftFriendsPendingIncoming() {
|
||||
const dispatch = useDispatch();
|
||||
const id = useSelector(idSelector);
|
||||
const pendingIncomingFriendship = useSelector(pendingFriendshipSelector)
|
||||
.filter(({adderId}) => adderId !== id);
|
||||
const usernames = useSelector(usernamesSelector);
|
||||
const channels = pendingIncomingFriendship
|
||||
.map(({adderId}) => ({
|
||||
name: usernames[adderId] || '?',
|
||||
actions: [
|
||||
['👎', 'Deny friend request', () => dispatch(submitRemoveFriend(adderId))],
|
||||
['👍', 'Confirm friend request', () => dispatch(submitConfirmFriend(adderId))],
|
||||
],
|
||||
}));
|
||||
return (
|
||||
<div
|
||||
className="chat--leftFriendsPendingIncoming"
|
||||
>
|
||||
<Channels
|
||||
channels={channels}
|
||||
type="/u/"
|
||||
title={(
|
||||
<>
|
||||
<span className="unread friends__requests">{pendingIncomingFriendship.length}</span>
|
||||
{' '}
|
||||
Incoming requests
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ChatLeftFriendsPendingIncoming.propTypes = {};
|
|
@ -0,0 +1,5 @@
|
|||
.friends__requests {
|
||||
color: #ffffff;
|
||||
font-size: 0.9em;
|
||||
padding: 0.5em 0.4em;
|
||||
}
|
41
app/src/react/components/friends/pending/outgoing/index.jsx
Normal file
41
app/src/react/components/friends/pending/outgoing/index.jsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
import './index.scss';
|
||||
|
||||
import React from 'react';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
|
||||
import {
|
||||
idSelector,
|
||||
pendingFriendshipSelector,
|
||||
submitRemoveFriend,
|
||||
usernamesSelector,
|
||||
} from '@reddichat/user/client';
|
||||
|
||||
import Channels from 'components/channels';
|
||||
|
||||
export default function ChatLeftFriendsPendingOutgoing() {
|
||||
const dispatch = useDispatch();
|
||||
const id = useSelector(idSelector);
|
||||
const pendingOutgoingFriendship = useSelector(pendingFriendshipSelector)
|
||||
.filter(({adderId}) => adderId === id);
|
||||
const usernames = useSelector(usernamesSelector);
|
||||
const channels = pendingOutgoingFriendship
|
||||
.map(({addeeId}) => ({
|
||||
name: usernames[addeeId] || '?',
|
||||
actions: [
|
||||
['×', 'Cancel friend request', () => dispatch(submitRemoveFriend(addeeId))],
|
||||
],
|
||||
}));
|
||||
return (
|
||||
<div
|
||||
className="chat--leftFriendsPendingOutgoing"
|
||||
>
|
||||
<Channels
|
||||
channels={channels}
|
||||
type="/u/"
|
||||
title="Outgoing requests"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ChatLeftFriendsPendingOutgoing.propTypes = {};
|
|
@ -2,53 +2,54 @@ import './index.scss';
|
|||
|
||||
import classnames from 'classnames';
|
||||
import React from 'react';
|
||||
// import {useDispatch, useSelector} from 'react-redux';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
|
||||
// import {
|
||||
// leftActiveIndexSelector,
|
||||
// leftIsOpenSelector,
|
||||
// setLeftActiveIndex,
|
||||
// } from '~/common/state/app';
|
||||
// import {
|
||||
// idSelector,
|
||||
// pendingFriendshipSelector,
|
||||
// unreadChannelSelector,
|
||||
// unreadUserSelector,
|
||||
// } from '~/common/state/user';
|
||||
import {
|
||||
leftActiveIndexSelector,
|
||||
leftIsOpenSelector,
|
||||
setLeftActiveIndex,
|
||||
} from '@reddichat/app/client';
|
||||
|
||||
// import Bar from './bar';
|
||||
// import Branding from './branding';
|
||||
// import ChatLeftFriends from './chat--leftFriends';
|
||||
import {
|
||||
idSelector,
|
||||
pendingFriendshipSelector,
|
||||
unreadChannelSelector,
|
||||
unreadUserSelector,
|
||||
} from '@reddichat/user/client';
|
||||
|
||||
import Bar from 'components/bar';
|
||||
import Branding from 'components/branding';
|
||||
import Friends from 'components/friends';
|
||||
// import ChatLeftRooms from './chat--leftRooms';
|
||||
|
||||
export default function ChatLeft() {
|
||||
// const dispatch = useDispatch();
|
||||
// const leftActiveIndex = useSelector(leftActiveIndexSelector);
|
||||
// const active = useSelector(leftActiveIndexSelector);
|
||||
// const isOpen = useSelector(leftIsOpenSelector);
|
||||
// const id = useSelector(idSelector);
|
||||
// const pendingFriendship = useSelector(pendingFriendshipSelector)
|
||||
// .filter(({addeeId}) => addeeId === id).length;
|
||||
// const unreadChannel = useSelector(unreadChannelSelector);
|
||||
// const unreadUser = useSelector(unreadUserSelector);
|
||||
const showsAsOpen = true;
|
||||
// const leftButtons = [
|
||||
// {
|
||||
// count: unreadChannel,
|
||||
// icon: '💬',
|
||||
// label: 'Chat',
|
||||
// },
|
||||
// {
|
||||
// count: unreadUser + pendingFriendship,
|
||||
// icon: '😁',
|
||||
// label: 'Friends',
|
||||
// },
|
||||
// ];
|
||||
const dispatch = useDispatch();
|
||||
const leftActiveIndex = useSelector(leftActiveIndexSelector);
|
||||
const active = useSelector(leftActiveIndexSelector);
|
||||
const isOpen = useSelector(leftIsOpenSelector);
|
||||
const id = useSelector(idSelector);
|
||||
const pendingFriendship = useSelector(pendingFriendshipSelector)
|
||||
.filter(({addeeId}) => addeeId === id).length;
|
||||
const unreadChannel = useSelector(unreadChannelSelector);
|
||||
const unreadUser = useSelector(unreadUserSelector);
|
||||
const showsAsOpen = isOpen;
|
||||
const leftButtons = [
|
||||
{
|
||||
count: unreadChannel,
|
||||
icon: '💬',
|
||||
label: 'Chat',
|
||||
},
|
||||
{
|
||||
count: unreadUser + pendingFriendship,
|
||||
icon: '😁',
|
||||
label: 'Friends',
|
||||
},
|
||||
];
|
||||
return (
|
||||
<div
|
||||
className={classnames('left', 'flexed', showsAsOpen ? 'open' : 'closed')}
|
||||
>
|
||||
{/* <Branding />
|
||||
<Branding />
|
||||
<Bar
|
||||
active={leftActiveIndex}
|
||||
buttons={leftButtons}
|
||||
|
@ -56,8 +57,8 @@ export default function ChatLeft() {
|
|||
isHorizontal
|
||||
onActive={(active) => dispatch(setLeftActiveIndex(active))}
|
||||
/>
|
||||
{0 === active && <ChatLeftRooms />}
|
||||
{1 === active && <ChatLeftFriends />} */}
|
||||
{/* {0 === active && <ChatLeftRooms />} */}
|
||||
{1 === active && <Friends />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user