From a60242617d249a81a5d48c0b14516201fdf41b0f Mon Sep 17 00:00:00 2001 From: HF Date: Fri, 30 Apr 2021 19:59:38 +0200 Subject: [PATCH] move context menus and rewrite some components from react classes to hooks --- src/actions/fetch.js | 31 +- src/actions/index.js | 9 - src/actions/types.js | 1 - src/components/Captcha.jsx | 3 + src/components/HistorySelect.jsx | 393 ++++++------------ src/components/UI.jsx | 4 +- src/components/UserMessages.jsx | 213 +++------- .../{ => contextmenus}/ChannelContextMenu.jsx | 4 +- .../{ => contextmenus}/ChannelDropDown.jsx | 0 .../{ => contextmenus}/UserContextMenu.jsx | 2 +- src/components/windows/Chat.jsx | 2 +- src/core/utils.js | 8 + src/reducers/windows.js | 22 +- 13 files changed, 253 insertions(+), 439 deletions(-) rename src/components/{ => contextmenus}/ChannelContextMenu.jsx (97%) rename src/components/{ => contextmenus}/ChannelDropDown.jsx (100%) rename src/components/{ => contextmenus}/UserContextMenu.jsx (99%) 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) - ? ( -
- - - -
- ) - : 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) + && ( +
+ + + +
+ )} + { (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,