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 1/3] 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; From 59f1657b4382a88ad87396af8852a6f9d3a0682a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 15 May 2020 00:25:19 +0100 Subject: [PATCH 2/3] Add jest-canvas-mock Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.json b/package.json index dda4a5a897..708ae91b0b 100644 --- a/package.json +++ b/package.json @@ -138,6 +138,7 @@ "flow-parser": "^0.57.3", "glob": "^5.0.14", "jest": "^24.9.0", + "jest-canvas-mock": "^2.2.0", "lolex": "^5.1.2", "matrix-mock-request": "^1.2.3", "matrix-react-test-utils": "^0.2.2", @@ -157,6 +158,7 @@ "testMatch": [ "/test/**/*-test.js" ], + "setupFiles": ["jest-canvas-mock"], "setupFilesAfterEnv": [ "/test/setupTests.js" ], From e79959a4eca99a7a4155851ec0860a01a55f371d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 15 May 2020 00:31:30 +0100 Subject: [PATCH 3/3] try fix tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/messages/TextualBody-test.js | 4 +-- yarn.lock | 25 +++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/test/components/views/messages/TextualBody-test.js b/test/components/views/messages/TextualBody-test.js index 59671327ce..364d239038 100644 --- a/test/components/views/messages/TextualBody-test.js +++ b/test/components/views/messages/TextualBody-test.js @@ -205,9 +205,9 @@ describe("", () => { expect(content.html()).toBe('' + 'Hey ' + '' + - 'Member' + + 'title="@member:domain.bla" alt="" aria-hidden="true" src="mxc://avatar.url/image.png">Member' + ''); }); }); diff --git a/yarn.lock b/yarn.lock index b0d3816dc4..0e9fe51490 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2470,6 +2470,11 @@ color-convert@^1.9.0: dependencies: color-name "1.1.3" +color-convert@~0.5.0: + version "0.5.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-0.5.3.tgz#bdb6c69ce660fadffe0b0007cc447e1b9f7282bd" + integrity sha1-vbbGnOZg+t/+CwAHzER+G59ygr0= + color-name@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" @@ -2723,6 +2728,11 @@ cssesc@^3.0.0: resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== +cssfontparser@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/cssfontparser/-/cssfontparser-1.2.1.tgz#f4022fc8f9700c68029d542084afbaf425a3f3e3" + integrity sha1-9AIvyPlwDGgCnVQghK+69CWj8+M= + cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0": version "0.3.8" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" @@ -4947,6 +4957,14 @@ istanbul-reports@^2.2.6: dependencies: html-escaper "^2.0.0" +jest-canvas-mock@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/jest-canvas-mock/-/jest-canvas-mock-2.2.0.tgz#45fbc58589c6ce9df50dc90bd8adce747cbdada7" + integrity sha512-DcJdchb7eWFZkt6pvyceWWnu3lsp5QWbUeXiKgEMhwB3sMm5qHM1GQhDajvJgBeiYpgKcojbzZ53d/nz6tXvJw== + dependencies: + cssfontparser "^1.2.1" + parse-color "^1.0.0" + jest-changed-files@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-24.9.0.tgz#08d8c15eb79a7fa3fc98269bc14b451ee82f8039" @@ -6404,6 +6422,13 @@ parse-asn1@^5.0.0: pbkdf2 "^3.0.3" safe-buffer "^5.1.1" +parse-color@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/parse-color/-/parse-color-1.0.0.tgz#7b748b95a83f03f16a94f535e52d7f3d94658619" + integrity sha1-e3SLlag/A/FqlPU15S1/PZRlhhk= + dependencies: + color-convert "~0.5.0" + parse-entities@^1.0.2, parse-entities@^1.1.0: version "1.2.2" resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-1.2.2.tgz#c31bf0f653b6661354f8973559cb86dd1d5edf50"