You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Avatar.js 5.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. // @flow
  2. import React, { PureComponent } from 'react';
  3. import { IconShareDesktop } from '../../icons';
  4. import { getParticipantById } from '../../participants';
  5. import { connect } from '../../redux';
  6. import { getAvatarColor, getInitials } from '../functions';
  7. import { StatelessAvatar } from '.';
  8. export type Props = {
  9. /**
  10. * The string we base the initials on (this is generated from a list of precendences).
  11. */
  12. _initialsBase: ?string,
  13. /**
  14. * An URL that we validated that it can be loaded.
  15. */
  16. _loadableAvatarUrl: ?string,
  17. /**
  18. * A prop to maintain compatibility with web.
  19. */
  20. className?: string,
  21. /**
  22. * A string to override the initials to generate a color of. This is handy if you don't want to make
  23. * the background color match the string that the initials are generated from.
  24. */
  25. colorBase?: string,
  26. /**
  27. * Display name of the entity to render an avatar for (if any). This is handy when we need
  28. * an avatar for a non-participasnt entity (e.g. a recent list item).
  29. */
  30. displayName?: string,
  31. /**
  32. * ID of the element, if any.
  33. */
  34. id?: string,
  35. /**
  36. * The ID of the participant to render an avatar for (if it's a participant avatar).
  37. */
  38. participantId?: string,
  39. /**
  40. * The size of the avatar.
  41. */
  42. size: number,
  43. /**
  44. * One of the expected status strings (e.g. 'available') to render a badge on the avatar, if necessary.
  45. */
  46. status?: ?string,
  47. /**
  48. * TestId of the element, if any.
  49. */
  50. testId?: string,
  51. /**
  52. * URL of the avatar, if any.
  53. */
  54. url: ?string,
  55. }
  56. type State = {
  57. avatarFailed: boolean
  58. }
  59. export const DEFAULT_SIZE = 65;
  60. /**
  61. * Implements a class to render avatars in the app.
  62. */
  63. class Avatar<P: Props> extends PureComponent<P, State> {
  64. /**
  65. * Instantiates a new {@code Component}.
  66. *
  67. * @inheritdoc
  68. */
  69. constructor(props: P) {
  70. super(props);
  71. this.state = {
  72. avatarFailed: false
  73. };
  74. this._onAvatarLoadError = this._onAvatarLoadError.bind(this);
  75. }
  76. /**
  77. * Implements {@code Component#componentDidUpdate}.
  78. *
  79. * @inheritdoc
  80. */
  81. componentDidUpdate(prevProps: P) {
  82. if (prevProps.url !== this.props.url) {
  83. // URI changed, so we need to try to fetch it again.
  84. // Eslint doesn't like this statement, but based on the React doc, it's safe if it's
  85. // wrapped in a condition: https://reactjs.org/docs/react-component.html#componentdidupdate
  86. // eslint-disable-next-line react/no-did-update-set-state
  87. this.setState({
  88. avatarFailed: false
  89. });
  90. }
  91. }
  92. /**
  93. * Implements {@code Componenr#render}.
  94. *
  95. * @inheritdoc
  96. */
  97. render() {
  98. const {
  99. _initialsBase,
  100. _loadableAvatarUrl,
  101. className,
  102. colorBase,
  103. id,
  104. size,
  105. status,
  106. testId,
  107. url
  108. } = this.props;
  109. const { avatarFailed } = this.state;
  110. const avatarProps = {
  111. className,
  112. color: undefined,
  113. id,
  114. initials: undefined,
  115. onAvatarLoadError: undefined,
  116. size,
  117. status,
  118. testId,
  119. url: undefined
  120. };
  121. // _loadableAvatarUrl is validated that it can be loaded, but uri (if present) is not, so
  122. // we still need to do a check for that. And an explicitly provided URI is higher priority than
  123. // an avatar URL anyhow.
  124. const effectiveURL = (!avatarFailed && url) || _loadableAvatarUrl;
  125. if (effectiveURL) {
  126. avatarProps.onAvatarLoadError = this._onAvatarLoadError;
  127. avatarProps.url = effectiveURL;
  128. }
  129. const initials = getInitials(_initialsBase);
  130. if (initials) {
  131. avatarProps.color = getAvatarColor(colorBase || _initialsBase);
  132. avatarProps.initials = initials;
  133. }
  134. return (
  135. <StatelessAvatar
  136. { ...avatarProps } />
  137. );
  138. }
  139. _onAvatarLoadError: () => void;
  140. /**
  141. * Callback to handle the error while loading of the avatar URI.
  142. *
  143. * @returns {void}
  144. */
  145. _onAvatarLoadError() {
  146. this.setState({
  147. avatarFailed: true
  148. });
  149. }
  150. }
  151. /**
  152. * Maps part of the Redux state to the props of this component.
  153. *
  154. * @param {Object} state - The Redux state.
  155. * @param {Props} ownProps - The own props of the component.
  156. * @returns {Props}
  157. */
  158. export function _mapStateToProps(state: Object, ownProps: Props) {
  159. const { colorBase, displayName, participantId } = ownProps;
  160. const _participant: ?Object = participantId && getParticipantById(state, participantId);
  161. const _initialsBase = _participant?.name ?? displayName;
  162. const screenShares = state['features/video-layout'].screenShares || [];
  163. let _loadableAvatarUrl = _participant?.loadableAvatarUrl;
  164. if (participantId && screenShares.includes(participantId)) {
  165. _loadableAvatarUrl = IconShareDesktop;
  166. }
  167. return {
  168. _initialsBase,
  169. _loadableAvatarUrl,
  170. colorBase: !colorBase && _participant ? _participant.id : colorBase
  171. };
  172. }
  173. export default connect(_mapStateToProps)(Avatar);