diff --git a/src/actions/index.js b/src/actions/index.js
index 062a27c..1a98662 100644
--- a/src/actions/index.js
+++ b/src/actions/index.js
@@ -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 {
if (window.innerWidth > 604 && !forceModal) { return toggleChatBox(); }
return showModal('CHAT');
}
-export function setChatChannel(cid: number): Action {
+export function openChatChannel(cid: number): Action {
return {
- type: 'SET_CHAT_CHANNEL',
+ type: 'OPEN_CHAT_CHANNEL',
+ cid,
+ };
+}
+
+export function closeChatChannel(cid: number): Action {
+ return {
+ type: 'CLOSE_CHAT_CHANNEL',
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
*/
-export function startDm(query): PromiseAction {
+export function startDm(windowId, query): PromiseAction {
return async (dispatch) => {
dispatch(setApiFetching(true));
const res = await requestStartDm(query);
@@ -704,7 +746,7 @@ export function startDm(query): PromiseAction {
} else {
const cid = Object.keys(res)[0];
dispatch(addChatChannel(res));
- dispatch(setChatChannel(cid));
+ dispatch(setChatChannel(windowId, cid));
}
dispatch(setApiFetching(false));
};
diff --git a/src/actions/types.js b/src/actions/types.js
index 9dadb69..5801255 100644
--- a/src/actions/types.js
+++ b/src/actions/types.js
@@ -65,14 +65,17 @@ export type Action =
isPing: boolean,
}
| { 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: 'REMOVE_CHAT_CHANNEL', cid: number }
| { type: 'MUTE_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_INPUT_MSG', message: string }
- | { type: 'ADD_CHAT_INPUT_MSG', message: string }
+ | { type: 'MOVE_WINDOW', windowId: number, xDiff: number, yDiff: number }
| { type: 'BLOCK_USER', userId: number, userName: string }
| { type: 'UNBLOCK_USER', userId: number, userName: string }
| { type: 'SET_BLOCKING_DM', blockDm: boolean }
diff --git a/src/client.js b/src/client.js
index a2b75e6..29dab42 100644
--- a/src/client.js
+++ b/src/client.js
@@ -67,7 +67,7 @@ function init() {
name,
text,
country,
- channelId,
+ Number(channelId),
userId,
isPing,
));
@@ -90,9 +90,8 @@ function init() {
//
function checkMobile() {
store.dispatch(setMobile(true));
- document.removeEventListener('touchstart', checkMobile, false);
}
- document.addEventListener('touchstart', checkMobile, false);
+ document.addEventListener('touchstart', checkMobile, { once: true });
store.dispatch(initTimer());
diff --git a/src/components/App.jsx b/src/components/App.jsx
index edaf23d..912102a 100644
--- a/src/components/App.jsx
+++ b/src/components/App.jsx
@@ -20,6 +20,7 @@ import Menu from './Menu';
import UI from './UI';
import ExpandMenuButton from './ExpandMenuButton';
import ModalRoot from './ModalRoot';
+import WindowsRoot from './WindowsRoot';
const App = () => (
setChannel(cid)}
+ onClick={() => setChatChannel(cid)}
className={
`chn${
(cid === chatChannel) ? ' selected' : ''
@@ -226,29 +225,4 @@ const ChannelDropDown = ({
);
};
-function mapStateToProps(state: State) {
- 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);
+export default React.memo(ChannelDropDown);
diff --git a/src/components/Chat.jsx b/src/components/Chat.jsx
index f299fdf..5be1351 100644
--- a/src/components/Chat.jsx
+++ b/src/components/Chat.jsx
@@ -4,10 +4,10 @@
*/
import React, {
- useRef, useLayoutEffect, useState, useEffect,
+ useRef, useLayoutEffect, useState, useEffect, useCallback,
} from 'react';
import useStayScrolled from 'react-stay-scrolled';
-import { connect } from 'react-redux';
+import { useSelector, useDispatch } from 'react-redux';
import { t } from 'ttag';
import type { State } from '../reducers';
@@ -18,8 +18,8 @@ import {
showUserAreaModal,
showChatModal,
setChatChannel,
- fetchChatMessages,
setChatInputMessage,
+ fetchChatMessages,
showContextMenu,
} from '../actions';
import ProtocolClient from '../socket/ProtocolClient';
@@ -30,27 +30,29 @@ function escapeRegExp(string) {
}
const Chat = ({
+ windowId,
showExpand,
- channels,
- messages,
- chatChannel,
- ownName,
- open,
- inputMessage,
- setInputMessage,
- setChannel,
- fetchMessages,
- fetching,
- blocked,
- triggerModal,
- openChannelContextMenu,
}) => {
const listRef = useRef();
const targetRef = useRef();
+
const [nameRegExp, setNameRegExp] = useState(null);
const [blockedIds, setBlockedIds] = useState([]);
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, {
initialScroll: Infinity,
inaccuracy: 10,
@@ -58,7 +60,7 @@ const Chat = ({
const channelMessages = messages[chatChannel] || [];
if (channels[chatChannel] && !messages[chatChannel] && !fetching) {
- fetchMessages(chatChannel);
+ dispatch(fetchChatMessages(chatChannel));
}
useLayoutEffect(() => {
@@ -94,6 +96,7 @@ const Chat = ({
// send message via websocket
ProtocolClient.sendChatMessage(msg, chatChannel);
setInputMessage('');
+ dispatch(setChatInputMessage(windowId, ''));
}
/*
@@ -102,13 +105,13 @@ const Chat = ({
* set channel to first available one
*/
useEffect(() => {
- if (!channels[chatChannel]) {
+ if (!chatChannel || !channels[chatChannel]) {
const cids = Object.keys(channels);
if (cids.length) {
setChannel(cids[0]);
}
}
- }, [chatChannel, channels]);
+ }, [channels]);
return (
dispatch(showChatModal())}
role="button"
title={t`maximize`}
tabIndex={-1}
@@ -168,6 +172,8 @@ const Chat = ({
msgArray={splitChatMessage(t`Start chatting here`, nameRegExp)}
country="xx"
uid={0}
+ dark={isDarkMode}
+ windowId={windowId}
/>
)
}
@@ -179,6 +185,8 @@ const Chat = ({
msgArray={splitChatMessage(message[1], nameRegExp)}
country={message[2]}
uid={message[3]}
+ dark={isDarkMode}
+ windowId={windowId}
/>
)))
}
@@ -192,9 +200,10 @@ const Chat = ({
setInputMessage(e.target.value)}
+ onChange={(e) => dispatch(
+ setChatInputMessage(windowId, e.target.value),
+ )}
autoComplete="off"
- id="chatmsginput"
maxLength="200"
type="text"
placeholder={t`Chat here`}
@@ -206,13 +215,16 @@ const Chat = ({
>
‣
-
+
) : (
dispatch(showUserAreaModal())}
style={{ textAlign: 'center', fontSize: 13 }}
role="button"
tabIndex={0}
@@ -224,52 +236,4 @@ const Chat = ({
);
};
-function mapStateToProps(state: State) {
- 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);
+export default Chat;
diff --git a/src/components/ChatButton.jsx b/src/components/ChatButton.jsx
index b399c15..f2c0550 100644
--- a/src/components/ChatButton.jsx
+++ b/src/components/ChatButton.jsx
@@ -10,7 +10,7 @@ import { connect } from 'react-redux';
import { MdForum } from 'react-icons/md';
import { t } from 'ttag';
-import { showChatModal } from '../actions';
+import { showChatModal, openChatWindow } from '../actions';
const ChatButton = ({
@@ -80,7 +80,8 @@ const ChatButton = ({
function mapDispatchToProps(dispatch) {
return {
open() {
- dispatch(showChatModal(false));
+ // dispatch(showChatModal(false));
+ dispatch(openChatWindow());
},
};
}
diff --git a/src/components/ChatMessage.jsx b/src/components/ChatMessage.jsx
index d6526a0..8b29d24 100644
--- a/src/components/ChatMessage.jsx
+++ b/src/components/ChatMessage.jsx
@@ -3,7 +3,7 @@
* @flow
*/
import React from 'react';
-import { connect } from 'react-redux';
+import { useDispatch } from 'react-redux';
import { showContextMenu } from '../actions';
import { colorFromText, setBrightness } from '../core/utils';
@@ -13,14 +13,16 @@ function ChatMessage({
name,
uid,
country,
+ dark,
+ windowId,
msgArray,
- openUserContextMenu,
- darkMode,
}) {
if (!name || !msgArray) {
return null;
}
+ const dispatch = useDispatch();
+
const isInfo = (name === 'info');
const isEvent = (name === 'event');
let className = 'msg';
@@ -51,7 +53,7 @@ function ChatMessage({
{name}
@@ -95,7 +96,7 @@ function ChatMessage({
{txt}
);
@@ -104,7 +105,7 @@ function ChatMessage({
{txt}
);
@@ -116,21 +117,4 @@ function ChatMessage({
);
}
-function mapStateToProps(state: State) {
- 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);
+export default React.memo(ChatMessage);
diff --git a/src/components/UI.jsx b/src/components/UI.jsx
index 91b3783..140bf21 100644
--- a/src/components/UI.jsx
+++ b/src/components/UI.jsx
@@ -32,26 +32,24 @@ const UI = ({
menuOpen,
menuType,
}) => {
+ const contextMenu = (menuOpen && menuType) ? CONTEXT_MENUS[menuType] : null;
+
if (isHistoricalView) {
- return (
-
-
- {(menuOpen && menuType) ? CONTEXT_MENUS[menuType] : null}
-
- );
+ return [
+ ,
+ contextMenu,
+ ];
}
- return (
-
-
-
-
- {(!is3D) &&
}
- {(is3D && isOnMobile) &&
}
-
-
- {(menuOpen && menuType) && CONTEXT_MENUS[menuType]}
-
- );
+ return [
+ ,
+ ,
+ ,
+ (!is3D) && ,
+ (is3D && isOnMobile) && ,
+ ,
+ ,
+ contextMenu,
+ ];
};
function mapStateToProps(state: State) {
diff --git a/src/components/UserContextMenu.jsx b/src/components/UserContextMenu.jsx
index 9df7817..ab4aba5 100644
--- a/src/components/UserContextMenu.jsx
+++ b/src/components/UserContextMenu.jsx
@@ -6,7 +6,7 @@
import React, {
useRef, useEffect,
} from 'react';
-import { connect } from 'react-redux';
+import { useSelector, useDispatch } from 'react-redux';
import { t } from 'ttag';
import {
@@ -16,23 +16,20 @@ import {
setUserBlock,
setChatChannel,
} from '../actions';
-import type { State } from '../reducers';
const UserContextMenu = ({
- xPos,
- yPos,
- uid,
- name,
- addToInput,
- dm,
- block,
- channels,
- fetching,
setChannel,
- close,
}) => {
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(() => {
const handleClickOutside = (event) => {
@@ -64,7 +61,7 @@ const UserContextMenu = ({
role="button"
tabIndex={0}
onClick={() => {
- addToInput(`@${name} `);
+ dispatch(addToChatInputMessage(windowId, `@${name} `));
close();
}}
style={{ borderTop: 'none' }}
@@ -83,13 +80,13 @@ const UserContextMenu = ({
for (let i = 0; i < cids.length; i += 1) {
const cid = cids[i];
if (channels[cid].length === 4 && channels[cid][3] === uid) {
- setChannel(cid);
+ dispatch(setChatChannel(windowId, cid));
close();
return;
}
}
if (!fetching) {
- dm(uid);
+ dispatch(startDm(windowId, { userId: uid }));
}
close();
}}
@@ -98,7 +95,7 @@ const UserContextMenu = ({
{
- block(uid, name);
+ dispatch(setUserBlock(uid, name, true));
close();
}}
role="button"
@@ -110,55 +107,4 @@ const UserContextMenu = ({
);
};
-function mapStateToProps(state: State) {
- 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);
+export default UserContextMenu;
diff --git a/src/components/Window.jsx b/src/components/Window.jsx
new file mode 100644
index 0000000..273a7d3
--- /dev/null
+++ b/src/components/Window.jsx
@@ -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:
,
+ 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 (
+
+ );
+};
+
+export default React.memo(Window);
diff --git a/src/components/WindowsRoot.jsx b/src/components/WindowsRoot.jsx
new file mode 100644
index 0000000..486d491
--- /dev/null
+++ b/src/components/WindowsRoot.jsx
@@ -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) => (
+
+ ));
+};
+
+export default WindowsRoot;
diff --git a/src/reducers/chat.js b/src/reducers/chat.js
index 9cd36ec..edca1cf 100644
--- a/src/reducers/chat.js
+++ b/src/reducers/chat.js
@@ -5,7 +5,6 @@ import { MAX_CHAT_MESSAGES } from '../core/constants';
import type { Action } from '../actions/types';
export type ChatState = {
- inputMessage: string,
/*
* {
* cid: [
@@ -30,7 +29,6 @@ export type ChatState = {
}
const initialState: ChatState = {
- inputMessage: '',
channels: {},
blocked: [],
messages: {},
@@ -63,7 +61,6 @@ export default function chat(
}
return {
...state,
- inputMessage: '',
channels,
blocked: [],
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': {
const {
name, text, country, channel, user,
diff --git a/src/reducers/chatRead.js b/src/reducers/chatRead.js
index affe373..4ef3970 100644
--- a/src/reducers/chatRead.js
+++ b/src/reducers/chatRead.js
@@ -18,15 +18,17 @@ export type ChatReadState = {
// booleans if channel is unread
// {cid: unread, ...}
unread: Object,
- // selected chat channel
- chatChannel: number,
+ // currently open chat channels can contain duplications
+ // just used to keep track of what channels we are seeing in
+ // windows to decide if readTS gets changed,
+ chatChannels: Array,
};
const initialState: ChatReadState = {
mute: [],
readTs: {},
unread: {},
- chatChannel: 1,
+ chatChannels: [],
};
@@ -57,11 +59,14 @@ export default function chatRead(
};
}
- case 'SET_CHAT_CHANNEL': {
+ case 'OPEN_CHAT_CHANNEL': {
const { cid } = action;
return {
...state,
- chatChannel: cid,
+ chatChannels: [
+ ...state.chatChannels,
+ cid,
+ ],
readTs: {
...state.readTs,
[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': {
const [cid] = Object.keys(action.channel);
return {
@@ -106,16 +124,14 @@ export default function chatRead(
case 'RECEIVE_CHAT_MESSAGE': {
const { channel: cid } = action;
- const { chatChannel } = state;
- // eslint-disable-next-line eqeqeq
- const readTs = (chatChannel == cid)
+ const { chatChannels } = state;
+ const readTs = chatChannels.includes(cid)
? {
...state.readTs,
// 15s treshold for desync
[cid]: Date.now() + TIME_DIFF_THREASHOLD,
} : state.readTs;
- // eslint-disable-next-line eqeqeq
- const unread = (chatChannel != cid)
+ const unread = chatChannels.includes(cid)
? {
...state.unread,
[cid]: true,
diff --git a/src/reducers/index.js b/src/reducers/index.js
index c62e651..9d50864 100644
--- a/src/reducers/index.js
+++ b/src/reducers/index.js
@@ -6,6 +6,7 @@ import audio from './audio';
import canvas from './canvas';
import gui from './gui';
import modal from './modal';
+import windows from './windows';
import user from './user';
import ranks from './ranks';
import alert from './alert';
@@ -59,6 +60,7 @@ export default persistCombineReducers(config, {
canvas,
gui,
modal,
+ windows,
user,
ranks,
alert,
diff --git a/src/reducers/windows.js b/src/reducers/windows.js
new file mode 100644
index 0000000..93c8ee4
--- /dev/null
+++ b/src/reducers/windows.js
@@ -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;
+ }
+}
diff --git a/src/styles/default.css b/src/styles/default.css
index 6dfed59..a383734 100644
--- a/src/styles/default.css
+++ b/src/styles/default.css
@@ -134,6 +134,18 @@ tr:nth-child(even) {
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 {
position: absolute;
font-size: 12px;