fixing bugs that got introduced in the past two commits

This commit is contained in:
HF 2020-11-28 02:04:40 +01:00
parent ac464ba5a7
commit b79a12f931
37 changed files with 651 additions and 240 deletions

View File

@ -97,7 +97,7 @@ Configuration takes place in the environment variables that are defined in ecosy
Notes: 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. - 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 - 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) - 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)

View File

@ -486,7 +486,6 @@ export function receivePixelUpdate(
export function loginUser( export function loginUser(
me: Object, me: Object,
): Action { ): Action {
console.log('login', me);
return { return {
type: 'LOGIN', type: 'LOGIN',
...me, ...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 * query: Object with either userId: number or userName: string
*/ */

View File

@ -72,6 +72,8 @@ export type Action =
| { type: 'SET_CHAT_CHANNEL', cid: number } | { type: 'SET_CHAT_CHANNEL', cid: number }
| { type: 'ADD_CHAT_CHANNEL', channel: Object } | { type: 'ADD_CHAT_CHANNEL', channel: Object }
| { type: 'REMOVE_CHAT_CHANNEL', cid: number } | { 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_FETCHING', fetching: boolean }
| { type: 'SET_CHAT_INPUT_MSG', message: string } | { type: 'SET_CHAT_INPUT_MSG', message: string }
| { type: 'ADD_CHAT_INPUT_MSG', message: string } | { type: 'ADD_CHAT_INPUT_MSG', message: string }

View File

@ -19,7 +19,6 @@ import ChatBox from './ChatBox';
import Menu from './Menu'; import Menu from './Menu';
import UI from './UI'; import UI from './UI';
import ExpandMenuButton from './ExpandMenuButton'; import ExpandMenuButton from './ExpandMenuButton';
import MinecraftTPButton from './MinecraftTPButton';
import ModalRoot from './ModalRoot'; import ModalRoot from './ModalRoot';
const App = () => ( const App = () => (
@ -34,7 +33,6 @@ const App = () => (
<OnlineBox /> <OnlineBox />
<CoordinatesBox /> <CoordinatesBox />
<ExpandMenuButton /> <ExpandMenuButton />
<MinecraftTPButton />
<UI /> <UI />
<ModalRoot /> <ModalRoot />
</IconContext.Provider> </IconContext.Provider>

View File

@ -11,6 +11,8 @@ import { connect } from 'react-redux';
import { import {
hideContextMenu, hideContextMenu,
setLeaveChannel, setLeaveChannel,
muteChatChannel,
unmuteChatChannel,
} from '../actions'; } from '../actions';
import type { State } from '../reducers'; import type { State } from '../reducers';
@ -20,6 +22,9 @@ const UserContextMenu = ({
cid, cid,
channels, channels,
leave, leave,
muteArr,
mute,
unmute,
close, close,
}) => { }) => {
const wrapperRef = useRef(null); const wrapperRef = useRef(null);
@ -41,6 +46,8 @@ const UserContextMenu = ({
}; };
}, [wrapperRef]); }, [wrapperRef]);
const isMuted = muteArr.includes(cid);
return ( return (
<div <div
ref={wrapperRef} ref={wrapperRef}
@ -50,8 +57,19 @@ const UserContextMenu = ({
top: yPos, top: yPos,
}} }}
> >
<div> <div
Mute role="button"
onClick={() => {
if (isMuted) {
unmute(cid);
} else {
mute(cid);
}
}}
tabIndex={0}
style={{ borderTop: 'none' }}
>
{`${(isMuted) ? '✔' : '✘'} Mute`}
</div> </div>
{(channels[cid][1] !== 0) {(channels[cid][1] !== 0)
&& ( && (
@ -62,7 +80,6 @@ const UserContextMenu = ({
close(); close();
}} }}
tabIndex={0} tabIndex={0}
style={{ borderTop: 'thin solid' }}
> >
Close Close
</div> </div>
@ -83,11 +100,13 @@ function mapStateToProps(state: State) {
const { const {
cid, cid,
} = args; } = args;
const { mute: muteArr } = state.chatRead;
return { return {
xPos, xPos,
yPos, yPos,
cid, cid,
channels, channels,
muteArr,
}; };
} }
@ -99,6 +118,12 @@ function mapDispatchToProps(dispatch) {
leave(cid) { leave(cid) {
dispatch(setLeaveChannel(cid)); dispatch(setLeaveChannel(cid));
}, },
mute(cid) {
dispatch(muteChatChannel(cid));
},
unmute(cid) {
dispatch(unmuteChatChannel(cid));
},
}; };
} }

View File

@ -19,7 +19,9 @@ import {
const ChannelDropDown = ({ const ChannelDropDown = ({
channels, channels,
chatChannel, chatChannel,
chatRead, unread,
chatNotify,
mute,
setChannel, setChannel,
}) => { }) => {
const [show, setShow] = useState(false); const [show, setShow] = useState(false);
@ -27,7 +29,9 @@ const ChannelDropDown = ({
// 1: DMs // 1: DMs
const [type, setType] = useState(0); const [type, setType] = useState(0);
const [offset, setOffset] = useState(0); const [offset, setOffset] = useState(0);
const [unreadAny, setUnreadAny] = useState(false);
const [chatChannelName, setChatChannelName] = useState('...'); const [chatChannelName, setChatChannelName] = useState('...');
const [hasDm, setHasDm] = useState(false);
const wrapperRef = useRef(null); const wrapperRef = useRef(null);
const buttonRef = useRef(null); const buttonRef = useRef(null);
@ -44,6 +48,10 @@ const ChannelDropDown = ({
} }
}, []); }, []);
const handleWindowResize = useCallback(() => {
setShow(false);
}, []);
useLayoutEffect(() => { useLayoutEffect(() => {
if (show) { if (show) {
if (channels[chatChannel]) { if (channels[chatChannel]) {
@ -52,12 +60,45 @@ const ChannelDropDown = ({
} }
document.addEventListener('mousedown', handleClickOutside); document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('touchstart', handleClickOutside); document.addEventListener('touchstart', handleClickOutside);
window.addEventListener('resize', handleWindowResize);
} else { } else {
document.removeEventListener('mousedown', handleClickOutside); document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('touchstart', handleClickOutside); document.removeEventListener('touchstart', handleClickOutside);
window.removeEventListener('resize', handleWindowResize);
} }
}, [show]); }, [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(() => { useEffect(() => {
if (channels[chatChannel]) { if (channels[chatChannel]) {
setChatChannelName(channels[chatChannel][0]); setChatChannelName(channels[chatChannel][0]);
@ -70,12 +111,14 @@ const ChannelDropDown = ({
> >
<div <div
ref={buttonRef} ref={buttonRef}
style={{ role="button"
width: 50, tabIndex={-1}
}}
onClick={() => setShow(!show)} onClick={() => setShow(!show)}
className="channelbtn" className={`channelbtn${(show) ? ' selected' : ''}`}
> >
{(unreadAny && chatNotify && !show) && (
<div style={{ top: -4 }} className="chnunread">⦿</div>
)}
{chatChannelName} {chatChannelName}
</div> </div>
{(show) {(show)
@ -84,23 +127,41 @@ const ChannelDropDown = ({
ref={wrapperRef} ref={wrapperRef}
style={{ style={{
position: 'absolute', position: 'absolute',
bottom: offset + 5, bottom: offset,
right: 9, right: 9,
}} }}
className="channeldd" className="channeldd"
> >
<div> <div
className="chntop"
>
<span <span
style={{ borderLeft: 'none' }}
className={`chntype${(type === 0) ? ' selected' : ''}`}
onClick={() => setType(0)} onClick={() => setType(0)}
role="button"
tabIndex={-1}
> >
<MdChat /> <MdChat />
</span> </span>
| {(hasDm)
<span && (
onClick={() => setType(1)} <span
> className={
<FaUserFriends /> `chntype${
</span> (type === 1) ? ' selected' : ''
}`
}
onClick={() => setType(1)}
role="button"
tabIndex={-1}
>
{(unreadAny && chatNotify && type !== 1) && (
<div className="chnunread">⦿</div>
)}
<FaUserFriends />
</span>
)}
</div> </div>
<div <div
className="channeldds" className="channeldds"
@ -116,8 +177,7 @@ const ChannelDropDown = ({
} }
return false; return false;
}).map((cid) => { }).map((cid) => {
const [name,, lastTs] = channels[cid]; const [name] = channels[cid];
console.log(`name ${name} lastTC ${lastTs} compare to ${chatRead[cid]}`);
return ( return (
<div <div
onClick={() => setChannel(cid)} onClick={() => setChannel(cid)}
@ -126,10 +186,12 @@ const ChannelDropDown = ({
(cid === chatChannel) ? ' selected' : '' (cid === chatChannel) ? ' selected' : ''
}` }`
} }
role="button"
tabIndex={-1}
> >
{ {
(chatRead[cid] < lastTs) ? ( (unread[cid] && chatNotify && !mute.includes(cid)) ? (
<span className="chnunread"></span> <div className="chnunread">⦿</div>
) : null ) : null
} }
{name} {name}
@ -145,15 +207,19 @@ const ChannelDropDown = ({
}; };
function mapStateToProps(state: State) { function mapStateToProps(state: State) {
const { channels } = state.chat;
const { const {
chatChannel, chatChannel,
chatRead, unread,
} = state.gui; mute,
const { channels } = state.chat; } = state.chatRead;
const { chatNotify } = state.audio;
return { return {
channels, channels,
chatChannel, chatChannel,
chatRead, unread,
mute,
chatNotify,
}; };
} }

View File

@ -12,7 +12,6 @@ import { connect } from 'react-redux';
import type { State } from '../reducers'; import type { State } from '../reducers';
import ChatMessage from './ChatMessage'; import ChatMessage from './ChatMessage';
import ChannelDropDown from './ChannelDropDown'; import ChannelDropDown from './ChannelDropDown';
import { MAX_CHAT_MESSAGES } from '../core/constants';
import { import {
showUserAreaModal, showUserAreaModal,
@ -23,7 +22,6 @@ import {
showContextMenu, showContextMenu,
} from '../actions'; } from '../actions';
import ProtocolClient from '../socket/ProtocolClient'; import ProtocolClient from '../socket/ProtocolClient';
import { saveSelection, restoreSelection } from '../utils/storeSelection';
import splitChatMessage from '../core/chatMessageFilter'; import splitChatMessage from '../core/chatMessageFilter';
function escapeRegExp(string) { function escapeRegExp(string) {
@ -48,7 +46,6 @@ const Chat = ({
}) => { }) => {
const listRef = useRef(); const listRef = useRef();
const targetRef = useRef(); const targetRef = useRef();
const [selection, setSelection] = useState(null);
const [nameRegExp, setNameRegExp] = useState(null); const [nameRegExp, setNameRegExp] = useState(null);
const [blockedIds, setBlockedIds] = useState([]); const [blockedIds, setBlockedIds] = useState([]);
const [btnSize, setBtnSize] = useState(20); const [btnSize, setBtnSize] = useState(20);
@ -59,7 +56,7 @@ const Chat = ({
}); });
const channelMessages = messages[chatChannel] || []; const channelMessages = messages[chatChannel] || [];
if (!messages[chatChannel] && !fetching) { if (channels[chatChannel] && !messages[chatChannel] && !fetching) {
fetchMessages(chatChannel); fetchMessages(chatChannel);
} }
@ -67,16 +64,6 @@ const Chat = ({
stayScrolled(); stayScrolled();
}, [channelMessages.length]); }, [channelMessages.length]);
/*
* TODO this removes focus from chat box, fix this
*
useEffect(() => {
if (channelMessages.length === MAX_CHAT_MESSAGES) {
restoreSelection(selection);
}
}, [channelMessages]);
*/
useEffect(() => { useEffect(() => {
const regExp = (ownName) const regExp = (ownName)
? new RegExp(`(^|\\s)(@${escapeRegExp(ownName)})(\\s|$)`, 'g') ? new RegExp(`(^|\\s)(@${escapeRegExp(ownName)})(\\s|$)`, 'g')
@ -168,7 +155,6 @@ const Chat = ({
className="chatarea" className="chatarea"
ref={listRef} ref={listRef}
style={{ flexGrow: 1 }} style={{ flexGrow: 1 }}
onMouseUp={() => { setSelection(saveSelection); }}
role="presentation" role="presentation"
> >
{ {
@ -204,6 +190,7 @@ const Chat = ({
style={{ flexGrow: 1, minWidth: 40 }} style={{ flexGrow: 1, minWidth: 40 }}
value={inputMessage} value={inputMessage}
onChange={(e) => setInputMessage(e.target.value)} onChange={(e) => setInputMessage(e.target.value)}
autoComplete="off"
id="chatmsginput" id="chatmsginput"
maxLength="200" maxLength="200"
type="text" type="text"
@ -235,7 +222,7 @@ const Chat = ({
function mapStateToProps(state: State) { function mapStateToProps(state: State) {
const { name } = state.user; const { name } = state.user;
const { chatChannel } = state.gui; const { chatChannel } = state.chatRead;
const { const {
channels, channels,
messages, messages,

View File

@ -3,24 +3,77 @@
* @flow * @flow
*/ */
import React from 'react'; import React, {
useState, useEffect,
} from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { MdForum } from 'react-icons/md'; import { MdForum } from 'react-icons/md';
import { showChatModal } from '../actions'; import { showChatModal } from '../actions';
const ChatButton = ({ open }) => ( const ChatButton = ({
<div chatOpen,
id="chatbutton" modalOpen,
className="actionbuttons" chatNotify,
onClick={open} channels,
role="button" unread,
tabIndex={0} mute,
> open,
<MdForum /> }) => {
</div>: null 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 (
<div
id="chatbutton"
className="actionbuttons"
onClick={open}
role="button"
tabIndex={0}
>
{(unreadAny) && (
<div
style={{
position: 'fixed',
bottom: 27,
right: 62,
top: 'unset',
}}
className="chnunread"
>⦿</div>
)}
<MdForum />
</div>: null
);
};
function mapDispatchToProps(dispatch) { function mapDispatchToProps(dispatch) {
return { 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);

View File

@ -11,7 +11,13 @@ import HelpButton from './HelpButton';
import SettingsButton from './SettingsButton'; import SettingsButton from './SettingsButton';
import LogInButton from './LogInButton'; import LogInButton from './LogInButton';
import DownloadButton from './DownloadButton'; 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({ function Menu({
menuOpen, menuOpen,
@ -37,7 +43,6 @@ function Menu({
<SettingsButton /> <SettingsButton />
<LogInButton /> <LogInButton />
<DownloadButton /> <DownloadButton />
<MinecraftButton />
<HelpButton /> <HelpButton />
</div> </div>
) )

View File

@ -21,7 +21,6 @@ import RegisterModal from './RegisterModal';
import CanvasSelectModal from './CanvasSelectModal'; import CanvasSelectModal from './CanvasSelectModal';
import ChatModal from './ChatModal'; import ChatModal from './ChatModal';
import ForgotPasswordModal from './ForgotPasswordModal'; import ForgotPasswordModal from './ForgotPasswordModal';
import MinecraftModal from './MinecraftModal';
const MODAL_COMPONENTS = { const MODAL_COMPONENTS = {
@ -32,7 +31,6 @@ const MODAL_COMPONENTS = {
REGISTER: RegisterModal, REGISTER: RegisterModal,
FORGOT_PASSWORD: ForgotPasswordModal, FORGOT_PASSWORD: ForgotPasswordModal,
CHAT: ChatModal, CHAT: ChatModal,
MINECRAFT: MinecraftModal,
CANVAS_SELECTION: CanvasSelectModal, CANVAS_SELECTION: CanvasSelectModal,
/* other modals */ /* other modals */
}; };

View File

@ -135,7 +135,7 @@ function SettingsModal({
<SettingsItem <SettingsItem
title="Disable Game Sounds" title="Disable Game Sounds"
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
description="All sound effects except Chat Notification will be disabled." description="All sound effects will be disabled."
keyBind="M" keyBind="M"
value={isMuted} value={isMuted}
onToggle={onMute} onToggle={onMute}

View File

@ -70,7 +70,7 @@ const SocialSettings = ({
} }
}} }}
> >
{bl[1]} {`${bl[1]}`}
</div> </div>
)) ))
} }

View File

@ -13,6 +13,7 @@ import {
addToChatInputMessage, addToChatInputMessage,
startDm, startDm,
setUserBlock, setUserBlock,
setChatChannel,
} from '../actions'; } from '../actions';
import type { State } from '../reducers'; import type { State } from '../reducers';
@ -24,6 +25,9 @@ const UserContextMenu = ({
addToInput, addToInput,
dm, dm,
block, block,
channels,
fetching,
setChannel,
close, close,
}) => { }) => {
const wrapperRef = useRef(null); const wrapperRef = useRef(null);
@ -56,13 +60,13 @@ const UserContextMenu = ({
}} }}
> >
<div <div
style={{ borderBottom: 'thin solid' }}
onClick={() => { onClick={() => {
block(uid, name); block(uid, name);
close(); close();
}} }}
role="button" role="button"
tabIndex={-1} tabIndex={-1}
style={{ borderTop: 'none' }}
> >
Block Block
</div> </div>
@ -70,11 +74,24 @@ const UserContextMenu = ({
role="button" role="button"
tabIndex={0} tabIndex={0}
onClick={() => { 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(); close();
}} }}
style={{ borderBottom: 'thin solid' }}
> >
DM DM
</div> </div>
@ -98,15 +115,23 @@ function mapStateToProps(state: State) {
yPos, yPos,
args, args,
} = state.contextMenu; } = state.contextMenu;
const {
channels,
} = state.chat;
const { const {
name, name,
uid, uid,
} = args; } = args;
const {
fetchingApi: fetching,
} = state.fetching;
return { return {
xPos, xPos,
yPos, yPos,
channels,
name, name,
uid, uid,
fetching,
}; };
} }
@ -129,6 +154,9 @@ function mapDispatchToProps(dispatch) {
close() { close() {
dispatch(hideContextMenu()); dispatch(hideContextMenu());
}, },
setChannel(channelId) {
dispatch(setChatChannel(channelId));
},
}; };
} }

View File

@ -211,10 +211,7 @@ class PixelPlainterControls {
this.store, this.store,
this.viewport, this.viewport,
this.renderer, this.renderer,
[ this.clickTapStartCoords,
this.clickTapStartCoords[0],
this.clickTapStartCoords[1],
],
); );
}, 800); }, 800);
} }

View File

@ -105,28 +105,22 @@ export class ChatProvider {
userId, userId,
channelId, channelId,
channelArray, channelArray,
notify = true,
) { ) {
/* const [, created] = await UserChannel.findOrCreate({
* since UserId and ChannelId are primary keys, where: {
* this will throw if already exists UserId: userId,
*/ ChannelId: channelId,
const relation = await UserChannel.create({ },
UserId: userId,
ChannelId: channelId,
}, {
raw: true, raw: true,
}); });
console.log('HEREEEEE HHEEERRREEE'); if (created) {
console.log(relation); webSockets.broadcastAddChatChannel(
userId,
webSockets.broadcastAddChatChannel( channelId,
userId, channelArray,
channelId, );
channelArray, }
notify,
);
} }
userHasChannelAccess(user, cid, write = false) { userHasChannelAccess(user, cid, write = false) {
@ -134,7 +128,7 @@ export class ChatProvider {
if (!write || user.regUser) { if (!write || user.regUser) {
return true; return true;
} }
} else if (user.regUser && user.channelIds.includes(cid)) { } else if (user.regUser && user.channels[cid]) {
return true; return true;
} }
return false; return false;
@ -146,7 +140,7 @@ export class ChatProvider {
} }
const channelArray = user.channels[cid]; const channelArray = user.channels[cid];
if (channelArray && channelArray.length === 4) { if (channelArray && channelArray.length === 4) {
return user.channels[cid][4]; return user.channels[cid][3];
} }
return null; return null;
} }

View File

@ -87,6 +87,10 @@ export const CHAT_CHANNELS = [
name: 'en', name: 'en',
}, { }, {
name: 'int', name: 'int',
}, {
name: 'pol',
}, {
name: 'art',
}, },
]; ];

View File

@ -30,11 +30,13 @@ const Message = Model.define('Message', {
Message.belongsTo(Channel, { Message.belongsTo(Channel, {
as: 'channel', as: 'channel',
foreignKey: 'cid', foreignKey: 'cid',
onDelete: 'cascade',
}); });
Message.belongsTo(RegUser, { Message.belongsTo(RegUser, {
as: 'user', as: 'user',
foreignKey: 'uid', foreignKey: 'uid',
onDelete: 'cascade',
}); });
export default Message; export default Message;

View File

@ -64,10 +64,11 @@ const RegUser = Model.define('User', {
defaultValue: false, defaultValue: false,
}, },
blockDm: { // currently just blockDm
type: DataType.BOOLEAN, blocks: {
type: DataType.TINYINT,
allowNull: false, allowNull: false,
defaultValue: false, defaultValue: 0,
}, },
discordid: { discordid: {
@ -120,6 +121,10 @@ const RegUser = Model.define('User', {
mcVerified(): boolean { mcVerified(): boolean {
return this.verified & 0x02; return this.verified & 0x02;
}, },
blockDm(): boolean {
return this.blocks & 0x01;
},
}, },
setterMethods: { setterMethods: {
@ -133,6 +138,11 @@ const RegUser = Model.define('User', {
this.setDataValue('verified', val); this.setDataValue('verified', val);
}, },
blockDm(num: boolean) {
const val = (num) ? (this.blocks | 0x01) : (this.blocks & ~0x01);
this.setDataValue('blocks', val);
},
password(value: string) { password(value: string) {
if (value) this.setDataValue('password', generateHash(value)); if (value) this.setDataValue('password', generateHash(value));
}, },

View File

@ -29,7 +29,6 @@ class User {
this.id = id; this.id = id;
this.ip = ip; this.ip = ip;
this.channels = {}; this.channels = {};
this.channelIds = [];
this.blocked = []; this.blocked = [];
this.ipSub = getIPv6Subnet(ip); this.ipSub = getIPv6Subnet(ip);
this.wait = null; this.wait = null;
@ -64,7 +63,6 @@ class User {
dmu1, dmu1,
dmu2, dmu2,
} = reguser.channel[i]; } = reguser.channel[i];
this.channelIds.push(id);
if (type === 1) { if (type === 1) {
/* in DMs: /* in DMs:
* the name is the name of the other user * 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 name = (dmu1.id === this.id) ? dmu2.name : dmu1.name;
const dmu = (dmu1.id === this.id) ? dmu2.id : dmu1.id; const dmu = (dmu1.id === this.id) ? dmu2.id : dmu1.id;
this.channels[id] = [ this.addChannel(id, [
name, name,
type, type,
lastTs, lastTs,
dmu, dmu,
]; ]);
} else { } else {
const { name } = reguser.channel[i]; const { name } = reguser.channel[i];
this.channels[id] = [ this.addChannel(id, [
name, name,
type, type,
lastTs, lastTs,
]; ]);
} }
} }
} }
@ -99,6 +97,14 @@ class User {
} }
} }
addChannel(cid, channelArray) {
this.channels[cid] = channelArray;
}
removeChannel(cid) {
delete this.channels[cid];
}
getName() { getName() {
return (this.regUser) ? this.regUser.name : null; return (this.regUser) ? this.regUser.name : null;
} }

View File

@ -54,7 +54,7 @@ export default function chat(
const channels = { ...state.channels }; const channels = { ...state.channels };
const messages = { ...state.messages }; const messages = { ...state.messages };
const keys = Object.keys(channels); 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]; const cid = keys[i];
if (channels[cid][1] === 0) { if (channels[cid][1] === 0) {
delete messages[cid]; delete messages[cid];
@ -81,7 +81,7 @@ export default function chat(
*/ */
const channels = { ...state.channels }; const channels = { ...state.channels };
const chanKeys = Object.keys(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]; const cid = chanKeys[i];
if (channels[cid][1] === 1 && channels[cid][3] === userId) { if (channels[cid][1] === 1 && channels[cid][3] === userId) {
delete channels[cid]; delete channels[cid];
@ -109,7 +109,10 @@ export default function chat(
case 'ADD_CHAT_CHANNEL': { case 'ADD_CHAT_CHANNEL': {
const { channel } = action; const { channel } = action;
console.log('adding channel', channel); const [cid] = Object.keys(channel);
if (state.channels[cid]) {
return state;
}
return { return {
...state, ...state,
channels: { channels: {
@ -121,11 +124,17 @@ export default function chat(
case 'REMOVE_CHAT_CHANNEL': { case 'REMOVE_CHAT_CHANNEL': {
const { cid } = action; const { cid } = action;
if (!state.channels[cid]) {
return state;
}
const channels = { ...state.channels }; const channels = { ...state.channels };
const messages = { ...state.messages };
delete messages[cid];
delete channels[cid]; delete channels[cid];
return { return {
...state, ...state,
channels, channels,
messages,
}; };
} }

153
src/reducers/chatRead.js Normal file
View File

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

View File

@ -15,10 +15,6 @@ export type GUIState = {
compactPalette: boolean, compactPalette: boolean,
paletteOpen: boolean, paletteOpen: boolean,
menuOpen: boolean, menuOpen: boolean,
chatChannel: number,
// timestamps of last read post per channel
// { 1: Date.now() }
chatRead: {},
style: string, style: string,
}; };
@ -33,8 +29,6 @@ const initialState: GUIState = {
compactPalette: false, compactPalette: false,
paletteOpen: true, paletteOpen: true,
menuOpen: false, menuOpen: false,
chatChannel: 1,
chatRead: {},
style: 'default', 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': { case 'SELECT_COLOR': {
const { const {
compactPalette, 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': { case 'PLACE_PIXEL': {
let { pixelsPlaced } = state; let { pixelsPlaced } = state;
pixelsPlaced += 1; pixelsPlaced += 1;

View File

@ -9,6 +9,7 @@ import modal from './modal';
import user from './user'; import user from './user';
import chat from './chat'; import chat from './chat';
import contextMenu from './contextMenu'; import contextMenu from './contextMenu';
import chatRead from './chatRead';
import fetching from './fetching'; import fetching from './fetching';
import type { AudioState } from './audio'; import type { AudioState } from './audio';
@ -28,6 +29,7 @@ export type State = {
user: UserState, user: UserState,
chat: ChatState, chat: ChatState,
contextMenu: ContextMenuState, contextMenu: ContextMenuState,
chatRead: ChatReadState,
fetching: FetchingState, fetching: FetchingState,
}; };
@ -52,5 +54,6 @@ export default persistCombineReducers(config, {
user, user,
chat, chat,
contextMenu, contextMenu,
chatRead,
fetching, fetching,
}); });

View File

@ -108,8 +108,8 @@ async function block(req: Request, res: Response) {
if (channel) { if (channel) {
const channelId = channel.id; const channelId = channel.id;
channel.destroy(); channel.destroy();
webSockets.broadcastRemoveChatChannel(user.id, channelId, false); webSockets.broadcastRemoveChatChannel(user.id, channelId);
webSockets.broadcastRemoveChatChannel(userId, channelId, true); webSockets.broadcastRemoveChatChannel(userId, channelId);
} }
if (ret) { if (ret) {

View File

@ -45,10 +45,10 @@ async function blockdm(req: Request, res: Response) {
const channel = channels[i]; const channel = channels[i];
if (channel.type === 1) { if (channel.type === 1) {
const channelId = channel.id; const channelId = channel.id;
channel.destroy();
const { dmu1id, dmu2id } = channel; const { dmu1id, dmu2id } = channel;
webSockets.broadcastRemoveChatChannel(dmu1id, channelId, true); channel.destroy();
webSockets.broadcastRemoveChatChannel(dmu2id, channelId, true); webSockets.broadcastRemoveChatChannel(dmu1id, channelId);
webSockets.broadcastRemoveChatChannel(dmu2id, channelId);
} }
} }

View File

@ -65,7 +65,7 @@ async function leaveChan(req: Request, res: Response) {
user.regUser.removeChannel(channel); user.regUser.removeChannel(channel);
webSockets.broadcastRemoveChatChannel(user.id, channelId, false); webSockets.broadcastRemoveChatChannel(user.id, channelId);
res.json({ res.json({
status: 'ok', status: 'ok',

View File

@ -47,12 +47,6 @@ async function startDm(req: Request, res: Response) {
const targetUser = await RegUser.findOne({ const targetUser = await RegUser.findOne({
where: query, where: query,
attributes: [
'id',
'name',
'blockDm',
],
raw: true,
}); });
if (!targetUser) { if (!targetUser) {
res.status(401); res.status(401);
@ -61,14 +55,15 @@ async function startDm(req: Request, res: Response) {
}); });
return; return;
} }
userId = targetUser.id;
userName = targetUser.name;
if (targetUser.blockDm) { if (targetUser.blockDm) {
res.status(401); res.status(401);
res.json({ 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 * check if blocked
@ -76,7 +71,7 @@ async function startDm(req: Request, res: Response) {
if (await isUserBlockedBy(user.id, userId)) { if (await isUserBlockedBy(user.id, userId)) {
res.status(401); res.status(401);
res.json({ res.json({
errors: ['You are blocked by this user'], errors: [`${userName} has blocked you.`],
}); });
return; return;
} }
@ -106,10 +101,19 @@ async function startDm(req: Request, res: Response) {
raw: true, raw: true,
}); });
const ChannelId = channel[0].id; const ChannelId = channel[0].id;
const curTime = Date.now();
const promises = [ const promises = [
ChatProvider.addUserToChannel(user.id, ChannelId, false), ChatProvider.addUserToChannel(
ChatProvider.addUserToChannel(userId, ChannelId, true), user.id,
ChannelId,
[userName, 1, curTime, userId],
),
ChatProvider.addUserToChannel(
userId,
ChannelId,
[user.getName(), 1, curTime, user.id],
),
]; ];
await Promise.all(promises); await Promise.all(promises);

View File

@ -104,7 +104,7 @@ class SocketServer extends WebSocketEvents {
}); });
ws.on('message', (message) => { ws.on('message', (message) => {
if (typeof message === 'string') { if (typeof message === 'string') {
SocketServer.onTextMessage(message, ws); this.onTextMessage(message, ws);
} else { } else {
this.onBinaryMessage(message, ws); 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) { findWsByUserId(userId) {
const { clients } = this.wss; const it = this.wss.clients.keys();
for (let i = 0; i < clients.length; i += 1) { let client = it.next();
const ws = clients[i]; while (!client.done) {
const ws = client.value;
if (ws.user.id === userId && ws.readyState === WebSocket.OPEN) { if (ws.user.id === userId && ws.readyState === WebSocket.OPEN) {
return ws; return ws;
} }
client = it.next();
} }
return null; return null;
} }
@ -190,35 +196,31 @@ class SocketServer extends WebSocketEvents {
userId: number, userId: number,
channelId: number, channelId: number,
channelArray: Array, channelArray: Array,
notify: boolean,
) { ) {
const ws = this.findWsByUserId(userId); this.wss.clients.forEach((ws) => {
if (ws) { if (ws.user.id === userId && ws.readyState === WebSocket.OPEN) {
ws.user.channels[channelId] = channelArray; ws.user.addChannel(channelId, channelArray);
const text = JSON.stringify([ const text = JSON.stringify([
'addch', { 'addch', {
[channelId]: channelArray, [channelId]: channelArray,
}, },
]); ]);
if (notify) {
ws.send(text); ws.send(text);
} }
} });
} }
broadcastRemoveChatChannel( broadcastRemoveChatChannel(
userId: number, userId: number,
channelId: number, channelId: number,
notify: boolean,
) { ) {
const ws = this.findWsByUserId(userId); this.wss.clients.forEach((ws) => {
if (ws) { if (ws.user.id === userId && ws.readyState === WebSocket.OPEN) {
delete ws.user.channels[channelId]; ws.user.removeChannel(channelId);
const text = JSON.stringify('remch', channelId); const text = JSON.stringify(['remch', channelId]);
if (notify) {
ws.send(text); ws.send(text);
} }
} });
} }
broadcastPixelBuffer(canvasId: number, chunkid, data: Buffer) { broadcastPixelBuffer(canvasId: number, chunkid, data: Buffer) {
@ -286,7 +288,7 @@ class SocketServer extends WebSocketEvents {
webSockets.broadcastOnlineCounter(online); webSockets.broadcastOnlineCounter(online);
} }
static async onTextMessage(text, ws) { async onTextMessage(text, ws) {
/* /*
* all client -> server text messages are * all client -> server text messages are
* chat messages in [message, channelId] format * chat messages in [message, channelId] format
@ -333,13 +335,11 @@ class SocketServer extends WebSocketEvents {
*/ */
const dmUserId = chatProvider.checkIfDm(user, channelId); const dmUserId = chatProvider.checkIfDm(user, channelId);
if (dmUserId) { if (dmUserId) {
console.log('is dm');
const dmWs = this.findWsByUserId(dmUserId); const dmWs = this.findWsByUserId(dmUserId);
if (!dmWs if (!dmWs
|| !chatProvider.userHasChannelAccess(dmWs.user, channelId) || !chatProvider.userHasChannelAccess(dmWs.user, channelId)
) { ) {
console.log('adding channel') await ChatProvider.addUserToChannel(
ChatProvider.addUserToChannel(
dmUserId, dmUserId,
channelId, channelId,
[ws.name, 1, Date.now(), user.id], [ws.name, 1, Date.now(), user.id],

View File

@ -28,14 +28,12 @@ class WebSocketEvents {
userId: number, userId: number,
channelId: number, channelId: number,
channelArray: Array, channelArray: Array,
notify: boolean,
) { ) {
} }
broadcastRemoveChatChannel( broadcastRemoveChatChannel(
userId: number, userId: number,
channelId: number, channelId: number,
notify: boolean,
) { ) {
} }

View File

@ -94,20 +94,17 @@ class WebSockets {
* @param userId numerical id of user * @param userId numerical id of user
* @param channelId numerical id of chat channel * @param channelId numerical id of chat channel
* @param channelArray array with channel info [name, type, lastTs] * @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( broadcastAddChatChannel(
userId: number, userId: number,
channelId: number, channelId: number,
channelArray: Array, channelArray: Array,
notify: boolean = true,
) { ) {
this.listeners.forEach( this.listeners.forEach(
(listener) => listener.broadcastAddChatChannel( (listener) => listener.broadcastAddChatChannel(
userId, userId,
channelId,
channelArray, channelArray,
notify,
), ),
); );
} }
@ -116,19 +113,16 @@ class WebSockets {
* broadcast Removing chat channel from user * broadcast Removing chat channel from user
* @param userId numerical id of user * @param userId numerical id of user
* @param channelId numerical id of chat channel * @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) * (i.e. false if the user already gets it via api response)
*/ */
broadcastRemoveChatChannel( broadcastRemoveChatChannel(
userId: number, userId: number,
channelId: number, channelId: number,
notify: boolean = true,
) { ) {
this.listeners.forEach( this.listeners.forEach(
(listener) => listener.broadcastRemoveChatChannel( (listener) => listener.broadcastRemoveChatChannel(
userId, userId,
channelId, channelId,
notify,
), ),
); );
} }

View File

@ -200,21 +200,31 @@ export default (store) => (next) => (action) => {
} }
case 'RECEIVE_CHAT_MESSAGE': { case 'RECEIVE_CHAT_MESSAGE': {
if (!chatNotify) break; if (mute || !chatNotify) break;
const { isPing } = action; const { isPing, channel } = action;
const { chatChannel } = state.gui; const { mute: muteCh, chatChannel } = state.chatRead;
// eslint-disable-next-line eqeqeq if (muteCh.includes(channel)) break;
if (!isPing && action.channel != chatChannel) { if (muteCh.includes(`${channel}`)) break;
break; const { channels } = state.chat;
}
const oscillatorNode = context.createOscillator(); const oscillatorNode = context.createOscillator();
const gainNode = context.createGain(); const gainNode = context.createGain();
oscillatorNode.type = 'sine'; oscillatorNode.type = 'sine';
oscillatorNode.frequency.setValueAtTime(310, context.currentTime); 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( oscillatorNode.frequency.exponentialRampToValueAtTime(
freq, freq,
context.currentTime + 0.025, context.currentTime + 0.025,

View File

@ -19,7 +19,7 @@ export default (store) => (next) => (action) => {
} }
case 'SET_NAME': case 'SET_NAME':
case 'LOGIN:': case 'LOGIN':
case 'LOGOUT': { case 'LOGOUT': {
ProtocolClient.reconnect(); ProtocolClient.reconnect();
break; break;

View File

@ -22,10 +22,14 @@ tr:nth-child(odd) {
color: #ff91a6; color: #ff91a6;
} }
.actionbuttons:hover, .menu > div:hover { .actionbuttons:hover, .menu > div:hover, .channeldd, .contextmenu {
background: linear-gradient(160deg, #61dcea , #ffb1e1, #ecffec, #ffb1e1, #61dcea); background: linear-gradient(160deg, #61dcea , #ffb1e1, #ecffec, #ffb1e1, #61dcea);
} }
.chn, .chntype, .contextmenu > div {
background-color: #ebebeb80;
}
#chatbutton { #chatbutton {
background: linear-gradient(135deg, orange , yellow, green, aqua, blue, violet); background: linear-gradient(135deg, orange , yellow, green, aqua, blue, violet);
} }

View File

@ -35,6 +35,25 @@ tr:nth-child(even) {
border-radius: 8px; 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 { .actionbuttons, .coorbox, .onlinebox, .cooldownbox, #historyselect {
background-color: rgba(59, 59, 59, 0.8); background-color: rgba(59, 59, 59, 0.8);
color: #f4f4f4; color: #f4f4f4;

View File

@ -65,6 +65,20 @@ tr:nth-child(even) {
background-color: hsla(216, 4%, 74%, .3); 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 { .modalinfo {
color: #ddd; color: #ddd;
} }

View File

@ -134,15 +134,20 @@ tr:nth-child(even) {
background-color: rgba(226, 226, 226); background-color: rgba(226, 226, 226);
border: solid black; border: solid black;
border-width: thin; border-width: thin;
color: #212121;
box-shadow: 0 0 2px 2px rgba(0,0,0,.2);
} }
.contextmenu > div { .contextmenu > div {
border-width: thin;
margin: 2px; margin: 2px;
height: 18px;
padding: 3px 2px 0px 0px;
background-color: #ebebeb;
border-top: thin solid #b1b1b2;
} }
.contextmenu > div:hover { .contextmenu > div:hover {
background-color: white; background-color: #c9c9c9;
cursor: pointer; cursor: pointer;
} }
@ -155,29 +160,42 @@ tr:nth-child(even) {
} }
.channelbtn { .channelbtn {
background-color: #d0d0d0; position: relative;
background-color: #ebebeb;
text-align: center; text-align: center;
border-style: solid; border-style: solid;
border-width: thin; border-width: thin;
border-radius: 4px; border-radius: 4px;
} width: 50px;
height: 100%;
.channelbtn:hover { white-space: nowrap;
cursor: pointer; font-size: 14px;
background-color: white; overflow-x: hidden;
color: #212121;
} }
.channeldd { .channeldd {
background-color: rgba(226, 226, 226); background-color: rgba(226, 226, 226);
color: #212121;
border: solid black;
border-width: thin;
width: 90px; width: 90px;
box-shadow: 0 0 2px 2px rgba(0,0,0,.2);
} }
.channeldds { .channeldds {
height: 120px; height: 120px;
overflow-y: scroll; overflow-y: auto;
overflow-x: hidden;
margin: 2px; margin: 2px;
} }
.chntop {
display: flex;
height: 24px;
border-bottom: solid thin;
}
.actionbuttons, .coorbox, .onlinebox, .cooldownbox, #palettebox { .actionbuttons, .coorbox, .onlinebox, .cooldownbox, #palettebox {
position: fixed; position: fixed;
background-color: rgba(226, 226, 226, 0.80); background-color: rgba(226, 226, 226, 0.80);
@ -224,10 +242,6 @@ tr:nth-child(even) {
} }
#helpbutton { #helpbutton {
left: 16px;
top: 221px;
}
#minecraftbutton {
left: 16px; left: 16px;
top: 180px; top: 180px;
} }
@ -485,15 +499,43 @@ tr:nth-child(even) {
cursor: pointer; cursor: pointer;
} }
.chn.selected, .chnunread { .chn {
font-weight: bold; position: relative;
font-size: 17px; 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 { .chnunread {
position: absolute;
top: -1px;
right: 1px;
font-weight: bold;
font-size: 12px;
color: red; 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 { .usermessages {
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;

View File

@ -1,7 +1,11 @@
.chatbox { .chatbox, .channeldd, .contextmenu {
border-radius: 8px; border-radius: 8px;
} }
.chntop {
margin-top: 4px;
}
.actionbuttons, .coorbox, .onlinebox, .cooldownbox, #historyselect { .actionbuttons, .coorbox, .onlinebox, .cooldownbox, #historyselect {
border-radius: 21px; border-radius: 21px;
} }