From 80c2aa51b63d587ab5240b9a1fab5966a6e1d02f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 21 Feb 2020 10:41:33 +0000 Subject: [PATCH] Transition BaseAvatar to hooks Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .eslintignore.errorfiles | 1 - src/components/views/avatars/BaseAvatar.js | 311 ++++++++++----------- 2 files changed, 144 insertions(+), 168 deletions(-) diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles index 36b03b121c..e326f15002 100644 --- a/.eslintignore.errorfiles +++ b/.eslintignore.errorfiles @@ -7,7 +7,6 @@ src/components/structures/RoomView.js src/components/structures/ScrollPanel.js src/components/structures/SearchBox.js src/components/structures/UploadBar.js -src/components/views/avatars/BaseAvatar.js src/components/views/avatars/MemberAvatar.js src/components/views/create_room/RoomAlias.js src/components/views/dialogs/DeactivateAccountDialog.js diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.js index 4c34cee853..1b5b28e1e3 100644 --- a/src/components/views/avatars/BaseAvatar.js +++ b/src/components/views/avatars/BaseAvatar.js @@ -2,7 +2,7 @@ Copyright 2015, 2016 OpenMarket Ltd Copyright 2018 New Vector Ltd Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,206 +17,183 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react'; import PropTypes from 'prop-types'; -import createReactClass from 'create-react-class'; import * as AvatarLogic from '../../../Avatar'; import SettingsStore from "../../../settings/SettingsStore"; import AccessibleButton from '../elements/AccessibleButton'; import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import {useEventEmitter} from "../../../hooks/useEventEmitter"; -export default createReactClass({ - displayName: 'BaseAvatar', +const useImageUrl = ({url, urls, idName, name, defaultToInitialLetter}) => { + const [imageUrls, setUrls] = useState([]); + const [urlsIndex, setIndex] = useState(); - propTypes: { - name: PropTypes.string.isRequired, // The name (first initial used as default) - idName: PropTypes.string, // ID for generating hash colours - title: PropTypes.string, // onHover title text - url: PropTypes.string, // highest priority of them all, shortcut to set in urls[0] - urls: PropTypes.array, // [highest_priority, ... , lowest_priority] - width: PropTypes.number, - height: PropTypes.number, - // XXX resizeMethod not actually used. - resizeMethod: PropTypes.string, - defaultToInitialLetter: PropTypes.bool, // true to add default url - inputRef: PropTypes.oneOfType([ - // Either a function - PropTypes.func, - // Or the instance of a DOM native element - PropTypes.shape({ current: PropTypes.instanceOf(Element) }), - ]), - }, - - statics: { - contextType: MatrixClientContext, - }, - - getDefaultProps: function() { - return { - width: 40, - height: 40, - resizeMethod: 'crop', - defaultToInitialLetter: true, - }; - }, - - getInitialState: function() { - return this._getState(this.props); - }, - - componentDidMount() { - this.unmounted = false; - this.context.on('sync', this.onClientSync); - }, - - componentWillUnmount() { - this.unmounted = true; - this.context.removeListener('sync', this.onClientSync); - }, - - componentWillReceiveProps: function(nextProps) { - // work out if we need to call setState (if the image URLs array has changed) - const newState = this._getState(nextProps); - const newImageUrls = newState.imageUrls; - const oldImageUrls = this.state.imageUrls; - if (newImageUrls.length !== oldImageUrls.length) { - this.setState(newState); // detected a new entry - } else { - // check each one to see if they are the same - for (let i = 0; i < newImageUrls.length; i++) { - if (oldImageUrls[i] !== newImageUrls[i]) { - this.setState(newState); // detected a diff - break; - } - } + const onError = () => { + const nextIndex = urlsIndex + 1; + if (nextIndex < imageUrls.length) { + // try the next one + setIndex(nextIndex); } - }, + }; - onClientSync: function(syncState, prevState) { - if (this.unmounted) return; + const defaultImageUrl = useMemo(() => AvatarLogic.defaultAvatarUrlForString(idName || name), [idName, name]); - // Consider the client reconnected if there is no error with syncing. - // This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP. - const reconnected = syncState !== "ERROR" && prevState !== syncState; - if (reconnected && - // Did we fall back? - this.state.urlsIndex > 0 - ) { - // Start from the highest priority URL again - this.setState({ - urlsIndex: 0, - }); - } - }, - - _getState: function(props) { + useEffect(() => { // work out the full set of urls to try to load. This is formed like so: - // imageUrls: [ props.url, props.urls, default image ] + // imageUrls: [ props.url, ...props.urls, default image ] - let urls = []; + let _urls = []; if (!SettingsStore.getValue("lowBandwidth")) { - urls = props.urls || []; + _urls = urls || []; - if (props.url) { - urls.unshift(props.url); // put in urls[0] + if (url) { + _urls.unshift(url); // put in urls[0] } } - let defaultImageUrl = null; - if (props.defaultToInitialLetter) { - defaultImageUrl = AvatarLogic.defaultAvatarUrlForString( - props.idName || props.name, - ); - urls.push(defaultImageUrl); // lowest priority + if (defaultToInitialLetter) { + _urls.push(defaultImageUrl); // lowest priority } // deduplicate URLs - urls = Array.from(new Set(urls)); + _urls = Array.from(new Set(_urls)); - return { - imageUrls: urls, - defaultImageUrl: defaultImageUrl, - urlsIndex: 0, - }; - }, + setIndex(0); + setUrls(_urls); + }, [url, ...(urls || [])]); // eslint-disable-line react-hooks/exhaustive-deps - onError: function(ev) { - const nextIndex = this.state.urlsIndex + 1; - if (nextIndex < this.state.imageUrls.length) { - // try the next one - this.setState({ - urlsIndex: nextIndex, - }); + const cli = useContext(MatrixClientContext); + const onClientSync = useCallback((syncState, prevState) => { + // Consider the client reconnected if there is no error with syncing. + // This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP. + const reconnected = syncState !== "ERROR" && prevState !== syncState; + if (reconnected && urlsIndex > 0 ) { // Did we fall back? + // Start from the highest priority URL again + setIndex(0); } - }, + }, [urlsIndex]); + useEventEmitter(cli, "sync", onClientSync); - render: function() { - const imageUrl = this.state.imageUrls[this.state.urlsIndex]; + const imageUrl = imageUrls[urlsIndex]; + return [imageUrl, imageUrl === defaultImageUrl, onError]; +}; - const { - name, idName, title, url, urls, width, height, resizeMethod, - defaultToInitialLetter, onClick, inputRef, - ...otherProps - } = this.props; +const BaseAvatar = (props) => { + const { + name, + idName, + title, + url, + urls, + width=40, + height=40, + resizeMethod="crop", // eslint-disable-line no-unused-vars + defaultToInitialLetter=true, + onClick, + inputRef, + ...otherProps + } = props; - if (imageUrl === this.state.defaultImageUrl) { - const initialLetter = AvatarLogic.getInitialLetter(name); - const textNode = ( - - ); - const imgNode = ( - - ); - if (onClick != null) { - return ( - - { textNode } - { imgNode } - - ); - } else { - return ( - - { textNode } - { imgNode } - - ); - } - } + lineHeight: height + "px", + }} + > + { initialLetter } + + ); + const imgNode = ( + + ); + if (onClick != null) { return ( + > + { textNode } + { imgNode } + ); } else { return ( - + + { textNode } + { imgNode } + ); } - }, -}); + } + + if (onClick != null) { + return ( + + ); + } else { + return ( + + ); + } +}; + +BaseAvatar.displayName = "BaseAvatar"; + +BaseAvatar.propTypes = { + name: PropTypes.string.isRequired, // The name (first initial used as default) + idName: PropTypes.string, // ID for generating hash colours + title: PropTypes.string, // onHover title text + url: PropTypes.string, // highest priority of them all, shortcut to set in urls[0] + urls: PropTypes.array, // [highest_priority, ... , lowest_priority] + width: PropTypes.number, + height: PropTypes.number, + // XXX resizeMethod not actually used. + resizeMethod: PropTypes.string, + defaultToInitialLetter: PropTypes.bool, // true to add default url + onClick: PropTypes.func, + inputRef: PropTypes.oneOfType([ + // Either a function + PropTypes.func, + // Or the instance of a DOM native element + PropTypes.shape({ current: PropTypes.instanceOf(Element) }), + ]), +}; + +export default BaseAvatar;