add basic window reducer and add chat to it
This commit is contained in:
parent
3b0dcd9545
commit
baca212686
|
@ -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));
|
||||
};
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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());
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
80
src/components/Window.jsx
Normal 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);
|
21
src/components/WindowsRoot.jsx
Normal file
21
src/components/WindowsRoot.jsx
Normal 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;
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
174
src/reducers/windows.js
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue
Block a user