diff --git a/src/actions/fetch.js b/src/actions/fetch.js
index 7afdcc7..b12274e 100644
--- a/src/actions/fetch.js
+++ b/src/actions/fetch.js
@@ -118,3 +118,32 @@ export async function requestBlockDm(block: boolean) {
return 'Connection Error';
}
}
+
+/*
+ * leaving Chat Channel (i.e. DM channel)
+ * channelId 8nteger id of channel
+ * return error string or null if successful
+ */
+export async function requestLeaveChan(channelId: boolean) {
+ const response = await fetchWithTimeout('api/leavechan', {
+ method: 'POST',
+ credentials: 'include',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ channelId }),
+ });
+
+ try {
+ const res = await response.json();
+ if (res.errors) {
+ return res.errors[0];
+ }
+ if (response.ok && res.status === 'ok') {
+ return null;
+ }
+ return 'Unknown Error';
+ } catch {
+ return 'Connection Error';
+ }
+}
diff --git a/src/actions/index.js b/src/actions/index.js
index 6022355..4b1c727 100644
--- a/src/actions/index.js
+++ b/src/actions/index.js
@@ -11,6 +11,7 @@ import {
requestStartDm,
requestBlock,
requestBlockDm,
+ requestLeaveChan,
} from './fetch';
export function sweetAlert(
@@ -760,10 +761,10 @@ export function showChatModal(forceModal: boolean = false): Action {
return showModal('CHAT');
}
-export function setChatChannel(channelId: number): Action {
+export function setChatChannel(cid: number): Action {
return {
type: 'SET_CHAT_CHANNEL',
- channelId,
+ cid,
};
}
@@ -797,6 +798,13 @@ export function blockingDm(blockDm: boolean): Action {
};
}
+export function removeChatChannel(cid: number): Action {
+ return {
+ type: 'REMOVE_CHAT_CHANNEL',
+ cid,
+ };
+}
+
/*
* query: Object with either userId: number or userName: string
*/
@@ -812,10 +820,10 @@ export function startDm(query): PromiseAction {
'OK',
));
} else {
- const channelId = res[0];
- if (channelId) {
+ const cid = res[0];
+ if (cid) {
dispatch(addChatChannel(res));
- dispatch(setChatChannel(channelId));
+ dispatch(setChatChannel(cid));
}
}
dispatch(setApiFetching(false));
@@ -866,6 +874,26 @@ export function setBlockingDm(
};
}
+export function setLeaveChannel(
+ cid: number,
+) {
+ return async (dispatch) => {
+ dispatch(setApiFetching(true));
+ const res = await requestLeaveChan(cid);
+ if (res) {
+ dispatch(sweetAlert(
+ 'Leaving Channel Error',
+ res,
+ 'error',
+ 'OK',
+ ));
+ } else {
+ dispatch(removeChatChannel(cid));
+ }
+ dispatch(setApiFetching(false));
+ };
+}
+
export function hideModal(): Action {
return {
type: 'HIDE_MODAL',
diff --git a/src/actions/types.js b/src/actions/types.js
index 982f819..1c4bf4d 100644
--- a/src/actions/types.js
+++ b/src/actions/types.js
@@ -69,8 +69,9 @@ export type Action =
isPing: boolean,
}
| { type: 'RECEIVE_CHAT_HISTORY', cid: number, history: Array }
- | { type: 'SET_CHAT_CHANNEL', channelId: number }
+ | { type: 'SET_CHAT_CHANNEL', cid: number }
| { type: 'ADD_CHAT_CHANNEL', channel: Array }
+ | { type: 'REMOVE_CHAT_CHANNEL', cid: number }
| { 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/ChannelContextMenu.jsx b/src/components/ChannelContextMenu.jsx
index 30a9c66..d2e1b81 100644
--- a/src/components/ChannelContextMenu.jsx
+++ b/src/components/ChannelContextMenu.jsx
@@ -10,6 +10,7 @@ import { connect } from 'react-redux';
import {
hideContextMenu,
+ setLeaveChannel,
} from '../actions';
import type { State } from '../reducers';
@@ -18,6 +19,7 @@ const UserContextMenu = ({
yPos,
cid,
channels,
+ leave,
close,
}) => {
const wrapperRef = useRef(null);
@@ -29,13 +31,14 @@ const UserContextMenu = ({
close();
}
};
+ const handleWindowResize = () => close();
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('touchstart', handleClickOutside);
- window.addEventListener('resize', handleClickOutside);
+ window.addEventListener('resize', handleWindowResize);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('touchstart', handleClickOutside);
- window.removeEventListener('resize', handleClickOutside);
+ window.removeEventListener('resize', handleWindowResize);
};
}, [wrapperRef]);
@@ -62,16 +65,19 @@ const UserContextMenu = ({
top: yPos,
}}
>
-
+
✔✘ Mute
{(channelArray[2] !== 0)
&& (
{
+ leave(cid);
+ close();
+ }}
tabIndex={0}
+ style={{ borderTop: 'thin solid' }}
>
Close
@@ -105,6 +111,9 @@ function mapDispatchToProps(dispatch) {
close() {
dispatch(hideContextMenu());
},
+ leave(cid) {
+ dispatch(setLeaveChannel(cid));
+ },
};
}
diff --git a/src/components/ChannelDropDown.jsx b/src/components/ChannelDropDown.jsx
index 7ffe68c..a53a746 100644
--- a/src/components/ChannelDropDown.jsx
+++ b/src/components/ChannelDropDown.jsx
@@ -19,6 +19,7 @@ import {
const ChannelDropDown = ({
channels,
chatChannel,
+ chatRead,
setChannel,
}) => {
const [show, setShow] = useState(false);
@@ -75,66 +76,79 @@ const ChannelDropDown = ({
>
{chatChannelName}
-
-
- setType(0)}
- >
-
-
- |
- setType(1)}
- >
-
-
-
+ {(show)
+ && (
- {
- 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]}
-
- ))
- }
+
+ 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}
+ className={
+ `chn${
+ (ch[0] === chatChannel) ? ' selected' : ''
+ }${
+ (chatRead[ch[0]] < ch[3]) ? ' unread' : ''
+ }`
+ }
+ >
+ {ch[1]}
+
+ ))
+ }
+
-
+ )}
);
};
function mapStateToProps(state: State) {
- const { chatChannel } = state.gui;
+ const {
+ chatChannel,
+ chatRead,
+ } = state.gui;
const { channels } = state.chat;
return {
channels,
chatChannel,
+ chatRead,
};
}
diff --git a/src/components/UserContextMenu.jsx b/src/components/UserContextMenu.jsx
index f89fc7d..76a8af1 100644
--- a/src/components/UserContextMenu.jsx
+++ b/src/components/UserContextMenu.jsx
@@ -35,13 +35,14 @@ const UserContextMenu = ({
close();
}
};
+ const handleWindowResize = () => close();
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('touchstart', handleClickOutside);
- window.addEventListener('resize', handleClickOutside);
+ window.addEventListener('resize', handleWindowResize);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('touchstart', handleClickOutside);
- window.removeEventListener('resize', handleClickOutside);
+ window.removeEventListener('resize', handleWindowResize);
};
}, [wrapperRef]);
diff --git a/src/core/ChatProvider.js b/src/core/ChatProvider.js
index b3d4337..52a303d 100644
--- a/src/core/ChatProvider.js
+++ b/src/core/ChatProvider.js
@@ -54,18 +54,12 @@ export class ChatProvider {
const { name } = CHAT_CHANNELS[i];
// eslint-disable-next-line no-await-in-loop
const channel = await Channel.findOrCreate({
- attributes: [
- 'id',
- 'type',
- 'lastMessage',
- ],
where: { name },
defaults: {
name,
},
- raw: true,
});
- const { id, type, lastMessage } = channel[0];
+ const { id, type, lastTs } = channel[0];
if (name === 'int') {
this.intChannelId = id;
}
@@ -76,7 +70,7 @@ export class ChatProvider {
id,
name,
type,
- lastMessage,
+ lastTs,
]);
this.defaultChannelIds.push(id);
}
diff --git a/src/data/models/Channel.js b/src/data/models/Channel.js
index e1104a0..39803b5 100644
--- a/src/data/models/Channel.js
+++ b/src/data/models/Channel.js
@@ -43,6 +43,17 @@ const Channel = Model.define('Channel', {
},
}, {
updatedAt: false,
+
+ getterMethods: {
+ lastTs(): number {
+ return new Date(this.lastMessage).valueOf();
+ },
+ },
+ setterMethods: {
+ lastTs(ts: number) {
+ this.setDataValue('lastMessage', new Date(ts).toISOString());
+ },
+ },
});
/*
diff --git a/src/data/models/User.js b/src/data/models/User.js
index 8531f8d..d482be1 100644
--- a/src/data/models/User.js
+++ b/src/data/models/User.js
@@ -60,7 +60,7 @@ class User {
const {
id,
type,
- lastMessage,
+ lastTs,
dmu1,
dmu2,
} = reguser.channel[i];
@@ -74,7 +74,7 @@ class User {
id,
name,
type,
- lastMessage,
+ lastTs,
]);
}
}
diff --git a/src/reducers/chat.js b/src/reducers/chat.js
index 8fe3230..19262db 100644
--- a/src/reducers/chat.js
+++ b/src/reducers/chat.js
@@ -8,7 +8,7 @@ export type ChatState = {
inputMessage: string,
// [[cid, name, type, lastMessage], [cid2, name2, type2, lastMessage2],...]
channels: Array,
- // [[userId, userName], [userId2, userName2],...]
+ // [[uId, userName], [userId2, userName2],...]
blocked: Array,
// { cid: [message1,message2,message3,...]}
messages: Object,
@@ -56,9 +56,9 @@ export default function chat(
case 'ADD_CHAT_CHANNEL': {
const { channel } = action;
- const channelId = channel[0];
+ const cid = channel[0];
const channels = state.channels
- .filter((ch) => (ch[0] !== channelId));
+ .filter((ch) => (ch[0] !== cid));
channels.push(channel);
return {
...state,
@@ -66,6 +66,18 @@ export default function chat(
};
}
+ case 'REMOVE_CHAT_CHANNEL': {
+ const { cid } = action;
+ const channels = state.channels.filter(
+ // eslint-disable-next-line eqeqeq
+ (chan) => (chan[0] != cid),
+ );
+ return {
+ ...state,
+ channels,
+ };
+ }
+
case 'SET_CHAT_INPUT_MSG': {
const { message } = action;
return {
diff --git a/src/reducers/gui.js b/src/reducers/gui.js
index 579d700..cf80355 100644
--- a/src/reducers/gui.js
+++ b/src/reducers/gui.js
@@ -16,6 +16,9 @@ export type GUIState = {
paletteOpen: boolean,
menuOpen: boolean,
chatChannel: number,
+ // timestamps of last read post per channel
+ // { 1: Date.now() }
+ chatRead: {},
style: string,
};
@@ -31,6 +34,7 @@ const initialState: GUIState = {
paletteOpen: true,
menuOpen: false,
chatChannel: 1,
+ chatRead: {},
style: 'default',
};
@@ -107,7 +111,7 @@ export default function gui(
case 'SET_CHAT_CHANNEL': {
return {
...state,
- chatChannel: action.channelId,
+ chatChannel: action.cid,
};
}
diff --git a/src/routes/api/index.js b/src/routes/api/index.js
index 42fa401..91ab4d6 100644
--- a/src/routes/api/index.js
+++ b/src/routes/api/index.js
@@ -19,6 +19,7 @@ import ranking from './ranking';
import history from './history';
import chatHistory from './chathistory';
import startDm from './startdm';
+import leaveChan from './leavechan';
import block from './block';
import blockdm from './blockdm';
@@ -85,6 +86,8 @@ router.get('/chathistory', chatHistory);
router.post('/startdm', startDm);
+router.post('/leavechan', leaveChan);
+
router.post('/block', block);
router.post('/blockdm', blockdm);
diff --git a/src/routes/api/leavechan.js b/src/routes/api/leavechan.js
new file mode 100644
index 0000000..686933e
--- /dev/null
+++ b/src/routes/api/leavechan.js
@@ -0,0 +1,72 @@
+/*
+ *
+ * starts a DM session
+ *
+ * @flow
+ */
+
+import type { Request, Response } from 'express';
+
+import logger from '../../core/logger';
+
+async function leaveChan(req: Request, res: Response) {
+ const channelId = parseInt(req.body.channelId, 10);
+ const { user } = req;
+
+ const errors = [];
+ if (channelId && Number.isNaN(channelId)) {
+ errors.push('Invalid channelId');
+ }
+ if (!user || !user.regUser) {
+ errors.push('You are not logged in');
+ }
+ if (errors.length) {
+ res.status(400);
+ res.json({
+ errors,
+ });
+ return;
+ }
+
+ const userChannels = user.regUser.channel;
+ let channel = null;
+ for (let i = 0; i < userChannels.length; i += 1) {
+ if (userChannels[i].id === channelId) {
+ channel = userChannels[i];
+ break;
+ }
+ }
+ if (!channel) {
+ res.status(401);
+ res.json({
+ errors: ['You are not in this channel'],
+ });
+ return;
+ }
+
+ /*
+ * Just supporting DMs by now, because
+ * Channels do not get deleted when all Users left.
+ * Group Channels need this.
+ * Faction and Default channels should be impossible to leave
+ */
+ if (channel.type !== 1) {
+ res.status(401);
+ res.json({
+ errors: ['Can not leave this channel'],
+ });
+ return;
+ }
+
+ logger.info(
+ `Removing user ${user.getName()} from channel ${channel.name || channelId}`,
+ );
+ user.regUser.removeChannel(channel);
+
+ // TODO: inform websocket to remove channelId from user
+ res.json({
+ status: 'ok',
+ });
+}
+
+export default leaveChan;
diff --git a/src/web.js b/src/web.js
index 5b18aad..9baaec0 100644
--- a/src/web.js
+++ b/src/web.js
@@ -194,7 +194,7 @@ app.get('/', async (req, res) => {
// ip config
// -----------------------------------------------------------------------------
// use this if models changed:
-const promise = models.sync({ alter: { drop: false } })
+const promise = models.sync({ alter: { drop: true } })
// const promise = models.sync()
.catch((err) => logger.error(err.stack));
promise.then(() => {