@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 +1 -2
- package/src/apis/index.js +1 -0
- package/src/apis/notificationActions.js +47 -0
- package/src/apis/reactionActions.js +2 -1
- package/src/components/CommentReply.js +1 -1
- package/src/components/CommentSection.js +155 -16
- package/src/components/ImagePopup.js +1 -3
- package/src/components/ImageUploader.js +4 -4
- package/src/components/MediaPlayer.js +191 -43
- package/src/components/PlussChat.js +1 -3
- package/src/components/VideoPopup.js +38 -31
- package/src/components/expo-image-picker-multiple/ImageBrowser.js +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plusscommunities/pluss-core-app",
|
|
3
|
-
"version": "1.
|
|
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
|
@@ -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 {
|
|
10
|
-
import {
|
|
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
|
-
{
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
258
|
-
const rollPermission = await
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
35
|
-
}
|
|
41
|
+
componentDidMount = async () => {
|
|
42
|
+
await this.setupForPlaying();
|
|
36
43
|
|
|
37
|
-
|
|
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
|
-
|
|
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 {
|
|
54
|
-
const isUrlLink =
|
|
55
|
-
const isYoutube =
|
|
56
|
-
|
|
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
|
-
|
|
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/
|
|
67
|
-
return `https://stream.mux.com/${this.props.
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
195
|
-
|
|
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 *
|
|
328
|
+
height = heightFactor ? SCREEN_WIDTH * heightFactor : SCREEN_HEIGHT;
|
|
201
329
|
}
|
|
202
330
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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(
|
|
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={
|
|
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 {
|
|
3
|
-
import
|
|
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 './
|
|
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
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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 {
|
|
66
|
+
const { isFullScreen } = this.state;
|
|
67
|
+
const { visible, onClose, animationType, style } = this.props;
|
|
73
68
|
|
|
74
69
|
return (
|
|
75
|
-
<Modal
|
|
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
|
|
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
|
|
53
|
-
const { status: cameraRoll } = await
|
|
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',
|