ソースを参照

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

Avatars are cached to the filesystem and loaded from there when requested again.
The cache is cleaned after a conference ends and on application startup
(defensive move).

In addition, implement a fully local avatar system, which is used as a fallback
when loading a remote avatar fails. It can also be forced using a prop.

The fully local avatars use a user icon as a mask and apply a background color
qhich is picked by hashing the URI passed to the avatar. If no URI is passed a
random color is chosen.

A grace period of 1 second is also implemented so a default local avatar will be
rendered if an Avatar component is mounted but has no URI. If a URI is specified
later on, it will be loaded and displayed. In case loading the remote avatar
fails, the locally generated one will be used.
j8
Saúl Ibarra Corretgé 8年前
コミット
122ebe48c7

+ 1
- 0
android/sdk/build.gradle ファイルの表示

@@ -26,6 +26,7 @@ dependencies {
26 26
     compile 'com.facebook.react:react-native:+'
27 27
 
28 28
     compile project(':react-native-background-timer')
29
+    compile project(':react-native-fetch-blob')
29 30
     compile project(':react-native-immersive')
30 31
     compile project(':react-native-keep-awake')
31 32
     compile project(':react-native-vector-icons')

+ 2
- 2
android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetView.java ファイルの表示

@@ -84,10 +84,10 @@ public class JitsiMeetView extends FrameLayout {
84 84
                 .addPackage(new com.oblador.vectoricons.VectorIconsPackage())
85 85
                 .addPackage(new com.ocetnik.timer.BackgroundTimerPackage())
86 86
                 .addPackage(new com.oney.WebRTCModule.WebRTCModulePackage())
87
+                .addPackage(new com.RNFetchBlob.RNFetchBlobPackage())
87 88
                 .addPackage(new com.rnimmersive.RNImmersivePackage())
88 89
                 .addPackage(new org.jitsi.meet.sdk.audiomode.AudioModePackage())
89
-                .addPackage(
90
-                        new org.jitsi.meet.sdk.externalapi.ExternalAPIPackage())
90
+                .addPackage(new org.jitsi.meet.sdk.externalapi.ExternalAPIPackage())
91 91
                 .addPackage(new org.jitsi.meet.sdk.proximity.ProximityPackage())
92 92
                 .setUseDeveloperSupport(BuildConfig.DEBUG)
93 93
                 .setInitialLifecycleState(LifecycleState.RESUMED)

+ 2
- 0
android/settings.gradle ファイルの表示

@@ -3,6 +3,8 @@ rootProject.name = 'jitsi-meet'
3 3
 include ':app', ':sdk'
4 4
 include ':react-native-background-timer'
5 5
 project(':react-native-background-timer').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-background-timer/android')
6
+include ':react-native-fetch-blob'
7
+project(':react-native-fetch-blob').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fetch-blob/android')
6 8
 include ':react-native-immersive'
7 9
 project(':react-native-immersive').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-immersive/android')
8 10
 include ':react-native-keep-awake'

+ 6
- 2
ios/Podfile ファイルの表示

@@ -17,8 +17,12 @@ target 'JitsiMeet' do
17 17
   ]
18 18
   pod 'Yoga', :path => '../node_modules/react-native/ReactCommon/yoga'
19 19
 
20
-  pod 'react-native-background-timer', :path => '../node_modules/react-native-background-timer'
21
-  pod 'react-native-keep-awake', :path => '../node_modules/react-native-keep-awake'
20
+  pod 'react-native-background-timer',
21
+    :path => '../node_modules/react-native-background-timer'
22
+  pod 'react-native-fetch-blob',
23
+    :path => '../node_modules/react-native-fetch-blob'
24
+  pod 'react-native-keep-awake',
25
+    :path => '../node_modules/react-native-keep-awake'
22 26
   pod 'react-native-webrtc', :path => '../node_modules/react-native-webrtc'
23 27
   pod 'RNVectorIcons', :path => '../node_modules/react-native-vector-icons'
24 28
 end

+ 2
- 0
package.json ファイルの表示

@@ -56,6 +56,8 @@
56 56
     "react-i18next": "4.7.0",
57 57
     "react-native": "0.42.3",
58 58
     "react-native-background-timer": "1.1.0",
59
+    "react-native-fetch-blob": "0.10.6",
60
+    "react-native-img-cache": "1.4.0",
59 61
     "react-native-immersive": "0.0.5",
60 62
     "react-native-keep-awake": "2.0.4",
61 63
     "react-native-locale-detector": "1.0.1",

+ 1
- 0
react/features/app/components/App.native.js ファイルの表示

@@ -8,6 +8,7 @@ import '../../mobile/audio-mode';
8 8
 import '../../mobile/background';
9 9
 import '../../mobile/external-api';
10 10
 import '../../mobile/full-screen';
11
+import '../../mobile/image-cache';
11 12
 import '../../mobile/proximity';
12 13
 import '../../mobile/wake-lock';
13 14
 

+ 10
- 15
react/features/base/participants/components/Avatar.native.js ファイルの表示

@@ -1,5 +1,8 @@
1 1
 import React, { Component } from 'react';
2
-import { Image } from 'react-native';
2
+import { CustomCachedImage } from 'react-native-img-cache';
3
+
4
+import AvatarImage from './AvatarImage';
5
+
3 6
 
4 7
 /**
5 8
  * Implements an avatar as a React Native/mobile {@link Component}.
@@ -52,21 +55,16 @@ export default class Avatar extends Component {
52 55
      * @returns {void}
53 56
      */
54 57
     componentWillReceiveProps(nextProps) {
55
-        // uri
56 58
         const prevURI = this.props && this.props.uri;
57 59
         const nextURI = nextProps && nextProps.uri;
58
-        let nextState;
59 60
 
60 61
         if (prevURI !== nextURI || !this.state) {
61
-            nextState = {
62
-                ...nextState,
63
-
62
+            const nextState = {
64 63
                 /**
65 64
                  * The source of the {@link Image} which is the actual
66
-                 * representation of this {@link Avatar}. As {@code Avatar}
67
-                 * accepts a single URI and {@code Image} deals with a set of
68
-                 * possibly multiple URIs, the state {@code source} was
69
-                 * explicitly introduced in order to reduce unnecessary renders.
65
+                 * representation of this {@link Avatar}. The state
66
+                 * {@code source} was explicitly introduced in order to reduce
67
+                 * unnecessary renders.
70 68
                  *
71 69
                  * @type {{
72 70
                  *     uri: string
@@ -76,9 +74,7 @@ export default class Avatar extends Component {
76 74
                     uri: nextURI
77 75
                 }
78 76
             };
79
-        }
80 77
 
81
-        if (nextState) {
82 78
             if (this.state) {
83 79
                 this.setState(nextState);
84 80
             } else {
@@ -100,10 +96,9 @@ export default class Avatar extends Component {
100 96
         const { uri, ...props } = this.props;
101 97
 
102 98
         return (
103
-            <Image
99
+            <CustomCachedImage
104 100
                 { ...props }
105
-
106
-                // XXX Avatar is expected to display the whole image.
101
+                component = { AvatarImage }
107 102
                 resizeMode = 'contain'
108 103
                 source = { this.state.source } />
109 104
         );

+ 200
- 0
react/features/base/participants/components/AvatarImage.native.js ファイルの表示

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

バイナリ
react/features/base/participants/components/defaultAvatar.png ファイルの表示


+ 1
- 0
react/features/mobile/image-cache/index.js ファイルの表示

@@ -0,0 +1 @@
1
+import './middleware';

+ 28
- 0
react/features/mobile/image-cache/middleware.js ファイルの表示

@@ -0,0 +1,28 @@
1
+/* @flow */
2
+
3
+import { ImageCache } from 'react-native-img-cache';
4
+
5
+import { APP_WILL_MOUNT } from '../../app';
6
+import { CONFERENCE_FAILED, CONFERENCE_LEFT } from '../../base/conference';
7
+import { MiddlewareRegistry } from '../../base/redux';
8
+
9
+/**
10
+ * Middleware that captures conference actions and application startup in order
11
+ * cleans up the image cache.
12
+ *
13
+ * @param {Store} store - Redux store.
14
+ * @returns {Function}
15
+ */
16
+// eslint-disable-next-line no-unused-vars
17
+MiddlewareRegistry.register(store => next => action => {
18
+    switch (action.type) {
19
+    case APP_WILL_MOUNT:
20
+    case CONFERENCE_FAILED:
21
+    case CONFERENCE_LEFT:
22
+        ImageCache.get().clear();
23
+        break;
24
+
25
+    }
26
+
27
+    return next(action);
28
+});

読み込み中…
キャンセル
保存