diff --git a/react/features/always-on-top/AlwaysOnTop.js b/react/features/always-on-top/AlwaysOnTop.js index dffccbcf90..31c74d9a10 100644 --- a/react/features/always-on-top/AlwaysOnTop.js +++ b/react/features/always-on-top/AlwaysOnTop.js @@ -2,6 +2,11 @@ import React, { Component } from 'react'; +// We need to reference these files directly to avoid loading things that are not available +// in this environment (e.g. JitsiMeetJS or interfaceConfig) +import StatelessAvatar from '../base/avatar/components/web/StatelessAvatar'; +import { getInitials } from '../base/avatar/functions'; + import Toolbar from './Toolbar'; const { api } = window.alwaysOnTop; @@ -17,6 +22,7 @@ const TOOLBAR_TIMEOUT = 4000; type State = { avatarURL: string, displayName: string, + formattedDisplayName: string, isVideoDisplayed: boolean, visible: boolean }; @@ -42,6 +48,7 @@ export default class AlwaysOnTop extends Component<*, State> { this.state = { avatarURL: '', displayName: '', + formattedDisplayName: '', isVideoDisplayed: true, visible: true }; @@ -78,10 +85,15 @@ export default class AlwaysOnTop extends Component<*, State> { * * @returns {void} */ - _displayNameChangedListener({ formattedDisplayName, id }) { + _displayNameChangedListener({ displayname, formattedDisplayName, id }) { if (api._getOnStageParticipant() === id - && formattedDisplayName !== this.state.displayName) { - this.setState({ displayName: formattedDisplayName }); + && (formattedDisplayName !== this.state.formattedDisplayName + || displayname !== this.state.displayName)) { + // I think the API has a typo using lowercase n for the displayname + this.setState({ + displayName: displayname, + formattedDisplayName + }); } } @@ -112,12 +124,14 @@ export default class AlwaysOnTop extends Component<*, State> { _largeVideoChangedListener() { const userID = api._getOnStageParticipant(); const avatarURL = api.getAvatarURL(userID); - const displayName = api._getFormattedDisplayName(userID); + const displayName = api.getDisplayName(userID); + const formattedDisplayName = api._getFormattedDisplayName(userID); const isVideoDisplayed = Boolean(api._getLargeVideo()); this.setState({ avatarURL, displayName, + formattedDisplayName, isVideoDisplayed }); } @@ -161,7 +175,7 @@ export default class AlwaysOnTop extends Component<*, State> { * @returns {ReactElement} */ _renderVideoNotAvailableScreen() { - const { avatarURL, displayName, isVideoDisplayed } = this.state; + const { avatarURL, displayName, formattedDisplayName, isVideoDisplayed } = this.state; if (isVideoDisplayed) { return null; @@ -169,19 +183,16 @@ export default class AlwaysOnTop extends Component<*, State> { return (
- { - avatarURL - ?
- -
- : null - } +
+ ) +
- { displayName } + { formattedDisplayName }
); diff --git a/react/features/base/avatar/components/AbstractStatelessAvatar.js b/react/features/base/avatar/components/AbstractStatelessAvatar.js new file mode 100644 index 0000000000..66d9d6786b --- /dev/null +++ b/react/features/base/avatar/components/AbstractStatelessAvatar.js @@ -0,0 +1,37 @@ +// @flow + +import { PureComponent } from 'react'; + +export type Props = { + + /** + * Color of the (initials based) avatar, if needed. + */ + color?: string, + + /** + * Initials to be used to render the initials based avatars. + */ + initials?: string, + + /** + * Callback to signal the failure of the loading of the URL. + */ + onAvatarLoadError?: Function, + + /** + * Expected size of the avatar. + */ + size?: number; + + /** + * The URL of the avatar to render. + */ + url?: ?string +}; + +/** + * Implements an abstract stateless avatar component that renders an avatar purely from what gets passed through + * props. + */ +export default class AbstractStatelessAvatar extends PureComponent

