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 {
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));
};

View File

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

View File

@ -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());

View File

@ -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 = () => (
<div>
@ -35,6 +36,7 @@ const App = () => (
<ExpandMenuButton />
<UI />
<ModalRoot />
<WindowsRoot />
</IconContext.Provider>
</div>
);

View File

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

View File

@ -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 (
<div
onClick={() => 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);

View File

@ -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 (
<div
@ -133,11 +136,12 @@ const Chat = ({
clientX,
clientY,
} = event;
openChannelContextMenu(
dispatch(showContextMenu(
'CHANNEL',
clientX,
clientY,
chatChannel,
);
{ cid: chatChannel },
));
}}
role="button"
title={t`Channel settings`}
@ -147,7 +151,7 @@ const Chat = ({
{(showExpand)
&& (
<span
onClick={triggerModal}
onClick={() => 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 = ({
<input
style={{ flexGrow: 1, minWidth: 40 }}
value={inputMessage}
onChange={(e) => 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 = ({
>
</button>
<ChannelDropDown />
<ChannelDropDown
setChatChannel={setChannel}
chatChannel={chatChannel}
/>
</form>
</div>
) : (
<div
className="modallink"
onClick={open}
onClick={() => 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;

View File

@ -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());
},
};
}

View File

@ -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({
<span
className="chatname"
style={{
color: setBrightness(colorFromText(name), darkMode),
color: setBrightness(colorFromText(name), dark),
cursor: 'pointer',
}}
role="button"
@ -61,12 +63,11 @@ function ChatMessage({
clientX,
clientY,
} = event;
openUserContextMenu(
clientX,
clientY,
dispatch(showContextMenu('USER', clientX, clientY, {
windowId,
uid,
name,
);
}));
}}
>
{name}
@ -95,7 +96,7 @@ function ChatMessage({
<span
className="ping"
style={{
color: setBrightness(colorFromText(txt.substr(1)), darkMode),
color: setBrightness(colorFromText(txt.substr(1)), dark),
}}
>{txt}</span>
);
@ -104,7 +105,7 @@ function ChatMessage({
<span
className="mention"
style={{
color: setBrightness(colorFromText(txt.substr(1)), darkMode),
color: setBrightness(colorFromText(txt.substr(1)), dark),
}}
>{txt}</span>
);
@ -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);

View File

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

View File

@ -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 = ({
</div>
<div
onClick={() => {
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;

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';
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,

View File

@ -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,

View File

@ -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,

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