Переглянути джерело

[RN] Cache avatars and provide a default in case load fails (2)

Refactors the previous "[RN] Cache avatars and provide a default in
case load fails" for the purposes of simplification but also modifies
its functionality at the same time. For example:

- Always displays the default avatar immediately which may be seen if
  the remote avatar needs to be downloaded.
- Does not use random colors.
- Uses a default avatar image which is not transparent and ugly but at
  least it's the same image that's used on Web. I've started talks to
  have images/avatar2.png replaced with a transparent and beautiful
  so that will land later on and we'll see the automagic colors in all
  their glory then.
master
Lyubo Marinov 8 роки тому
джерело
коміт
00e058d392

+ 172
- 14
react/features/base/participants/components/Avatar.native.js Переглянути файл

@@ -1,7 +1,22 @@
1 1
 import React, { Component } from 'react';
2
-import { CustomCachedImage } from 'react-native-img-cache';
2
+import { View } from 'react-native';
3
+import { CachedImage, ImageCache } from 'react-native-img-cache';
3 4
 
4
-import AvatarImage from './AvatarImage';
5
+import { Platform } from '../../react';
6
+import { ColorPalette } from '../../styles';
7
+
8
+// FIXME @lyubomir: The string images/avatar2.png appears three times in our
9
+// source code at the time of this writing. Firstly, it presents a maintenance
10
+// obstacle which increases the risks of inconsistency. Secondly, it is
11
+// repulsive (when enlarged, especially, on mobile/React Native, for example).
12
+/**
13
+ * The default image/source to be used in case none is specified or the
14
+ * specified one fails to load.
15
+ *
16
+ * @private
17
+ * @type {string}
18
+ */
19
+const _DEFAULT_SOURCE = require('../../../../../images/avatar2.png');
5 20
 
6 21
 /**
7 22
  * Implements an avatar as a React Native/mobile {@link Component}.
@@ -60,6 +75,8 @@ export default class Avatar extends Component {
60 75
 
61 76
         if (prevURI !== nextURI || !this.state) {
62 77
             const nextState = {
78
+                backgroundColor: this._getBackgroundColor(nextProps),
79
+
63 80
                 /**
64 81
                  * The source of the {@link Image} which is the actual
65 82
                  * representation of this {@link Avatar}. The state
@@ -70,9 +87,7 @@ export default class Avatar extends Component {
70 87
                  *     uri: string
71 88
                  * }}
72 89
                  */
73
-                source: {
74
-                    uri: nextURI
75
-                }
90
+                source: _DEFAULT_SOURCE
76 91
             };
77 92
 
78 93
             if (this.state) {
@@ -80,9 +95,95 @@ export default class Avatar extends Component {
80 95
             } else {
81 96
                 this.state = nextState;
82 97
             }
98
+
99
+            // XXX @lyubomir: My logic for the character # bellow is as follows:
100
+            // - Technically, URI is supposed to start with a scheme and scheme
101
+            //   cannot contain the character #.
102
+            // - Technically, the character # in URI signals the start of the
103
+            //   fragment/hash.
104
+            // - Technically, the fragment/hash does not imply a retrieval
105
+            //   action.
106
+            // - Practically, the fragment/hash does not always mandate a
107
+            //   retrieval action. For example, an HTML anchor with an href that
108
+            //   starts with the character # does not cause a Web browser to
109
+            //   initiate a retrieval action.
110
+            // So I'll use the character # at the start of URI to not initiate
111
+            // an image retrieval action.
112
+            if (nextURI && !nextURI.startsWith('#')) {
113
+                const nextSource = { uri: nextURI };
114
+
115
+                // Wait for the source/URI to load.
116
+                ImageCache.get().on(
117
+                    nextSource,
118
+                    /* observer */ () => {
119
+                        this._unmounted || this.setState((prevState, props) => {
120
+                            if (props.uri === nextURI
121
+                                    && (!prevState.source
122
+                                        || prevState.source.uri !== nextURI)) {
123
+                                return { source: nextSource };
124
+                            }
125
+
126
+                            return {};
127
+                        });
128
+                    },
129
+                    /* immutable */ true);
130
+            }
83 131
         }
84 132
     }
85 133
 
