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 { dateToString } from '../core/utils';
/*
* Adds customizeable timeout to fetch
* defaults to 8s
*/
async function fetchWithTimeout(resource, options) {
async function fetchWithTimeout(resource, options = {}) {
const { timeout = 8000 } = options;
const controller = new AbortController();
@ -189,6 +191,26 @@ export async function requestSolveCaptcha(text) {
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) {
return makeAPIPOSTRequest(
'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) {
return makeAPIPOSTRequest(
'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(
mailreg: boolean,
): Action {

View File

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

View File

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

View File

@ -2,276 +2,159 @@
* LogIn Form
* @flow
*/
import React from 'react';
import { connect } from 'react-redux';
import React, {
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 { requestHistoricalTimes } from '../actions/fetch';
function dateToString(date) {
// YYYY-MM-DD
const timeString = date.substr(0, 4) + date.substr(5, 2) + date.substr(8, 2);
// YYYYMMDD
return timeString;
}
function stringToDate(timeString) {
function stringToDate(dateString) {
if (!dateString) return null;
// YYYYMMDD
// eslint-disable-next-line max-len
const date = `${timeString.substr(0, 4)}-${timeString.substr(4, 2)}-${timeString.substr(6, 2)}`;
// YYYY-MM-DD
return date;
return `${dateString.substr(0, 4)}-${dateString.substr(4, 2)}-${dateString.substr(6, 2)}`;
}
async function getTimes(day, canvasId) {
try {
const date = dateToString(day);
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 [];
}
function stringToTime(timeString) {
if (!timeString) return null;
return `${timeString.substr(0, 2)}:${timeString.substr(2, 2)}`;
}
class HistorySelect extends React.Component {
constructor() {
super();
const date = new Date();
let day = date.getDate();
let month = date.getMonth() + 1;
if (month < 10) month = `0${month}`;
if (day < 10) day = `0${day}`;
const max = `${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 {
submitting,
} = this.state;
if (submitting) {
return;
}
this.setState({
submitting: true,
times: [],
selectedTime: null,
});
const {
canvasId,
setTime,
} = this.props;
const date = evt.target.value;
const times = await getTimes(date, canvasId);
if (times.length === 0) {
this.setState({
submitting: false,
selectedDate: null,
});
return;
}
setTime(date, times[0]);
this.setState({
submitting: false,
selectedDate: date,
selectedTime: (times) ? times[0] : null,
times,
});
}
handleTimeChange(evt) {
const {
setTime,
} = 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;
}
const {
setTime,
canvasId,
} = this.props;
let newPos = times.indexOf(selectedTime) + diff;
if (newPos >= times.length || newPos < 0) {
if (newPos < 0) {
this.dateSelect.stepDown(1);
} else {
this.dateSelect.stepUp(1);
}
selectedDate = this.dateSelect.value;
this.setState({
submitting: true,
times: [],
selectedTime: null,
});
times = await getTimes(selectedDate, canvasId);
if (times.length === 0) {
this.setState({
submitting: false,
selectedDate: null,
});
return;
}
this.setState({
submitting: false,
selectedDate,
});
newPos = (newPos < 0) ? (times.length - 1) : 0;
}
selectedTime = times[newPos];
this.setState({
times,
selectedTime,
});
setTime(selectedDate, selectedTime);
}
render() {
const {
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 (
<div id="historyselect">
<input
type="date"
requiredPattern="\d{4}-\d{2}-\d{2}"
value={selectedDate}
min={canvasStartDate}
max={max}
ref={(ref) => { this.dateSelect = ref; }}
onChange={this.handleDateChange}
/>
<div>
{ (selectedTime)
? (
<div>
<button
type="button"
className="hsar"
onClick={() => this.changeTime(-1)}
></button>
<select
value={selectedTime}
onChange={this.handleTimeChange}
>
{times.map((value) => (
<option
value={value}
selected={value === selectedTime}
>
{value}
</option>
))}
</select>
<button
type="button"
className="hsar"
onClick={() => this.changeTime(+1)}
></button>
</div>
)
: null }
{ (submitting) ? <p>Loading...</p> : null }
{ (!selectedDate && !submitting) ? <p>Select Date above</p> : null }
</div>
</div>
);
}
function getToday() {
const date = new Date();
let day = date.getDate();
let month = date.getMonth() + 1;
if (month < 10) month = `0${month}`;
if (day < 10) day = `0${day}`;
return `${date.getFullYear()}-${month}-${day}`;
}
const HistorySelect = () => {
const dateSelect = useRef(null);
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));
},
};
}
const [submitting, setSubmitting] = useState(false);
const [times, setTimes] = useState([]);
const [max] = useState(getToday());
function mapStateToProps(state: State) {
const {
const [
canvasId,
canvasStartDate,
historicalDate,
historicalTime,
} = state.canvas;
return {
canvasId, canvasStartDate, historicalDate, historicalTime,
};
}
] = useSelector((state) => [
state.canvas.canvasId,
state.canvas.canvasStartDate,
state.canvas.historicalDate,
state.canvas.historicalTime,
], shallowEqual);
export default connect(mapStateToProps, mapDispatchToProps)(HistorySelect);
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) {
return;
}
setSubmitting(true);
const date = evt.target.value;
const newTimes = await requestHistoricalTimes(date, canvasId);
if (newTimes && newTimes.length) {
setTimes(newTimes);
setTime(date, newTimes[0]);
}
setSubmitting(false);
}, [submitting, times]);
const changeTime = useCallback(async (diff) => {
if (!times.length
|| !dateSelect || !dateSelect.current || !dateSelect.current.value) {
return;
}
let newTimes = times;
let newPos = times.indexOf(stringToTime(historicalTime)) + diff;
let newSelectedDate = dateSelect.current.value;
if (newPos >= times.length || newPos < 0) {
setSubmitting(true);
if (newPos < 0) {
dateSelect.current.stepDown(1);
} else {
dateSelect.current.stepUp(1);
}
newSelectedDate = dateSelect.current.value;
newTimes = await requestHistoricalTimes(
newSelectedDate,
canvasId,
);
setSubmitting(false);
if (!newTimes || !newTimes.length) {
return;
}
newPos = (newPos < 0) ? (newTimes.length - 1) : 0;
}
setTimes(newTimes);
setTime(newSelectedDate, newTimes[newPos]);
}, [historicalTime, times, submitting]);
const selectedDate = stringToDate(historicalDate);
const selectedTime = stringToTime(historicalTime);
return (
<div id="historyselect">
<input
type="date"
requiredPattern="\d{4}-\d{2}-\d{2}"
value={selectedDate}
min={canvasStartDate}
max={max}
ref={dateSelect}
onChange={handleDateChange}
/>
<div>
{ (!!times.length && historicalTime && !submitting)
&& (
<div>
<button
type="button"
className="hsar"
onClick={() => changeTime(-1)}
></button>
<select
value={selectedTime}
onChange={(evt) => setTime(selectedDate, evt.target.value)}
>
{times.map((value) => (
<option
value={value}
selected={value === selectedTime}
>
{value}
</option>
))}
</select>
<button
type="button"
className="hsar"
onClick={() => changeTime(+1)}
></button>
</div>
)}
{ (submitting) && <p>{`${t`Loading`}...`}</p> }
{ (!times.length && !submitting) && <p>{t`Select Date above`}</p> }
</div>
</div>
);
};
export default React.memo(HistorySelect);

View File

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

View File

@ -2,170 +2,65 @@
* Messages on top of UserArea
* @flow
*/
import React from 'react';
import { connect } from 'react-redux';
import React, { useState } from 'react';
import { useSelector } from 'react-redux';
import { t } from 'ttag';
import { setMinecraftName, remFromMessages } from '../actions';
import { requestResendVerify, requestMcLink } from '../actions/fetch';
import { requestResendVerify } from '../actions/fetch';
class UserMessages extends React.Component {
constructor() {
super();
this.state = {
resentVerify: false,
sentLink: false,
verifyAnswer: null,
linkAnswer: null,
};
const UserMessages = () => {
const [resentVerify, setResentVerify] = useState(false);
const [verifyAnswer, setVerifyAnswer] = useState(null);
this.submitResendVerify = this.submitResendVerify.bind(this);
this.submitMcLink = this.submitMcLink.bind(this);
const messages = useSelector((state) => state.user.messages);
if (!messages) {
return null;
}
async submitResendVerify() {
const { resentVerify } = this.state;
if (resentVerify) return;
this.setState({
resentVerify: true,
});
return (
<div style={{ paddingLeft: '5%', paddingRight: '5%' }}>
{messages.includes('not_verified')
&& messages.splice(messages.indexOf('not_verified'), 1)
&& (
<p className="usermessages">
{t`Please verify your mail address&nbsp;
or your account could get deleted after a few days.`}&nbsp;
{(verifyAnswer)
? (
<span
className="modallink"
>
{verifyAnswer}
</span>
)
: (
<span
role="button"
tabIndex={-1}
className="modallink"
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.`}
</span>
)}
</p>
)}
{messages.map((message) => {
if (message === 'not_verified') return null;
return <p className="usermessages" key={message}>{message}</p>;
})}
</div>
);
};
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 (
<div style={{ paddingLeft: '5%', paddingRight: '5%' }}>
{(messages.includes('not_verified')
&& messages.splice(messages.indexOf('not_verified'), 1))
? (
<p className="usermessages">
{t`Please verify your mail address&nbsp;
or your account could get deleted after a few days.`}&nbsp;
{(verifyAnswer)
? (
<span
className="modallink"
>
{verifyAnswer}
</span>
)
: (
<span
role="button"
tabIndex={-1}
className="modallink"
onClick={this.submitResendVerify}
>
{t`Click here to request a new verification mail.`}
</span>
)}
</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>
) : null}
{messages.map((message) => (
<p className="usermessages" key={message}>{message}</p>
))}
</div>
);
}
}
function mapDispatchToProps(dispatch) {
return {
setMCName(minecraftname) {
dispatch(setMinecraftName(minecraftname));
},
remFromUserMessages(message) {
dispatch(remFromMessages(message));
},
};
}
function mapStateToProps(state: State) {
const { messages, minecraftname } = state.user;
return { messages, minecraftname };
}
export default connect(mapStateToProps, mapDispatchToProps)(UserMessages);
export default React.memo(UserMessages);

View File

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

View File

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

View File

@ -11,7 +11,7 @@ import { useSelector, useDispatch } from 'react-redux';
import { t } from 'ttag';
import ChatMessage from '../ChatMessage';
import ChannelDropDown from '../ChannelDropDown';
import ChannelDropDown from '../contextmenus/ChannelDropDown';
import {
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));
}
/*
* 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
// in ui and rendeer, y is height
export function getChunkOfPixel(

View File

@ -501,7 +501,7 @@ export default function windows(
const yMax = height - SCREEN_MARGIN_S;
let modified = false;
const newWindows = [];
let newWindows = [];
for (let i = 0; i < state.windows.length; i += 1) {
const win = state.windows[i];
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 {
...state,
showWindows: true,