@plusscommunities/pluss-circles-web-groups 1.0.11-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.
@@ -0,0 +1,591 @@
1
+ import React, { Component } from 'react';
2
+ import { connect } from 'react-redux';
3
+ import _ from 'lodash';
4
+ import { Link } from 'react-router-dom';
5
+ import { PlussCore } from '../feature.config';
6
+ import { circlesLoaded } from '../actions';
7
+ import { circleActions } from '../apis';
8
+ import FontAwesome from 'react-fontawesome';
9
+ import moment from 'moment';
10
+ import { values } from '../values.config';
11
+
12
+ const { Components, Helper, Actions, Session, Colours } = PlussCore;
13
+
14
+ class Circle extends Component {
15
+ constructor(props) {
16
+ super(props);
17
+ const circleId = Helper.safeReadParams(props, 'circleId');
18
+ this.state = {
19
+ circleId,
20
+ circle: _.find(props.circles, (c) => {
21
+ return c.Id === circleId;
22
+ }),
23
+ messageInput: '',
24
+ messages: [],
25
+ images: [],
26
+ files: [],
27
+ membersExpanded: true,
28
+ };
29
+ }
30
+
31
+ componentDidMount() {
32
+ this.checkGetData();
33
+ this.connect();
34
+ this.props.setNavData({ hideSideMenu: true });
35
+ this.getFiles();
36
+ }
37
+
38
+ componentWillUnmount() {
39
+ this.props.setNavData({ hideSideMenu: false });
40
+ this.disconnect();
41
+ }
42
+
43
+ scrollToBottom() {
44
+ if (!this.chat) return;
45
+ const scrollHeight = this.chat.scrollHeight;
46
+ const height = this.chat.clientHeight;
47
+ const maxScrollTop = scrollHeight - height;
48
+ this.chat.scrollTop = maxScrollTop > 0 ? maxScrollTop : 0;
49
+ }
50
+
51
+ getFiles = () => {
52
+ circleActions.getFiles(this.state.circleId).then((res) => {
53
+ this.setState({
54
+ files: res.data,
55
+ });
56
+ });
57
+ circleActions.getImages(this.state.circleId).then((res) => {
58
+ this.setState({
59
+ images: res.data,
60
+ });
61
+ });
62
+ };
63
+
64
+ checkGetData = () => {
65
+ if (this.state.circle) {
66
+ return;
67
+ }
68
+ const { auth } = this.props;
69
+ this.setState({ loadingAll: true }, async () => {
70
+ try {
71
+ const res = await circleActions.getAll(auth.site);
72
+ console.log('getData', res.data);
73
+ const circle = _.find(res.data, (c) => {
74
+ return c.Id === this.state.circleId;
75
+ });
76
+
77
+ this.props.circlesLoaded(res.data);
78
+ this.setState({ loadingAll: false, circle });
79
+ } catch (error) {
80
+ console.error('getData', error);
81
+ this.setState({ loadingAll: false });
82
+ }
83
+ });
84
+ };
85
+
86
+ mergeMessages(receivedMessages, excludePending) {
87
+ const newMessages = _.sortBy(_.concat(this.state.messages, receivedMessages), 'createdAt');
88
+
89
+ this.setState({
90
+ messages: _.filter(
91
+ _.uniqBy(newMessages, (m) => {
92
+ return m._id;
93
+ }),
94
+ (m) => {
95
+ return !excludePending || !m.uploading;
96
+ },
97
+ ),
98
+ });
99
+ if (!_.isEmpty(receivedMessages)) {
100
+ setTimeout(() => {
101
+ this.scrollToBottom();
102
+ }, 100);
103
+ }
104
+ }
105
+
106
+ connect = () => {
107
+ this.getMessages();
108
+
109
+ this.interval = setInterval(this.getMessages, 2000);
110
+ };
111
+
112
+ disconnect = () => {
113
+ clearInterval(this.interval);
114
+ };
115
+
116
+ getMaxTime() {
117
+ const maxMessage = _.maxBy(this.state.messages, (m) => {
118
+ return m.createdAt;
119
+ });
120
+ if (maxMessage) {
121
+ return maxMessage.createdAt;
122
+ }
123
+ return 0;
124
+ }
125
+
126
+ getDateMessages = async (date) => {
127
+ const startOf = moment(date, 'YYYY-MM-DD').startOf('d');
128
+ const endOf = moment(date, 'YYYY-MM-DD').endOf('d');
129
+ const res = await circleActions.getMessages(this.state.circleId, 10000, startOf.valueOf(), endOf.valueOf());
130
+ this.mergeMessages(res.data);
131
+ };
132
+
133
+ getMessages = async (excludePending) => {
134
+ const res = await circleActions.getMessages(this.state.circleId, 50, this.getMaxTime() + 1);
135
+
136
+ this.mergeMessages(res.data, excludePending);
137
+ };
138
+
139
+ getCircle() {
140
+ return this.state.circle || { IsPrivate: true };
141
+ }
142
+
143
+ getTitle = () => {
144
+ const { circle } = this.state;
145
+ if (!circle) {
146
+ return '';
147
+ }
148
+ if (circle.IsPrivate) {
149
+ return `PM: ${circle.Audience.map((user) => {
150
+ return user.displayName;
151
+ }).join(', ')}`;
152
+ }
153
+ return circle.Title;
154
+ };
155
+
156
+ onHandleChange = (event) => {
157
+ var stateChange = {};
158
+ stateChange[event.target.getAttribute('id')] = event.target.value;
159
+ this.setState(stateChange);
160
+ };
161
+
162
+ onImageUpdated = (image) => {
163
+ console.log('updated image');
164
+ console.log(image);
165
+ this.setState({
166
+ imageInput: image,
167
+ });
168
+ };
169
+
170
+ showImageInput = () => {
171
+ this.setState({
172
+ imageInputShowing: true,
173
+ fileInputShowing: false,
174
+ });
175
+ };
176
+
177
+ onFileUpdated = (url) => {
178
+ console.log('updated url');
179
+ console.log(url);
180
+ this.setState({
181
+ fileInput: url,
182
+ });
183
+ };
184
+
185
+ showFileInput = () => {
186
+ this.setState({
187
+ imageInputShowing: false,
188
+ fileInputShowing: true,
189
+ });
190
+ };
191
+
192
+ isMember() {
193
+ const audience = this.getCircle().Audience || [];
194
+ return _.some(audience, (u) => {
195
+ return u.userId === this.props.user.Id;
196
+ });
197
+ }
198
+
199
+ handleMessageDateChange = (date) => {
200
+ this.setState({
201
+ messageDate: date,
202
+ messageDateText: moment(date, 'YYYY-MM-DD').format('DD/MM/YYYY'),
203
+ showMessageDate: false,
204
+ messages: [],
205
+ });
206
+
207
+ this.disconnect();
208
+ this.getDateMessages(date);
209
+ };
210
+
211
+ onClearDate = () => {
212
+ this.setState(
213
+ {
214
+ messageDate: undefined,
215
+ messageDateText: undefined,
216
+ showMessageDate: false,
217
+ messages: [],
218
+ },
219
+ this.connect,
220
+ );
221
+ };
222
+
223
+ toggleMembers = () => {
224
+ this.setState({
225
+ membersExpanded: !this.state.membersExpanded,
226
+ });
227
+ };
228
+
229
+ toggleImages = () => {
230
+ this.setState({
231
+ imagesExpanded: !this.state.imagesExpanded,
232
+ });
233
+ };
234
+
235
+ toggleFiles = () => {
236
+ this.setState({
237
+ filesExpanded: !this.state.filesExpanded,
238
+ });
239
+ };
240
+
241
+ onKeyDown = (event) => {
242
+ if (event.key === 'Enter' && !event.shiftKey) {
243
+ event.preventDefault();
244
+ this.sendMessage();
245
+ }
246
+ };
247
+
248
+ sendMessage = () => {
249
+ const message = {
250
+ _id: Helper.randomString(),
251
+ text: this.state.messageInput,
252
+ user: {
253
+ _id: this.props.user.Id,
254
+ name: this.props.user.displayName,
255
+ avatar: this.props.user.profilePic,
256
+ },
257
+ };
258
+
259
+ if (!_.isEmpty(this.state.imageInput)) {
260
+ message.image = this.state.imageInput;
261
+ }
262
+
263
+ if (!_.isEmpty(this.state.fileInput)) {
264
+ message.attachments = this.state.fileInput;
265
+ }
266
+
267
+ if (_.isEmpty(message.text) && _.isEmpty(message.image) && _.isEmpty(message.attachments)) {
268
+ return;
269
+ }
270
+
271
+ const clonedMessage = _.cloneDeep(message);
272
+ clonedMessage.uploading = true;
273
+
274
+ circleActions.sendMessage(this.state.circleId, message).then((res) => {
275
+ Object.keys(res.data).forEach((key) => {
276
+ clonedMessage[key] = res.data[key];
277
+ });
278
+ clonedMessage.uploading = false;
279
+ this.getMessages(true);
280
+ });
281
+ this.setState({
282
+ messages: [...this.state.messages, clonedMessage],
283
+ messageInput: '',
284
+ imageInput: null,
285
+ imageInputShowing: false,
286
+ fileInput: null,
287
+ fileInputShowing: false,
288
+ });
289
+ setTimeout(() => {
290
+ this.scrollToBottom();
291
+ this.imageInput && this.imageInput.getWrappedInstance().setValue(null);
292
+ this.fileInput && this.fileInput.getWrappedInstance().setValue(null);
293
+ }, 100);
294
+ };
295
+
296
+ validateCircleAdmin() {
297
+ if (Session.validateAccess(this.props.auth.site, values.permission, this.props.auth)) {
298
+ return true;
299
+ }
300
+ return _.some(this.getCircle().Audience, (user) => {
301
+ return user.userId === this.props.user.Id && user.isAdmin;
302
+ });
303
+ }
304
+
305
+ renderChatInput() {
306
+ if (!this.isMember()) {
307
+ return (
308
+ <Components.Text type="highlightedHelp" className="chat_noMessage">
309
+ You can't send a message to this {_.capitalize(values.entityName)} as you are not a member.
310
+ </Components.Text>
311
+ );
312
+ }
313
+ return (
314
+ <div className="chat_inputFlex">
315
+ <FontAwesome className="chat_send" name="paper-plane" onClick={this.sendMessage} />
316
+ <div className="chat_inputContainer">
317
+ <Components.GenericInput
318
+ id="messageInput"
319
+ type="textarea"
320
+ className="chat_input"
321
+ componentClass="textarea"
322
+ placeholder="Enter message..."
323
+ value={this.state.messageInput}
324
+ onChange={(e) => this.onHandleChange(e)}
325
+ inputStyle={{
326
+ minHeight: 50,
327
+ }}
328
+ onKeyDown={this.onKeyDown}
329
+ />
330
+ <div>
331
+ <FontAwesome
332
+ className={`chat_imageIcon${this.state.imageInputShowing ? ' chat_imageIcon-selected' : ''}`}
333
+ name="camera"
334
+ onClick={this.showImageInput}
335
+ />
336
+ <FontAwesome
337
+ className={`chat_imageIcon${this.state.fileInputShowing ? ' chat_imageIcon-selected' : ''}`}
338
+ name="paperclip"
339
+ onClick={this.showFileInput}
340
+ />
341
+ </div>
342
+ <div className="overflow-x" style={{ display: this.state.imageInputShowing ? 'block' : 'none' }}>
343
+ <Components.ImageInput
344
+ ref={(ref) => {
345
+ this.imageInput = ref;
346
+ }}
347
+ multiple
348
+ limit={10}
349
+ refreshCallback={this.onImageUpdated}
350
+ noMenu
351
+ noCompress
352
+ noDownload
353
+ />
354
+ </div>
355
+ <div className="overflow-x" style={{ display: this.state.fileInputShowing ? 'block' : 'none' }}>
356
+ <Components.FileInput
357
+ ref={(ref) => {
358
+ this.fileInput = ref;
359
+ }}
360
+ multiple
361
+ limit={10}
362
+ refreshCallback={this.onFileUpdated}
363
+ noDownload
364
+ accept="application/pdf"
365
+ simpleStyle
366
+ />
367
+ </div>
368
+ </div>
369
+ </div>
370
+ );
371
+ }
372
+
373
+ renderMessage(m) {
374
+ if (m.system) {
375
+ return (
376
+ <div key={m._id} className="message">
377
+ <Components.Text type="h5" className="message_system">
378
+ {m.text}
379
+ </Components.Text>
380
+ </div>
381
+ );
382
+ }
383
+ const isSelf = m.user._id === this.props.user.Id;
384
+ return (
385
+ <div key={m._id} className={`message${isSelf ? ' message-self' : ''}${m.uploading ? ' message-uploading' : ''}`}>
386
+ <Components.Text type="h5-noUpper" className="message_time">
387
+ {moment.utc(m.createdAt).local().format('D MMM YYYY • h:mma')}
388
+ </Components.Text>
389
+ <div className="message_inner">
390
+ <Components.ProfilePic size={40} image={m.user.avatar} className="message_profilePic" />
391
+ <div className="message_bubbleContainer">
392
+ <Components.Text type="body" className="message_name">
393
+ {m.user.name}
394
+ </Components.Text>
395
+ <div className="message_bubble">
396
+ <Components.Text type="body" className="message_text">
397
+ {Helper.toParagraphed(m.text)}
398
+ </Components.Text>
399
+ <div>
400
+ {(m.image || []).map((url, i) => {
401
+ return (
402
+ <a href={url} target="_blank" key={i}>
403
+ <img className="message_image" src={url} alt={Helper.getFileName(url)} />
404
+ </a>
405
+ );
406
+ })}
407
+ </div>
408
+ <div>
409
+ {(m.attachments || []).map((url, i) => {
410
+ return <Components.Attachment source={url} key={i} white={isSelf} />;
411
+ })}
412
+ </div>
413
+ </div>
414
+ </div>
415
+ </div>
416
+ </div>
417
+ );
418
+ }
419
+
420
+ renderHeaderRight() {
421
+ if (!this.validateCircleAdmin() || this.getCircle().IsPrivate) {
422
+ return <div className="flex flex-center"></div>;
423
+ }
424
+ return (
425
+ <div className="flex flex-center">
426
+ <Link to={`/${values.featureKey}/edit/${this.state.circleId}`}>
427
+ <FontAwesome className="header_back" name="cog" />
428
+ </Link>
429
+ </div>
430
+ );
431
+ }
432
+
433
+ renderEmptyDate() {
434
+ return this.renderMessage({
435
+ system: true,
436
+ text: 'No messages on this date',
437
+ });
438
+ }
439
+
440
+ renderSideBar() {
441
+ const members = this.getCircle().Audience || [];
442
+ return (
443
+ <div className="chat_sideBar">
444
+ <div className="chat_section">
445
+ <div className="chat_section_titleSection">
446
+ <FontAwesome
447
+ className="chat_section_titleSection_caret"
448
+ name={`chevron-${this.state.membersExpanded ? 'up' : 'down'}`}
449
+ onClick={this.toggleMembers}
450
+ />
451
+ <div className="flex-1">
452
+ <Components.Text type="formTitleMedium">
453
+ Member{Helper.getPluralS(members.length)} ({members.length})
454
+ </Components.Text>
455
+ </div>
456
+ </div>
457
+ {this.state.membersExpanded && (
458
+ <div className="paddingTop-8">
459
+ {members.map((user) => {
460
+ return <Components.UserListing user={user} />;
461
+ })}
462
+ </div>
463
+ )}
464
+ </div>
465
+ <div className="chat_section">
466
+ <div className="chat_section_titleSection">
467
+ <FontAwesome
468
+ className="chat_section_titleSection_caret"
469
+ name={`chevron-${this.state.imagesExpanded ? 'up' : 'down'}`}
470
+ onClick={this.toggleImages}
471
+ />
472
+ <div className="flex-1">
473
+ <Components.Text type="formTitleMedium">
474
+ Image{Helper.getPluralS(this.state.images.length)} ({this.state.images.length})
475
+ </Components.Text>
476
+ </div>
477
+ </div>
478
+ {this.state.imagesExpanded && (
479
+ <div className="paddingTop-8">
480
+ {this.state.images.map((image, i) => {
481
+ return (
482
+ <a href={image.Url} target="_blank">
483
+ <img src={image.Url} className="chat_section_image" alt={Helper.getFileName(image.Url)} />
484
+ </a>
485
+ );
486
+ })}
487
+ </div>
488
+ )}
489
+ </div>
490
+ <div className="chat_section">
491
+ <div className="chat_section_titleSection">
492
+ <FontAwesome
493
+ className="chat_section_titleSection_caret"
494
+ name={`chevron-${this.state.filesExpanded ? 'up' : 'down'}`}
495
+ onClick={this.toggleFiles}
496
+ />
497
+ <div className="flex-1">
498
+ <Components.Text type="formTitleMedium">
499
+ File{Helper.getPluralS(this.state.files.length)} ({this.state.files.length})
500
+ </Components.Text>
501
+ </div>
502
+ </div>
503
+ {this.state.filesExpanded && (
504
+ <div className="paddingTop-8">
505
+ {this.state.files.map((file, i) => {
506
+ return <Components.Attachment source={file.Url} key={i} />;
507
+ })}
508
+ </div>
509
+ )}
510
+ </div>
511
+ </div>
512
+ );
513
+ }
514
+
515
+ render() {
516
+ return (
517
+ <Components.OverlayPage fullPage fullPageStyle={{ display: 'flex', flexDirection: 'column' }}>
518
+ <Components.Header rightContent={this.renderHeaderRight()}>
519
+ <FontAwesome
520
+ className="header_back"
521
+ name="angle-left"
522
+ onClick={() => {
523
+ window.history.back();
524
+ }}
525
+ />
526
+ <Components.Text type="formTitleLarge" className="header_title">
527
+ {this.getTitle()}
528
+ </Components.Text>
529
+ </Components.Header>
530
+ <div className="flex flex-1 flex-reverse overflow-hidden">
531
+ {this.renderSideBar()}
532
+ <div className="flex-1 flex flex-column overflow-hidden">
533
+ <Components.Header onlyContainer>
534
+ <div className="flex flex-center flex-1 paddingHorizontal-16">
535
+ <Components.Text type="h5" className="marginRight-20">
536
+ Filter by
537
+ </Components.Text>
538
+ <div>
539
+ <Components.GenericInput
540
+ id="messageDate"
541
+ label="Date"
542
+ alwaysShowLabel
543
+ value={this.state.messageDateText}
544
+ readOnly
545
+ onClick={() => this.setState({ showMessageDate: !this.state.showMessageDate })}
546
+ rightContent={
547
+ !_.isEmpty(this.state.messageDate) && (
548
+ <Components.SVGIcon
549
+ colour={Colours.COLOUR_DUSK_LIGHT}
550
+ icon="close"
551
+ className="timepicker_clear"
552
+ onClick={this.onClearDate}
553
+ />
554
+ )
555
+ }
556
+ />
557
+ {this.state.showMessageDate && (
558
+ <Components.DatePicker hideTop selectedDate={this.state.messageDate} selectDate={this.handleMessageDateChange} />
559
+ )}
560
+ </div>
561
+ </div>
562
+ </Components.Header>
563
+ <div className="chat">
564
+ <div className="chat_newMessage">{this.renderChatInput()}</div>
565
+ <div ref={(ref) => (this.chat = ref)} className="chat_messages">
566
+ {_.isEmpty(this.state.messages) && !_.isEmpty(this.state.messageDate) && this.renderEmptyDate()}
567
+ {this.state.messages.map((m) => {
568
+ return this.renderMessage(m);
569
+ })}
570
+ </div>
571
+ </div>
572
+ </div>
573
+ </div>
574
+ </Components.OverlayPage>
575
+ );
576
+ }
577
+ }
578
+
579
+ const styles = {};
580
+
581
+ const mapStateToProps = (state) => {
582
+ const { circles } = state[values.reducerKey];
583
+ const { auth } = state;
584
+ return {
585
+ circles,
586
+ auth,
587
+ user: Helper.getUserFromState(state),
588
+ };
589
+ };
590
+
591
+ export default connect(mapStateToProps, { circlesLoaded, setNavData: Actions.setNavData })(Circle);