@plusscommunities/pluss-circles-web 1.5.5 → 1.5.6

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.
@@ -1,760 +1,852 @@
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';
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
11
 
12
12
  const { Components, Helper, Actions, Session, Colours } = PlussCore;
13
13
 
14
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
- replyingTo: null,
29
- showDeleteConfirm: false,
30
- messageToDelete: null,
31
- deletingMessageId: null,
32
- imageInput: null,
33
- fileInput: null,
34
- imageInputShowing: false,
35
- fileInputShowing: false,
36
- };
37
- }
38
-
39
- componentDidMount() {
40
- this.checkGetData();
41
- this.connect();
42
- this.props.setNavData({ hideSideMenu: true });
43
- this.getFiles();
44
- }
45
-
46
- componentWillUnmount() {
47
- this.props.setNavData({ hideSideMenu: false });
48
- this.disconnect();
49
- }
50
-
51
- scrollToBottom() {
52
- if (!this.chat) return;
53
- const scrollHeight = this.chat.scrollHeight;
54
- const height = this.chat.clientHeight;
55
- const maxScrollTop = scrollHeight - height;
56
- this.chat.scrollTop = maxScrollTop > 0 ? maxScrollTop : 0;
57
- }
58
-
59
- getFiles = () => {
60
- circleActions.getFiles(this.state.circleId).then((res) => {
61
- this.setState({
62
- files: res.data,
63
- });
64
- });
65
- circleActions.getImages(this.state.circleId).then((res) => {
66
- this.setState({
67
- images: res.data,
68
- });
69
- });
70
- };
71
-
72
- checkGetData = () => {
73
- if (this.state.circle) {
74
- return;
75
- }
76
- const { auth } = this.props;
77
- this.setState({ loadingAll: true }, async () => {
78
- try {
79
- const res = await circleActions.getAll(auth.site);
80
- console.log('getData', res.data);
81
- const circle = _.find(res.data, (c) => {
82
- return c.Id === this.state.circleId;
83
- });
84
-
85
- this.props.circlesLoaded(res.data);
86
- this.setState({ loadingAll: false, circle });
87
- } catch (error) {
88
- console.error('getData', error);
89
- this.setState({ loadingAll: false });
90
- }
91
- });
92
- };
93
-
94
- mergeMessages(receivedMessages, excludePending) {
95
- const newMessages = _.sortBy(_.concat(this.state.messages, receivedMessages), 'createdAt');
96
-
97
- this.setState({
98
- messages: _.filter(
99
- _.uniqBy(newMessages, (m) => {
100
- return m._id;
101
- }),
102
- (m) => {
103
- return !excludePending || !m.uploading;
104
- },
105
- ),
106
- });
107
- if (!_.isEmpty(receivedMessages)) {
108
- setTimeout(() => {
109
- this.scrollToBottom();
110
- }, 100);
111
- }
112
- }
113
-
114
- connect = () => {
115
- this.getMessages();
116
-
117
- this.interval = setInterval(this.getMessages, 2000);
118
- };
119
-
120
- disconnect = () => {
121
- clearInterval(this.interval);
122
- };
123
-
124
- getMaxTime() {
125
- const maxMessage = _.maxBy(this.state.messages, (m) => {
126
- return m.createdAt;
127
- });
128
- if (maxMessage) {
129
- return maxMessage.createdAt;
130
- }
131
- return 0;
132
- }
133
-
134
- getDateMessages = async (date) => {
135
- const startOf = moment(date, 'YYYY-MM-DD').startOf('d');
136
- const endOf = moment(date, 'YYYY-MM-DD').endOf('d');
137
- const res = await circleActions.getMessages(this.state.circleId, 10000, startOf.valueOf(), endOf.valueOf());
138
- this.mergeMessages(res.data);
139
- };
140
-
141
- getMessages = async (excludePending) => {
142
- const res = await circleActions.getMessages(this.state.circleId, 50, this.getMaxTime() + 1);
143
-
144
- this.mergeMessages(res.data, excludePending);
145
- };
146
-
147
- getCircle() {
148
- return this.state.circle || { IsPrivate: true };
149
- }
150
-
151
- getTitle = () => {
152
- const { circle } = this.state;
153
- if (!circle) {
154
- return '';
155
- }
156
- if (circle.IsPrivate) {
157
- return `PM: ${circle.Audience.map((user) => {
158
- return user.displayName;
159
- }).join(', ')}`;
160
- }
161
- return circle.Title;
162
- };
163
-
164
- onHandleChange = (event) => {
165
- var stateChange = {};
166
- stateChange[event.target.getAttribute('id')] = event.target.value;
167
- this.setState(stateChange);
168
- };
169
-
170
- onImageUpdated = (image) => {
171
- console.log('updated image');
172
- console.log(image);
173
- this.setState({
174
- imageInput: image,
175
- });
176
- };
177
-
178
- showImageInput = () => {
179
- this.setState({
180
- imageInputShowing: true,
181
- fileInputShowing: false,
182
- });
183
- };
184
-
185
- onFileUpdated = (url) => {
186
- console.log('updated url');
187
- console.log(url);
188
- this.setState({
189
- fileInput: url,
190
- });
191
- };
192
-
193
- showFileInput = () => {
194
- this.setState({
195
- imageInputShowing: false,
196
- fileInputShowing: true,
197
- });
198
- };
199
-
200
- onReply = (m) => {
201
- this.setState({
202
- replyingTo: m,
203
- });
204
- if (m) {
205
- const input = document.getElementById('messageInput');
206
- if (input) input.focus();
207
- }
208
- };
209
-
210
- isMember() {
211
- const audience = this.getCircle().Audience || [];
212
- return _.some(audience, (u) => {
213
- return u.userId === this.props.user.Id;
214
- });
215
- }
216
-
217
- handleMessageDateChange = (date) => {
218
- this.setState({
219
- messageDate: date,
220
- messageDateText: moment(date, 'YYYY-MM-DD').format('DD/MM/YYYY'),
221
- showMessageDate: false,
222
- messages: [],
223
- });
224
-
225
- this.disconnect();
226
- this.getDateMessages(date);
227
- };
228
-
229
- onClearDate = () => {
230
- this.setState(
231
- {
232
- messageDate: undefined,
233
- messageDateText: undefined,
234
- showMessageDate: false,
235
- messages: [],
236
- },
237
- this.connect,
238
- );
239
- };
240
-
241
- toggleMembers = () => {
242
- this.setState({
243
- membersExpanded: !this.state.membersExpanded,
244
- });
245
- };
246
-
247
- toggleImages = () => {
248
- this.setState({
249
- imagesExpanded: !this.state.imagesExpanded,
250
- });
251
- };
252
-
253
- toggleFiles = () => {
254
- this.setState({
255
- filesExpanded: !this.state.filesExpanded,
256
- });
257
- };
258
-
259
- onKeyDown = (event) => {
260
- if (event.key === 'Enter' && !event.shiftKey) {
261
- event.preventDefault();
262
- this.sendMessage();
263
- }
264
- };
265
-
266
- onDeleteMessage = (message) => {
267
- this.setState({
268
- showDeleteConfirm: true,
269
- messageToDelete: message,
270
- });
271
- };
272
-
273
- onConfirmDelete = async () => {
274
- const { messageToDelete } = this.state;
275
- if (!messageToDelete) return;
276
-
277
- // Optimistically update UI - mark as deleted immediately
278
- const updatedMessages = this.state.messages.map((m) => {
279
- if (m._id === messageToDelete._id) {
280
- return { ...m, deleted: true, text: ' ' };
281
- }
282
- return m;
283
- });
284
-
285
- this.setState({
286
- messages: updatedMessages,
287
- showDeleteConfirm: false,
288
- deletingMessageId: messageToDelete._id,
289
- messageToDelete: null,
290
- });
291
-
292
- try {
293
- await circleActions.deleteMessage(messageToDelete._id, this.state.circleId);
294
- // Message already marked as deleted, just clear the deleting state
295
- this.setState({ deletingMessageId: null });
296
- } catch (error) {
297
- console.error('Failed to delete message:', error);
298
- // Revert the optimistic update on failure
299
- const revertedMessages = this.state.messages.map((m) => {
300
- if (m._id === messageToDelete._id) {
301
- return messageToDelete;
302
- }
303
- return m;
304
- });
305
- this.setState({
306
- messages: revertedMessages,
307
- deletingMessageId: null,
308
- });
309
- alert('Failed to delete message. Please try again.');
310
- }
311
- };
312
-
313
- onCancelDelete = () => {
314
- this.setState({
315
- showDeleteConfirm: false,
316
- messageToDelete: null,
317
- });
318
- };
319
-
320
- sendMessage = () => {
321
- const message = {
322
- _id: Helper.randomString(),
323
- text: this.state.messageInput,
324
- user: {
325
- _id: this.props.user.Id,
326
- name: this.props.user.displayName,
327
- avatar: this.props.user.profilePic,
328
- },
329
- };
330
-
331
- if (!_.isEmpty(this.state.imageInput)) {
332
- message.image = this.state.imageInput;
333
- }
334
-
335
- if (!_.isEmpty(this.state.fileInput)) {
336
- message.attachments = this.state.fileInput;
337
- }
338
-
339
- if (_.isEmpty(message.text) && _.isEmpty(message.image) && _.isEmpty(message.attachments)) {
340
- return;
341
- }
342
-
343
- if (this.state.replyingTo) {
344
- message.replyingTo = this.state.replyingTo;
345
- }
346
-
347
- const clonedMessage = _.cloneDeep(message);
348
- clonedMessage.uploading = true;
349
-
350
- circleActions.sendMessage(this.state.circleId, message).then((res) => {
351
- Object.keys(res.data).forEach((key) => {
352
- clonedMessage[key] = res.data[key];
353
- });
354
- clonedMessage.uploading = false;
355
- this.getMessages(true);
356
- });
357
- this.setState({
358
- messages: [...this.state.messages, clonedMessage],
359
- messageInput: '',
360
- imageInput: null,
361
- imageInputShowing: false,
362
- fileInput: null,
363
- fileInputShowing: false,
364
- });
365
- setTimeout(() => {
366
- this.onReply(null);
367
- this.scrollToBottom();
368
- this.imageInput && this.imageInput.getWrappedInstance().setValue(null);
369
- this.fileInput && this.fileInput.getWrappedInstance().setValue(null);
370
- }, 100);
371
- };
372
-
373
- validateCircleAdmin() {
374
- if (Session.validateAccess(this.props.auth.site, values.permission, this.props.auth)) {
375
- return true;
376
- }
377
- return _.some(this.getCircle().Audience, (user) => {
378
- return user.userId === this.props.user.Id && user.isAdmin;
379
- });
380
- }
381
-
382
- renderChatInput() {
383
- if (!this.isMember()) {
384
- return (
385
- <Components.Text type="highlightedHelp" className="chat_noMessage">
386
- You can't send a message to this {_.capitalize(values.entityName)} as you are not a member.
387
- </Components.Text>
388
- );
389
- }
390
- return (
391
- <div className="chat_inputFlex">
392
- <FontAwesome className="chat_send" name="paper-plane" onClick={this.sendMessage} />
393
- <div className="chat_inputContainer">
394
- <Components.GenericInput
395
- id="messageInput"
396
- type="textarea"
397
- className="chat_input"
398
- componentClass="textarea"
399
- placeholder="Enter message..."
400
- value={this.state.messageInput}
401
- onChange={(e) => this.onHandleChange(e)}
402
- inputStyle={{
403
- minHeight: 50,
404
- }}
405
- onKeyDown={this.onKeyDown}
406
- />
407
- <div>
408
- <FontAwesome
409
- className={`chat_imageIcon${this.state.imageInputShowing ? ' chat_imageIcon-selected' : ''}`}
410
- name="camera"
411
- onClick={this.showImageInput}
412
- />
413
- <FontAwesome
414
- className={`chat_imageIcon${this.state.fileInputShowing ? ' chat_imageIcon-selected' : ''}`}
415
- name="paperclip"
416
- onClick={this.showFileInput}
417
- />
418
- </div>
419
- <div className="overflow-x" style={{ display: this.state.imageInputShowing ? 'block' : 'none' }}>
420
- <Components.ImageInput
421
- ref={(ref) => {
422
- this.imageInput = ref;
423
- }}
424
- multiple
425
- limit={10}
426
- refreshCallback={this.onImageUpdated}
427
- noMenu
428
- noCompress
429
- noDownload
430
- />
431
- </div>
432
- <div className="overflow-x" style={{ display: this.state.fileInputShowing ? 'block' : 'none' }}>
433
- <Components.FileInput
434
- ref={(ref) => {
435
- this.fileInput = ref;
436
- }}
437
- multiple
438
- limit={10}
439
- refreshCallback={this.onFileUpdated}
440
- noDownload
441
- accept="application/pdf"
442
- simpleStyle
443
- />
444
- </div>
445
- </div>
446
- </div>
447
- );
448
- }
449
-
450
- renderMessage(m) {
451
- if (m.system) {
452
- return (
453
- <div key={m._id} className="message">
454
- <Components.Text type="h5" className="message_system">
455
- {m.text}
456
- </Components.Text>
457
- </div>
458
- );
459
- }
460
- const isSelf = m.user._id === this.props.user.Id;
461
- const isDeleted = m.deleted === true;
462
- const isDeleting = this.state.deletingMessageId === m._id;
463
-
464
- return (
465
- <div key={m._id} className={`message${isSelf ? ' message-self' : ''}${m.uploading ? ' message-uploading' : ''}${isDeleting ? ' message-deleting' : ''}`}>
466
- <Components.Text type="h5-noUpper" className="message_time">
467
- {moment.utc(m.createdAt).local().format('D MMM YYYY h:mma')}
468
- </Components.Text>
469
- <div className="message_inner">
470
- <Components.ProfilePic size={40} image={m.user.avatar} className="message_profilePic" />
471
- <div className="message_bubbleContainer">
472
- <Components.Text type="body" className="message_name">
473
- {m.user.name}
474
- {m.replyingTo && !isDeleted ? ` replied to ${m.replyingTo.user.name}` : ''}
475
- </Components.Text>
476
- {m.replyingTo && !isDeleted && (
477
- <div className="message_replyBubble">
478
- <Components.Text type="body" className="message_text">
479
- {Helper.toParagraphed((m.replyingTo.text || '').substr(0, 100))}
480
- </Components.Text>
481
- </div>
482
- )}
483
- <div className="message_bubble">
484
- <Components.Text type="body" className="message_text">
485
- {isDeleted ? '[Message deleted]' : Helper.toParagraphed(m.text)}
486
- </Components.Text>
487
- {!isDeleted && (
488
- <>
489
- <div>
490
- {(m.image || []).map((url, i) => {
491
- return (
492
- <a href={url} target="_blank" key={i}>
493
- <img className="message_image" src={url} alt={Helper.getFileName(url)} />
494
- </a>
495
- );
496
- })}
497
- </div>
498
- <div>
499
- {(m.attachments || []).map((url, i) => {
500
- return <Components.Attachment source={url} key={i} white={isSelf} />;
501
- })}
502
- </div>
503
- </>
504
- )}
505
- </div>
506
- {!isDeleted && (
507
- <div className="message_reply" style={{ display: 'flex', gap: '16px' }}>
508
- <Components.Text
509
- type="button"
510
- onClick={() => {
511
- this.onReply(m);
512
- }}
513
- >
514
- Reply
515
- </Components.Text>
516
- {isSelf && (
517
- <Components.Text
518
- type="button"
519
- onClick={() => {
520
- this.onDeleteMessage(m);
521
- }}
522
- >
523
- Delete
524
- </Components.Text>
525
- )}
526
- </div>
527
- )}
528
- </div>
529
- </div>
530
- </div>
531
- );
532
- }
533
-
534
- renderHeaderRight() {
535
- if (!this.validateCircleAdmin() || this.getCircle().IsPrivate) {
536
- return <div className="flex flex-center"></div>;
537
- }
538
- return (
539
- <div className="flex flex-center">
540
- <Link to={`/${values.featureKey}/edit/${this.state.circleId}`}>
541
- <FontAwesome className="header_back" name="cog" />
542
- </Link>
543
- </div>
544
- );
545
- }
546
-
547
- renderEmptyDate() {
548
- return this.renderMessage({
549
- system: true,
550
- text: 'No messages on this date',
551
- });
552
- }
553
-
554
- renderSideBar() {
555
- const members = this.getCircle().Audience || [];
556
- return (
557
- <div className="chat_sideBar">
558
- <div className="chat_section">
559
- <div className="chat_section_titleSection">
560
- <FontAwesome
561
- className="chat_section_titleSection_caret"
562
- name={`chevron-${this.state.membersExpanded ? 'up' : 'down'}`}
563
- onClick={this.toggleMembers}
564
- />
565
- <div className="flex-1">
566
- <Components.Text type="formTitleMedium">
567
- Member{Helper.getPluralS(members.length)} ({members.length})
568
- </Components.Text>
569
- </div>
570
- </div>
571
- {this.state.membersExpanded && (
572
- <div className="paddingTop-8">
573
- {members.map((user) => {
574
- return <Components.UserListing user={user} />;
575
- })}
576
- </div>
577
- )}
578
- </div>
579
- <div className="chat_section">
580
- <div className="chat_section_titleSection">
581
- <FontAwesome
582
- className="chat_section_titleSection_caret"
583
- name={`chevron-${this.state.imagesExpanded ? 'up' : 'down'}`}
584
- onClick={this.toggleImages}
585
- />
586
- <div className="flex-1">
587
- <Components.Text type="formTitleMedium">
588
- Image{Helper.getPluralS(this.state.images.length)} ({this.state.images.length})
589
- </Components.Text>
590
- </div>
591
- </div>
592
- {this.state.imagesExpanded && (
593
- <div className="paddingTop-8">
594
- {this.state.images.map((image) => {
595
- return (
596
- <a href={image.Url} target="_blank">
597
- <img src={image.Url} className="chat_section_image" alt={Helper.getFileName(image.Url)} />
598
- </a>
599
- );
600
- })}
601
- </div>
602
- )}
603
- </div>
604
- <div className="chat_section">
605
- <div className="chat_section_titleSection">
606
- <FontAwesome
607
- className="chat_section_titleSection_caret"
608
- name={`chevron-${this.state.filesExpanded ? 'up' : 'down'}`}
609
- onClick={this.toggleFiles}
610
- />
611
- <div className="flex-1">
612
- <Components.Text type="formTitleMedium">
613
- File{Helper.getPluralS(this.state.files.length)} ({this.state.files.length})
614
- </Components.Text>
615
- </div>
616
- </div>
617
- {this.state.filesExpanded && (
618
- <div className="paddingTop-8">
619
- {this.state.files.map((file, i) => {
620
- return <Components.Attachment source={file.Url} key={i} />;
621
- })}
622
- </div>
623
- )}
624
- </div>
625
- </div>
626
- );
627
- }
628
-
629
- renderReplyTo() {
630
- if (!this.state.replyingTo) {
631
- return null;
632
- }
633
- const m = this.state.replyingTo;
634
- return (
635
- <div className="chat_replyTo">
636
- <div className="chat_replyTo_container">
637
- <Components.Text type="h5">Replying to {m && m.user && !_.isEmpty(m.user.displayName) ? m.user.displayName : ''}</Components.Text>
638
- {m && !_.isEmpty(m.text) && <Components.Text type="body">{m.text.substr(0, 50)}</Components.Text>}
639
- </div>
640
- <div className="chat_replyTo_remove">
641
- <Components.SVGIcon
642
- className="removeIcon"
643
- icon="close"
644
- onClick={() => {
645
- this.onReply(null);
646
- }}
647
- colour={Colours.COLOUR_DUSK}
648
- />
649
- </div>
650
- </div>
651
- );
652
- }
653
-
654
- renderDeleteConfirmPopup() {
655
- if (!this.state.showDeleteConfirm) return null;
656
-
657
- return (
658
- <Components.Popup
659
- title="Delete Message"
660
- buttons={[
661
- {
662
- text: 'Delete',
663
- onClick: this.onConfirmDelete,
664
- type: 'primary',
665
- isActive: true,
666
- },
667
- {
668
- text: 'Cancel',
669
- onClick: this.onCancelDelete,
670
- type: 'tertiary',
671
- isActive: true,
672
- },
673
- ]}
674
- onClose={this.onCancelDelete}
675
- hasPadding
676
- >
677
- <Components.Text type="body">
678
- Are you sure you want to delete this message? This action cannot be undone.
679
- </Components.Text>
680
- </Components.Popup>
681
- );
682
- }
683
-
684
- render() {
685
- return (
686
- <Components.OverlayPage fullPage fullPageStyle={{ display: 'flex', flexDirection: 'column' }}>
687
- {this.renderDeleteConfirmPopup()}
688
- <Components.Header rightContent={this.renderHeaderRight()}>
689
- <FontAwesome
690
- className="header_back"
691
- name="angle-left"
692
- onClick={() => {
693
- window.history.back();
694
- }}
695
- />
696
- <Components.Text type="formTitleLarge" className="header_title">
697
- {this.getTitle()}
698
- </Components.Text>
699
- </Components.Header>
700
- <div className="flex flex-1 flex-reverse overflow-hidden">
701
- {this.renderSideBar()}
702
- <div className="flex-1 flex flex-column overflow-hidden">
703
- <Components.Header onlyContainer>
704
- <div className="flex flex-center flex-1 paddingHorizontal-16">
705
- <Components.Text type="h5" className="marginRight-20">
706
- Filter by
707
- </Components.Text>
708
- <div>
709
- <Components.GenericInput
710
- id="messageDate"
711
- label="Date"
712
- alwaysShowLabel
713
- value={this.state.messageDateText}
714
- readOnly
715
- onClick={() => this.setState({ showMessageDate: !this.state.showMessageDate })}
716
- rightContent={
717
- !_.isEmpty(this.state.messageDate) && (
718
- <Components.SVGIcon
719
- colour={Colours.COLOUR_DUSK_LIGHT}
720
- icon="close"
721
- className="timepicker_clear"
722
- onClick={this.onClearDate}
723
- />
724
- )
725
- }
726
- />
727
- {this.state.showMessageDate && (
728
- <Components.DatePicker hideTop selectedDate={this.state.messageDate} selectDate={this.handleMessageDateChange} />
729
- )}
730
- </div>
731
- </div>
732
- </Components.Header>
733
- <div className="chat">
734
- <div className="chat_newMessage">{this.renderChatInput()}</div>
735
- {this.renderReplyTo()}
736
- <div ref={(ref) => (this.chat = ref)} className="chat_messages">
737
- {_.isEmpty(this.state.messages) && !_.isEmpty(this.state.messageDate) && this.renderEmptyDate()}
738
- {this.state.messages.map((m) => {
739
- return this.renderMessage(m);
740
- })}
741
- </div>
742
- </div>
743
- </div>
744
- </div>
745
- </Components.OverlayPage>
746
- );
747
- }
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
+ replyingTo: null,
29
+ showDeleteConfirm: false,
30
+ messageToDelete: null,
31
+ deletingMessageId: null,
32
+ imageInput: null,
33
+ fileInput: null,
34
+ imageInputShowing: false,
35
+ fileInputShowing: false,
36
+ };
37
+ }
38
+
39
+ componentDidMount() {
40
+ this.checkGetData();
41
+ this.connect();
42
+ this.props.setNavData({ hideSideMenu: true });
43
+ this.getFiles();
44
+ }
45
+
46
+ componentWillUnmount() {
47
+ this.props.setNavData({ hideSideMenu: false });
48
+ this.disconnect();
49
+ }
50
+
51
+ scrollToBottom() {
52
+ if (!this.chat) return;
53
+ const scrollHeight = this.chat.scrollHeight;
54
+ const height = this.chat.clientHeight;
55
+ const maxScrollTop = scrollHeight - height;
56
+ this.chat.scrollTop = maxScrollTop > 0 ? maxScrollTop : 0;
57
+ }
58
+
59
+ getFiles = () => {
60
+ circleActions.getFiles(this.state.circleId).then((res) => {
61
+ this.setState({
62
+ files: res.data,
63
+ });
64
+ });
65
+ circleActions.getImages(this.state.circleId).then((res) => {
66
+ this.setState({
67
+ images: res.data,
68
+ });
69
+ });
70
+ };
71
+
72
+ checkGetData = () => {
73
+ if (this.state.circle) {
74
+ return;
75
+ }
76
+ const { auth } = this.props;
77
+ this.setState({ loadingAll: true }, async () => {
78
+ try {
79
+ const res = await circleActions.getAll(auth.site);
80
+ console.log("getData", res.data);
81
+ const circle = _.find(res.data, (c) => {
82
+ return c.Id === this.state.circleId;
83
+ });
84
+
85
+ this.props.circlesLoaded(res.data);
86
+ this.setState({ loadingAll: false, circle });
87
+ } catch (error) {
88
+ console.error("getData", error);
89
+ this.setState({ loadingAll: false });
90
+ }
91
+ });
92
+ };
93
+
94
+ mergeMessages(receivedMessages, excludePending) {
95
+ const newMessages = _.sortBy(
96
+ _.concat(this.state.messages, receivedMessages),
97
+ "createdAt",
98
+ );
99
+
100
+ this.setState({
101
+ messages: _.filter(
102
+ _.uniqBy(newMessages, (m) => {
103
+ return m._id;
104
+ }),
105
+ (m) => {
106
+ return !excludePending || !m.uploading;
107
+ },
108
+ ),
109
+ });
110
+ if (!_.isEmpty(receivedMessages)) {
111
+ setTimeout(() => {
112
+ this.scrollToBottom();
113
+ }, 100);
114
+ }
115
+ }
116
+
117
+ connect = () => {
118
+ this.getMessages();
119
+
120
+ this.interval = setInterval(this.getMessages, 2000);
121
+ };
122
+
123
+ disconnect = () => {
124
+ clearInterval(this.interval);
125
+ };
126
+
127
+ getMaxTime() {
128
+ const maxMessage = _.maxBy(this.state.messages, (m) => {
129
+ return m.createdAt;
130
+ });
131
+ if (maxMessage) {
132
+ return maxMessage.createdAt;
133
+ }
134
+ return 0;
135
+ }
136
+
137
+ getDateMessages = async (date) => {
138
+ const startOf = moment(date, "YYYY-MM-DD").startOf("d");
139
+ const endOf = moment(date, "YYYY-MM-DD").endOf("d");
140
+ const res = await circleActions.getMessages(
141
+ this.state.circleId,
142
+ 10000,
143
+ startOf.valueOf(),
144
+ endOf.valueOf(),
145
+ );
146
+ this.mergeMessages(res.data);
147
+ };
148
+
149
+ getMessages = async (excludePending) => {
150
+ const res = await circleActions.getMessages(
151
+ this.state.circleId,
152
+ 50,
153
+ this.getMaxTime() + 1,
154
+ );
155
+
156
+ this.mergeMessages(res.data, excludePending);
157
+ };
158
+
159
+ getCircle() {
160
+ return this.state.circle || { IsPrivate: true };
161
+ }
162
+
163
+ getTitle = () => {
164
+ const { circle } = this.state;
165
+ if (!circle) {
166
+ return "";
167
+ }
168
+ if (circle.IsPrivate) {
169
+ return `PM: ${circle.Audience.map((user) => {
170
+ return user.displayName;
171
+ }).join(", ")}`;
172
+ }
173
+ return circle.Title;
174
+ };
175
+
176
+ onHandleChange = (event) => {
177
+ var stateChange = {};
178
+ stateChange[event.target.getAttribute("id")] = event.target.value;
179
+ this.setState(stateChange);
180
+ };
181
+
182
+ onImageUpdated = (image) => {
183
+ console.log("updated image");
184
+ console.log(image);
185
+ this.setState({
186
+ imageInput: image,
187
+ });
188
+ };
189
+
190
+ showImageInput = () => {
191
+ this.setState({
192
+ imageInputShowing: true,
193
+ fileInputShowing: false,
194
+ });
195
+ };
196
+
197
+ onFileUpdated = (url) => {
198
+ console.log("updated url");
199
+ console.log(url);
200
+ this.setState({
201
+ fileInput: url,
202
+ });
203
+ };
204
+
205
+ showFileInput = () => {
206
+ this.setState({
207
+ imageInputShowing: false,
208
+ fileInputShowing: true,
209
+ });
210
+ };
211
+
212
+ onReply = (m) => {
213
+ this.setState({
214
+ replyingTo: m,
215
+ });
216
+ if (m) {
217
+ const input = document.getElementById("messageInput");
218
+ if (input) input.focus();
219
+ }
220
+ };
221
+
222
+ isMember() {
223
+ const audience = this.getCircle().Audience || [];
224
+ return _.some(audience, (u) => {
225
+ return u.userId === this.props.user.Id;
226
+ });
227
+ }
228
+
229
+ handleMessageDateChange = (date) => {
230
+ this.setState({
231
+ messageDate: date,
232
+ messageDateText: moment(date, "YYYY-MM-DD").format("DD/MM/YYYY"),
233
+ showMessageDate: false,
234
+ messages: [],
235
+ });
236
+
237
+ this.disconnect();
238
+ this.getDateMessages(date);
239
+ };
240
+
241
+ onClearDate = () => {
242
+ this.setState(
243
+ {
244
+ messageDate: undefined,
245
+ messageDateText: undefined,
246
+ showMessageDate: false,
247
+ messages: [],
248
+ },
249
+ this.connect,
250
+ );
251
+ };
252
+
253
+ toggleMembers = () => {
254
+ this.setState({
255
+ membersExpanded: !this.state.membersExpanded,
256
+ });
257
+ };
258
+
259
+ toggleImages = () => {
260
+ this.setState({
261
+ imagesExpanded: !this.state.imagesExpanded,
262
+ });
263
+ };
264
+
265
+ toggleFiles = () => {
266
+ this.setState({
267
+ filesExpanded: !this.state.filesExpanded,
268
+ });
269
+ };
270
+
271
+ onKeyDown = (event) => {
272
+ if (event.key === "Enter" && !event.shiftKey) {
273
+ event.preventDefault();
274
+ this.sendMessage();
275
+ }
276
+ };
277
+
278
+ onDeleteMessage = (message) => {
279
+ this.setState({
280
+ showDeleteConfirm: true,
281
+ messageToDelete: message,
282
+ });
283
+ };
284
+
285
+ onConfirmDelete = async () => {
286
+ const { messageToDelete } = this.state;
287
+ if (!messageToDelete) return;
288
+
289
+ // Optimistically update UI - mark as deleted immediately
290
+ const updatedMessages = this.state.messages.map((m) => {
291
+ if (m._id === messageToDelete._id) {
292
+ return { ...m, deleted: true, text: " " };
293
+ }
294
+ return m;
295
+ });
296
+
297
+ this.setState({
298
+ messages: updatedMessages,
299
+ showDeleteConfirm: false,
300
+ deletingMessageId: messageToDelete._id,
301
+ messageToDelete: null,
302
+ });
303
+
304
+ try {
305
+ await circleActions.deleteMessage(
306
+ messageToDelete._id,
307
+ this.state.circleId,
308
+ );
309
+ // Message already marked as deleted, just clear the deleting state
310
+ this.setState({ deletingMessageId: null });
311
+ } catch (error) {
312
+ console.error("Failed to delete message:", error);
313
+ // Revert the optimistic update on failure
314
+ const revertedMessages = this.state.messages.map((m) => {
315
+ if (m._id === messageToDelete._id) {
316
+ return messageToDelete;
317
+ }
318
+ return m;
319
+ });
320
+ this.setState({
321
+ messages: revertedMessages,
322
+ deletingMessageId: null,
323
+ });
324
+ alert("Failed to delete message. Please try again.");
325
+ }
326
+ };
327
+
328
+ onCancelDelete = () => {
329
+ this.setState({
330
+ showDeleteConfirm: false,
331
+ messageToDelete: null,
332
+ });
333
+ };
334
+
335
+ sendMessage = () => {
336
+ const message = {
337
+ _id: Helper.randomString(),
338
+ text: this.state.messageInput,
339
+ user: {
340
+ _id: this.props.user.Id,
341
+ name: this.props.user.displayName,
342
+ avatar: this.props.user.profilePic,
343
+ },
344
+ };
345
+
346
+ if (!_.isEmpty(this.state.imageInput)) {
347
+ message.image = this.state.imageInput;
348
+ }
349
+
350
+ if (!_.isEmpty(this.state.fileInput)) {
351
+ message.attachments = this.state.fileInput;
352
+ }
353
+
354
+ if (
355
+ _.isEmpty(message.text) &&
356
+ _.isEmpty(message.image) &&
357
+ _.isEmpty(message.attachments)
358
+ ) {
359
+ return;
360
+ }
361
+
362
+ if (this.state.replyingTo) {
363
+ message.replyingTo = this.state.replyingTo;
364
+ }
365
+
366
+ const clonedMessage = _.cloneDeep(message);
367
+ clonedMessage.uploading = true;
368
+
369
+ circleActions.sendMessage(this.state.circleId, message).then((res) => {
370
+ Object.keys(res.data).forEach((key) => {
371
+ clonedMessage[key] = res.data[key];
372
+ });
373
+ clonedMessage.uploading = false;
374
+ this.getMessages(true);
375
+ });
376
+ this.setState({
377
+ messages: [...this.state.messages, clonedMessage],
378
+ messageInput: "",
379
+ imageInput: null,
380
+ imageInputShowing: false,
381
+ fileInput: null,
382
+ fileInputShowing: false,
383
+ });
384
+ setTimeout(() => {
385
+ this.onReply(null);
386
+ this.scrollToBottom();
387
+ this.imageInput && this.imageInput.setValue(null);
388
+ this.fileInput && this.fileInput.setValue(null);
389
+ }, 100);
390
+ };
391
+
392
+ validateCircleAdmin() {
393
+ if (
394
+ Session.validateAccess(
395
+ this.props.auth.site,
396
+ values.permission,
397
+ this.props.auth,
398
+ )
399
+ ) {
400
+ return true;
401
+ }
402
+ return _.some(this.getCircle().Audience, (user) => {
403
+ return user.userId === this.props.user.Id && user.isAdmin;
404
+ });
405
+ }
406
+
407
+ renderChatInput() {
408
+ if (!this.isMember()) {
409
+ return (
410
+ <Components.Text type="highlightedHelp" className="chat_noMessage">
411
+ You can't send a message to this {_.capitalize(values.entityName)} as
412
+ you are not a member.
413
+ </Components.Text>
414
+ );
415
+ }
416
+ return (
417
+ <div className="chat_inputFlex">
418
+ <FontAwesome
419
+ className="chat_send"
420
+ name="paper-plane"
421
+ onClick={this.sendMessage}
422
+ />
423
+ <div className="chat_inputContainer">
424
+ <Components.GenericInput
425
+ id="messageInput"
426
+ type="textarea"
427
+ className="chat_input"
428
+ componentClass="textarea"
429
+ placeholder="Enter message..."
430
+ value={this.state.messageInput}
431
+ onChange={(e) => this.onHandleChange(e)}
432
+ inputStyle={{
433
+ minHeight: 50,
434
+ }}
435
+ onKeyDown={this.onKeyDown}
436
+ />
437
+ <div>
438
+ <FontAwesome
439
+ className={`chat_imageIcon${this.state.imageInputShowing ? " chat_imageIcon-selected" : ""}`}
440
+ name="camera"
441
+ onClick={this.showImageInput}
442
+ />
443
+ <FontAwesome
444
+ className={`chat_imageIcon${this.state.fileInputShowing ? " chat_imageIcon-selected" : ""}`}
445
+ name="paperclip"
446
+ onClick={this.showFileInput}
447
+ />
448
+ </div>
449
+ <div
450
+ className="overflow-x"
451
+ style={{ display: this.state.imageInputShowing ? "block" : "none" }}
452
+ >
453
+ <Components.ImageInput
454
+ ref={(ref) => {
455
+ this.imageInput = ref;
456
+ }}
457
+ multiple
458
+ limit={10}
459
+ refreshCallback={this.onImageUpdated}
460
+ noMenu
461
+ noCompress
462
+ noDownload
463
+ />
464
+ </div>
465
+ <div
466
+ className="overflow-x"
467
+ style={{ display: this.state.fileInputShowing ? "block" : "none" }}
468
+ >
469
+ <Components.FileInput
470
+ ref={(ref) => {
471
+ this.fileInput = ref;
472
+ }}
473
+ multiple
474
+ limit={10}
475
+ refreshCallback={this.onFileUpdated}
476
+ noDownload
477
+ accept="application/pdf"
478
+ simpleStyle
479
+ />
480
+ </div>
481
+ </div>
482
+ </div>
483
+ );
484
+ }
485
+
486
+ renderMessage(m) {
487
+ if (m.system) {
488
+ return (
489
+ <div key={m._id} className="message">
490
+ <Components.Text type="h5" className="message_system">
491
+ {m.text}
492
+ </Components.Text>
493
+ </div>
494
+ );
495
+ }
496
+ const isSelf = m.user._id === this.props.user.Id;
497
+ const isDeleted = m.deleted === true;
498
+ const isDeleting = this.state.deletingMessageId === m._id;
499
+
500
+ return (
501
+ <div
502
+ key={m._id}
503
+ className={`message${isSelf ? " message-self" : ""}${m.uploading ? " message-uploading" : ""}${isDeleting ? " message-deleting" : ""}`}
504
+ >
505
+ <Components.Text type="h5-noUpper" className="message_time">
506
+ {moment.utc(m.createdAt).local().format("D MMM YYYY • h:mma")}
507
+ </Components.Text>
508
+ <div className="message_inner">
509
+ <Components.ProfilePic
510
+ size={40}
511
+ image={m.user.avatar}
512
+ className="message_profilePic"
513
+ />
514
+ <div className="message_bubbleContainer">
515
+ <Components.Text type="body" className="message_name">
516
+ {m.user.name}
517
+ {m.replyingTo && !isDeleted
518
+ ? ` replied to ${m.replyingTo.user.name}`
519
+ : ""}
520
+ </Components.Text>
521
+ {m.replyingTo && !isDeleted && (
522
+ <div className="message_replyBubble">
523
+ <Components.Text type="body" className="message_text">
524
+ {Helper.toParagraphed(
525
+ (m.replyingTo.text || "").substr(0, 100),
526
+ )}
527
+ </Components.Text>
528
+ </div>
529
+ )}
530
+ <div className="message_bubble">
531
+ <Components.Text type="body" className="message_text">
532
+ {isDeleted ? "[Message deleted]" : Helper.toParagraphed(m.text)}
533
+ </Components.Text>
534
+ {!isDeleted && (
535
+ <>
536
+ <div>
537
+ {(m.image || []).map((url, i) => {
538
+ return (
539
+ <a href={url} target="_blank" key={i}>
540
+ <img
541
+ className="message_image"
542
+ src={url}
543
+ alt={Helper.getFileName(url)}
544
+ />
545
+ </a>
546
+ );
547
+ })}
548
+ </div>
549
+ <div>
550
+ {(m.attachments || []).map((url, i) => {
551
+ return (
552
+ <Components.Attachment
553
+ source={url}
554
+ key={i}
555
+ white={isSelf}
556
+ />
557
+ );
558
+ })}
559
+ </div>
560
+ </>
561
+ )}
562
+ </div>
563
+ {!isDeleted && (
564
+ <div
565
+ className="message_reply"
566
+ style={{ display: "flex", gap: "16px" }}
567
+ >
568
+ <Components.Text
569
+ type="button"
570
+ onClick={() => {
571
+ this.onReply(m);
572
+ }}
573
+ >
574
+ Reply
575
+ </Components.Text>
576
+ {isSelf && (
577
+ <Components.Text
578
+ type="button"
579
+ onClick={() => {
580
+ this.onDeleteMessage(m);
581
+ }}
582
+ >
583
+ Delete
584
+ </Components.Text>
585
+ )}
586
+ </div>
587
+ )}
588
+ </div>
589
+ </div>
590
+ </div>
591
+ );
592
+ }
593
+
594
+ renderHeaderRight() {
595
+ if (!this.validateCircleAdmin() || this.getCircle().IsPrivate) {
596
+ return <div className="flex flex-center"></div>;
597
+ }
598
+ return (
599
+ <div className="flex flex-center">
600
+ <Link to={`/${values.featureKey}/edit/${this.state.circleId}`}>
601
+ <FontAwesome className="header_back" name="cog" />
602
+ </Link>
603
+ </div>
604
+ );
605
+ }
606
+
607
+ renderEmptyDate() {
608
+ return this.renderMessage({
609
+ system: true,
610
+ text: "No messages on this date",
611
+ });
612
+ }
613
+
614
+ renderSideBar() {
615
+ const members = this.getCircle().Audience || [];
616
+ return (
617
+ <div className="chat_sideBar">
618
+ <div className="chat_section">
619
+ <div className="chat_section_titleSection">
620
+ <FontAwesome
621
+ className="chat_section_titleSection_caret"
622
+ name={`chevron-${this.state.membersExpanded ? "up" : "down"}`}
623
+ onClick={this.toggleMembers}
624
+ />
625
+ <div className="flex-1">
626
+ <Components.Text type="formTitleMedium">
627
+ Member{Helper.getPluralS(members.length)} ({members.length})
628
+ </Components.Text>
629
+ </div>
630
+ </div>
631
+ {this.state.membersExpanded && (
632
+ <div className="paddingTop-8">
633
+ {members.map((user) => {
634
+ return <Components.UserListing user={user} />;
635
+ })}
636
+ </div>
637
+ )}
638
+ </div>
639
+ <div className="chat_section">
640
+ <div className="chat_section_titleSection">
641
+ <FontAwesome
642
+ className="chat_section_titleSection_caret"
643
+ name={`chevron-${this.state.imagesExpanded ? "up" : "down"}`}
644
+ onClick={this.toggleImages}
645
+ />
646
+ <div className="flex-1">
647
+ <Components.Text type="formTitleMedium">
648
+ Image{Helper.getPluralS(this.state.images.length)} (
649
+ {this.state.images.length})
650
+ </Components.Text>
651
+ </div>
652
+ </div>
653
+ {this.state.imagesExpanded && (
654
+ <div className="paddingTop-8">
655
+ {this.state.images.map((image) => {
656
+ return (
657
+ <a href={image.Url} target="_blank">
658
+ <img
659
+ src={image.Url}
660
+ className="chat_section_image"
661
+ alt={Helper.getFileName(image.Url)}
662
+ />
663
+ </a>
664
+ );
665
+ })}
666
+ </div>
667
+ )}
668
+ </div>
669
+ <div className="chat_section">
670
+ <div className="chat_section_titleSection">
671
+ <FontAwesome
672
+ className="chat_section_titleSection_caret"
673
+ name={`chevron-${this.state.filesExpanded ? "up" : "down"}`}
674
+ onClick={this.toggleFiles}
675
+ />
676
+ <div className="flex-1">
677
+ <Components.Text type="formTitleMedium">
678
+ File{Helper.getPluralS(this.state.files.length)} (
679
+ {this.state.files.length})
680
+ </Components.Text>
681
+ </div>
682
+ </div>
683
+ {this.state.filesExpanded && (
684
+ <div className="paddingTop-8">
685
+ {this.state.files.map((file, i) => {
686
+ return <Components.Attachment source={file.Url} key={i} />;
687
+ })}
688
+ </div>
689
+ )}
690
+ </div>
691
+ </div>
692
+ );
693
+ }
694
+
695
+ renderReplyTo() {
696
+ if (!this.state.replyingTo) {
697
+ return null;
698
+ }
699
+ const m = this.state.replyingTo;
700
+ return (
701
+ <div className="chat_replyTo">
702
+ <div className="chat_replyTo_container">
703
+ <Components.Text type="h5">
704
+ Replying to{" "}
705
+ {m && m.user && !_.isEmpty(m.user.displayName)
706
+ ? m.user.displayName
707
+ : ""}
708
+ </Components.Text>
709
+ {m && !_.isEmpty(m.text) && (
710
+ <Components.Text type="body">
711
+ {m.text.substr(0, 50)}
712
+ </Components.Text>
713
+ )}
714
+ </div>
715
+ <div className="chat_replyTo_remove">
716
+ <Components.SVGIcon
717
+ className="removeIcon"
718
+ icon="close"
719
+ onClick={() => {
720
+ this.onReply(null);
721
+ }}
722
+ colour={Colours.COLOUR_DUSK}
723
+ />
724
+ </div>
725
+ </div>
726
+ );
727
+ }
728
+
729
+ renderDeleteConfirmPopup() {
730
+ if (!this.state.showDeleteConfirm) return null;
731
+
732
+ return (
733
+ <Components.Popup
734
+ title="Delete Message"
735
+ buttons={[
736
+ {
737
+ text: "Delete",
738
+ onClick: this.onConfirmDelete,
739
+ type: "primary",
740
+ isActive: true,
741
+ },
742
+ {
743
+ text: "Cancel",
744
+ onClick: this.onCancelDelete,
745
+ type: "tertiary",
746
+ isActive: true,
747
+ },
748
+ ]}
749
+ onClose={this.onCancelDelete}
750
+ hasPadding
751
+ >
752
+ <Components.Text type="body">
753
+ Are you sure you want to delete this message? This action cannot be
754
+ undone.
755
+ </Components.Text>
756
+ </Components.Popup>
757
+ );
758
+ }
759
+
760
+ render() {
761
+ return (
762
+ <Components.OverlayPage
763
+ fullPage
764
+ fullPageStyle={{ display: "flex", flexDirection: "column" }}
765
+ >
766
+ {this.renderDeleteConfirmPopup()}
767
+ <Components.Header rightContent={this.renderHeaderRight()}>
768
+ <FontAwesome
769
+ className="header_back"
770
+ name="angle-left"
771
+ onClick={() => {
772
+ window.history.back();
773
+ }}
774
+ />
775
+ <Components.Text type="formTitleLarge" className="header_title">
776
+ {this.getTitle()}
777
+ </Components.Text>
778
+ </Components.Header>
779
+ <div className="flex flex-1 flex-reverse overflow-hidden">
780
+ {this.renderSideBar()}
781
+ <div className="flex-1 flex flex-column overflow-hidden">
782
+ <Components.Header onlyContainer>
783
+ <div className="flex flex-center flex-1 paddingHorizontal-16">
784
+ <Components.Text type="h5" className="marginRight-20">
785
+ Filter by
786
+ </Components.Text>
787
+ <div>
788
+ <Components.GenericInput
789
+ id="messageDate"
790
+ label="Date"
791
+ alwaysShowLabel
792
+ value={this.state.messageDateText}
793
+ readOnly
794
+ onClick={() =>
795
+ this.setState({
796
+ showMessageDate: !this.state.showMessageDate,
797
+ })
798
+ }
799
+ rightContent={
800
+ !_.isEmpty(this.state.messageDate) && (
801
+ <Components.SVGIcon
802
+ colour={Colours.COLOUR_DUSK_LIGHT}
803
+ icon="close"
804
+ className="timepicker_clear"
805
+ onClick={this.onClearDate}
806
+ />
807
+ )
808
+ }
809
+ />
810
+ {this.state.showMessageDate && (
811
+ <Components.DatePicker
812
+ hideTop
813
+ selectedDate={this.state.messageDate}
814
+ selectDate={this.handleMessageDateChange}
815
+ />
816
+ )}
817
+ </div>
818
+ </div>
819
+ </Components.Header>
820
+ <div className="chat">
821
+ <div className="chat_newMessage">{this.renderChatInput()}</div>
822
+ {this.renderReplyTo()}
823
+ <div ref={(ref) => (this.chat = ref)} className="chat_messages">
824
+ {_.isEmpty(this.state.messages) &&
825
+ !_.isEmpty(this.state.messageDate) &&
826
+ this.renderEmptyDate()}
827
+ {this.state.messages.map((m) => {
828
+ return this.renderMessage(m);
829
+ })}
830
+ </div>
831
+ </div>
832
+ </div>
833
+ </div>
834
+ </Components.OverlayPage>
835
+ );
836
+ }
748
837
  }
749
838
 
750
839
  const mapStateToProps = (state) => {
751
- const { circles } = state[values.reducerKey];
752
- const { auth } = state;
753
- return {
754
- circles,
755
- auth,
756
- user: Helper.getUserFromState(state),
757
- };
840
+ const { circles } = state[values.reducerKey];
841
+ const { auth } = state;
842
+ return {
843
+ circles,
844
+ auth,
845
+ user: Helper.getUserFromState(state),
846
+ };
758
847
  };
759
848
 
760
- export default connect(mapStateToProps, { circlesLoaded, setNavData: Actions.setNavData })(Circle);
849
+ export default connect(mapStateToProps, {
850
+ circlesLoaded,
851
+ setNavData: Actions.setNavData,
852
+ })(Circle);