@plusscommunities/pluss-core-app 1.3.1 → 1.4.4-beta.0

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,10 +1,13 @@
1
1
  {
2
2
  "name": "@plusscommunities/pluss-core-app",
3
- "version": "1.3.1",
3
+ "version": "1.4.4-beta.0",
4
4
  "description": "Core extension package for Pluss Communities platform",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
7
+ "prepatch": "npm version prepatch --preid=beta",
7
8
  "patch": "npm version patch",
9
+ "preupload": "npm publish --access public --tag beta && rm -rf node_modules",
10
+ "preupload:p": "npm run prepatch && npm run preupload",
8
11
  "upload": "npm publish --access public && rm -rf node_modules",
9
12
  "upload:p": "npm run patch && npm run upload"
10
13
  },
@@ -12,30 +15,33 @@
12
15
  "license": "ISC",
13
16
  "dependencies": {
14
17
  "@expo/vector-icons": "^12.0.0",
15
- "aws-amplify": "^3.4.0",
16
- "aws-amplify-react-native": "^4.3.3",
17
- "expo-av": "~9.1.2",
18
- "expo-constants": "~10.1.3",
19
- "expo-file-system": "~11.0.2",
20
- "expo-image-manipulator": "~9.1.0",
21
- "expo-image-picker": "~10.1.4",
22
- "expo-linear-gradient": "~9.1.0",
23
- "expo-media-library": "~12.0.2",
24
- "expo-screen-orientation": "~3.1.0",
25
- "expo-sharing": "~9.1.2",
18
+ "@react-native-async-storage/async-storage": "~1.15.0",
19
+ "@react-native-community/netinfo": "7.1.3",
20
+ "@react-native-picker/picker": "2.2.1",
21
+ "aws-amplify": "^4.3.11",
22
+ "aws-amplify-react-native": "^6.0.2",
23
+ "expo-av": "~10.2.0",
24
+ "expo-constants": "~13.0.0",
25
+ "expo-file-system": "~13.1.0",
26
+ "expo-image-manipulator": "~10.2.0",
27
+ "expo-image-picker": "~12.0.1",
28
+ "expo-linear-gradient": "~11.0.0",
29
+ "expo-media-library": "~14.0.0",
30
+ "expo-screen-orientation": "~4.1.1",
31
+ "expo-sharing": "~10.1.0",
26
32
  "expo-video-player": "^1.6.0",
27
33
  "js-cookie": "^2.2.1",
28
34
  "lodash": "^4.17.4",
29
35
  "mime-types": "^2.1.24",
30
36
  "moment": "^2.18.1",
31
- "react": "16.13.1",
32
- "react-native": "https://github.com/expo/react-native/archive/sdk-41.0.0.tar.gz",
37
+ "react": "17.0.1",
38
+ "react-native": "0.64.3",
33
39
  "react-native-auto-height-image": "3.1.3",
34
40
  "react-native-elements": "^0.17.0",
35
41
  "react-native-image-zoom-viewer": "^3.0.1",
36
42
  "react-native-iphone-x-helper": "^1.3.1",
37
43
  "react-native-vimeo-iframe": "^1.0.4",
38
- "react-native-webview": "11.2.3",
44
+ "react-native-webview": "11.15.0",
39
45
  "react-native-youtube-iframe": "^2.2.1",
40
46
  "react-redux": "^5.0.5"
41
47
  },
package/src/apis/index.js CHANGED
@@ -5,4 +5,6 @@ export * from './contactActions';
5
5
  export * from './eventActions';
6
6
  export * from './analyticsActions';
7
7
  export * from './notificationActions';
8
+ export * from './typeActions';
8
9
  export { default as userActions } from './userActions';
10
+ export { default as profileActions } from './profileActions';
@@ -0,0 +1,13 @@
1
+ import { getUrl } from '../helper';
2
+ import { authedFunction } from '../session';
3
+
4
+ const profileActions = {
5
+ getUserTagsBySite: site => {
6
+ return authedFunction({
7
+ method: 'GET',
8
+ url: getUrl('profile', 'usertags/site', { site }),
9
+ });
10
+ },
11
+ };
12
+
13
+ export default profileActions;
@@ -26,13 +26,14 @@ export const reactionActions = {
26
26
  },
27
27
  });
28
28
  },
