@plusscommunities/pluss-core-app 1.2.2 → 1.3.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.2.2",
3
+ "version": "1.3.1",
4
4
  "description": "Core extension package for Pluss Communities platform",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -21,7 +21,6 @@
21
21
  "expo-image-picker": "~10.1.4",
22
22
  "expo-linear-gradient": "~9.1.0",
23
23
  "expo-media-library": "~12.0.2",
24
- "expo-permissions": "~12.0.1",
25
24
  "expo-screen-orientation": "~3.1.0",
26
25
  "expo-sharing": "~9.1.2",
27
26
  "expo-video-player": "^1.6.0",
package/src/apis/index.js CHANGED
@@ -4,4 +4,5 @@ export * from './followerActions';
4
4
  export * from './contactActions';
5
5
  export * from './eventActions';
6
6
  export * from './analyticsActions';
7
+ export * from './notificationActions';
7
8
  export { default as userActions } from './userActions';
@@ -0,0 +1,47 @@
1
+ import { getUrl } from '../helper';
2
+ import { authedFunction } from '../session';
3
+
4
+ export const notificationActions = {
5
+ getAppNotificationSetting: () => {
6
+ return authedFunction({
7
+ method: 'GET',
8
+ url: getUrl('notifications', 'getState/get', {
9
+ type: 'app',
10
+ }),
11
+ });
12
+ },
13
+ muteApp: () => {
14
+ return authedFunction({
15
+ method: 'POST',
16
+ url: getUrl('notifications', 'updateState/mute'),
17
+ data: { type: 'app' },
18
+ });
19
+ },
20
+ unmuteApp: () => {
21
+ return authedFunction({
22
+ method: 'POST',
23
+ url: getUrl('notifications', 'updateState/unmute'),
24
+ data: { type: 'app' },
25
+ });
26
+ },
27
+ getEntityNotificationSetting: (type, id) => {
28
+ return authedFunction({
29
+ method: 'GET',
30
+ url: getUrl('notifications', 'getState/get', { type, id }),
31
+ });
32
+ },
33
+ muteEntity: (type, id) => {
34
+ return authedFunction({
35
+ method: 'POST',
36
+ url: getUrl('notifications', 'updateState/mute'),
37
+ data: { type, id },
38
+ });
39
+ },
40
+ unmuteEntity: (type, id) => {
41
+ return authedFunction({
42
+ method: 'POST',
43
+ url: getUrl('notifications', 'updateState/unmute'),
44
+ data: { type, id },
45
+ });
46
+ },
47
+ };
@@ -26,10 +26,11 @@ export const reactionActions = {
26
26
  },
27
27
  });
28
28
  },
