|
|
@@ -1,10 +1,9 @@
|
|
1
|
1
|
// @flow
|
|
2
|
2
|
|
|
3
|
|
-import React, { Component } from 'react';
|
|
|
3
|
+import React, { Component, Fragment } from 'react';
|
|
4
|
4
|
import { Image, View } from 'react-native';
|
|
|
5
|
+import FastImage from 'react-native-fast-image';
|
|
5
|
6
|
|
|
6
|
|
-import { CachedImage, ImageCache } from '../../../mobile/image-cache';
|
|
7
|
|
-import { Platform } from '../../react';
|
|
8
|
7
|
import { ColorPalette } from '../../styles';
|
|
9
|
8
|
|
|
10
|
9
|
import styles from './styles';
|
|
|
@@ -46,7 +45,8 @@ type Props = {
|
|
46
|
45
|
*/
|
|
47
|
46
|
type State = {
|
|
48
|
47
|
backgroundColor: string,
|
|
49
|
|
- source: number | { uri: string }
|
|
|
48
|
+ source: ?{ uri: string },
|
|
|
49
|
+ useDefaultAvatar: boolean
|
|
50
|
50
|
};
|
|
51
|
51
|
|
|
52
|
52
|
/**
|
|
|
@@ -68,6 +68,9 @@ export default class Avatar extends Component<Props, State> {
|
|
68
|
68
|
constructor(props: Props) {
|
|
69
|
69
|
super(props);
|
|
70
|
70
|
|
|
|
71
|
+ // Bind event handlers so they are only bound once per instance.
|
|
|
72
|
+ this._onAvatarLoaded = this._onAvatarLoaded.bind(this);
|
|
|
73
|
+
|
|
71
|
74
|
// Fork (in Facebook/React speak) the prop uri because Image will
|
|
72
|
75
|
// receive it through a source object. Additionally, other props may be
|
|
73
|
76
|
// forked as well.
|
|
|
@@ -94,18 +97,8 @@ export default class Avatar extends Component<Props, State> {
|
|
94
|
97
|
if (prevURI !== nextURI || assignState) {
|
|
95
|
98
|
const nextState = {
|
|
96
|
99
|
backgroundColor: this._getBackgroundColor(nextProps),
|
|
97
|
|
-
|
|
98
|
|
- /**
|
|
99
|
|
- * The source of the {@link Image} which is the actual
|
|
100
|
|
- * representation of this {@link Avatar}. The state
|
|
101
|
|
- * {@code source} was explicitly introduced in order to reduce
|
|
102
|
|
- * unnecessary renders.
|
|
103
|
|
- *
|
|
104
|
|
- * @type {{
|
|
105
|
|
- * uri: string
|
|
106
|
|
- * }}
|
|
107
|
|
- */
|
|
108
|
|
- source: _DEFAULT_SOURCE
|
|
|
100
|
+ source: undefined,
|
|
|
101
|
+ useDefaultAvatar: true
|
|
109
|
102
|
};
|
|
110
|
103
|
|
|
111
|
104
|
if (assignState) {
|
|
|
@@ -130,7 +123,14 @@ export default class Avatar extends Component<Props, State> {
|
|
130
|
123
|
// an image retrieval action.
|
|
131
|
124
|
if (nextURI && !nextURI.startsWith('#')) {
|
|
132
|
125
|
const nextSource = { uri: nextURI };
|
|
133
|
|
- const observer = () => {
|
|
|
126
|
+
|
|
|
127
|
+ if (assignState) {
|
|
|
128
|
+ // eslint-disable-next-line react/no-direct-mutation-state
|
|
|
129
|
+ this.state = {
|
|
|
130
|
+ ...this.state,
|
|
|
131
|
+ source: nextSource
|
|
|
132
|
+ };
|
|
|
133
|
+ } else {
|
|
134
|
134
|
this._unmounted || this.setState((prevState, props) => {
|
|
135
|
135
|
if (props.uri === nextURI
|
|
136
|
136
|
&& (!prevState.source
|
|
|
@@ -140,22 +140,6 @@ export default class Avatar extends Component<Props, State> {
|
|
140
|
140
|
|
|
141
|
141
|
return {};
|
|
142
|
142
|
});
|
|
143
|
|
- };
|
|
144
|
|
-
|
|
145
|
|
- // Wait for the source/URI to load.
|
|
146
|
|
- if (ImageCache) {
|
|
147
|
|
- ImageCache.get().on(
|
|
148
|
|
- nextSource,
|
|
149
|
|
- observer,
|
|
150
|
|
- /* immutable */ true);
|
|
151
|
|
- } else if (assignState) {
|
|
152
|
|
- // eslint-disable-next-line react/no-direct-mutation-state
|
|
153
|
|
- this.state = {
|
|
154
|
|
- ...this.state,
|
|
155
|
|
- source: nextSource
|
|
156
|
|
- };
|
|
157
|
|
- } else {
|
|
158
|
|
- observer();
|
|
159
|
143
|
}
|
|
160
|
144
|
}
|
|
161
|
145
|
}
|
|
|
@@ -204,106 +188,114 @@ export default class Avatar extends Component<Props, State> {
|
|
204
|
188
|
}
|
|
205
|
189
|
|
|
206
|
190
|
/**
|
|
207
|
|
- * Implements React's {@link Component#render()}.
|
|
|
191
|
+ * Helper which computes the style for the {@code Image} / {@code FastImage}
|
|
|
192
|
+ * component.
|
|
208
|
193
|
*
|
|
209
|
|
- * @inheritdoc
|
|
|
194
|
+ * @private
|
|
|
195
|
+ * @returns {Object}
|
|
210
|
196
|
*/
|
|
211
|
|
- render() {
|
|
212
|
|
- // Propagate all props of this Avatar but the ones consumed by this
|
|
213
|
|
- // Avatar to the Image it renders.
|
|
214
|
|
- const {
|
|
215
|
|
- /* eslint-disable no-unused-vars */
|
|
216
|
|
-
|
|
217
|
|
- // The following are forked in state:
|
|
218
|
|
- uri: forked0,
|
|
|
197
|
+ _getImageStyle() {
|
|
|
198
|
+ const { size } = this.props;
|
|
219
|
199
|
|
|
220
|
|
- /* eslint-enable no-unused-vars */
|
|
221
|
|
-
|
|
222
|
|
- size,
|
|
223
|
|
- ...props
|
|
224
|
|
- } = this.props;
|
|
225
|
|
- const {
|
|
226
|
|
- backgroundColor,
|
|
227
|
|
- source
|
|
228
|
|
- } = this.state;
|
|
229
|
|
-
|
|
230
|
|
- // Compute the base style
|
|
231
|
|
- const borderRadius = size / 2;
|
|
232
|
|
- const style = {
|
|
|
200
|
+ return {
|
|
233
|
201
|
...styles.avatar,
|
|
234
|
|
-
|
|
235
|
|
- // XXX Workaround for Android: for radii < 80 the border radius
|
|
236
|
|
- // doesn't work properly, but applying a radius twice as big seems
|
|
237
|
|
- // to do the trick.
|
|
238
|
|
- borderRadius:
|
|
239
|
|
- Platform.OS === 'android' && borderRadius < 80
|
|
240
|
|
- ? size * 2
|
|
241
|
|
- : borderRadius,
|
|
|
202
|
+ borderRadius: size / 2,
|
|
242
|
203
|
height: size,
|
|
243
|
204
|
width: size
|
|
244
|
205
|
};
|
|
|
206
|
+ }
|
|
245
|
207
|
|
|
246
|
|
- // If we're rendering the _DEFAULT_SOURCE, then we want to do some
|
|
247
|
|
- // additional fu like having automagical colors generated per
|
|
248
|
|
- // participant, transparency to make the intermediate state while
|
|
249
|
|
- // downloading the remote image a little less "in your face", etc.
|
|
250
|
|
- let styleWithBackgroundColor;
|
|
251
|
|
-
|
|
252
|
|
- if (source === _DEFAULT_SOURCE && backgroundColor) {
|
|
253
|
|
- styleWithBackgroundColor = {
|
|
254
|
|
- ...style,
|
|
255
|
|
-
|
|
256
|
|
- backgroundColor,
|
|
257
|
|
-
|
|
258
|
|
- // FIXME @lyubomir: Without the opacity bellow I feel like the
|
|
259
|
|
- // avatar colors are too strong. Besides, we use opacity for the
|
|
260
|
|
- // ToolbarButtons. That's where I copied the value from and we
|
|
261
|
|
- // may want to think about "standardizing" the opacity in the
|
|
262
|
|
- // app in a way similar to ColorPalette.
|
|
263
|
|
- opacity: 0.1,
|
|
264
|
|
- overflow: 'hidden'
|
|
265
|
|
- };
|
|
266
|
|
- }
|
|
|
208
|
+ _onAvatarLoaded: () => void;
|
|
267
|
209
|
|
|
268
|
|
- // If we're styling with backgroundColor, we need to wrap the Image in a
|
|
269
|
|
- // View because of a bug in React Native for Android:
|
|
|
210
|
+ /**
|
|
|
211
|
+ * Handler called when the remote image was loaded. When this happens we
|
|
|
212
|
+ * show that instead of the default locally generated one.
|
|
|
213
|
+ *
|
|
|
214
|
+ * @private
|
|
|
215
|
+ * @returns {void}
|
|
|
216
|
+ */
|
|
|
217
|
+ _onAvatarLoaded() {
|
|
|
218
|
+ this._unmounted || this.setState({ useDefaultAvatar: false });
|
|
|
219
|
+ }
|
|
|
220
|
+
|
|
|
221
|
+ /**
|
|
|
222
|
+ * Renders a default, locally generated avatar image.
|
|
|
223
|
+ *
|
|
|
224
|
+ * @private
|
|
|
225
|
+ * @returns {ReactElement}
|
|
|
226
|
+ */
|
|
|
227
|
+ _renderDefaultAvatar() {
|
|
|
228
|
+ // When using a local image, react-native-fastimage falls back to a
|
|
|
229
|
+ // regular Image, so we need to wrap it in a view to make it round.
|
|
270
|
230
|
// https://github.com/facebook/react-native/issues/3198
|
|
271
|
|
- let imageStyle;
|
|
272
|
|
- let viewStyle;
|
|
273
|
231
|
|
|
274
|
|
- if (styleWithBackgroundColor) {
|
|
275
|
|
- if (Platform.OS === 'android') {
|
|
276
|
|
- imageStyle = style;
|
|
277
|
|
- viewStyle = styleWithBackgroundColor;
|
|
278
|
|
- } else {
|
|
279
|
|
- imageStyle = styleWithBackgroundColor;
|
|
280
|
|
- }
|
|
281
|
|
- } else {
|
|
282
|
|
- imageStyle = style;
|
|
283
|
|
- }
|
|
|
232
|
+ const { backgroundColor, useDefaultAvatar } = this.state;
|
|
|
233
|
+ const imageStyle = this._getImageStyle();
|
|
|
234
|
+ const viewStyle = {
|
|
|
235
|
+ ...imageStyle,
|
|
284
|
236
|
|
|
285
|
|
- let element
|
|
286
|
|
- = React.createElement(
|
|
|
237
|
+ backgroundColor,
|
|
|
238
|
+ display: useDefaultAvatar ? 'flex' : 'none',
|
|
|
239
|
+
|
|
|
240
|
+ // FIXME @lyubomir: Without the opacity bellow I feel like the
|
|
|
241
|
+ // avatar colors are too strong. Besides, we use opacity for the
|
|
|
242
|
+ // ToolbarButtons. That's where I copied the value from and we
|
|
|
243
|
+ // may want to think about "standardizing" the opacity in the
|
|
|
244
|
+ // app in a way similar to ColorPalette.
|
|
|
245
|
+ opacity: 0.1,
|
|
|
246
|
+ overflow: 'hidden'
|
|
|
247
|
+ };
|
|
287
|
248
|
|
|
288
|
|
- // XXX CachedImage removed support for images which clearly do
|
|
289
|
|
- // not need caching.
|
|
290
|
|
- typeof source === 'number' ? Image : CachedImage,
|
|
291
|
|
- {
|
|
292
|
|
- ...props,
|
|
|
249
|
+ return (
|
|
|
250
|
+ <View style = { viewStyle }>
|
|
|
251
|
+ <Image
|
|
293
|
252
|
|
|
294
|
253
|
// The Image adds a fade effect without asking, so lets
|
|
295
|
254
|
// explicitly disable it. More info here:
|
|
296
|
255
|
// https://github.com/facebook/react-native/issues/10194
|
|
297
|
|
- fadeDuration: 0,
|
|
298
|
|
- resizeMode: 'contain',
|
|
299
|
|
- source,
|
|
300
|
|
- style: imageStyle
|
|
301
|
|
- });
|
|
302
|
|
-
|
|
303
|
|
- if (viewStyle) {
|
|
304
|
|
- element = React.createElement(View, { style: viewStyle }, element);
|
|
305
|
|
- }
|
|
|
256
|
+ fadeDuration = { 0 }
|
|
|
257
|
+ resizeMode = 'contain'
|
|
|
258
|
+ source = { _DEFAULT_SOURCE }
|
|
|
259
|
+ style = { imageStyle } />
|
|
|
260
|
+ </View>
|
|
|
261
|
+ );
|
|
|
262
|
+ }
|
|
306
|
263
|
|
|
307
|
|
- return element;
|
|
|
264
|
+ /**
|
|
|
265
|
+ * Renders an avatar using a remote image.
|
|
|
266
|
+ *
|
|
|
267
|
+ * @private
|
|
|
268
|
+ * @returns {ReactElement}
|
|
|
269
|
+ */
|
|
|
270
|
+ _renderAvatar() {
|
|
|
271
|
+ const { source, useDefaultAvatar } = this.state;
|
|
|
272
|
+ const style = {
|
|
|
273
|
+ ...this._getImageStyle(),
|
|
|
274
|
+ display: useDefaultAvatar ? 'none' : 'flex'
|
|
|
275
|
+ };
|
|
|
276
|
+
|
|
|
277
|
+ return (
|
|
|
278
|
+ <FastImage
|
|
|
279
|
+ onLoad = { this._onAvatarLoaded }
|
|
|
280
|
+ resizeMode = 'contain'
|
|
|
281
|
+ source = { source }
|
|
|
282
|
+ style = { style } />
|
|
|
283
|
+ );
|
|
|
284
|
+ }
|
|
|
285
|
+
|
|
|
286
|
+ /**
|
|
|
287
|
+ * Implements React's {@link Component#render()}.
|
|
|
288
|
+ *
|
|
|
289
|
+ * @inheritdoc
|
|
|
290
|
+ */
|
|
|
291
|
+ render() {
|
|
|
292
|
+ const { source, useDefaultAvatar } = this.state;
|
|
|
293
|
+
|
|
|
294
|
+ return (
|
|
|
295
|
+ <Fragment>
|
|
|
296
|
+ { source && this._renderAvatar() }
|
|
|
297
|
+ { useDefaultAvatar && this._renderDefaultAvatar() }
|
|
|
298
|
+ </Fragment>
|
|
|
299
|
+ );
|
|
308
|
300
|
}
|
|
309
|
301
|
}
|