diff --git a/src/actions/index.js b/src/actions/index.js
index 7ddd0a3..2202844 100644
--- a/src/actions/index.js
+++ b/src/actions/index.js
@@ -584,7 +584,7 @@ export function fetchMe(): PromiseAction {
function receiveChatHistory(
cid: number,
history: Array,
-) {
+): Action {
return {
type: 'RECEIVE_CHAT_HISTORY',
cid,
@@ -748,6 +748,48 @@ export function setChatChannel(channelId: number): Action {
};
}
+export function addChatChannel(channel: Array): Action {
+ return {
+ type: 'ADD_CHAT_CHANNEL',
+ channel,
+ };
+}
+
+export function startDm(query): PromiseAction {
+ return async (dispatch) => {
+ const response = await fetch('api/startdm', {
+ method: 'POST',
+ credentials: 'include',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(query),
+ });
+
+ try {
+ const res = await response.json();
+ if (res.errors) {
+ dispatch(sweetAlert(
+ 'Direct Message Error',
+ res.errors[0],
+ 'error',
+ 'OK',
+ ));
+ }
+ if (response.ok) {
+ const { channel } = res;
+ const channelId = channel[0];
+ if (channelId) {
+ await dispatch(addChatChannel(channel));
+ dispatch(setChatChannel(channelId));
+ }
+ }
+ } catch {
+
+ }
+ };
+}
+
export function hideModal(): Action {
return {
type: 'HIDE_MODAL',
diff --git a/src/actions/types.js b/src/actions/types.js
index 5b0f6c4..5a086d7 100644
--- a/src/actions/types.js
+++ b/src/actions/types.js
@@ -70,6 +70,7 @@ export type Action =
}
| { type: 'RECEIVE_CHAT_HISTORY', cid: number, history: Array }
| { type: 'SET_CHAT_CHANNEL', channelId: number }
+ | { type: 'ADD_CHAT_CHANNEL', channel: Array }
| { type: 'SET_CHAT_FETCHING', fetching: boolean }
| { type: 'SET_CHAT_INPUT_MSG', message: string }
| { type: 'ADD_CHAT_INPUT_MSG', message: string }
diff --git a/src/components/ChannelDropDown.jsx b/src/components/ChannelDropDown.jsx
index b92ba09..7ffe68c 100644
--- a/src/components/ChannelDropDown.jsx
+++ b/src/components/ChannelDropDown.jsx
@@ -5,9 +5,11 @@
*/
import React, {
- useRef, useLayoutEffect, useState, useEffect,
+ useRef, useState, useEffect, useCallback,
} from 'react';
import { connect } from 'react-redux';
+import { MdChat } from 'react-icons/md';
+import { FaUserFriends } from 'react-icons/fa';
import type { State } from '../reducers';
import {
@@ -20,55 +22,108 @@ const ChannelDropDown = ({
setChannel,
}) => {
const [show, setShow] = useState(false);
+ // 0: global and faction channels
+ // 1: DMs
+ const [type, setType] = useState(0);
+ const [offset, setOffset] = useState(0);
+ const [chatChannelName, setChatChannelName] = useState('...');
const wrapperRef = useRef(null);
+ const buttonRef = useRef(null);
useEffect(() => {
- function handleClickOutside(event) {
- if (wrapperRef.current
- && !wrapperRef.current.contains(event.target)
- ) {
- setShow(false);
+ setOffset(buttonRef.current.clientHeight);
+ }, [buttonRef]);
+
+ const handleClickOutside = useCallback((event) => {
+ if (wrapperRef.current
+ && !wrapperRef.current.contains(event.target)
+ && !buttonRef.current.contains(event.target)
+ ) {
+ setShow(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ if (show) {
+ document.addEventListener('mousedown', handleClickOutside);
+ document.addEventListener('touchstart', handleClickOutside);
+ } else {
+ document.removeEventListener('mousedown', handleClickOutside);
+ document.removeEventListener('touchstart', handleClickOutside);
+ }
+ }, [show]);
+
+ useEffect(() => {
+ for (let i = 0; i < channels.length; i += 1) {
+ if (channels[i][0] === chatChannel) {
+ setChatChannelName(channels[i][1]);
}
}
-
- document.addEventListener('mousedown', handleClickOutside);
- document.addEventListener('touchstart', handleClickOutside);
- return () => {
- document.removeEventListener('mousedown', handleClickOutside);
- document.addEventListener('touchstart', handleClickOutside);
- };
- }, [wrapperRef]);
+ }, [chatChannel, channels]);
return (
setShow(true)}
+ onClick={() => setShow(!show)}
className="channelbtn"
>
- {chatChannel}
+ {chatChannelName}
- {
- channels.map((ch) => (
-
- {ch[1]}
-
- ))
- }
+
+ setType(0)}
+ >
+
+
+ |
+ setType(1)}
+ >
+
+
+
+
+ {
+ channels.filter((ch) => {
+ const chType = ch[2];
+ if (type === 1 && chType === 1) {
+ return true;
+ }
+ if (type === 0 && chType !== 1) {
+ return true;
+ }
+ return false;
+ }).map((ch) => (
+
setChannel(ch[0])}
+ style={(ch[0] === chatChannel) ? {
+ fontWeight: 'bold',
+ fontSize: 17,
+ } : null}
+ >
+ {ch[1]}
+
+ ))
+ }
+
);
diff --git a/src/components/UserContextMenu.jsx b/src/components/UserContextMenu.jsx
index 367b314..4e581a6 100644
--- a/src/components/UserContextMenu.jsx
+++ b/src/components/UserContextMenu.jsx
@@ -11,7 +11,7 @@ import { connect } from 'react-redux';
import {
hideContextMenu,
addToChatInputMessage,
- setChatInputMessage,
+ startDm,
} from '../actions';
import type { State } from '../reducers';
@@ -21,24 +21,24 @@ const UserContextMenu = ({
yPos,
uid,
name,
- setInput,
addToInput,
+ dm,
close,
}) => {
const wrapperRef = useRef(null);
+
useEffect(() => {
- function handleClickOutside(event) {
+ const handleClickOutside = (event) => {
if (wrapperRef.current && !wrapperRef.current.contains(event.target)) {
close();
}
- }
-
+ };
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('touchstart', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
- document.addEventListener('touchstart', handleClickOutside);
+ document.removeEventListener('touchstart', handleClickOutside);
};
}, [wrapperRef]);
@@ -60,7 +60,8 @@ const UserContextMenu = ({
role="button"
tabIndex={0}
onClick={() => {
- setInput('loool');
+ dm(uid);
+ close();
}}
style={{ borderBottom: 'thin solid' }}
>
@@ -108,8 +109,8 @@ function mapDispatchToProps(dispatch) {
input.select();
}
},
- setInput(text) {
- dispatch(setChatInputMessage(text));
+ dm(userId) {
+ dispatch(startDm({ userId }));
},
close() {
dispatch(hideContextMenu());
diff --git a/src/core/ChatProvider.js b/src/core/ChatProvider.js
index c0eb7aa..b3d4337 100644
--- a/src/core/ChatProvider.js
+++ b/src/core/ChatProvider.js
@@ -1,6 +1,4 @@
/* @flow */
-import Sequelize from 'sequelize';
-
import logger from './logger';
import redis from '../data/redis';
import User from '../data/models/User';
@@ -58,16 +56,16 @@ export class ChatProvider {
const channel = await Channel.findOrCreate({
attributes: [
'id',
+ 'type',
'lastMessage',
],
where: { name },
defaults: {
name,
- lastMessage: Sequelize.literal('CURRENT_TIMESTAMP'),
},
raw: true,
});
- const { id } = channel[0];
+ const { id, type, lastMessage } = channel[0];
if (name === 'int') {
this.intChannelId = id;
}
@@ -77,6 +75,8 @@ export class ChatProvider {
this.defaultChannels.push([
id,
name,
+ type,
+ lastMessage,
]);
this.defaultChannelIds.push(id);
}
diff --git a/src/core/event.js b/src/core/event.js
index 36271df..0c2951b 100644
--- a/src/core/event.js
+++ b/src/core/event.js
@@ -156,7 +156,7 @@ class Event {
}
static broadcastChatMessage(message) {
- if (chatProvier.enChannelId && chatProvider.eventUserId) {
+ if (chatProvider.enChannelId && chatProvider.eventUserId) {
chatProvider.broadcastChatMessage(
EVENT_USER_NAME,
message,
diff --git a/src/core/me.js b/src/core/me.js
index fd86422..4f1dbf7 100644
--- a/src/core/me.js
+++ b/src/core/me.js
@@ -30,12 +30,11 @@ export default async function getMe(user) {
delete userdata.mailVerified;
delete userdata.mcVerified;
+ userdata.canvases = canvases;
userdata.channels = [
...chatProvider.defaultChannels,
...userdata.channels,
];
- userdata.canvases = canvases;
-
return userdata;
}
diff --git a/src/core/passport.js b/src/core/passport.js
index 7466812..55444f7 100644
--- a/src/core/passport.js
+++ b/src/core/passport.js
@@ -25,6 +25,23 @@ import { getIPFromRequest } from '../utils/ip';
const include = [{
model: Channel,
as: 'channel',
+ include: [{
+ model: RegUser,
+ as: 'dmu1',
+ foreignKey: 'dmu1id',
+ attributes: [
+ 'id',
+ 'name',
+ ],
+ }, {
+ model: RegUser,
+ as: 'dmu2',
+ foreignKey: 'dmu2id',
+ attributes: [
+ 'id',
+ 'name',
+ ],
+ }],
}, {
model: RegUser,
through: UserBlock,
diff --git a/src/data/models/Channel.js b/src/data/models/Channel.js
index 60cc610..e1104a0 100644
--- a/src/data/models/Channel.js
+++ b/src/data/models/Channel.js
@@ -7,6 +7,7 @@
*/
import DataType from 'sequelize';
+
import Model from '../sequelize';
import RegUser from './RegUser';
@@ -37,6 +38,7 @@ const Channel = Model.define('Channel', {
lastMessage: {
type: DataType.DATE,
+ defaultValue: DataType.literal('CURRENT_TIMESTAMP'),
allowNull: false,
},
}, {
@@ -49,6 +51,7 @@ const Channel = Model.define('Channel', {
* (associating it here allows us too
* keep track of users leaving and joining DMs and ending up
* in the same conversation)
+ * dmu1id < dmu2id
*/
Channel.belongsTo(RegUser, {
as: 'dmu1',
diff --git a/src/data/models/User.js b/src/data/models/User.js
index bc25a45..6120295 100644
--- a/src/data/models/User.js
+++ b/src/data/models/User.js
@@ -14,7 +14,6 @@ import logger from '../../core/logger';
import Model from '../sequelize';
import RegUser from './RegUser';
import { getIPv6Subnet } from '../../utils/ip';
-
import { ADMIN_IDS } from '../../core/config';
@@ -35,7 +34,6 @@ class User {
this.wait = null;
// following gets populated by passport
this.regUser = null;
- this.channelIds = [];
}
static async name2Id(name: string) {
@@ -56,12 +54,28 @@ class User {
setRegUser(reguser) {
this.regUser = reguser;
this.id = reguser.id;
- for (let i = 0; i < reguser.channel.length; i += 1) {
- this.channelIds.push(reguser.channel[i].id);
- this.channels.push([
- reguser.channel[i].id,
- reguser.channel[i].name,
- ]);
+ if (reguser.channel) {
+ for (let i = 0; i < reguser.channel.length; i += 1) {
+ const {
+ id,
+ type,
+ lastMessage,
+ dmu1,
+ dmu2,
+ } = reguser.channel[i];
+ // in DMs the name is the name of the other user
+ let { name } = reguser.channel[i];
+ if (type === 1) {
+ name = (dmu1.id === this.id) ? dmu2.name : dmu1.name;
+ }
+ this.channelIds.push(id);
+ this.channels.push([
+ id,
+ name,
+ type,
+ lastMessage,
+ ]);
+ }
}
}
diff --git a/src/data/models/UserBlock.js b/src/data/models/UserBlock.js
index 4783ccb..14c9571 100644
--- a/src/data/models/UserBlock.js
+++ b/src/data/models/UserBlock.js
@@ -14,4 +14,16 @@ const UserBlock = Model.define('UserBlock', {
timestamps: false,
});
+export async function isUserBlockedBy(userId, blockedById) {
+ const exists = await UserBlock.findOne({
+ where: {
+ uid: userId,
+ buid: blockedById,
+ },
+ raw: true,
+ attributes: ['uid'],
+ });
+ return !!exists;
+}
+
export default UserBlock;
diff --git a/src/reducers/chat.js b/src/reducers/chat.js
index a89864f..707eb57 100644
--- a/src/reducers/chat.js
+++ b/src/reducers/chat.js
@@ -33,6 +33,18 @@ export default function chat(
};
}
+ case 'ADD_CHAT_CHANNEL': {
+ const { channel } = action;
+ const channelId = channel[0];
+ const channels = state.channels
+ .filter((ch) => (ch[0] !== channelId));
+ channels.push(channel);
+ return {
+ ...state,
+ channels,
+ };
+ }
+
case 'SET_CHAT_FETCHING': {
const { fetching } = action;
return {
diff --git a/src/routes/api/index.js b/src/routes/api/index.js
index 08458f1..6b674ce 100644
--- a/src/routes/api/index.js
+++ b/src/routes/api/index.js
@@ -18,6 +18,7 @@ import auth from './auth';
import ranking from './ranking';
import history from './history';
import chatHistory from './chathistory';
+import startDm from './startdm';
const router = express.Router();
@@ -80,6 +81,8 @@ router.post('/mctp', mctp);
router.get('/chathistory', chatHistory);
+router.post('/startdm', startDm);
+
router.use('/auth', auth(passport));
export default router;
diff --git a/src/routes/api/startdm.js b/src/routes/api/startdm.js
new file mode 100644
index 0000000..d46f80c
--- /dev/null
+++ b/src/routes/api/startdm.js
@@ -0,0 +1,131 @@
+/*
+ *
+ * starts a DM session
+ *
+ * @flow
+ */
+
+import type { Request, Response } from 'express';
+
+import logger from '../../core/logger';
+import { Channel, UserChannel, RegUser } from '../../data/models';
+import { isUserBlockedBy } from '../../data/models/UserBlock';
+
+async function startDm(req: Request, res: Response) {
+ let userId = parseInt(req.body.userId, 10);
+ let { userName } = req.body;
+ const { user } = req;
+
+ const errors = [];
+ const query = {};
+ if (userId) {
+ if (userId && Number.isNaN(userId)) {
+ errors.push('Invalid userId');
+ }
+ query.id = userId;
+ }
+ if (userName) {
+ query.name = userName;
+ }
+ if (!userName && !userId) {
+ errors.push('No userId or userName defined');
+ }
+ if (!user || !user.regUser) {
+ errors.push('You are not logged in');
+ }
+ if (user && userId && user.id === userId) {
+ errors.push('You can not start DM to yourself.');
+ }
+ if (errors.length) {
+ res.status(400);
+ res.json({
+ errors,
+ });
+ return;
+ }
+
+ const targetUser = await RegUser.findOne({
+ where: query,
+ attributes: [
+ 'id',
+ 'name',
+ ],
+ raw: true,
+ });
+ if (!targetUser) {
+ res.status(401);
+ res.json({
+ errors: ['Target user does not exist'],
+ });
+ return;
+ }
+ userId = targetUser.id;
+ userName = targetUser.name;
+
+ /*
+ * check if blocked
+ */
+ if (await isUserBlockedBy(user.id, userId)) {
+ res.status(401);
+ res.json({
+ errors: ['You are blocked by this user'],
+ });
+ return;
+ }
+
+ logger.info(
+ `Creating DM Channel between ${user.regUser.name} and ${userName}`,
+ );
+ /*
+ * start DM session
+ */
+ let dmu1id = null;
+ let dmu2id = null;
+ if (user.id > userId) {
+ dmu1id = userId;
+ dmu2id = user.id;
+ } else {
+ dmu1id = user.id;
+ dmu2id = userId;
+ }
+
+ const channel = await Channel.findOrCreate({
+ where: {
+ type: 1,
+ dmu1id,
+ dmu2id,
+ },
+ raw: true,
+ });
+ const ChannelId = channel[0].id;
+ const { lastMessage } = channel[0];
+
+ const promises = [
+ UserChannel.findOrCreate({
+ where: {
+ UserId: dmu1id,
+ ChannelId,
+ },
+ raw: true,
+ }),
+ UserChannel.findOrCreate({
+ where: {
+ UserId: dmu2id,
+ ChannelId,
+ },
+ raw: true,
+ }),
+ ];
+ await Promise.all(promises);
+
+ res.json({
+ channel: [
+ ChannelId,
+ userName,
+ 1,
+ lastMessage,
+ ],
+ });
+}
+
+export default startDm;
diff --git a/src/styles/default.css b/src/styles/default.css
index 6f603c8..95967c8 100644
--- a/src/styles/default.css
+++ b/src/styles/default.css
@@ -167,6 +167,13 @@ tr:nth-child(even) {
.channeldd {
background-color: rgba(226, 226, 226);
+ width: 90px;
+}
+
+.channeldds {
+ height: 120px;
+ overflow-y: scroll;
+ margin: 2px;
}
.actionbuttons, .coorbox, .onlinebox, .cooldownbox, #palettebox {