29
- addComment: (entityId, entityType, site, comment, image) => {
29
+ addComment: (entityId, entityType, entityName, site, comment, image) => {
30
30
  const data = {
31
31
  entityId,
32
32
  entityType,
33
+ entityName,
33
34
  site,
34
35
  comment,
35
36
  };
@@ -96,7 +96,7 @@ class CommentReply extends Component {
96
96
  this.props.commentSection.getWrappedInstance().startedAddingComment();
97
97
  }
98
98
  reactionActions
99
- .addComment(this.props.entityId, this.props.entityType, this.props.site, text, image)
99
+ .addComment(this.props.entityId, this.props.entityType, this.props.entityName, this.props.site, text, image)
100
100
  .then(res => {
101
101
  if (this.props.commentSection) {
102
102
  this.props.commentSection.getWrappedInstance().commentAdded(res.data);
@@ -1,16 +1,13 @@
1
1
  import React, { Component } from 'react';
2
- import { View, Text, TouchableOpacity, Image } from 'react-native';
2
+ import { View, Text, TouchableOpacity, Image, StyleSheet } from 'react-native';
3
3
  import _ from 'lodash';
4
4
  import moment from 'moment';
5
5
  import { connect } from 'react-redux';
6
6
  import { Icon } from 'react-native-elements';
7
- import { getPluralS, getThumb300, get1400 } from '../helper';
7
+ import { getPluralS, getThumb300, get1400, getSiteSettingFromState } from '../helper';
8
8
  import { getMainBrandingColourFromState, TEXT_DARKEST, BG_GREY, TEXT_LIGHT, LINEGREY } from '../colours';
9
- import { Spinner } from './Spinner';
10
- import { reactionActions } from '../apis';
11
- import { ConfirmPopup } from './ConfirmPopup';
12
- import { ProfilePic } from './ProfilePic';
13
- import { ImagePopup } from './ImagePopup';
9
+ import { reactionActions, notificationActions } from '../apis';
10
+ import { ConfirmPopup, ProfilePic, ImagePopup, InlineButton, Spinner } from './';
14
11
 
15
12
  class CommentSection extends Component {
16
13
  constructor(props) {
@@ -23,10 +20,15 @@ class CommentSection extends Component {
23
20
  commentToReport: null,
24
21
  commentReportedStatus: null,
25
22
  reportLoading: false,
23
+ processing: false,
24
+ muteExpiry: null,
25
+ muteLoaded: false,
26
26
  };
27
27
  }
28
28
 
29
29
  componentDidMount() {
30
+ this.getNotificationSate();
31
+
30
32
  if (!_.includes(this.props.user.hidden, 'viewComment')) {
31
33
  this.getComments();
32
34
  }
@@ -97,6 +99,64 @@ class CommentSection extends Component {
97
99
  this.props.commentReply.getWrappedInstance().focusInput();
98
100
  }
99
101
 
102
+ onMute = () => {
103
+ const { entityType, entityId } = this.props;
104
+ this.setState({ processing: true }, async () => {
105
+ try {
106
+ const { data } = await notificationActions.muteEntity(entityType, entityId);
107
+ // console.log('onMute', data);
108
+ const muteExpiry = moment(data.Expiry);
109
+ this.setState({ muteExpiry, processing: false });
110
+ } catch (error) {
111
+ console.error('onMute', error);
112
+ this.setState({ processing: false });
113
+ }
114
+ });
115
+ };
116
+
117
+ onUnmute = () => {
118
+ const { entityType, entityId } = this.props;
119
+ this.setState({ processing: true }, async () => {
120
+ try {
121
+ const { data } = await notificationActions.unmuteEntity(entityType, entityId);
122
+ // console.log('onUnmute', data);
123
+ this.setState({ muteExpiry: null, processing: false });
124
+ } catch (error) {
125
+ console.error('onUnmute', error);
126
+ this.setState({ processing: false });
127
+ }
128
+ });
129
+ };
130
+
131
+ isMuted = () => {
132
+ const { muteExpiry } = this.state;
133
+ return muteExpiry && moment() <= muteExpiry;
134
+ };
135
+
136
+ getMuteRemaining = () => {
137
+ const { muteExpiry } = this.state;
138
+ const actual = muteExpiry ? moment.duration(muteExpiry.diff(moment())).asHours() : 0;
139
+ return Math.abs(Math.ceil(actual));
140
+ };
141
+
142
+ getNotificationSate = () => {
143
+ const { processing } = this.state;
144
+ const { entityType, entityId, notificationsForComments } = this.props;
145
+ if (processing || !notificationsForComments) return;
146
+
147
+ this.setState({ processing: true }, async () => {
148
+ try {
149
+ const { data } = await notificationActions.getEntityNotificationSetting(entityType, entityId);
150
+ // console.log('getNotificationSate', data);
151
+ const muteExpiry = data ? moment(data.Expiry) : null;
152
+ this.setState({ muteExpiry, muteLoaded: true, processing: false });
153
+ } catch (error) {
154
+ console.error('getNotificationSate', error);
155
+ this.setState({ processing: false });
156
+ }
157
+ });
158
+ };
159
+
100
160
  getAdjustedSize(size) {
101
161
  if (this.props.scaleFont) {
102
162
  return size + this.props.user.fontScale;
@@ -288,6 +348,47 @@ class CommentSection extends Component {
288
348
  );
289
349
  }
290
350
 
351
+ renderMute() {
352
+ const { notificationsForComments } = this.props;
353
+ const { muteLoaded, muteExpiry, processing } = this.state;
354
+ if (!notificationsForComments || !muteLoaded) return null;
355
+
356
+ if (processing)
357
+ return (
358
+ <View style={styles.muteSpinnerContainer}>
359
+ <Spinner size={'small'} color={this.props.colourBrandingMain} />
360
+ </View>
361
+ );
362
+
363
+ const isMuted = this.isMuted();
364
+ const hours = this.getMuteRemaining();
365
+ const mutedFor = `Muted for ${hours} hour${getPluralS(hours)}`;
366
+ // console.log(muteExpiry.format('DD MMM YYYY hh:mm a'));
367
+
368
+ return (
369
+ <View style={styles.muteContainer}>
370
+ {isMuted && muteExpiry ? <Text style={[{ color: this.props.colourBrandingMain }, styles.mutedForText]}>{mutedFor}</Text> : null}
371
+ <InlineButton
372
+ onPress={isMuted ? this.onUnmute : this.onMute}
373
+ color="#fff"
374
+ style={[{ borderColor: this.props.colourBrandingMain }, styles.muteButton]}
375
+ disabled={processing}
376
+ disabledOpacity
377
+ noText
378
+ >
379
+ <View style={styles.muteButtonInner}>
380
+ <Icon
381
+ name={isMuted ? 'bell-o' : 'bell-slash-o'}
382
+ type={'font-awesome'}
383
+ iconStyle={[{ color: this.props.colourBrandingMain }, styles.muteButtonIcon]}
384
+ />
385
+ <Text style={[{ color: this.props.colourBrandingMain }, styles.muteButtonText]}>{isMuted ? 'Unmute' : 'Mute'}</Text>
386
+ </View>
387
+ </InlineButton>
388
+ </View>
389
+ );
390
+ }
391
+
291
392
  renderComments() {
292
393
  if (this.state.commentsLoading) {
293
394
  return (
@@ -317,11 +418,14 @@ class CommentSection extends Component {
317
418
  <View style={styles.commentSection}>
318
419
  {!this.isEmpty() && (
319
420
  <View style={styles.commentSectionTitleRow}>
320
- {!this.props.hideAddComment && this.state.comments.length > 2 && (
321
- <TouchableOpacity onPress={this.onGoToAdd.bind(this)}>
322
- <Text style={[styles.goToText, { color: this.props.colourBrandingMain }]}>Add a comment</Text>
323
- </TouchableOpacity>
324
- )}
421
+ <View style={{ alignItems: 'flex-end' }}>
422
+ {this.renderMute()}
423
+ {!this.props.hideAddComment && this.state.comments.length > 2 && (
424
+ <TouchableOpacity onPress={this.onGoToAdd.bind(this)}>
425
+ <Text style={[styles.goToText, { color: this.props.colourBrandingMain }]}>Add a comment</Text>
426
+ </TouchableOpacity>
427
+ )}
428
+ </View>
325
429
  <Text style={[styles.commentCount, { fontSize: this.getAdjustedSize(15) }]}>
326
430
  {this.state.comments.length}
327
431
  {` comment${getPluralS(this.state.comments.length)}`}
@@ -374,22 +478,22 @@ class CommentSection extends Component {
374
478
  }
375
479
  }
376
480
 
377
- const styles = {
481
+ const styles = StyleSheet.create({
378
482
  commentSectionOuter: {
379
483
  flex: 1,
380
484
  paddingHorizontal: 16,
381
485
  },
382
486
  commentSection: {
383
487
  flex: 1,
384
- paddingVertical: 16,
488
+ paddingVertical: 12,
385
489
  borderTopColor: LINEGREY,
386
490
  borderTopWidth: 1,
387
- borderTopStyle: 'solid',
388
491
  },
389
492
  commentCount: {
390
493
  fontFamily: 'sf-semibold',
391
494
  color: TEXT_DARKEST,
392
495
  flex: 1,
496
+ marginTop: 4,
393
497
  },
394
498
  comment: {
395
499
  marginTop: 16,
@@ -463,12 +567,47 @@ const styles = {
463
567
  reportLoadingContainer: {
464
568
  paddingVertical: 10,
465
569
  },
466
- };
570
+ goToText: {
571
+ marginTop: 4,
572
+ },
573
+ muteSpinnerContainer: {
574
+ width: 100,
575
+ marginBottom: 6,
576
+ },
577
+ muteContainer: {
578
+ flexDirection: 'row',
579
+ alignItems: 'center',
580
+ justifyContent: 'flex-end',
581
+ marginBottom: 6,
582
+ },
583
+ mutedForText: {
584
+ fontFamily: 'sf-semibold',
585
+ fontSize: 13,
586
+ marginRight: 8,
587
+ },
588
+ muteButton: {
589
+ borderWidth: 1,
590
+ paddingHorizontal: 8,
591
+ },
592
+ muteButtonInner: {
593
+ flexDirection: 'row',
594
+ alignItems: 'center',
595
+ },
596
+ muteButtonIcon: {
597
+ fontSize: 14,
598
+ },
599
+ muteButtonText: {
600
+ fontFamily: 'sf-semibold',
601
+ fontSize: 14,
602
+ marginLeft: 6,
603
+ },
604
+ });
467
605
 
468
606
  const mapStateToProps = state => {
469
607
  return {
470
608
  colourBrandingMain: getMainBrandingColourFromState(state),
471
609
  user: state.user,
610
+ notificationsForComments: getSiteSettingFromState(state, 'NotificationsForComments', false),
472
611
  };
473
612
  };
474
613
 
@@ -110,9 +110,7 @@ class ImagePopup extends Component {
110
110
  const { showFullscreenVideo, currentVideoUrl } = this.state;
111
111
  if (!currentVideoUrl) return;
112
112
 
113
- return (
114
- <VideoPopup uri={currentVideoUrl} visible={showFullscreenVideo} showFullscreenButton={false} onClose={this.toggleFullscreenVideo} />
115
- );
113
+ return <VideoPopup uri={currentVideoUrl} visible={showFullscreenVideo} onClose={this.toggleFullscreenVideo} />;
116
114
  }
117
115
 
118
116
  render() {
@@ -3,7 +3,7 @@ import { View, ScrollView, TouchableOpacity, Text, Platform, Linking, Modal, Dim
3
3
  import { connect } from 'react-redux';
4
4
  import { Icon } from 'react-native-elements';
5
5
  import _ from 'lodash';
6
- import * as Permissions from 'expo-permissions';
6
+ import { Camera } from 'expo-camera';
7
7
  import * as MediaLibrary from 'expo-media-library';
8
8
  import * as ImageManipulator from 'expo-image-manipulator';
9
9
  import * as ImagePicker from 'expo-image-picker';
@@ -60,7 +60,7 @@ class ImageUploader extends Component {
60
60
  }
61
61
 
62
62
  loadLocalAlbums = async () => {
63
- const hasPermission = await Permissions.getAsync(Permissions.CAMERA_ROLL);
63
+ const hasPermission = await MediaLibrary.getPermissionsAsync();
64
64
  if (!hasPermission.granted) return;
65
65
 
66
66
  this.setState({ loadingLocalFolders: true }, async () => {
@@ -254,8 +254,8 @@ class ImageUploader extends Component {
254
254
  }
255
255
 
256
256
  askPermissionsAsync = async () => {
257
- const cameraPermission = await Permissions.askAsync(Permissions.CAMERA);
258
- const rollPermission = await Permissions.askAsync(Permissions.CAMERA_ROLL);
257
+ const cameraPermission = await Camera.requestCameraPermissionsAsync();
258
+ const rollPermission = await MediaLibrary.requestPermissionsAsync();
259
259
  if (cameraPermission.status !== 'granted' || rollPermission.status !== 'granted') {
260
260
  this.showWarningPopup(cameraPermission.status !== 'granted', rollPermission.status !== 'granted');
261
261
  return false;
@@ -1,14 +1,17 @@
1
1
  import React, { Component } from 'react';
2
- import { View, StyleSheet, Dimensions } from 'react-native';
2
+ import { Platform, View, StyleSheet, Dimensions } from 'react-native';
3
+ import * as ScreenOrientation from 'expo-screen-orientation';
4
+ import { DeviceMotion } from 'expo-sensors';
3
5
  import YoutubePlayer, { getYoutubeMeta } from 'react-native-youtube-iframe';
4
- // import { Vimeo } from 'react-native-vimeo-iframe';
6
+ import { Vimeo } from 'react-native-vimeo-iframe';
5
7
  import WebView from 'react-native-webview';
6
8
  import { Video } from 'expo-av';
9
+ import VideoPlayer from 'expo-video-player';
7
10
  import { Spinner } from './Spinner';
8
11
 
9
12
  const SCREEN_HEIGHT = Dimensions.get('window').height;
10
13
  const SCREEN_WIDTH = Dimensions.get('window').width;
11
- const EXPO_VIDEO_PROPS = { rate: 1, isMuted: false, volume: 1, shouldPlay: true };
14
+ const EXPO_VIDEO_PROPS = { rate: 1, isMuted: false, volume: 1 };
12
15
  const RETRY_RECOVER = 30000;
13
16
  const RETRY_LOADING = 10000;
14
17
 
@@ -16,27 +19,42 @@ class MediaPlayer extends Component {
16
19
  constructor(props) {
17
20
  super(props);
18
21
  this.state = {
22
+ deviceOrientation: null,
23
+ isLandscape: false,
19
24
  isUrlLink: true,
20
25
  isYoutube: false,
26
+ isVimeo: false,
21
27
  forceWebview: false,
22
- heightFactor: 0.565,
28
+ heightFactor: 0,
23
29
  playbackLoaded: false,
24
30
  playbackBuffering: false,
25
31
  playbackPlaying: false,
32
+ readyToRender: false,
26
33
  };
27
34
  this.youtubePlayer = null;
28
35
  this.videoPlayer = null;
29
36
  this.checkStreamLoaded = null;
30
37
  this.retryLoading = false;
38
+ this.orientationQueue = [];
31
39
  }
32
40
 
33
- componentDidMount() {
34
- this.setupForLiveStreaming();
35
- }
41
+ componentDidMount = async () => {
42
+ await this.setupForPlaying();
36
43
 
37
- componentWillUnmount() {
44
+ ScreenOrientation.lockAsync(Platform.OS === 'ios' ? ScreenOrientation.OrientationLock.DEFAULT : ScreenOrientation.OrientationLock.ALL);
45
+ const motionAvailalble = await DeviceMotion.isAvailableAsync();
46
+ // console.log('motionAvailalble', motionAvailalble);
47
+ if (motionAvailalble) {
48
+ DeviceMotion.setUpdateInterval(500);
49
+ DeviceMotion.addListener(this.onMotionChange);
50
+ }
51
+ };
52
+
53
+ componentWillUnmount = () => {
38
54
  if (this.checkStreamLoaded) clearInterval(this.checkStreamLoaded);
39
- }
55
+ DeviceMotion.removeAllListeners();
56
+ ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP);
57
+ };
40
58
 
41
59
  getYoutubeVideoId = url => {
42
60
  url = url.replace(/(>|<)/gi, '').split(/(vi\/|v=|\/v\/|youtu\.be\/|\/embed\/)/);
@@ -48,23 +66,36 @@ class MediaPlayer extends Component {
48
66
  }
49
67
  };
50
68
 
51
- setupForLiveStreaming = async () => {
69
+ getVimeoId = url => {
70
+ const result = url.match(
71
+ /(?:www\.|player\.)?vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/(?:[^\/]*)\/videos\/|album\/(?:\d+)\/video\/|video\/|)(\d+)(?:[a-zA-Z0-9_\-]+)?/i,
72
+ );
73
+ return result[1];
74
+ };
75
+
76
+ setupForPlaying = async () => {
52
77
  // 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 });
78
+ const { source, useVideoPlayer } = this.props;
79
+ const isUrlLink = source && source.startsWith('http');
80
+ const isYoutube = source && (source.includes('youtube.com/watch?v=') || source.includes('youtu.be/'));
81
+ const isVimeo = source && source.includes('vimeo.com/');
82
+ let heightFactor = 0;
83
+
84
+ if (isYoutube) {
85
+ const metadata = await getYoutubeMeta(this.getYoutubeVideoId(source));
86
+ heightFactor = metadata.height / metadata.width;
87
+ } else if (useVideoPlayer) {
88
+ heightFactor = 0;
89
+ } else {
90
+ heightFactor = 0.565;
91
+ }
57
92
 
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
- // }
93
+ this.setState({ isUrlLink, isYoutube, isVimeo, playbackLoaded: isYoutube || isVimeo, heightFactor, readyToRender: true });
63
94
  };
64
95
 
65
96
  getStreamPlaybackUrl = () => {
66
- // return 'https://stream.mux.com/nflNhyFQMlrSt00II01Hjh2BLSW009PH1tl023pdpJ01s1vc.m3u8';
67
- return `https://stream.mux.com/${this.props.streamInfo.playbackId}.m3u8`;
97
+ // return 'https://stream.mux.com/Ym4nKktRpGuZNBBBlzhxpyRwZLpvMlPRchcu01x6Ur600.m3u8';
98
+ return `https://stream.mux.com/${this.props.playbackId}.m3u8`;
68
99
  };
69
100
 
70
101
  checkReloadStream = (wait = RETRY_LOADING, force = false) => {
@@ -90,6 +121,48 @@ class MediaPlayer extends Component {
90
121
  }, wait);
91
122
  };
92
123
 
124
+ isLandscape = deviceRotation => {
125
+ const { beta, gamma } = deviceRotation;
126
+ const absGamma = Math.abs(gamma);
127
+ const absBeta = Math.abs(beta);
128
+ const isGammaNegative = Math.sign(gamma) === -1;
129
+
130
+ if (absGamma <= 0.04 && absBeta <= 0.24) {
131
+ //Portrait mode, on a flat surface.
132
+ return false;
133
+ } else if ((absGamma <= 1.0 || absGamma >= 2.3) && absBeta >= 0.5) {
134
+ //General Portrait mode, accounting for forward and back tilt on the top of the phone.
135
+ return false;
136
+ } else {
137
+ if (isGammaNegative) {
138
+ //Landscape mode with the top of the phone to the left.
139
+ return true;
140
+ } else {
141
+ //Landscape mode with the top of the phone to the right.
142
+ return true;
143
+ }
144
+ }
145
+ };
146
+
147
+ onMotionChange = async changeEvent => {
148
+ const deviceRotation = changeEvent.rotation;
149
+ const deviceOrientation = changeEvent.orientation;
150
+ const isLandscape = this.isLandscape(deviceRotation);
151
+ this.orientationQueue.push(isLandscape);
152
+ if (this.orientationQueue.length > 2) this.orientationQueue = this.orientationQueue.slice(-2);
153
+ // console.log('onMotionChange - orientationQueue', this.orientationQueue);
154
+ if (
155
+ isLandscape !== this.state.isLandscape &&
156
+ isLandscape === (Math.abs(deviceOrientation) === 90) &&
157
+ this.orientationQueue.every(i => i === isLandscape)
158
+ ) {
159
+ this.setState({ isLandscape }, () => {
160
+ // console.log('isLandscape', this.state.isLandscape);
161
+ if (this.props.orientationChanged) this.props.orientationChanged(this.state.isLandscape);
162
+ });
163
+ }
164
+ };
165
+
93
166
  onStreamLoadStart = () => {
94
167
  console.log('Stream load started');
95
168
  this.checkStreamLoaded = setInterval(() => {
@@ -137,16 +210,24 @@ class MediaPlayer extends Component {
137
210
 
138
211
  onYoutubeReady = () => {
139
212
  // console.log('onYoutubeReady');
140
- this.setState({ playbackLoaded: true });
141
213
  };
142
214
 
143
- getYoutubePlayer = (youtubeId, width, height) => (
215
+ onVideoPlayStatusUpdate = status => {
216
+ // console.log('onVideoPlayStatusUpdate', status);
217
+ };
218
+
219
+ onVideoPlayError = error => {
220
+ // console.log('onVideoPlayError', error);
221
+ this.setState({ forceWebview: true });
222
+ };
223
+
224
+ getYoutubePlayer = (youtubeId, width, height, autoPlay) => (
144
225
  <YoutubePlayer
145
226
  ref={ref => (this.youtubePlayer = ref)}
146
227
  height={height}
147
228
  width={width}
148
229
  videoId={youtubeId}
149
- play
230
+ play={autoPlay}
150
231
  // onChangeState={event => console.log('onChangeState', event)}
151
232
  onReady={this.onYoutubeReady}
152
233
  onError={this.onYoutubeError}
@@ -160,6 +241,24 @@ class MediaPlayer extends Component {
160
241
  />
161
242
  );
162
243
 
244
+ getVimeoPlayer = (vimeoId, width, height, autoPlay) => (
245
+ <View style={{ width, height }}>
246
+ <Vimeo
247
+ style={{ marginLeft: -8, marginRight: -5 }}
248
+ videoId={vimeoId}
249
+ // onReady={() => console.log('Video is ready')}
250
+ // onPlay={() => console.log('Video is playing')}
251
+ // onPlayProgress={data => console.log('Video progress data:', data)}
252
+ // onFinish={() => console.log('Video is finished')}
253
+ loop={false}
254
+ autoPlay={autoPlay}
255
+ controls={true}
256
+ speed={true}
257
+ time={'0m0s'}
258
+ />
259
+ </View>
260
+ );
261
+
163
262
  getWebviewPlayer = embedUrl => (
164
263
  <WebView
165
264
  startInLoadingState
@@ -172,13 +271,14 @@ class MediaPlayer extends Component {
172
271
  />
173
272
  );
174
273
 
175
- getVideoPlayer = (embedUrl, width, height) => (
274
+ getStreamPlayer = (embedUrl, width, height, autoPlay) => (
176
275
  <Video
177
276
  ref={vp => {
178
277
  this.videoPlayer = vp;
179
278
  }}
180
279
  source={{ uri: embedUrl }}
181
280
  {...EXPO_VIDEO_PROPS}
281
+ shouldPlay={autoPlay}
182
282
  resizeMode={Video.RESIZE_MODE_CONTAIN}
183
283
  useNativeControls
184
284
  style={{ width, height }}
@@ -189,37 +289,83 @@ class MediaPlayer extends Component {
189
289
  />
190
290
  );
191
291
 
292
+ getVideoPlayer = (embedUrl, width, height, autoPlay) => (
293
+ <VideoPlayer
294
+ videoProps={{
295
+ shouldPlay: autoPlay,
296
+ resizeMode: Video.RESIZE_MODE_CONTAIN,
297
+ source: { uri: embedUrl },
298
+ }}
299
+ showFullscreenButton={false}
300
+ errorCallback={this.onVideoPlayError}
301
+ playbackCallback={this.onVideoPlayStatusUpdate}
302
+ height={height}
303
+ width={width}
304
+ />
305
+ );
306
+
192
307
  render() {
193
308
  let width, height;
194
- const { landscape, streamInfo } = this.props;
195
- if (landscape) {
309
+ const { playbackId, source, autoPlay, useVideoPlayer } = this.props;
310
+ const {
311
+ readyToRender,
312
+ isLandscape,
313
+ heightFactor,
314
+ isUrlLink,
315
+ isYoutube,
316
+ isVimeo,
317
+ forceWebview,
318
+ playbackLoaded,
319
+ playbackBuffering,
320
+ } = this.state;
321
+ if (!readyToRender) return null;
322
+
323
+ if (isLandscape) {
196
324
  width = SCREEN_HEIGHT;
197
325
  height = SCREEN_WIDTH;
198
326
  } else {
199
327
  width = SCREEN_WIDTH;
200
- height = SCREEN_WIDTH * this.state.heightFactor;
328
+ height = heightFactor ? SCREEN_WIDTH * heightFactor : SCREEN_HEIGHT;
201
329
  }
202
330
 
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;
331
+ let embedUrl, player, showLoading;
332
+ if (!forceWebview) {
333
+ if (isYoutube) {
334
+ const youtubeId = this.getYoutubeVideoId(source);
335
+ embedUrl = youtubeId ? `https://www.youtube.com/embed/${youtubeId}` : source;
336
+ player = this.getYoutubePlayer(youtubeId, width, height, autoPlay);
337
+ } else if (isVimeo) {
338
+ const vimeoId = this.getVimeoId(source);
339
+ embedUrl = source;
340
+ player = this.getVimeoPlayer(vimeoId, width, height, autoPlay);
341
+ } else if (playbackId) {
342
+ embedUrl = this.getStreamPlaybackUrl();
343
+ player = this.getStreamPlayer(embedUrl, width, height, autoPlay);
344
+ showLoading = true;
345
+ } else if (useVideoPlayer) {
346
+ embedUrl = source;
347
+ player = this.getVideoPlayer(embedUrl, width, height, autoPlay);
348
+ }
349
+ }
350
+ if (!player && isUrlLink) {
351
+ embedUrl = source;
212
352
  player = this.getWebviewPlayer(embedUrl);
213
- } else if (playbackId) {
214
- embedUrl = this.getStreamPlaybackUrl();
215
- player = this.getVideoPlayer(embedUrl, width, height);
216
353
  }
217
354
 
218
- // console.log('Streaming:', embedUrl, 'landscape', landscape, 'playbackLoaded', playbackLoaded, 'playbackBuffering', playbackBuffering);
355
+ // console.log({
356
+ // Streaming: embedUrl,
357
+ // Landscape: isLandscape,
358
+ // Loaded: playbackLoaded,
359
+ // Buffering: playbackBuffering,
360
+ // LoadingIndicator: showLoading,
361
+ // Width: width,
362
+ // Height: height,
363
+ // HeightFactor: heightFactor,
364
+ // });
219
365
  return (
220
- <View style={[styles.container, !isUrlLink && { alignItems: 'center' }, { width, height }]}>
366
+ <View style={styles.container}>
221
367
  {player}
222
- {(!playbackLoaded || playbackBuffering) && (
368
+ {showLoading && (!playbackLoaded || playbackBuffering) && (
223
369
  <View style={styles.loadingContainer}>
224
370
  <Spinner color={'#fff'} />
225
371
  </View>
@@ -232,6 +378,8 @@ class MediaPlayer extends Component {
232
378
  const styles = StyleSheet.create({
233
379
  container: {
234
380
  backgroundColor: '#000',
381
+ alignItems: 'center',
382
+ justifyContent: 'center',
235
383
  },
236
384
  webView: {
237
385
  flex: 1,
@@ -713,9 +713,7 @@ class PlussChat extends Component {
713
713
  const { showFullscreenVideo, currentVideoUrl } = this.state;
714
714
  if (!currentVideoUrl) return;
715
715
 
716
- return (
717
- <VideoPopup uri={currentVideoUrl} visible={showFullscreenVideo} showFullscreenButton={false} onClose={this.toggleFullscreenVideo} />
718
- );
716
+ return <VideoPopup uri={currentVideoUrl} visible={showFullscreenVideo} onClose={this.toggleFullscreenVideo} />;
719
717
  }
720
718
 
721
719
  renderPDF() {
@@ -1,12 +1,9 @@
1
1
  import React, { Component } from 'react';
2
- import { Dimensions, Modal, TouchableOpacity, StyleSheet } from 'react-native';
3
- import { Video } from 'expo-av';
4
- import VideoPlayer from 'expo-video-player';
2
+ import { Platform, View, Modal, TouchableOpacity, StyleSheet } from 'react-native';
3
+ import * as ScreenOrientation from 'expo-screen-orientation';
5
4
  import { StatusBarHeight, getCompressed, imageExists } from '../helper';
6
5
  import { Pl60Icon } from '../fonts';
7
- import { SharingTools } from './SharingTools';
8
-
9
- const SCREEN_HEIGHT = Dimensions.get('window').height;
6
+ import { SharingTools, MediaPlayer } from './';
10
7
 
11
8
  class VideoPopup extends Component {
12
9
  constructor(props) {
@@ -14,18 +11,26 @@ class VideoPopup extends Component {
14
11
 
15
12
  this.state = {
16
13
  uri: '',
14
+ isFullScreen: false,
17
15
  };
18
16
  }
19
17
 
20
- UNSAFE_componentWillMount() {
18
+ UNSAFE_componentWillMount = async () => {
21
19
  this.checkCompressed(this.props.uri);
22
- }
20
+ await ScreenOrientation.lockAsync(
21
+ Platform.OS === 'ios' ? ScreenOrientation.OrientationLock.DEFAULT : ScreenOrientation.OrientationLock.ALL,
22
+ );
23
+ };
23
24
 
24
25
  UNSAFE_componentWillReceiveProps(nextProps) {
25
26
  if (this.props.uri === nextProps.uri) return;
26
27
  this.checkCompressed(nextProps.uri);
27
28
  }
28
29
 
30
+ componentWillUnmount = async () => {
31
+ await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP);
32
+ };
33
+
29
34
  checkCompressed = url => {
30
35
  const compressedUri = getCompressed(url);
31
36
  imageExists(compressedUri).then(compressedExists => {
@@ -33,11 +38,6 @@ class VideoPopup extends Component {
33
38
  });
34
39
  };
35
40
 
36
- playbackCallback = status => {
37
- const { playbackCallback } = this.props;
38
- if (playbackCallback) playbackCallback(status);
39
- };
40
-
41
41
  renderClose() {
42
42
  return (
43
43
  <TouchableOpacity style={styles.menuIconContainer} onPress={this.props.onClose} activeOpacity={0.6}>
@@ -47,35 +47,36 @@ class VideoPopup extends Component {
47
47
  }
48
48
 
49
49
  renderPlayer() {
50
- const { shouldPlay, showFullscreenButton, switchToPortrait, switchToLandscape } = this.props;
51
50
  const { uri } = this.state;
52
51
  if (!uri) return null;
53
- console.log(`playing video\ncurrent: ${uri}\noriginal: ${this.props.uri}`);
54
52
 
55
53
  return (
56
- <VideoPlayer
57
- videoProps={{
58
- shouldPlay: shouldPlay || true,
59
- resizeMode: Video.RESIZE_MODE_CONTAIN,
60
- source: { uri },
61
- }}
62
- showFullscreenButton={showFullscreenButton}
63
- switchToPortrait={switchToPortrait}
64
- switchToLandscape={switchToLandscape}
65
- playbackCallback={this.playbackCallback}
66
- height={SCREEN_HEIGHT}
67
- />
54
+ <View style={styles.playerContainer}>
55
+ <MediaPlayer
56
+ source={uri}
57
+ useVideoPlayer={true}
58
+ autoPlay={true}
59
+ orientationChanged={isFullScreen => this.setState({ isFullScreen })}
60
+ />
61
+ </View>
68
62
  );
69
63
  }
70
64
 
71
65
  render() {
72
- const { visible, onClose, style } = this.props;
66
+ const { isFullScreen } = this.state;
67
+ const { visible, onClose, animationType, style } = this.props;
73
68
 
74
69
  return (
75
- <Modal visible={visible} animationType="fade" onRequestClose={onClose} style={[styles.container, style]}>
70
+ <Modal
71
+ visible={visible}
72
+ animationType={animationType || 'fade'}
73
+ onRequestClose={onClose}
74
+ style={[styles.container, style]}
75
+ supportedOrientations={['portrait', 'landscape']}
76
+ >
76
77
  {this.renderPlayer()}
77
- {this.renderClose()}
78
- <SharingTools uri={this.props.uri} />
78
+ {!isFullScreen && this.renderClose()}
79
+ {!isFullScreen && <SharingTools uri={this.props.uri} />}
79
80
  </Modal>
80
81
  );
81
82
  }
@@ -101,6 +102,12 @@ const styles = StyleSheet.create({
101
102
  color: '#fff',
102
103
  zIndex: 3,
103
104
  },
105
+ playerContainer: {
106
+ flex: 1,
107
+ backgroundColor: '#000',
108
+ alignItems: 'center',
109
+ justifyContent: 'center',
110
+ },
104
111
  });
105
112
 
106
113
  export { VideoPopup };
@@ -3,7 +3,7 @@ import { StyleSheet, View, FlatList, Dimensions, ActivityIndicator, Platform } f
3
3
  import _ from 'lodash';
4
4
  import * as ScreenOrientation from 'expo-screen-orientation';
5
5
  import * as MediaLibrary from 'expo-media-library';
6
- import * as Permissions from 'expo-permissions';
6
+ import { Camera } from 'expo-camera';
7
7
  import { isVideo } from '../../helper';
8
8
  import ImageTile from './ImageTile';
9
9
 
@@ -49,8 +49,8 @@ export default class ImageBrowser extends React.Component {
49
49
  };
50
50
 
51
51
  getPermissionsAsync = async () => {
52
- const { status: camera } = await Permissions.askAsync(Permissions.CAMERA);
53
- const { status: cameraRoll } = await Permissions.askAsync(Permissions.CAMERA_ROLL);
52
+ const { status: camera } = await Camera.requestCameraPermissionsAsync();
53
+ const { status: cameraRoll } = await MediaLibrary.requestPermissionsAsync();
54
54
  this.setState({
55
55
  hasCameraPermission: camera === 'granted',
56
56
  hasCameraRollPermission: cameraRoll === 'granted',