Procházet zdrojové kódy

rn: refactor Avatar to deal with FastImage changes

Updating react-native-fast-image brings a couple of interesting changes:

- onLoad is not called for cached images (reported and ignored upstream)
- load progress not working if component not displayed (on Android)

In order to fix this, a combination of 2 approaches was used:

- onLoadEnd / onError are used to detect if the image is loaded
- off-screen rendering is used on Android to get progress events

While implementing the above, yours truly noticed the complexity was increasing
way too much, so some extra refactoring was also performed:

- componentWillReceiveProps is dropped
- an auxiliary component (AvatarContent) is used for the actual content of the
  Avatar, with the former passing the key prop to the latter

Using the key prop ensures AvatarContent will be recreated if the URI changes,
which is not a bad idea anyway, since the new image needs to be downloaded.
master
Saúl Ibarra Corretgé před 6 roky
rodič
revize
0a9333af02
1 změnil soubory, kde provedl 122 přidání a 111 odebrání
  1. 122
    111
      react/features/base/participants/components/Avatar.native.js

+ 122
- 111
react/features/base/participants/components/Avatar.native.js Zobrazit soubor

1
 // @flow
1
 // @flow
2
 
2
 
3
-import React, { Component, Fragment } from 'react';
4
-import { Image, View } from 'react-native';
3
+import React, { Component, Fragment, PureComponent } from 'react';
4
+import { Dimensions, Image, Platform, View } from 'react-native';
5
 import FastImage from 'react-native-fast-image';
5
 import FastImage from 'react-native-fast-image';
6
 
6
 
7
 import { ColorPalette } from '../../styles';
7
 import { ColorPalette } from '../../styles';
44
  * The type of the React {@link Component} state of {@link Avatar}.
44
  * The type of the React {@link Component} state of {@link Avatar}.
45
  */
45
  */
