@plusscommunities/pluss-core-app 1.1.3 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plusscommunities/pluss-core-app",
3
- "version": "1.1.3",
3
+ "version": "1.2.1",
4
4
  "description": "Core extension package for Pluss Communities platform",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -36,6 +36,7 @@
36
36
  "react-native-image-zoom-viewer": "^3.0.1",
37
37
  "react-native-iphone-x-helper": "^1.3.1",
38
38
  "react-native-webview": "11.2.3",
39
+ "react-native-youtube-iframe": "^2.2.1",
39
40
  "react-redux": "^5.0.5"
40
41
  },
41
42
  "peerDependencies": {
@@ -276,6 +276,7 @@ class Header extends Component {
276
276
  <Animated.View
277
277
  style={[
278
278
  styles.container,
279
+ !this.props.noShadow && styles.containerShadow,
279
280
  this.props.transparent && styles.transparentContainer,
280
281
  this.props.style,
281
282
  { backgroundColor: this.props.headerColourOffset, elevation: this.props.headerElev, shadowColor: this.props.headerShadow },
@@ -297,6 +298,7 @@ class Header extends Component {
297
298
  <View
298
299
  style={[
299
300
  styles.container,
301
+ !this.props.noShadow && styles.containerShadow,
300
302
  this.props.transparent && styles.transparentContainer,
301
303
  this.props.lineSeparated && styles.lineSeparated,
302
304
  this.props.popupHeader && styles.popupHeader,
@@ -331,6 +333,9 @@ const styles = {
331
333
  container: {
332
334
  alignSelf: 'stretch',
333
335
  backgroundColor: '#fff',
336
+ zIndex: 10,
337
+ },
338
+ containerShadow: {
334
339
  shadowColor: '#000',
335
340
  shadowOffset: {
336
341
  width: 0,
@@ -339,7 +344,6 @@ const styles = {
339
344
  shadowOpacity: 0.1,
340
345
  shadowRadius: 6,
341
346
  elevation: 3,
342
- zIndex: 10,
343
347
  },
344
348
  lineSeparated: {
345
349
  borderBottomWidth: 1,
@@ -1,27 +1,17 @@
1
1
  import React, { Component } from 'react';
2
- import { View, StyleSheet, Animated } from 'react-native';
3
- import { TEXT_BLUEGREY } from '../colours';
2
+ import { StyleSheet, Animated, Dimensions } from 'react-native';
3
+ import { connect } from 'react-redux';
4
+ import { getMainBrandingColourFromState } from '../colours';
4
5
 
5
- const OPACITY_START = 0.1;
6
- const OPACITY_END1 = 0.4;
7
- const OPACITY_END2 = 0.7;
8
- const OPACITY_END3 = 1;
9
- const LEFT_START = 1;
10
- const LEFT_END1 = 16;
11
- const LEFT_END2 = 32;
12
- const CIRCLE_SIZE = 8;
13
- const DEFAULT_HEIGHT = 38;
6
+ const DEFAULT_HEIGHT = 4;
7
+ const DEFAULT_WIDTH = Dimensions.get('window').width;
14
8
 
15
9
  class LoadingIndicator extends Component {
16
10
  constructor(props) {
17
11
  super(props);
18
12
  this.state = {
19
- animatedOpacity1: new Animated.Value(OPACITY_START),
20
- animatedOpacity2: new Animated.Value(OPACITY_START),
21
- animatedOpacity3: new Animated.Value(OPACITY_START),
22
- animatedOpacity4: new Animated.Value(0),
23
- animatedLeft4: new Animated.Value(LEFT_START),
24
- animatedHeight: new Animated.Value(this.props.visible ? DEFAULT_HEIGHT : 0),
13
+ animatedLeft: new Animated.Value(-DEFAULT_WIDTH),
14
+ animatedOpacity: new Animated.Value(0),
25
15
  };
26
16
  this.height = 0;
27
17
  }
@@ -36,8 +26,8 @@ class LoadingIndicator extends Component {
36
26
 
37
27
  componentDidUpdate(prevProps) {
38
28
  if (prevProps.visible !== this.props.visible) {
39
- Animated.timing(this.state.animatedHeight, {
40
- toValue: this.props.visible ? DEFAULT_HEIGHT : 0,
29
+ Animated.timing(this.state.animatedOpacity, {
30
+ toValue: this.props.visible ? 0.8 : 0,
41
31
  duration: 300,
42
32
  useNativeDriver: false,
43
33
  }).start();
@@ -46,95 +36,32 @@ class LoadingIndicator extends Component {
46
36
 
47
37
  animate = () => {
48
38
  Animated.sequence([
49
- Animated.timing(this.state.animatedOpacity4, {
50
- toValue: OPACITY_END1,
51
- duration: 750,
39
+ Animated.timing(this.state.animatedLeft, {
40
+ toValue: DEFAULT_WIDTH,
41
+ duration: 1500,
52
42
  useNativeDriver: false,
53
43
  }),
54
- Animated.parallel([
55
- Animated.timing(this.state.animatedOpacity1, {
56
- toValue: OPACITY_END1,
57
- duration: 250,
58
- useNativeDriver: false,
59
- }),
60
- Animated.timing(this.state.animatedLeft4, {
61
- toValue: LEFT_END1,
62
- duration: 250,
63
- useNativeDriver: false,
64
- }),
65
- ]),
66
- Animated.timing(this.state.animatedOpacity4, {
67
- toValue: OPACITY_END2,
68
- duration: 250,
69
- useNativeDriver: false,
70
- }),
71
- Animated.parallel([
72
- Animated.timing(this.state.animatedOpacity2, {
73
- toValue: OPACITY_END2,
74
- duration: 250,
75
- useNativeDriver: false,
76
- }),
77
- Animated.timing(this.state.animatedLeft4, {
78
- toValue: LEFT_END2,
79
- duration: 250,
80
- useNativeDriver: false,
81
- }),
82
- ]),
83
- Animated.parallel([
84
- Animated.timing(this.state.animatedOpacity4, {
85
- toValue: OPACITY_END3,
86
- duration: 250,
87
- useNativeDriver: false,
88
- }),
89
- Animated.timing(this.state.animatedOpacity3, {
90
- toValue: OPACITY_END3,
91
- duration: 250,
92
- useNativeDriver: false,
93
- }),
94
- ]),
95
- Animated.parallel([
96
- Animated.timing(this.state.animatedLeft4, {
97
- toValue: LEFT_START,
98
- duration: 1,
99
- useNativeDriver: false,
100
- }),
101
- Animated.timing(this.state.animatedOpacity4, {
102
- toValue: 0,
103
- duration: 1,
104
- useNativeDriver: false,
105
- }),
106
- ]),
107
- Animated.timing(this.state.animatedOpacity3, {
108
- toValue: OPACITY_START,
109
- duration: 250,
110
- useNativeDriver: false,
111
- }),
112
- Animated.timing(this.state.animatedOpacity2, {
113
- toValue: OPACITY_START,
114
- duration: 250,
115
- useNativeDriver: false,
116
- }),
117
- Animated.timing(this.state.animatedOpacity1, {
118
- toValue: OPACITY_START,
119
- duration: 250,
44
+ Animated.delay(500),
45
+ Animated.timing(this.state.animatedLeft, {
46
+ toValue: -DEFAULT_WIDTH,
47
+ duration: 1500,
120
48
  useNativeDriver: false,
121
49
  }),
50
+ Animated.delay(500),
122
51
  ]).start(() => {
123
52
  this.animate();
124
53
  });
125
54
  };
126
55
 
127
56
  render() {
128
- const { animatedOpacity1, animatedOpacity2, animatedOpacity3, animatedOpacity4, animatedLeft4, animatedHeight } = this.state;
129
-
130
57
  return (
131
- <Animated.View style={[styles.container, this.props.style, { height: animatedHeight }]} onLayout={this.onLayout}>
132
- <View style={styles.innerContainer}>
133
- <Animated.View style={[styles.circle, { opacity: animatedOpacity1 }]} />
134
- <Animated.View style={[styles.circle, { opacity: animatedOpacity2 }]} />
135
- <Animated.View style={[styles.circle, { opacity: animatedOpacity3 }]} />
136
- <Animated.View style={[styles.circle, { position: 'absolute', left: animatedLeft4, opacity: animatedOpacity4 }]} />
137
- </View>
58
+ <Animated.View
59
+ style={[styles.container, this.props.style, { height: DEFAULT_HEIGHT, opacity: this.state.animatedOpacity }]}
60
+ onLayout={this.onLayout}
61
+ >
62
+ <Animated.View
63
+ style={[styles.line, { backgroundColor: this.props.colourBrandingMain, left: this.state.animatedLeft, height: DEFAULT_HEIGHT }]}
64
+ />
138
65
  </Animated.View>
139
66
  );
140
67
  }
@@ -145,20 +72,23 @@ const styles = StyleSheet.create({
145
72
  alignItems: 'center',
146
73
  justifyContent: 'center',
147
74
  overflow: 'hidden',
148
- backgroundColor: '#ebeff2',
75
+ backgroundColor: '#fff',
149
76
  width: '100%',
150
77
  height: 38,
151
78
  },
152
79
  innerContainer: {
153
80
  flexDirection: 'row',
154
81
  },
155
- circle: {
156
- width: CIRCLE_SIZE,
157
- height: CIRCLE_SIZE,
158
- borderRadius: CIRCLE_SIZE / 2,
159
- backgroundColor: TEXT_BLUEGREY,
160
- marginHorizontal: 4,
82
+ line: {
83
+ width: DEFAULT_WIDTH,
161
84
  },
162
85
  });
163
86
 
164
- export default LoadingIndicator;
87
+ const mapStateToProps = state => {
88
+ return {
89
+ colourBrandingMain: getMainBrandingColourFromState(state),
90
+ };
91
+ };
92
+
93
+ export default connect(mapStateToProps, {})(LoadingIndicator);
94
+ // export { loadingIndicator as Default };
@@ -0,0 +1,250 @@
1
+ import React, { Component } from 'react';
2
+ import { View, StyleSheet, Dimensions } from 'react-native';
3
+ import YoutubePlayer, { getYoutubeMeta } from 'react-native-youtube-iframe';
4
+ // import { Vimeo } from 'react-native-vimeo-iframe';
5
+ import WebView from 'react-native-webview';
6
+ import { Video } from 'expo-av';
7
+ import { Spinner } from './Spinner';
8
+
9
+ const SCREEN_HEIGHT = Dimensions.get('window').height;
10
+ const SCREEN_WIDTH = Dimensions.get('window').width;
11
+ const EXPO_VIDEO_PROPS = { rate: 1, isMuted: false, volume: 1, shouldPlay: true };
12
+ const RETRY_RECOVER = 30000;
13
+ const RETRY_LOADING = 10000;
14
+
15
+ class MediaPlayer extends Component {
16
+ constructor(props) {
17
+ super(props);
18
+ this.state = {
19
+ isUrlLink: true,
20
+ isYoutube: false,
21
+ forceWebview: false,
22
+ heightFactor: 0.565,
23
+ playbackLoaded: false,
24
+ playbackBuffering: false,
25
+ playbackPlaying: false,
26
+ };
27
+ this.youtubePlayer = null;
28
+ this.videoPlayer = null;
29
+ this.checkStreamLoaded = null;
30
+ this.retryLoading = false;
31
+ }
32
+
33
+ componentDidMount() {
34
+ this.setupForLiveStreaming();
35
+ }
36
+
37
+ componentWillUnmount() {
38
+ if (this.checkStreamLoaded) clearInterval(this.checkStreamLoaded);
39
+ }
40
+
41
+ getYoutubeVideoId = url => {
42
+ url = url.replace(/(>|<)/gi, '').split(/(vi\/|v=|\/v\/|youtu\.be\/|\/embed\/)/);
43
+ if (url[2] !== undefined) {
44
+ const ids = url[2].split(/[^0-9a-z_\-]/i);
45
+ return ids[0];
46
+ } else {
47
+ return '';
48
+ }
49
+ };
50
+
51
+ setupForLiveStreaming = async () => {
52
+ // Extract live stream info
53
+ const { streamUrl } = this.props.streamInfo;
54
+ const isUrlLink = streamUrl.startsWith('http');
55
+ const isYoutube = streamUrl.includes('youtube.com/watch?v=') || streamUrl.includes('youtu.be/');
56
+ this.setState({ isUrlLink, isYoutube });
57
+
58
+ // if (isYoutube) {
59
+ // const metadata = await getYoutubeMeta(this.getYoutubeVideoId(streamUrl));
60
+ // this.setState({ heightFactor: metadata.height / metadata.width });
61
+ // console.log('youtube metadata', metadata);
62
+ // }
63
+ };
64
+
65
+ getStreamPlaybackUrl = () => {
66
+ // return 'https://stream.mux.com/nflNhyFQMlrSt00II01Hjh2BLSW009PH1tl023pdpJ01s1vc.m3u8';
67
+ return `https://stream.mux.com/${this.props.streamInfo.playbackId}.m3u8`;
68
+ };
69
+
70
+ checkReloadStream = (wait = RETRY_LOADING, force = false) => {
71
+ if (!force && this.retryLoading) return;
72
+
73
+ console.log(`Check loading in ${wait} milliseconds...`);
74
+ this.retryLoading = true;
75
+ setTimeout(async () => {
76
+ const { playbackLoaded, playbackBuffering } = this.state;
77
+ try {
78
+ if (playbackLoaded === false || playbackBuffering === true) {
79
+ console.log('Reloading started');
80
+ await this.videoPlayer.loadAsync(
81
+ { uri: this.getStreamPlaybackUrl(), overrideFileExtensionAndroid: 'm3u8' },
82
+ EXPO_VIDEO_PROPS,
83
+ false,
84
+ );
85
+ } else {
86
+ console.log('Already loaded - not reloading');
87
+ }
88
+ this.retryLoading = false;
89
+ } catch {}
90
+ }, wait);
91
+ };
92
+
93
+ onStreamLoadStart = () => {
94
+ console.log('Stream load started');
95
+ this.checkStreamLoaded = setInterval(() => {
96
+ const { playbackLoaded } = this.state;
97
+ if (playbackLoaded === false) {
98
+ console.log('Stream loading failed unexpectedly');
99
+ this.checkReloadStream(1000, true);
100
+ } else {
101
+ clearInterval(this.checkStreamLoaded);
102
+ }
103
+ }, RETRY_RECOVER);
104
+ };
105
+
106
+ onStreamLoaded = status => {
107
+ console.log(`Stream loaded - isLoaded:${status.isLoaded}, isBuffering:${status.isBuffering}, isPlaying:${status.isPlaying}`);
108
+ };
109
+
110
+ onStreamError = error => {
111
+ console.log('Stream error', error);
112
+ };
113
+
114
+ onStreamStatusUpdate = status => {
115
+ this.setState(
116
+ {
117
+ playbackLoaded: status.isLoaded,
118
+ playbackBuffering: status.isBuffering,
119
+ playbackPlaying: status.isPlaying,
120
+ },
121
+ () => {
122
+ const { playbackLoaded, playbackBuffering, playbackPlaying, shouldPlay } = this.state;
123
+ // console.log(`Status updated - isLoaded:${playbackLoaded}, isBuffering:${playbackBuffering}, isPlaying:${playbackPlaying}`);
124
+ if (playbackLoaded === false && playbackBuffering === undefined && playbackPlaying === undefined) {
125
+ this.checkReloadStream();
126
+ } else if (playbackLoaded === true && playbackBuffering === true) {
127
+ this.checkReloadStream(RETRY_RECOVER);
128
+ }
129
+ },
130
+ );
131
+ };
132
+
133
+ onYoutubeError = e => {
134
+ // console.log('onYoutubeError', e);
135
+ this.setState({ forceWebview: true });
136
+ };
137
+
138
+ onYoutubeReady = () => {
139
+ // console.log('onYoutubeReady');
140
+ this.setState({ playbackLoaded: true });
141
+ };
142
+
143
+ getYoutubePlayer = (youtubeId, width, height) => (
144
+ <YoutubePlayer
145
+ ref={ref => (this.youtubePlayer = ref)}
146
+ height={height}
147
+ width={width}
148
+ videoId={youtubeId}
149
+ play
150
+ // onChangeState={event => console.log('onChangeState', event)}
151
+ onReady={this.onYoutubeReady}
152
+ onError={this.onYoutubeError}
153
+ // onPlaybackQualityChange={q => console.log('onPlaybackQualityChange', q)}
154
+ volume={50}
155
+ playbackRate={1}
156
+ playerParams={{
157
+ cc_lang_pref: 'us',
158
+ showClosedCaptions: true,
159
+ }}
160
+ />
161
+ );
162
+
163
+ getWebviewPlayer = embedUrl => (
164
+ <WebView
165
+ startInLoadingState
166
+ javaScriptEnabled
167
+ scrollEnabled={false}
168
+ automaticallyAdjustContentInsets={false}
169
+ mediaPlaybackRequiresUserAction
170
+ style={styles.webView}
171
+ source={{ uri: embedUrl }}
172
+ />
173
+ );
174
+
175
+ getVideoPlayer = (embedUrl, width, height) => (
176
+ <Video
177
+ ref={vp => {
178
+ this.videoPlayer = vp;
179
+ }}
180
+ source={{ uri: embedUrl }}
181
+ {...EXPO_VIDEO_PROPS}
182
+ resizeMode={Video.RESIZE_MODE_CONTAIN}
183
+ useNativeControls
184
+ style={{ width, height }}
185
+ onLoadStart={this.onStreamLoadStart}
186
+ onLoad={this.onStreamLoaded}
187
+ onError={this.onStreamError}
188
+ onPlaybackStatusUpdate={this.onStreamStatusUpdate}
189
+ />
190
+ );
191
+
192
+ render() {
193
+ let width, height;
194
+ const { landscape, streamInfo } = this.props;
195
+ if (landscape) {
196
+ width = SCREEN_HEIGHT;
197
+ height = SCREEN_WIDTH;
198
+ } else {
199
+ width = SCREEN_WIDTH;
200
+ height = SCREEN_WIDTH * this.state.heightFactor;
201
+ }
202
+
203
+ const { isUrlLink, isYoutube, forceWebview, playbackLoaded, playbackBuffering } = this.state;
204
+ const { playbackId, streamUrl } = streamInfo;
205
+ let embedUrl, player;
206
+ if (isYoutube && !forceWebview) {
207
+ const youtubeId = this.getYoutubeVideoId(streamUrl);
208
+ embedUrl = youtubeId ? `https://www.youtube.com/embed/${youtubeId}` : streamUrl;
209
+ player = this.getYoutubePlayer(youtubeId, width, height);
210
+ } else if (isUrlLink) {
211
+ embedUrl = streamUrl;
212
+ player = this.getWebviewPlayer(embedUrl);
213
+ } else if (playbackId) {
214
+ embedUrl = this.getStreamPlaybackUrl();
215
+ player = this.getVideoPlayer(embedUrl, width, height);
216
+ }
217
+
218
+ // console.log('Streaming:', embedUrl, 'landscape', landscape, 'playbackLoaded', playbackLoaded, 'playbackBuffering', playbackBuffering);
219
+ return (
220
+ <View style={[styles.container, !isUrlLink && { alignItems: 'center' }, { width, height }]}>
221
+ {player}
222
+ {(!playbackLoaded || playbackBuffering) && (
223
+ <View style={styles.loadingContainer}>
224
+ <Spinner color={'#fff'} />
225
+ </View>
226
+ )}
227
+ </View>
228
+ );
229
+ }
230
+ }
231
+
232
+ const styles = StyleSheet.create({
233
+ container: {
234
+ backgroundColor: '#000',
235
+ },
236
+ webView: {
237
+ flex: 1,
238
+ },
239
+ loadingContainer: {
240
+ position: 'absolute',
241
+ top: 50,
242
+ left: 0,
243
+ right: 0,
244
+ bottom: 50,
245
+ justifyContent: 'center',
246
+ alignItems: 'center',
247
+ },
248
+ });
249
+
250
+ export default MediaPlayer;
@@ -47,3 +47,4 @@ export { default as UserListing } from './UserListing';
47
47
  export { default as PlussChat } from './PlussChat';
48
48
  export { default as PositionedImage } from './PositionedImage';
49
49
  export { default as FormattedText } from './FormattedText';
50
+ export { default as MediaPlayer } from './MediaPlayer';
package/src/helper.js CHANGED
@@ -91,6 +91,20 @@ const isVideo = url => {
91
91
  return ['mov', 'mp4'].includes(extension);
92
92
  };
93
93
 
94
+ const isVideoUrl = url => {
95
+ const urlToCheck = getValidUrl(url);
96
+ if (isVideo(urlToCheck)) {
97
+ return true;
98
+ }
99
+ if (isYoutube(urlToCheck)) {
100
+ return true;
101
+ }
102
+ if (isVimeo(urlToCheck)) {
103
+ return true;
104
+ }
105
+ return false;
106
+ };
107
+
94
108
  const isStockImage = url => {
95
109
  return url.indexOf('https://pluss60-dev-uploads.s3.ap-southeast-2.amazonaws.com/uploads/library') !== -1;
96
110
  };
@@ -300,6 +314,29 @@ const isTablet = () => {
300
314
  return Dimensions.get('screen').width > 600;
301
315
  };
302
316
 
317
+ const isVimeo = url => {
318
+ if (_.isEmpty(url)) {
319
+ return false;
320
+ }
321
+ if (url.indexOf('https://vimeo.com/') === 0) {
322
+ return true;
323
+ }
324
+ if (url.indexOf('https://player.vimeo.com/video/') === 0) {
325
+ return true;
326
+ }
327
+ };
328
+
329
+ const getVimeoEmbed = url => {
330
+ if (_.isEmpty(url)) {
331
+ return '';
332
+ }
333
+ if (url.indexOf('https://vimeo.com/') === 0) {
334
+ const vimeoId = 'lol'; //TODO
335
+ return `https://player.vimeo.com/video/${vimeoId}`;
336
+ }
337
+ return url; //TODO
338
+ };
339
+
303
340
  const isYoutube = url => {
304
341
  if (_.isUndefined(url) || _.isEmpty(url)) {
305
342
  return false;
@@ -386,4 +423,5 @@ export {
386
423
  getValidUrl,
387
424
  getUserPreview,
388
425
  allowComments,
426
+ isVideoUrl,
389
427
  };