134
+    /**
135
+     * Notifies this <tt>Component</tt> that it will be unmounted and destroyed
136
+     * and, most importantly, that it should no longer call
137
+     * {@link #setState(Object)}. <tt>Avatar</tt> needs it because it downloads
138
+     * images via {@link ImageCache} which will asynchronously notify about
139
+     * success.
140
+     *
141
+     * @inheritdoc
142
+     * @returns {void}
143
+     */
144
+    componentWillUnmount() {
145
+        this._unmounted = true;
146
+    }
147
+
148
+    /**
149
+     * Computes a hash over the URI and returns a HSL background color. We use
150
+     * 75% as lightness, for nice pastel style colors.
151
+     *
152
+     * @param {Object} props - The read-only React <tt>Component</tt> props from
153
+     * which the background color is to be generated.
154
+     * @private
155
+     * @returns {string} - The HSL CSS property.
156
+     */
157
+    _getBackgroundColor({ uri }) {
158
+        if (!uri) {
159
+            // @lyubomir: I'm leaving @saghul's implementation which picks up a
160
+            // random color bellow so that we have it in the source code in
161
+            // case we decide to use it in the future. However, I think at the
162
+            // time of this writing that the randomness reduces the
163
+            // predictability which React is supposed to bring to our app.
164
+            return ColorPalette.white;
165
+        }
166
+
167
+        let hash = 0;
168
+
169
+        if (typeof uri === 'string') {
170
+            /* eslint-disable no-bitwise */
171
+
172
+            for (let i = 0; i < uri.length; i++) {
173
+                hash = uri.charCodeAt(i) + ((hash << 5) - hash);
174
+                hash |= 0;  // Convert to 32-bit integer
175
+            }
176
+
177
+            /* eslint-enable no-bitwise */
178
+        } else {
179
+            // @saghul: If we have no URI yet, we have no data to hash from. So
180
+            // use a random value.
181
+            hash = Math.floor(Math.random() * 360);
182
+        }
183
+
184
+        return `hsl(${hash % 360}, 100%, 75%)`;
185
+    }
186
+
86 187
     /**
87 188
      * Implements React's {@link Component#render()}.
88 189
      *
@@ -91,16 +192,73 @@ export default class Avatar extends Component {
91 192
     render() {
92 193
         // Propagate all props of this Avatar but the ones consumed by this
93 194
         // Avatar to the Image it renders.
195
+        const {
196
+            /* eslint-disable no-unused-vars */
197
+
198
+            // The following are forked in state:
199
+            uri: forked0,
200
+
201
+            /* eslint-enable no-unused-vars */
202
+
203
+            style,
204
+            ...props
205
+        } = this.props;
206
+        const {
207
+            backgroundColor,
208
+            source
209
+        } = this.state;
210
+
211
+        // If we're rendering the _DEFAULT_SOURCE, then we want to do some
212
+        // additional fu like having automagical colors generated per
213
+        // participant, transparency to make the intermediate state while
214
+        // downloading the remote image a little less "in your face", etc.
215
+        let styleWithBackgroundColor;
216
+
217
+        if (source === _DEFAULT_SOURCE && backgroundColor) {
218
+            styleWithBackgroundColor = {
219
+                ...style,
220
+
221
+                backgroundColor,
94 222
 
95
-        // eslint-disable-next-line no-unused-vars
96
-        const { uri, ...props } = this.props;
223
+                // FIXME @lyubomir: Without the opacity bellow I feel like the
224
+                // avatar colors are too strong. Besides, we use opacity for the
225
+                // ToolbarButtons. That's where I copied the value from and we
226
+                // may want to think about "standardizing" the opacity in the
227
+                // app in a way similar to ColorPalette.
228
+                opacity: 0.1,
229
+                overflow: 'hidden'
230
+            };
231
+        }
232
+
233
+        // If we're styling with backgroundColor, we need to wrap the Image in a
234
+        // View because of a bug in React Native for Android:
235
+        // https://github.com/facebook/react-native/issues/3198
236
+        let imageStyle;
237
+        let viewStyle;
238
+
239
+        if (styleWithBackgroundColor) {
240
+            if (Platform.OS === 'android') {
241
+                imageStyle = style;
242
+                viewStyle = styleWithBackgroundColor;
243
+            } else {
244
+                imageStyle = styleWithBackgroundColor;
245
+            }
246
+        } else {
247
+            imageStyle = style;
248
+        }
249
+
250
+        let element = React.createElement(CachedImage, {
251
+            ...props,
252
+
253
+            resizeMode: 'contain',
254
+            source,
255
+            style: imageStyle
256
+        });
257
+
258
+        if (viewStyle) {
259
+            element = React.createElement(View, { style: viewStyle }, element);
260
+        }
97 261
 
