add popu-up menu

This commit is contained in:
HF 2020-11-08 15:36:22 +01:00
parent 2161fe11a2
commit ad79ac1746
15 changed files with 379 additions and 36 deletions

View File

@ -199,6 +199,7 @@ export function receiveChatMessage(
text: string, text: string,
country: string, country: string,
channel: number, channel: number,
user: number,
isPing: boolean, isPing: boolean,
): Action { ): Action {
return { return {
@ -207,6 +208,7 @@ export function receiveChatMessage(
text, text,
country, country,
channel, channel,
user,
isPing, isPing,
}; };
} }
@ -705,6 +707,35 @@ export function showCanvasSelectionModal(): Action {
return showModal('CANVAS_SELECTION'); return showModal('CANVAS_SELECTION');
} }
export function showContextMenu(
menuType: string,
xPos: number,
yPos: number,
args: Object,
): Action {
return {
type: 'SHOW_CONTEXT_MENU',
menuType,
xPos,
yPos,
args,
};
}
export function setChatInputMessage(message: string): Action {
return {
type: 'SET_CHAT_INPUT_MSG',
message,
};
}
export function addToChatInputMessage(message: string): Action {
return {
type: 'ADD_CHAT_INPUT_MSG',
message,
};
}
export function showChatModal(forceModal: boolean = false): Action { export function showChatModal(forceModal: boolean = false): Action {
if (window.innerWidth > 604 && !forceModal) { return toggleChatBox(); } if (window.innerWidth > 604 && !forceModal) { return toggleChatBox(); }
return showModal('CHAT'); return showModal('CHAT');
@ -723,6 +754,12 @@ export function hideModal(): Action {
}; };
} }
export function hideContextMenu(): Action {
return {
type: 'HIDE_CONTEXT_MENU',
};
}
export function reloadUrl(): Action { export function reloadUrl(): Action {
return { return {
type: 'RELOAD_URL', type: 'RELOAD_URL',

View File

@ -65,11 +65,14 @@ export type Action =
text: string, text: string,
country: string, country: string,
channel: number, channel: number,
user: number,
isPing: boolean, isPing: boolean,
} }
| { type: 'RECEIVE_CHAT_HISTORY', cid: number, history: Array } | { type: 'RECEIVE_CHAT_HISTORY', cid: number, history: Array }
| { type: 'SET_CHAT_CHANNEL', channelId: number } | { type: 'SET_CHAT_CHANNEL', channelId: number }
| { type: 'SET_CHAT_FETCHING', fetching: boolean } | { type: 'SET_CHAT_FETCHING', fetching: boolean }
| { type: 'SET_CHAT_INPUT_MSG', message: string }
| { type: 'ADD_CHAT_INPUT_MSG', message: string }
| { type: 'RECEIVE_ME', | { type: 'RECEIVE_ME',
name: string, name: string,
waitSeconds: number, waitSeconds: number,
@ -90,7 +93,14 @@ export type Action =
| { type: 'SET_MAILREG', mailreg: boolean } | { type: 'SET_MAILREG', mailreg: boolean }
| { type: 'REM_FROM_MESSAGES', message: string } | { type: 'REM_FROM_MESSAGES', message: string }
| { type: 'SHOW_MODAL', modalType: string } | { type: 'SHOW_MODAL', modalType: string }
| { type: 'SHOW_CONTEXT_MENU',
menuType: string,
xPos: number,
yPos: number,
args: Object,
}
| { type: 'HIDE_MODAL' } | { type: 'HIDE_MODAL' }
| { type: 'HIDE_CONTEXT_MENU' }
| { type: 'RELOAD_URL' } | { type: 'RELOAD_URL' }
| { type: 'SET_HISTORICAL_TIME', date: string, time: string } | { type: 'SET_HISTORICAL_TIME', date: string, time: string }
| { type: 'ON_VIEW_FINISH_CHANGE' }; | { type: 'ON_VIEW_FINISH_CHANGE' };

View File

@ -52,9 +52,22 @@ function init() {
ProtocolClient.on('setWsName', (name) => { ProtocolClient.on('setWsName', (name) => {
nameRegExp = new RegExp(`(^|\\s+)(@${name})(\\s+|$)`, 'g'); nameRegExp = new RegExp(`(^|\\s+)(@${name})(\\s+|$)`, 'g');
}); });
ProtocolClient.on('chatMessage', (name, text, country, channelId) => { ProtocolClient.on('chatMessage', (
name,
text,
country,
channelId,
userId,
) => {
const isPing = (nameRegExp && text.match(nameRegExp)); const isPing = (nameRegExp && text.match(nameRegExp));
store.dispatch(receiveChatMessage(name, text, country, channelId, isPing)); store.dispatch(receiveChatMessage(
name,
text,
country,
channelId,
userId,
isPing,
));
}); });
ProtocolClient.on('changedMe', () => { ProtocolClient.on('changedMe', () => {
store.dispatch(fetchMe()); store.dispatch(fetchMe());

View File

@ -17,6 +17,7 @@ import {
showUserAreaModal, showUserAreaModal,
setChatChannel, setChatChannel,
fetchChatMessages, fetchChatMessages,
setChatInputMessage,
} from '../actions'; } from '../actions';
import ProtocolClient from '../socket/ProtocolClient'; import ProtocolClient from '../socket/ProtocolClient';
import { saveSelection, restoreSelection } from '../utils/storeSelection'; import { saveSelection, restoreSelection } from '../utils/storeSelection';
@ -32,13 +33,13 @@ const Chat = ({
chatChannel, chatChannel,
ownName, ownName,
open, open,
inputMessage,
setInputMessage,
setChannel, setChannel,
fetchMessages, fetchMessages,
fetching, fetching,
}) => { }) => {
const listRef = useRef(); const listRef = useRef();
const inputRef = useRef();
const [inputMessage, setInputMessage] = useState('');
const [selection, setSelection] = useState(null); const [selection, setSelection] = useState(null);
const [nameRegExp, setNameRegExp] = useState(null); const [nameRegExp, setNameRegExp] = useState(null);
@ -56,13 +57,15 @@ const Chat = ({
stayScrolled(); stayScrolled();
}, [channelMessages.length]); }, [channelMessages.length]);
/*
* TODO this removes focus from chat box, fix this
*
useEffect(() => { useEffect(() => {
// TODO this removes focus from chat box, fix this
return;
if (channelMessages.length === MAX_CHAT_MESSAGES) { if (channelMessages.length === MAX_CHAT_MESSAGES) {
restoreSelection(selection); restoreSelection(selection);
} }
}, [channelMessages]); }, [channelMessages]);
*/
useEffect(() => { useEffect(() => {
const regExp = (ownName) const regExp = (ownName)
@ -71,16 +74,6 @@ const Chat = ({
setNameRegExp(regExp); setNameRegExp(regExp);
}, [ownName]); }, [ownName]);
function padToInputMessage(txt) {
const lastChar = inputMessage.substr(-1);
const pad = (lastChar && lastChar !== ' ');
let newMsg = inputMessage;
if (pad) newMsg += ' ';
newMsg += txt;
setInputMessage(newMsg);
inputRef.current.focus();
}
function handleSubmit(e) { function handleSubmit(e) {
e.preventDefault(); e.preventDefault();
const msg = inputMessage.trim(); const msg = inputMessage.trim();
@ -123,7 +116,7 @@ const Chat = ({
name="info" name="info"
msgArray={splitChatMessage('Start chatting here', nameRegExp)} msgArray={splitChatMessage('Start chatting here', nameRegExp)}
country="xx" country="xx"
insertText={(txt) => padToInputMessage(txt)} uid={0}
/> />
) )
} }
@ -133,7 +126,7 @@ const Chat = ({
name={message[0]} name={message[0]}
msgArray={splitChatMessage(message[1], nameRegExp)} msgArray={splitChatMessage(message[1], nameRegExp)}
country={message[2]} country={message[2]}
insertText={(txt) => padToInputMessage(txt)} uid={message[3]}
/> />
)) ))
} }
@ -148,7 +141,7 @@ const Chat = ({
style={{ flexGrow: 1, minWidth: 40 }} style={{ flexGrow: 1, minWidth: 40 }}
value={inputMessage} value={inputMessage}
onChange={(e) => setInputMessage(e.target.value)} onChange={(e) => setInputMessage(e.target.value)}
ref={inputRef} id="chatmsginput"
maxLength="200" maxLength="200"
type="text" type="text"
placeholder="Chat here" placeholder="Chat here"
@ -198,11 +191,17 @@ const Chat = ({
function mapStateToProps(state: State) { function mapStateToProps(state: State) {
const { name } = state.user; const { name } = state.user;
const { chatChannel } = state.gui; const { chatChannel } = state.gui;
const { channels, messages, fetching } = state.chat; const {
channels,
messages,
fetching,
inputMessage,
} = state.chat;
return { return {
channels, channels,
messages, messages,
fetching, fetching,
inputMessage,
chatChannel, chatChannel,
ownName: name, ownName: name,
}; };
@ -219,6 +218,9 @@ function mapDispatchToProps(dispatch) {
fetchMessages(channelId) { fetchMessages(channelId) {
dispatch(fetchChatMessages(channelId)); dispatch(fetchChatMessages(channelId));
}, },
setInputMessage(message) {
dispatch(setChatInputMessage(message));
},
}; };
} }

View File

@ -5,14 +5,16 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { showContextMenu } from '../actions';
import { colorFromText, setBrightness } from '../core/utils'; import { colorFromText, setBrightness } from '../core/utils';
function ChatMessage({ function ChatMessage({
name, name,
msgArray, uid,
country, country,
insertText, msgArray,
openUserContextMenu,
darkMode, darkMode,
}) { }) {
if (!name || !msgArray) { if (!name || !msgArray) {
@ -54,8 +56,17 @@ function ChatMessage({
}} }}
role="button" role="button"
tabIndex={-1} tabIndex={-1}
onClick={() => { onClick={(event) => {
insertText(`@${name} `); const {
clientX,
clientY,
} = event;
openUserContextMenu(
clientX,
clientY,
uid,
name,
);
}} }}
> >
{name} {name}
@ -103,4 +114,15 @@ function mapStateToProps(state: State) {
return { darkMode }; return { darkMode };
} }
export default connect(mapStateToProps)(ChatMessage); function mapDispatchToProps(dispatch) {
return {
openUserContextMenu(xPos, yPos, uid, name) {
dispatch(showContextMenu('USER', xPos, yPos, {
uid,
name,
}));
},
};
}
export default connect(mapStateToProps, mapDispatchToProps)(ChatMessage);

View File

@ -14,11 +14,28 @@ import PalselButton from './PalselButton';
import Palette from './Palette'; import Palette from './Palette';
import HistorySelect from './HistorySelect'; import HistorySelect from './HistorySelect';
import Mobile3DControls from './Mobile3DControls'; import Mobile3DControls from './Mobile3DControls';
import UserContextMenu from './UserContextMenu';
const UI = ({ isHistoricalView, is3D, isOnMobile }) => { const CONTEXT_MENUS = {
USER: <UserContextMenu />,
/* other context menus */
};
const UI = ({
isHistoricalView,
is3D,
isOnMobile,
menuOpen,
menuType,
}) => {
if (isHistoricalView) { if (isHistoricalView) {
return <HistorySelect />; return (
<div>
<HistorySelect />
{(menuOpen && menuType) ? CONTEXT_MENUS[menuType] : null}
</div>
);
} }
return ( return (
<div> <div>
@ -28,6 +45,7 @@ const UI = ({ isHistoricalView, is3D, isOnMobile }) => {
{(is3D && isOnMobile) ? <Mobile3DControls /> : null} {(is3D && isOnMobile) ? <Mobile3DControls /> : null}
<CoolDownBox /> <CoolDownBox />
<NotifyBox /> <NotifyBox />
{(menuOpen && menuType) ? CONTEXT_MENUS[menuType] : null}
</div> </div>
); );
}; };
@ -40,10 +58,16 @@ function mapStateToProps(state: State) {
const { const {
isOnMobile, isOnMobile,
} = state.user; } = state.user;
const {
menuOpen,
menuType,
} = state.contextMenu;
return { return {
isHistoricalView, isHistoricalView,
is3D, is3D,
isOnMobile, isOnMobile,
menuOpen,
menuType,
}; };
} }

View File

@ -0,0 +1,120 @@
/*
*
* @flow
*/
import React, {
useRef, useEffect,
} from 'react';
import { connect } from 'react-redux';
import {
hideContextMenu,
addToChatInputMessage,
setChatInputMessage,
} from '../actions';
import type { State } from '../reducers';
const UserContextMenu = ({
xPos,
yPos,
uid,
name,
setInput,
addToInput,
close,
}) => {
const wrapperRef = useRef(null);
useEffect(() => {
function handleClickOutside(event) {
if (wrapperRef.current && !wrapperRef.current.contains(event.target)) {
close();
}
}
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('touchstart', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.addEventListener('touchstart', handleClickOutside);
};
}, [wrapperRef]);
return (
<div
ref={wrapperRef}
className="contextmenu"
style={{
left: xPos,
top: yPos,
}}
>
<div
style={{ borderBottom: 'thin solid' }}
>
Block
</div>
<div
role="button"
tabIndex={0}
onClick={() => {
setInput('loool');
}}
style={{ borderBottom: 'thin solid' }}
>
DM
</div>
<div
role="button"
tabIndex={0}
onClick={() => {
addToInput(`@${name} `);
close();
}}
>
Ping
</div>
</div>
);
};
function mapStateToProps(state: State) {
const {
xPos,
yPos,
args,
} = state.contextMenu;
const {
name,
uid,
} = args;
return {
xPos,
yPos,
name,
uid,
};
}
function mapDispatchToProps(dispatch) {
return {
addToInput(text) {
dispatch(addToChatInputMessage(text));
const input = document.getElementById('chatmsginput');
if (input) {
input.focus();
input.select();
}
},
setInput(text) {
dispatch(setChatInputMessage(text));
},
close() {
dispatch(hideContextMenu());
},
};
}
export default connect(mapStateToProps, mapDispatchToProps)(UserContextMenu);

View File

@ -68,6 +68,7 @@ class ChatMessageBuffer {
name, name,
message, message,
flag, flag,
uid,
]); ]);
} }
} }
@ -80,6 +81,7 @@ class ChatMessageBuffer {
as: 'user', as: 'user',
foreignKey: 'uid', foreignKey: 'uid',
attributes: [ attributes: [
'id',
'name', 'name',
'flag', 'flag',
], ],
@ -101,11 +103,13 @@ class ChatMessageBuffer {
message, message,
'user.name': name, 'user.name': name,
'user.flag': flag, 'user.flag': flag,
'user.id': uid,
} = messagesModel[i]; } = messagesModel[i];
messages.push([ messages.push([
name, name,
message, message,
flag, flag,
uid,
]); ]);
} }
return messages; return messages;

