move context menus and rewrite some components from react classes to hooks

This commit is contained in:
HF 2021-04-30 19:59:38 +02:00
parent c11976cdca
commit a60242617d
13 changed files with 253 additions and 439 deletions

View File

@ -7,11 +7,13 @@
import { t } from 'ttag'; import { t } from 'ttag';
import { dateToString } from '../core/utils';
/* /*
* Adds customizeable timeout to fetch * Adds customizeable timeout to fetch
* defaults to 8s * defaults to 8s
*/ */
async function fetchWithTimeout(resource, options) { async function fetchWithTimeout(resource, options = {}) {
const { timeout = 8000 } = options; const { timeout = 8000 } = options;
const controller = new AbortController(); const controller = new AbortController();
@ -189,6 +191,26 @@ export async function requestSolveCaptcha(text) {
return res; return res;
} }
export async function requestHistoricalTimes(day, canvasId) {
try {
const date = dateToString(day);
const url = `api/history?day=${date}&id=${canvasId}`;
const response = await fetchWithTimeout(url);
if (response.status !== 200) {
return [];
}
const times = await response.json();
const parsedTimes = times
.map((a) => `${a.substr(0, 2)}:${a.substr(-2, 2)}`);
return [
'00:00',
...parsedTimes,
];
} catch {
return [];
}
}
export function requestPasswordChange(newPassword, password) { export function requestPasswordChange(newPassword, password) {
return makeAPIPOSTRequest( return makeAPIPOSTRequest(
'api/auth/change_passwd', 'api/auth/change_passwd',
@ -202,13 +224,6 @@ export async function requestResendVerify() {
); );
} }
export function requestMcLink(accepted) {
return makeAPIPOSTRequest(
'api/auth/mclink',
{ accepted },
);
}
export function requestNameChange(name) { export function requestNameChange(name) {
return makeAPIPOSTRequest( return makeAPIPOSTRequest(
'api/auth/change_name', 'api/auth/change_name',

View File

@ -421,15 +421,6 @@ export function setName(
}; };
} }
export function setMinecraftName(
minecraftname: string,
): Action {
return {
type: 'SET_MINECRAFT_NAME',
minecraftname,
};
}
export function setMailreg( export function setMailreg(
mailreg: boolean, mailreg: boolean,
): Action { ): Action {

View File

@ -132,7 +132,6 @@ export type Action =
| { type: 'LOGOUT' } | { type: 'LOGOUT' }
| { type: 'RECEIVE_STATS', totalRanking: Object, totalDailyRanking: Object } | { type: 'RECEIVE_STATS', totalRanking: Object, totalDailyRanking: Object }
| { type: 'SET_NAME', name: string } | { type: 'SET_NAME', name: string }
| { type: 'SET_MINECRAFT_NAME', minecraftname: string }
| { type: 'SET_MAILREG', mailreg: boolean } | { type: 'SET_MAILREG', mailreg: boolean }
| { type: 'REM_FROM_MESSAGES', message: string } | { type: 'REM_FROM_MESSAGES', message: string }
| { type: 'SHOW_CONTEXT_MENU', | { type: 'SHOW_CONTEXT_MENU',

View File

@ -6,6 +6,8 @@
* @flow * @flow
*/ */
/* eslint-disable jsx-a11y/no-autofocus */
import React, { useState } from 'react'; import React, { useState } from 'react';
import { t } from 'ttag'; import { t } from 'ttag';
@ -96,6 +98,7 @@ const Captcha = ({ callback, close }) => {
autoCorrect="off" autoCorrect="off"
autoCapitalize="off" autoCapitalize="off"
spellCheck="false" spellCheck="false"
autoFocus
style={{ style={{
width: '6em', width: '6em',
fontSize: 21, fontSize: 21,

View File

@ -2,205 +2,113 @@
* LogIn Form * LogIn Form
* @flow * @flow
*/ */
import React from 'react'; import React, {
import { connect } from 'react-redux'; useState, useCallback, useRef,
} from 'react';
import { useSelector, shallowEqual, useDispatch } from 'react-redux';
import { t } from 'ttag';
import type { State } from '../reducers'; import { dateToString } from '../core/utils';
import { selectHistoricalTime } from '../actions'; import { selectHistoricalTime } from '../actions';
import { requestHistoricalTimes } from '../actions/fetch';
function dateToString(date) {
// YYYY-MM-DD function stringToDate(dateString) {
const timeString = date.substr(0, 4) + date.substr(5, 2) + date.substr(8, 2); if (!dateString) return null;
// YYYYMMDD
return timeString;
}
function stringToDate(timeString) {
// YYYYMMDD // YYYYMMDD
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
const date = `${timeString.substr(0, 4)}-${timeString.substr(4, 2)}-${timeString.substr(6, 2)}`; return `${dateString.substr(0, 4)}-${dateString.substr(4, 2)}-${dateString.substr(6, 2)}`;
// YYYY-MM-DD
return date;
} }
async function getTimes(day, canvasId) { function stringToTime(timeString) {
try { if (!timeString) return null;
const date = dateToString(day); return `${timeString.substr(0, 2)}:${timeString.substr(2, 2)}`;
const response = await fetch(`./api/history?day=${date}&id=${canvasId}`);
if (response.status !== 200) {
return [];
}
const times = await response.json();
const parsedTimes = times
.map((a) => `${a.substr(0, 2)}:${a.substr(-2, 2)}`);
return [
'00:00',
...parsedTimes,
];
} catch {
return [];
}
} }
class HistorySelect extends React.Component { function getToday() {
constructor() {
super();
const date = new Date(); const date = new Date();
let day = date.getDate(); let day = date.getDate();
let month = date.getMonth() + 1; let month = date.getMonth() + 1;
if (month < 10) month = `0${month}`; if (month < 10) month = `0${month}`;
if (day < 10) day = `0${day}`; if (day < 10) day = `0${day}`;
const max = `${date.getFullYear()}-${month}-${day}`; return `${date.getFullYear()}-${month}-${day}`;
this.state = {
submitting: false,
selectedDate: null,
selectedTime: null,
times: [],
max,
};
this.dateSelect = null;
this.handleDateChange = this.handleDateChange.bind(this);
this.handleTimeChange = this.handleTimeChange.bind(this);
this.changeTime = this.changeTime.bind(this);
} }
async handleDateChange(evt) { const HistorySelect = () => {
const { const dateSelect = useRef(null);
submitting,
} = this.state;
const [submitting, setSubmitting] = useState(false);
const [times, setTimes] = useState([]);
const [max] = useState(getToday());
const [
canvasId,
canvasStartDate,
historicalDate,
historicalTime,
] = useSelector((state) => [
state.canvas.canvasId,
state.canvas.canvasStartDate,
state.canvas.historicalDate,
state.canvas.historicalTime,
], shallowEqual);
const dispatch = useDispatch();
const setTime = useCallback((date, time) => {
const timeString = time.substr(0, 2) + time.substr(-2, 2);
const dateString = dateToString(date);
dispatch(selectHistoricalTime(dateString, timeString));
}, [dispatch]);
const handleDateChange = useCallback(async (evt) => {
if (submitting) { if (submitting) {
return; return;
} }
setSubmitting(true);
this.setState({
submitting: true,
times: [],
selectedTime: null,
});
const {
canvasId,
setTime,
} = this.props;
const date = evt.target.value; const date = evt.target.value;
const times = await getTimes(date, canvasId); const newTimes = await requestHistoricalTimes(date, canvasId);
if (times.length === 0) { if (newTimes && newTimes.length) {
this.setState({ setTimes(newTimes);
submitting: false, setTime(date, newTimes[0]);
selectedDate: null,
});
return;
}
setTime(date, times[0]);
this.setState({
submitting: false,
selectedDate: date,
selectedTime: (times) ? times[0] : null,
times,
});
} }
setSubmitting(false);
}, [submitting, times]);
handleTimeChange(evt) { const changeTime = useCallback(async (diff) => {
const { if (!times.length
setTime, || !dateSelect || !dateSelect.current || !dateSelect.current.value) {
} = this.props;
const {
selectedDate,
} = this.state;
const selectedTime = evt.target.value;
this.setState({
selectedTime,
});
setTime(selectedDate, selectedTime);
}
async changeTime(diff) {
let {
times,
selectedDate,
selectedTime,
} = this.state;
if (!selectedTime || times.length === 0) {
return; return;
} }
const { let newTimes = times;
setTime, let newPos = times.indexOf(stringToTime(historicalTime)) + diff;
canvasId, let newSelectedDate = dateSelect.current.value;
} = this.props;
let newPos = times.indexOf(selectedTime) + diff;
if (newPos >= times.length || newPos < 0) { if (newPos >= times.length || newPos < 0) {
setSubmitting(true);
if (newPos < 0) { if (newPos < 0) {
this.dateSelect.stepDown(1); dateSelect.current.stepDown(1);
} else { } else {
this.dateSelect.stepUp(1); dateSelect.current.stepUp(1);
} }
selectedDate = this.dateSelect.value; newSelectedDate = dateSelect.current.value;
this.setState({ newTimes = await requestHistoricalTimes(
submitting: true, newSelectedDate,
times: [], canvasId,
selectedTime: null, );
}); setSubmitting(false);
times = await getTimes(selectedDate, canvasId); if (!newTimes || !newTimes.length) {
if (times.length === 0) {
this.setState({
submitting: false,
selectedDate: null,
});
return; return;
} }
this.setState({ newPos = (newPos < 0) ? (newTimes.length - 1) : 0;
submitting: false,
selectedDate,
});
newPos = (newPos < 0) ? (times.length - 1) : 0;
} }
selectedTime = times[newPos]; setTimes(newTimes);
this.setState({ setTime(newSelectedDate, newTimes[newPos]);
times, }, [historicalTime, times, submitting]);
selectedTime,
});
setTime(selectedDate, selectedTime);
}
render() { const selectedDate = stringToDate(historicalDate);
const { const selectedTime = stringToTime(historicalTime);
canvasStartDate,
} = this.props;
const {
submitting,
max,
} = this.state;
let {
times,
selectedDate,
selectedTime,
} = this.state;
if (!selectedDate) {
const {
historicalDate,
historicalTime,
} = this.props;
if (historicalDate && historicalTime) {
selectedDate = stringToDate(historicalDate);
selectedTime = historicalTime;
times = [historicalTime];
this.setState({
selectedDate,
selectedTime,
times,
});
}
}
return ( return (
<div id="historyselect"> <div id="historyselect">
@ -210,21 +118,21 @@ class HistorySelect extends React.Component {
value={selectedDate} value={selectedDate}
min={canvasStartDate} min={canvasStartDate}
max={max} max={max}
ref={(ref) => { this.dateSelect = ref; }} ref={dateSelect}
onChange={this.handleDateChange} onChange={handleDateChange}
/> />
<div> <div>
{ (selectedTime) { (!!times.length && historicalTime && !submitting)
? ( && (
<div> <div>
<button <button
type="button" type="button"
className="hsar" className="hsar"
onClick={() => this.changeTime(-1)} onClick={() => changeTime(-1)}
></button> ></button>
<select <select
value={selectedTime} value={selectedTime}
onChange={this.handleTimeChange} onChange={(evt) => setTime(selectedDate, evt.target.value)}
> >
{times.map((value) => ( {times.map((value) => (
<option <option
@ -238,40 +146,15 @@ class HistorySelect extends React.Component {
<button <button
type="button" type="button"
className="hsar" className="hsar"
onClick={() => this.changeTime(+1)} onClick={() => changeTime(+1)}
></button> ></button>
</div> </div>
) )}
: null } { (submitting) && <p>{`${t`Loading`}...`}</p> }
{ (submitting) ? <p>Loading...</p> : null } { (!times.length && !submitting) && <p>{t`Select Date above`}</p> }
{ (!selectedDate && !submitting) ? <p>Select Date above</p> : null }
</div> </div>
</div> </div>
); );
}
}
function mapDispatchToProps(dispatch) {
return {
setTime(date: string, time: string) {
const timeString = time.substr(0, 2) + time.substr(-2, 2);
const dateString = dateToString(date);
dispatch(selectHistoricalTime(dateString, timeString));
},
}; };
}
function mapStateToProps(state: State) { export default React.memo(HistorySelect);
const {
canvasId,
canvasStartDate,
historicalDate,
historicalTime,
} = state.canvas;
return {
canvasId, canvasStartDate, historicalDate, historicalTime,
};
}
export default connect(mapStateToProps, mapDispatchToProps)(HistorySelect);

View File

@ -15,8 +15,8 @@ import Palette from './Palette';
import Alert from './Alert'; import Alert from './Alert';
import HistorySelect from './HistorySelect'; import HistorySelect from './HistorySelect';
import Mobile3DControls from './Mobile3DControls'; import Mobile3DControls from './Mobile3DControls';
import UserContextMenu from './UserContextMenu'; import UserContextMenu from './contextmenus/UserContextMenu';
import ChannelContextMenu from './ChannelContextMenu'; import ChannelContextMenu from './contextmenus/ChannelContextMenu';
const CONTEXT_MENUS = { const CONTEXT_MENUS = {

View File

@ -2,85 +2,28 @@
* Messages on top of UserArea * Messages on top of UserArea
* @flow * @flow
*/ */
import React from 'react'; import React, { useState } from 'react';
import { connect } from 'react-redux'; import { useSelector } from 'react-redux';
import { t } from 'ttag'; import { t } from 'ttag';
import { setMinecraftName, remFromMessages } from '../actions'; import { requestResendVerify } from '../actions/fetch';
import { requestResendVerify, requestMcLink } from '../actions/fetch';
class UserMessages extends React.Component { const UserMessages = () => {
constructor() { const [resentVerify, setResentVerify] = useState(false);
super(); const [verifyAnswer, setVerifyAnswer] = useState(null);
this.state = {
resentVerify: false,
sentLink: false,
verifyAnswer: null,
linkAnswer: null,
};
this.submitResendVerify = this.submitResendVerify.bind(this); const messages = useSelector((state) => state.user.messages);
this.submitMcLink = this.submitMcLink.bind(this);
if (!messages) {
return null;
} }
async submitResendVerify() {
const { resentVerify } = this.state;
if (resentVerify) return;
this.setState({
resentVerify: true,
});
const { errors } = await requestResendVerify();
const verifyAnswer = (errors)
? errors[0]
: t`A new verification mail is getting sent to you.`;
this.setState({
verifyAnswer,
});
}
async submitMcLink(accepted) {
const { sentLink } = this.state;
if (sentLink) return;
this.setState({
sentLink: true,
});
const { errors } = await requestMcLink(accepted);
if (errors) {
this.setState({
linkAnswer: errors[0],
});
return;
}
const { setMCName, remFromUserMessages } = this.props;
if (!accepted) {
setMCName(null);
}
remFromUserMessages('not_mc_verified');
this.setState({
linkAnswer: (accepted)
? t`You successfully linked your mc account.`
: t`You denied.`,
});
}
render() {
const { messages: messagesr } = this.props;
if (!messagesr) return null;
// state variable is not allowed to be changed, make copy
const messages = [...messagesr];
const { verifyAnswer, linkAnswer } = this.state;
const { minecraftname } = this.props;
return ( return (
<div style={{ paddingLeft: '5%', paddingRight: '5%' }}> <div style={{ paddingLeft: '5%', paddingRight: '5%' }}>
{(messages.includes('not_verified') {messages.includes('not_verified')
&& messages.splice(messages.indexOf('not_verified'), 1)) && messages.splice(messages.indexOf('not_verified'), 1)
? ( && (
<p className="usermessages"> <p className="usermessages">
{t`Please verify your mail address&nbsp; {t`Please verify your mail address&nbsp;
or your account could get deleted after a few days.`}&nbsp; or your account could get deleted after a few days.`}&nbsp;
@ -97,75 +40,27 @@ class UserMessages extends React.Component {
role="button" role="button"
tabIndex={-1} tabIndex={-1}
className="modallink" className="modallink"
onClick={this.submitResendVerify} onClick={async () => {
if (resentVerify) return;
setResentVerify(true);
const { errors } = await requestResendVerify();
const answer = (errors)
? errors[0]
: t`A new verification mail is getting sent to you.`;
setVerifyAnswer(answer);
}}
> >
{t`Click here to request a new verification mail.`} {t`Click here to request a new verification mail.`}
</span> </span>
)} )}
</p> </p>
) : null}
{(messages.includes('not_mc_verified')
&& messages.splice(messages.indexOf('not_mc_verified'), 1))
? (
<p className="usermessages">
{t`You requested to link your mc account ${minecraftname}.`}
&nbsp;
{(linkAnswer)
? (
<span
className="modallink"
>
{linkAnswer}
</span>
)
: (
<span>
<span
role="button"
tabIndex={-1}
className="modallink"
onClick={() => {
this.submitMcLink(true);
}}
>
{t`Accept`}
</span>&nbsp;or&nbsp;
<span
role="button"
tabIndex={-1}
className="modallink"
onClick={() => {
this.submitMcLink(false);
}}
>
{t`Deny`}
</span>.
</span>
)} )}
</p> {messages.map((message) => {
) : null} if (message === 'not_verified') return null;
{messages.map((message) => ( return <p className="usermessages" key={message}>{message}</p>;
<p className="usermessages" key={message}>{message}</p> })}
))}
</div> </div>
); );
}
}
function mapDispatchToProps(dispatch) {
return {
setMCName(minecraftname) {
dispatch(setMinecraftName(minecraftname));
},
remFromUserMessages(message) {
dispatch(remFromMessages(message));
},
}; };
}
function mapStateToProps(state: State) { export default React.memo(UserMessages);
const { messages, minecraftname } = state.user;
return { messages, minecraftname };
}
export default connect(mapStateToProps, mapDispatchToProps)(UserMessages);

View File

@ -14,8 +14,8 @@ import {
setLeaveChannel, setLeaveChannel,
muteChatChannel, muteChatChannel,
unmuteChatChannel, unmuteChatChannel,
} from '../actions'; } from '../../actions';
import type { State } from '../reducers'; import type { State } from '../../reducers';
const ChannelContextMenu = ({ const ChannelContextMenu = ({
xPos, xPos,

View File

@ -15,7 +15,7 @@ import {
startDm, startDm,
setUserBlock, setUserBlock,
setChatChannel, setChatChannel,
} from '../actions'; } from '../../actions';
const UserContextMenu = () => { const UserContextMenu = () => {
const wrapperRef = useRef(null); const wrapperRef = useRef(null);

View File

@ -11,7 +11,7 @@ import { useSelector, useDispatch } from 'react-redux';
import { t } from 'ttag'; import { t } from 'ttag';
import ChatMessage from '../ChatMessage'; import ChatMessage from '../ChatMessage';
import ChannelDropDown from '../ChannelDropDown'; import ChannelDropDown from '../contextmenus/ChannelDropDown';
import { import {
showUserAreaModal, showUserAreaModal,

View File

@ -38,6 +38,14 @@ export function clamp(n: number, min: number, max: number): number {
return Math.max(min, Math.min(n, max)); return Math.max(min, Math.min(n, max));
} }
/*
* convert YYYY-MM-DD to YYYYMMDD
*/
export function dateToString(date: string) {
// YYYY-MM-DD
return date.substr(0, 4) + date.substr(5, 2) + date.substr(8, 2);
}
// z is assumed to be height here // z is assumed to be height here
// in ui and rendeer, y is height // in ui and rendeer, y is height
export function getChunkOfPixel( export function getChunkOfPixel(

View File

@ -501,7 +501,7 @@ export default function windows(
const yMax = height - SCREEN_MARGIN_S; const yMax = height - SCREEN_MARGIN_S;
let modified = false; let modified = false;
const newWindows = []; let newWindows = [];
for (let i = 0; i < state.windows.length; i += 1) { for (let i = 0; i < state.windows.length; i += 1) {
const win = state.windows[i]; const win = state.windows[i];
const { const {
@ -525,6 +525,26 @@ export default function windows(
} }
} }
if (action.type === 'RECEIVE_ME') {
const args = { ...state.args };
newWindows = newWindows.filter((win) => {
if (win.open) return true;
// eslint-disable-next-line no-console
console.log(
`Cleaning up window from previous session: ${win.windowId}`,
);
delete args[win.windowId];
return false;
});
return {
...state,
showWindows: true,
windows: newWindows,
args,
};
}
return { return {
...state, ...state,
showWindows: true, showWindows: true,