diff --git a/src/actions/fetch.js b/src/actions/fetch.js
index 56ede23..24f7bd4 100644
--- a/src/actions/fetch.js
+++ b/src/actions/fetch.js
@@ -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',
diff --git a/src/actions/index.js b/src/actions/index.js
index 0bd844d..3b7d676 100644
--- a/src/actions/index.js
+++ b/src/actions/index.js
@@ -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 {
diff --git a/src/actions/types.js b/src/actions/types.js
index 0d0f5d3..6bd21cc 100644
--- a/src/actions/types.js
+++ b/src/actions/types.js
@@ -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',
diff --git a/src/components/Captcha.jsx b/src/components/Captcha.jsx
index db5aedd..7835512 100644
--- a/src/components/Captcha.jsx
+++ b/src/components/Captcha.jsx
@@ -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,
diff --git a/src/components/HistorySelect.jsx b/src/components/HistorySelect.jsx
index 87f17aa..be3826c 100644
--- a/src/components/HistorySelect.jsx
+++ b/src/components/HistorySelect.jsx
@@ -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 (
-
-
{ this.dateSelect = ref; }}
- onChange={this.handleDateChange}
- />
-
- { (selectedTime)
- ? (
-
- this.changeTime(-1)}
- >←
-
- {times.map((value) => (
-
- {value}
-
- ))}
-
- this.changeTime(+1)}
- >→
-
- )
- : null }
- { (submitting) ?
Loading...
: null }
- { (!selectedDate && !submitting) ?
Select Date above
: null }
-
-
- );
- }
+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 (
+
+
+
+ { (!!times.length && historicalTime && !submitting)
+ && (
+
+ changeTime(-1)}
+ >←
+ setTime(selectedDate, evt.target.value)}
+ >
+ {times.map((value) => (
+
+ {value}
+
+ ))}
+
+ changeTime(+1)}
+ >→
+
+ )}
+ { (submitting) &&
{`${t`Loading`}...`}
}
+ { (!times.length && !submitting) &&
{t`Select Date above`}
}
+
+
+ );
+};
+
+export default React.memo(HistorySelect);
diff --git a/src/components/UI.jsx b/src/components/UI.jsx
index 074d6ea..1fedeee 100644
--- a/src/components/UI.jsx
+++ b/src/components/UI.jsx
@@ -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 = {
diff --git a/src/components/UserMessages.jsx b/src/components/UserMessages.jsx
index 27441ec..6bf6b14 100644
--- a/src/components/UserMessages.jsx
+++ b/src/components/UserMessages.jsx
@@ -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 (
+
+ {messages.includes('not_verified')
+ && messages.splice(messages.indexOf('not_verified'), 1)
+ && (
+
+ {t`Please verify your mail address
+ or your account could get deleted after a few days.`}
+ {(verifyAnswer)
+ ? (
+
+ {verifyAnswer}
+
+ )
+ : (
+ {
+ 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.`}
+
+ )}
+
+ )}
+ {messages.map((message) => {
+ if (message === 'not_verified') return null;
+ return
{message}
;
+ })}
+
+ );
+};
- 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 (
-
- {(messages.includes('not_verified')
- && messages.splice(messages.indexOf('not_verified'), 1))
- ? (
-
- {t`Please verify your mail address
- or your account could get deleted after a few days.`}
- {(verifyAnswer)
- ? (
-
- {verifyAnswer}
-
- )
- : (
-
- {t`Click here to request a new verification mail.`}
-
- )}
-
- ) : null}
- {(messages.includes('not_mc_verified')
- && messages.splice(messages.indexOf('not_mc_verified'), 1))
- ? (
-
- {t`You requested to link your mc account ${minecraftname}.`}
-
- {(linkAnswer)
- ? (
-
- {linkAnswer}
-
- )
- : (
-
- {
- this.submitMcLink(true);
- }}
- >
- {t`Accept`}
- or
- {
- this.submitMcLink(false);
- }}
- >
- {t`Deny`}
- .
-
- )}
-
- ) : null}
- {messages.map((message) => (
-
{message}
- ))}
-
- );
- }
-}
-
-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);
diff --git a/src/components/ChannelContextMenu.jsx b/src/components/contextmenus/ChannelContextMenu.jsx
similarity index 97%
rename from src/components/ChannelContextMenu.jsx
rename to src/components/contextmenus/ChannelContextMenu.jsx
index ffb7b8f..a8a9f40 100644
--- a/src/components/ChannelContextMenu.jsx
+++ b/src/components/contextmenus/ChannelContextMenu.jsx
@@ -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,
diff --git a/src/components/ChannelDropDown.jsx b/src/components/contextmenus/ChannelDropDown.jsx
similarity index 100%
rename from src/components/ChannelDropDown.jsx
rename to src/components/contextmenus/ChannelDropDown.jsx
diff --git a/src/components/UserContextMenu.jsx b/src/components/contextmenus/UserContextMenu.jsx
similarity index 99%
rename from src/components/UserContextMenu.jsx
rename to src/components/contextmenus/UserContextMenu.jsx
index d726b99..bc81182 100644
--- a/src/components/UserContextMenu.jsx
+++ b/src/components/contextmenus/UserContextMenu.jsx
@@ -15,7 +15,7 @@ import {
startDm,
setUserBlock,
setChatChannel,
-} from '../actions';
+} from '../../actions';
const UserContextMenu = () => {
const wrapperRef = useRef(null);
diff --git a/src/components/windows/Chat.jsx b/src/components/windows/Chat.jsx
index b8147ea..dc70093 100644
--- a/src/components/windows/Chat.jsx
+++ b/src/components/windows/Chat.jsx
@@ -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,
diff --git a/src/core/utils.js b/src/core/utils.js
index c82b1f7..44322bc 100644
--- a/src/core/utils.js
+++ b/src/core/utils.js
@@ -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(
diff --git a/src/reducers/windows.js b/src/reducers/windows.js
index 8c6140f..25d54e6 100644
--- a/src/reducers/windows.js
+++ b/src/reducers/windows.js
@@ -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,