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