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 = () => (
@@ -35,6 +36,7 @@ const App = () => ( +
); diff --git a/src/components/ChannelContextMenu.jsx b/src/components/ChannelContextMenu.jsx index 730b5ec..fddb5a9 100644 --- a/src/components/ChannelContextMenu.jsx +++ b/src/components/ChannelContextMenu.jsx @@ -17,7 +17,7 @@ import { } from '../actions'; import type { State } from '../reducers'; -const UserContextMenu = ({ +const ChannelContextMenu = ({ xPos, yPos, cid, @@ -128,4 +128,4 @@ function mapDispatchToProps(dispatch) { }; } -export default connect(mapStateToProps, mapDispatchToProps)(UserContextMenu); +export default connect(mapStateToProps, mapDispatchToProps)(ChannelContextMenu); diff --git a/src/components/ChannelDropDown.jsx b/src/components/ChannelDropDown.jsx index 3f921e8..f689c50 100644 --- a/src/components/ChannelDropDown.jsx +++ b/src/components/ChannelDropDown.jsx @@ -7,22 +7,15 @@ import React, { useRef, useState, useEffect, useCallback, useLayoutEffect, } from 'react'; -import { connect } from 'react-redux'; +import { useSelector } from 'react-redux'; import { MdChat } from 'react-icons/md'; import { FaUserFriends } from 'react-icons/fa'; import type { State } from '../reducers'; -import { - setChatChannel, -} from '../actions'; const ChannelDropDown = ({ - channels, + setChatChannel, chatChannel, - unread, - chatNotify, - mute, - setChannel, }) => { const [show, setShow] = useState(false); const [sortChans, setSortChans] = useState([]); @@ -36,6 +29,12 @@ const ChannelDropDown = ({ const wrapperRef = 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(() => { setOffset(buttonRef.current.clientHeight); }, [buttonRef]); @@ -200,7 +199,7 @@ const ChannelDropDown = ({ const [cid, unreadCh, name] = ch; return (
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 ( +
+
Move Here
+ +
+ ); +}; + +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;