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,
country: string,
channel: number,
user: number,
isPing: boolean,
): Action {
return {
@ -207,6 +208,7 @@ export function receiveChatMessage(
text,
country,
channel,
user,
isPing,
};
}
@ -705,6 +707,35 @@ export function showCanvasSelectionModal(): Action {
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 {
if (window.innerWidth > 604 && !forceModal) { return toggleChatBox(); }
return showModal('CHAT');
@ -723,6 +754,12 @@ export function hideModal(): Action {
};
}
export function hideContextMenu(): Action {
return {
type: 'HIDE_CONTEXT_MENU',
};
}
export function reloadUrl(): Action {
return {
type: 'RELOAD_URL',

View File

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

View File

@ -52,9 +52,22 @@ function init() {
ProtocolClient.on('setWsName', (name) => {
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));
store.dispatch(receiveChatMessage(name, text, country, channelId, isPing));
store.dispatch(receiveChatMessage(
name,
text,
country,
channelId,
userId,
isPing,
));
});
ProtocolClient.on('changedMe', () => {
store.dispatch(fetchMe());

View File

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

View File

@ -5,14 +5,16 @@
import React from 'react';
import { connect } from 'react-redux';
import { showContextMenu } from '../actions';
import { colorFromText, setBrightness } from '../core/utils';
function ChatMessage({
name,
msgArray,
uid,
country,
insertText,
msgArray,
openUserContextMenu,
darkMode,
}) {
if (!name || !msgArray) {
@ -54,8 +56,17 @@ function ChatMessage({
}}
role="button"
tabIndex={-1}
onClick={() => {
insertText(`@${name} `);
onClick={(event) => {
const {
clientX,
clientY,
} = event;
openUserContextMenu(
clientX,
clientY,
uid,
name,
);
}}
>
{name}
@ -103,4 +114,15 @@ function mapStateToProps(state: State) {
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 HistorySelect from './HistorySelect';
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) {
return <HistorySelect />;
return (
<div>
<HistorySelect />
{(menuOpen && menuType) ? CONTEXT_MENUS[menuType] : null}
</div>
);
}
return (
<div>
@ -28,6 +45,7 @@ const UI = ({ isHistoricalView, is3D, isOnMobile }) => {
{(is3D && isOnMobile) ? <Mobile3DControls /> : null}
<CoolDownBox />
<NotifyBox />
{(menuOpen && menuType) ? CONTEXT_MENUS[menuType] : null}
</div>
);
};
@ -40,10 +58,16 @@ function mapStateToProps(state: State) {
const {
isOnMobile,
} = state.user;
const {
menuOpen,
menuType,
} = state.contextMenu;
return {
isHistoricalView,
is3D,
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,
message,
flag,
uid,
]);
}
}
@ -80,6 +81,7 @@ class ChatMessageBuffer {
as: 'user',
foreignKey: 'uid',
attributes: [
'id',
'name',
'flag',
],
@ -101,11 +103,13 @@ class ChatMessageBuffer {
message,
'user.name': name,
'user.flag': flag,
'user.id': uid,
} = messagesModel[i];
messages.push([
name,
message,
flag,
uid,
]);
}
return messages;

View File

@ -5,6 +5,7 @@ import { MAX_CHAT_MESSAGES } from '../core/constants';
import type { Action } from '../actions/types';
export type ChatState = {
inputMessage: string,
// [[cid, name], [cid2, name2],...]
channels: Array,
// { cid: [message1,message2,message2,...]}
@ -14,6 +15,7 @@ export type ChatState = {
}
const initialState: ChatState = {
inputMessage: '',
channels: [],
messages: {},
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': {
const {
name, text, country, channel,
name, text, country, channel, user,
} = action;
if (!state.messages[channel]) {
return state;
@ -50,7 +76,7 @@ export default function chat(
...state.messages,
[channel]: [
...state.messages[channel],
[name, text, country],
[name, text, country, user],
],
};
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 user from './user';
import chat from './chat';
import contextMenu from './contextMenu';
import type { AudioState } from './audio';
import type { CanvasState } from './canvas';
@ -15,6 +16,7 @@ import type { GUIState } from './gui';
import type { ModalState } from './modal';
import type { UserState } from './user';
import type { ChatState } from './chat';
import type { ContextMenuState } from './contextMenu';
export type State = {
audio: AudioState,
@ -23,12 +25,19 @@ export type State = {
modal: ModalState,
user: UserState,
chat: ChatState,
contextMenu: ContextMenuState,
};
const config = {
key: 'primary',
storage: localForage,
blacklist: ['user', 'canvas', 'modal', 'chat'],
blacklist: [
'user',
'canvas',
'modal',
'chat',
'contextMenu',
],
};
export default persistCombineReducers(config, {
@ -38,4 +47,5 @@ export default persistCombineReducers(config, {
modal,
user,
chat,
contextMenu,
});

View File

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

View File

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

View File

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

View File

@ -124,6 +124,26 @@ tr:nth-child(even) {
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 {
height: 200px;
width: 350px;