選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

Avatar.native.js 9.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. // @flow
  2. import React, { Component, Fragment } from 'react';
  3. import { Image, View } from 'react-native';
  4. import FastImage from 'react-native-fast-image';
  5. import { ColorPalette } from '../../styles';
  6. import styles from './styles';
  7. /**
  8. * The default image/source to be used in case none is specified or the
  9. * specified one fails to load.
  10. *
  11. * XXX The relative path to the default/stock (image) file is defined by the
  12. * {@code const} {@code DEFAULT_AVATAR_RELATIVE_PATH}. Unfortunately, the
  13. * packager of React Native cannot deal with it early enough for the following
  14. * {@code require} to succeed at runtime. Anyway, be sure to synchronize the
  15. * relative path on Web and mobile for the purposes of consistency.
  16. *
  17. * @private
  18. * @type {string}
  19. */
  20. const _DEFAULT_SOURCE = require('../../../../../images/avatar.png');
  21. /**
  22. * The type of the React {@link Component} props of {@link Avatar}.
  23. */
  24. type Props = {
  25. /**
  26. * The size for the {@link Avatar}.
  27. */
  28. size: number,
  29. /**
  30. * The URI of the {@link Avatar}.
  31. */
  32. uri: string
  33. };
  34. /**
  35. * The type of the React {@link Component} state of {@link Avatar}.
  36. */
  37. type State = {
  38. backgroundColor: string,
  39. source: ?{ uri: string },
  40. useDefaultAvatar: boolean
  41. };
  42. /**
  43. * Implements an avatar as a React Native/mobile {@link Component}.
  44. */
  45. export default class Avatar extends Component<Props, State> {
  46. /**
  47. * The indicator which determines whether this {@code Avatar} has been
  48. * unmounted.
  49. */
  50. _unmounted: ?boolean;
  51. /**
  52. * Initializes a new Avatar instance.
  53. *
  54. * @param {Props} props - The read-only React Component props with which
  55. * the new instance is to be initialized.
  56. */
  57. constructor(props: Props) {
  58. super(props);
  59. // Bind event handlers so they are only bound once per instance.
  60. this._onAvatarLoaded = this._onAvatarLoaded.bind(this);
  61. // Fork (in Facebook/React speak) the prop uri because Image will
  62. // receive it through a source object. Additionally, other props may be
  63. // forked as well.
  64. this.componentWillReceiveProps(props);
  65. }
  66. /**
  67. * Notifies this mounted React Component that it will receive new props.
  68. * Forks (in Facebook/React speak) the prop {@code uri} because
  69. * {@link Image} will receive it through a {@code source} object.
  70. * Additionally, other props may be forked as well.
  71. *
  72. * @inheritdoc
  73. * @param {Props} nextProps - The read-only React Component props that this
  74. * instance will receive.
  75. * @returns {void}
  76. */
  77. componentWillReceiveProps(nextProps: Props) {
  78. // uri
  79. const prevURI = this.props && this.props.uri;
  80. const nextURI = nextProps && nextProps.uri;
  81. const assignState = !this.state;
  82. if (prevURI !== nextURI || assignState) {
  83. const nextState = {
  84. backgroundColor: this._getBackgroundColor(nextProps),
  85. source: undefined,
  86. useDefaultAvatar: true
  87. };
  88. if (assignState) {
  89. // eslint-disable-next-line react/no-direct-mutation-state
  90. this.state = nextState;
  91. } else {
  92. this.setState(nextState);
  93. }
  94. // XXX @lyubomir: My logic for the character # bellow is as follows:
  95. // - Technically, URI is supposed to start with a scheme and scheme
  96. // cannot contain the character #.
  97. // - Technically, the character # in URI signals the start of the
  98. // fragment/hash.
  99. // - Technically, the fragment/hash does not imply a retrieval
  100. // action.
  101. // - Practically, the fragment/hash does not always mandate a
  102. // retrieval action. For example, an HTML anchor with an href that
  103. // starts with the character # does not cause a Web browser to
  104. // initiate a retrieval action.
  105. // So I'll use the character # at the start of URI to not initiate
  106. // an image retrieval action.
  107. if (nextURI && !nextURI.startsWith('#')) {
  108. const nextSource = { uri: nextURI };
  109. if (assignState) {
  110. // eslint-disable-next-line react/no-direct-mutation-state
  111. this.state = {
  112. ...this.state,
  113. source: nextSource
  114. };
  115. } else {
  116. this._unmounted || this.setState((prevState, props) => {
  117. if (props.uri === nextURI
  118. && (!prevState.source
  119. || prevState.source.uri !== nextURI)) {
  120. return { source: nextSource };
  121. }
  122. return {};
  123. });
  124. }
  125. }
  126. }
  127. }
  128. /**
  129. * Notifies this {@code Component} that it will be unmounted and destroyed
  130. * and, most importantly, that it should no longer call
  131. * {@link #setState(Object)}. {@code Avatar} needs it because it downloads
  132. * images via {@link ImageCache} which will asynchronously notify about
  133. * success.
  134. *
  135. * @inheritdoc
  136. * @returns {void}
  137. */
  138. componentWillUnmount() {
  139. this._unmounted = true;
  140. }
  141. /**
  142. * Computes a hash over the URI and returns a HSL background color. We use
  143. * 75% as lightness, for nice pastel style colors.
  144. *
  145. * @param {Object} props - The read-only React {@code Component} props from
  146. * which the background color is to be generated.
  147. * @private
  148. * @returns {string} - The HSL CSS property.
  149. */
  150. _getBackgroundColor({ uri }) {
  151. if (!uri) {
  152. return ColorPalette.white;
  153. }
  154. let hash = 0;
  155. /* eslint-disable no-bitwise */
  156. for (let i = 0; i < uri.length; i++) {
  157. hash = uri.charCodeAt(i) + ((hash << 5) - hash);
  158. hash |= 0; // Convert to 32-bit integer
  159. }
  160. /* eslint-enable no-bitwise */
  161. return `hsl(${hash % 360}, 100%, 75%)`;
  162. }
  163. /**
  164. * Helper which computes the style for the {@code Image} / {@code FastImage}
  165. * component.
  166. *
  167. * @private
  168. * @returns {Object}
  169. */
  170. _getImageStyle() {
  171. const { size } = this.props;
  172. return {
  173. ...styles.avatar,
  174. borderRadius: size / 2,
  175. height: size,
  176. width: size
  177. };
  178. }
  179. _onAvatarLoaded: () => void;
  180. /**
  181. * Handler called when the remote image was loaded. When this happens we
  182. * show that instead of the default locally generated one.
  183. *
  184. * @private
  185. * @returns {void}
  186. */
  187. _onAvatarLoaded() {
  188. this._unmounted || this.setState({ useDefaultAvatar: false });
  189. }
  190. /**
  191. * Renders a default, locally generated avatar image.
  192. *
  193. * @private
  194. * @returns {ReactElement}
  195. */
  196. _renderDefaultAvatar() {
  197. // When using a local image, react-native-fastimage falls back to a
  198. // regular Image, so we need to wrap it in a view to make it round.
  199. // https://github.com/facebook/react-native/issues/3198
  200. const { backgroundColor, useDefaultAvatar } = this.state;
  201. const imageStyle = this._getImageStyle();
  202. const viewStyle = {
  203. ...imageStyle,
  204. backgroundColor,
  205. display: useDefaultAvatar ? 'flex' : 'none',
  206. // FIXME @lyubomir: Without the opacity bellow I feel like the
  207. // avatar colors are too strong. Besides, we use opacity for the
  208. // ToolbarButtons. That's where I copied the value from and we
  209. // may want to think about "standardizing" the opacity in the
  210. // app in a way similar to ColorPalette.
  211. opacity: 0.1,
  212. overflow: 'hidden'
  213. };
  214. return (
  215. <View style = { viewStyle }>
  216. <Image
  217. // The Image adds a fade effect without asking, so lets
  218. // explicitly disable it. More info here:
  219. // https://github.com/facebook/react-native/issues/10194
  220. fadeDuration = { 0 }
  221. resizeMode = 'contain'
  222. source = { _DEFAULT_SOURCE }
  223. style = { imageStyle } />
  224. </View>
  225. );
  226. }
  227. /**
  228. * Renders an avatar using a remote image.
  229. *
  230. * @private
  231. * @returns {ReactElement}
  232. */
  233. _renderAvatar() {
  234. const { source, useDefaultAvatar } = this.state;
  235. const style = {
  236. ...this._getImageStyle(),
  237. display: useDefaultAvatar ? 'none' : 'flex'
  238. };
  239. return (
  240. <FastImage
  241. onLoad = { this._onAvatarLoaded }
  242. resizeMode = 'contain'
  243. source = { source }
  244. style = { style } />
  245. );
  246. }
  247. /**
  248. * Implements React's {@link Component#render()}.
  249. *
  250. * @inheritdoc
  251. */
  252. render() {
  253. const { source, useDefaultAvatar } = this.state;
  254. return (
  255. <Fragment>
  256. { source && this._renderAvatar() }
  257. { useDefaultAvatar && this._renderDefaultAvatar() }
  258. </Fragment>
  259. );
  260. }
  261. }