98
-        return (
99
-            <CustomCachedImage
100
-                { ...props }
101
-                component = { AvatarImage }
102
-                resizeMode = 'contain'
103
-                source = { this.state.source } />
104
-        );
262
+        return element;
105 263
     }
106 264
 }

+ 0
- 208
react/features/base/participants/components/AvatarImage.native.js Переглянути файл

@@ -1,208 +0,0 @@
1
-import React, { Component } from 'react';
2
-import { Image, View } from 'react-native';
3
-
4
-import { Platform } from '../../react';
5
-
6
-/**
7
- * The default avatar to be used, in case the requested URI is not available
8
- * or fails to load. It is an inline version of images/avatar2.png.
9
- *
10
- * @type {string}
11
- */
12
-const DEFAULT_AVATAR = require('./defaultAvatar.png');
13
-
14
-/**
15
- * The number of milliseconds to wait when the avatar URI is undefined before we
16
- * start showing a default locally generated one. Note that since we have no
17
- * URI, we have nothing we can cache, so the color will be random.
18
- *
19
- * @type {number}
20
- */
21
-const UNDEFINED_AVATAR_TIMEOUT = 1000;
22
-
23
-/**
24
- * Implements an Image component wrapper, which returns a default image if the
25
- * requested one fails to load. The default image background is chosen by
26
- * hashing the URL of the image.
27
- */
28
-export default class AvatarImage extends Component {
29
-    /**
30
-     * AvatarImage component's property types.
31
-     *
32
-     * @static
33
-     */
34
-    static propTypes = {
35
-        /**
36
-         * If set to <tt>true</tt> it will not load the URL, but will use the
37
-         * default instead.
38
-         */
39
-        forceDefault: React.PropTypes.bool,
40
-
41
-        /**
42
-         * The source the {@link Image}.
43
-         */
44
-        source: React.PropTypes.object,
45
-
46
-        /**
47
-         * The optional style to add to the {@link Image} in order to customize
48
-         * its base look (and feel).
49
-         */
50
-        style: React.PropTypes.object
51
-    };
52
-
53
-    /**
54
-     * Initializes new AvatarImage component.
55
-     *
56
-     * @param {Object} props - Component props.
57
-     */
58
-    constructor(props) {
59
-        super(props);
60
-
61
-        this.state = {
62
-            failed: false,
63
-            showDefault: false
64
-        };
65
-
66
-        this.componentWillReceiveProps(props);
67
-
68
-        this._onError = this._onError.bind(this);
69
-    }
70
-
71
-    /**
72
-     * Notifies this mounted React Component that it will receive new props.
73
-     * If the URI is undefined, wait {@code UNDEFINED_AVATAR_TIMEOUT} ms and
74
-     * start showing a default locally generated avatar afterwards.
75
-     *
76
-     * Once a URI is passed, it will be rendered instead, except if loading it
77
-     * fails, in which case we fallback to a locally generated avatar again.
78
-     *
79
-     * @inheritdoc
80
-     * @param {Object} nextProps - The read-only React Component props that this
81
-     * instance will receive.
82
-     * @returns {void}
83
-     */
84
-    componentWillReceiveProps(nextProps) {
85
-        const prevSource = this.props.source;
86
-        const prevURI = prevSource && prevSource.uri;
87
-        const nextSource = nextProps.source;
88
-        const nextURI = nextSource && nextSource.uri;
89
-
90
-        if (typeof prevURI === 'undefined') {
91
-            clearTimeout(this._timeout);
92
-            if (typeof nextURI === 'undefined') {
93
-                this._timeout
94
-                    = setTimeout(
95
-                        () => this.setState({ showDefault: true }),
96
-                        UNDEFINED_AVATAR_TIMEOUT);
97
-            } else {
98
-                this.setState({ showDefault: nextProps.forceDefault });
99
-            }
100
-        }
101
-    }
102
-
103
-    /**
104
-     * Clear the timer just in case. See {@code componentWillReceiveProps} for
105
-     * details.
106
-     *
107
-     * @inheritdoc
108
-     */
109
-    componentWillUnmount() {
110
-        clearTimeout(this._timeout);
111
-    }
112
-
113
-    /**
114
-     * Computes a hash over the URI and returns a HSL background color. We use
115
-     * 75% as lightness, for nice pastel style colors.
116
-     *
117
-     * @private
118
-     * @returns {string} - The HSL CSS property.
119
-     */
120
-    _getBackgroundColor() {
121
-        const uri = this.props.source.uri;
122
-        let hash = 0;
123
-
124
-        // If we have no URI yet we have no data to hash from, so use a random
125
-        // value.
126
-        if (typeof uri === 'undefined') {
127
-            hash = Math.floor(Math.random() * 360);
128
-        } else {
129
-            /* eslint-disable no-bitwise */
130
-
131
-            for (let i = 0; i < uri.length; i++) {
132
-                hash = uri.charCodeAt(i) + ((hash << 5) - hash);
133
-                hash |= 0;  // Convert to 32bit integer
134
-            }
135
-
136
-            /* eslint-enable no-bitwise */
137
-        }
138
-
139
-        return `hsl(${hash % 360}, 100%, 75%)`;
140
-    }
141
-
142
-    /**
143
-     * Error handler for image loading. When an image fails to load we'll mark
144
-     * it as failed and load the default URI instead.
145
-     *
146
-     * @private
147
-     * @returns {void}
148
-     */
149
-    _onError() {
150
-        this.setState({ failed: true });
151
-    }
152
-
153
-    /**
154
-     * Implements React's {@link Component#render()}.
155
-     *
156
-     * @inheritdoc
157
-     */
158
-    render() {
159
-        const { failed, showDefault } = this.state;
160
-        const {
161
-            // The following is/are forked in state:
162
-            forceDefault, // eslint-disable-line no-unused-vars
163
-
164
-            source,
165
-            style,
166
-            ...props
167
-        } = this.props;
168
-
169
-        if (failed || showDefault) {
170
-            const coloredBackground = {
171
-                ...style,
172
-                backgroundColor: this._getBackgroundColor(),
173
-                overflow: 'hidden'
174
-            };
175
-
176
-            // We need to wrap the Image in a View because of a bug in React
177
-            // Native for Android:
178
-            // https://github.com/facebook/react-native/issues/3198
179
-            const workaround3198 = Platform.OS === 'android';
180
-            let element = React.createElement(Image, {
181
-                ...props,
182
-                source: DEFAULT_AVATAR,
183
-                style: workaround3198 ? style : coloredBackground
184
-            });
185
-
186
-            if (workaround3198) {
187
-                element
188
-                    = React.createElement(
189
-                        View,
190
-                        { style: coloredBackground },
191
-                        element);
192
-            }
193
-
194
-            return element;
195
-        } else if (typeof source.uri === 'undefined') {
196
-            return null;
197
-        }
198
-
199
-        // We have a URI and it's time to render it.
200
-        return (
201
-            <Image
202
-                { ...props }
203
-                onError = { this._onError }
204
-                source = { source }
205
-                style = { style } />
206
-        );
207
-    }
208
-}

+ 11
- 0
react/features/base/participants/components/ParticipantView.native.js Переглянути файл

@@ -198,6 +198,17 @@ function _mapStateToProps(state, ownProps) {
198 198
     if (participant) {
199 199
         avatar = getAvatarURL(participant);
200 200
         connectionStatus = participant.connectionStatus;
201
+
202
+        // Avatar (on React Native) now has the ability to generate an
203
+        // automatically-colored default image when no URI/URL is specified or
204
+        // when it fails to load. In order to make the coloring permanent(ish)
205
+        // per participant, Avatar will need something permanent(ish) per
206
+        // perticipant, obviously. A participant's ID is such a piece of data.
207
+        // But the local participant changes her ID as she joins, leaves.
208
+        // TODO @lyubomir: The participants may change their avatar URLs at
209
+        // runtime which means that, if their old and new avatar URLs fail to
210
+        // download, Avatar will change their automatically-generated colors.
211
+        avatar || participant.local || (avatar = `#${participant.id}`);
201 212
     }
202 213
 
203 214
     return {

BIN
react/features/base/participants/components/defaultAvatar.png Переглянути файл


Завантаження…
Відмінити
Зберегти