소스 검색

[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
+}

BIN
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
+});

Loading…
취소
저장