what is life
This commit is contained in:
parent
dc08d02dc8
commit
e3bb4ce125
20 changed files with 23719 additions and 3211 deletions
26430
bfrontend/package-lock.json
generated
26430
bfrontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -5,6 +5,9 @@ import Logger from '../Util/Logger';
|
||||||
const { log: authLog, error: authError } = Logger([ 'Authenticator' ]);
|
const { log: authLog, error: authError } = Logger([ 'Authenticator' ]);
|
||||||
|
|
||||||
const Authenticator = {
|
const Authenticator = {
|
||||||
|
getToken: function() {
|
||||||
|
return localStorage.getItem("token");
|
||||||
|
},
|
||||||
getLoggedInUserFromCookie: async function() {
|
getLoggedInUserFromCookie: async function() {
|
||||||
authLog('Fetching current logged in user status...');
|
authLog('Fetching current logged in user status...');
|
||||||
const { json, isOK, err } = await APIRequest.authenticated('/api/v1/users/current/info');
|
const { json, isOK, err } = await APIRequest.authenticated('/api/v1/users/current/info');
|
||||||
|
|
|
@ -1,85 +1,121 @@
|
||||||
import io from 'socket.io-client';
|
import logger from "../../Util/Logger";
|
||||||
|
|
||||||
import Logger from '../../Util/Logger';
|
const logGateway = logger([ "Gateway" ]);
|
||||||
|
|
||||||
|
const opcodes = {
|
||||||
|
0: { name: "HELLO", data: "JSON" },
|
||||||
|
1: { name: "YOO", data: "JSON" },
|
||||||
|
2: { name: "YOO_ACK", data: "JSON" },
|
||||||
|
3: { name: "ACTION_CREATE_MESSAGE", data: "JSON" },
|
||||||
|
4: { name: "EVENT_CREATE_MESSAGE", data: "JSON" }
|
||||||
|
};
|
||||||
|
|
||||||
|
const opcodeSeparator = "@";
|
||||||
|
|
||||||
|
const parseMessage = (message) => {
|
||||||
|
if (typeof message !== "string") throw new Error("msg: message not a string");
|
||||||
|
const stringParts = message.split(opcodeSeparator);
|
||||||
|
if (stringParts < 2) throw new Error("msg: message does not split into more than 2 parts");
|
||||||
|
const components = [ stringParts.shift(), stringParts.join(opcodeSeparator) ];
|
||||||
|
const op = parseInt(components[0]);
|
||||||
|
if (isNaN(op)) throw new Error(`msg: message does not contain valid opcode: ${op}`);
|
||||||
|
|
||||||
|
const opcodeData = opcodes[op];
|
||||||
|
let data = components[1];
|
||||||
|
if (!opcodeData) throw new Error(`msg: message contains unknown opcode ${op}`);
|
||||||
|
if (opcodeData.data === "JSON") {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
} else if (opcodeData.data === "string") {
|
||||||
|
data = data.toString(); // NOTE: This isnt needed lol
|
||||||
|
} else {
|
||||||
|
throw new Error(`msg: invalid data type on opcode ${op}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
opcode: op,
|
||||||
|
data: data,
|
||||||
|
dataType: opcodeData.data,
|
||||||
|
opcodeType: opcodeData.name || null
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOpcodeByName = (name) => {
|
||||||
|
for (const [key, value] of Object.entries(opcodes)) if (value.name === name) return key;
|
||||||
|
};
|
||||||
|
|
||||||
const { log: gatewayLog } = Logger([ 'Gateway' ]);
|
|
||||||
const { log: gatewayHandshakeLog } = Logger([ 'Gateway', 'Handshake' ]);
|
|
||||||
|
|
||||||
class GatewayConnection {
|
class GatewayConnection {
|
||||||
constructor(url="") {
|
constructor(token, gatewayUrl) {
|
||||||
this.isConnected = false;
|
this.ws = new WebSocket(gatewayUrl);
|
||||||
this.socket = null;
|
|
||||||
this.url = url;
|
this.handshakeCompleted = false;
|
||||||
|
this.sessionInformation = null;
|
||||||
|
|
||||||
// TODO: set up proper event listening and such, not this dumb crap
|
this.ws.onopen = () => logGateway("Open");
|
||||||
this.onDisconnect = () => {}
|
this.ws.onclose = (e) => {
|
||||||
this.onConnect = () => {}
|
this.handshakeCompleted = false;
|
||||||
|
logGateway(`Close: ${e.code}:${e.reason}`);
|
||||||
|
this.fire("onclose", e);
|
||||||
|
};
|
||||||
|
this.ws.onmessage = (message) => {
|
||||||
|
try {
|
||||||
|
const packet = parseMessage(message.data);
|
||||||
|
if (!packet) return console.error("gateway: invalid packet from server");
|
||||||
|
|
||||||
|
switch (packet.opcodeType) {
|
||||||
|
case "HELLO": {
|
||||||
|
// Got HELLO from server, send YOO as soon as possible
|
||||||
|
logGateway("Got HELLO", packet.data);
|
||||||
|
logGateway("Sending YOO");
|
||||||
|
this.ws.send(this.packet("YOO", { token }));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "YOO_ACK": {
|
||||||
|
// Server accepted connection
|
||||||
|
logGateway("Got YOO_ACK", packet.data);
|
||||||
|
this.handshakeCompleted = true;
|
||||||
|
this.sessionInformation = packet.data;
|
||||||
|
logGateway("Handshake complete");
|
||||||
|
this.fire("onopen", packet.data);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "EVENT_CREATE_MESSAGE": {
|
||||||
|
// New message
|
||||||
|
// console.log("gateway: got new message", packet.data);
|
||||||
|
this.fire("onmessage", packet.data);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
logGateway("Got unknown packet", message.data);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
return console.error("err: gateway:", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
GatewayConnection.prototype.disconnect = function() {
|
GatewayConnection.prototype.sendMessage = function(content, channelId) {
|
||||||
this.socket?.disconnect();
|
if (!this.sessionInformation) throw new Error("gateway: tried to send message before handshake completion");
|
||||||
this.socket = null;
|
|
||||||
this.isConnected = false;
|
this.ws.send(this.packet("ACTION_CREATE_MESSAGE", {
|
||||||
|
content,
|
||||||
|
channel: {
|
||||||
|
_id: channelId
|
||||||
|
}
|
||||||
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
GatewayConnection.prototype.connect = function(token) {
|
|
||||||
gatewayHandshakeLog('Trying to connect to gateway');
|
|
||||||
this.socket = io(`${this.url}/gateway`, {
|
|
||||||
query: {
|
|
||||||
token
|
|
||||||
},
|
|
||||||
transports: ['websocket']
|
|
||||||
});
|
|
||||||
|
|
||||||
this.socket.on('connect', () => {
|
GatewayConnection.prototype.packet = function(op, data) {
|
||||||
this.socket.once('hello', (debugInfo) => {
|
if (typeof op === "string") op = getOpcodeByName(op);
|
||||||
gatewayHandshakeLog('Got hello from server, sending yoo...', debugInfo);
|
return `${op}${opcodeSeparator}${JSON.stringify(data)}`;
|
||||||
this.socket.emit('yoo');
|
|
||||||
this.isConnected = true;
|
|
||||||
this.debugInfo = debugInfo;
|
|
||||||
this.onConnect('CONNECT_RECEIVED_HELLO');
|
|
||||||
gatewayHandshakeLog('Assuming that server received yoo and that connection is completed.');
|
|
||||||
});
|
|
||||||
})
|
|
||||||
|
|
||||||
this.socket.on('error', (e) => {
|
|
||||||
gatewayLog('Gateway error', e);
|
|
||||||
this.isConnected = false;
|
|
||||||
this.socket = null;
|
|
||||||
this.onDisconnect('DISCONNECT_ERR', e);
|
|
||||||
});
|
|
||||||
this.socket.on('disconnectNotification', (e) => {
|
|
||||||
gatewayLog('Received disconnect notfication', e);
|
|
||||||
this.isConnected = false;
|
|
||||||
this.socket = null;
|
|
||||||
this.onDisconnect('DISCONNECT_NOTIF', e);
|
|
||||||
});
|
|
||||||
this.socket.on('disconnect', (e) => {
|
|
||||||
gatewayLog('Disconnected from gateway: ', e);
|
|
||||||
this.isConnected = false;
|
|
||||||
this.onDisconnect('DISCONNECT', e);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
GatewayConnection.prototype.sendMessage = function(categoryId, content) {
|
GatewayConnection.prototype.fire = function(eventName, ...args) {
|
||||||
if (!this.isConnected) return 1;
|
if (this[eventName]) return this[eventName](...args);
|
||||||
if (content.length >= 2000) return 1;
|
|
||||||
|
|
||||||
this.socket.emit('message', {
|
|
||||||
category: {
|
|
||||||
_id: categoryId
|
|
||||||
},
|
|
||||||
content
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
GatewayConnection.prototype.subscribeToCategoryChats = function(categoryIds) {
|
|
||||||
if (!this.isConnected) return;
|
|
||||||
|
|
||||||
gatewayLog('Subscribing to channel(s)', categoryIds);
|
|
||||||
|
|
||||||
this.socket.emit('subscribe', categoryIds);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default GatewayConnection;
|
export default GatewayConnection;
|
|
@ -1,19 +1,22 @@
|
||||||
import GatewayConnection from './GatewayConnection';
|
import GatewayConnection from './GatewayConnection';
|
||||||
import config from '../../Config';
|
import config from '../../Config';
|
||||||
|
import Authenticator from '../Authenticator';
|
||||||
import store from '../../Global/store';
|
import store from '../../Global/store';
|
||||||
|
console.log(Authenticator);
|
||||||
|
|
||||||
const globalGatewayConnection = new GatewayConnection(config.gatewayUrl);
|
const globalGatewayConnection = new GatewayConnection(Authenticator.getToken(), config.gatewayUrl);
|
||||||
|
|
||||||
globalGatewayConnection.onConnect = function() {
|
globalGatewayConnection.onopen = (sessionData) => {
|
||||||
store.dispatch({ type: 'gateway/connectionstatus', gateway: { isConnected: true } });
|
store.dispatch({ type: 'gateway/connectionstatus', gateway: { isConnected: true } });
|
||||||
globalGatewayConnection.socket.on('message', (message) => {
|
store.dispatch({ type: 'authenticator/updatelocaluserobject', user: sessionData.user });
|
||||||
store.dispatch({ type: 'messagestore/addmessage', message });
|
store.dispatch({ type: 'channels/updatechannellist', user: sessionData.channels })
|
||||||
});
|
|
||||||
globalGatewayConnection.socket.on('clientListUpdate', (clientListEvent) => {
|
|
||||||
store.dispatch({ type: 'presence/category/clientlistupdate', clientListEvent });
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
globalGatewayConnection.onDisconnect = function() {
|
|
||||||
|
globalGatewayConnection.onmessage = (message) => {
|
||||||
|
store.dispatch({ type: 'messagestore/addmessage', message });
|
||||||
|
};
|
||||||
|
|
||||||
|
globalGatewayConnection.onclose = function() {
|
||||||
store.dispatch({ type: 'gateway/connectionstatus', gateway: { isConnected: false } });
|
store.dispatch({ type: 'gateway/connectionstatus', gateway: { isConnected: false } });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,75 +0,0 @@
|
||||||
import CategoryListLoader from './CategoryListLoader';
|
|
||||||
import CategoryButton from './CategoryButton';
|
|
||||||
import APIRequest from '../../API/APIRequest';
|
|
||||||
import { couldNotReach } from '../../Util/Errors';
|
|
||||||
|
|
||||||
import { connect, useDispatch } from 'react-redux'
|
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
import Logger from '../../Util/Logger';
|
|
||||||
import gatewayConnection from '../../API/Gateway/globalGatewayConnection';
|
|
||||||
|
|
||||||
const { log: loaderLog } = Logger([ 'CategoryList', 'Loader' ]);
|
|
||||||
|
|
||||||
function CategoryList({ selectedCategoryId, gatewayStatus }) {
|
|
||||||
const [ categoryList, setCategoryList ] = useState();
|
|
||||||
const [ error, setError ] = useState();
|
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loaderLog('Loading CategoryList...');
|
|
||||||
APIRequest.authenticated('/api/v1/content/category/list?count=50')
|
|
||||||
.then(({ isOK, json }) => {
|
|
||||||
if (!isOK) return setError(true);
|
|
||||||
|
|
||||||
loaderLog('Got category list from server, dispatching...');
|
|
||||||
setCategoryList(json.categories || []);
|
|
||||||
dispatch({ type: 'categories/updatecategorylist', categories: json.categories });
|
|
||||||
loaderLog('Subscribing to all categories...');
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setError(true);
|
|
||||||
});
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!categoryList) return;
|
|
||||||
if (!gatewayStatus.isConnected) return;
|
|
||||||
|
|
||||||
// TODO: IMPORTANT: Subscribing to a lot of channels puts strain on the server
|
|
||||||
gatewayConnection.subscribeToCategoryChats(categoryList.map(category => category._id));
|
|
||||||
}, [gatewayStatus, categoryList]);
|
|
||||||
|
|
||||||
if (!categoryList) {
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<p>
|
|
||||||
{ couldNotReach }
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<CategoryListLoader />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<div className="category-list">
|
|
||||||
{ categoryList.map((category) => ( <CategoryButton key={ category._id } category={ category } selected={ (category._id === selectedCategoryId) } /> )) }
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const stateToProps = (state) => {
|
|
||||||
return {
|
|
||||||
selectedCategoryId: state?.selectedCategoryId,
|
|
||||||
gatewayStatus: state?.gateway,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(stateToProps)(CategoryList);
|
|
|
@ -1,8 +0,0 @@
|
||||||
import ProfileLink from '../UI/ProfileLink'
|
|
||||||
|
|
||||||
// TODO: Stop using this component and just use the ProfileLink component
|
|
||||||
export default function CategoryProfile({ category, size, offset=false }) {
|
|
||||||
return (
|
|
||||||
<ProfileLink object={ category } size={ size } type="category" offset={ offset } />
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,23 +1,23 @@
|
||||||
import CategoryProfile from './CategoryProfileLink';
|
import ChannelProfile from './ChannelProfileLink';
|
||||||
import gatewayConnection from '../../API/Gateway/globalGatewayConnection';
|
import gatewayConnection from '../../API/Gateway/globalGatewayConnection';
|
||||||
|
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
export default function CategoryButton({ category, selected }) {
|
export default function ChannelButton({ channel, selected }) {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
let buttonClasses = 'button category-button';
|
let buttonClasses = 'button channel-button';
|
||||||
if (selected) buttonClasses += ' pressed';
|
if (selected) buttonClasses += ' pressed';
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
if (gatewayConnection.isConnected) {
|
if (gatewayConnection.isConnected) {
|
||||||
history.push(`/categories/${category._id}`);
|
history.push(`/channels/${channel._id}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button className={ buttonClasses } onClick={ handleClick }>
|
<button className={ buttonClasses } onClick={ handleClick }>
|
||||||
<CategoryProfile category={ category } size="32" />
|
<ChannelProfile channel={ channel } size="32" />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
75
bfrontend/src/Components/Channels/ChannelList.js
Normal file
75
bfrontend/src/Components/Channels/ChannelList.js
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
import ChannelListLoader from './ChannelListLoader';
|
||||||
|
import ChannelButton from './ChannelButton';
|
||||||
|
import APIRequest from '../../API/APIRequest';
|
||||||
|
import { couldNotReach } from '../../Util/Errors';
|
||||||
|
|
||||||
|
import { connect, useDispatch } from 'react-redux'
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import Logger from '../../Util/Logger';
|
||||||
|
import gatewayConnection from '../../API/Gateway/globalGatewayConnection';
|
||||||
|
|
||||||
|
const { log: loaderLog } = Logger([ 'ChannelList', 'Loader' ]);
|
||||||
|
|
||||||
|
function ChannelList({ selectedChannelId, gatewayStatus }) {
|
||||||
|
const [ channelList, setChannelList ] = useState();
|
||||||
|
const [ error, setError ] = useState();
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loaderLog('Loading ChannelList...');
|
||||||
|
APIRequest.authenticated('/api/v1/content/channel/list?count=50')
|
||||||
|
.then(({ isOK, json }) => {
|
||||||
|
if (!isOK) return setError(true);
|
||||||
|
|
||||||
|
loaderLog('Got channel list from server, dispatching...');
|
||||||
|
setChannelList(json.channels || []);
|
||||||
|
dispatch({ type: 'channels/updatechannellist', channels: json.channels });
|
||||||
|
loaderLog('Subscribing to all channels...');
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setError(true);
|
||||||
|
});
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!channelList) return;
|
||||||
|
if (!gatewayStatus.isConnected) return;
|
||||||
|
|
||||||
|
// TODO: IMPORTANT: Subscribing to a lot of channels puts strain on the server
|
||||||
|
gatewayConnection.subscribeToChannelChats(channelList.map(channel => channel._id));
|
||||||
|
}, [gatewayStatus, channelList]);
|
||||||
|
|
||||||
|
if (!channelList) {
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
{ couldNotReach }
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ChannelListLoader />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div className="channel-list">
|
||||||
|
{ channelList.map((channel) => ( <ChannelButton key={ channel._id } channel={ channel } selected={ (channel._id === selectedChannelId) } /> )) }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stateToProps = (state) => {
|
||||||
|
return {
|
||||||
|
selectedChannelId: state?.selectedChannelId,
|
||||||
|
gatewayStatus: state?.gateway,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(stateToProps)(ChannelList);
|
|
@ -1,7 +1,7 @@
|
||||||
import ContentLoader from "react-content-loader"
|
import ContentLoader from "react-content-loader"
|
||||||
import { useMediaPredicate } from "react-media-hook";
|
import { useMediaPredicate } from "react-media-hook";
|
||||||
|
|
||||||
export default function CategoryListLoader(props) {
|
export default function ChannelListLoader(props) {
|
||||||
const lessThan600 = useMediaPredicate("(max-width: 600px)");
|
const lessThan600 = useMediaPredicate("(max-width: 600px)");
|
||||||
|
|
||||||
if (lessThan600) {
|
if (lessThan600) {
|
8
bfrontend/src/Components/Channels/ChannelProfileLink.js
Normal file
8
bfrontend/src/Components/Channels/ChannelProfileLink.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import ProfileLink from '../UI/ProfileLink'
|
||||||
|
|
||||||
|
// TODO: Stop using this component and just use the ProfileLink component
|
||||||
|
export default function ChannelProfile({ channel, size, offset=false }) {
|
||||||
|
return (
|
||||||
|
<ProfileLink object={ channel } size={ size } type="channel" offset={ offset } />
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import CategoryViewLoader from './CategoryViewLoader';
|
import ChannelViewLoader from './ChannelViewLoader';
|
||||||
import CategoryProfile from './CategoryProfileLink';
|
import ChannelProfile from './ChannelProfileLink';
|
||||||
import Message from '../Messages/Message';
|
import Message from '../Messages/Message';
|
||||||
import gatewayConnection from '../../API/Gateway/globalGatewayConnection';
|
import gatewayConnection from '../../API/Gateway/globalGatewayConnection';
|
||||||
|
|
||||||
|
@ -7,10 +7,10 @@ import { useParams } from 'react-router-dom';
|
||||||
import { connect, useDispatch } from 'react-redux';
|
import { connect, useDispatch } from 'react-redux';
|
||||||
import { useState, useRef, useEffect } from 'react';
|
import { useState, useRef, useEffect } from 'react';
|
||||||
|
|
||||||
function CategoryView({ categories, messages }) {
|
function ChannelView({ channels, messages }) {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const [ textInput, setTextInput ] = useState('');
|
const [ textInput, setTextInput ] = useState('');
|
||||||
const [ category, setCategory ] = useState();
|
const [ channel, setChannel ] = useState();
|
||||||
|
|
||||||
const textInputRef = useRef(null);
|
const textInputRef = useRef(null);
|
||||||
const invisibleBottomMessageRef = useRef(null);
|
const invisibleBottomMessageRef = useRef(null);
|
||||||
|
@ -33,18 +33,18 @@ function CategoryView({ categories, messages }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!categories) return;
|
if (!channels) return;
|
||||||
|
|
||||||
setCategory(categories.find(x => x._id === id));
|
setChannel(channels.find(x => x._id === id));
|
||||||
}, [ categories, id ]);
|
}, [ channels, id ]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!category) return;
|
if (!channel) return;
|
||||||
|
|
||||||
dispatch({ type: 'categories/selectcategory', categoryId: category._id });
|
dispatch({ type: 'channels/selectchannel', channelId: channel._id });
|
||||||
}, [ category, dispatch ]);
|
}, [ channel, dispatch ]);
|
||||||
|
|
||||||
if (category) {
|
if (channel) {
|
||||||
let messagesView = messages.map(m => <Message key={ m._id } message={ m } />);
|
let messagesView = messages.map(m => <Message key={ m._id } message={ m } />);
|
||||||
|
|
||||||
if (messagesView === undefined || messagesView.length <= 0) {
|
if (messagesView === undefined || messagesView.length <= 0) {
|
||||||
|
@ -58,7 +58,7 @@ function CategoryView({ categories, messages }) {
|
||||||
return (
|
return (
|
||||||
<div className="card main-card main-content-card">
|
<div className="card main-card main-content-card">
|
||||||
<div className="card bar-card">
|
<div className="card bar-card">
|
||||||
<CategoryProfile category={ category } />
|
<ChannelProfile channel={ channel } />
|
||||||
</div>
|
</div>
|
||||||
<div className="card message-list-card">
|
<div className="card message-list-card">
|
||||||
{ messagesView }
|
{ messagesView }
|
||||||
|
@ -73,7 +73,7 @@ function CategoryView({ categories, messages }) {
|
||||||
return (
|
return (
|
||||||
<div className="card main-card main-content-card">
|
<div className="card main-card main-content-card">
|
||||||
<div className="card bar-card">
|
<div className="card bar-card">
|
||||||
<CategoryViewLoader />
|
<ChannelViewLoader />
|
||||||
</div>
|
</div>
|
||||||
<div className="card message-list-card">
|
<div className="card message-list-card">
|
||||||
|
|
||||||
|
@ -88,12 +88,12 @@ function CategoryView({ categories, messages }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const stateToProps = (state, ownProps) => {
|
const stateToProps = (state, ownProps) => {
|
||||||
const categoryId = ownProps.match.params.id; // NOTE(hippoz): kind of a hack, but it works and idk if theres any other solution
|
const channelId = ownProps.match.params.id; // NOTE(hippoz): kind of a hack, but it works and idk if theres any other solution
|
||||||
|
|
||||||
return {
|
return {
|
||||||
categories: state?.categories,
|
channels: state?.channels,
|
||||||
messages: state?.messages[categoryId] || [],
|
messages: state?.messages[channelId] || [],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(stateToProps)(CategoryView);
|
export default connect(stateToProps)(ChannelView);
|
|
@ -1,6 +1,6 @@
|
||||||
import ContentLoader from "react-content-loader"
|
import ContentLoader from "react-content-loader"
|
||||||
|
|
||||||
export default function CategoryViewLoader(props) {
|
export default function ChannelViewLoader(props) {
|
||||||
return (
|
return (
|
||||||
<ContentLoader
|
<ContentLoader
|
||||||
speed={1.5}
|
speed={1.5}
|
|
@ -4,7 +4,7 @@ import Authenticator from '../../API/Authenticator';
|
||||||
import Notification from '../UI/Notification';
|
import Notification from '../UI/Notification';
|
||||||
import './../../Styles/App.scss';
|
import './../../Styles/App.scss';
|
||||||
import { couldNotReach } from '../../Util/Errors';
|
import { couldNotReach } from '../../Util/Errors';
|
||||||
import CategoryView from '../Categories/CategoryView';
|
import ChannelView from '../Channels/ChannelView';
|
||||||
import Sidebar from '../UI/Sidebar';
|
import Sidebar from '../UI/Sidebar';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
@ -44,7 +44,7 @@ function App({ user }) {
|
||||||
{ user && <Sidebar /> }
|
{ user && <Sidebar /> }
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/login" component={ Login } />
|
<Route path="/login" component={ Login } />
|
||||||
<Route path="/categories/:id" component={ CategoryView } />
|
<Route path="/channels/:id" component={ ChannelView } />
|
||||||
|
|
||||||
<Route path="/">
|
<Route path="/">
|
||||||
<Root user={user} />
|
<Root user={user} />
|
||||||
|
|
|
@ -18,7 +18,7 @@ export default function ProfileLink({ object, size=32, type, offset=true, childr
|
||||||
}
|
}
|
||||||
|
|
||||||
const classes = offset ? 'profile-link profile-link-offset-text' : 'profile-link';
|
const classes = offset ? 'profile-link profile-link-offset-text' : 'profile-link';
|
||||||
const title = type === 'category' ? object.title : object.username;
|
const title = type === 'channel' ? object.title : object.username;
|
||||||
const bottomInfo = offset ? children : null;
|
const bottomInfo = offset ? children : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import CategoryList from '../Categories/CategoryList';
|
import ChannelList from '../Channels/ChannelList';
|
||||||
import UserProfileLink from '../Users/UserProfileLink';
|
import UserProfileLink from '../Users/UserProfileLink';
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
function Sidebar({ user }) {
|
function Sidebar({ user }) {
|
||||||
return (
|
return (
|
||||||
<div className="card main-card category-list-container">
|
<div className="card main-card channel-list-container">
|
||||||
<div className="card bar-card">
|
<div className="card bar-card">
|
||||||
<UserProfileLink user={ user } />
|
<UserProfileLink user={ user } />
|
||||||
</div>
|
</div>
|
||||||
<CategoryList />
|
<ChannelList />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import ProfileLink from '../UI/ProfileLink'
|
import ProfileLink from '../UI/ProfileLink'
|
||||||
|
|
||||||
// TODO: Stop using this component and just use the ProfileLink component
|
// TODO: Stop using this component and just use the ProfileLink component
|
||||||
export default function CategoryProfile({ user, size, offset=false }) {
|
export default function ChannelProfile({ user, size, offset=false }) {
|
||||||
return (
|
return (
|
||||||
<ProfileLink object={ user } size={ size } type="user" offset={ offset } />
|
<ProfileLink object={ user } size={ size } type="user" offset={ offset } />
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
const config = {
|
const config = {
|
||||||
apiUrl: 'http://localhost:3000',
|
apiUrl: 'http://localhost:3000',
|
||||||
gatewayUrl: '' // Leave blank for it to look for the gateway on the current page, if that makes sense
|
gatewayUrl: '', // Leave blank for it to look for the gateway on the current page, if that makes sense
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
|
@ -3,13 +3,13 @@ import { createStore } from 'redux';
|
||||||
const intitialState = {
|
const intitialState = {
|
||||||
user: null,
|
user: null,
|
||||||
|
|
||||||
categories: null,
|
channels: null,
|
||||||
|
|
||||||
gateway: { isConnected: false },
|
gateway: { isConnected: false },
|
||||||
messages: {},
|
messages: {},
|
||||||
categoryPresenceClientList: {},
|
channelPresenceClientList: {},
|
||||||
|
|
||||||
selectedCategoryId: undefined
|
selectedChannelId: undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
const reducer = (state = intitialState, payload) => {
|
const reducer = (state = intitialState, payload) => {
|
||||||
|
@ -21,10 +21,10 @@ const reducer = (state = intitialState, payload) => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'categories/updatecategorylist': {
|
case 'channels/updatechannellist': {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
categories: payload.categories
|
channels: payload.channels
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,27 +42,27 @@ const reducer = (state = intitialState, payload) => {
|
||||||
...state,
|
...state,
|
||||||
messages: {
|
messages: {
|
||||||
...state.messages,
|
...state.messages,
|
||||||
[payload.message.category._id]: [
|
[payload.message.channel._id]: [
|
||||||
...state.messages[payload.message.category._id] || [],
|
...state.messages[payload.message.channel._id] || [],
|
||||||
payload.message
|
payload.message
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'categories/selectcategory': {
|
case 'channels/selectchannel': {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
selectedCategoryId: payload.categoryId
|
selectedChannelId: payload.channelId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'presence/category/clientlistupdate': {
|
case 'presence/channel/clientlistupdate': {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
categoryPresenceClientList: {
|
channelPresenceClientList: {
|
||||||
...state.categoryPresenceClientList || [],
|
...state.channelPresenceClientList || [],
|
||||||
[payload.clientListEvent.category._id]: payload.clientListEvent.clientList
|
[payload.clientListEvent.channel._id]: payload.clientListEvent.clientList
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,12 +34,12 @@ $nord15: #B48EAD;
|
||||||
--accent-color-light: #{$nord2darker};
|
--accent-color-light: #{$nord2darker};
|
||||||
--accent-color-very-light: #{$nord3darker};
|
--accent-color-very-light: #{$nord3darker};
|
||||||
|
|
||||||
--category-top-bar-color: var(--accent-color-light);
|
--channel-top-bar-color: var(--accent-color-light);
|
||||||
--category-bottom-text-bar-color: var(--accent-color-light);
|
--channel-bottom-text-bar-color: var(--accent-color-light);
|
||||||
|
|
||||||
--category-list-background-color: var(--background-color);
|
--channel-list-background-color: var(--background-color);
|
||||||
--category-message-list-background-color: var(--background-color);
|
--channel-message-list-background-color: var(--background-color);
|
||||||
--category-message-color: var(--accent-color-dark);
|
--channel-message-color: var(--accent-color-dark);
|
||||||
|
|
||||||
--button-color: var(--accent-color-dark);
|
--button-color: var(--accent-color-dark);
|
||||||
--button-hover-color: var(--accent-color-light);
|
--button-hover-color: var(--accent-color-light);
|
||||||
|
@ -48,9 +48,9 @@ $nord15: #B48EAD;
|
||||||
|
|
||||||
--default-border-radius: 0px;
|
--default-border-radius: 0px;
|
||||||
--default-button-border-radius: 0px;
|
--default-button-border-radius: 0px;
|
||||||
--category-message-border-radius: 0px;
|
--channel-message-border-radius: 0px;
|
||||||
--bar-cards-border-radius: 0px;
|
--bar-cards-border-radius: 0px;
|
||||||
--category-button-border-radius: 4px;
|
--channel-button-border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
|
@ -100,7 +100,7 @@ body {
|
||||||
@include card;
|
@include card;
|
||||||
|
|
||||||
&.bar-card {
|
&.bar-card {
|
||||||
background-color: var(--category-top-bar-color);
|
background-color: var(--channel-top-bar-color);
|
||||||
min-height: 50px;
|
min-height: 50px;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
|
|
||||||
|
@ -115,7 +115,7 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
&.bar-card-bottom {
|
&.bar-card-bottom {
|
||||||
background-color: var(--category-bottom-text-bar-color);
|
background-color: var(--channel-bottom-text-bar-color);
|
||||||
box-shadow: 0px -3px 5px var(--card-box-shadow-color);
|
box-shadow: 0px -3px 5px var(--card-box-shadow-color);
|
||||||
|
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
|
@ -132,7 +132,7 @@ body {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
background-color: var(--category-message-list-background-color);
|
background-color: var(--channel-message-list-background-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,7 +161,7 @@ body {
|
||||||
border-radius: var(--default-button-border-radius);
|
border-radius: var(--default-button-border-radius);
|
||||||
min-height: 25px;
|
min-height: 25px;
|
||||||
|
|
||||||
&.category-button {
|
&.channel-button {
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
max-width: 200px;
|
max-width: 200px;
|
||||||
|
|
||||||
|
@ -169,7 +169,7 @@ body {
|
||||||
max-height: 100px;
|
max-height: 100px;
|
||||||
|
|
||||||
margin: 4px;
|
margin: 4px;
|
||||||
border-radius: var(--category-button-border-radius);
|
border-radius: var(--channel-button-border-radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.pressed {
|
&.pressed {
|
||||||
|
@ -181,19 +181,19 @@ body {
|
||||||
background-color: var(--accent-color-light);
|
background-color: var(--accent-color-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-list {
|
.channel-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-list-container {
|
.channel-list-container {
|
||||||
@include fancy-scrollbar-firefox;
|
@include fancy-scrollbar-firefox;
|
||||||
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
|
||||||
background-color: var(--category-list-background-color);
|
background-color: var(--channel-list-background-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-picture {
|
.profile-picture {
|
||||||
|
@ -256,7 +256,7 @@ body {
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
border-radius: var(--category-message-border-radius);
|
border-radius: var(--channel-message-border-radius);
|
||||||
background-color: var(--background-color);
|
background-color: var(--background-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -281,7 +281,7 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 600px) {
|
@media only screen and (max-width: 600px) {
|
||||||
.button.category-button {
|
.button.channel-button {
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
max-width: 100px;
|
max-width: 100px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,8 +63,8 @@ const getCreatePostError = (json) => {
|
||||||
case 'body': {
|
case 'body': {
|
||||||
return 'Invalid content. Must be between 3 and 1000 characters';
|
return 'Invalid content. Must be between 3 and 1000 characters';
|
||||||
}
|
}
|
||||||
case 'category': {
|
case 'channel': {
|
||||||
return 'Invalid category. Something went wrong.';
|
return 'Invalid channel. Something went wrong.';
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
return 'Invalid value sent to server. Something went wrong.';
|
return 'Invalid value sent to server. Something went wrong.';
|
||||||
|
@ -73,7 +73,7 @@ const getCreatePostError = (json) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'ERROR_CATEGORY_NOT_FOUND': {
|
case 'ERROR_CATEGORY_NOT_FOUND': {
|
||||||
return 'The category you tried to post to no longer exists.';
|
return 'The channel you tried to post to no longer exists.';
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'ERROR_ACCESS_DENIED': {
|
case 'ERROR_ACCESS_DENIED': {
|
||||||
|
@ -86,7 +86,7 @@ const getCreatePostError = (json) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCreateCategoryError = (json) => {
|
const getCreateChannelError = (json) => {
|
||||||
switch (json.message) {
|
switch (json.message) {
|
||||||
case 'ERROR_REQUEST_INVALID_DATA': {
|
case 'ERROR_REQUEST_INVALID_DATA': {
|
||||||
switch (json.errors[0].param) {
|
switch (json.errors[0].param) {
|
||||||
|
@ -110,6 +110,6 @@ const getCreateCategoryError = (json) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const couldNotReach = "Whoops! We couldn't reach Brainlet."
|
const couldNotReach = "Whoops! We couldn't reach Brainlet.";
|
||||||
|
|
||||||
module.exports = { couldNotReach, getLoginMessageFromError, getSignupMessageFromError, getCreatePostError, getCreateCategoryError }
|
module.exports = { couldNotReach, getLoginMessageFromError, getSignupMessageFromError, getCreatePostError, getCreateChannelError }
|
Loading…
Reference in a new issue