diff --git a/releases/dev/nynja-app-web.yaml b/releases/dev/nynja-app-web.yaml index 559d97948ed4c988c96865387763470e39e40eda..de8ee12346727496270ec3a0cc3ed60264b9504a 100644 --- a/releases/dev/nynja-app-web.yaml +++ b/releases/dev/nynja-app-web.yaml @@ -28,7 +28,7 @@ spec: appleAppSiteAssociation: appId: 9GKQ5AMF2B.com.nynja.dev.mobile.communicator mqtt: - host: 34.221.152.42 + host: nynja-dev-uw2-messaging01.dev-eu.nynja.net confcall: service: calling-service.callconf.svc.cluster.local historyService: calling-service-history.callconf.svc.cluster.local diff --git a/src/assets/css/fonts.css b/src/assets/css/fonts.css index 2c3793aadbbc032005f5c18bb563901276923d97..758dec7eee21fd6fc51b56c3bf1cf44edbfb3ca7 100644 --- a/src/assets/css/fonts.css +++ b/src/assets/css/fonts.css @@ -327,3 +327,6 @@ .icon-ic_new_chat:before { content: "\e955"; } +.icon-ic_notifications_normal:before { + content: "\e957"; +} diff --git a/src/assets/fonts/demo_font_icons/demo.html b/src/assets/fonts/demo_font_icons/demo.html index e11669474d7709872c82d59b5ad7b7cdbbbad01c..cc522712058a9e99fbd811fa222e7a5fd85b391b 100644 --- a/src/assets/fonts/demo_font_icons/demo.html +++ b/src/assets/fonts/demo_font_icons/demo.html @@ -1,22 +1,26 @@ + IcoMoon Demo - + + +
-

Font Name: icomoon (Glyphs: 87)

+

Font Name: icomoon (Glyphs: 87) +

Grid Size: Unknown

- + icon-volume-mute2
@@ -32,7 +36,7 @@
- + icon-add_contact
@@ -48,7 +52,7 @@
- + icon-all
@@ -64,7 +68,7 @@
- + icon-audio
@@ -80,7 +84,7 @@
- + icon-contact_request
@@ -96,7 +100,7 @@
- + icon-contact
@@ -112,7 +116,7 @@
- + icon-files
@@ -128,7 +132,7 @@
- + icon-link
@@ -144,7 +148,7 @@
- + icon-location
@@ -160,7 +164,7 @@
- + icon-photo
@@ -176,7 +180,7 @@
- + icon-photos
@@ -192,7 +196,7 @@
- + icon-storage
@@ -208,7 +212,7 @@
- + icon-upload
@@ -224,7 +228,7 @@
- + icon-video
@@ -240,7 +244,7 @@
- + icon-videocall
@@ -256,7 +260,7 @@
- + icon-voicecall
@@ -272,7 +276,7 @@
- + icon-accepted
@@ -288,7 +292,7 @@
- + icon-ignored
@@ -304,7 +308,7 @@
- + icon-mic
@@ -320,7 +324,7 @@
- + icon-send
@@ -336,7 +340,7 @@
- + icon-calls
@@ -352,7 +356,7 @@
- + icon-chats
@@ -368,7 +372,7 @@
- + icon-contacts
@@ -384,7 +388,7 @@
- + icon-groups
@@ -400,7 +404,7 @@
- + icon-settings
@@ -416,7 +420,7 @@
- + icon-star
@@ -432,7 +436,7 @@
- + icon-add-open
@@ -448,7 +452,7 @@
- + icon-add
@@ -464,7 +468,7 @@
- + icon-play
@@ -480,7 +484,7 @@
- + icon-pause
@@ -496,7 +500,7 @@
- + icon-cancel
@@ -512,7 +516,7 @@
- + icon-download
@@ -528,7 +532,7 @@
- + icon-expand
@@ -544,7 +548,7 @@
- + icon-read
@@ -560,7 +564,7 @@
- + icon-sent
@@ -576,7 +580,7 @@
- + icon-unsent
@@ -592,7 +596,7 @@
- + icon-number
@@ -608,7 +612,7 @@
- + icon-username
@@ -624,7 +628,7 @@
- + icon-country
@@ -640,7 +644,7 @@
- + icon-phone
@@ -656,7 +660,7 @@
- + icon-new-group
@@ -672,7 +676,7 @@
- + icon-channels
@@ -688,7 +692,7 @@
- + icon-family
@@ -704,7 +708,7 @@
- + icon-history
@@ -720,7 +724,7 @@
- + icon-new_channel
@@ -736,7 +740,7 @@
- + icon-new_chat
@@ -752,7 +756,7 @@
- + icon-work
@@ -768,7 +772,7 @@
- + icon-delete
@@ -784,7 +788,7 @@
- + icon-play2
@@ -800,7 +804,7 @@
- + icon-stop
@@ -816,7 +820,7 @@
- + icon-cm_admin
@@ -832,7 +836,7 @@
- + icon-cm_another_transcribe
@@ -848,7 +852,7 @@
- + icon-cm_another_translate
@@ -864,7 +868,7 @@
- + icon-cm_copy
@@ -880,7 +884,7 @@
- + icon-cm_delete
@@ -896,7 +900,7 @@
- + icon-cm_edit
@@ -912,7 +916,7 @@
- + icon-cm_forward
@@ -928,7 +932,7 @@
- + icon-cm_info
@@ -944,7 +948,7 @@
- + icon-cm_reply
@@ -960,7 +964,7 @@
- + icon-cm_share
@@ -976,7 +980,7 @@
- + icon-cm_star
@@ -992,7 +996,7 @@
- + icon-cm_to_downloads
@@ -1008,7 +1012,7 @@
- + icon-cm_to_gallery
@@ -1024,7 +1028,7 @@
- + icon-cm_transcribe
@@ -1040,7 +1044,7 @@
- + icon-cm_translate
@@ -1056,7 +1060,7 @@
- + icon-arrow_down
@@ -1072,7 +1076,7 @@
- + icon-arrow_up
@@ -1088,7 +1092,7 @@
- + icon-calendar
@@ -1104,7 +1108,7 @@
- + icon-schedule
@@ -1120,7 +1124,7 @@
- + icon-save-edited
@@ -1136,7 +1140,7 @@
- + icon-closed-envelope
@@ -1152,7 +1156,7 @@
- + icon-email
@@ -1168,7 +1172,7 @@
- + icon-facebook
@@ -1184,7 +1188,7 @@
- + icon-ic_access
@@ -1200,7 +1204,7 @@
- + icon-ic_app
@@ -1216,7 +1220,7 @@
- + icon-ic_bots
@@ -1232,7 +1236,7 @@
- + icon-ic_design
@@ -1248,7 +1252,7 @@
- + icon-ic_freelance
@@ -1264,7 +1268,7 @@
- + icon-ic_groups
@@ -1280,7 +1284,7 @@
- + icon-ic_interpretation
@@ -1296,7 +1300,7 @@
- + icon-ic_market
@@ -1312,7 +1316,7 @@
- + icon-ic_media
@@ -1328,7 +1332,7 @@
- + icon-ic_stickers
@@ -1344,7 +1348,7 @@
- + icon-ic_support
@@ -1360,7 +1364,7 @@
- + icon-ic_virtual-goods
@@ -1376,7 +1380,7 @@
- + icon-ic_call
@@ -1392,7 +1396,7 @@
- + icon-ic_new_chat
@@ -1405,18 +1409,32 @@
+
+
+ + + + icon-ic_notifications_normal +
+
+ + +
+
+ liga: + +
+

Font Test Drive

- +
 
