diff --git a/README.md b/README.md
index f292d5d..68033ef 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 62222bd..9605af2 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 7c90f72..88c37f8 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 e2a2819..edaf23d 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 a12ea3e..4934e93 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 f0fa6fb..f0a8248 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 ad4c07e..3b5bb8f 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 ebbb7d8..ecf9862 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 (
+
: 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 426cab2..3ad91e5 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 d2f1da1..868709f 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 3dc3eeb..abd8d9a 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 76a8af1..1fa9243 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 785ac75..7da2d51 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 b7d6bd9..47372f7 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 29ba4fc..a949ecb 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 208f055..336bd0a 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 bd745aa..233f492 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 c81a0e1..a0e398e 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 1b61365..1f8d6d3 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 0000000..affe373
--- /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 f9814c5..92c39b2 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 ff068f3..28fc39e 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 263ad4b..3fc97f7 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 1e2ac0d..3677baf 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 84fd008..59070d5 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 bebc9e2..eb3c09f 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 8a23334..5d24bbd 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 a24f326..fa21382 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 ae9891a..ceb29d9 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 02fbfa7..3ee8d5d 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 456661d..10485fe 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 5150a6a..e04ce2a 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 d22635b..9a6d557 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 d0f5911..526353d 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 31aeaf1..98d5fe2 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 040fce0..1395ce3 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;
}