From b79a12f9316b510fb1577cf9b4dfa05441032c50 Mon Sep 17 00:00:00 2001 From: HF Date: Sat, 28 Nov 2020 02:04:40 +0100 Subject: [PATCH] fixing bugs that got introduced in the past two commits --- README.md | 2 +- src/actions/index.js | 15 ++- src/actions/types.js | 2 + src/components/App.jsx | 2 - src/components/ChannelContextMenu.jsx | 31 +++++- src/components/ChannelDropDown.jsx | 108 ++++++++++++++---- src/components/Chat.jsx | 19 +--- src/components/ChatButton.jsx | 104 ++++++++++++++--- src/components/Menu.jsx | 9 +- src/components/ModalRoot.jsx | 2 - src/components/SettingsModal.jsx | 2 +- src/components/SocialSettings.jsx | 2 +- src/components/UserContextMenu.jsx | 36 +++++- src/controls/PixelPainterControls.js | 5 +- src/core/ChatProvider.js | 34 +++--- src/core/constants.js | 4 + src/data/models/Message.js | 2 + src/data/models/RegUser.js | 16 ++- src/data/models/User.js | 18 ++- src/reducers/chat.js | 15 ++- src/reducers/chatRead.js | 153 ++++++++++++++++++++++++++ src/reducers/gui.js | 56 ---------- src/reducers/index.js | 3 + src/routes/api/block.js | 4 +- src/routes/api/blockdm.js | 6 +- src/routes/api/leavechan.js | 2 +- src/routes/api/startdm.js | 28 +++-- src/socket/SocketServer.js | 54 ++++----- src/socket/WebSocketEvents.js | 2 - src/socket/websockets.js | 8 +- src/store/audio.js | 26 +++-- src/store/protocolClientHook.js | 2 +- src/styles/arkeros.css | 6 +- src/styles/dark-round.css | 19 ++++ src/styles/dark.css | 14 +++ src/styles/default.css | 74 ++++++++++--- src/styles/light-round.css | 6 +- 37 files changed, 651 insertions(+), 240 deletions(-) create mode 100644 src/reducers/chatRead.js diff --git a/README.md b/README.md index f292d5d8..68033efe 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ Configuration takes place in the environment variables that are defined in ecosy Notes: - to be able to use USE_PROXYCHECK, you have to have an account on proxycheck.io or getipintel or another checker setup and you might set some proxies in`proxies.json that get used for making proxycheck requests. Look into `src/isProxy.js` to see how things work, but keep in mind that this isn't neccessarily how pixelplanet.fun uses it. -- Admins are users with 0cd and access to `./admintools` for image-upload and whatever +- Admins are users with 0cd and access to `Admintools`in their User Menu for image-upload and whatever - You can find out the id of a user by looking into the logs (i.e. `info: {ip} / {id} wants to place 2 in (1701, -8315)`) when he places a pixel or by checking the MySql Users database - If you use gmail as mail transport, make sure that less-secure apps are allowed to access it in your settings [here](https://myaccount.google.com/lesssecureapps) diff --git a/src/actions/index.js b/src/actions/index.js index 62222bd7..9605af2e 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -486,7 +486,6 @@ export function receivePixelUpdate( export function loginUser( me: Object, ): Action { - console.log('login', me); return { type: 'LOGIN', ...me, @@ -794,6 +793,20 @@ export function removeChatChannel(cid: number): Action { }; } +export function muteChatChannel(cid: number): Action { + return { + type: 'MUTE_CHAT_CHANNEL', + cid, + }; +} + +export function unmuteChatChannel(cid: number): Action { + return { + type: 'UNMUTE_CHAT_CHANNEL', + cid, + }; +} + /* * query: Object with either userId: number or userName: string */ diff --git a/src/actions/types.js b/src/actions/types.js index 7c90f72e..88c37f84 100644 --- a/src/actions/types.js +++ b/src/actions/types.js @@ -72,6 +72,8 @@ export type Action = | { type: 'SET_CHAT_CHANNEL', cid: number } | { type: 'ADD_CHAT_CHANNEL', channel: Object } | { type: 'REMOVE_CHAT_CHANNEL', cid: number } + | { type: 'MUTE_CHAT_CHANNEL', cid: number } + | { type: 'UNMUTE_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/App.jsx b/src/components/App.jsx index e2a2819b..edaf23da 100644 --- a/src/components/App.jsx +++ b/src/components/App.jsx @@ -19,7 +19,6 @@ import ChatBox from './ChatBox'; import Menu from './Menu'; import UI from './UI'; import ExpandMenuButton from './ExpandMenuButton'; -import MinecraftTPButton from './MinecraftTPButton'; import ModalRoot from './ModalRoot'; const App = () => ( @@ -34,7 +33,6 @@ const App = () => ( - diff --git a/src/components/ChannelContextMenu.jsx b/src/components/ChannelContextMenu.jsx index a12ea3ea..4934e93d 100644 --- a/src/components/ChannelContextMenu.jsx +++ b/src/components/ChannelContextMenu.jsx @@ -11,6 +11,8 @@ import { connect } from 'react-redux'; import { hideContextMenu, setLeaveChannel, + muteChatChannel, + unmuteChatChannel, } from '../actions'; import type { State } from '../reducers'; @@ -20,6 +22,9 @@ const UserContextMenu = ({ cid, channels, leave, + muteArr, + mute, + unmute, close, }) => { const wrapperRef = useRef(null); @@ -41,6 +46,8 @@ const UserContextMenu = ({ }; }, [wrapperRef]); + const isMuted = muteArr.includes(cid); + return (
-
- ✔✘ Mute +
{ + if (isMuted) { + unmute(cid); + } else { + mute(cid); + } + }} + tabIndex={0} + style={{ borderTop: 'none' }} + > + {`${(isMuted) ? '✔' : '✘'} Mute`}
{(channels[cid][1] !== 0) && ( @@ -62,7 +80,6 @@ const UserContextMenu = ({ close(); }} tabIndex={0} - style={{ borderTop: 'thin solid' }} > Close
@@ -83,11 +100,13 @@ function mapStateToProps(state: State) { const { cid, } = args; + const { mute: muteArr } = state.chatRead; return { xPos, yPos, cid, channels, + muteArr, }; } @@ -99,6 +118,12 @@ function mapDispatchToProps(dispatch) { leave(cid) { dispatch(setLeaveChannel(cid)); }, + mute(cid) { + dispatch(muteChatChannel(cid)); + }, + unmute(cid) { + dispatch(unmuteChatChannel(cid)); + }, }; } diff --git a/src/components/ChannelDropDown.jsx b/src/components/ChannelDropDown.jsx index f0fa6fb0..f0a82489 100644 --- a/src/components/ChannelDropDown.jsx +++ b/src/components/ChannelDropDown.jsx @@ -19,7 +19,9 @@ import { const ChannelDropDown = ({ channels, chatChannel, - chatRead, + unread, + chatNotify, + mute, setChannel, }) => { const [show, setShow] = useState(false); @@ -27,7 +29,9 @@ const ChannelDropDown = ({ // 1: DMs const [type, setType] = useState(0); const [offset, setOffset] = useState(0); + const [unreadAny, setUnreadAny] = useState(false); const [chatChannelName, setChatChannelName] = useState('...'); + const [hasDm, setHasDm] = useState(false); const wrapperRef = useRef(null); const buttonRef = useRef(null); @@ -44,6 +48,10 @@ const ChannelDropDown = ({ } }, []); + const handleWindowResize = useCallback(() => { + setShow(false); + }, []); + useLayoutEffect(() => { if (show) { if (channels[chatChannel]) { @@ -52,12 +60,45 @@ const ChannelDropDown = ({ } document.addEventListener('mousedown', handleClickOutside); document.addEventListener('touchstart', handleClickOutside); + window.addEventListener('resize', handleWindowResize); } else { document.removeEventListener('mousedown', handleClickOutside); document.removeEventListener('touchstart', handleClickOutside); + window.removeEventListener('resize', handleWindowResize); } }, [show]); + useEffect(() => { + const cids = Object.keys(channels); + let i = 0; + while (i < cids.length) { + const cid = cids[i]; + if ( + channels[cid][1] !== 0 + && unread[cid] + && !mute.includes(cid) + ) { + setUnreadAny(true); + break; + } + i += 1; + } + if (i === cids.length) { + setUnreadAny(false); + } + }, [unread]); + + useEffect(() => { + const cids = Object.keys(channels); + for (let i = 0; i < cids.length; i += 1) { + if (channels[cids[i]][1] === 1) { + setHasDm(true); + return; + } + } + setHasDm(false); + }, [channels]); + useEffect(() => { if (channels[chatChannel]) { setChatChannelName(channels[chatChannel][0]); @@ -70,12 +111,14 @@ const ChannelDropDown = ({ >
setShow(!show)} - className="channelbtn" + className={`channelbtn${(show) ? ' selected' : ''}`} > + {(unreadAny && chatNotify && !show) && ( +
⦿
+ )} {chatChannelName}
{(show) @@ -84,23 +127,41 @@ const ChannelDropDown = ({ ref={wrapperRef} style={{ position: 'absolute', - bottom: offset + 5, + bottom: offset, right: 9, }} className="channeldd" > -
+
setType(0)} + role="button" + tabIndex={-1} > - | - setType(1)} - > - - + {(hasDm) + && ( + setType(1)} + role="button" + tabIndex={-1} + > + {(unreadAny && chatNotify && type !== 1) && ( +
⦿
+ )} + +
+ )}
{ - const [name,, lastTs] = channels[cid]; - console.log(`name ${name} lastTC ${lastTs} compare to ${chatRead[cid]}`); + const [name] = channels[cid]; return (
setChannel(cid)} @@ -126,10 +186,12 @@ const ChannelDropDown = ({ (cid === chatChannel) ? ' selected' : '' }` } + role="button" + tabIndex={-1} > { - (chatRead[cid] < lastTs) ? ( - + (unread[cid] && chatNotify && !mute.includes(cid)) ? ( +
⦿
) : null } {name} @@ -145,15 +207,19 @@ const ChannelDropDown = ({ }; function mapStateToProps(state: State) { + const { channels } = state.chat; const { chatChannel, - chatRead, - } = state.gui; - const { channels } = state.chat; + unread, + mute, + } = state.chatRead; + const { chatNotify } = state.audio; return { channels, chatChannel, - chatRead, + unread, + mute, + chatNotify, }; } diff --git a/src/components/Chat.jsx b/src/components/Chat.jsx index ad4c07e1..3b5bb8f7 100644 --- a/src/components/Chat.jsx +++ b/src/components/Chat.jsx @@ -12,7 +12,6 @@ import { connect } from 'react-redux'; import type { State } from '../reducers'; import ChatMessage from './ChatMessage'; import ChannelDropDown from './ChannelDropDown'; -import { MAX_CHAT_MESSAGES } from '../core/constants'; import { showUserAreaModal, @@ -23,7 +22,6 @@ import { showContextMenu, } from '../actions'; import ProtocolClient from '../socket/ProtocolClient'; -import { saveSelection, restoreSelection } from '../utils/storeSelection'; import splitChatMessage from '../core/chatMessageFilter'; function escapeRegExp(string) { @@ -48,7 +46,6 @@ const Chat = ({ }) => { const listRef = useRef(); const targetRef = useRef(); - const [selection, setSelection] = useState(null); const [nameRegExp, setNameRegExp] = useState(null); const [blockedIds, setBlockedIds] = useState([]); const [btnSize, setBtnSize] = useState(20); @@ -59,7 +56,7 @@ const Chat = ({ }); const channelMessages = messages[chatChannel] || []; - if (!messages[chatChannel] && !fetching) { + if (channels[chatChannel] && !messages[chatChannel] && !fetching) { fetchMessages(chatChannel); } @@ -67,16 +64,6 @@ const Chat = ({ stayScrolled(); }, [channelMessages.length]); - /* - * TODO this removes focus from chat box, fix this - * - useEffect(() => { - if (channelMessages.length === MAX_CHAT_MESSAGES) { - restoreSelection(selection); - } - }, [channelMessages]); - */ - useEffect(() => { const regExp = (ownName) ? new RegExp(`(^|\\s)(@${escapeRegExp(ownName)})(\\s|$)`, 'g') @@ -168,7 +155,6 @@ const Chat = ({ className="chatarea" ref={listRef} style={{ flexGrow: 1 }} - onMouseUp={() => { setSelection(saveSelection); }} role="presentation" > { @@ -204,6 +190,7 @@ const Chat = ({ style={{ flexGrow: 1, minWidth: 40 }} value={inputMessage} onChange={(e) => setInputMessage(e.target.value)} + autoComplete="off" id="chatmsginput" maxLength="200" type="text" @@ -235,7 +222,7 @@ const Chat = ({ function mapStateToProps(state: State) { const { name } = state.user; - const { chatChannel } = state.gui; + const { chatChannel } = state.chatRead; const { channels, messages, diff --git a/src/components/ChatButton.jsx b/src/components/ChatButton.jsx index ebbb7d82..ecf9862a 100644 --- a/src/components/ChatButton.jsx +++ b/src/components/ChatButton.jsx @@ -3,24 +3,77 @@ * @flow */ -import React from 'react'; +import React, { + useState, useEffect, +} from 'react'; import { connect } from 'react-redux'; import { MdForum } from 'react-icons/md'; import { showChatModal } from '../actions'; -const ChatButton = ({ open }) => ( -
- -
: null -); +const ChatButton = ({ + chatOpen, + modalOpen, + chatNotify, + channels, + unread, + mute, + open, +}) => { + const [unreadAny, setUnreadAny] = useState(false); + + /* + * almost the same as in ChannelDropDown + * just cares about chatNotify too + */ + useEffect(() => { + if (!chatNotify || modalOpen || chatOpen) { + setUnreadAny(false); + return; + } + const cids = Object.keys(channels); + let i = 0; + while (i < cids.length) { + const cid = cids[i]; + if ( + channels[cid][1] !== 0 + && unread[cid] + && !mute.includes(cid) + ) { + setUnreadAny(true); + break; + } + i += 1; + } + if (i === cids.length) { + setUnreadAny(false); + } + }); + + return ( +
+ {(unreadAny) && ( +
⦿
+ )} + +
: null + ); +}; function mapDispatchToProps(dispatch) { return { @@ -30,4 +83,29 @@ function mapDispatchToProps(dispatch) { }; } -export default connect(null, mapDispatchToProps)(ChatButton); +function mapStateToProps(state) { + const { + chatOpen, + modalOpen, + } = state.modal; + const { + chatNotify, + } = state.audio; + const { + channels, + } = state.chat; + const { + unread, + mute, + } = state.chatRead; + return { + chatOpen, + modalOpen, + chatNotify, + channels, + unread, + mute, + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(ChatButton); diff --git a/src/components/Menu.jsx b/src/components/Menu.jsx index 426cab2e..3ad91e5b 100644 --- a/src/components/Menu.jsx +++ b/src/components/Menu.jsx @@ -11,7 +11,13 @@ import HelpButton from './HelpButton'; import SettingsButton from './SettingsButton'; import LogInButton from './LogInButton'; import DownloadButton from './DownloadButton'; -import MinecraftButton from './MinecraftButton'; +/* + * removed MinecraftButton cause it didn't get used in over a year + * also CSS rule got removed + * and MinecraftModal from ModalRoot + * and MinecraftTPButton from App + * (support for it will be otherwise still kept) + */ function Menu({ menuOpen, @@ -37,7 +43,6 @@ function Menu({ -
) diff --git a/src/components/ModalRoot.jsx b/src/components/ModalRoot.jsx index d2f1da1d..868709f6 100644 --- a/src/components/ModalRoot.jsx +++ b/src/components/ModalRoot.jsx @@ -21,7 +21,6 @@ import RegisterModal from './RegisterModal'; import CanvasSelectModal from './CanvasSelectModal'; import ChatModal from './ChatModal'; import ForgotPasswordModal from './ForgotPasswordModal'; -import MinecraftModal from './MinecraftModal'; const MODAL_COMPONENTS = { @@ -32,7 +31,6 @@ const MODAL_COMPONENTS = { REGISTER: RegisterModal, FORGOT_PASSWORD: ForgotPasswordModal, CHAT: ChatModal, - MINECRAFT: MinecraftModal, CANVAS_SELECTION: CanvasSelectModal, /* other modals */ }; diff --git a/src/components/SettingsModal.jsx b/src/components/SettingsModal.jsx index 3dc3eeb3..abd8d9a1 100644 --- a/src/components/SettingsModal.jsx +++ b/src/components/SettingsModal.jsx @@ -135,7 +135,7 @@ function SettingsModal({ - {bl[1]} + {`⦸ ${bl[1]}`}
)) } diff --git a/src/components/UserContextMenu.jsx b/src/components/UserContextMenu.jsx index 76a8af1d..1fa92430 100644 --- a/src/components/UserContextMenu.jsx +++ b/src/components/UserContextMenu.jsx @@ -13,6 +13,7 @@ import { addToChatInputMessage, startDm, setUserBlock, + setChatChannel, } from '../actions'; import type { State } from '../reducers'; @@ -24,6 +25,9 @@ const UserContextMenu = ({ addToInput, dm, block, + channels, + fetching, + setChannel, close, }) => { const wrapperRef = useRef(null); @@ -56,13 +60,13 @@ const UserContextMenu = ({ }} >
{ block(uid, name); close(); }} role="button" tabIndex={-1} + style={{ borderTop: 'none' }} > Block
@@ -70,11 +74,24 @@ const UserContextMenu = ({ role="button" tabIndex={0} onClick={() => { - dm(uid); - // TODO if DM Channel with user already exist, just switch + /* + * if dm channel already exists, + * just switch + */ + const cids = Object.keys(channels); + for (let i = 0; i < cids.length; i += 1) { + const cid = cids[i]; + if (channels[cid].length === 4 && channels[cid][3] === uid) { + setChannel(cid); + close(); + return; + } + } + if (!fetching) { + dm(uid); + } close(); }} - style={{ borderBottom: 'thin solid' }} > DM
@@ -98,15 +115,23 @@ function mapStateToProps(state: State) { yPos, args, } = state.contextMenu; + const { + channels, + } = state.chat; const { name, uid, } = args; + const { + fetchingApi: fetching, + } = state.fetching; return { xPos, yPos, + channels, name, uid, + fetching, }; } @@ -129,6 +154,9 @@ function mapDispatchToProps(dispatch) { close() { dispatch(hideContextMenu()); }, + setChannel(channelId) { + dispatch(setChatChannel(channelId)); + }, }; } diff --git a/src/controls/PixelPainterControls.js b/src/controls/PixelPainterControls.js index 785ac759..7da2d516 100644 --- a/src/controls/PixelPainterControls.js +++ b/src/controls/PixelPainterControls.js @@ -211,10 +211,7 @@ class PixelPlainterControls { this.store, this.viewport, this.renderer, - [ - this.clickTapStartCoords[0], - this.clickTapStartCoords[1], - ], + this.clickTapStartCoords, ); }, 800); } diff --git a/src/core/ChatProvider.js b/src/core/ChatProvider.js index b7d6bd99..47372f7c 100644 --- a/src/core/ChatProvider.js +++ b/src/core/ChatProvider.js @@ -105,28 +105,22 @@ export class ChatProvider { userId, channelId, channelArray, - notify = true, ) { - /* - * since UserId and ChannelId are primary keys, - * this will throw if already exists - */ - const relation = await UserChannel.create({ - UserId: userId, - ChannelId: channelId, - }, { + const [, created] = await UserChannel.findOrCreate({ + where: { + UserId: userId, + ChannelId: channelId, + }, raw: true, }); - console.log('HEREEEEE HHEEERRREEE'); - console.log(relation); - - webSockets.broadcastAddChatChannel( - userId, - channelId, - channelArray, - notify, - ); + if (created) { + webSockets.broadcastAddChatChannel( + userId, + channelId, + channelArray, + ); + } } userHasChannelAccess(user, cid, write = false) { @@ -134,7 +128,7 @@ export class ChatProvider { if (!write || user.regUser) { return true; } - } else if (user.regUser && user.channelIds.includes(cid)) { + } else if (user.regUser && user.channels[cid]) { return true; } return false; @@ -146,7 +140,7 @@ export class ChatProvider { } const channelArray = user.channels[cid]; if (channelArray && channelArray.length === 4) { - return user.channels[cid][4]; + return user.channels[cid][3]; } return null; } diff --git a/src/core/constants.js b/src/core/constants.js index 29ba4fc2..a949ecb9 100644 --- a/src/core/constants.js +++ b/src/core/constants.js @@ -87,6 +87,10 @@ export const CHAT_CHANNELS = [ name: 'en', }, { name: 'int', + }, { + name: 'pol', + }, { + name: 'art', }, ]; diff --git a/src/data/models/Message.js b/src/data/models/Message.js index 208f0558..336bd0af 100644 --- a/src/data/models/Message.js +++ b/src/data/models/Message.js @@ -30,11 +30,13 @@ const Message = Model.define('Message', { Message.belongsTo(Channel, { as: 'channel', foreignKey: 'cid', + onDelete: 'cascade', }); Message.belongsTo(RegUser, { as: 'user', foreignKey: 'uid', + onDelete: 'cascade', }); export default Message; diff --git a/src/data/models/RegUser.js b/src/data/models/RegUser.js index bd745aa8..233f492b 100644 --- a/src/data/models/RegUser.js +++ b/src/data/models/RegUser.js @@ -64,10 +64,11 @@ const RegUser = Model.define('User', { defaultValue: false, }, - blockDm: { - type: DataType.BOOLEAN, + // currently just blockDm + blocks: { + type: DataType.TINYINT, allowNull: false, - defaultValue: false, + defaultValue: 0, }, discordid: { @@ -120,6 +121,10 @@ const RegUser = Model.define('User', { mcVerified(): boolean { return this.verified & 0x02; }, + + blockDm(): boolean { + return this.blocks & 0x01; + }, }, setterMethods: { @@ -133,6 +138,11 @@ const RegUser = Model.define('User', { this.setDataValue('verified', val); }, + blockDm(num: boolean) { + const val = (num) ? (this.blocks | 0x01) : (this.blocks & ~0x01); + this.setDataValue('blocks', val); + }, + password(value: string) { if (value) this.setDataValue('password', generateHash(value)); }, diff --git a/src/data/models/User.js b/src/data/models/User.js index c81a0e18..a0e398e3 100644 --- a/src/data/models/User.js +++ b/src/data/models/User.js @@ -29,7 +29,6 @@ class User { this.id = id; this.ip = ip; this.channels = {}; - this.channelIds = []; this.blocked = []; this.ipSub = getIPv6Subnet(ip); this.wait = null; @@ -64,7 +63,6 @@ class User { dmu1, dmu2, } = reguser.channel[i]; - this.channelIds.push(id); if (type === 1) { /* in DMs: * the name is the name of the other user @@ -72,19 +70,19 @@ class User { */ const name = (dmu1.id === this.id) ? dmu2.name : dmu1.name; const dmu = (dmu1.id === this.id) ? dmu2.id : dmu1.id; - this.channels[id] = [ + this.addChannel(id, [ name, type, lastTs, dmu, - ]; + ]); } else { const { name } = reguser.channel[i]; - this.channels[id] = [ + this.addChannel(id, [ name, type, lastTs, - ]; + ]); } } } @@ -99,6 +97,14 @@ class User { } } + addChannel(cid, channelArray) { + this.channels[cid] = channelArray; + } + + removeChannel(cid) { + delete this.channels[cid]; + } + getName() { return (this.regUser) ? this.regUser.name : null; } diff --git a/src/reducers/chat.js b/src/reducers/chat.js index 1b613654..1f8d6d33 100644 --- a/src/reducers/chat.js +++ b/src/reducers/chat.js @@ -54,7 +54,7 @@ export default function chat( const channels = { ...state.channels }; const messages = { ...state.messages }; const keys = Object.keys(channels); - for (let i = 0; i < messages.length; i += 1) { + for (let i = 0; i < keys.length; i += 1) { const cid = keys[i]; if (channels[cid][1] === 0) { delete messages[cid]; @@ -81,7 +81,7 @@ export default function chat( */ const channels = { ...state.channels }; const chanKeys = Object.keys(channels); - for (let i = 0; i < chanKeys; i += 1) { + for (let i = 0; i < chanKeys.length; i += 1) { const cid = chanKeys[i]; if (channels[cid][1] === 1 && channels[cid][3] === userId) { delete channels[cid]; @@ -109,7 +109,10 @@ export default function chat( case 'ADD_CHAT_CHANNEL': { const { channel } = action; - console.log('adding channel', channel); + const [cid] = Object.keys(channel); + if (state.channels[cid]) { + return state; + } return { ...state, channels: { @@ -121,11 +124,17 @@ export default function chat( case 'REMOVE_CHAT_CHANNEL': { const { cid } = action; + if (!state.channels[cid]) { + return state; + } const channels = { ...state.channels }; + const messages = { ...state.messages }; + delete messages[cid]; delete channels[cid]; return { ...state, channels, + messages, }; } diff --git a/src/reducers/chatRead.js b/src/reducers/chatRead.js new file mode 100644 index 00000000..affe373e --- /dev/null +++ b/src/reducers/chatRead.js @@ -0,0 +1,153 @@ +/* + * local save state for chat stuff + * + * @flow + */ + +import type { Action } from '../actions/types'; + +const TIME_DIFF_THREASHOLD = 15000; + +export type ChatReadState = { + // channels that are muted + // [cid, cid2, ...] + mute: Array, + // timestamps of last read + // {cid: lastTs, ...} + readTs: Object, + // booleans if channel is unread + // {cid: unread, ...} + unread: Object, + // selected chat channel + chatChannel: number, +}; + +const initialState: ChatReadState = { + mute: [], + readTs: {}, + unread: {}, + chatChannel: 1, +}; + + +export default function chatRead( + state: ModalState = initialState, + action: Action, +): ChatReadState { + switch (action.type) { + case 'RECEIVE_ME': + case 'LOGIN': { + const { channels } = action; + const cids = Object.keys(channels); + const readTs = {}; + const unread = {}; + for (let i = 0; i < cids.length; i += 1) { + const cid = cids[i]; + if (!state.readTs[cid]) { + readTs[cid] = 0; + } else { + readTs[cid] = state.readTs[cid]; + } + unread[cid] = (channels[cid][2] > readTs[cid]); + } + return { + ...state, + readTs, + unread, + }; + } + + case 'SET_CHAT_CHANNEL': { + const { cid } = action; + return { + ...state, + chatChannel: cid, + readTs: { + ...state.readTs, + [cid]: Date.now() + TIME_DIFF_THREASHOLD, + }, + unread: { + ...state.unread, + [cid]: false, + }, + }; + } + + case 'ADD_CHAT_CHANNEL': { + const [cid] = Object.keys(action.channel); + return { + ...state, + readTs: { + ...state.readTs, + [cid]: state.readTs[cid] || 0, + }, + unread: { + ...state.unread, + [cid]: true, + }, + }; + } + + case 'REMOVE_CHAT_CHANNEL': { + const { cid } = action; + if (!state.readTs[cid]) { + return state; + } + const readTs = { ...state.readTs }; + delete readTs[cid]; + const unread = { ...state.unread }; + delete unread[cid]; + return { + ...state, + readTs, + unread, + }; + } + + case 'RECEIVE_CHAT_MESSAGE': { + const { channel: cid } = action; + const { chatChannel } = state; + // eslint-disable-next-line eqeqeq + const readTs = (chatChannel == cid) + ? { + ...state.readTs, + // 15s treshold for desync + [cid]: Date.now() + TIME_DIFF_THREASHOLD, + } : state.readTs; + // eslint-disable-next-line eqeqeq + const unread = (chatChannel != cid) + ? { + ...state.unread, + [cid]: true, + } : state.unread; + return { + ...state, + readTs, + unread, + }; + } + + case 'MUTE_CHAT_CHANNEL': { + const { cid } = action; + return { + ...state, + mute: [ + ...state.mute, + cid, + ], + }; + } + + case 'UNMUTE_CHAT_CHANNEL': { + const { cid } = action; + const mute = state.mute.filter((id) => (id !== cid)); + return { + ...state, + mute, + }; + } + + default: + return state; + } +} diff --git a/src/reducers/gui.js b/src/reducers/gui.js index f9814c53..92c39b24 100644 --- a/src/reducers/gui.js +++ b/src/reducers/gui.js @@ -15,10 +15,6 @@ export type GUIState = { compactPalette: boolean, paletteOpen: boolean, menuOpen: boolean, - chatChannel: number, - // timestamps of last read post per channel - // { 1: Date.now() } - chatRead: {}, style: string, }; @@ -33,8 +29,6 @@ const initialState: GUIState = { compactPalette: false, paletteOpen: true, menuOpen: false, - chatChannel: 1, - chatRead: {}, style: 'default', }; @@ -108,19 +102,6 @@ export default function gui( }; } - case 'SET_CHAT_CHANNEL': { - const { cid } = action; - - return { - ...state, - chatChannel: cid, - chatRead: { - ...state.chatRead, - cid: Date.now(), - }, - }; - } - case 'SELECT_COLOR': { const { compactPalette, @@ -145,43 +126,6 @@ export default function gui( }; } - case 'RECEIVE_ME': - case 'LOGIN': { - const { channels } = action; - const cids = Object.keys(channels); - const chatRead = { ...state.chatRead }; - for (let i = 0; i < cids.length; i += 1) { - const cid = cids[i]; - chatRead[cid] = 0; - } - return { - ...state, - chatRead, - }; - } - - case 'ADD_CHAT_CHANNEL': { - const [cid] = Object.keys(action.channel); - return { - ...state, - chatRead: { - ...state.chatRead, - [cid]: 0, - }, - }; - } - - case 'REMOVE_CHAT_CHANNEL': { - const { cid } = action; - const chatRead = { ...state.chatRead }; - delete chatRead[cid]; - return { - ...state, - chatRead, - }; - } - - case 'PLACE_PIXEL': { let { pixelsPlaced } = state; pixelsPlaced += 1; diff --git a/src/reducers/index.js b/src/reducers/index.js index ff068f35..28fc39ef 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -9,6 +9,7 @@ import modal from './modal'; import user from './user'; import chat from './chat'; import contextMenu from './contextMenu'; +import chatRead from './chatRead'; import fetching from './fetching'; import type { AudioState } from './audio'; @@ -28,6 +29,7 @@ export type State = { user: UserState, chat: ChatState, contextMenu: ContextMenuState, + chatRead: ChatReadState, fetching: FetchingState, }; @@ -52,5 +54,6 @@ export default persistCombineReducers(config, { user, chat, contextMenu, + chatRead, fetching, }); diff --git a/src/routes/api/block.js b/src/routes/api/block.js index 263ad4bc..3fc97f75 100644 --- a/src/routes/api/block.js +++ b/src/routes/api/block.js @@ -108,8 +108,8 @@ async function block(req: Request, res: Response) { if (channel) { const channelId = channel.id; channel.destroy(); - webSockets.broadcastRemoveChatChannel(user.id, channelId, false); - webSockets.broadcastRemoveChatChannel(userId, channelId, true); + webSockets.broadcastRemoveChatChannel(user.id, channelId); + webSockets.broadcastRemoveChatChannel(userId, channelId); } if (ret) { diff --git a/src/routes/api/blockdm.js b/src/routes/api/blockdm.js index 1e2ac0da..3677baf0 100644 --- a/src/routes/api/blockdm.js +++ b/src/routes/api/blockdm.js @@ -45,10 +45,10 @@ async function blockdm(req: Request, res: Response) { const channel = channels[i]; if (channel.type === 1) { const channelId = channel.id; - channel.destroy(); const { dmu1id, dmu2id } = channel; - webSockets.broadcastRemoveChatChannel(dmu1id, channelId, true); - webSockets.broadcastRemoveChatChannel(dmu2id, channelId, true); + channel.destroy(); + webSockets.broadcastRemoveChatChannel(dmu1id, channelId); + webSockets.broadcastRemoveChatChannel(dmu2id, channelId); } } diff --git a/src/routes/api/leavechan.js b/src/routes/api/leavechan.js index 84fd0089..59070d53 100644 --- a/src/routes/api/leavechan.js +++ b/src/routes/api/leavechan.js @@ -65,7 +65,7 @@ async function leaveChan(req: Request, res: Response) { user.regUser.removeChannel(channel); - webSockets.broadcastRemoveChatChannel(user.id, channelId, false); + webSockets.broadcastRemoveChatChannel(user.id, channelId); res.json({ status: 'ok', diff --git a/src/routes/api/startdm.js b/src/routes/api/startdm.js index bebc9e24..eb3c09fd 100644 --- a/src/routes/api/startdm.js +++ b/src/routes/api/startdm.js @@ -47,12 +47,6 @@ async function startDm(req: Request, res: Response) { const targetUser = await RegUser.findOne({ where: query, - attributes: [ - 'id', - 'name', - 'blockDm', - ], - raw: true, }); if (!targetUser) { res.status(401); @@ -61,14 +55,15 @@ async function startDm(req: Request, res: Response) { }); return; } + userId = targetUser.id; + userName = targetUser.name; if (targetUser.blockDm) { res.status(401); res.json({ - errors: ['Target user doesn\'t allo DMs'], + errors: [`${userName} doesn't allow DMs`], }); + return; } - userId = targetUser.id; - userName = targetUser.name; /* * check if blocked @@ -76,7 +71,7 @@ async function startDm(req: Request, res: Response) { if (await isUserBlockedBy(user.id, userId)) { res.status(401); res.json({ - errors: ['You are blocked by this user'], + errors: [`${userName} has blocked you.`], }); return; } @@ -106,10 +101,19 @@ async function startDm(req: Request, res: Response) { raw: true, }); const ChannelId = channel[0].id; + const curTime = Date.now(); const promises = [ - ChatProvider.addUserToChannel(user.id, ChannelId, false), - ChatProvider.addUserToChannel(userId, ChannelId, true), + ChatProvider.addUserToChannel( + user.id, + ChannelId, + [userName, 1, curTime, userId], + ), + ChatProvider.addUserToChannel( + userId, + ChannelId, + [user.getName(), 1, curTime, user.id], + ), ]; await Promise.all(promises); diff --git a/src/socket/SocketServer.js b/src/socket/SocketServer.js index 8a233348..5d24bbdc 100644 --- a/src/socket/SocketServer.js +++ b/src/socket/SocketServer.js @@ -104,7 +104,7 @@ class SocketServer extends WebSocketEvents { }); ws.on('message', (message) => { if (typeof message === 'string') { - SocketServer.onTextMessage(message, ws); + this.onTextMessage(message, ws); } else { this.onBinaryMessage(message, ws); } @@ -175,13 +175,19 @@ class SocketServer extends WebSocketEvents { }); } + /* + * keep in mind that a user could + * be connected from multiple devices + */ findWsByUserId(userId) { - const { clients } = this.wss; - for (let i = 0; i < clients.length; i += 1) { - const ws = clients[i]; + const it = this.wss.clients.keys(); + let client = it.next(); + while (!client.done) { + const ws = client.value; if (ws.user.id === userId && ws.readyState === WebSocket.OPEN) { return ws; } + client = it.next(); } return null; } @@ -190,35 +196,31 @@ class SocketServer extends WebSocketEvents { userId: number, channelId: number, channelArray: Array, - notify: boolean, ) { - const ws = this.findWsByUserId(userId); - if (ws) { - ws.user.channels[channelId] = channelArray; - const text = JSON.stringify([ - 'addch', { - [channelId]: channelArray, - }, - ]); - if (notify) { + this.wss.clients.forEach((ws) => { + if (ws.user.id === userId && ws.readyState === WebSocket.OPEN) { + ws.user.addChannel(channelId, channelArray); + const text = JSON.stringify([ + 'addch', { + [channelId]: channelArray, + }, + ]); ws.send(text); } - } + }); } broadcastRemoveChatChannel( userId: number, channelId: number, - notify: boolean, ) { - const ws = this.findWsByUserId(userId); - if (ws) { - delete ws.user.channels[channelId]; - const text = JSON.stringify('remch', channelId); - if (notify) { + this.wss.clients.forEach((ws) => { + if (ws.user.id === userId && ws.readyState === WebSocket.OPEN) { + ws.user.removeChannel(channelId); + const text = JSON.stringify(['remch', channelId]); ws.send(text); } - } + }); } broadcastPixelBuffer(canvasId: number, chunkid, data: Buffer) { @@ -286,7 +288,7 @@ class SocketServer extends WebSocketEvents { webSockets.broadcastOnlineCounter(online); } - static async onTextMessage(text, ws) { + async onTextMessage(text, ws) { /* * all client -> server text messages are * chat messages in [message, channelId] format @@ -333,13 +335,11 @@ class SocketServer extends WebSocketEvents { */ const dmUserId = chatProvider.checkIfDm(user, channelId); if (dmUserId) { - console.log('is dm'); const dmWs = this.findWsByUserId(dmUserId); - if (!dmWs + if (!dmWs || !chatProvider.userHasChannelAccess(dmWs.user, channelId) ) { - console.log('adding channel') - ChatProvider.addUserToChannel( + await ChatProvider.addUserToChannel( dmUserId, channelId, [ws.name, 1, Date.now(), user.id], diff --git a/src/socket/WebSocketEvents.js b/src/socket/WebSocketEvents.js index a24f3268..fa213825 100644 --- a/src/socket/WebSocketEvents.js +++ b/src/socket/WebSocketEvents.js @@ -28,14 +28,12 @@ class WebSocketEvents { userId: number, channelId: number, channelArray: Array, - notify: boolean, ) { } broadcastRemoveChatChannel( userId: number, channelId: number, - notify: boolean, ) { } diff --git a/src/socket/websockets.js b/src/socket/websockets.js index ae9891a4..ceb29d9f 100644 --- a/src/socket/websockets.js +++ b/src/socket/websockets.js @@ -94,20 +94,17 @@ class WebSockets { * @param userId numerical id of user * @param channelId numerical id of chat channel * @param channelArray array with channel info [name, type, lastTs] - * @param notify if user should get notified over websocket - * (i.e. false if the user already gets it via api response) */ broadcastAddChatChannel( userId: number, channelId: number, channelArray: Array, - notify: boolean = true, ) { this.listeners.forEach( (listener) => listener.broadcastAddChatChannel( userId, + channelId, channelArray, - notify, ), ); } @@ -116,19 +113,16 @@ class WebSockets { * broadcast Removing chat channel from user * @param userId numerical id of user * @param channelId numerical id of chat channel - * @param notify if user should get notified over websocket * (i.e. false if the user already gets it via api response) */ broadcastRemoveChatChannel( userId: number, channelId: number, - notify: boolean = true, ) { this.listeners.forEach( (listener) => listener.broadcastRemoveChatChannel( userId, channelId, - notify, ), ); } diff --git a/src/store/audio.js b/src/store/audio.js index 02fbfa70..3ee8d5de 100644 --- a/src/store/audio.js +++ b/src/store/audio.js @@ -200,21 +200,31 @@ export default (store) => (next) => (action) => { } case 'RECEIVE_CHAT_MESSAGE': { - if (!chatNotify) break; + if (mute || !chatNotify) break; - const { isPing } = action; - const { chatChannel } = state.gui; - // eslint-disable-next-line eqeqeq - if (!isPing && action.channel != chatChannel) { - break; - } + const { isPing, channel } = action; + const { mute: muteCh, chatChannel } = state.chatRead; + if (muteCh.includes(channel)) break; + if (muteCh.includes(`${channel}`)) break; + const { channels } = state.chat; const oscillatorNode = context.createOscillator(); const gainNode = context.createGain(); oscillatorNode.type = 'sine'; oscillatorNode.frequency.setValueAtTime(310, context.currentTime); - const freq = (isPing) ? 540 : 355; + /* + * ping if user mention or + * message in DM channel that is not currently open + */ + const freq = (isPing + || ( + channels[channel] + && channels[channel][1] === 1 + // eslint-disable-next-line eqeqeq + && channel != chatChannel + ) + ) ? 540 : 355; oscillatorNode.frequency.exponentialRampToValueAtTime( freq, context.currentTime + 0.025, diff --git a/src/store/protocolClientHook.js b/src/store/protocolClientHook.js index 456661d6..10485fed 100644 --- a/src/store/protocolClientHook.js +++ b/src/store/protocolClientHook.js @@ -19,7 +19,7 @@ export default (store) => (next) => (action) => { } case 'SET_NAME': - case 'LOGIN:': + case 'LOGIN': case 'LOGOUT': { ProtocolClient.reconnect(); break; diff --git a/src/styles/arkeros.css b/src/styles/arkeros.css index 5150a6af..e04ce2a9 100644 --- a/src/styles/arkeros.css +++ b/src/styles/arkeros.css @@ -22,10 +22,14 @@ tr:nth-child(odd) { color: #ff91a6; } -.actionbuttons:hover, .menu > div:hover { +.actionbuttons:hover, .menu > div:hover, .channeldd, .contextmenu { background: linear-gradient(160deg, #61dcea , #ffb1e1, #ecffec, #ffb1e1, #61dcea); } +.chn, .chntype, .contextmenu > div { + background-color: #ebebeb80; +} + #chatbutton { background: linear-gradient(135deg, orange , yellow, green, aqua, blue, violet); } diff --git a/src/styles/dark-round.css b/src/styles/dark-round.css index d22635bf..9a6d5574 100644 --- a/src/styles/dark-round.css +++ b/src/styles/dark-round.css @@ -35,6 +35,25 @@ tr:nth-child(even) { border-radius: 8px; } +.channeldd, .contextmenu { + background-color: #535356; + color: #efefef; + border-radius: 8px; +} + +.chntop { + margin-top: 4px; +} + +.chn, .chntype, .contextmenu > div { + background-color: #5f5f5f; +} + +.chn.selected, .chn:hover, .chntype.selected, .chntype:hover, +.contextmenu > div:hover { + background-color: #404040; +} + .actionbuttons, .coorbox, .onlinebox, .cooldownbox, #historyselect { background-color: rgba(59, 59, 59, 0.8); color: #f4f4f4; diff --git a/src/styles/dark.css b/src/styles/dark.css index d0f5911a..526353de 100644 --- a/src/styles/dark.css +++ b/src/styles/dark.css @@ -65,6 +65,20 @@ tr:nth-child(even) { background-color: hsla(216, 4%, 74%, .3); } +.channeldd, .contextmenu { + background-color: #535356; + color: #efefef; +} + +.chn, .chntype, .contextmenu > div { + background-color: #5f5f5f; +} + +.chn.selected, .chn:hover, .chntype.selected, .chntype:hover, +.contextmenu > div:hover { + background-color: #404040; +} + .modalinfo { color: #ddd; } diff --git a/src/styles/default.css b/src/styles/default.css index 31aeaf1e..98d5fe2c 100644 --- a/src/styles/default.css +++ b/src/styles/default.css @@ -134,15 +134,20 @@ tr:nth-child(even) { background-color: rgba(226, 226, 226); border: solid black; border-width: thin; + color: #212121; + box-shadow: 0 0 2px 2px rgba(0,0,0,.2); } .contextmenu > div { - border-width: thin; margin: 2px; + height: 18px; + padding: 3px 2px 0px 0px; + background-color: #ebebeb; + border-top: thin solid #b1b1b2; } .contextmenu > div:hover { - background-color: white; + background-color: #c9c9c9; cursor: pointer; } @@ -155,29 +160,42 @@ tr:nth-child(even) { } .channelbtn { - background-color: #d0d0d0; + position: relative; + background-color: #ebebeb; text-align: center; border-style: solid; border-width: thin; border-radius: 4px; -} - -.channelbtn:hover { - cursor: pointer; - background-color: white; + width: 50px; + height: 100%; + white-space: nowrap; + font-size: 14px; + overflow-x: hidden; + color: #212121; } .channeldd { background-color: rgba(226, 226, 226); + color: #212121; + border: solid black; + border-width: thin; width: 90px; + box-shadow: 0 0 2px 2px rgba(0,0,0,.2); } .channeldds { height: 120px; - overflow-y: scroll; + overflow-y: auto; + overflow-x: hidden; margin: 2px; } +.chntop { + display: flex; + height: 24px; + border-bottom: solid thin; +} + .actionbuttons, .coorbox, .onlinebox, .cooldownbox, #palettebox { position: fixed; background-color: rgba(226, 226, 226, 0.80); @@ -224,10 +242,6 @@ tr:nth-child(even) { } #helpbutton { - left: 16px; - top: 221px; -} -#minecraftbutton { left: 16px; top: 180px; } @@ -485,15 +499,43 @@ tr:nth-child(even) { cursor: pointer; } -.chn.selected, .chnunread { - font-weight: bold; - font-size: 17px; +.chn { + position: relative; + background-color: #ebebeb; + white-space: nowrap; + border-bottom: solid thin #b1b1b2; + padding: 1px; + font-size: 15px; + height: 22px; +} + +.chn.selected, .chn:hover, .channelbtn.selected, .channelbtn:hover { + cursor: pointer; + background-color: #c9c9c9; } .chnunread { + position: absolute; + top: -1px; + right: 1px; + font-weight: bold; + font-size: 12px; color: red; } +.chntype { + position: relative; + flex: auto; + text-align: center; + background-color: #ebebeb; + border-left: solid thin #b1b1b2; +} +.chntype.selected, .chntype:hover { + cursor: pointer; + font-size: 110%; + background-color: #c9c9c9; +} + .usermessages { font-size: 14px; font-weight: 500; diff --git a/src/styles/light-round.css b/src/styles/light-round.css index 040fce07..1395ce3f 100644 --- a/src/styles/light-round.css +++ b/src/styles/light-round.css @@ -1,7 +1,11 @@ -.chatbox { +.chatbox, .channeldd, .contextmenu { border-radius: 8px; } +.chntop { + margin-top: 4px; +} + .actionbuttons, .coorbox, .onlinebox, .cooldownbox, #historyselect { border-radius: 21px; }