add basic window reducer and add chat to it

This commit is contained in:
HF 2021-04-27 01:54:43 +02:00
parent 3b0dcd9545
commit baca212686
18 changed files with 482 additions and 291 deletions

View File

@ -610,28 +610,21 @@ export function showContextMenu(
}; };
} }
export function setChatInputMessage(message: string): Action {
return {
type: 'SET_CHAT_INPUT_MSG',
message,
};
}
export function addToChatInputMessage(message: string): Action {
return {
type: 'ADD_CHAT_INPUT_MSG',
message,
};
}
export function showChatModal(forceModal: boolean = false): Action { export function showChatModal(forceModal: boolean = false): Action {
if (window.innerWidth > 604 && !forceModal) { return toggleChatBox(); } if (window.innerWidth > 604 && !forceModal) { return toggleChatBox(); }
return showModal('CHAT'); return showModal('CHAT');
} }
export function setChatChannel(cid: number): Action { export function openChatChannel(cid: number): Action {
return { return {
type: 'SET_CHAT_CHANNEL', type: 'OPEN_CHAT_CHANNEL',
cid,
};
}
export function closeChatChannel(cid: number): Action {
return {
type: 'CLOSE_CHAT_CHANNEL',
cid, cid,
}; };
} }
@ -687,10 +680,59 @@ export function unmuteChatChannel(cid: number): Action {
}; };
} }
export function setChatChannel(windowId: number, cid: number): Action {
return {
type: 'SET_CHAT_CHANNEL',
windowId,
cid,
};
}
export function setChatInputMessage(windowId: number, msg: string): Action {
return {
type: 'SET_CHAT_INPUT_MSG',
windowId,
msg,
};
}
export function addToChatInputMessage(windowId: number, msg: string): Action {
return {
type: 'ADD_CHAT_INPUT_MSG',
windowId,
msg,
};
}
export function moveWindow(windowId, xDiff, yDiff): Action {
return {
type: 'MOVE_WINDOW',
windowId,
xDiff,
yDiff,
};
}
export function openChatWindow(): Action {
return {
type: 'OPEN_WINDOW',
windowType: 'CHAT',
title: 'chat',
width: 700,
height: 300,
xPos: 100,
yPos: 100,
args: {
chatChannel: 1,
inputMessage: '',
},
};
}
/* /*
* query: Object with either userId: number or userName: string * query: Object with either userId: number or userName: string
*/ */
export function startDm(query): PromiseAction { export function startDm(windowId, query): PromiseAction {
return async (dispatch) => { return async (dispatch) => {
dispatch(setApiFetching(true)); dispatch(setApiFetching(true));
const res = await requestStartDm(query); const res = await requestStartDm(query);
@ -704,7 +746,7 @@ export function startDm(query): PromiseAction {
} else { } else {
const cid = Object.keys(res)[0]; const cid = Object.keys(res)[0];
dispatch(addChatChannel(res)); dispatch(addChatChannel(res));
dispatch(setChatChannel(cid)); dispatch(setChatChannel(windowId, cid));
} }
dispatch(setApiFetching(false)); dispatch(setApiFetching(false));
}; };

View File

@ -65,14 +65,17 @@ export type Action =
isPing: boolean, isPing: boolean,
} }
| { type: 'RECEIVE_CHAT_HISTORY', cid: number, history: Array } | { type: 'RECEIVE_CHAT_HISTORY', cid: number, history: Array }
| { type: 'SET_CHAT_CHANNEL', cid: number } | { type: 'OPEN_CHAT_CHANNEL', cid: number }
| { type: 'CLOSE_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: 'MUTE_CHAT_CHANNEL', cid: number }
| { type: 'UNMUTE_CHAT_CHANNEL', cid: number } | { type: 'UNMUTE_CHAT_CHANNEL', cid: number }
| { type: 'SET_CHAT_CHANNEL', windowId: number, cid: number }
| { type: 'SET_CHAT_INPUT_MSG', windowId: number, msg: string }
| { type: 'ADD_CHAT_INPUT_MSG', windowId: number, msg: string }
| { type: 'SET_CHAT_FETCHING', fetching: boolean } | { type: 'SET_CHAT_FETCHING', fetching: boolean }
| { type: 'SET_CHAT_INPUT_MSG', message: string } | { type: 'MOVE_WINDOW', windowId: number, xDiff: number, yDiff: number }
| { type: 'ADD_CHAT_INPUT_MSG', message: string }
| { type: 'BLOCK_USER', userId: number, userName: string } | { type: 'BLOCK_USER', userId: number, userName: string }
| { type: 'UNBLOCK_USER', userId: number, userName: string } | { type: 'UNBLOCK_USER', userId: number, userName: string }
| { type: 'SET_BLOCKING_DM', blockDm: boolean } | { type: 'SET_BLOCKING_DM', blockDm: boolean }

View File

@ -67,7 +67,7 @@ function init() {
name, name,
text, text,
country, country,
channelId, Number(channelId),
userId, userId,
isPing, isPing,
)); ));
@ -90,9 +90,8 @@ function init() {
// //
function checkMobile() { function checkMobile() {
store.dispatch(setMobile(true)); store.dispatch(setMobile(true));
document.removeEventListener('touchstart', checkMobile, false);
} }
document.addEventListener('touchstart', checkMobile, false); document.addEventListener('touchstart', checkMobile, { once: true });
store.dispatch(initTimer()); store.dispatch(initTimer());

View File

@ -20,6 +20,7 @@ import Menu from './Menu';
import UI from './UI'; import UI from './UI';
import ExpandMenuButton from './ExpandMenuButton'; import ExpandMenuButton from './ExpandMenuButton';
import ModalRoot from './ModalRoot'; import ModalRoot from './ModalRoot';
import WindowsRoot from './WindowsRoot';
const App = () => ( const App = () => (
<div> <div>
@ -35,6 +36,7 @@ const App = () => (
<ExpandMenuButton /> <ExpandMenuButton />
<UI /> <UI />
<ModalRoot /> <ModalRoot />
<WindowsRoot />
</IconContext.Provider> </IconContext.Provider>
</div> </div>
); );

View File

@ -17,7 +17,7 @@ import {
} from '../actions'; } from '../actions';
import type { State } from '../reducers'; import type { State } from '../reducers';
const UserContextMenu = ({ const ChannelContextMenu = ({
xPos, xPos,
yPos, yPos,
cid, cid,
@ -128,4 +128,4 @@ function mapDispatchToProps(dispatch) {
}; };
} }
export default connect(mapStateToProps, mapDispatchToProps)(UserContextMenu); export default connect(mapStateToProps, mapDispatchToProps)(ChannelContextMenu);

View File

@ -7,22 +7,15 @@
import React, { import React, {
useRef, useState, useEffect, useCallback, useLayoutEffect, useRef, useState, useEffect, useCallback, useLayoutEffect,
} from 'react'; } from 'react';
import { connect } from 'react-redux'; import { useSelector } from 'react-redux';
import { MdChat } from 'react-icons/md'; import { MdChat } from 'react-icons/md';
import { FaUserFriends } from 'react-icons/fa'; import { FaUserFriends } from 'react-icons/fa';
import type { State } from '../reducers'; import type { State } from '../reducers';
import {
setChatChannel,
} from '../actions';
const ChannelDropDown = ({ const ChannelDropDown = ({
channels, setChatChannel,
chatChannel, chatChannel,
unread,
chatNotify,
mute,
setChannel,
}) => { }) => {
const [show, setShow] = useState(false); const [show, setShow] = useState(false);
const [sortChans, setSortChans] = useState([]); const [sortChans, setSortChans] = useState([]);
@ -36,6 +29,12 @@ const ChannelDropDown = ({
const wrapperRef = useRef(null); const wrapperRef = useRef(null);
const buttonRef = useRef(null); const buttonRef = useRef(null);
const unread = useSelector((state) => state.chatRead.unread);
const mute = useSelector((state) => state.chatRead.mute);
const channels = useSelector((state) => state.chat.channels);
const chatNotify = useSelector((state) => state.audio.chatNotify);
useEffect(() => { useEffect(() => {
setOffset(buttonRef.current.clientHeight); setOffset(buttonRef.current.clientHeight);
}, [buttonRef]); }, [buttonRef]);
@ -200,7 +199,7 @@ const ChannelDropDown = ({
const [cid, unreadCh, name] = ch; const [cid, unreadCh, name] = ch;
return ( return (
<div <div
onClick={() => setChannel(cid)} onClick={() => setChatChannel(cid)}
className={ className={
`chn${ `chn${
(cid === chatChannel) ? ' selected' : '' (cid === chatChannel) ? ' selected' : ''
@ -226,29 +225,4 @@ const ChannelDropDown = ({
); );
}; };
function mapStateToProps(state: State) { export default React.memo(ChannelDropDown);
const { channels } = state.chat;
const {
chatChannel,
unread,
mute,
} = state.chatRead;
const { chatNotify } = state.audio;
return {
channels,
chatChannel,
unread,
mute,
chatNotify,
};
}
function mapDispatchToProps(dispatch) {
return {
setChannel(channelId) {
dispatch(setChatChannel(channelId));
},
};
}
export default connect(mapStateToProps, mapDispatchToProps)(ChannelDropDown);

View File

@ -4,10 +4,10 @@
*/ */
import React, { import React, {
useRef, useLayoutEffect, useState, useEffect, useRef, useLayoutEffect, useState, useEffect, useCallback,
} from 'react'; } from 'react';
import useStayScrolled from 'react-stay-scrolled'; import useStayScrolled from 'react-stay-scrolled';
import { connect } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import { t } from 'ttag'; import { t } from 'ttag';
import type { State } from '../reducers'; import type { State } from '../reducers';
@ -18,8 +18,8 @@ import {
showUserAreaModal, showUserAreaModal,
showChatModal, showChatModal,
setChatChannel, setChatChannel,
fetchChatMessages,
setChatInputMessage, setChatInputMessage,
fetchChatMessages,
showContextMenu, showContextMenu,
} from '../actions'; } from '../actions';
import ProtocolClient from '../socket/ProtocolClient'; import ProtocolClient from '../socket/ProtocolClient';
@ -30,27 +30,29 @@ function escapeRegExp(string) {
} }
const Chat = ({ const Chat = ({
windowId,
showExpand, showExpand,
channels,
messages,
chatChannel,
ownName,
open,
inputMessage,
setInputMessage,
setChannel,
fetchMessages,
fetching,
blocked,
triggerModal,
openChannelContextMenu,
}) => { }) => {
const listRef = useRef(); const listRef = useRef();
const targetRef = useRef(); const targetRef = useRef();
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);
const dispatch = useDispatch();
const setChannel = useCallback((cid) => dispatch(
setChatChannel(windowId, cid),
), [dispatch]);
const ownName = useSelector((state) => state.user.name);
const isDarkMode = useSelector((state) => state.gui.style.indexOf('dark') !== -1);
const fetching = useSelector((state) => state.fetching.fetchingChat);
const { channels, messages, blocked } = useSelector((state) => state.chat);
const { chatChannel, inputMessage } = useSelector((state) => state.windows.args[windowId]);
const { stayScrolled } = useStayScrolled(listRef, { const { stayScrolled } = useStayScrolled(listRef, {
initialScroll: Infinity, initialScroll: Infinity,
inaccuracy: 10, inaccuracy: 10,
@ -58,7 +60,7 @@ const Chat = ({
const channelMessages = messages[chatChannel] || []; const channelMessages = messages[chatChannel] || [];
if (channels[chatChannel] && !messages[chatChannel] && !fetching) { if (channels[chatChannel] && !messages[chatChannel] && !fetching) {
fetchMessages(chatChannel); dispatch(fetchChatMessages(chatChannel));
} }
useLayoutEffect(() => { useLayoutEffect(() => {
@ -94,6 +96,7 @@ const Chat = ({
// send message via websocket // send message via websocket
ProtocolClient.sendChatMessage(msg, chatChannel); ProtocolClient.sendChatMessage(msg, chatChannel);
setInputMessage(''); setInputMessage('');
dispatch(setChatInputMessage(windowId, ''));
} }
/* /*
@ -102,13 +105,13 @@ const Chat = ({
* set channel to first available one * set channel to first available one
*/ */
useEffect(() => { useEffect(() => {
if (!channels[chatChannel]) { if (!chatChannel || !channels[chatChannel]) {
const cids = Object.keys(channels); const cids = Object.keys(channels);
if (cids.length) { if (cids.length) {
setChannel(cids[0]); setChannel(cids[0]);
} }
} }
}, [chatChannel, channels]); }, [channels]);
return ( return (
<div <div
@ -133,11 +136,12 @@ const Chat = ({
clientX, clientX,
clientY, clientY,
} = event; } = event;
openChannelContextMenu( dispatch(showContextMenu(
'CHANNEL',
clientX, clientX,
clientY, clientY,
chatChannel, { cid: chatChannel },
); ));
}} }}
role="button" role="button"
title={t`Channel settings`} title={t`Channel settings`}
@ -147,7 +151,7 @@ const Chat = ({
{(showExpand) {(showExpand)
&& ( && (
<span <span
onClick={triggerModal} onClick={() => dispatch(showChatModal())}
role="button" role="button"
title={t`maximize`} title={t`maximize`}
tabIndex={-1} tabIndex={-1}
@ -168,6 +172,8 @@ const Chat = ({
msgArray={splitChatMessage(t`Start chatting here`, nameRegExp)} msgArray={splitChatMessage(t`Start chatting here`, nameRegExp)}
country="xx" country="xx"
uid={0} uid={0}
dark={isDarkMode}
windowId={windowId}
/> />
) )
} }
@ -179,6 +185,8 @@ const Chat = ({
msgArray={splitChatMessage(message[1], nameRegExp)} msgArray={splitChatMessage(message[1], nameRegExp)}
country={message[2]} country={message[2]}
uid={message[3]} uid={message[3]}
dark={isDarkMode}
windowId={windowId}
/> />
))) )))
} }
@ -192,9 +200,10 @@ const Chat = ({
<input <input
style={{ flexGrow: 1, minWidth: 40 }} style={{ flexGrow: 1, minWidth: 40 }}
value={inputMessage} value={inputMessage}
onChange={(e) => setInputMessage(e.target.value)} onChange={(e) => dispatch(
setChatInputMessage(windowId, e.target.value),
)}
autoComplete="off" autoComplete="off"
id="chatmsginput"
maxLength="200" maxLength="200"
type="text" type="text"
placeholder={t`Chat here`} placeholder={t`Chat here`}
@ -206,13 +215,16 @@ const Chat = ({
> >
</button> </button>
<ChannelDropDown /> <ChannelDropDown
setChatChannel={setChannel}
chatChannel={chatChannel}
/>
</form> </form>
</div> </div>
) : ( ) : (
<div <div
className="modallink" className="modallink"
onClick={open} onClick={() => dispatch(showUserAreaModal())}
style={{ textAlign: 'center', fontSize: 13 }} style={{ textAlign: 'center', fontSize: 13 }}
role="button" role="button"
tabIndex={0} tabIndex={0}
@ -224,52 +236,4 @@ const Chat = ({
); );
}; };
function mapStateToProps(state: State) { export default Chat;
const { name } = state.user;
const { chatChannel } = state.chatRead;
const {
channels,
messages,
inputMessage,
blocked,
} = state.chat;
const {
fetchingChat: fetching,
} = state.fetching;
return {
channels,
messages,
fetching,
blocked,
inputMessage,
chatChannel,
ownName: name,
};
}
function mapDispatchToProps(dispatch) {
return {
open() {
dispatch(showUserAreaModal());
},
triggerModal() {
dispatch(showChatModal(true));
},
setChannel(channelId) {
dispatch(setChatChannel(channelId));
},
fetchMessages(channelId) {
dispatch(fetchChatMessages(channelId));
},
setInputMessage(message) {
dispatch(setChatInputMessage(message));
},
openChannelContextMenu(xPos, yPos, cid) {
dispatch(showContextMenu('CHANNEL', xPos, yPos, {
cid,
}));
},
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Chat);

View File

@ -10,7 +10,7 @@ import { connect } from 'react-redux';
import { MdForum } from 'react-icons/md'; import { MdForum } from 'react-icons/md';
import { t } from 'ttag'; import { t } from 'ttag';
import { showChatModal } from '../actions'; import { showChatModal, openChatWindow } from '../actions';
const ChatButton = ({ const ChatButton = ({
@ -80,7 +80,8 @@ const ChatButton = ({
function mapDispatchToProps(dispatch) { function mapDispatchToProps(dispatch) {
return { return {
open() { open() {
dispatch(showChatModal(false)); // dispatch(showChatModal(false));
dispatch(openChatWindow());
}, },
}; };
} }

View File

@ -3,7 +3,7 @@
* @flow * @flow
*/ */
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { useDispatch } from 'react-redux';
import { showContextMenu } from '../actions'; import { showContextMenu } from '../actions';
import { colorFromText, setBrightness } from '../core/utils'; import { colorFromText, setBrightness } from '../core/utils';
@ -13,14 +13,16 @@ function ChatMessage({
name, name,
uid, uid,
country, country,
dark,
windowId,
msgArray, msgArray,
openUserContextMenu,
darkMode,
}) { }) {
if (!name || !msgArray) { if (!name || !msgArray) {
return null; return null;
} }
const dispatch = useDispatch();
const isInfo = (name === 'info'); const isInfo = (name === 'info');
const isEvent = (name === 'event'); const isEvent = (name === 'event');
let className = 'msg'; let className = 'msg';
@ -51,7 +53,7 @@ function ChatMessage({
<span <span
className="chatname" className="chatname"
style={{ style={{
color: setBrightness(colorFromText(name), darkMode), color: setBrightness(colorFromText(name), dark),
cursor: 'pointer', cursor: 'pointer',
}} }}
role="button" role="button"
@ -61,12 +63,11 @@ function ChatMessage({
clientX, clientX,
clientY, clientY,
} = event; } = event;
openUserContextMenu( dispatch(showContextMenu('USER', clientX, clientY, {
clientX, windowId,
clientY,
uid, uid,
name, name,
); }));
}} }}
> >
{name} {name}
@ -95,7 +96,7 @@ function ChatMessage({
<span <span
className="ping" className="ping"
style={{ style={{
color: setBrightness(colorFromText(txt.substr(1)), darkMode), color: setBrightness(colorFromText(txt.substr(1)), dark),
}} }}
>{txt}</span> >{txt}</span>
); );
@ -104,7 +105,7 @@ function ChatMessage({
<span <span
className="mention" className="mention"
style={{ style={{
color: setBrightness(colorFromText(txt.substr(1)), darkMode), color: setBrightness(colorFromText(txt.substr(1)), dark),
}} }}
>{txt}</span> >{txt}</span>
); );
@ -116,21 +117,4 @@ function ChatMessage({
); );
} }
function mapStateToProps(state: State) { export default React.memo(ChatMessage);
const { style } = state.gui;
const darkMode = style.indexOf('dark') !== -1;
return { darkMode };
}
function mapDispatchToProps(dispatch) {
return {
openUserContextMenu(xPos, yPos, uid, name) {
dispatch(showContextMenu('USER', xPos, yPos, {
uid,
name,
}));
},
};
}
export default connect(mapStateToProps, mapDispatchToProps)(ChatMessage);

View File

@ -32,26 +32,24 @@ const UI = ({
menuOpen, menuOpen,
menuType, menuType,
}) => { }) => {
const contextMenu = (menuOpen && menuType) ? CONTEXT_MENUS[menuType] : null;
if (isHistoricalView) { if (isHistoricalView) {
return ( return [
<div> <HistorySelect />,
<HistorySelect /> contextMenu,
{(menuOpen && menuType) ? CONTEXT_MENUS[menuType] : null} ];
</div>
);
} }
return ( return [
<div> <Alert />,
<Alert /> <PalselButton />,
<PalselButton /> <Palette />,
<Palette /> (!is3D) && <GlobeButton />,
{(!is3D) && <GlobeButton />} (is3D && isOnMobile) && <Mobile3DControls />,
{(is3D && isOnMobile) && <Mobile3DControls />} <CoolDownBox />,
<CoolDownBox /> <NotifyBox />,
<NotifyBox /> contextMenu,
{(menuOpen && menuType) && CONTEXT_MENUS[menuType]} ];
</div>
);
}; };
function mapStateToProps(state: State) { function mapStateToProps(state: State) {

View File

@ -6,7 +6,7 @@
import React, { import React, {
useRef, useEffect, useRef, useEffect,
} from 'react'; } from 'react';
import { connect } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import { t } from 'ttag'; import { t } from 'ttag';
import { import {
@ -16,23 +16,20 @@ import {
setUserBlock, setUserBlock,
setChatChannel, setChatChannel,
} from '../actions'; } from '../actions';
import type { State } from '../reducers';
const UserContextMenu = ({ const UserContextMenu = ({
xPos,
yPos,
uid,
name,
addToInput,
dm,
block,
channels,
fetching,
setChannel, setChannel,
close,
}) => { }) => {
const wrapperRef = useRef(null); const wrapperRef = useRef(null);
const { xPos, yPos, args } = useSelector((state) => state.contextMenu);
const { windowId, name, uid } = args;
const channels = useSelector((state) => state.chat.channels);
const fetching = useSelector((state) => state.fetching.fetchingApi);
const dispatch = useDispatch();
const close = () => dispatch(hideContextMenu());
useEffect(() => { useEffect(() => {
const handleClickOutside = (event) => { const handleClickOutside = (event) => {
@ -64,7 +61,7 @@ const UserContextMenu = ({
role="button" role="button"
tabIndex={0} tabIndex={0}
onClick={() => { onClick={() => {
addToInput(`@${name} `); dispatch(addToChatInputMessage(windowId, `@${name} `));
close(); close();
}} }}
style={{ borderTop: 'none' }} style={{ borderTop: 'none' }}
@ -83,13 +80,13 @@ const UserContextMenu = ({
for (let i = 0; i < cids.length; i += 1) { for (let i = 0; i < cids.length; i += 1) {
const cid = cids[i]; const cid = cids[i];
if (channels[cid].length === 4 && channels[cid][3] === uid) { if (channels[cid].length === 4 && channels[cid][3] === uid) {
setChannel(cid); dispatch(setChatChannel(windowId, cid));
close(); close();
return; return;
} }
} }
if (!fetching) { if (!fetching) {
dm(uid); dispatch(startDm(windowId, { userId: uid }));
} }
close(); close();
}} }}
@ -98,7 +95,7 @@ const UserContextMenu = ({
</div> </div>
<div <div
onClick={() => { onClick={() => {
block(uid, name); dispatch(setUserBlock(uid, name, true));
close(); close();
}} }}
role="button" role="button"
@ -110,55 +107,4 @@ const UserContextMenu = ({
); );
}; };
function mapStateToProps(state: State) { export default UserContextMenu;
const {
xPos,
yPos,
args,
} = state.contextMenu;
const {
channels,
} = state.chat;
const {
name,
uid,
} = args;
const {
fetchingApi: fetching,
} = state.fetching;
return {
xPos,
yPos,
channels,
name,
uid,
fetching,
};
}
function mapDispatchToProps(dispatch) {
return {
addToInput(text) {
dispatch(addToChatInputMessage(text));
const input = document.getElementById('chatmsginput');
if (input) {
input.focus();
input.select();
}
},
dm(userId) {
dispatch(startDm({ userId }));
},
block(userId, userName) {
dispatch(setUserBlock(userId, userName, true));
},
close() {
dispatch(hideContextMenu());
},
setChannel(channelId) {
dispatch(setChatChannel(channelId));
},
};
}
export default connect(mapStateToProps, mapDispatchToProps)(UserContextMenu);

80
src/components/Window.jsx Normal file
View File

@ -0,0 +1,80 @@
/*
* draw window
* @flow
*/
import React, { useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import Chat from './Chat';
import {
moveWindow,
} from '../actions';
const selectWindowById = (state, windowId) => state.windows.windows.find((win) => win.windowId === windowId);
const WINDOW_COMPONENTS = {
NONE: <div />,
CHAT: Chat,
};
const Window = ({ id }) => {
const win = useSelector((state) => selectWindowById(state, id));
const dispatch = useDispatch();
const startMove = useCallback((event) => {
event.preventDefault();
event.stopPropagation();
let {
clientX: startX,
clientY: startY,
} = event;
const move = (evt) => {
const {
clientX: curX,
clientY: curY,
} = evt;
dispatch(moveWindow(id, curX - startX, curY - startY));
startX = curX;
startY = curY;
};
document.addEventListener('mousemove', move);
const stopMove = () => {
document.removeEventListener('mousemove', move);
};
document.addEventListener('mouseup', stopMove, { once: true });
document.addEventListener('mouseleave', stopMove, { once: true });
}, []);
const {
width, height,
xPos, yPos,
windowType,
title,
} = win;
const Content = WINDOW_COMPONENTS[windowType];
console.log(`render window ${id}`);
return (
<div
className="window"
style={{
left: xPos,
top: yPos,
width,
height,
}}
>
<div
className="topbar"
onMouseDown={startMove}
>Move Here</div>
<Content windowId={id} />
</div>
);
};
export default React.memo(Window);

View File

@ -0,0 +1,21 @@
/*
* draw windows
* @flow
*/
import React from 'react';
import { useSelector, shallowEqual } from 'react-redux';
import Window from './Window';
const selectWindowIds = (state) => state.windows.windows.map((win) => win.windowId);
const WindowsRoot = () => {
const windowIds = useSelector(selectWindowIds, shallowEqual);
return windowIds.map((id) => (
<Window id={id} />
));
};
export default WindowsRoot;

View File

@ -5,7 +5,6 @@ import { MAX_CHAT_MESSAGES } from '../core/constants';
import type { Action } from '../actions/types'; import type { Action } from '../actions/types';
export type ChatState = { export type ChatState = {
inputMessage: string,
/* /*
* { * {
* cid: [ * cid: [
@ -30,7 +29,6 @@ export type ChatState = {
} }
const initialState: ChatState = { const initialState: ChatState = {
inputMessage: '',
channels: {}, channels: {},
blocked: [], blocked: [],
messages: {}, messages: {},
@ -63,7 +61,6 @@ export default function chat(
} }
return { return {
...state, ...state,
inputMessage: '',
channels, channels,
blocked: [], blocked: [],
messages, messages,
@ -138,30 +135,6 @@ export default function chat(
}; };
} }
case 'SET_CHAT_INPUT_MSG': {
const { message } = action;
return {
...state,
inputMessage: message,
};
}
case 'ADD_CHAT_INPUT_MSG': {
const { message } = action;
let { inputMessage } = state;
const lastChar = inputMessage.substr(-1);
const pad = (lastChar && lastChar !== ' ');
if (pad) {
inputMessage += ' ';
}
inputMessage += message;
return {
...state,
inputMessage,
};
}
case 'RECEIVE_CHAT_MESSAGE': { case 'RECEIVE_CHAT_MESSAGE': {
const { const {
name, text, country, channel, user, name, text, country, channel, user,

View File

@ -18,15 +18,17 @@ export type ChatReadState = {
// booleans if channel is unread // booleans if channel is unread
// {cid: unread, ...} // {cid: unread, ...}
unread: Object, unread: Object,
// selected chat channel // currently open chat channels can contain duplications
chatChannel: number, // just used to keep track of what channels we are seeing in
// windows to decide if readTS gets changed,
chatChannels: Array,
}; };
const initialState: ChatReadState = { const initialState: ChatReadState = {
mute: [], mute: [],
readTs: {}, readTs: {},
unread: {}, unread: {},
chatChannel: 1, chatChannels: [],
}; };
@ -57,11 +59,14 @@ export default function chatRead(
}; };
} }
case 'SET_CHAT_CHANNEL': { case 'OPEN_CHAT_CHANNEL': {
const { cid } = action; const { cid } = action;
return { return {
...state, ...state,
chatChannel: cid, chatChannels: [
...state.chatChannels,
cid,
],
readTs: { readTs: {
...state.readTs, ...state.readTs,
[cid]: Date.now() + TIME_DIFF_THREASHOLD, [cid]: Date.now() + TIME_DIFF_THREASHOLD,
@ -73,6 +78,19 @@ export default function chatRead(
}; };
} }
case 'CLOSE_CHAT_CHANNEL': {
const { cid } = action;
const chatChannels = [...state.chatChannels];
const pos = chatChannels.indexOf(cid);
if (pos !== -1) {
chatChannels.splice(pos, 1);
}
return {
...state,
chatChannels,
};
}
case 'ADD_CHAT_CHANNEL': { case 'ADD_CHAT_CHANNEL': {
const [cid] = Object.keys(action.channel); const [cid] = Object.keys(action.channel);
return { return {
@ -106,16 +124,14 @@ export default function chatRead(
case 'RECEIVE_CHAT_MESSAGE': { case 'RECEIVE_CHAT_MESSAGE': {
const { channel: cid } = action; const { channel: cid } = action;
const { chatChannel } = state; const { chatChannels } = state;
// eslint-disable-next-line eqeqeq const readTs = chatChannels.includes(cid)
const readTs = (chatChannel == cid)
? { ? {
...state.readTs, ...state.readTs,
// 15s treshold for desync // 15s treshold for desync
[cid]: Date.now() + TIME_DIFF_THREASHOLD, [cid]: Date.now() + TIME_DIFF_THREASHOLD,
} : state.readTs; } : state.readTs;
// eslint-disable-next-line eqeqeq const unread = chatChannels.includes(cid)
const unread = (chatChannel != cid)
? { ? {
...state.unread, ...state.unread,
[cid]: true, [cid]: true,

View File

@ -6,6 +6,7 @@ import audio from './audio';
import canvas from './canvas'; import canvas from './canvas';
import gui from './gui'; import gui from './gui';
import modal from './modal'; import modal from './modal';
import windows from './windows';
import user from './user'; import user from './user';
import ranks from './ranks'; import ranks from './ranks';
import alert from './alert'; import alert from './alert';
@ -59,6 +60,7 @@ export default persistCombineReducers(config, {
canvas, canvas,
gui, gui,
modal, modal,
windows,
user, user,
ranks, ranks,
alert, alert,

174
src/reducers/windows.js Normal file
View File

@ -0,0 +1,174 @@
/*
* state for open windows and modal and its content
*
* @flow
*/
import type { Action } from '../actions/types';
export type WindowsState = {
// [
// {
// windowId: number,
// windowType: string,
// title: string,
// width: number,
// height: number,
// xPos: percentage,
// yPos: percentage,
// },
// ]
windows: Array,
// {
// windowId: {
// ...
// }
// }
args: Object,
}
const initialState: WindowsState = {
windows: [],
args: {},
};
export default function windows(
state: WindowsState = initialState,
action: Action,
): WindowsState {
switch (action.type) {
case 'OPEN_WINDOW': {
const {
windowType,
title,
width,
height,
xPos,
yPos,
args,
} = action;
let windowId = Math.floor(Math.random() * 99999) + 1;
while (state.args[windowId]) {
windowId += 1;
}
return {
...state,
windows: [
...state.windows,
{
windowId,
windowType,
title,
width,
height,
xPos,
yPos,
args,
},
],
args: {
...state.args,
[windowId]: args,
},
};
}
case 'CLOSE_WINDOW': {
const {
windowId,
} = action;
const args = { ...state.args };
delete args[windowId];
return {
...state,
windows: state.windows.filter((win) => win.windowId !== windowId),
args,
};
}
case 'MOVE_WINDOW': {
const {
windowId,
xDiff,
yDiff,
} = action;
const newWindows = state.windows.map((win) => {
if (win.windowId !== windowId) return win;
return {
...win,
xPos: win.xPos + xDiff,
yPos: win.yPos + yDiff,
};
});
return {
...state,
windows: newWindows,
};
}
case 'CLOSE_ALL_WINDOWS': {
return initialState;
}
/*
* args specific actions
*/
case 'ADD_CHAT_INPUT_MSG': {
const {
windowId,
msg,
} = action;
let { inputMessage } = state.args[windowId];
const lastChar = inputMessage.substr(-1);
const pad = (lastChar && lastChar !== ' ') ? ' ' : '';
inputMessage += pad + msg;
return {
...state,
args: {
...state.args,
[windowId]: {
...state.args[windowId],
inputMessage,
},
},
};
}
case 'SET_CHAT_CHANNEL': {
const {
windowId,
cid,
} = action;
return {
...state,
args: {
...state.args,
[windowId]: {
...state.args[windowId],
chatChannel: cid,
},
},
};
}
case 'SET_CHAT_INPUT_MSG': {
const {
windowId,
msg,
} = action;
return {
...state,
args: {
...state.args,
[windowId]: {
...state.args[windowId],
inputMessage: msg,
},
},
};
}
default:
return state;
}
}

View File

@ -134,6 +134,18 @@ tr:nth-child(even) {
transition: 0.3s; transition: 0.3s;
} }
.window {
position: fixed;
background-color: rgba(226, 226, 226, 0.92);
border: solid black;
border-width: thin;
overflow: hidden;
}
.topbar {
cursor: move;
}
.contextmenu { .contextmenu {
position: absolute; position: absolute;
font-size: 12px; font-size: 12px;