send channel type and lastMessage to client

use name of other user on DM channels
add startdm api to start direct messages
This commit is contained in:
HF 2020-11-15 01:12:28 +01:00
parent fc09ffcb45
commit cce2ad1f80
15 changed files with 349 additions and 52 deletions

View File

@ -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',

View File

@ -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 }

View File

@ -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 (
<div
style={{position: 'relative'}}
style={{ position: 'relative' }}
>
<div
ref={buttonRef}
style={{
width: 50,
}}
ref={wrapperRef}
onClick={() => setShow(true)}
onClick={() => setShow(!show)}
className="channelbtn"
>
{chatChannel}
{chatChannelName}
</div>
<div
ref={wrapperRef}
style={{
position: 'absolute',
bottom: 10,
right: 10,
bottom: offset + 5,
right: 9,
display: (show) ? 'initial' : 'none',
}}
className="channeldd"
>
{
channels.map((ch) => (
<div>
{ch[1]}
</div>
))
}
<div>
<span
onClick={() => setType(0)}
>
<MdChat />
</span>
|
<span
onClick={() => setType(1)}
>
<FaUserFriends />
</span>
</div>
<div
className="channeldds"
>
{
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) => (
<div
onClick={() => setChannel(ch[0])}
style={(ch[0] === chatChannel) ? {
fontWeight: 'bold',
fontSize: 17,
} : null}
>
{ch[1]}
</div>
))
}
</div>
</div>
</div>
);

View File

@ -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());

View File

@ -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);
}

View File

@ -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,

View File

@ -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;
}

View File

@ -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,

View File

@ -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',

View File

@ -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,
]);
}
}
}

View File

@ -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;

View File

@ -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 {

View File

@ -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;

131
src/routes/api/startdm.js Normal file
View File

@ -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;

View File

@ -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 {