View File

@ -5,6 +5,7 @@ import { MAX_CHAT_MESSAGES } from '../core/constants';
import type { Action } from '../actions/types'; import type { Action } from '../actions/types';
export type ChatState = { export type ChatState = {
inputMessage: string,
// [[cid, name], [cid2, name2],...] // [[cid, name], [cid2, name2],...]
channels: Array, channels: Array,
// { cid: [message1,message2,message2,...]} // { cid: [message1,message2,message2,...]}
@ -14,6 +15,7 @@ export type ChatState = {
} }
const initialState: ChatState = { const initialState: ChatState = {
inputMessage: '',
channels: [], channels: [],
messages: {}, messages: {},
fetching: false, fetching: false,
@ -39,9 +41,33 @@ export default function chat(
}; };
} }
case 'SET_CHAT_INPUT_MSG': {
const { message } = action;
return {
...state,
inputMessage: message,
};
}
case 'ADD_CHAT_INPUT_MSG': {
const { message } = action;
let { inputMessage } = state;
const lastChar = inputMessage.substr(-1);
const pad = (lastChar && lastChar !== ' ');
if (pad) {
inputMessage += ' ';
}
inputMessage += message;
return {
...state,
inputMessage,
};
}
case 'RECEIVE_CHAT_MESSAGE': { case 'RECEIVE_CHAT_MESSAGE': {
const { const {
name, text, country, channel, name, text, country, channel, user,
} = action; } = action;
if (!state.messages[channel]) { if (!state.messages[channel]) {
return state; return state;
@ -50,7 +76,7 @@ export default function chat(
...state.messages, ...state.messages,
[channel]: [ [channel]: [
...state.messages[channel], ...state.messages[channel],
[name, text, country], [name, text, country, user],
], ],
}; };
if (messages[channel].length > MAX_CHAT_MESSAGES) { if (messages[channel].length > MAX_CHAT_MESSAGES) {

View File

@ -0,0 +1,57 @@
/**
* https://stackoverflow.com/questions/35623656/how-can-i-display-a-modal-dialog-in-redux-that-performs-asynchronous-actions/35641680#35641680
*
* @flow
*/
import type { Action } from '../actions/types';
export type ContextMenuState = {
menuOpen: boolean,
menuType: ?string,
xPos: number,
yPos: number,
args: Object,
};
const initialState: ContextMenuState = {
menuOpen: false,
menuType: null,
xPos: 0,
yPos: 0,
args: {},
};
export default function contextMenu(
state: ModalState = initialState,
action: Action,
): ContextMenuState {
switch (action.type) {
case 'SHOW_CONTEXT_MENU': {
const {
menuType, xPos, yPos, args,
} = action;
return {
...state,
menuType,
xPos,
yPos,
menuOpen: true,
args: {
...args,
},
};
}
case 'HIDE_CONTEXT_MENU': {
return {
...state,
menuOpen: false,
};
}
default:
return state;
}
}

View File

@ -8,6 +8,7 @@ import gui from './gui';
import modal from './modal'; import modal from './modal';
import user from './user'; import user from './user';
import chat from './chat'; import chat from './chat';
import contextMenu from './contextMenu';
import type { AudioState } from './audio'; import type { AudioState } from './audio';
import type { CanvasState } from './canvas'; import type { CanvasState } from './canvas';
@ -15,6 +16,7 @@ import type { GUIState } from './gui';
import type { ModalState } from './modal'; import type { ModalState } from './modal';
import type { UserState } from './user'; import type { UserState } from './user';
import type { ChatState } from './chat'; import type { ChatState } from './chat';
import type { ContextMenuState } from './contextMenu';
export type State = { export type State = {
audio: AudioState, audio: AudioState,
@ -23,12 +25,19 @@ export type State = {
modal: ModalState, modal: ModalState,
user: UserState, user: UserState,
chat: ChatState, chat: ChatState,
contextMenu: ContextMenuState,
}; };
const config = { const config = {
key: 'primary', key: 'primary',
storage: localForage, storage: localForage,
blacklist: ['user', 'canvas', 'modal', 'chat'], blacklist: [
'user',
'canvas',
'modal',
'chat',
'contextMenu',
],
}; };
export default persistCombineReducers(config, { export default persistCombineReducers(config, {
@ -38,4 +47,5 @@ export default persistCombineReducers(config, {
modal, modal,
user, user,
chat, chat,
contextMenu,
}); });

View File

@ -24,8 +24,6 @@ export default function modal(
action: Action, action: Action,
): ModalState { ): ModalState {
switch (action.type) { switch (action.type) {
// clear hover when placing a pixel
// fixes a bug with iPad
case 'SHOW_MODAL': { case 'SHOW_MODAL': {
const { modalType } = action; const { modalType } = action;
const chatOpen = (modalType === 'CHAT') ? false : state.chatOpen; const chatOpen = (modalType === 'CHAT') ? false : state.chatOpen;

View File

@ -169,10 +169,10 @@ class ProtocolClient extends EventEmitter {
const data = JSON.parse(message); const data = JSON.parse(message);
if (Array.isArray(data)) { if (Array.isArray(data)) {
if (data.length === 4) { if (data.length === 5) {
// Ordinary array: Chat message // Ordinary array: Chat message
const [name, text, country, channelId] = data; const [name, text, country, channelId, userId] = data;
this.emit('chatMessage', name, text, country, channelId); this.emit('chatMessage', name, text, country, channelId, userId);
} }
} else { } else {
// string = name // string = name

View File

@ -169,7 +169,7 @@ class SocketServer extends WebSocketEvents {
id: number, id: number,
country: string, country: string,
) { ) {
const text = JSON.stringify([name, message, country, channelId]); const text = JSON.stringify([name, message, country, channelId, id]);
this.wss.clients.forEach((ws) => { this.wss.clients.forEach((ws) => {
if (ws.readyState === WebSocket.OPEN) { if (ws.readyState === WebSocket.OPEN) {
if (chatProvider.userHasChannelAccess(ws.user, channelId)) { if (chatProvider.userHasChannelAccess(ws.user, channelId)) {

View File

@ -124,6 +124,26 @@ tr:nth-child(even) {
transition: 0.3s; transition: 0.3s;
} }
.contextmenu {
position: absolute;
width: 90px;
font-size: 12px;
z-index: 10;
background-color: rgba(226, 226, 226);
border: solid black;
border-width: thin;
}
.contextmenu > div {
border-width: thin;
margin: 2px;
}
.contextmenu > div:hover {
background-color: white;
cursor: pointer;
}
.chatbox.show { .chatbox.show {
height: 200px; height: 200px;
width: 350px; width: 350px;