29
- addComment: (entityId, entityType, entityName, site, comment, image) => {
29
+ addComment: (entityId, entityType, entityName, site, comment, image, parentId) => {
30
30
  const data = {
31
31
  entityId,
32
32
  entityType,
33
33
  entityName,
34
34
  site,
35
35
  comment,
36
+ parentId,
36
37
  };
37
38
  if (!_.isEmpty(image)) data.image = image;
38
39
  return authedFunction({
@@ -0,0 +1,13 @@
1
+ import { getUrl } from '../helper';
2
+ import { authedFunction } from '../session';
3
+
4
+ export const typeActions = {
5
+ getUserTypes: site => {
6
+ const url = getUrl('types', 'getusertypes');
7
+ return authedFunction({
8
+ method: 'POST',
9
+ url,
10
+ data: { site },
11
+ });
12
+ },
13
+ };
@@ -0,0 +1,61 @@
1
+ import React, { PureComponent } from 'react';
2
+ import { StyleSheet } from 'react-native';
3
+ import { connect } from 'react-redux';
4
+ import { FormCardSectionOptionLauncher } from './FormCardSectionOptionLauncher';
5
+ import { getMainBrandingColourFromState, TEXT_DARK } from '../colours';
6
+ import { Services } from '../config';
7
+
8
+ class AudienceSelectorLauncher extends PureComponent {
9
+ onPressAudience = () => {
10
+ const { user, audienceType, audienceTypeSelection } = this.props;
11
+
12
+ Services.navigation.navigate('audienceSelectorPage', {
13
+ site: user.site,
14
+ audienceType: audienceType || 'Custom',
15
+ audienceTypeSelection: audienceTypeSelection || [],
16
+ onChange: (audienceType, audienceTypeSelection) => {
17
+ if (this.props.onChange) this.props.onChange(audienceType, audienceTypeSelection);
18
+ },
19
+ });
20
+ };
21
+
22
+ render() {
23
+ const { style, textStyle, audienceTypeSelection } = this.props;
24
+ const selected =
25
+ audienceTypeSelection && audienceTypeSelection.length > 0
26
+ ? `Current selection: ${audienceTypeSelection.map(i => i.Title).join(', ')}`
27
+ : '';
28
+
29
+ return (
30
+ <FormCardSectionOptionLauncher
31
+ sectionStyle={[styles.audienceSection, style]}
32
+ textStyle={[styles.audienceText, textStyle]}
33
+ onPress={this.onPressAudience}
34
+ title="Audience"
35
+ description={selected}
36
+ value={selected ? 'Custom' : 'All Users'}
37
+ />
38
+ );
39
+ }
40
+ }
41
+
42
+ const styles = StyleSheet.create({
43
+ audienceSection: {
44
+ paddingHorizontal: 0,
45
+ },
46
+ audienceText: {
47
+ fontFamily: 'sf-semibold',
48
+ fontSize: 16,
49
+ color: TEXT_DARK,
50
+ },
51
+ });
52
+
53
+ const mapStateToProps = state => {
54
+ return {
55
+ user: state.user,
56
+ colourBrandingMain: getMainBrandingColourFromState(state),
57
+ };
58
+ };
59
+
60
+ const audienceSelectorLauncher = connect(mapStateToProps, {})(AudienceSelectorLauncher);
61
+ export { audienceSelectorLauncher as AudienceSelectorLauncher };
@@ -0,0 +1,344 @@
1
+ import React, { Component } from 'react';
2
+ import { connect } from 'react-redux';
3
+ import _ from 'lodash';
4
+ import { TouchableOpacity, View, ScrollView, Text, StyleSheet } from 'react-native';
5
+ import { Icon } from 'react-native-elements';
6
+ import { Services } from '../config';
7
+ import { FormCard } from './FormCard';
8
+ import { FormCardSection } from './FormCardSection';
9
+ import { InlineButton } from './InlineButton';
10
+ import Header from './Header';
11
+ import { Spinner } from './Spinner';
12
+ import { TEXT_DARK, COLOUR_GREEN, BG_GREY, INACTIVE_BUTTON, TEXT_LIGHT, getMainBrandingColourFromState } from '../colours';
13
+ import { typeActions, profileActions } from '../apis';
14
+
15
+ class AudienceSelectorPage extends Component {
16
+ constructor(props) {
17
+ super(props);
18
+
19
+ this.state = {
20
+ loading: false,
21
+ categories: [
22
+ {
23
+ name: 'All Primary Users',
24
+ key: 'resident',
25
+ },
26
+ {
27
+ name: 'All Staff Users',
28
+ key: 'staff',
29
+ },
30
+ {
31
+ name: 'All Linked Users',
32
+ key: 'family',
33
+ },
34
+ ],
35
+ types: [],
36
+ tags: [],
37
+ combinedList: [],
38
+ seeAll: false,
39
+ };
40
+ }
41
+
42
+ componentDidMount = async () => {
43
+ try {
44
+ this.setState({ loading: true });
45
+ await this.getUserTypes();
46
+ await this.getUserTags();
47
+ this.setState({ loading: false, combinedList: this.getAvailableAudienceTags() });
48
+ } catch (error) {
49
+ console.error('loading types error', error);
50
+ this.setState({ loading: false });
51
+ }
52
+ };
53
+
54
+ getUserTypes = async () => {
55
+ const { data } = await typeActions.getUserTypes(this.props.site);
56
+ data.forEach(e => {
57
+ e.name = e.category ? `(${e.category[0].toUpperCase() + e.category.substring(1)}) ${e.displayName}` : e.displayName;
58
+ e.key = e.typeName;
59
+ });
60
+ // console.log('getUserTypes', data);
61
+ this.setState({ types: data });
62
+ };
63
+
64
+ getUserTags = async () => {
65
+ const { data } = await profileActions.getUserTagsBySite(this.props.site);
66
+ data.forEach(e => {
67
+ e.name = e.Title;
68
+ e.key = e.Id;
69
+ });
70
+ // console.log('getUserTags', data);
71
+ this.setState({ tags: data });
72
+ };
73
+
74
+ getAvailableAudienceTags = () => {
75
+ const { categories, types, tags } = this.state;
76
+ const { audienceTypeSelection } = this.props;
77
+
78
+ const categoryTags = categories.map(c => {
79
+ const Id = `category_${c.key}`;
80
+ return {
81
+ AudienceType: 'Category',
82
+ AudienceTypeSelection: c.key,
83
+ Id,
84
+ Title: c.name,
85
+ Selected: audienceTypeSelection.some(i => i.Id === Id),
86
+ };
87
+ });
88
+ const userTypeTags = types.map(t => {
89
+ const Id = `userType_${t.typeName}`;
90
+ return {
91
+ AudienceType: 'UserType',
92
+ AudienceTypeSelection: t.typeName,
93
+ Id,
94
+ Title: `User Type: ${t.displayName}`,
95
+ Selected: audienceTypeSelection.some(i => i.Id === Id),
96
+ };
97
+ });
98
+ const userTagTags = tags.map(t => {
99
+ const Id = `userTag_${t.Id}`;
100
+ return {
101
+ AudienceType: 'UserTags',
102
+ AudienceTypeSelection: t.Id,
103
+ Id,
104
+ Title: `User Tag: ${t.Title}`,
105
+ Selected: audienceTypeSelection.some(i => i.Id === Id),
106
+ };
107
+ });
108
+ return [...categoryTags, ...userTypeTags, ...userTagTags];
109
+ };
110
+
111
+ onPressBack = () => {
112
+ Services.navigation.goBack();
113
+ };
114
+
115
+ onSelectAll = () => {
116
+ const newList = [...this.state.combinedList];
117
+ newList.forEach(i => (i.Selected = false));
118
+ this.setState({ combinedList: newList });
119
+ };
120
+
121
+ onSeeAll = () => {
122
+ this.setState({ seeAll: !this.state.seeAll });
123
+ };
124
+
125
+ onToggleAudienceOption = option => {
126
+ const newList = [...this.state.combinedList];
127
+ const selected = newList.find(i => i.Id === option.Id);
128
+ if (selected) {
129
+ selected.Selected = !selected.Selected;
130
+ this.setState({ combinedList: newList });
131
+ }
132
+ };
133
+
134
+ onDone = () => {
135
+ if (this.props.onChange) {
136
+ const { combinedList } = this.state;
137
+ const selected = combinedList.filter(i => i.Selected);
138
+ if (selected && selected.length > 0) {
139
+ this.props.onChange('Custom', selected);
140
+ } else {
141
+ this.props.onChange(null, null);
142
+ }
143
+ }
144
+ Services.navigation.goBack();
145
+ };
146
+
147
+ renderOption(label, selected, onSelect, key = null, hasUnderline = true) {
148
+ return (
149
+ <TouchableOpacity key={key} onPress={onSelect}>
150
+ <FormCardSection hasUnderline={hasUnderline} hasContent>
151
+ <View style={styles.labelContainer}>
152
+ <Text style={styles.labelText}>{label}</Text>
153
+ <Icon
154
+ name="check-circle"
155
+ type="font-awesome"
156
+ iconStyle={[{ color: INACTIVE_BUTTON, fontSize: 20 }, selected && { color: COLOUR_GREEN }]}
157
+ />
158
+ </View>
159
+ </FormCardSection>
160
+ </TouchableOpacity>
161
+ );
162
+ }
163
+
164
+ renderSelectAll() {
165
+ const { combinedList, loading } = this.state;
166
+ const allSelected = !combinedList.find(i => i.Selected);
167
+ if (loading) return null;
168
+
169
+ return (
170
+ <FormCard style={styles.selectAllContainer}>{this.renderOption('All Users', allSelected, this.onSelectAll, null, false)}</FormCard>
171
+ );
172
+ }
173
+
174
+ renderSelection() {
175
+ const { loading, combinedList, seeAll } = this.state;
176
+ if (loading) return null;
177
+
178
+ const selectedText = combinedList
179
+ .filter(i => i.Selected)
180
+ .map(i => i.Title)
181
+ .join(', ');
182
+ const hasSelected = !_.isEmpty(selectedText);
183
+
184
+ return (
185
+ <View style={styles.selectionContainer}>
186
+ <View style={styles.selectionContainerInner}>
187
+ <Text style={styles.selectionTitle}>or select from below</Text>
188
+ {hasSelected ? (
189
+ <TouchableOpacity onPress={this.onSeeAll}>
190
+ <Text style={styles.seeAllButton}>{seeAll ? 'See less' : 'See all'}</Text>
191
+ </TouchableOpacity>
192
+ ) : null}
193
+ </View>
194
+ {hasSelected ? (
195
+ <Text style={styles.selectionText} numberOfLines={seeAll ? null : 1}>{`Current selection: ${selectedText}`}</Text>
196
+ ) : null}
197
+ </View>
198
+ );
199
+ }
200
+
201
+ renderTips() {
202
+ return (
203
+ <View style={styles.tipContainer}>
204
+ <Text style={styles.tipText}>
205
+ <Text style={{ fontFamily: 'sf-semibold' }}>Tip: </Text>
206
+ Group your users using User Tags from your website Community Manager
207
+ </Text>
208
+ </View>
209
+ );
210
+ }
211
+
212
+ renderAvailableAudiences() {
213
+ const { loading, combinedList } = this.state;
214
+
215
+ return (
216
+ <ScrollView style={styles.availableScrollContainer} contentContainerStyle={styles.availabelScrollContent}>
217
+ <FormCard>
218
+ {combinedList.map((option, index) => {
219
+ const notLast = index < combinedList.length - 1;
220
+ return this.renderOption(option.Title, option.Selected, () => this.onToggleAudienceOption(option), index, notLast);
221
+ })}
222
+ </FormCard>
223
+ {loading ? <Spinner /> : null}
224
+ {this.renderTips()}
225
+ </ScrollView>
226
+ );
227
+ }
228
+
229
+ renderButtons() {
230
+ const { loading } = this.state;
231
+
232
+ return (
233
+ <View style={styles.doneButtonContainer}>
234
+ <InlineButton
235
+ color={loading ? INACTIVE_BUTTON : this.props.colourBrandingMain}
236
+ onPress={this.onDone}
237
+ touchableStyle={styles.doneButton}
238
+ fillTouchable
239
+ large
240
+ disabled={loading}
241
+ >
242
+ Done
243
+ </InlineButton>
244
+ </View>
245
+ );
246
+ }
247
+
248
+ render() {
249
+ return (
250
+ <View style={styles.container}>
251
+ <Header leftIcon="angle-left" onPressLeft={this.onPressBack} text={'Select Audience'} />
252
+ {this.renderSelectAll()}
253
+ {this.renderSelection()}
254
+ {this.renderAvailableAudiences()}
255
+ {this.renderButtons()}
256
+ </View>
257
+ );
258
+ }
259
+ }
260
+
261
+ const styles = StyleSheet.create({
262
+ container: {
263
+ flex: 1,
264
+ position: 'relative',
265
+ backgroundColor: BG_GREY,
266
+ },
267
+ selectAllContainer: {
268
+ marginTop: 20,
269
+ },
270
+ selectionContainer: {
271
+ padding: 20,
272
+ },
273
+ selectionContainerInner: {
274
+ flexDirection: 'row',
275
+ justifyContent: 'space-between',
276
+ },
277
+ selectionTitle: {
278
+ fontFamily: 'sf-bold',
279
+ fontSize: 14,
280
+ color: TEXT_DARK,
281
+ },
282
+ selectionText: {
283
+ fontFamily: 'sf-bold',
284
+ fontSize: 14,
285
+ color: TEXT_DARK,
286
+ marginTop: 10,
287
+ },
288
+ seeAllButton: {
289
+ marginLeft: 10,
290
+ fontFamily: 'sf-bold',
291
+ fontSize: 14,
292
+ color: TEXT_LIGHT,
293
+ },
294
+ availableScrollContainer: {
295
+ flex: 1,
296
+ },
297
+ availabelScrollContent: {
298
+ flexGrow: 1,
299
+ justifyContent: 'space-between',
300
+ flexDirection: 'column',
301
+ },
302
+ tipContainer: {
303
+ padding: 10,
304
+ },
305
+ tipText: {
306
+ fontSize: 14,
307
+ fontFamily: 'sf-regular',
308
+ color: TEXT_DARK,
309
+ },
310
+ labelContainer: {
311
+ flexDirection: 'row',
312
+ justifyContent: 'space-between',
313
+ },
314
+ labelText: {
315
+ fontFamily: 'sf-medium',
316
+ fontSize: 16,
317
+ color: TEXT_DARK,
318
+ },
319
+ description: {
320
+ marginTop: 5,
321
+ fontSize: 14,
322
+ fontFamily: 'sf-regular',
323
+ color: TEXT_DARK,
324
+ },
325
+ doneButtonContainer: {
326
+ backgroundColor: '#fff',
327
+ marginTop: 1,
328
+ flexDirection: 'row',
329
+ paddingTop: 10,
330
+ paddingBottom: 20,
331
+ },
332
+ doneButton: {
333
+ flex: 1,
334
+ marginHorizontal: 6,
335
+ },
336
+ });
337
+
338
+ const mapStateToProps = state => {
339
+ return {
340
+ colourBrandingMain: getMainBrandingColourFromState(state),
341
+ };
342
+ };
343
+
344
+ export default connect(mapStateToProps, {})(AudienceSelectorPage);
@@ -2,6 +2,7 @@ import React, { Component } from 'react';
2
2
  import { connect } from 'react-redux';
3
3
  import { Keyboard, TextInput, View, ImageBackground, TouchableOpacity, Dimensions } from 'react-native';
4
4
  import { Icon } from 'react-native-elements';
5
+ import { getBottomSpace } from 'react-native-iphone-x-helper';
5
6
  import _ from 'lodash';
6
7
  import { getApiError } from '../session';
7
8
  import { getShadowStyle } from '../helper';
@@ -26,9 +27,9 @@ class CommentReply extends Component {
26
27
  }
27
28
 
28
29
  componentDidMount() {
29
- if (this.props.commentSection) {
30
+ if (this.props.commentSection && this.props.commentSection.current) {
30
31
  this.setState({
31
- commentsLoading: this.props.commentSection.getWrappedInstance().isLoading(),
32
+ commentsLoading: this.props.commentSection.current.getWrappedInstance().isLoading(),
32
33
  });
33
34
  }
34
35
  }
@@ -39,7 +40,7 @@ class CommentReply extends Component {
39
40
  });
40
41
  setTimeout(() => {
41
42
  if (!this.props.noScroll) {
42
- this.props.scrollView.scrollToEnd({ animated: true });
43
+ this.props.scrollView?.current?.scrollToEnd({ animated: true });
43
44
  }
44
45
  }, 500);
45
46
  };
@@ -81,7 +82,7 @@ class CommentReply extends Component {
81
82
  addComment() {
82
83
  setTimeout(() => {
83
84
  if (!this.props.noScroll) {
84
- this.props.scrollView.scrollToEnd({ animated: true });
85
+ this.props.scrollView?.current?.scrollToEnd({ animated: true });
85
86
  }
86
87
  }, 500);
87
88
  const text = this.state.commentInput;
@@ -93,20 +94,20 @@ class CommentReply extends Component {
93
94
  });
94
95
  Keyboard.dismiss();
95
96
  if (this.props.commentSection) {
96
- this.props.commentSection.getWrappedInstance().startedAddingComment();
97
+ this.props.commentSection?.current?.getWrappedInstance().startedAddingComment();
97
98
  }
98
99
  reactionActions
99
- .addComment(this.props.entityId, this.props.entityType, this.props.entityName, this.props.site, text, image)
100
+ .addComment(this.props.entityId, this.props.entityType, this.props.entityName, this.props.site, text, image, this.props.threadId)
100
101
  .then(res => {
101
102
  if (this.props.commentSection) {
102
- this.props.commentSection.getWrappedInstance().commentAdded(res.data);
103
+ this.props.commentSection?.current?.getWrappedInstance().commentAdded(res.data);
103
104
  }
104
105
  this.setState({
105
106
  addingComment: false,
106
107
  });
107
108
  setTimeout(() => {
108
109
  if (!this.props.noScroll) {
109
- this.props.scrollView.scrollToEnd({ animated: true });
110
+ this.props.scrollView?.current?.scrollToEnd({ animated: true });
110
111
  }
111
112
  }, 500);
112
113
  })
@@ -125,7 +126,7 @@ class CommentReply extends Component {
125
126
  if (this.state.uploadingCommentImage || !_.isEmpty(this.state.commentImageInput)) {
126
127
  return;
127
128
  }
128
- this.commentImageUploader.getWrappedInstance().showUploadMenu();
129
+ this.commentImageUploader.showUploadMenu();
129
130
  }
130
131
 
131
132
  renderImageAttachment() {
@@ -181,9 +182,11 @@ class CommentReply extends Component {
181
182
 
182
183
  render() {
183
184
  if (_.includes(this.props.user.hidden, 'addComment')) {
185
+ console.log('no way');
184
186
  return null;
185
187
  }
186
188
  if (this.state.commentsLoading) {
189
+ console.log('still loading');
187
190
  return null;
188
191
  }
189
192
  return (
@@ -202,12 +205,12 @@ class CommentReply extends Component {
202
205
  value={this.state.commentInput}
203
206
  onFocus={() => {
204
207
  if (!this.props.noScroll) {
205
- this.props.scrollView.scrollToEnd({ animated: true });
208
+ this.props.scrollView?.current?.scrollToEnd({ animated: true });
206
209
  }
207
210
  }}
208
211
  onChangeText={value => {
209
212
  if (!this.props.noScroll) {
210
- this.props.scrollView.scrollToEnd({ animated: false });
213
+ this.props.scrollView?.current?.scrollToEnd({ animated: false });
211
214
  }
212
215
  this.setState({
213
216
  commentInput: value,
@@ -290,6 +293,7 @@ const styles = {
290
293
  backgroundColor: '#fff',
291
294
  paddingVertical: 8,
292
295
  paddingHorizontal: 8,
296
+ paddingBottom: 8 + getBottomSpace(),
293
297
  flexGrow: 1,
294
298
  },
295
299
  inputText: {
@@ -329,5 +333,5 @@ const mapStateToProps = state => {
329
333
  return { user };
330
334
  };
331
335
 
332
- const commentReply = connect(mapStateToProps, {}, null, { withRef: true })(CommentReply);
336
+ const commentReply = connect(mapStateToProps, {}, null, { forwardRef: true })(CommentReply);
333
337
  export { commentReply as CommentReply };
@@ -4,10 +4,15 @@ 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, getSiteSettingFromState } from '../helper';
7
+ import { getPluralS, getThumb300, get1400, getSiteSettingFromState, getFirstName, getPluralOptions } from '../helper';
8
8
  import { getMainBrandingColourFromState, TEXT_DARKEST, BG_GREY, TEXT_LIGHT, LINEGREY } from '../colours';
9
9
  import { reactionActions, notificationActions } from '../apis';
10
- import { ConfirmPopup, ProfilePic, ImagePopup, InlineButton, Spinner } from './';
10
+ import { ConfirmPopup } from './ConfirmPopup';
11
+ import { ProfilePic } from './ProfilePic';
12
+ import { ImagePopup } from './ImagePopup';
13
+ import { InlineButton } from './InlineButton';
14
+ import { Spinner } from './Spinner';
15
+ import { Services } from '../config';
11
16
 
12
17
  class CommentSection extends Component {
13
18
  constructor(props) {
@@ -96,9 +101,13 @@ class CommentSection extends Component {
96
101
  }
97
102
 
98
103
  onGoToAdd() {
99
- this.props.commentReply.getWrappedInstance().focusInput();
104
+ this.props.commentReply.focusInput();
100
105
  }
101
106
 
107
+ onOpenThread = comment => {
108
+ Services.navigation.navigate('thread', { ...this.props, threadId: comment.Id });
109
+ };
110
+
102
111
  onMute = () => {
103
112
  const { entityType, entityId } = this.props;
104
113
  this.setState({ processing: true }, async () => {
@@ -170,9 +179,8 @@ class CommentSection extends Component {
170
179
  commentsLoadStarted: true,
171
180
  commentsLoading: true,
172
181
  });
173
- if (this.props.commentReply) {
174
- this.props.commentReply.getWrappedInstance().loadingStarted();
175
- }
182
+ this.props.commentReply?.current?.getWrappedInstance().loadingStarted();
183
+
176
184
  this.loadComments();
177
185
  }
178
186
  }
@@ -191,9 +199,17 @@ class CommentSection extends Component {
191
199
  this.setState(
192
200
  {
193
201
  comments: _.sortBy(
194
- _.uniqBy(_.concat(this.state.comments, res.data), c => {
195
- return c.Id;
196
- }),
202
+ _.uniqBy(
203
+ _.filter(_.concat(this.state.comments, res.data), c => {
204
+ if (!this.props.threadId) {
205
+ return true;
206
+ }
207
+ return c.Id === this.props.threadId || c.ParentId === this.props.threadId;
208
+ }),
209
+ c => {
210
+ return c.Id;
211
+ },
212
+ ),
197
213
  'Timestamp',
198
214
  ),
199
215
  commentsLoading: false,
@@ -204,9 +220,9 @@ class CommentSection extends Component {
204
220
  }
205
221
  },
206
222
  );
207
- if (this.props.commentReply) {
208
- this.props.commentReply.getWrappedInstance().loadingCompleted();
209
- }
223
+ //if (this.props.commentReply) {
224
+ this.props.commentReply?.current?.getWrappedInstance().loadingCompleted();
225
+ //}
210
226
  if (this.props.live) {
211
227
  this.loadTimer = setTimeout(() => {
212
228
  this.loadComments();
@@ -310,6 +326,55 @@ class CommentSection extends Component {
310
326
  );
311
327
  }
312
328
 
329
+ renderReplyText = c => {
330
+ if (this.props.threadId || this.props.hideReplyButton) {
331
+ return null;
332
+ }
333
+
334
+ const threadComments = _.filter(this.state.comments, innerC => {
335
+ return innerC.ParentId === c.Id;
336
+ });
337
+
338
+ let content = null;
339
+
340
+ if (_.isEmpty(threadComments)) {
341
+ // no replies
342
+ content = (
343
+ <Text style={[styles.commentRepliesText, { color: this.props.colourBrandingMain }]}>{`Reply to ${getFirstName(
344
+ c.User ? c.User.displayName : 'comment',
345
+ )}`}</Text>
346
+ );
347
+ } else {
348
+ // existing replies
349
+ const profilePics = _.take(
350
+ _.uniqBy(threadComments, c => c.UserId),
351
+ 3,
352
+ );
353
+ content = (
354
+ <View style={styles.multiReplyContainer}>
355
+ {profilePics.map((c, i) => {
356
+ return <ProfilePic style={{ marginRight: -10 }} Diameter={20} ProfilePic={c.User.profilePic} />;
357
+ })}
358
+ <Text style={[styles.commentRepliesText, { marginLeft: 20, color: this.props.colourBrandingMain }]}>{`${
359
+ threadComments.length
360
+ } repl${getPluralOptions(threadComments.length, 'y', 'ies')}`}</Text>
361
+ </View>
362
+ );
363
+ }
364
+
365
+ return (
366
+ <View style={styles.commentReplies}>
367
+ <TouchableOpacity
368
+ onPress={() => {
369
+ this.onOpenThread(c);
370
+ }}
371
+ >
372
+ {content}
373
+ </TouchableOpacity>
374
+ </View>
375
+ );
376
+ };
377
+
313
378
  renderComment(c) {
314
379
  return (
315
380
  <View style={styles.comment} key={c.Id}>
@@ -323,27 +388,32 @@ class CommentSection extends Component {
323
388
  <Icon name="trash" type="font-awesome" iconStyle={styles.commentButtonIcon} />
324
389
  </View>
325
390
  </TouchableOpacity>
326
- ) : (
327
- !this.props.disableFlag && (
328
- <TouchableOpacity onPress={this.onPressReportComment.bind(this, c)}>
329
- <View style={[styles.commentButtonContainer, { backgroundColor: this.props.colourBrandingMain }]}>
330
- <Icon name="flag" type="font-awesome" iconStyle={styles.commentButtonIcon} />
331
- </View>
332
- </TouchableOpacity>
333
- )
334
- )}
391
+ ) : null
392
+ // (
393
+ // !this.props.disableFlag && (
394
+ // <TouchableOpacity onPress={this.onPressReportComment.bind(this, c)}>
395
+ // <View style={[styles.commentButtonContainer, { backgroundColor: this.props.colourBrandingMain }]}>
396
+ // <Icon name="flag" type="font-awesome" iconStyle={styles.commentButtonIcon} />
397
+ // </View>
398
+ // </TouchableOpacity>
399
+ // )
400
+ // )
401
+ }
335
402
  <Text style={[styles.commentName, { fontSize: this.getAdjustedSize(13) }]}>{c.User.displayName}</Text>
336
403
  </View>
337
404
  {!_.isEmpty(c.Comment) && <Text style={[styles.commentText, { fontSize: this.getAdjustedSize(13) }]}>{c.Comment}</Text>}
338
405
  {this.renderCommentImage(c)}
339
406
  </View>
340
407
  </View>
341
- <Text style={[styles.commentTime, { fontSize: this.getAdjustedSize(13) }]}>
342
- {moment
343
- .utc(c.Timestamp)
344
- .local()
345
- .format('D MMM • h:mma')}
346
- </Text>
408
+ <View style={styles.commentBottom}>
409
+ <Text style={[styles.commentTime, { fontSize: this.getAdjustedSize(13) }]}>
410
+ {moment
411
+ .utc(c.Timestamp)
412
+ .local()
413
+ .format('D MMM • h:mma')}
414
+ </Text>
415
+ {this.renderReplyText(c)}
416
+ </View>
347
417
  </View>
348
418
  );
349
419
  }
@@ -414,6 +484,11 @@ class CommentSection extends Component {
414
484
  if (this.props.reverseOrder) {
415
485
  source = source.reverse();
416
486
  }
487
+ if (!this.props.showReplies && !this.props.threadId) {
488
+ source = _.filter(source, c => {
489
+ return !c.ParentId;
490
+ });
491
+ }
417
492
  return (
418
493
  <View style={styles.commentSection}>
419
494
  {!this.isEmpty() && (
@@ -535,12 +610,30 @@ const styles = StyleSheet.create({
535
610
  fontFamily: 'sf-regular',
536
611
  color: TEXT_DARKEST,
537
612
  },
613
+ commentBottom: {
614
+ flexDirection: 'row-reverse',
615
+ justifyContent: 'space-between',
616
+ alignItems: 'center',
617
+ },
538
618
  commentTime: {
539
619
  fontFamily: 'sf-regular',
540
620
  marginTop: 4,
541
621
  color: TEXT_LIGHT,
542
622
  textAlign: 'right',
543
623
  },
624
+ commentReplies: {
625
+ flex: 1,
626
+ paddingLeft: 48,
627
+ },
628
+ commentRepliesText: {
629
+ fontFamily: 'sf-semibold',
630
+ fontSize: 15,
631
+ },
632
+ multiReplyContainer: {
633
+ flexDirection: 'row',
634
+ alignItems: 'center',
635
+ paddingVertical: 4,
636
+ },
544
637
  commentImageContainer: {
545
638
  marginTop: 8,
546
639
  width: 60,
@@ -611,5 +704,5 @@ const mapStateToProps = state => {
611
704
  };
612
705
  };
613
706
 
614
- const commentSection = connect(mapStateToProps, {}, null, { withRef: true })(CommentSection);
707
+ const commentSection = connect(mapStateToProps, {}, null, { forwardRef: true })(CommentSection);
615
708
  export { commentSection as CommentSection };
@@ -2,23 +2,26 @@ import React, { PureComponent } from 'react';
2
2
  import { View, Text, TouchableOpacity } from 'react-native';
3
3
  import { Icon } from 'react-native-elements';
4
4
  import { connect } from 'react-redux';
5
- import { TEXT_DARK, getMainBrandingColourFromState } from '../colours';
5
+ import { TEXT_DARK, TEXT_LIGHT, getMainBrandingColourFromState } from '../colours';
6
6
  import { FormCardSection } from './FormCardSection';
7
7
 
8
8
  class FormCardSectionOptionLauncher extends PureComponent {
9
9
  render() {
10
+ const { onPress, sectionStyle, textStyle, title, description, value, icon, colourBrandingMain } = this.props;
11
+
10
12
  return (
11
- <TouchableOpacity onPress={this.props.onPress}>
12
- <FormCardSection hasContent sectionStyle={this.props.sectionStyle}>
13
+ <TouchableOpacity onPress={onPress}>
14
+ <FormCardSection hasContent sectionStyle={sectionStyle}>
13
15
  <View style={styles.container}>
14
- <Text style={[styles.text, { marginRight: 16, flex: 1 }, this.props.textStyle]}>{this.props.title}</Text>
15
- <Text style={[styles.text, { marginRight: 16 }]}>{this.props.value}</Text>
16
+ <Text style={[styles.text, { marginRight: 16, flex: 1 }, textStyle]}>{title}</Text>
17
+ <Text style={[styles.text, { marginRight: 16 }]}>{value}</Text>
16
18
  <Icon
17
- name={this.props.icon ? this.props.icon : 'angle-right'}
19
+ name={icon ? icon : 'angle-right'}
18
20
  type="font-awesome"
19
- iconStyle={[styles.text, { fontSize: 20, color: this.props.colourBrandingMain }]}
21
+ iconStyle={[styles.text, { fontSize: 20, color: colourBrandingMain }]}
20
22
  />
21
23
  </View>
24
+ {description ? <Text style={styles.description}>{description}</Text> : null}
22
25
  </FormCardSection>
23
26
  </TouchableOpacity>
24
27
  );
@@ -36,6 +39,12 @@ const styles = {
36
39
  fontSize: 17,
37
40
  color: TEXT_DARK,
38
41
  },
42
+ description: {
43
+ fontFamily: 'sf-regular',
44
+ fontSize: 15,
45
+ color: TEXT_LIGHT,
46
+ marginTop: 8,
47
+ },
39
48
  };
40
49
 
41
50
  const mapStateToProps = state => {
@@ -5,7 +5,7 @@ import { Spinner } from './Spinner';
5
5
 
6
6
  class ImageUploadProgress extends Component {
7
7
  onRetryUpload = (imageUri, uploadUri) => {
8
- this.props.uploader.getWrappedInstance().retryUpload(imageUri, uploadUri);
8
+ this.props.uploader.retryUpload(imageUri, uploadUri);
9
9
  };
10
10
 
11
11
  render() {
@@ -786,4 +786,4 @@ const mapStateToProps = state => {
786
786
  };
787
787
  };
788
788
 
789
- export default connect(mapStateToProps, { stockImagesLoaded, imageLibraryLoaded }, null, { withRef: true })(ImageUploader);
789
+ export default connect(mapStateToProps, { stockImagesLoaded, imageLibraryLoaded }, null, { forwardRef: true })(ImageUploader);
@@ -158,7 +158,7 @@ class PlussChat extends Component {
158
158
  };
159
159
 
160
160
  showUploadMenu() {
161
- this.imageUploader.getWrappedInstance().showUploadMenu();
161
+ this.imageUploader.showUploadMenu();
162
162
  }
163
163
 
164
164
  onFocusInput = () => {
@@ -3,7 +3,8 @@ import { Platform, View, Modal, TouchableOpacity, StyleSheet } from 'react-nativ
3
3
  import * as ScreenOrientation from 'expo-screen-orientation';
4
4
  import { StatusBarHeight, getCompressed, imageExists } from '../helper';
5
5
  import { Pl60Icon } from '../fonts';
6
- import { SharingTools, MediaPlayer } from './';
6
+ import { SharingTools } from './SharingTools';
7
+ import { MediaPlayer } from './MediaPlayer';
7
8
 
8
9
  class VideoPopup extends Component {
9
10
  constructor(props) {
@@ -36,6 +36,7 @@ export * from './FontScalePopup';
36
36
  export * from './FontScaleButton';
37
37
  export * from './UserListPopup';
38
38
  export * from './Reactions';
39
+ export * from './AudienceSelectorLauncher';
39
40
  export { default as EmptyStateWidget } from './EmptyStateWidget';
40
41
  export { default as EmptyStateMain } from './EmptyStateMain';
41
42
  export { default as LoadingStateWidget } from './LoadingStateWidget';
@@ -48,3 +49,4 @@ export { default as PlussChat } from './PlussChat';
48
49
  export { default as PositionedImage } from './PositionedImage';
49
50
  export { default as FormattedText } from './FormattedText';
50
51
  export { default as MediaPlayer } from './MediaPlayer';
52
+ export { default as AudienceSelectorPage } from './AudienceSelectorPage';