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(() => {