{} diff --git a/react/features/base/avatar/components/AbstractAvatar.js b/react/features/base/avatar/components/Avatar.js similarity index 68% rename from react/features/base/avatar/components/AbstractAvatar.js rename to react/features/base/avatar/components/Avatar.js index 5dede94bcd..521b433e47 100644 --- a/react/features/base/avatar/components/AbstractAvatar.js +++ b/react/features/base/avatar/components/Avatar.js @@ -1,11 +1,14 @@ // @flow -import { PureComponent } from 'react'; +import React, { PureComponent } from 'react'; import { getParticipantById } from '../../participants'; +import { connect } from '../../redux'; import { getAvatarColor, getInitials } from '../functions'; +import { StatelessAvatar } from '.'; + export type Props = { /** @@ -18,6 +21,11 @@ export type Props = { */ _loadableAvatarUrl: ?string, + /** + * A prop to maintain compatibility with web. + */ + className?: string, + /** * A string to override the initials to generate a color of. This is handy if you don't want to make * the background color match the string that the initials are generated from. @@ -30,6 +38,11 @@ export type Props = { */ displayName?: string, + /** + * ID of the element, if any. + */ + id?: string, + /** * The ID of the participant to render an avatar for (if it's a participant avatar). */ @@ -41,9 +54,9 @@ export type Props = { size: number, /** - * URI of the avatar, if any. + * URL of the avatar, if any. */ - uri: ?string, + url: ?string, } type State = { @@ -53,9 +66,9 @@ type State = { export const DEFAULT_SIZE = 65; /** - * Implements an abstract class to render avatars in the app. + * Implements a class to render avatars in the app. */ -export default class AbstractAvatar extends PureComponent { +class Avatar extends PureComponent { /** * Instantiates a new {@code Component}. * @@ -77,7 +90,7 @@ export default class AbstractAvatar extends PureComponent { * @inheritdoc */ componentDidUpdate(prevProps: P) { - if (prevProps.uri !== this.props.uri) { + if (prevProps.url !== this.props.url) { // URI changed, so we need to try to fetch it again. // Eslint doesn't like this statement, but based on the React doc, it's safe if it's @@ -99,25 +112,45 @@ export default class AbstractAvatar extends PureComponent { const { _initialsBase, _loadableAvatarUrl, + className, colorBase, - uri + id, + size, + url } = this.props; const { avatarFailed } = this.state; + const avatarProps = { + className, + color: undefined, + id, + initials: undefined, + onAvatarLoadError: undefined, + size, + url: undefined + }; + // _loadableAvatarUrl is validated that it can be loaded, but uri (if present) is not, so // we still need to do a check for that. And an explicitly provided URI is higher priority than // an avatar URL anyhow. - if ((uri && !avatarFailed) || _loadableAvatarUrl) { - return this._renderURLAvatar((!avatarFailed && uri) || _loadableAvatarUrl); + const effectiveURL = (!avatarFailed && url) || _loadableAvatarUrl; + + if (effectiveURL) { + avatarProps.onAvatarLoadError = this._onAvatarLoadError; + avatarProps.url = effectiveURL; + } else { + const initials = getInitials(_initialsBase); + + if (initials) { + avatarProps.color = getAvatarColor(colorBase || _initialsBase); + avatarProps.initials = initials; + } } - const _initials = getInitials(_initialsBase); - - if (_initials) { - return this._renderInitialsAvatar(_initials, getAvatarColor(colorBase || _initialsBase)); - } - - return this._renderDefaultAvatar(); + return ( + + ); } _onAvatarLoadError: () => void; @@ -132,30 +165,6 @@ export default class AbstractAvatar extends PureComponent { avatarFailed: true }); } - - /** - * Function to render the actual, platform specific default avatar component. - * - * @returns {React$Element<*>} - */ - _renderDefaultAvatar: () => React$Element<*> - - /** - * Function to render the actual, platform specific initials-based avatar component. - * - * @param {string} initials - The initials to use. - * @param {string} color - The color to use. - * @returns {React$Element<*>} - */ - _renderInitialsAvatar: (string, string) => React$Element<*> - - /** - * Function to render the actual, platform specific URL-based avatar component. - * - * @param {string} uri - The URI of the avatar. - * @returns {React$Element<*>} - */ - _renderURLAvatar: ?string => React$Element<*> } /** @@ -168,10 +177,12 @@ export default class AbstractAvatar extends PureComponent { export function _mapStateToProps(state: Object, ownProps: Props) { const { displayName, participantId } = ownProps; const _participant = participantId && getParticipantById(state, participantId); - const _initialsBase = (_participant && (_participant.name || _participant.email)) || displayName; + const _initialsBase = (_participant && _participant.name) || displayName; return { _initialsBase, _loadableAvatarUrl: _participant && _participant.loadableAvatarUrl }; } + +export default connect(_mapStateToProps)(Avatar); diff --git a/react/features/base/avatar/components/index.native.js b/react/features/base/avatar/components/index.native.js index a32ec60612..62d202f325 100644 --- a/react/features/base/avatar/components/index.native.js +++ b/react/features/base/avatar/components/index.native.js @@ -1,3 +1,4 @@ // @flow export * from './native'; +export { default as Avatar } from './Avatar'; diff --git a/react/features/base/avatar/components/index.web.js b/react/features/base/avatar/components/index.web.js index 40d5f46528..bdc40ea0a5 100644 --- a/react/features/base/avatar/components/index.web.js +++ b/react/features/base/avatar/components/index.web.js @@ -1,3 +1,4 @@ // @flow export * from './web'; +export { default as Avatar } from './Avatar'; diff --git a/react/features/base/avatar/components/native/Avatar.js b/react/features/base/avatar/components/native/Avatar.js deleted file mode 100644 index d3f02fd2df..0000000000 --- a/react/features/base/avatar/components/native/Avatar.js +++ /dev/null @@ -1,106 +0,0 @@ -// @flow - -import React from 'react'; -import { Image, Text, View } from 'react-native'; - -import { connect } from '../../../redux'; -import { type StyleType } from '../../../styles'; - -import AbstractAvatar, { - _mapStateToProps, - type Props as AbstractProps, - DEFAULT_SIZE -} from '../AbstractAvatar'; - -import RemoteAvatar, { DEFAULT_AVATAR } from './RemoteAvatar'; -import styles from './styles'; - -type Props = AbstractProps & { - - /** - * External style of the component. - */ - style?: StyleType -} - -/** - * Implements an avatar component that has 4 ways to render an avatar: - * - * - Based on an explicit avatar URI, if provided - * - Gravatar, if there is any - * - Based on initials generated from name or email - * - Default avatar icon, if any of the above fails - */ -class Avatar extends AbstractAvatar { - - _onAvatarLoadError: () => void; - - /** - * Implements {@code AbstractAvatar#_renderDefaultAvatar}. - * - * @inheritdoc - */ - _renderDefaultAvatar() { - return this._wrapAvatar( - - ); - } - - /** - * Implements {@code AbstractAvatar#_renderGravatar}. - * - * @inheritdoc - */ - _renderInitialsAvatar(initials, color) { - return this._wrapAvatar( - - { initials } - - ); - } - - /** - * Implements {@code AbstractAvatar#_renderGravatar}. - * - * @inheritdoc - */ - _renderURLAvatar(uri) { - return this._wrapAvatar( - - ); - } - - /** - * Wraps an avatar into a common wrapper. - * - * @param {React#Component} avatar - The avatar component. - * @returns {React#Component} - */ - _wrapAvatar(avatar) { - return ( - - { avatar } - - ); - } -} - -export default connect(_mapStateToProps)(Avatar); diff --git a/react/features/base/avatar/components/native/RemoteAvatar.js b/react/features/base/avatar/components/native/RemoteAvatar.js deleted file mode 100644 index 8fa86563fc..0000000000 --- a/react/features/base/avatar/components/native/RemoteAvatar.js +++ /dev/null @@ -1,50 +0,0 @@ -// @flow - -import React, { PureComponent } from 'react'; -import { Image } from 'react-native'; - -import styles from './styles'; - -export const DEFAULT_AVATAR = require('../../../../../../images/avatar.png'); - -type Props = { - - /** - * Callback for load errors. - */ - onError: Function, - - /** - * Size of the avatar. - */ - size: number, - - /** - * URI of the avatar to load. - */ - uri: string -}; - -/** - * Implements a private class that is used to fetch and render remote avatars based on an URI. - */ -export default class RemoteAvatar extends PureComponent { - - /** - * Implements {@code Component#render}. - * - * @inheritdoc - */ - render() { - const { onError, size, uri } = this.props; - - return ( - - ); - } -} diff --git a/react/features/base/avatar/components/native/StatelessAvatar.js b/react/features/base/avatar/components/native/StatelessAvatar.js new file mode 100644 index 0000000000..860e663b96 --- /dev/null +++ b/react/features/base/avatar/components/native/StatelessAvatar.js @@ -0,0 +1,112 @@ +// @flow + +import React from 'react'; +import { Image, Text, View } from 'react-native'; + +import { type StyleType } from '../../../styles'; + +import AbstractStatelessAvatar, { type Props as AbstractProps } from '../AbstractStatelessAvatar'; + +import styles from './styles'; + +type Props = AbstractProps & { + + /** + * External style passed to the componant. + */ + style?: StyleType +}; + +const DEFAULT_AVATAR = require('../../../../../../images/avatar.png'); + +/** + * Implements a stateless avatar component that renders an avatar purely from what gets passed through + * props. + */ +export default class StatelessAvatar extends AbstractStatelessAvatar { + /** + * Implements {@code Component#render}. + * + * @inheritdoc + */ + render() { + const { initials, size, style, url } = this.props; + + let avatar; + + if (url) { + avatar = this._renderURLAvatar(); + } else if (initials) { + avatar = this._renderInitialsAvatar(); + } else { + avatar = this._renderDefaultAvatar(); + } + + return ( + + { avatar } + + ); + } + + /** + * Renders the default avatar. + * + * @returns {React$Element<*>} + */ + _renderDefaultAvatar() { + const { size } = this.props; + + return ( + + ); + } + + /** + * Renders the initials-based avatar. + * + * @returns {React$Element<*>} + */ + _renderInitialsAvatar() { + const { color, initials, size } = this.props; + + return ( + + { initials } + + ); + } + + /** + * Renders the url-based avatar. + * + * @returns {React$Element<*>} + */ + _renderURLAvatar() { + const { onAvatarLoadError, size, url } = this.props; + + return ( + + ); + } +} diff --git a/react/features/base/avatar/components/native/index.js b/react/features/base/avatar/components/native/index.js index 2fda91a5d7..82ca95fd84 100644 --- a/react/features/base/avatar/components/native/index.js +++ b/react/features/base/avatar/components/native/index.js @@ -1,3 +1,3 @@ // @flow -export { default as Avatar } from './Avatar'; +export { default as StatelessAvatar } from './StatelessAvatar'; diff --git a/react/features/base/avatar/components/native/styles.js b/react/features/base/avatar/components/native/styles.js index 5b2478078a..c2c2ceff08 100644 --- a/react/features/base/avatar/components/native/styles.js +++ b/react/features/base/avatar/components/native/styles.js @@ -2,12 +2,14 @@ import { ColorPalette } from '../../../styles'; +const DEFAULT_SIZE = 65; + /** * The styles of the feature base/participants. */ export default { - avatarContainer: (size: number) => { + avatarContainer: (size: number = DEFAULT_SIZE) => { return { alignItems: 'center', borderRadius: size / 2, @@ -18,7 +20,7 @@ export default { }; }, - avatarContent: (size: number) => { + avatarContent: (size: number = DEFAULT_SIZE) => { return { height: size, width: size @@ -32,7 +34,7 @@ export default { justifyContent: 'center' }, - initialsText: (size: number) => { + initialsText: (size: number = DEFAULT_SIZE) => { return { color: 'rgba(255, 255, 255, 0.6)', fontSize: size * 0.5, diff --git a/react/features/base/avatar/components/web/Avatar.js b/react/features/base/avatar/components/web/Avatar.js deleted file mode 100644 index 0d9a1e7ad2..0000000000 --- a/react/features/base/avatar/components/web/Avatar.js +++ /dev/null @@ -1,98 +0,0 @@ -// @flow - -import React from 'react'; - -import { connect } from '../../../redux'; - -import AbstractAvatar, { - _mapStateToProps, - type Props as AbstractProps -} from '../AbstractAvatar'; - -type Props = AbstractProps & { - className?: string, - id: string -}; - -/** - * Implements an avatar as a React/Web {@link Component}. - */ -class Avatar extends AbstractAvatar { - /** - * Constructs a style object to be used on the avatars. - * - * @param {string?} color - The desired background color. - * @returns {Object} - */ - _getAvatarStyle(color) { - const { size } = this.props; - - return { - backgroundColor: color || undefined, - fontSize: size ? size * 0.5 : '180%', - height: size || '100%', - width: size || '100%' - }; - } - - /** - * Constructs a list of class names required for the avatar component. - * - * @param {string} additional - Any additional class to add. - * @returns {string} - */ - _getAvatarClassName(additional) { - return `avatar ${additional || ''} ${this.props.className || ''}`; - } - - _onAvatarLoadError: () => void; - - /** - * Implements {@code AbstractAvatar#_renderDefaultAvatar}. - * - * @inheritdoc - */ - _renderDefaultAvatar() { - return ( - - ); - } - - /** - * Implements {@code AbstractAvatar#_renderGravatar}. - * - * @inheritdoc - */ - _renderInitialsAvatar(initials, color) { - return ( -