46
 type State = {
46
 type State = {
47
+
48
+    /**
49
+     * Background color for the locally generated avatar.
50
+     */
47
     backgroundColor: string,
51
     backgroundColor: string,
48
-    source: ?{ uri: string },
49
-    useDefaultAvatar: boolean
50
-};
51
 
52
 
52
-/**
53
- * Implements an avatar as a React Native/mobile {@link Component}.
54
- */
55
-export default class Avatar extends Component<Props, State> {
56
     /**
53
     /**
57
-     * The indicator which determines whether this {@code Avatar} has been
58
-     * unmounted.
54
+     * Error indicator for non-local avatars.
55
+     */
56
+    error: boolean,
57
+
58
+    /**
59
+     * Indicates if the non-local avatar was loaded or not.
60
+     */
61
+    loaded: boolean,
62
+
63
+    /**
64
+     * Source for the non-local avatar.
59
      */
65
      */
60
-    _unmounted: ?boolean;
66
+    source: { uri: ?string }
67
+};
61
 
68
 
69
+/**
70
+ * Implements a React Native/mobile {@link Component} wich renders the content
71
+ * of an Avatar.
72
+ */
73
+class AvatarContent extends Component<Props, State> {
62
     /**
74
     /**
63
      * Initializes a new Avatar instance.
75
      * Initializes a new Avatar instance.
64
      *
76
      *
68
     constructor(props: Props) {
80
     constructor(props: Props) {
69
         super(props);
81
         super(props);
70
 
82
 
83
+        // Set the image source. The logic for the character # below is as
84
+        // follows:
85
+        // - Technically, URI is supposed to start with a scheme and scheme
86
+        //   cannot contain the character #.
87
+        // - Technically, the character # in a URI signals the start of the
88
+        //   fragment/hash.
89
+        // - Technically, the fragment/hash does not imply a retrieval
90
+        //   action.
91
+        // - Practically, the fragment/hash does not always mandate a
92
+        //   retrieval action. For example, an HTML anchor with an href that
93
+        //   starts with the character # does not cause a Web browser to
94
+        //   initiate a retrieval action.
95
+        // So I'll use the character # at the start of URI to not initiate
96
+        // an image retrieval action.
97
+        const source = {};
98
+
99
+        if (props.uri && !props.uri.startsWith('#')) {
100
+            source.uri = props.uri;
101
+        }
102
+
103
+        this.state = {
104
+            backgroundColor: this._getBackgroundColor(props),
105
+            error: false,
106
+            loaded: false,
107
+            source
108
+        };
109
+
71
         // Bind event handlers so they are only bound once per instance.
110
         // Bind event handlers so they are only bound once per instance.
72
         this._onAvatarLoaded = this._onAvatarLoaded.bind(this);
111
         this._onAvatarLoaded = this._onAvatarLoaded.bind(this);
73
-
74
-        // Fork (in Facebook/React speak) the prop uri because Image will
75
-        // receive it through a source object. Additionally, other props may be
76
-        // forked as well.
77
-        this.componentWillReceiveProps(props);
112
+        this._onAvatarLoadError = this._onAvatarLoadError.bind(this);
78
     }
113
     }
79
 
114
 
80
     /**
115
     /**
81
-     * Notifies this mounted React Component that it will receive new props.
82
-     * Forks (in Facebook/React speak) the prop {@code uri} because
83
-     * {@link Image} will receive it through a {@code source} object.
84
-     * Additionally, other props may be forked as well.
85
-     *
86
-     * @inheritdoc
87
-     * @param {Props} nextProps - The read-only React Component props that this
88
-     * instance will receive.
89
-     * @returns {void}
116
+     * Computes if the default avatar (ie, locally generated) should be used
117
+     * or not.
90
      */
118
      */
91
-    componentWillReceiveProps(nextProps: Props) {
92
-        // uri
93
-        const prevURI = this.props && this.props.uri;
94
-        const nextURI = nextProps && nextProps.uri;
95
-        const assignState = !this.state;
96
-
97
-        if (prevURI !== nextURI || assignState) {
98
-            const nextState = {
99
-                backgroundColor: this._getBackgroundColor(nextProps),
100
-                source: undefined,
101
-                useDefaultAvatar: true
102
-            };
103
-
104
-            if (assignState) {
105
-                // eslint-disable-next-line react/no-direct-mutation-state
106
-                this.state = nextState;
107
-            } else {
108
-                this.setState(nextState);
109
-            }
110
-
111
-            // XXX @lyubomir: My logic for the character # bellow is as follows:
112
-            // - Technically, URI is supposed to start with a scheme and scheme
113
-            //   cannot contain the character #.
114
-            // - Technically, the character # in URI signals the start of the
115
-            //   fragment/hash.
116
-            // - Technically, the fragment/hash does not imply a retrieval
117
-            //   action.
118
-            // - Practically, the fragment/hash does not always mandate a
119
-            //   retrieval action. For example, an HTML anchor with an href that
120
-            //   starts with the character # does not cause a Web browser to
121
-            //   initiate a retrieval action.
122
-            // So I'll use the character # at the start of URI to not initiate
123
-            // an image retrieval action.
124
-            if (nextURI && !nextURI.startsWith('#')) {
125
-                const nextSource = { uri: nextURI };
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
-                    this._unmounted || this.setState((prevState, props) => {
135
-                        if (props.uri === nextURI
136
-                                && (!prevState.source
137
-                                    || prevState.source.uri !== nextURI)) {
138
-                            return { source: nextSource };
139
-                        }
140
-
141
-                        return {};
142
-                    });
143
-                }
144
-            }
145
-        }
146
-    }
119
+    get useDefaultAvatar() {
120
+        const { error, loaded, source } = this.state;
147
 
121
 
148
-    /**
149
-     * Notifies this {@code Component} that it will be unmounted and destroyed,
150
-     * and most importantly, that it should no longer call
151
-     * {@link #setState(Object)}. The {@code Avatar} needs it because it
152
-     * downloads images via {@link ImageCache} which will asynchronously notify
153
-     * about success.
154
-     *
155
-     * @inheritdoc
156
-     * @returns {void}
157
-     */
158
-    componentWillUnmount() {
159
-        this._unmounted = true;
122
+        return !source.uri || error || !loaded;
160
     }
123
     }
161
 
124
 
162
     /**
125
     /**
208
     _onAvatarLoaded: () => void;
171
     _onAvatarLoaded: () => void;
209
 
172
 
210
     /**
173
     /**
211
-     * Handler called when the remote image was loaded. When this happens we
212
-     * show that instead of the default locally generated one.
174
+     * Handler called when the remote image loading finishes. This doesn't
175
+     * necessarily mean the load was successful.
213
      *
176
      *
214
      * @private
177
      * @private
215
      * @returns {void}
178
      * @returns {void}
216
      */
179
      */
217
     _onAvatarLoaded() {
180
     _onAvatarLoaded() {
218
-        this._unmounted || this.setState({ useDefaultAvatar: false });
181
+        this.setState({ loaded: true });
182
+    }
183
+
184
+    _onAvatarLoadError: () => void;
185
+
186
+    /**
187
+     * Handler called when the remote image loading failed.
188
+     *
189
+     * @private
190
+     * @returns {void}
191
+     */
192
+    _onAvatarLoadError() {
193
+        this.setState({ error: true });
219
     }
194
     }
220
 
195
 
221
     /**
196
     /**
229
         // regular Image, so we need to wrap it in a view to make it round.
204
         // regular Image, so we need to wrap it in a view to make it round.
230
         // https://github.com/facebook/react-native/issues/3198
205
         // https://github.com/facebook/react-native/issues/3198
231
 
206
 
232
-        const { backgroundColor, useDefaultAvatar } = this.state;
207
+        const { backgroundColor } = this.state;
233
         const imageStyle = this._getImageStyle();
208
         const imageStyle = this._getImageStyle();
234
         const viewStyle = {
209
         const viewStyle = {
235
             ...imageStyle,
210
             ...imageStyle,
236
 
211
 
237
             backgroundColor,
212
             backgroundColor,
238
-            display: useDefaultAvatar ? 'flex' : 'none',
239
 
213
 
240
-            // FIXME @lyubomir: Without the opacity bellow I feel like the
214
+            // FIXME @lyubomir: Without the opacity below I feel like the
241
             // avatar colors are too strong. Besides, we use opacity for the
215
             // avatar colors are too strong. Besides, we use opacity for the
242
             // ToolbarButtons. That's where I copied the value from and we
216
             // ToolbarButtons. That's where I copied the value from and we
243
             // may want to think about "standardizing" the opacity in the
217
             // may want to think about "standardizing" the opacity in the
268
      * @returns {ReactElement}
242
      * @returns {ReactElement}
269
      */
243
      */
270
     _renderAvatar() {
244
     _renderAvatar() {
271
-        const { source, useDefaultAvatar } = this.state;
272
-        const style = {
273
-            ...this._getImageStyle(),
274
-            display: useDefaultAvatar ? 'none' : 'flex'
275
-        };
245
+        const { source } = this.state;
246
+        let extraStyle;
247
+
248
+        if (this.useDefaultAvatar) {
249
+            // On Android, the image loading indicators don't work unless the
250
+            // Glide image is actually created, so we cannot use display: none.
251
+            // Instead, render it off-screen, which does the trick.
252
+            if (Platform.OS === 'android') {
253
+                const windowDimensions = Dimensions.get('window');
254
+
255
+                extraStyle = {
256
+                    bottom: -windowDimensions.height,
257
+                    right: -windowDimensions.width
258
+                };
259
+            } else {
260
+                extraStyle = { display: 'none' };
261
+            }
262
+        }
276
 
263
 
277
-        return (
264
+        return (// $FlowFixMe
278
             <FastImage
265
             <FastImage
279
-                onLoad = { this._onAvatarLoaded }
266
+                onError = { this._onAvatarLoadError }
267
+                onLoadEnd = { this._onAvatarLoaded }
280
                 resizeMode = 'contain'
268
                 resizeMode = 'contain'
281
                 source = { source }
269
                 source = { source }
282
-                style = { style } />
270
+                style = { [ this._getImageStyle(), extraStyle ] } />
283
         );
271
         );
284
     }
272
     }
285
 
273
 
289
      * @inheritdoc
277
      * @inheritdoc
290
      */
278
      */
291
     render() {
279
     render() {
292
-        const { source, useDefaultAvatar } = this.state;
280
+        const { source } = this.state;
293
 
281
 
294
         return (
282
         return (
295
             <Fragment>
283
             <Fragment>
296
-                { source && this._renderAvatar() }
297
-                { useDefaultAvatar && this._renderDefaultAvatar() }
284
+                { source.uri && this._renderAvatar() }
285
+                { this.useDefaultAvatar && this._renderDefaultAvatar() }
298
             </Fragment>
286
             </Fragment>
299
         );
287
         );
300
     }
288
     }
301
 }
289
 }
290
+
291
+/* eslint-disable react/no-multi-comp */
292
+
293
+/**
294
+ * Implements an avatar as a React Native/mobile {@link Component}.
295
+ *
296
+ * Note: we use `key` in order to trigger a new component creation in case
297
+ * the URI changes.
298
+ */
299
+export default class Avatar extends PureComponent<Props> {
300
+    /**
301
+     * Implements React's {@link Component#render()}.
302
+     *
303
+     * @inheritdoc
304
+     */
305
+    render() {
306
+        return (
307
+            <AvatarContent
308
+                key = { this.props.uri }
309
+                { ...this.props } />
310
+        );
311
+    }
312
+}

Načítá se…
Zrušit
Uložit