@@ -1427,4 +1445,5 @@ - + + \ No newline at end of file diff --git a/src/assets/fonts/demo_font_icons/fonts/icomoon.eot b/src/assets/fonts/demo_font_icons/fonts/icomoon.eot index 661a87351065eee4cc234c1495202d0f01a03d9a..2015514674d489f44258ae548340da3a3a24a02d 100644 Binary files a/src/assets/fonts/demo_font_icons/fonts/icomoon.eot and b/src/assets/fonts/demo_font_icons/fonts/icomoon.eot differ diff --git a/src/assets/fonts/demo_font_icons/fonts/icomoon.svg b/src/assets/fonts/demo_font_icons/fonts/icomoon.svg index 238810aa13a0c72fa9e79ded9b32ac12d3fdf8c7..6eedb812d5937339b187bb9786b2cd3ac4a70cfc 100644 --- a/src/assets/fonts/demo_font_icons/fonts/icomoon.svg +++ b/src/assets/fonts/demo_font_icons/fonts/icomoon.svg @@ -93,5 +93,6 @@ + \ No newline at end of file diff --git a/src/assets/fonts/demo_font_icons/fonts/icomoon.ttf b/src/assets/fonts/demo_font_icons/fonts/icomoon.ttf index 44b3bfbf168498a53d1129685168d1bb5f9403c5..dec4d52080d6e933dab9c3939518c5e6de29ab18 100644 Binary files a/src/assets/fonts/demo_font_icons/fonts/icomoon.ttf and b/src/assets/fonts/demo_font_icons/fonts/icomoon.ttf differ diff --git a/src/assets/fonts/demo_font_icons/fonts/icomoon.woff b/src/assets/fonts/demo_font_icons/fonts/icomoon.woff index 5795684eba26df4abded0f3fe08ce88b5cfcb1ea..67ed487ba969e981a5bdb257e0e2c376a5d2a6e8 100644 Binary files a/src/assets/fonts/demo_font_icons/fonts/icomoon.woff and b/src/assets/fonts/demo_font_icons/fonts/icomoon.woff differ diff --git a/src/assets/fonts/demo_font_icons/style.css b/src/assets/fonts/demo_font_icons/style.css index fd61d8649a67d466313dbfda8b3d7161f769d1e3..7e1a763b23450d06b991ad46d10947f1b11ae338 100644 --- a/src/assets/fonts/demo_font_icons/style.css +++ b/src/assets/fonts/demo_font_icons/style.css @@ -285,3 +285,6 @@ .icon-ic_new_chat:before { content: "\e955"; } +.icon-ic_notifications_normal:before { + content: "\e957"; +} diff --git a/src/assets/fonts/icons/icomoon.eot b/src/assets/fonts/icons/icomoon.eot old mode 100755 new mode 100644 index 661a87351065eee4cc234c1495202d0f01a03d9a..2015514674d489f44258ae548340da3a3a24a02d Binary files a/src/assets/fonts/icons/icomoon.eot and b/src/assets/fonts/icons/icomoon.eot differ diff --git a/src/assets/fonts/icons/icomoon.svg b/src/assets/fonts/icons/icomoon.svg old mode 100755 new mode 100644 index 238810aa13a0c72fa9e79ded9b32ac12d3fdf8c7..6eedb812d5937339b187bb9786b2cd3ac4a70cfc --- a/src/assets/fonts/icons/icomoon.svg +++ b/src/assets/fonts/icons/icomoon.svg @@ -93,5 +93,6 @@ + \ No newline at end of file diff --git a/src/assets/fonts/icons/icomoon.ttf b/src/assets/fonts/icons/icomoon.ttf old mode 100755 new mode 100644 index 44b3bfbf168498a53d1129685168d1bb5f9403c5..dec4d52080d6e933dab9c3939518c5e6de29ab18 Binary files a/src/assets/fonts/icons/icomoon.ttf and b/src/assets/fonts/icons/icomoon.ttf differ diff --git a/src/assets/fonts/icons/icomoon.woff b/src/assets/fonts/icons/icomoon.woff old mode 100755 new mode 100644 index 5795684eba26df4abded0f3fe08ce88b5cfcb1ea..67ed487ba969e981a5bdb257e0e2c376a5d2a6e8 Binary files a/src/assets/fonts/icons/icomoon.woff and b/src/assets/fonts/icons/icomoon.woff differ diff --git a/src/assets/languages/ActionsMenu/ActionsMenu.en.json b/src/assets/languages/ActionsMenu/ActionsMenu.en.json index f896b1bc174034c069d79b00a8eb5d4af0a7ba73..5341bb22ad5d71124783b7142b24a1148d1ac254 100644 --- a/src/assets/languages/ActionsMenu/ActionsMenu.en.json +++ b/src/assets/languages/ActionsMenu/ActionsMenu.en.json @@ -35,5 +35,7 @@ "bots": "Bots", "apps": "Apps", "emailSubject": "Join the NYNJA SuperApp", - "emailBody": "I'm using the new SuperApp called NYNJA! It is an amazing productivity and communications app for teams that work! Download it here: $1 and add me as a contact with my NYNJA user ID: $2" + "emailBody": "I'm using the new SuperApp called NYNJA! It is an amazing productivity and communications app for teams that work! Download it here: $1 and add me as a contact with my NYNJA user ID: $2", + "settings": "Settings", + "notifications": "Notifications" } \ No newline at end of file diff --git a/src/assets/languages/Modals/MediaPreviewModal.en.json b/src/assets/languages/Modals/MediaPreviewModal.en.json new file mode 100644 index 0000000000000000000000000000000000000000..effc26bfc79dc271058a5a508e705dc1f0b453d7 --- /dev/null +++ b/src/assets/languages/Modals/MediaPreviewModal.en.json @@ -0,0 +1,8 @@ +{ + "mediaPreviewModal": { + "share": "Share...", + "saveAs": "Save As...", + "showInMaps": "Show in Maps", + "close": "Close" + } +} \ No newline at end of file diff --git a/src/assets/languages/Noifications/Notifications.en.json b/src/assets/languages/Noifications/Notifications.en.json new file mode 100644 index 0000000000000000000000000000000000000000..cd1980e58af0e8824fa2b571c1be26b3fff3c914 --- /dev/null +++ b/src/assets/languages/Noifications/Notifications.en.json @@ -0,0 +1,18 @@ +{ + "title": "Notifications | NYNJA", + "appSounds": "App Sounds", + "messageNotifications": "Message Notifications", + "showNotifications": "Show Notifications", + "incomingMessageSound": "Incoming message sound", + "groupEvents": "Group Events", + "groupEventSound": "Group event sound", + "callNotifications": "Call Notifications", + "soundNotificationsDuringCall": "Sound notifications during call", + "incomingCallSoundInMutedChat": "Incoming call sound In muted chat", + "incomingCallSound": "Incoming call sound", + "otherNotifications": "Other Notifications", + "otherNotificationsSound": "Other notifications sound", + "notifications": "Notifications", + "mute": "Mute", + "unmute": "Unmute" +} \ No newline at end of file diff --git a/src/componets/Modals/PreviewModals/MediaPreview/MediaPreview.js b/src/componets/Modals/PreviewModals/MediaPreview/MediaPreview.js index 9de9347861e438e6467732b3725da0d7e45415ed..39bd5d1bd99c28e0b302e4dc70ed287532e361bb 100644 --- a/src/componets/Modals/PreviewModals/MediaPreview/MediaPreview.js +++ b/src/componets/Modals/PreviewModals/MediaPreview/MediaPreview.js @@ -8,6 +8,8 @@ import saveAs from 'file-saver'; import { profileSelectors } from 'Resource/profile'; import LinearProgress from 'material-ui/Progress/LinearProgress'; import { fileStore } from 'Core/resource/messages/files/modules/Store.module'; +import translations from 'Assets/languages/Modals/MediaPreviewModal.en'; +import LocalizeHOC from '../../../../containers/LocalizeHOC'; const mapStateToProps = (state) => { return { @@ -28,15 +30,6 @@ class MediaPreview extends Component { audioError: false, showOriginalMessage: true, }; - - this.toggleAudio = this.toggleAudio.bind(this); - this.toggleMute = this.toggleMute.bind(this); - this.audioTimeUpdate = this.audioTimeUpdate.bind(this); - this.audioMetadataLoaded = this.audioMetadataLoaded.bind(this); - this.onAudioError = this.onAudioError.bind(this); - this.onAudioEnd = this.onAudioEnd.bind(this); - this.pauseAudio = this.pauseAudio.bind(this); - this.jumpInTime = this.jumpInTime.bind(this); } componentDidMount() { @@ -65,18 +58,18 @@ class MediaPreview extends Component { document.removeEventListener("voicePlayStart", this.pauseAudio); } - pauseAudio(event) { + pauseAudio = (event) => { if (this.audio && JSON.stringify(event.data) != JSON.stringify(this.props.data) && !this.audio.paused && !this.audio.ended) { this.audio.pause(); this.setState({ playing: false }); } } - onAudioError() { + onAudioError = () => { this.setState({ audioError: true }); } - audioMetadataLoaded(event) { + audioMetadataLoaded = (event) => { const audio = event.currentTarget; self = this; @@ -84,6 +77,7 @@ class MediaPreview extends Component { audio.currentTime = 1e101; audio.ontimeupdate = function () { this.ontimeupdate = () => { return; } + try { if (audio.duration >= 60 * 60) { self.setState({ duration: new Date(audio.duration * 1000).toISOString().substr(11, 8) }); @@ -102,7 +96,7 @@ class MediaPreview extends Component { } } - audioTimeUpdate(event) { + audioTimeUpdate = (event) => { const audio = event.currentTarget; if (!audio.duration || audio.duration == Infinity) { @@ -124,17 +118,17 @@ class MediaPreview extends Component { } } - jumpInTime(event) { + jumpInTime = (event) => { if (this.state.audioError || !this.audio) { return; } - const rel = event.offsetX / event.currentTarget.childNodes[0].clientWidth; + const rel = event.layerX / event.currentTarget.childNodes[1].offsetWidth; const jump = (this.audio.duration * rel) || 0; this.audio.currentTime = jump; } - toggleAudio() { + toggleAudio = () => { if (this.audio && !this.state.audioError) { if (this.audio.paused || this.audio.ended) { this.audio.play(); @@ -151,7 +145,7 @@ class MediaPreview extends Component { } } - toggleMute() { + toggleMute = () => { if (this.audio && !this.state.audioError) { this.audio.muted = !this.audio.muted; this.setState(prevState => ({ @@ -160,7 +154,7 @@ class MediaPreview extends Component { } } - onAudioEnd() { + onAudioEnd = () => { setTimeout(() => { this.setState({ playing: false, audioProgress: 0 }); }, 500); @@ -171,7 +165,7 @@ class MediaPreview extends Component { } render() { - const { classes, showModal, onCloseModal, fileType, fileName, fileSize, fileCreated, locationURLS, onShare } = this.props; + const { classes, showModal, onCloseModal, fileType, fileName, fileSize, fileCreated, locationURLS, onShare, translate } = this.props; const { playing, muted, fileURL, audioProgress, duration } = this.state; let media; @@ -202,7 +196,9 @@ class MediaPreview extends Component { case filesType.AUDIO: media = (
-
diff --git a/src/containers/ChatBottomPanel/ChatBottomPanel.styles.js b/src/containers/ChatBottomPanel/ChatBottomPanel.styles.js index 32aae8e07a4c5959ba2ce3b8f9bd63df0cb30dca..2ef34387db147e581146bb31bbd5f5e04a670255 100644 --- a/src/containers/ChatBottomPanel/ChatBottomPanel.styles.js +++ b/src/containers/ChatBottomPanel/ChatBottomPanel.styles.js @@ -47,7 +47,8 @@ export default theme => ({ boxShadow: '0 0 30px 30px #151619' }, inputWrap: { - flex: '1 0 auto' + flex: '1 0 auto', + maxWidth: 'calc(100% - 133px)' }, input: { zIndex: 1, @@ -61,7 +62,10 @@ export default theme => ({ borderRadius: 6, background: '#35383e', width: '100%', - maxWidth: '626px' + maxWidth: '626px', + '& textarea': { + overflow: 'hidden', + } }, allowMediaOpenImg: { maxWidth: '100% !important' diff --git a/src/containers/ChatBottomPanel/Mentions.styles.js b/src/containers/ChatBottomPanel/Mentions.styles.js index 693bd0301bb7fd166ce90f09bda8dd1dff0c08ab..177224f91279a08667ec0a84ebb0a19999cbd87a 100644 --- a/src/containers/ChatBottomPanel/Mentions.styles.js +++ b/src/containers/ChatBottomPanel/Mentions.styles.js @@ -18,7 +18,7 @@ export default { border: 'none', color: '#ffffff', caretColor: '#b80000', - width: '98%', + width: '93%', maxWidth: 399, height: 32 } @@ -46,7 +46,7 @@ export default { caretColor: '#b80000', fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', maxHeight: 64, - width: '98%' + width: '93%' } }, diff --git a/src/containers/ContactStorage/ContactStorage.js b/src/containers/ContactStorage/ContactStorage.js index dc2e8aeac8a5143ebfa7c6cdb56580378fa838bc..efc4b6f2d1b596eb97518a5fcd13da443672ae9e 100644 --- a/src/containers/ContactStorage/ContactStorage.js +++ b/src/containers/ContactStorage/ContactStorage.js @@ -312,7 +312,8 @@ class ContactStorage extends Component { storageType, dialogId, isGroup, - storageLoading + storageLoading, + fromGroupOptions } = this.props; const { showPreview, previewFile, showShare } = this.state; @@ -335,7 +336,7 @@ class ContactStorage extends Component { className={this.props.storageArrowClassname} onClick={this.props.toggleStorage} > - + { storageLoading && diff --git a/src/containers/ContactStorage/ContactStorage.styles.js b/src/containers/ContactStorage/ContactStorage.styles.js index 0a0bef912bd9a23d05f72f3386568e517ffbdd1d..e68a2884fcbdb3c2d4fce9fa755a8d6461f0d54b 100644 --- a/src/containers/ContactStorage/ContactStorage.styles.js +++ b/src/containers/ContactStorage/ContactStorage.styles.js @@ -182,7 +182,7 @@ export default (theme, avatarPic) => ({ padding: 20, color: '#3386EA', borderBottom: '1px solid #000', - whiteSpace: 'nowrap', + whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', '&:hover': { @@ -192,4 +192,7 @@ export default (theme, avatarPic) => ({ color: '#3386EA', } }, + hide: { + display: 'none !important' + } }); diff --git a/src/containers/ConversationsList/ConversationsList.js b/src/containers/ConversationsList/ConversationsList.js index 5b6ebe9857226d14e484dd1a6497f148313b7584..8448cc903e8aad844b83a4bcf9d4eaa806ec1403 100644 --- a/src/containers/ConversationsList/ConversationsList.js +++ b/src/containers/ConversationsList/ConversationsList.js @@ -328,8 +328,8 @@ class ConversationsList extends Component { member = admins[phone_id]; } - const notifications = member.settings.filter(el => el.key === 'NOTIFICATIONS')[0]; - const value = notifications ? (notifications.value === 'true') ? true : false : false; + const notifications = member.settings.filter(el => el.group === 'GROUP_NOTIFICATIONS')[0]; + const value = notifications ? JSON.parse(notifications.value) : true; if (data === 'muteGroup') { return !value; @@ -482,8 +482,8 @@ class ConversationsList extends Component { member = admins[phone_id]; } - const notifications = member.settings.filter(el => el.key === 'NOTIFICATIONS')[0]; - const value = notifications ? (notifications.value === 'true') : false; + const notifications = member.settings.filter(el => el.group === 'GROUP_NOTIFICATIONS')[0]; + const value = notifications ? JSON.parse(notifications.value) : true; this.setState({ selectedGroup: selectedGroup.id, @@ -513,7 +513,7 @@ class ConversationsList extends Component { autoTranslateIncoming: autoTranslateIncoming ? JSON.parse(autoTranslateIncoming.value) : false, autoTranslateOutgoing: autoTranslateOutgoing ? autoTranslateOutgoing.value : 'Off', translateLang: translateLang ? translateLang.value : 'English:en', - notifications: !notificationsGroup ? true : false + notifications: !notificationsGroup }; actionsGroup.setNotifications(payload); diff --git a/src/containers/GroupOptions/GroupOptions.js b/src/containers/GroupOptions/GroupOptions.js index 65a26256bf7bbe003a49fc67965ed939812f1bbe..2746530bcd9ddff3c16b0aa04a8758dbac36471d 100644 --- a/src/containers/GroupOptions/GroupOptions.js +++ b/src/containers/GroupOptions/GroupOptions.js @@ -3,7 +3,6 @@ import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import TextField from 'material-ui/TextField'; import { withStyles } from 'material-ui/styles'; -import { addTranslationForLanguage, getTranslate } from 'react-localize-redux'; import { Tabs, Tab, Button, Chip, Avatar, ExpansionPanel, ExpansionPanelDetails, ExpansionPanelSummary, Switch @@ -17,7 +16,6 @@ import { contactsActions, contactsSelectors } from 'Resource/contacts/index'; import { notificationActions } from 'Resource/notification'; import { SliderPanel, MembersList, MemberDetailsPanel, ContactStorage, ExportCsvPanel } from 'Containers'; import { FancyAvatar, SearchBar, Modal } from '../../componets'; -import * as ComponentJson from 'Assets/languages/Groups/GroupOptions.en.json'; import { withRouter } from 'react-router'; import modes from 'Assets/helpers/modes'; import CustomScroll from 'react-custom-scrollbars'; @@ -29,6 +27,8 @@ import * as filesType from 'Core/configs/File.config'; import { ContextMenu, Item } from 'react-contexify'; import 'react-contexify/dist/ReactContexify.min.css'; import { storageActions } from 'Core/resource/storage'; +import translations from 'Assets/languages/Groups/GroupOptions.en.json'; +import LocalizeHOC from "../../containers/LocalizeHOC"; const TRANSLATION_KEY_PATH = ''; @@ -178,6 +178,7 @@ const defaultState = { myRoomAliasUpdated: '', groupLink: '', contacts: {}, + notifications: true, displayAddModal: false, displayRemoveModal: false, displayPromoteModal: false, @@ -215,9 +216,6 @@ const defaultState = { } }; -@connect(state => ({ - translate: getTranslate(state.locale) -})) class GroupOptions extends PureComponent { constructor(props) { super(props); @@ -228,7 +226,7 @@ class GroupOptions extends PureComponent { // this.setState({ [`${stateName}`]: !this.state[stateName] }); // } - toggle(panelName) { + toggle(panelName, callback) { const { router } = this.props; const newStatuses = Object.assign({}, this.state.panelsStatuses); @@ -246,7 +244,11 @@ class GroupOptions extends PureComponent { } } - this.setState({ panelsStatuses: newStatuses }); + this.setState({ panelsStatuses: newStatuses }, () => { + if (callback) { + callback(); + } + }); } toggleModal = () => { @@ -476,7 +478,6 @@ class GroupOptions extends PureComponent { // if (this.state.roomName === '' && this.props.room && this.props.room.name) { // this.setState({ roomName: this.props.room.name }); // } - this.props.dispatch(addTranslationForLanguage(ComponentJson, 'en')); } componentWillReceiveProps(props) { @@ -497,23 +498,26 @@ class GroupOptions extends PureComponent { const { settings, alias } = getMemberSettings(); myAlias = alias; - const showCsvPanel = router.params.mode === modes.exportCsv; - const notifications = settings.filter(el => el.key === 'NOTIFICATIONS')[0]; + const notifications = settings.filter(el => el.group === 'GROUP_NOTIFICATIONS')[0]; + const panelsStatuses = this.state.panelsStatuses; + + panelsStatuses.showAdminsPanel = router.params.mode === modes.exportCsv; + if (lastSelectedRoomName !== roomName) { this.setState({ - notifications: notifications ? notifications.value === "true" : false, + notifications: notifications ? JSON.parse(notifications.value) : true, myRoomAliasUpdated: alias, roomNameUpdated: props.room && props.room.name ? props.room.name : '', roomDescUpdated: props.room && props.room.description ? props.room.description : '', lastSelectedRoomName: roomName, - panelsStatuses: { ...this.state.panelsStatuses, showCsvPanel }, + panelsStatuses, groupLink, groupLinkShort: linkId, }); } else { this.setState({ - panelsStatuses: { ...this.state.panelsStatuses, showCsvPanel }, - notifications: notifications ? notifications.value === "true" : false, + notifications: notifications ? JSON.parse(notifications.value) : true, + panelsStatuses, }); } } @@ -556,14 +560,14 @@ class GroupOptions extends PureComponent { } } - handleNotificationsChange = name => (event) => { - this.setState({ [name]: event.target.checked }, - () => this.submitNotificationsChange() + handleNotificationsChange = (event) => { + const newValue = event.target.checked; + this.setState({ notifications: newValue }, + () => this.submitNotificationsChange(newValue) ); }; - submitNotificationsChange = () => { - const { notifications } = this.state; + submitNotificationsChange = (notifications) => { const { getMemberSettings, room, actions } = this.props; const { settings, id } = getMemberSettings(); const autoTranslateIncoming = settings.filter(el => el.key === 'AUTO_TRANSLATE')[0]; @@ -576,7 +580,7 @@ class GroupOptions extends PureComponent { autoTranslateIncoming: autoTranslateIncoming ? JSON.parse(autoTranslateIncoming.value) : false, autoTranslateOutgoing: autoTranslateOutgoing ? autoTranslateOutgoing.value : 'Off', translateLang: translateLang ? translateLang.value : 'English:en', - notifications: notifications ? !notifications.value : false + notifications }; actions.setNotifications(payload); @@ -774,20 +778,21 @@ class GroupOptions extends PureComponent { handleStorageItemClick = (storageType) => { const { actions } = this.props; - this.toggle(`show${storageType}StoragePanel`); - - const data = { - type: storageType, - dialogId: this.props.router.params.id || this.props.router.params.phone_id, - isGroup: true - }; + const getstorageData = () => { + const data = { + type: storageType, + dialogId: this.props.router.params.id || this.props.router.params.phone_id, + isGroup: true + }; - this.setState({ - storageType, - activeDialogId: data.dialogId - }); + this.setState({ + storageType, + activeDialogId: data.dialogId + }); - actions.getStorage(data); + actions.getStorage(data); + } + this.toggle(`show${storageType}StoragePanel`, getstorageData); } closeAllModals = () => { @@ -913,7 +918,7 @@ class GroupOptions extends PureComponent { roomDesc, roomDescUpdated, lastSelectedRoomName, myRoomAlias, myRoomAliasUpdated, groupLink, showControll, panelsStatuses, displayClearHistoryModal, displayDeleteAndClearHistoryModal, displayDeleteAndKeepHistoryModal, displayRemoveModal, displayPromoteModal, displayDemoteModal, displayPromoteBeforeLeavingModal, showMoreDesc, selectedAdmins, adminsToRemove, - activeDialogId, storageType + activeDialogId, storageType, notifications } = this.state; const { classes, translate, isAdmin, isBanned, router, room, connectionStatus @@ -946,10 +951,9 @@ class GroupOptions extends PureComponent { const disableChangeAliasBtn = this.validateEmptyAlias(); const roomDescShrinked = roomDesc ? `${roomDesc.substring(0, 100)}...` : ''; const showStorage = showimageStoragePanel || showvideoStoragePanel || showfileStoragePanel || showaudioStoragePanel || showcontactStoragePanel || showlocationStoragePanel || showlinkStoragePanel; - + return (
- { isAdmin ? @@ -1136,7 +1140,7 @@ class GroupOptions extends PureComponent { onClick={() => this.toggle('showNotificationsPanel')}> {translate('groupOptionsPanel.notifications')} - {this.state.notifications ? translate('notifications.unmute') : translate('notifications.mute')} + {notifications ? translate('notifications.unmute') : translate('notifications.mute')} @@ -1371,6 +1375,7 @@ class GroupOptions extends PureComponent { storageType={storageType} dialogId={activeDialogId} isGroup={true} + fromGroupOptions={true} /> @@ -1444,9 +1449,9 @@ class GroupOptions extends PureComponent {
{translate('groupOptionsPanel.notifications')}
@@ -1704,7 +1709,8 @@ class GroupOptions extends PureComponent { } } +const localized = LocalizeHOC({ translations })(GroupOptions); export default connect( mapStateToProps, mapDispatchToProps -)(withStyles(styles)(withRouter(GroupOptions))); +)(withStyles(styles)(withRouter(localized))); diff --git a/src/containers/GroupsMainArea/GroupsMainArea.js b/src/containers/GroupsMainArea/GroupsMainArea.js index ac7cf4e002aa4c291697a4e91d7bb45a6e2a4431..50e0b8c56eeca66ea27a4809ec36f327993051eb 100644 --- a/src/containers/GroupsMainArea/GroupsMainArea.js +++ b/src/containers/GroupsMainArea/GroupsMainArea.js @@ -15,7 +15,6 @@ import { ForwardMessagePanel, ScheduleMessagePanel, LanguageSelectDialog, - GroupOptions, MediaClipboardHandler } from '../../containers'; import stylesFunc from "./GroupsMainArea.styles"; @@ -343,15 +342,10 @@ class GroupsMainArea extends Component { storageType, expandChatFeature, showAlternativeHeader, - showGroupOptions, isBanned, translate, storageArrowClassname, toggleStorage, - isMarketplaceShown, - showStartGroupCall, - onStorageItemClick, - params, } = this.props; const { @@ -378,7 +372,6 @@ class GroupsMainArea extends Component { return (
- {!isMarketplaceShown && !showStartGroupCall && !isBanned && } el.key === 'AUTO_TRANSCRIBE')[0]; const autoTranslateTranscribe = data.filter(el => el.key === 'AUTO_TRANSLATE_TRANSCRIBE')[0]; const transcribeLang = data.filter(el => el.key === 'TRANSCRIBE_LANGUAGE')[0]; - const notifications = data.filter(el => el.key === 'NOTIFICATIONS')[0]; + const notifications = data.filter(el => el.group === 'GROUP_NOTIFICATIONS')[0]; this.setState({ autoTranslateIncoming: autoTranslateIncoming ? JSON.parse(autoTranslateIncoming.value) : false, @@ -175,7 +175,7 @@ class LanguageSettingsPanel extends Component { autoTranscribe: autoTranscribe ? JSON.parse(autoTranscribe.value) : true, autoTranslateTranscribe: autoTranslateTranscribe ? JSON.parse(autoTranslateTranscribe.value) : false, transcribeLang: transcribeLang ? transcribeLang.value : 'English:en', - notifications: notifications ? JSON.parse(notifications.value) : false + notifications: notifications ? JSON.parse(notifications.value) : true }); } diff --git a/src/core/configs/BrowserNotifications.config.js b/src/core/configs/BrowserNotifications.config.js new file mode 100644 index 0000000000000000000000000000000000000000..0c0e3c569f179aa7ff772eea4399ae0df119d0de --- /dev/null +++ b/src/core/configs/BrowserNotifications.config.js @@ -0,0 +1,4 @@ +export const P2P_CHAT_MESSAGE = 'p2p-chat-message'; +export const GROUP_CHAT_MESSAGE = 'group-chat-message'; +export const GROUP_EVENT_MESSAGE = 'group-event-message'; +export const OTHER_NOTIFICATIONS = 'other-notifications'; diff --git a/src/core/managers/browserNotificationsManager.js b/src/core/managers/browserNotificationsManager.js new file mode 100644 index 0000000000000000000000000000000000000000..b2817410c9400cb3d1df019ffcc0813bff6a5657 --- /dev/null +++ b/src/core/managers/browserNotificationsManager.js @@ -0,0 +1,56 @@ +import { browserHistory } from 'react-router'; +import logoPng from 'Assets/img/logo.png'; + +/** + * Requests permission to show browser notifications + * + * @returns void + */ +const requestNotificationPermission = () => { + if (window.Notification && window.Notification.permission !== 'denied') { + window.Notification.requestPermission(); + } +}; + +/** + * Shows browser notification + * + * @param {String} body + * @param {String} type + * @param {String} redirectLink + * @param {String} title + * @param {String} icon + * @param {Function} callback + * @returns void + */ +const showNotification = (body, type, redirectLink = null, title = 'NYNJA', icon = null, callback = null) => { + const options = { + body: body, + icon: icon ? icon : logoPng + }; + + if (type) { + options.tag = type; + } + + const notification = new window.Notification(title, options); + + notification.onclick = (event) => { + event.preventDefault(); + parent.focus(); + window.focus(); + + if (typeof redirectLink === 'string') { + browserHistory.push(redirectLink); + } + + if (typeof callback === 'function') { + callback(); + } + }; +}; + +export { + requestNotificationPermission, + showNotification +}; diff --git a/src/core/resource/Root.reducer.js b/src/core/resource/Root.reducer.js index 8e7ea181d75e1c0ac2c489d1c4e0f505e8d4055a..da6513cf7c92bbcdfa013524fc8e359375ef3c30 100644 --- a/src/core/resource/Root.reducer.js +++ b/src/core/resource/Root.reducer.js @@ -26,6 +26,7 @@ import { settingsReducer } from './messages/settings/index'; import { scheduleMessageReducer } from './messages/schedule'; import { marketplaceReducer } from './marketplace'; import { storageReducer } from './storage'; +import { settings } from './settings/index'; // avoid circle dependencies const appReducer = combineReducers({ @@ -59,7 +60,8 @@ const appReducer = combineReducers({ room }), marketplace: marketplaceReducer, - storage: storageReducer + storage: storageReducer, + settings, }); const rootReducer = (state, action) => { diff --git a/src/core/resource/Root.saga.js b/src/core/resource/Root.saga.js index 80aeea0a365a5d9c4552c0abad62ea9d3d935a30..dce5270e3456d4c8b23b5be145c00662a1653eea 100644 --- a/src/core/resource/Root.saga.js +++ b/src/core/resource/Root.saga.js @@ -18,6 +18,7 @@ import { rootSettings } from './messages/settings/index'; import { rootScheduleMessage } from './messages/schedule/'; import rootMarketplaceSaga from './marketplace/modules/MarketplaceSaga'; import { rootStorageSaga } from './storage'; +import { rootSettingsSaga } from './settings/index'; const isGenerator = fn => fn instanceof (function* () {}).constructor; @@ -41,6 +42,7 @@ function * rootSaga() { yield fork(rootScheduleMessage); yield fork(rootMarketplaceSaga); yield fork(rootStorageSaga); + yield fork(rootSettingsSaga); } export { rootSaga }; diff --git a/src/core/resource/contacts/modules/Contacts.module.js b/src/core/resource/contacts/modules/Contacts.module.js index 2c9e6eead701ccb88363aca0fdfc7f2af300598f..f95ebe6355dbc3d64a0b1b80f92854d6b7991e63 100644 --- a/src/core/resource/contacts/modules/Contacts.module.js +++ b/src/core/resource/contacts/modules/Contacts.module.js @@ -1,10 +1,8 @@ import { createAction, createReducer } from 'redux-act'; -import has from 'lodash/has'; import { profileSelectors } from 'Resource/profile'; import { userListSelectors } from 'Resource/userlist'; import merge from 'deepmerge'; import map from 'lodash/map'; -import matches from 'lodash/matches'; import filter from 'lodash/filter'; const stateName = 'contacts'; @@ -104,15 +102,6 @@ const getAllRequestedContacts = (state) => { return getFullContactsByStatus(state, { status }); }; -// Deprecated -// const getAuthContactRequest = (state) => { -// const status = { -// is_authorized: true -// }; - -// return getFullContactsByStatus(state, { status }); -// }; - const getFriends = (state) => { const status = { is_friend: true @@ -167,7 +156,6 @@ const responseContactInternal = createAction('RESPONSE_CONTACT_INTERNAL'); const responseContactAuthorization = createAction('RESPONSE_CONTACT_AUTHORIZATION'); // Server action catch in saga const saveNewAuthorizationContactRequest = createAction('SAVE_NEW_AUTHORIZATION_CONTACT_REQUEST'); // Saga call save data to store - //-- const requestContact = createAction('REQUEST_CONTACT', acceptApply); const acceptContact = createAction('ACCEPT_CONTACT', acceptApply); @@ -177,7 +165,6 @@ const unblockContact = createAction('UNBLOCK_CONTACT', acceptApply); const muteContact = createAction('MUTE_CONTACT', initApply); const unmuteContact = createAction('UNMUTE_CONTACT', initApply); - const initHandler = (state, payload) => Object.assign( {}, state, diff --git a/src/core/resource/contacts/sagas/Contact.saga.js b/src/core/resource/contacts/sagas/Contact.saga.js index 3ebbfa51b1256bdef0e93f318ee85c4caba044c4..cea4f8d0ee911a7afbf487fd2f46bfed2f100bc1 100644 --- a/src/core/resource/contacts/sagas/Contact.saga.js +++ b/src/core/resource/contacts/sagas/Contact.saga.js @@ -1,16 +1,15 @@ -import { contactsActions } from '../modules/Contacts.module'; +import { contactsActions, contactsSelectors } from '../modules/Contacts.module'; import { userListActions } from '../../userlist/modules/UserList.module'; import { searchListActions } from '../../search/modules/SearchList.module'; import { credentialsSelectors } from '../../credentials/modules/Credentials.module'; import { delay } from 'redux-saga'; -import { take, call, put, select, race } from 'redux-saga/effects'; +import { take, put, select, race } from 'redux-saga/effects'; import { profileSelectors } from 'Resource/profile'; -import { generateSchema, processSend, processSearchSend } from '../api/ContactRequest.api'; -import { supplierActions } from 'Resource/supplier'; -import { p2pActions as actions } from '../../messages/personal'; import { openMessageModal, closeMessageModal } from '../../notification/modules/notification'; import NynjaCommunicator from '../../../SDK/nynjaCommunicator.sdk.js'; -import constants from '../../../configs/Constants.config'; +import { showNotification } from '../../../managers/browserNotificationsManager'; +import { settingsSelectors } from '../../settings/index'; +import * as notificationTypes from '../../../configs/BrowserNotifications.config'; function* requestContactAuthorization() { while (true) { @@ -18,7 +17,13 @@ function* requestContactAuthorization() { const rosterId = yield select(profileSelectors.getActiveRosterId); yield put(userListActions.saveNewUser({ rosterId: rosterId, payload })); yield put(contactsActions.responseContactUpdate({ rosterId: rosterId, payload: payload })); // Нет времени обьяснять))) - yield put(openMessageModal('New contact request')); + const { allOthernotifications } = yield select(settingsSelectors.getNotificationsSettings); + + if (allOthernotifications) { + const body = `${payload.names} ${payload.surnames} wants to add you on NYNJA!`; + const type = notificationTypes.OTHER_NOTIFICATIONS; + showNotification(body, type); + } } } @@ -346,18 +351,43 @@ function* handleContactUpdate() { const rosterId = yield select(profileSelectors.getActiveRosterId); - const contactsManager = facade.getContactsManager(); - // console.log('handle response', response); - if (response) { - // console.log('handle ContactUpdate', response); - yield put(userListActions.settingsUpdate({ id: rosterId, phoneId: response.payload._data.phone_id, ...response.payload._data })); - yield put(contactsActions.responseContactUpdate({ rosterId, payload: response.payload._data })); - yield put(searchListActions.updateContact({ id: rosterId, ...response.payload._data })); - // yield put(openMessageModal('Contact details updated')); + if (response) { + const { allOthernotifications } = yield select(settingsSelectors.getNotificationsSettings); + const ownPhoneId = yield select(profileSelectors.getPhoneId); + const contact = response.payload._data; + const contactId = contact.phone_id; + + let shouldNotify = false; + let isAlreadyFriend = false; + + if (allOthernotifications && ownPhoneId !== contactId) { + const friends = yield select(contactsSelectors.getFriends); + + for (const f of friends) { + if (f.phone_id === contactId) { + isAlreadyFriend = true; + break; + } + } + + if (!isAlreadyFriend) { + shouldNotify = true; + } + } + + yield put(userListActions.settingsUpdate({ id: rosterId, phoneId: contact.phone_id, ...contact })); + yield put(contactsActions.responseContactUpdate({ rosterId, payload: contact })); + yield put(searchListActions.updateContact({ id: rosterId, ...contact })); + + if (allOthernotifications && shouldNotify) { + const body = `${contact.names} ${contact.surnames} accepted your contact request! Send a message!`; + const type = notificationTypes.OTHER_NOTIFICATIONS; + const redirectLink = `/chats/${contactId}`; + showNotification(body, type, redirectLink); + } } + if (contactRequest) { - // console.log('handle contactRequest', contactRequest.payload); - // console.log('handle contactRequest phone_id', contactRequest.payload._data.phone_id); yield put(userListActions.settingsUpdate({ id: rosterId, phoneId: contactRequest.payload._data.phone_id, ...contactRequest.payload._data })); yield put(contactsActions.responseContactUpdate({ rosterId, payload: contactRequest.payload._data })); yield put(searchListActions.updateContact({ id: rosterId, ...contactRequest.payload._data })); @@ -365,16 +395,16 @@ function* handleContactUpdate() { yield delay(3000); yield put(closeMessageModal()); } + if (banned) { - // console.log('handle ContactBanned', banned); yield put(userListActions.settingsUpdate({ id: rosterId, phoneId: banned.payload._data.phone_id, ...banned.payload._data })); yield put(contactsActions.responseContactUpdate({ rosterId, payload: banned.payload._data })); yield put(openMessageModal('Contact blocked')); yield delay(3000); yield put(closeMessageModal()); } + if (ignoreContact) { - // console.log('handle ignoreContact', ignoreContact); yield put(userListActions.settingsUpdate({ id: rosterId, phoneId: ignoreContact.payload._data.phone_id, ...ignoreContact.payload._data })); yield put(contactsActions.responseContactUpdate({ rosterId, payload: ignoreContact.payload._data })); yield put(searchListActions.updateContact({ id: rosterId, ...ignoreContact.payload._data })); @@ -382,12 +412,12 @@ function* handleContactUpdate() { yield delay(3000); yield put(closeMessageModal()); } + if (searchByUsername) { - // console.log('handle searchByUsername', searchByUsername); yield put(searchListActions.update({ id: rosterId, ...searchByUsername.payload })); } + if (searchByNumber) { - // console.log('handle searchByNumber', searchByNumber); yield put(searchListActions.update({ id: rosterId, ...searchByNumber.payload })); } } diff --git a/src/core/resource/messages/groups/sagas/GroupsMessages.saga.js b/src/core/resource/messages/groups/sagas/GroupsMessages.saga.js index 1eecd99d513582e0676ccaa0de874345ed7f1c96..180a7bafdd12103ce9b35b373b9d5432643d6824 100644 --- a/src/core/resource/messages/groups/sagas/GroupsMessages.saga.js +++ b/src/core/resource/messages/groups/sagas/GroupsMessages.saga.js @@ -1,27 +1,25 @@ import { take, call, put, select, race, takeEvery } from 'redux-saga/effects'; import { delay } from 'redux-saga'; -import { - groupApi as api, - roomActions as actions, - roomsSelectors as selectors, -} from '../index'; -import { - p2pApi, -} from '../../personal/index'; +import { roomActions as actions, roomsSelectors as selectors } from '../index'; +import { p2pApi } from '../../personal/index'; import { queueActions } from '../../../app/index'; import { profileSelectors } from 'Resource/profile'; import { supplierActions } from 'Resource/supplier'; import { credentialsSelectors } from 'Resource/credentials'; import { openMessageModal } from "../../../notification/modules/notification"; -import * as filesTypes from 'Core/configs/File.config'; import { uploadActions } from '../../files'; -import NynjaCommunicator from '../../../../SDK/nynjaCommunicator.sdk.js'; import { replySelector, replyActions } from 'Resource/messages/reply/index'; -const uuidv4 = require('uuid/v4'); +import { settingsSelectors } from '../../../settings/index'; +import { roomListSelectors } from '../../../roomlist/index'; import { fileStore } from 'Core/resource/messages/files/modules/Store.module'; import { getStore } from '../../../../store/index'; -import { getVideoThumb, prepareContactData, isLink } from '../../utils/commonFunctions.js'; +import { getVideoThumb, prepareContactData, isLink, capitalize } from '../../utils/commonFunctions.js'; +import * as filesTypes from 'Core/configs/File.config'; +import * as notificationTypes from '../../../../configs/BrowserNotifications.config'; +import NynjaCommunicator from '../../../../SDK/nynjaCommunicator.sdk.js'; +import { showNotification } from '../../../../managers/browserNotificationsManager'; +const uuidv4 = require('uuid/v4'); function* send(data) { const encoded = yield call(p2pApi.processSend, data); yield put(supplierActions.send(encoded)); @@ -282,45 +280,22 @@ function* receiveGroupChatMessage() { yield put(actions.recieve({ payload: messagePayload.data })); + + const messageSender = messagePayload.data.from; + const groupId = messagePayload.data.to; + + // If message is not from the currently opened group and its not sent by current user call notification saga + if (messageSender !== phoneId + && window.location.pathname.indexOf(`/conversations/${groupId}`) === -1 + && window.location.pathname.indexOf(`/groups/${groupId}`) === -1) { + yield call(handleNotifications, messagePayload.data); + } } } } - // yield put(roomListActions.newMessage({ - // rosterId: yield select(profileSelectors.getActiveRosterId), - // payload: messagePayload.data - // })); - } } -//function * messageProcessingWatcher() { -// try { -// while (true) { -// console.log('------------'); -// const { response, request } = yield race({ -// response: take(actions.messageResponse.getType()), -// request: take(actions.send.getType()) -// }); -// -// const rosterId = yield select(profileSelectors.getActiveRosterId); -// -// if (request) { -// yield call(handleRequest, request, rosterId); -// } -// if (response) { -// const { payload: messagePayload } = yield take(facade.getEventTypes().EVENT_NEW_GROUP_CHAT_MESSAGE); -// yield put(actions.recieve({ -// payload: messagePayload -// })); -// // yield call(handleResponse, response, rosterId); -// } -// -// } -// } catch(e) { -// -// } -//} - function* messageDeleteWatcher() { while (true) { const { responseIn, responseOut } = yield race({ @@ -341,6 +316,7 @@ function* messageDeleteWatcher() { } catch (e) { } } } + function* messageDeleteResponseWatcher() { while (true) { const facade = yield select(NynjaCommunicator.getInstance); @@ -357,38 +333,6 @@ function* messageDeleteResponseWatcher() { } } -// function * cursor(activeDialogId) { -// const phoneId = yield select(profileSelectors.getPhoneId); -// const rosterId = yield select(profileSelectors.getActiveRosterId); -// const lastMessage = yield select(selectors.getLastMessageByPhoneId, activeDialogId); -// const { from, to, id } = lastMessage; -// const encoded = yield call(historyApi.historyUpdate, { from, to, phoneId, id }); -// yield put(supplierActions.send(encoded)); - -// yield take(actions.responseUpdateCounters.getType()); -// } - -// function * cursorWatcher() { -// while(true) { -// yield take(actions.uploadMessages.getType()); -// yield delay(500); -// const activeRoomId = yield select(selectors.getActiveRoomId); -// yield call(cursor, activeRoomId); - -// const rosterId = yield select(profileSelectors.getActiveRosterId); -// const dialog = yield select(selectors.getDialogByPhoneId, activeRoomId); -// //tmp -// const data = { -// ...dialog, -// unread_amount: 0 -// }; -// yield put(actions.update({ phoneId: activeRoomId, rosterId, data })); - -// const messages = yield select(selectors.getMessagesByPhoneId, activeRoomId); -// yield put(actions.updateAllMessagesStatus({ phoneId: activeRoomId, rosterId, messages }, "is_unread")) -// } -// } - function* cursor(data) { try { const { phoneId, groupId, lastReadMessageId, state } = data; @@ -420,6 +364,63 @@ function* cursorWatcher() { } } +function* handleNotifications(payload) { + const ownPhoneId = yield select(profileSelectors.getPhoneId); + const roomId = payload.to; + const msgSenderId = payload.from; + const room = yield select(roomListSelectors.getRoomById, roomId); + const { allMessageNotifications, allGroupEventsNotifications } = yield select(settingsSelectors.getNotificationsSettings); + const { settings } = yield select(roomListSelectors.getMemberById, roomId, ownPhoneId); + const msgSender = yield select(roomListSelectors.getMemberById, roomId, msgSenderId); + let notifications = settings.filter(el => el.group === 'GROUP_NOTIFICATIONS')[0]; + notifications = notifications ? JSON.parse(notifications.value) : true; + + if (allMessageNotifications && room) { + let body = ''; + let senderName = ''; + let groupName = room.name; + const { files } = payload; + + if (msgSender) { + senderName = msgSender.alias ? msgSender.alias : msgSender.names; + } + + for (const k in files) { + if (files.hasOwnProperty(k)) { + const { mime, payload, id } = files[k]; + const isSystem = id.startsWith('srv_'); + + if (isSystem && notifications && allGroupEventsNotifications) { + // body = 'SYSTEM-MESSAGE'; + break; + } else if (mime === filesTypes.TEXT && !isSystem && notifications) { + body = `${senderName}@${groupName}: ${payload}`; + break; + } else if (mime === filesTypes.IMAGE + || mime === filesTypes.VIDEO + || mime === filesTypes.FILE + || mime === filesTypes.AUDIO + || mime === filesTypes.STICKER + || mime === filesTypes.LOCATION + || mime === filesTypes.PLACE + || mime === filesTypes.CONTACT + ) { + if (notifications && !isSystem) { + body = `${senderName}@${groupName}: ${capitalize(mime)}`; + } + break; + } + } + } + + if (body.length > 0) { + const type = notificationTypes.GROUP_CHAT_MESSAGE; + const redirectLink = `/groups/${roomId}`; + showNotification(body, type, redirectLink); + } + } +} + export { //messageProcessingWatcher, sendGroupChatMessage, diff --git a/src/core/resource/messages/personal/sagas/PersonalMessages.saga.js b/src/core/resource/messages/personal/sagas/PersonalMessages.saga.js index 6349af2cc0a096dbcd0d9427f108ed8c18ccbac4..e3a2a90e88bb4fbcad2bf86c4811e2b7a982f9c3 100644 --- a/src/core/resource/messages/personal/sagas/PersonalMessages.saga.js +++ b/src/core/resource/messages/personal/sagas/PersonalMessages.saga.js @@ -6,20 +6,23 @@ import { p2pSelectors as selectors } from '../index'; import { queueActions } from '../../../app/index'; -import { userListActions } from 'Resource/userlist'; +import { userListActions, userListSelectors } from '../../../userlist/index'; import { profileSelectors } from 'Resource/profile'; import { supplierActions } from 'Resource/supplier'; import { openMessageModal } from '../../../notification/modules/notification'; import * as filesTypes from 'Core/configs/File.config'; +import * as notificationTypes from '../../../../configs/BrowserNotifications.config'; import { notificationActions } from 'Resource/notification'; import NynjaCommunicator from '../../../../SDK/nynjaCommunicator.sdk.js'; import { replySelector, replyActions } from 'Resource/messages/reply/index'; import { getStore } from '../../../../store/index'; import { fileStore } from 'Core/resource/messages/files/modules/Store.module'; -import { getVideoThumb, prepareContactData, isLink } from '../../utils/commonFunctions.js'; +import { getVideoThumb, prepareContactData, isLink, capitalize } from '../../utils/commonFunctions.js'; import saveAs from 'file-saver'; import { supplierSagas } from 'Resource/supplier'; import { credentialsSelectors, manageCredentialsActions } from 'Resource/credentials'; +import { showNotification } from '../../../../managers/browserNotificationsManager'; +import { settingsSelectors } from '../../../settings/index'; const uuidv4 = require('uuid/v4'); @@ -132,6 +135,15 @@ function* handleResponse(payload, rosterId) { if (payload.from === currentChatId && payload.from !== payload.to) { yield call(cursor, currentChatId); } + + // If message is not from the currently opened chat and its not sent by current user call notification saga + if (payload.from !== payload.to + && payload.to === userId + && window.location.pathname.indexOf(`/conversations/${payload.from}`) === -1 + && window.location.pathname.indexOf(`/chats/${payload.from}`) === -1 + ) { + yield call(handleNotifications, payload); + } } function* receiveP2PChatMessage() { @@ -486,6 +498,7 @@ function* exportToCsv({ payload }) { try { const facade = yield select(NynjaCommunicator.getInstance); const messagesManager = facade.getMessagesManager(); + filters.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; const res = yield messagesManager.getP2PChatCsv(to, filters); if (res.status === 200) { @@ -510,6 +523,47 @@ function* exportToCsvWatcher() { yield takeEvery(actions.exportP2PChatToCsv.getType(), exportToCsv); } +function* handleNotifications(payload) { + const { allMessageNotifications } = yield select(settingsSelectors.getNotificationsSettings); + const { settings, names } = yield select(userListSelectors.getUserById, payload.from); + + let currentChatNotifications = settings.filter(el => el.key === 'Notifications')[0]; + currentChatNotifications = currentChatNotifications ? JSON.parse(currentChatNotifications.value) : true; + const isNotificationsEnabled = currentChatNotifications && allMessageNotifications; + + if (isNotificationsEnabled) { + let body = ''; + const { files } = payload; + + for (const k in files) { + if (files.hasOwnProperty(k)) { + const { mime, payload, id } = files[k]; + const isSystem = id.startsWith('srv_'); + + if (mime === filesTypes.TEXT && !isSystem) { + body = `${names}: ${payload}`; + break; + } else if (mime === filesTypes.IMAGE + || mime === filesTypes.VIDEO + || mime === filesTypes.FILE + || mime === filesTypes.AUDIO + || mime === filesTypes.STICKER + || mime === filesTypes.LOCATION + || mime === filesTypes.PLACE + || mime === filesTypes.CONTACT + ) { + body = `${names}: ${capitalize(mime)}`; + break; + } + } + } + + const type = notificationTypes.P2P_CHAT_MESSAGE; + const redirectLink = `/chats/${payload.from}`; + showNotification(body, type, redirectLink); + } +} + function* messageAckWatcher() { const facade = yield select(NynjaCommunicator.getInstance); yield takeEvery(facade.getEventTypes().EVENT_ACK_RECEIVED, handleAck); diff --git a/src/core/resource/messages/utils/commonFunctions.js b/src/core/resource/messages/utils/commonFunctions.js index 0ccc8e91b2b5646bf6fdc5b25ceac011bd21252f..02f079106c697c493fc2965a0ef9d31a4cef7ec8 100644 --- a/src/core/resource/messages/utils/commonFunctions.js +++ b/src/core/resource/messages/utils/commonFunctions.js @@ -213,6 +213,20 @@ function isLink(str) { return /^(ftp|http|https|rtsp):\/\/[^ "]+$/.test(str); } +/** + * Capitalizes the first character of the given string + * + * @param {String} s + * @returns {String} + */ +const capitalize = (s) => { + if (typeof s !== 'string') { + return ''; + } + + return `${s.charAt(0).toUpperCase()}${s.slice(1)}`; +} + export { getVideoThumb, prepareContactData, @@ -221,5 +235,6 @@ export { fileTypeStyling, getUploadType, isLink, + capitalize, isMediaType, }; diff --git a/src/core/resource/profile/sagas/ProfileInit.saga.js b/src/core/resource/profile/sagas/ProfileInit.saga.js index 3c501c348b0e63adc7f16d036d9b4e36767500a9..e7b4cae7deff5e63b403f28d9b4b93e0dc70aaf3 100644 --- a/src/core/resource/profile/sagas/ProfileInit.saga.js +++ b/src/core/resource/profile/sagas/ProfileInit.saga.js @@ -3,31 +3,29 @@ import { browserHistory } from 'react-router'; import { profileInitActions as actions, profileInitSelectors as selectors, - profileSelectors } from '../index'; -import { browserStorage } from 'Core/store'; import { userListActions, userListSelectors } from '../../userlist'; import { roomListActions, roomListSelectors } from '../../roomlist'; import { contactsActions, contactsSelectors } from '../../contacts'; import { rosterInitActions, rosterInitSelectors } from '../../roster'; -import { p2pActions, p2pSelectors, p2pFilters, p2pMembersSelectors } from '../../messages/personal'; -import { historyActions } from '../../messages/history'; +import { settingsActions, settingsSelectors } from '../../settings'; +import { p2pActions, p2pSelectors } from '../../messages/personal'; import { credentialsSelectors, manageCredentialsActions as credentialsActions } from "../../credentials"; import constants from 'Core/configs/Constants.config'; -function * profileInit() { +function* profileInit() { //add works only when auth`ed, not blacklisted //change true to configurable attempts - while(true) { + while (true) { const { payload } = yield take(actions.init.getType()); const profile = selectors.pullReceivedProfile(payload); yield put(actions.initSuccess(profile)); yield put(rosterInitActions.init(rosterInitSelectors.pullReceivedRosters(payload))); yield put(contactsActions.init(contactsSelectors.pullReceivedContacts(payload))); - + const roomList = yield call(roomListSelectors.pullReceivedRoomList, payload); yield put(roomListActions.init(roomList)); const userList = yield call(userListSelectors.pullReceivedUserList, payload); @@ -35,10 +33,13 @@ function * profileInit() { yield put(p2pActions.init(p2pSelectors.pullReceivedUnread(payload))); + const settings = yield call(settingsSelectors.pullReceivedSettingsList, payload); + yield put(settingsActions.init(settings)); + const instance = yield select(credentialsSelectors.getStatus); const user = yield select(userListSelectors.getUserById, profile.phone_id); if (user && Object.keys(user).length && user.names) { - if(window.Intercom){ + if (window.Intercom) { window.Intercom('boot', { app_id: constants.intercomAppId, alignment: constants.intercomAlignment, @@ -46,22 +47,21 @@ function * profileInit() { horizontal_padding: constants.horizontal_padding, user_id: user.phone_id, phone: String(user.phone_id).split("_").pop(), - name: user.names+" "+user.surnames, - }); + name: user.names + " " + user.surnames, + }); } const status = { is_authorized: !instance.is_authorized }; yield put(credentialsActions.update(status)); const path = window.location.pathname; - if (path === "/" || path.search(/^\/auth.+/g) > -1 ) { + if (path === "/" || path.search(/^\/auth.+/g) > -1) { yield call(browserHistory.push, "/dashboard"); } - } else { yield call(browserHistory.push, "/auth/completion"); } } } -export { profileInit }; \ No newline at end of file +export { profileInit }; diff --git a/src/core/resource/roomlist/modules/RoomList.module.js b/src/core/resource/roomlist/modules/RoomList.module.js index f46560a4d40bed53ad62ddab784e51c6bb46ea69..27903fe5d49af18eb6fc1ccafc8c9ddebcdceb77 100644 --- a/src/core/resource/roomlist/modules/RoomList.module.js +++ b/src/core/resource/roomlist/modules/RoomList.module.js @@ -45,6 +45,10 @@ const getRoomById = (state, id) => { const getMemberById = (state, roomId, memberId) => { const room = getRoomById(state, roomId); + if (!room) { + return; + } + const totalMembers = { ...room.members, ...room.admins }; return totalMembers[memberId]; }; diff --git a/src/core/resource/roomlist/sagas/Rooms.saga.js b/src/core/resource/roomlist/sagas/Rooms.saga.js index 18fcf5297997b7d63a4237c57c091613dff94ad3..79923a475d2f1c3660377daee2fb24a092a128f3 100644 --- a/src/core/resource/roomlist/sagas/Rooms.saga.js +++ b/src/core/resource/roomlist/sagas/Rooms.saga.js @@ -1,7 +1,8 @@ import { take, takeEvery, call, put, select } from 'redux-saga/effects'; import { delay } from 'redux-saga'; import { profileSelectors } from 'Resource/profile'; -import { roomListActions } from '../modules/RoomList.module'; +import { settingsSelectors } from '../../settings/index'; +import { roomListActions, roomListSelectors } from '../modules/RoomList.module'; import { profileInitActions } from "../../profile"; import NynjaCommunicator from '../../../SDK/nynjaCommunicator.sdk.js'; import { queueActions } from '../../app/index'; @@ -11,6 +12,8 @@ import { getStore } from '../../../store/index'; import saveAs from 'file-saver'; import { supplierSagas } from 'Resource/supplier'; import { manageCredentialsActions } from 'Resource/credentials'; +import { showNotification } from '../../../managers/browserNotificationsManager'; +import * as notificationTypes from '../../../configs/BrowserNotifications.config'; const uuidv4 = require('uuid/v4'); @@ -28,10 +31,8 @@ function* loadGroupLink() { const groupLink = sessionStorage.getItem('groupLink'); if (groupLink) { sessionStorage.removeItem('groupLink'); - console.log('sessionStorage.groupLink: ', groupLink); yield browserHistory.push(`/group/join/${groupLink}`); } - } function* requestCreateNewGroup() { @@ -86,9 +87,59 @@ function* responseUpdateRoom() { const facade = yield select(NynjaCommunicator.getInstance); const { payload: roomPayload } = yield take(facade.getEventTypes().EVENT_UPDATE_GROUP_CHAT); const rosterId = yield select(profileSelectors.getActiveRosterId); - yield put(queueActions.queueRem(`group-avatar-update-${roomPayload.data.id}`)); - yield put(queueActions.queueRem(`group-name-update-${roomPayload.data.id}`)); - yield put(queueActions.queueRem(`group-description-update-${roomPayload.data.id}`)); + const ownPhoneId = yield select(profileSelectors.getPhoneId); + const groupId = roomPayload.data.id; + + const avatarUpdateJobKey = `group-avatar-update-${groupId}`; + const groupNameUpdateJobKey = `group-name-update-${groupId}`; + const groupDescUpdateJobKey = `group-description-update-${groupId}` + + const { allGroupEventsNotifications } = yield select(settingsSelectors.getNotificationsSettings); + const { settings } = yield select(roomListSelectors.getMemberById, groupId, ownPhoneId); + let notifications = settings.filter(el => el.group === 'GROUP_NOTIFICATIONS')[0]; + notifications = notifications ? JSON.parse(notifications.value) : true; + + // If the update is not caused by the currently logged user and he is not curretnly viewing the group + if ( + allGroupEventsNotifications + && notifications + && window.location.pathname.indexOf(`/conversations/${groupId}`) === -1 + && window.location.pathname.indexOf(`/groups/${groupId}`) === -1 + ) { + const isAvatarUpdateJob = yield call(getQueueJob, avatarUpdateJobKey); + const isGroupNameUpdateJob = yield call(getQueueJob, groupNameUpdateJobKey); + const isGroupDescUpdateJob = yield call(getQueueJob, groupDescUpdateJobKey); + + if (!isAvatarUpdateJob && !isGroupNameUpdateJob && !isGroupDescUpdateJob) { + const groupBeforeUpdate = yield select(roomListSelectors.getRoomById, groupId); + const groupAfterUpdate = roomPayload.data; + const groupName = groupBeforeUpdate.name; + const lastMsgSenderId = groupBeforeUpdate ? groupBeforeUpdate.last_msg.from : ''; + const lastMsgSender = groupBeforeUpdate ? groupBeforeUpdate.admins[lastMsgSenderId] : undefined; + const lastMsgSenderName = lastMsgSender ? lastMsgSender.alias ? lastMsgSender.alias : lastMsgSender.names : 'Admin'; + const type = notificationTypes.GROUP_EVENT_MESSAGE; + const redirectLink = `/groups/${groupId}`; + const isNameUpdate = groupBeforeUpdate.name !== groupAfterUpdate.name; + const isDescUpdate = groupBeforeUpdate.tos !== groupAfterUpdate.tos; + const isAvatarUpdate = groupAfterUpdate.data.length > 0; + + if (isNameUpdate) { + const body = `${lastMsgSenderName} edited the group's ${groupName} name`; + showNotification(body, type, redirectLink); + } else if (isDescUpdate) { + const body = `${lastMsgSenderName} edited the group's ${groupName} description`; + showNotification(body, type, redirectLink); + } else if (isAvatarUpdate) { + const body = `${lastMsgSenderName} edited the group's ${groupName} photo`; + showNotification(body, type, redirectLink); + } + } + } + + yield put(queueActions.queueRem(avatarUpdateJobKey)); + yield put(queueActions.queueRem(groupNameUpdateJobKey)); + yield put(queueActions.queueRem(groupDescUpdateJobKey)); + yield put(roomListActions.update({ id: rosterId, payload: roomPayload.data @@ -116,7 +167,6 @@ function* clearRoomChatHistoryRequest() { function* leaveAndKeepHistory() { while (true) { const request = yield take(roomListActions.leaveGroup.getType()); - const rosterId = yield select(profileSelectors.getActiveRosterId); if (request && request.payload) { // leave group yield put(queueActions.queueAdd({ @@ -133,7 +183,7 @@ function* leaveAndKeepHistory() { function* leaveAndClearHistory() { while (true) { const request = yield take(roomListActions.leaveAndClearGroup.getType()); - const rosterId = yield select(profileSelectors.getActiveRosterId); + if (request && request.payload) { // clear history // yield put(queueActions.queueAdd({ @@ -158,25 +208,63 @@ function* leaveRoomResponse() { while (true) { const facade = yield select(NynjaCommunicator.getInstance); try { - const { - payload: roomPayload - } = yield take(facade.getEventTypes().EVENT_GROUP_CHAT_LEAVE); + const { payload: roomPayload } = yield take(facade.getEventTypes().EVENT_GROUP_CHAT_LEAVE); const rosterId = yield select(profileSelectors.getActiveRosterId); - const isQueueJob = yield call(getQueueJob, `group-leave-${roomPayload.data.id}`); - yield put(queueActions.queueRem(`group-leave-${roomPayload.data.id}`)); + const ownPhoneId = yield select(profileSelectors.getPhoneId); + const groupId = roomPayload.data.id; + const groupName = roomPayload.data.name; + const leaveGroupJobKey = `group-leave-${groupId}`; + const isQueueJob = yield call(getQueueJob, leaveGroupJobKey); + const type = notificationTypes.GROUP_EVENT_MESSAGE; + const redirectLink = `groups/${groupId}`; + + const { allGroupEventsNotifications } = yield select(settingsSelectors.getNotificationsSettings); + const { settings } = yield select(roomListSelectors.getMemberById, groupId, ownPhoneId); + let notifications = settings.filter(el => el.group === 'GROUP_NOTIFICATIONS')[0]; + notifications = notifications ? JSON.parse(notifications.value) : true; + + yield put(queueActions.queueRem(leaveGroupJobKey)); + if (isQueueJob) { const globalState = getStore().getState(); const rooms = globalState.roomlist[rosterId]; const baseUrl = window.location.href.indexOf('conversations') !== -1 ? 'conversations' : 'groups'; + if (rooms && roomPayload.data) { Object.values(rooms).forEach((room) => { - if (room.id === roomPayload.data.id) { + if (room.id === groupId) { delete rooms[room.id]; } }); + yield browserHistory.push(`/${baseUrl}`); } } + + if ( + allGroupEventsNotifications + && notifications + && window.location.pathname.indexOf(`/conversations/${groupId}`) === -1 + && window.location.pathname.indexOf(`/groups/${groupId}`) === -1 + ) { + if (isQueueJob) { + const body = `You have left the group ${groupName}`; + showNotification(body, type); + } else { + let leftNames = ''; + + for (const a of roomPayload.data.admins) { + leftNames += `${a.alias ? a.alias : a.names} `; + } + + for (const m of roomPayload.data.members) { + leftNames += `${m.alias ? m.alias : m.names} `; + } + + const body = `${leftNames}left group ${groupName}`; + showNotification(body, type, redirectLink); + } + } } catch (e) { console.log('clearRoomHistroryResponse: ', e); } @@ -235,7 +323,6 @@ function* requestUpdateRoom() { * if groupName payload should have groupId, groupName * if groupDescription payload should have groupId, groupDescription */ - if (roomId && imageBlob) { // Send request to update group avata let fileUrl; @@ -293,9 +380,7 @@ function* requestRoomDetails() { while (true) { const request = yield take(roomListActions.getDetails.getType()); - const facade = yield select(NynjaCommunicator.getInstance); if (request && request.payload && request.payload.id) { - const groupChatsManager = facade.getGroupChatsManager(); try { yield put(queueActions.queueAdd({ type: 0, @@ -337,8 +422,25 @@ function* responseIncommingRoom() { const currentUserId = yield select(profileSelectors.getPhoneId); const isCreator = roomPayload.data.admins[currentUserId]; + const creator = Object.values(roomPayload.data.admins)[0]; + const type = notificationTypes.GROUP_EVENT_MESSAGE; + const groupName = roomPayload.data.name; + const groupId = roomPayload.data.id; + const { allGroupEventsNotifications } = yield select(settingsSelectors.getNotificationsSettings); + if (isCreator) { - yield browserHistory.push(`/groups/${roomPayload.data.id}`); + yield browserHistory.push(`/groups/${groupId}`); + if (allGroupEventsNotifications) { + const body = `You have created the group ${groupName}`; + showNotification(body, type); + } + } else { + if (allGroupEventsNotifications) { + const creatorName = creator ? creator.alias ? creator.alias : creator.names : ''; + const redirectLink = `/groups/${groupId}`; + const body = `${creatorName} invited you to the group ${groupName}`; + showNotification(body, type, redirectLink); + } } } } catch (error) { @@ -350,11 +452,120 @@ function* responseIncommingRoom() { function* responseIncommingRoomMember() { while (true) { const facade = yield select(NynjaCommunicator.getInstance); - const { payload: roomPayload } = yield take(facade.getEventTypes().EVENT_NEW_GROUP_CHAT_MEMBERS); - yield put(queueActions.queueRem(`room-add-members-${roomPayload.data.id}`)); - yield put(queueActions.queueRem(`room-update-member-status-${roomPayload.data.id}`)); const rosterId = yield select(profileSelectors.getActiveRosterId); + const ownPhoneId = yield select(profileSelectors.getPhoneId); + const groupId = roomPayload.data.id; + const { allGroupEventsNotifications } = yield select(settingsSelectors.getNotificationsSettings); + const { settings } = yield select(roomListSelectors.getMemberById, groupId, ownPhoneId); + let notifications = settings.filter(el => el.group === 'GROUP_NOTIFICATIONS')[0]; + notifications = notifications ? JSON.parse(notifications.value) : true; + + const addMembersJobKey = `room-add-members-${groupId}`; + const updateStatusJobKey = `room-update-member-status-${groupId}`; + + /** + * The response status from the server for adding new members/promoting members to admins/demoting admins to members + * is the same for all three actions so some magic have to be done in order to deduce for which action is the response + * MAGIC START + */ + if ( + allGroupEventsNotifications + && notifications + && window.location.pathname.indexOf(`/conversations/${groupId}`) === -1 + && window.location.pathname.indexOf(`/groups/${groupId}`) === -1 + ) { + const isAddMembersJob = yield call(getQueueJob, addMembersJobKey); + const isUpdateStatusJob = yield call(getQueueJob, updateStatusJobKey); + const type = notificationTypes.GROUP_EVENT_MESSAGE; + const redirectLink = `/groups/${groupId}`; + + if (!isAddMembersJob && !isUpdateStatusJob) { + const groupAfterUpdate = roomPayload.data; + const groupName = groupAfterUpdate.name; + const isPromotingAdmins = Object.keys(groupAfterUpdate.admins).length > 1; + + if (isPromotingAdmins) { + let isMyPromotion = false; + + for (const a in groupAfterUpdate.admins) { + if (groupAfterUpdate.admins.hasOwnProperty(a)) { + const element = groupAfterUpdate.admins[a]; + if (element.phone_id === ownPhoneId) { + isMyPromotion = true; + break; + } + } + } + + if (isMyPromotion) { + const body = `You have been promoted to an admin in group ${groupName}`; + showNotification(body, type, redirectLink); + } + } else { + const groupBeforeUpdate = yield select(roomListSelectors.getRoomById, groupId); + const oldLastMsg = groupBeforeUpdate.last_msg; + const newLastMsg = groupAfterUpdate.last_msg; + const adminAlias = groupAfterUpdate.admins['0'].alias; + let addedMembers = ''; + + let isAboutMe = false; + let isDemoting = false; + let isAdding = false; + + for (const m in groupAfterUpdate.members) { + if (groupAfterUpdate.members.hasOwnProperty(m)) { + const element = groupAfterUpdate.members[m]; + + if (element.phone_id === ownPhoneId) { + isAboutMe = true; + } + + addedMembers += `${element.alias ? element.alias : element.names} `; + } + } + + if (!groupBeforeUpdate) { + isAdding = true; + } else { + if (oldLastMsg && newLastMsg) { + const isOldSystem = oldLastMsg.msg_id.startsWith('srv_'); + const isNewSystem = newLastMsg.msg_id.startsWith('srv_'); + + if (!isNewSystem) { + isDemoting = true; + } else if (!isOldSystem) { + isAdding = true; + } else { + if (oldLastMsg.msg_id === newLastMsg.msg_id) { + isDemoting = true; + } else { + isAdding = true; + } + } + } + } + + if (isDemoting && isAboutMe) { + const body = `You have been demoted to a member in group ${groupName}`; + showNotification(body, type, redirectLink); + } + + if (isAdding) { + const body = isAboutMe + ? `${adminAlias} invited you to the group ${groupName}` + : `${adminAlias} added ${addedMembers} to the group ${groupName}`; + showNotification(body, type, redirectLink); + } + } + } + } + /** + * MAGIC END + */ + + yield put(queueActions.queueRem(addMembersJobKey)); + yield put(queueActions.queueRem(updateStatusJobKey)); if (roomPayload.data.tup === 'Room') { yield put(roomListActions.update({ @@ -499,10 +710,49 @@ function* responseJoinMemberToRoom() { function* responseRemoveMembersFromRoom() { while (true) { const facade = yield select(NynjaCommunicator.getInstance); - const { payload: roomPayload } = yield take(facade.getEventTypes().EVENT_GROUP_CHAT_MEMBERS_REMOVED); - yield put(queueActions.queueRem(`room-remove-members-${roomPayload.data.id}`)); const rosterId = yield select(profileSelectors.getActiveRosterId); + const ownPhoneId = yield select(profileSelectors.getPhoneId); + const groupId = roomPayload.data.id; + const removeMembersJobKey = `room-remove-members-${groupId}`; + const { allGroupEventsNotifications } = yield select(settingsSelectors.getNotificationsSettings); + const { settings } = yield select(roomListSelectors.getMemberById, groupId, ownPhoneId); + let notifications = settings.filter(el => el.group === 'GROUP_NOTIFICATIONS')[0]; + notifications = notifications ? JSON.parse(notifications.value) : true; + + yield put(queueActions.queueRem(removeMembersJobKey)); + + if ( + allGroupEventsNotifications + && notifications + && window.location.pathname.indexOf(`/conversations/${groupId}`) === -1 + && window.location.pathname.indexOf(`/groups/${groupId}`) === -1 + ) { + const groupName = roomPayload.data.name; + const redirectLink = `/groups/${groupName}`; + const type = notificationTypes.GROUP_EVENT_MESSAGE; + const admin = roomPayload.data.admins[0].alias ? roomPayload.data.admins[0].alias : roomPayload.data.admins[0].names; + let isAboutMe = false; + let body = ''; + let removedMembers = ''; + + for (const m of roomPayload.data.members) { + if (m.phone_id === ownPhoneId) { + isAboutMe = true; + } + + removedMembers += `${m.alias ? m.alias : m.names} `; + } + + if (isAboutMe) { + body = `${admin} removed you from the group ${groupName}`; + } else { + body = `${admin} removed ${removedMembers} from the group ${groupName}`; + } + + showNotification(body, type, redirectLink); + } + if (roomPayload.data.tup === 'Room') { yield put(roomListActions.updateRemovedMembers({ id: rosterId, @@ -588,6 +838,7 @@ function* exportToCsv({ payload }) { try { const facade = yield select(NynjaCommunicator.getInstance); const messagesManager = facade.getMessagesManager(); + filters.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; const res = yield messagesManager.getGroupChatCsv(roomId, filters); if (res.status === 200) { diff --git a/src/core/resource/settings/index.js b/src/core/resource/settings/index.js new file mode 100644 index 0000000000000000000000000000000000000000..51ad64d23dcaaa01e250891d9103c7094d267de7 --- /dev/null +++ b/src/core/resource/settings/index.js @@ -0,0 +1,9 @@ +import { settingsActions, settingsSelectors, settings } from './modules/Settings.module'; +import rootSettingsSaga from './sagas/RootSettings.saga'; + +export { + settingsActions, + settingsSelectors, + settings, + rootSettingsSaga +}; diff --git a/src/core/resource/settings/modules/Settings.module.js b/src/core/resource/settings/modules/Settings.module.js new file mode 100644 index 0000000000000000000000000000000000000000..352699082288e92db5ac5843822df7363f7b2e8b --- /dev/null +++ b/src/core/resource/settings/modules/Settings.module.js @@ -0,0 +1,120 @@ +import { createAction, createReducer } from 'redux-act'; +import merge from 'deepmerge'; + +const stateName = 'settings'; +const notifiactionsName = 'notifications'; +const actionPrefix = stateName.toUpperCase(); +const notificationsPrefix = notifiactionsName.toUpperCase(); +const initialState = { + init: false, + updated: 0, + notifications: { + allAppSound: true, + allMessageNotifications: true, + allGroupEventsNotifications: true, + allIncomingCallNotifications: true, + allOthernotifications: true, + muteUnmuteDuringCallNotifications: true, + }, +}; + +// Helpers START +const initApply = data => data; + +const getSettingsLocalStorage = () => { + let data = {}; + + try { + data = JSON.parse(localStorage.getItem(stateName)) || {} + } catch (e) { } + + return data; +}; + +const updateSettingsLocalStorage = ({ settings }) => { + try { + localStorage.setItem(stateName, JSON.stringify(settings)); + } catch (e) { } +}; +// Helpers END + +// Selectors START +const getInstance = (state) => { + const instance = state[stateName] || {}; + if (instance.hasOwnProperty('init') && !instance.init) { + throw new Error(`${stateName}, have not already init'ed`); + } + return instance; +}; + +const pullReceivedSettingsList = ({ settings = [] }) => { + // Getting initial settings from localStorage until backend implement saving in DB + const data = getSettingsLocalStorage(); + + const result = Object.assign(data, { + init: true, + updated: (new Date()).getTime(), + }); + + return result; +}; + +const getNotificationsSettings = (state) => { + const settings = getInstance(state); + return settings[notifiactionsName] || {}; +}; +// Selectors END + +// Actions START +const init = createAction(`${actionPrefix}_INIT`, initApply); +const updateNotificationsSettings = createAction(`${actionPrefix}_${notificationsPrefix}_UPDATE`, initApply); +const updateLocalStorage = createAction(`${actionPrefix}_UPDATE_LOCAL_STORAGE`, updateSettingsLocalStorage); +// Actions END + +// Handlers START +const defaultMergeHandler = (state, payload) => merge(state, payload); + +const updateNotificationsSettingsHandler = (state, payload) => { + const newState = Object.assign( + {}, + state, + merge( + state, + { + [notifiactionsName]: { + [payload.setting]: payload.value + } + } + ), + { + init: true, + updated: (new Date()).getTime() + } + ); + + return newState; +}; +// Handlers END + +const settings = createReducer({ + [init]: defaultMergeHandler, + [updateNotificationsSettings]: updateNotificationsSettingsHandler, +}, initialState); + +const settingsActions = { + init, + updateNotificationsSettings, + updateLocalStorage, +}; + +const settingsSelectors = { + getInstance, + pullReceivedSettingsList, + getNotificationsSettings, +}; + +export { + settingsActions, + settingsSelectors, + settings, +}; diff --git a/src/core/resource/settings/sagas/RootSettings.saga.js b/src/core/resource/settings/sagas/RootSettings.saga.js new file mode 100644 index 0000000000000000000000000000000000000000..d31aa758e3abfdccc3c171625dc0c6fcab50e865 --- /dev/null +++ b/src/core/resource/settings/sagas/RootSettings.saga.js @@ -0,0 +1,10 @@ +import { fork } from 'redux-saga/effects'; +import { + updateNotificationsSettingsWatcher, +} from './Settings.saga'; + +function* rootSettingsSaga() { + yield fork(updateNotificationsSettingsWatcher); +} + +export default rootSettingsSaga; diff --git a/src/core/resource/settings/sagas/Settings.saga.js b/src/core/resource/settings/sagas/Settings.saga.js new file mode 100644 index 0000000000000000000000000000000000000000..da389932b7ca8a0032f1edb65cb504544bccccc0 --- /dev/null +++ b/src/core/resource/settings/sagas/Settings.saga.js @@ -0,0 +1,18 @@ +import { takeEvery, put, select } from 'redux-saga/effects'; +import { settingsActions, settingsSelectors } from '../index'; + +function* updateNotificationsSettings({ payload }) { + const settings = yield select(settingsSelectors.getInstance); + yield put(settingsActions.updateLocalStorage({ settings })); +} + +function* updateNotificationsSettingsWatcher() { + yield takeEvery( + settingsActions.updateNotificationsSettings.getType(), + updateNotificationsSettings + ); +} + +export { + updateNotificationsSettingsWatcher, +}; diff --git a/src/layouts/Conversations/Conversations.js b/src/layouts/Conversations/Conversations.js index ebe9147033d580957783294a5efe9f921790fa0c..cbe4cc7dd62128c67c225224f67c2687e8eb51dc 100644 --- a/src/layouts/Conversations/Conversations.js +++ b/src/layouts/Conversations/Conversations.js @@ -7,7 +7,7 @@ import stylesFunc from './Conversations.styles'; import Helmet from 'react-helmet'; import { ConversationsList, ChatFeaturesMenuContainer, ChatMarketplace, - DragAndDrop, ChatMainArea, GroupsMainArea, + DragAndDrop, ChatMainArea, GroupsMainArea, GroupOptions } from '../../containers'; import { GroupCallEdit } from '../../componets'; import { profileSelectors } from 'Resource/profile'; @@ -203,6 +203,7 @@ class Conversations extends PureComponent { } /> } + {!isMarketplaceShown && !showStartGroupCall && !isBanned && } {!isMarketplaceShown && !showStartGroupCall && isGroup && } /> diff --git a/src/layouts/Notifications/Notifications.js b/src/layouts/Notifications/Notifications.js new file mode 100644 index 0000000000000000000000000000000000000000..4a1e25eeb4200234e3142e28c33a541c12bd1174 --- /dev/null +++ b/src/layouts/Notifications/Notifications.js @@ -0,0 +1,256 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import { withStyles } from 'material-ui/styles'; +import { Switch } from 'material-ui' +import stylesFunc from './Notifications.styles'; +import Helmet from 'react-helmet'; +import { SliderPanel } from '../../containers'; +import translations from '../../assets/languages/Noifications/Notifications.en'; +import LocalizeHOC from '../../containers/LocalizeHOC'; +import isEqual from 'lodash/isEqual'; +import { settingsSelectors, settingsActions } from '../../core/resource/settings/index'; + +const styles = theme => (stylesFunc(theme)); + +const mapStateToProps = (state) => { + const notifications = settingsSelectors.getNotificationsSettings(state); + + return { + notifications, + }; +}; + +const mapDispatchToProps = dispatch => ({ + actions: bindActionCreators({ + ...settingsActions + }, dispatch), +}); + +export class Notifications extends Component { + constructor(props) { + super(props); + + this.state = { + settings: { + allAppSound: true, + allMessageNotifications: true, + allGroupEventsNotifications: true, + allIncomingCallNotifications: true, + allOthernotifications: true, + muteUnmuteDuringCallNotifications: true, + }, + panelsStatuses: { + showIncomingMessageSoundsPanel: false, + showGroupEventSoundsPanel: false, + showIncomingCallSoundsPanel: false, + showOtherNotificationsSoundsPanel: false, + showMuteUnmuteDuringCallSoundPanel: false, + }, + }; + } + + componentDidMount() { + const { notifications } = this.props; + if (Object.keys(notifications).length > 0) { + this.setState({ settings: notifications }); + } + } + + componentDidUpdate(prevProps, prevState) { + const prevSettings = prevProps.notifications; + const newSettings = this.props.notifications; + const isDifferent = isEqual(prevSettings, newSettings); + + if (!isDifferent) { + this.setState({ settings: newSettings }); + } + } + + toggle(panelName) { + const newStatuses = Object.assign({}, this.state.panelsStatuses); + + for (let panel in newStatuses) { + if (newStatuses.hasOwnProperty(panel)) { + if (panel === panelName) { + newStatuses[panel] = !newStatuses[panel]; + } else { + newStatuses[panel] = false; + } + } + } + + this.setState({ panelsStatuses: newStatuses }); + } + + handleNotificationsChange = name => (event) => { + const { actions } = this.props; + + actions.updateNotificationsSettings({ + setting: name, + value: event.target.checked + }); + } + + render() { + const { classes, translate } = this.props; + const { settings, panelsStatuses } = this.state; + const { allAppSound, allMessageNotifications, allGroupEventsNotifications, allIncomingCallNotifications, + allOthernotifications, muteUnmuteDuringCallNotifications + } = settings; + const { showIncomingMessageSoundsPanel, showGroupEventSoundsPanel, showIncomingCallSoundsPanel, + showOtherNotificationsSoundsPanel, showMuteUnmuteDuringCallSoundPanel + } = panelsStatuses; + + return ( +
+ +
+ {/*
+
+ {translate('appSounds')} + +
+
*/} + {/* Message notifications */} +
+

{translate('messageNotifications')}

+
+
+ {translate('showNotifications')} + +
+ {/*
+ +
*/} + {/* Group events */} +
+

{translate('groupEvents')}

+
+
+ {translate('showNotifications')} + +
+ {/*
+ +
*/} + {/* Call Notifications */} +
+

{translate('callNotifications')}

+
+
+ +
+
+ {translate('incomingCallSoundInMutedChat')} + +
+ {/*
+ +
*/} + {/* Other Notifications */} +
+

{translate('otherNotifications')}

+
+
+ {translate('showNotifications')} + +
+ {/*
+ +
*/} +
+ + {/* Notifications Panel */} + +
+ {translate('notifications')} + +
+
+ {/* Sound Panel */} + {/* +
+ {translate('notifications')} +
+
*/} +
+ ); + } +} + +Notifications.propTypes = { + classes: PropTypes.object.isRequired, +}; + +const localized = LocalizeHOC({ translations })(Notifications); +export default connect( + mapStateToProps, + mapDispatchToProps +)(withStyles(styles)(localized)); diff --git a/src/layouts/Notifications/Notifications.styles.js b/src/layouts/Notifications/Notifications.styles.js new file mode 100644 index 0000000000000000000000000000000000000000..421e6cbd0eef9d46d036397d797537f0e5156f3c --- /dev/null +++ b/src/layouts/Notifications/Notifications.styles.js @@ -0,0 +1,118 @@ +export default theme => ({ + wrap: { + paddingLeft: 285, + transition: 'padding-left 0.2s ease-out', + '& [class*="bottom-panel-wrap"]': { + paddingLeft: 285 + }, + }, + notificationWrap: { + position: 'fixed', + top: 64, + left: 205, + zIndex: 9, + width: 572, + height: 'calc(100% - 64px)', + background: '#27292e', + transition: 'width 0.2s ease-out' + }, + settingsItemWrap: { + marginTop: '20px', + borderBottom: '1px solid #34373c', + borderTop: '1px solid #34373c' + }, + toogleItem: { + width: '100%', + color: '#ffffff', + height: 36, + display: 'flex', + padding: '2px 10px 1px', + position: 'relative', + alignItems: 'center', + lineHeight: 1.2, + flexDirection: 'row', + justifyContent: 'space-between', + + }, + toggleLabel: { + overflowWrap: 'break-word', + marginLeft: 25, + fontSize: '16px' + }, + switchChecked: { + color: '#C90010 !important' + }, + separator: { + width: '100%', + color: '#919294', + height: 36, + display: 'flex', + padding: '9px 35px', + position: 'relative', + lineHeight: 1.2, + flexDirection: 'row', + justifyContent: 'space-between', + background: "#1b1c20", + marginTop: '20px', + }, + buttonContainer: { + '& button': { + textTransform: 'capitalize', + fontSize: '1em', + flexDirection: 'column', + alignItems: 'flex-start', + width: '100%', + color: '#FFFFFF', + padding: 10, + paddingLeft: 35, + textAlign: 'left', + cursor: 'pointer', + transition: 'background 0.2s ease-out', + '&:hover': { + background: '#151619', + '&:after': { + borderColor: '#d00000' + } + }, + '&:disabled': { + color: 'graytext' + } + } + }, + iconWrapp: { + position: 'realative', + borderTop: '1px solid #34373c', + borderBottom: '1px solid #34373c', + '& i.icon-expand': { + transform: 'rotate(-90deg)', + position: 'absolute', + right: '20px', + lineHeight: '18px', + color: '#34373c', + '&:before': { + fontSize: 8 + } + }, + '& .members-number': { + position: 'absolute', + lineHeight: '18px', + right: '40px', + color: '#4c4e52', + fontSize: 14 + } + }, + notificationsPanel: { + left: 777, + zIndex: 7, + background: '#151619', + top: 66, + paddingTop: 20 + }, + soundsPanel: { + left: 777, + zIndex: 7, + background: '#151619', + top: 66, + paddingTop: 20 + }, +}); diff --git a/src/layouts/index.js b/src/layouts/index.js index c206864975d0cf81e88302226bf0912999ce75bf..f98d288dd02e9b491b33777b0178cff22366acd3 100644 --- a/src/layouts/index.js +++ b/src/layouts/index.js @@ -6,10 +6,9 @@ import Groups from './Groups/Groups'; import CallHistory from './CallHistory/CallHistory'; import CreateGroup from './CreateGroup/CreateGroup'; import Contacts from './Contacts/Contacts'; -import { AuthInit } from './Auth'; -import { AuthVerify } from './Auth'; -import { AuthCompletion } from './Auth'; +import { AuthInit, AuthVerify, AuthCompletion } from './Auth'; import Profile from './Profile/Profile'; +import Notifications from './Notifications/Notifications' export { Home, @@ -24,4 +23,5 @@ export { CreateGroup, Contacts, Profile, + Notifications }; diff --git a/src/pages/NinjaApp/NinjaApp.js b/src/pages/NinjaApp/NinjaApp.js index 515e53289f537723cc5b4f51ad8880798441f612..ac106a20b383f565c8a76950bc8e3da9e9cc4b19 100644 --- a/src/pages/NinjaApp/NinjaApp.js +++ b/src/pages/NinjaApp/NinjaApp.js @@ -2,7 +2,6 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { MenuAppBar, ActionMenu } from '../../containers'; -import { Grid } from 'material-ui'; import { withStyles } from 'material-ui/styles'; import stylesFunc from './NinjaApp.styles'; import darkBgImg from '../../assets/img/bg/dark-mountains.jpg'; @@ -10,6 +9,7 @@ import { profileSelectors } from 'Resource/profile'; import { authActions } from 'Resource/credentials'; import { bindActionCreators } from 'redux'; import { AudioVideo } from '../../componets'; +import { requestNotificationPermission } from '../../core/managers/browserNotificationsManager'; const styles = theme => (stylesFunc(theme, darkBgImg)); @@ -26,7 +26,9 @@ export class Dashboard extends Component { classes: PropTypes.object.isRequired }; - componentWillMount() {} + componentDidMount() { + requestNotificationPermission(); + } renderChildren(globalProps, childProps) { return React.cloneElement(globalProps, childProps); @@ -46,9 +48,8 @@ export class Dashboard extends Component { /> - { this.renderChildren(children, { personalData }) } + {this.renderChildren(children, { personalData })}
- ); } } diff --git a/src/routes.js b/src/routes.js index 694f0194bdd6fa7b805eb8afef6125a44713261a..32a544368b73263af5764e50fd13871a497471e6 100644 --- a/src/routes.js +++ b/src/routes.js @@ -14,7 +14,8 @@ import { Profile, Groups, CallHistory, - CreateGroup + CreateGroup, + Notifications } from './layouts'; import RootMarketplace from './containers/Marketplace/RootMarketplace'; @@ -57,6 +58,7 @@ export default (dispatch) => + {/* */}