- { initials } -
- ); - } - - /** - * Implements {@code AbstractAvatar#_renderGravatar}. - * - * @inheritdoc - */ - _renderURLAvatar(uri) { - return ( - - ); - } -} - -export default connect(_mapStateToProps)(Avatar); diff --git a/react/features/base/avatar/components/web/StatelessAvatar.js b/react/features/base/avatar/components/web/StatelessAvatar.js new file mode 100644 index 0000000000..63f06d4b42 --- /dev/null +++ b/react/features/base/avatar/components/web/StatelessAvatar.js @@ -0,0 +1,96 @@ +// @flow + +import React from 'react'; + +import AbstractStatelessAvatar, { type Props as AbstractProps } from '../AbstractStatelessAvatar'; + +type Props = AbstractProps & { + + /** + * External class name passed through props. + */ + className?: string, + + /** + * The default avatar URL if we want to override the app bundled one (e.g. AlwaysOnTop) + */ + defaultAvatar?: string, + + /** + * ID of the component to be rendered. + */ + id?: string +}; + +/** + * Implements a stateless avatar component that renders an avatar purely from what gets passed through + * props. + */ +export default class StatelessAvatar extends AbstractStatelessAvatar { + /** + * Implements {@code Component#render}. + * + * @inheritdoc + */ + render() { + const { initials, url } = this.props; + + if (url) { + return ( + + ); + } + + if (initials) { + return ( +
+ { initials } +
+ ); + } + + // default avatar + return ( + + ); + } + + /** + * Constructs a style object to be used on the avatars. + * + * @param {string?} color - The desired background color. + * @returns {Object} + */ + _getAvatarStyle(color) { + const { size } = this.props; + + return { + backgroundColor: color || undefined, + fontSize: size ? size * 0.5 : '180%', + height: size || '100%', + width: size || '100%' + }; + } + + /** + * Constructs a list of class names required for the avatar component. + * + * @param {string} additional - Any additional class to add. + * @returns {string} + */ + _getAvatarClassName(additional) { + return `avatar ${additional || ''} ${this.props.className || ''}`; + } +} diff --git a/react/features/base/avatar/components/web/index.js b/react/features/base/avatar/components/web/index.js index 2fda91a5d7..82ca95fd84 100644 --- a/react/features/base/avatar/components/web/index.js +++ b/react/features/base/avatar/components/web/index.js @@ -1,3 +1,3 @@ // @flow -export { default as Avatar } from './Avatar'; +export { default as StatelessAvatar } from './StatelessAvatar'; diff --git a/react/features/base/avatar/index.js b/react/features/base/avatar/index.js index 68ddef9ba1..359d82e99d 100644 --- a/react/features/base/avatar/index.js +++ b/react/features/base/avatar/index.js @@ -1,3 +1,4 @@ // @flow export * from './components'; +export * from './functions'; diff --git a/react/features/base/react/components/native/AvatarListItem.js b/react/features/base/react/components/native/AvatarListItem.js index 64d1e0e113..2b0d458b32 100644 --- a/react/features/base/react/components/native/AvatarListItem.js +++ b/react/features/base/react/components/native/AvatarListItem.js @@ -91,7 +91,7 @@ export default class AvatarListItem extends Component { displayName = { title } size = { avatarSize } style = { avatarStyle } - uri = { avatar } /> + url = { avatar } /> next => action => { const result = next(action); - if (participant && (participant.loadableAvatarUrl !== loadableAvatarUrl)) { - APP.API.notifyAvatarChanged( - id, - loadableAvatarUrl - ); + if (participant) { + if (loadableAvatarUrl) { + participant.loadableAvatarUrl !== loadableAvatarUrl && APP.API.notifyAvatarChanged( + id, + loadableAvatarUrl + ); + } else { + // There is no loadable explicit URL. In this case the Avatar component would + // decide to render initials or the default avatar, but the external API needs + // a URL when it needs to be rendered, so if there is no initials, we return the default + // Avatar URL as if it was a usual avatar URL. If there are (or may be) initials + // we send undefined to signal the api user that it's not an URL that needs to be rendered. + // + // NOTE: we may implement a special URL format later to signal that the avatar is based + // on initials, that API consumers can handle as they want, e.g. initials://jm + APP.API.notifyAvatarChanged( + id, + participant.name ? undefined : _getDefaultAvatarUrl() + ); + } } return result; @@ -65,7 +80,7 @@ MiddlewareRegistry.register(store => next => action => { case CONFERENCE_JOINED: { const state = store.getState(); const { room } = state['features/base/conference']; - const { name, id } = getLocalParticipant(state); + const { loadableAvatarUrl, name, id } = getLocalParticipant(state); APP.API.notifyConferenceJoined( room, @@ -76,7 +91,7 @@ MiddlewareRegistry.register(store => next => action => { name, interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME ), - avatarURL: getAvatarURLByParticipantId(state, id) + avatarURL: loadableAvatarUrl } ); break; @@ -125,3 +140,12 @@ MiddlewareRegistry.register(store => next => action => { return result; }); + +/** + * Returns the absolute URL of the default avatar. + * + * @returns {string} + */ +function _getDefaultAvatarUrl() { + return new URL('images/avatar.png', getBaseUrl()).href; +} diff --git a/react/features/mobile/incoming-call/components/IncomingCallPage.js b/react/features/mobile/incoming-call/components/IncomingCallPage.js index 510055dcd1..784cda30f7 100644 --- a/react/features/mobile/incoming-call/components/IncomingCallPage.js +++ b/react/features/mobile/incoming-call/components/IncomingCallPage.js @@ -130,7 +130,7 @@ class IncomingCallPage extends Component { + url = { this.props._callerAvatarURL } /> );