add link hook and use context for window data

This commit is contained in:
HF 2022-09-04 21:12:47 +02:00
parent b51303fe6f
commit 715c6ff1cc
26 changed files with 306 additions and 191 deletions

View File

@ -117,7 +117,7 @@ persistStore(store, {}, () => {
SocketClient.connect();
});
(function () {
(function load() {
const onLoad = () => {
renderApp(document.getElementById('app'), store);

View File

@ -3,11 +3,10 @@
*/
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { t } from 'ttag';
import useInterval from './hooks/interval';
import { showHelpModal } from '../store/actions/windows';
import useLink from './hooks/link';
import {
largeDurationToString,
} from '../core/utils';
@ -22,7 +21,7 @@ const BanInfo = ({ close }) => {
const [expire, setExpire] = useState(null);
const [submitting, setSubmitting] = useState(false);
const dispatch = useDispatch();
const link = useLink();
const handleSubmit = async () => {
if (submitting) {
@ -70,7 +69,7 @@ const BanInfo = ({ close }) => {
tabIndex={0}
className="modallink"
onClick={() => {
dispatch(showHelpModal());
link('HELP', { target: 'fullscreen' });
close();
}}
>{t`Help`}</span>

View File

@ -38,6 +38,8 @@ function ChatMessage({
className += ' redtext';
}
console.log('RENDER MESSAGE');
const pArray = parseParagraph(msg);
return (

View File

@ -1,18 +1,17 @@
/*
*/
import React from 'react';
import { useDispatch } from 'react-redux';
import { t } from 'ttag';
import LogInForm from './LogInForm';
import { changeWindowType } from '../store/actions/windows';
import useLink from './hooks/link';
const logoStyle = {
marginRight: 5,
};
const LogInArea = ({ windowId }) => {
const dispatch = useDispatch();
const LogInArea = () => {
const link = useLink();
return (
<div style={{ textAlign: 'center' }}>
@ -23,7 +22,7 @@ const LogInArea = ({ windowId }) => {
<LogInForm />
<p
className="modallink"
onClick={() => dispatch(changeWindowType(windowId, 'FORGOT_PASSWORD'))}
onClick={() => link('FORGOT_PASSWORD')}
role="presentation"
>
{t`I forgot my Password.`}</p>
@ -71,9 +70,7 @@ const LogInArea = ({ windowId }) => {
<h2>{t`or register here:`}</h2>
<button
type="button"
onClick={
() => dispatch(changeWindowType(windowId, 'REGISTER'))
}
onClick={() => link('REGISTER')}
>
{t`Register`}
</button>

View File

@ -2,11 +2,16 @@
* UI for single-window popUp
*/
import React, { useCallback } from 'react';
import React, { useMemo } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { selectWindowType, selectWIndowArgs } from '../store/selectors/popup';
import { setWindowArgs, setWindowTitle } from '../store/actions/popup';
import {
setWindowArgs,
setWindowTitle,
changeWindowType,
} from '../store/actions/popup';
import WindowContext from './context/window';
import COMPONENTS from './windows';
const UIPopUp = () => {
@ -17,12 +22,13 @@ const UIPopUp = () => {
const dispatch = useDispatch();
const setArgs = useCallback(
(newArgs) => dispatch(setWindowArgs(newArgs),
), [dispatch]);
const setTitle = useCallback(
(title) => dispatch(setWindowTitle(title),
), [dispatch]);
const contextData = useMemo(() => ({
args,
setArgs: (newArgs) => dispatch(setWindowArgs(newArgs)),
setTitle: (title) => dispatch(setWindowTitle(title)),
// eslint-disable-next-line max-len
changeType: (newType, newTitel, newArgs) => dispatch(changeWindowType(newType, newTitel, newArgs)),
}), [args]);
return (
<div
@ -33,9 +39,11 @@ const UIPopUp = () => {
overflow: 'auto',
}}
>
{(windowType)
? <Content args={args} setArgs={setArgs} setTitle={setTitle} />
: <h1>Loading</h1>}
<WindowContext.Provider value={contextData}>
{(windowType)
? <Content />
: <h1>Loading</h1>}
</WindowContext.Provider>
</div>
);
};

View File

@ -8,7 +8,7 @@ import React, {
import { useSelector, useDispatch } from 'react-redux';
import { t } from 'ttag';
import { openPopUp } from '../core/popUps';
import { openWindowPopUp } from './hooks/link';
import {
moveWindow,
removeWindow,
@ -19,6 +19,7 @@ import {
focusWindow,
setWindowTitle,
setWindowArgs,
changeWindowType,
} from '../store/actions/windows';
import {
makeSelectWindowById,
@ -27,8 +28,9 @@ import {
selectShowWindows,
} from '../store/selectors/windows';
import useDrag from './hooks/drag';
import WindowContext from './context/window';
import COMPONENTS from './windows';
import popUpTypes, { buildPopUpUrl } from './windows/popUpAvailable';
import popUpTypes from './windows/popUpAvailable';
const Window = ({ id }) => {
const [render, setRender] = useState(false);
@ -46,12 +48,13 @@ const Window = ({ id }) => {
const dispatch = useDispatch();
const setArgs = useCallback(
(newArgs) => dispatch(setWindowArgs(id, newArgs),
), [dispatch]);
const setTitle = useCallback(
(title) => dispatch(setWindowTitle(id, title),
), [dispatch]);
const contextData = useMemo(() => ({
args,
setArgs: (newArgs) => dispatch(setWindowArgs(id, newArgs)),
setTitle: (title) => dispatch(setWindowTitle(id, title)),
// eslint-disable-next-line max-len
changeType: (newType, newTitel, newArgs) => dispatch(changeWindowType(id, newType, newTitel, newArgs)),
}), [id, args]);
const {
open,
@ -156,8 +159,8 @@ const Window = ({ id }) => {
{popUpTypes.includes(windowType) && (
<div
onClick={(evt) => {
openPopUp(
buildPopUpUrl(windowType, args),
openWindowPopUp(
windowType, args,
xPos, yPos, width, height,
);
close(evt);
@ -185,7 +188,9 @@ const Window = ({ id }) => {
className="modal-content"
key="content"
>
<Content args={args} setArgs={setArgs} setTitle={setTitle} />
<WindowContext.Provider value={contextData}>
<Content />
</WindowContext.Provider>
</div>
</div>
);
@ -229,8 +234,8 @@ const Window = ({ id }) => {
className="win-topbtn"
key="pobtnm"
onClick={(evt) => {
openPopUp(
buildPopUpUrl(windowType, args),
openWindowPopUp(
windowType, args,
xPos, yPos, width, height,
);
close(evt);
@ -268,7 +273,9 @@ const Window = ({ id }) => {
className="win-content"
key="content"
>
<Content args={args} setArgs={setArgs} setTitle={setTitle} />
<WindowContext.Provider value={contextData}>
<Content />
</WindowContext.Provider>
</div>
</div>
);

View File

@ -3,21 +3,19 @@
*/
import React from 'react';
import { useDispatch } from 'react-redux';
import { FaFlipboard } from 'react-icons/fa';
import { t } from 'ttag';
import { showCanvasSelectionModal } from '../../store/actions/windows';
import useLink from '../hooks/link';
const CanvasSwitchButton = () => {
const dispatch = useDispatch();
const link = useLink();
return (
<div
id="canvasbutton"
className="actionbuttons"
onClick={() => dispatch(showCanvasSelectionModal())}
onClick={() => link('CANVAS_SELECTION', { target: 'fullscreen' })}
role="button"
title={t`Canvas Selection`}
tabIndex={-1}

View File

@ -3,21 +3,19 @@
*/
import React from 'react';
import { useDispatch } from 'react-redux';
import { FaQuestion } from 'react-icons/fa';
import { t } from 'ttag';
import { showHelpModal } from '../../store/actions/windows';
import useLink from '../hooks/link';
const HelpButton = () => {
const dispatch = useDispatch();
const link = useLink();
return (
<div
id="helpbutton"
className="actionbuttons"
onClick={() => dispatch(showHelpModal())}
onClick={() => link('HELP', { target: 'fullscreen' })}
role="button"
title={t`Help`}
tabIndex={-1}

View File

@ -3,21 +3,19 @@
*/
import React from 'react';
import { useDispatch } from 'react-redux';
import { MdPerson } from 'react-icons/md';
import { t } from 'ttag';
import { showUserAreaModal } from '../../store/actions/windows';
import useLink from '../hooks/link';
const LogInButton = () => {
const dispatch = useDispatch();
const link = useLink();
return (
<div
id="loginbutton"
className="actionbuttons"
onClick={() => dispatch(showUserAreaModal())}
onClick={() => link('USERAREA', { target: 'fullscreen' })}
role="button"
title={t`User Area`}
tabIndex={-1}

View File

@ -3,21 +3,20 @@
*/
import React from 'react';
import { useDispatch } from 'react-redux';
import { FaCog } from 'react-icons/fa';
import { t } from 'ttag';
import { showSettingsModal } from '../../store/actions/windows';
import useLink from '../hooks/link';
const SettingsButton = () => {
const dispatch = useDispatch();
const link = useLink();
return (
<div
id="settingsbutton"
className="actionbuttons"
onClick={() => dispatch(showSettingsModal())}
onClick={() => link('SETTINGS', { target: 'fullscreen' })}
role="button"
title={t`Settings`}
tabIndex={-1}

View File

@ -0,0 +1,17 @@
/*
* context for window to provide window-specific
* state (args) and set stuff
*/
import { createContext } from 'react';
const WindowContext = createContext();
/*
* {
* args: object,
* setArgs: function,
* setTitle: function,
* changeType: function,
* }
*/
export default WindowContext;

View File

@ -0,0 +1,132 @@
/*
* function to link to window
*/
import { useCallback, useContext } from 'react';
import { useDispatch } from 'react-redux';
import { isPopUp, buildPopUpUrl } from '../windows/popUpAvailable';
import { openWindow } from '../../store/actions/windows';
import WindowContext from '../context/window';
function openPopUp(url, xPos, yPos, width, height) {
let left;
let top;
try {
if (window.innerWidth <= 604) {
width = window.innerWidth;
height = window.innerHeight;
left = window.top.screenX;
top = window.top.screenY;
} else {
left = Math.round(window.top.screenX + xPos);
top = Math.round(window.top.screenY + yPos);
}
if (Number.isNaN(left) || Number.isNaN(top)) {
throw new Error('NaN');
}
} catch {
left = 0;
top = 0;
}
try {
return window.open(
url,
url,
// eslint-disable-next-line max-len
`popup=yes,width=${width},height=${height},left=${left},top=${top},toolbar=no,status=no,directories=no,menubar=no`,
);
} catch {
return null;
}
}
export function openWindowPopUp(windowType, args, xPos, yPos, width, height) {
openPopUp(buildPopUpUrl(windowType, args), xPos, yPos, width, height);
}
function useLink() {
const dispatch = useDispatch();
const contextData = useContext(WindowContext);
return useCallback((windowType, options = {}) => {
const {
xPos = null,
yPos = null,
width = null,
height = null,
args = null,
} = options;
if (options.target === 'popup') {
// open as popup
openWindowPopUp(
windowType,
args,
xPos,
yPos,
width,
height,
);
return;
}
const {
title = '',
} = options;
const isMain = !isPopUp();
if (options.target === 'blank' && isMain) {
// open as new window
const {
cloneable = true,
} = options;
dispatch(openWindow(
windowType.toUpperCase(),
title,
args,
false,
cloneable,
xPos,
yPos,
width,
height,
));
return;
}
if (options.target === 'fullscreen' && isMain) {
// open as fullscreen modal
const {
cloneable = true,
} = options;
dispatch(openWindow(
windowType.toUpperCase(),
title,
args,
true,
cloneable,
xPos,
yPos,
width,
height,
));
return;
}
if (!contextData) {
// open within browser window
window.location.href = buildPopUpUrl(windowType, args);
return;
}
// open within window
contextData.changeType(windowType, title, args);
}, [contextData]);
}
export default useLink;

View File

@ -8,10 +8,10 @@ import { t } from 'ttag';
import CanvasItem from '../CanvasItem';
import { selectCanvas } from '../../store/actions';
import { changeWindowType } from '../../store/actions/windows';
import useLink from '../hooks/link';
const CanvasSelect = ({ windowId }) => {
const CanvasSelect = () => {
const [canvases, showHiddenCanvases, online] = useSelector((state) => [
state.canvas.canvases,
state.canvas.showHiddenCanvases,
@ -21,6 +21,8 @@ const CanvasSelect = ({ windowId }) => {
const selCanvas = useCallback((canvasId) => dispatch(selectCanvas(canvasId)),
[dispatch]);
const link = useLink();
return (
<div className="content">
<p>
@ -31,7 +33,7 @@ const CanvasSelect = ({ windowId }) => {
role="button"
tabIndex={0}
className="modallink"
onClick={() => dispatch(changeWindowType(windowId, 'ARCHIVE'))}
onClick={() => link('ARCHIVE')}
>{t`Archive`}</span>)
</p>
{

View File

@ -3,12 +3,14 @@
*/
import React, {
useRef, useLayoutEffect, useState, useEffect, useCallback,
useRef, useLayoutEffect, useState, useEffect, useCallback, useContext,
} from 'react';
import useStayScrolled from 'react-stay-scrolled';
import { useSelector, useDispatch } from 'react-redux';
import { t } from 'ttag';
import WindowContext from '../context/window';
import useLink from '../hooks/link';
import ContextMenu from '../contextmenus';
import ChatMessage from '../ChatMessage';
import ChannelDropDown from '../contextmenus/ChannelDropDown';
@ -17,19 +19,12 @@ import {
markChannelAsRead,
sendChatMessage,
} from '../../store/actions';
import {
showUserAreaModal,
} from '../../store/actions/windows';
import {
fetchChatMessages,
} from '../../store/actions/thunks';
const Chat = ({
args,
setArgs,
setTitle,
}) => {
const Chat = () => {
const listRef = useRef();
const targetRef = useRef();
const inputRef = useRef();
@ -44,10 +39,18 @@ const Chat = ({
const fetching = useSelector((state) => state.fetching.fetchingChat);
const { channels, messages, blocked } = useSelector((state) => state.chat);
const {
args,
setArgs,
setTitle,
} = useContext(WindowContext);
const {
chatChannel = 1,
} = args;
const link = useLink();
const setChannel = useCallback((cid) => {
dispatch(markChannelAsRead(cid));
setArgs({
@ -226,7 +229,7 @@ const Chat = ({
key="nlipt"
onClick={(evt) => {
evt.stopPropagation();
dispatch(showUserAreaModal());
link('USERAREA', { target: 'fullscreen' });
}}
style={{
textAlign: 'center',

View File

@ -2,12 +2,11 @@
* Form for requesting password-reset mail
*/
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { t } from 'ttag';
import { changeWindowType } from '../../store/actions/windows';
import { validateEMail } from '../../utils/validation';
import { requestNewPassword } from '../../store/actions/fetch';
import useLink from '../hooks/link';
function validate(email) {
const errors = [];
@ -22,14 +21,13 @@ const inputStyles = {
maxWidth: '35em',
};
const ForgotPassword = ({ windowId }) => {
const ForgotPassword = () => {
const [email, setEmail] = useState('');
const [submitting, setSubmitting] = useState(false);
const [success, setSuccess] = useState(false);
const [errors, setErrors] = useState([]);
const dispatch = useDispatch();
const back = () => dispatch(changeWindowType(windowId, 'USERAREA'));
const link = useLink();
const handleSubmit = async (evt) => {
evt.preventDefault();
@ -59,7 +57,7 @@ const ForgotPassword = ({ windowId }) => {
<p className="modalmessage">
{t`Sent you a mail with instructions to reset your password.`}
</p>
<button type="button" onClick={back}>
<button type="button" onClick={() => link('USERAREA')}>
Back
</button>
</div>
@ -85,7 +83,10 @@ const ForgotPassword = ({ windowId }) => {
<button type="submit">
{(submitting) ? '...' : t`Submit`}
</button>
<button type="button" onClick={back}>{t`Cancel`}</button>
<button
type="button"
onClick={() => link('USERAREA')}
>{t`Cancel`}</button>
</form>
</div>
);

View File

@ -5,14 +5,14 @@
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { t } from 'ttag';
import Captcha from '../Captcha';
import {
validateEMail, validateName, validatePassword,
} from '../../utils/validation';
import { requestRegistration } from '../../store/actions/fetch';
import { loginUser } from '../../store/actions';
import { changeWindowType } from '../../store/actions/windows';
import useLink from '../hooks/link';
function validate(name, email, password, confirmPassword) {
@ -30,7 +30,7 @@ function validate(name, email, password, confirmPassword) {
return errors;
}
const Register = ({ windowId }) => {
const Register = () => {
const [submitting, setSubmitting] = useState('');
const [errors, setErrors] = useState([]);
// used to be able to force Captcha rerender on error
@ -38,6 +38,8 @@ const Register = ({ windowId }) => {
const dispatch = useDispatch();
const link = useLink();
const handleSubmit = async (evt) => {
evt.preventDefault();
if (submitting) {
@ -73,7 +75,7 @@ const Register = ({ windowId }) => {
}
dispatch(loginUser(me));
dispatch(changeWindowType(windowId, 'USERAREA'));
link('USERAREA');
};
return (
@ -126,7 +128,7 @@ const Register = ({ windowId }) => {
</button>
<button
type="button"
onClick={() => dispatch(changeWindowType(windowId, 'USERAREA'))}
onClick={() => link('USERAREA')}
>
{t`Cancel`}
</button>

View File

@ -2,13 +2,14 @@
*
*/
import React, { Suspense, useCallback } from 'react';
import React, { Suspense, useCallback, useContext } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { t } from 'ttag';
import {
fetchStats,
} from '../../store/actions/thunks';
import WindowContext from '../context/window';
import useInterval from '../hooks/interval';
import LogInArea from '../LogInArea';
import Tabs from '../Tabs';
@ -20,10 +21,16 @@ const Converter = React.lazy(() => import(/* webpackChunkName: "converter" */ '.
// eslint-disable-next-line max-len
const Modtools = React.lazy(() => import(/* webpackChunkName: "modtools" */ '../Modtools'));
const UserArea = ({ args, setArgs, setTitle }) => {
const UserArea = () => {
const name = useSelector((state) => state.user.name);
const userlvl = useSelector((state) => state.user.userlvl);
const lastStatsFetch = useSelector((state) => state.ranks.lastFetch);
const {
args,
setArgs,
setTitle,
} = useContext(WindowContext);
const {
activeTab = t`Profile`,
} = args;
@ -33,6 +40,7 @@ const UserArea = ({ args, setArgs, setTitle }) => {
setArgs({
activeTab: label,
});
setTitle(label);
}, [setArgs]);
useInterval(() => {
@ -45,7 +53,7 @@ const UserArea = ({ args, setArgs, setTitle }) => {
<div style={{ textAlign: 'center' }}>
<Tabs activeTab={activeTab} setActiveTab={setActiveTab}>
<div label={t`Profile`}>
{(name) ? <UserAreaContent /> : <LogInArea windowId="1" />}
{(name) ? <UserAreaContent /> : <LogInArea />}
</div>
<div label={t`Ranking`}>
<Rankings />

View File

@ -7,6 +7,22 @@ export const argsTypes = {
CHAT: ['chatChannel'],
};
const availablePopups = [
'HELP',
'SETTINGS',
'USERAREA',
'CHAT',
'CANVAS_SELECTION',
'ARCHIVE',
'REGISTER',
'FORGOT_PASSWORD',
];
export function isPopUp() {
const fPath = window.location.pathname.split('/')[1];
return fPath && availablePopups.includes(fPath.toUpperCase());
}
export function buildPopUpUrl(windowType, argsIn) {
const args = { ...argsIn };
let path = `/${windowType.toLowerCase()}`;
@ -33,11 +49,4 @@ export function buildPopUpUrl(windowType, argsIn) {
return path;
}
export default [
'HELP',
'SETTINGS',
'USERAREA',
'CHAT',
'CANVAS_SELECTION',
'ARCHIVE',
];
export default availablePopups;

View File

@ -51,37 +51,4 @@ class PopUps {
}
const popUps = new PopUps();
export function openPopUp(url, xPos, yPos, width, height) {
let left;
let top;
try {
if (window.innerWidth <= 604) {
width = window.innerWidth;
height = window.innerHeight;
left = window.top.screenX;
top = window.top.screenY;
} else {
left = Math.round(window.top.screenX + xPos);
top = Math.round(window.top.screenY + yPos);
}
if (Number.isNaN(left) || Number.isNaN(top)) {
throw new Error('NaN');
}
} catch {
left = 0;
top = 0;
}
try {
return window.open(
url,
url,
// eslint-disable-next-line max-len
`popup=yes,width=${width},height=${height},left=${left},top=${top},toolbar=no,status=no,directories=no,menubar=no`,
);
} catch {
return null;
}
}
export default popUps;

View File

@ -72,7 +72,7 @@ persistStore(store, {}, () => {
}
});
(function () {
(function load() {
const onLoad = () => {
renderAppPopUp(document.getElementById('app'), store);
document.removeEventListener('DOMContentLoaded', onLoad);

View File

@ -15,3 +15,17 @@ export function setWindowTitle(title) {
title,
};
}
export function changeWindowType(
windowType,
title = '',
args = null,
) {
return {
type: 'CHANGE_WIN_TYPE',
windowType,
title,
args,
};
}

View File

@ -2,8 +2,6 @@
* Actions that are exclusively used by windows
*/
import { t } from 'ttag';
export function openWindow(
windowType,
title = '',
@ -40,35 +38,12 @@ export function setWindowArgs(
};
}
function showFullscreenWindow(modalType, title) {
return openWindow(
modalType,
title,
null,
true,
);
}
export function closeFullscreenWindows() {
return {
type: 'CLOSE_FULLSCREEN_WINS',
};
}
export function showSettingsModal() {
return showFullscreenWindow(
'SETTINGS',
'',
);
}
export function showUserAreaModal() {
return showFullscreenWindow(
'USERAREA',
'',
);
}
export function changeWindowType(
windowId,
windowType,
@ -92,40 +67,6 @@ export function setWindowTitle(windowId, title) {
};
}
export function showRegisterModal() {
return showFullscreenWindow(
'REGISTER',
t`Register New Account`,
);
}
export function showForgotPasswordModal() {
return showFullscreenWindow(
'FORGOT_PASSWORD',
t`Restore my Password`,
);
}
export function showHelpModal() {
return showFullscreenWindow(
'HELP',
t`Welcome to PixelPlanet.fun`,
);
}
export function showArchiveModal() {
return showFullscreenWindow(
'ARCHIVE',
t`Look at past Canvases`,
);
}
export function showCanvasSelectionModal() {
return showFullscreenWindow(
'CANVAS_SELECTION',
'',
);
}
export function closeWindow(windowId) {
return {
type: 'CLOSE_WIN',

View File

@ -42,7 +42,6 @@ const initialState = {
windowType: 'SETTINGS',
title: '',
args: {},
isPopup: window.opener && !window.opener.closed,
...getWinDataFromURL(),
};
@ -67,6 +66,22 @@ export default function popup(
};
}
case 'CHANGE_WIN_TYPE': {
const {
windowType,
title,
args,
} = action;
return {
...state,
windowType,
title,
args: {
...args,
},
};
}
default:
return state;
}

View File

@ -11,6 +11,7 @@ import gui from './reducers/gui';
import ranks from './reducers/ranks';
import chatRead from './reducers/chatRead';
import user from './reducers/user';
import canvas from './reducers/canvas';
import chat from './reducers/chat';
import fetching from './reducers/fetching';
@ -52,6 +53,7 @@ export default {
ranks: ranksPersist,
chatRead: chatReadPersist,
user,
canvas,
chat,
fetching,
};

View File

@ -18,7 +18,6 @@ import sharedReducers, {
* reducers
*/
import windows from './reducers/windows';
import canvas from './reducers/canvas';
import alert from './reducers/alert';
/*
@ -43,7 +42,6 @@ const windowsPersist = persistReducer({
const reducers = combineReducers({
...sharedReducers,
windows: windowsPersist,
canvas,
alert,
});

View File

@ -13,7 +13,6 @@ import thunk from 'redux-thunk';
* reducers
*/
import sharedReducers from './sharedReducers';
import canvas from './reducers/canvas';
import popup from './reducers/popup';
/*
@ -25,7 +24,6 @@ import title from './middleware/titlePopUp';
const reducers = combineReducers({
...sharedReducers,
canvas,
popup,
});