Feature: Add web push notification permission in frontend (#766)
Add webpush notification permission in frontend Co-authored-by: Sojan <sojan@pepalo.com>
This commit is contained in:
parent
5bd7a4c511
commit
e9131ea558
37 changed files with 651 additions and 318 deletions
|
@ -71,14 +71,14 @@ SENTRY_DSN=
|
||||||
# Disable if you want to write logs to a file
|
# Disable if you want to write logs to a file
|
||||||
RAILS_LOG_TO_STDOUT=true
|
RAILS_LOG_TO_STDOUT=true
|
||||||
LOG_LEVEL=info
|
LOG_LEVEL=info
|
||||||
LOG_SIZE=500
|
LOG_SIZE=500
|
||||||
|
|
||||||
# Credentials to access sidekiq dashboard in production
|
# Credentials to access sidekiq dashboard in production
|
||||||
SIDEKIQ_AUTH_USERNAME=
|
SIDEKIQ_AUTH_USERNAME=
|
||||||
SIDEKIQ_AUTH_PASSWORD=
|
SIDEKIQ_AUTH_PASSWORD=
|
||||||
|
|
||||||
### This environment variables are only required if you are setting up social media channels
|
### This environment variables are only required if you are setting up social media channels
|
||||||
#facebook
|
#facebook
|
||||||
FB_VERIFY_TOKEN=
|
FB_VERIFY_TOKEN=
|
||||||
FB_APP_SECRET=
|
FB_APP_SECRET=
|
||||||
FB_APP_ID=
|
FB_APP_ID=
|
||||||
|
@ -101,3 +101,8 @@ CHARGEBEE_API_KEY=
|
||||||
CHARGEBEE_SITE=
|
CHARGEBEE_SITE=
|
||||||
CHARGEBEE_WEBHOOK_USERNAME=
|
CHARGEBEE_WEBHOOK_USERNAME=
|
||||||
CHARGEBEE_WEBHOOK_PASSWORD=
|
CHARGEBEE_WEBHOOK_PASSWORD=
|
||||||
|
|
||||||
|
## Push Notification
|
||||||
|
## generate a new key value here : https://d3v.one/vapid-key-generator/
|
||||||
|
# VAPID_PUBLIC_KEY=
|
||||||
|
# VAPID_PRIVATE_KEY=
|
||||||
|
|
|
@ -13,7 +13,7 @@ Layout/LineLength:
|
||||||
Metrics/ClassLength:
|
Metrics/ClassLength:
|
||||||
Max: 125
|
Max: 125
|
||||||
RSpec/ExampleLength:
|
RSpec/ExampleLength:
|
||||||
Max: 15
|
Max: 25
|
||||||
Style/Documentation:
|
Style/Documentation:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
Style/FrozenStringLiteralComment:
|
Style/FrozenStringLiteralComment:
|
||||||
|
|
3
Gemfile
3
Gemfile
|
@ -80,6 +80,9 @@ gem 'sidekiq'
|
||||||
##-- used for single column multiple binary flags in notification settings/feature flagging --##
|
##-- used for single column multiple binary flags in notification settings/feature flagging --##
|
||||||
gem 'flag_shih_tzu'
|
gem 'flag_shih_tzu'
|
||||||
|
|
||||||
|
##-- Push notification service --##
|
||||||
|
gem 'webpush'
|
||||||
|
|
||||||
group :development do
|
group :development do
|
||||||
gem 'annotate'
|
gem 'annotate'
|
||||||
gem 'bullet'
|
gem 'bullet'
|
||||||
|
|
|
@ -219,6 +219,7 @@ GEM
|
||||||
haikunator (1.1.0)
|
haikunator (1.1.0)
|
||||||
hana (1.3.5)
|
hana (1.3.5)
|
||||||
hashie (4.1.0)
|
hashie (4.1.0)
|
||||||
|
hkdf (0.3.0)
|
||||||
http-accept (1.7.0)
|
http-accept (1.7.0)
|
||||||
http-cookie (1.0.3)
|
http-cookie (1.0.3)
|
||||||
domain_name (~> 0.5)
|
domain_name (~> 0.5)
|
||||||
|
@ -489,6 +490,9 @@ GEM
|
||||||
rack-proxy (>= 0.6.1)
|
rack-proxy (>= 0.6.1)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
semantic_range (>= 2.3.0)
|
semantic_range (>= 2.3.0)
|
||||||
|
webpush (1.0.0)
|
||||||
|
hkdf (~> 0.2)
|
||||||
|
jwt (~> 2.0)
|
||||||
websocket-driver (0.7.1)
|
websocket-driver (0.7.1)
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
websocket-extensions (0.1.4)
|
websocket-extensions (0.1.4)
|
||||||
|
@ -532,7 +536,7 @@ DEPENDENCIES
|
||||||
letter_opener
|
letter_opener
|
||||||
listen
|
listen
|
||||||
mini_magick
|
mini_magick
|
||||||
mock_redis
|
mock_redis!
|
||||||
pg
|
pg
|
||||||
pry-rails
|
pry-rails
|
||||||
puma
|
puma
|
||||||
|
@ -568,6 +572,7 @@ DEPENDENCIES
|
||||||
valid_email2
|
valid_email2
|
||||||
web-console
|
web-console
|
||||||
webpacker
|
webpacker
|
||||||
|
webpush
|
||||||
wisper (= 2.0.0)
|
wisper (= 2.0.0)
|
||||||
|
|
||||||
RUBY VERSION
|
RUBY VERSION
|
||||||
|
|
28
app/builders/notification_subscription_builder.rb
Normal file
28
app/builders/notification_subscription_builder.rb
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
class NotificationSubscriptionBuilder
|
||||||
|
pattr_initialize [:params, :user!]
|
||||||
|
|
||||||
|
def perform
|
||||||
|
# if multiple accounts were used to login in same browser
|
||||||
|
move_subscription_to_user if identifier_subscription && identifier_subscription.user_id != user.id
|
||||||
|
build_identifier_subscription if identifier_subscription.blank?
|
||||||
|
identifier_subscription
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def identifier
|
||||||
|
@identifier ||= params[:subscription_attributes][:endpoint] if params[:subscription_type] == 'browser_push'
|
||||||
|
end
|
||||||
|
|
||||||
|
def identifier_subscription
|
||||||
|
@identifier_subscription ||= NotificationSubscription.find_by(identifier: identifier)
|
||||||
|
end
|
||||||
|
|
||||||
|
def move_subscription_to_user
|
||||||
|
@identifier_subscription.update(user_id: user.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_identifier_subscription
|
||||||
|
user.notification_subscriptions.create(params.merge(identifier: identifier))
|
||||||
|
end
|
||||||
|
end
|
|
@ -20,10 +20,11 @@ class Api::V1::Accounts::NotificationSettingsController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def notification_setting_params
|
def notification_setting_params
|
||||||
params.require(:notification_settings).permit(selected_email_flags: [])
|
params.require(:notification_settings).permit(selected_email_flags: [], selected_push_flags: [])
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_flags
|
def update_flags
|
||||||
@notification_setting.selected_email_flags = notification_setting_params[:selected_email_flags]
|
@notification_setting.selected_email_flags = notification_setting_params[:selected_email_flags]
|
||||||
|
@notification_setting.selected_push_flags = notification_setting_params[:selected_push_flags]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,7 +2,8 @@ class Api::V1::NotificationSubscriptionsController < Api::BaseController
|
||||||
before_action :set_user
|
before_action :set_user
|
||||||
|
|
||||||
def create
|
def create
|
||||||
notification_subscription = @user.notification_subscriptions.create(notification_subscription_params)
|
notification_subscription = NotificationSubscriptionBuilder.new(user: @user, params: notification_subscription_params).perform
|
||||||
|
|
||||||
render json: notification_subscription
|
render json: notification_subscription
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||||
before_action :set_contact
|
before_action :set_contact
|
||||||
|
|
||||||
def toggle_typing
|
def toggle_typing
|
||||||
head :ok if conversation.nil?
|
head :ok && return if conversation.nil?
|
||||||
|
|
||||||
if permitted_params[:typing_status] == 'on'
|
if permitted_params[:typing_status] == 'on'
|
||||||
trigger_typing_event(CONVERSATION_TYPING_ON)
|
trigger_typing_event(CONVERSATION_TYPING_ON)
|
||||||
|
|
9
app/javascript/dashboard/api/notificationSubscription.js
Normal file
9
app/javascript/dashboard/api/notificationSubscription.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import ApiClient from './ApiClient';
|
||||||
|
|
||||||
|
class NotificationSubscriptions extends ApiClient {
|
||||||
|
constructor() {
|
||||||
|
super('notification_subscriptions');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new NotificationSubscriptions();
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
:type="type"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:class="computedClass"
|
:class="computedClass"
|
||||||
@click="onClick"
|
@click="onClick"
|
||||||
|
@ -39,6 +39,10 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: 'submit',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
computedClass() {
|
computedClass() {
|
||||||
|
|
93
app/javascript/dashboard/helper/pushHelper.js
Normal file
93
app/javascript/dashboard/helper/pushHelper.js
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
/* eslint-disable no-console */
|
||||||
|
import NotificationSubscriptions from '../api/notificationSubscription';
|
||||||
|
import auth from '../api/auth';
|
||||||
|
|
||||||
|
export const verifyServiceWorkerExistence = (callback = () => {}) => {
|
||||||
|
if (!('serviceWorker' in navigator)) {
|
||||||
|
// Service Worker isn't supported on this browser, disable or hide UI.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!('PushManager' in window)) {
|
||||||
|
// Push isn't supported on this browser, disable or hide UI.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigator.serviceWorker
|
||||||
|
.register('/sw.js')
|
||||||
|
.then(registration => callback(registration))
|
||||||
|
.catch(registrationError => {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
console.log('SW registration failed: ', registrationError);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hasPushPermissions = () => {
|
||||||
|
if ('Notification' in window) {
|
||||||
|
return Notification.permission === 'granted';
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateKeys = str =>
|
||||||
|
btoa(String.fromCharCode.apply(null, new Uint8Array(str)))
|
||||||
|
.replace(/\+/g, '-')
|
||||||
|
.replace(/\//g, '_');
|
||||||
|
|
||||||
|
export const getPushSubscriptionPayload = subscription => ({
|
||||||
|
subscription_type: 'browser_push',
|
||||||
|
subscription_attributes: {
|
||||||
|
endpoint: subscription.endpoint,
|
||||||
|
p256dh: generateKeys(subscription.getKey('p256dh')),
|
||||||
|
auth: generateKeys(subscription.getKey('auth')),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const sendRegistrationToServer = subscription => {
|
||||||
|
if (auth.isLoggedIn()) {
|
||||||
|
return NotificationSubscriptions.create(
|
||||||
|
getPushSubscriptionPayload(subscription)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const registerSubscription = (onSuccess = () => {}) => {
|
||||||
|
if (!window.chatwootConfig.vapidPublicKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigator.serviceWorker.ready
|
||||||
|
.then(serviceWorkerRegistration =>
|
||||||
|
serviceWorkerRegistration.pushManager.subscribe({
|
||||||
|
userVisibleOnly: true,
|
||||||
|
applicationServerKey: window.chatwootConfig.vapidPublicKey,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.then(sendRegistrationToServer)
|
||||||
|
.then(() => {
|
||||||
|
onSuccess();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
window.bus.$emit(
|
||||||
|
'newToastMessage',
|
||||||
|
'This browser does not support desktop notification'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const requestPushPermissions = ({ onSuccess }) => {
|
||||||
|
if (!('Notification' in window)) {
|
||||||
|
window.bus.$emit(
|
||||||
|
'newToastMessage',
|
||||||
|
'This browser does not support desktop notification'
|
||||||
|
);
|
||||||
|
} else if (Notification.permission === 'granted') {
|
||||||
|
registerSubscription(onSuccess);
|
||||||
|
} else if (Notification.permission !== 'denied') {
|
||||||
|
Notification.requestPermission(permission => {
|
||||||
|
if (permission === 'granted') {
|
||||||
|
registerSubscription(onSuccess);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
|
@ -10,11 +10,11 @@
|
||||||
"REMOVE_IMAGE": "Remove",
|
"REMOVE_IMAGE": "Remove",
|
||||||
"UPLOAD_IMAGE": "Upload image",
|
"UPLOAD_IMAGE": "Upload image",
|
||||||
"UPDATE_IMAGE": "Update image",
|
"UPDATE_IMAGE": "Update image",
|
||||||
"PROFILE_SECTION" : {
|
"PROFILE_SECTION": {
|
||||||
"TITLE": "Profile",
|
"TITLE": "Profile",
|
||||||
"NOTE": "Your email address is your identity and is used to log in."
|
"NOTE": "Your email address is your identity and is used to log in."
|
||||||
},
|
},
|
||||||
"PASSWORD_SECTION" : {
|
"PASSWORD_SECTION": {
|
||||||
"TITLE": "Password",
|
"TITLE": "Password",
|
||||||
"NOTE": "Updating your password would reset your logins in multiple devices."
|
"NOTE": "Updating your password would reset your logins in multiple devices."
|
||||||
},
|
},
|
||||||
|
@ -22,15 +22,25 @@
|
||||||
"TITLE": "Access Token",
|
"TITLE": "Access Token",
|
||||||
"NOTE": "This token can be used if you are building an API based integration"
|
"NOTE": "This token can be used if you are building an API based integration"
|
||||||
},
|
},
|
||||||
"EMAIL_NOTIFICATIONS_SECTION" : {
|
"EMAIL_NOTIFICATIONS_SECTION": {
|
||||||
"TITLE": "Email Notifications",
|
"TITLE": "Email Notifications",
|
||||||
"NOTE": "Update your email notification preferences here",
|
"NOTE": "Update your email notification preferences here",
|
||||||
"CONVERSATION_ASSIGNMENT": "Send email notifications when a conversation is assigned to me",
|
"CONVERSATION_ASSIGNMENT": "Send email notifications when a conversation is assigned to me",
|
||||||
"CONVERSATION_CREATION": "Send email notifications when a new conversation is created",
|
"CONVERSATION_CREATION": "Send email notifications when a new conversation is created"
|
||||||
"UPDATE_SUCCESS": "Your email notification preferences are updated successfully",
|
},
|
||||||
|
"API": {
|
||||||
|
"UPDATE_SUCCESS": "Your notification preferences are updated successfully",
|
||||||
"UPDATE_ERROR": "There is an error while updating the preferences, please try again"
|
"UPDATE_ERROR": "There is an error while updating the preferences, please try again"
|
||||||
},
|
},
|
||||||
"PROFILE_IMAGE":{
|
"PUSH_NOTIFICATIONS_SECTION": {
|
||||||
|
"TITLE": "Push Notifications",
|
||||||
|
"NOTE": "Update your push notification preferences here",
|
||||||
|
"CONVERSATION_ASSIGNMENT": "Send push notifications when a conversation is assigned to me",
|
||||||
|
"CONVERSATION_CREATION": "Send push notifications when a new conversation is created",
|
||||||
|
"HAS_ENABLED_PUSH": "You have enabled push for this browser.",
|
||||||
|
"REQUEST_PUSH": "Enable push notifications"
|
||||||
|
},
|
||||||
|
"PROFILE_IMAGE": {
|
||||||
"LABEL": "Profile Image"
|
"LABEL": "Profile Image"
|
||||||
},
|
},
|
||||||
"NAME": {
|
"NAME": {
|
||||||
|
|
|
@ -1,114 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="profile--settings--row row">
|
|
||||||
<div class="columns small-3 ">
|
|
||||||
<h4 class="block-title">
|
|
||||||
{{ $t('PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.TITLE') }}
|
|
||||||
</h4>
|
|
||||||
<p>{{ $t('PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.NOTE') }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="columns small-9">
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
v-model="selectedNotifications"
|
|
||||||
class="email-notification--checkbox"
|
|
||||||
type="checkbox"
|
|
||||||
value="email_conversation_creation"
|
|
||||||
@input="handleInput"
|
|
||||||
/>
|
|
||||||
<label for="conversation_creation">
|
|
||||||
{{
|
|
||||||
$t(
|
|
||||||
'PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.CONVERSATION_CREATION'
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
v-model="selectedNotifications"
|
|
||||||
class="email-notification--checkbox"
|
|
||||||
type="checkbox"
|
|
||||||
value="email_conversation_assignment"
|
|
||||||
@input="handleInput"
|
|
||||||
/>
|
|
||||||
<label for="conversation_assignment">
|
|
||||||
{{
|
|
||||||
$t(
|
|
||||||
'PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.CONVERSATION_ASSIGNMENT'
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
/* global bus */
|
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
selectedNotifications: [],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapGetters({
|
|
||||||
selectedEmailFlags: 'userNotificationSettings/getSelectedEmailFlags',
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
selectedEmailFlags(value) {
|
|
||||||
this.selectedNotifications = value;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.$store.dispatch('userNotificationSettings/get');
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async handleInput(e) {
|
|
||||||
const selectedValue = e.target.value;
|
|
||||||
if (this.selectedEmailFlags.includes(e.target.value)) {
|
|
||||||
const selectedEmailFlags = this.selectedEmailFlags.filter(
|
|
||||||
flag => flag !== selectedValue
|
|
||||||
);
|
|
||||||
this.selectedNotifications = selectedEmailFlags;
|
|
||||||
} else {
|
|
||||||
this.selectedNotifications = [
|
|
||||||
...this.selectedEmailFlags,
|
|
||||||
selectedValue,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
this.$store.dispatch(
|
|
||||||
'userNotificationSettings/update',
|
|
||||||
this.selectedNotifications
|
|
||||||
);
|
|
||||||
bus.$emit(
|
|
||||||
'newToastMessage',
|
|
||||||
this.$t(
|
|
||||||
'PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.UPDATE_SUCCESS'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
bus.$emit(
|
|
||||||
'newToastMessage',
|
|
||||||
this.$t(
|
|
||||||
'PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.UPDATE_ERROR'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import '~dashboard/assets/scss/variables.scss';
|
|
||||||
|
|
||||||
.email-notification--checkbox {
|
|
||||||
font-size: $font-size-large;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -82,7 +82,7 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<email-notifications />
|
<notification-settings />
|
||||||
<div class="profile--settings--row row">
|
<div class="profile--settings--row row">
|
||||||
<div class="columns small-3 ">
|
<div class="columns small-3 ">
|
||||||
<h4 class="block-title">
|
<h4 class="block-title">
|
||||||
|
@ -111,11 +111,11 @@ import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||||
import { required, minLength, email } from 'vuelidate/lib/validators';
|
import { required, minLength, email } from 'vuelidate/lib/validators';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { clearCookiesOnLogout } from '../../../../store/utils/api';
|
import { clearCookiesOnLogout } from '../../../../store/utils/api';
|
||||||
import EmailNotifications from './EmailNotifications';
|
import NotificationSettings from './NotificationSettings';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
EmailNotifications,
|
NotificationSettings,
|
||||||
Thumbnail,
|
Thumbnail,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
|
|
@ -0,0 +1,226 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="profile--settings--row row">
|
||||||
|
<div class="columns small-3 ">
|
||||||
|
<h4 class="block-title">
|
||||||
|
{{ $t('PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.TITLE') }}
|
||||||
|
</h4>
|
||||||
|
<p>
|
||||||
|
{{ $t('PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.NOTE') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="columns small-9">
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
v-model="selectedEmailFlags"
|
||||||
|
class="notification--checkbox"
|
||||||
|
type="checkbox"
|
||||||
|
value="email_conversation_creation"
|
||||||
|
@input="handleEmailInput"
|
||||||
|
/>
|
||||||
|
<label for="conversation_creation">
|
||||||
|
{{
|
||||||
|
$t(
|
||||||
|
'PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.CONVERSATION_CREATION'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
v-model="selectedEmailFlags"
|
||||||
|
class="notification--checkbox"
|
||||||
|
type="checkbox"
|
||||||
|
value="email_conversation_assignment"
|
||||||
|
@input="handleEmailInput"
|
||||||
|
/>
|
||||||
|
<label for="conversation_assignment">
|
||||||
|
{{
|
||||||
|
$t(
|
||||||
|
'PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.CONVERSATION_ASSIGNMENT'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="vapidPublicKey" class="profile--settings--row row push-row">
|
||||||
|
<div class="columns small-3 ">
|
||||||
|
<h4 class="block-title">
|
||||||
|
{{ $t('PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.TITLE') }}
|
||||||
|
</h4>
|
||||||
|
<p>{{ $t('PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.NOTE') }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="columns small-9">
|
||||||
|
<p v-if="hasEnabledPushPermissions">
|
||||||
|
{{
|
||||||
|
$t(
|
||||||
|
'PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.HAS_ENABLED_PUSH'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
<div v-else>
|
||||||
|
<woot-submit-button
|
||||||
|
:button-text="
|
||||||
|
$t(
|
||||||
|
'PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.REQUEST_PUSH'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
class="button nice small"
|
||||||
|
type="button"
|
||||||
|
@click="onRequestPermissions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
v-model="selectedPushFlags"
|
||||||
|
class="notification--checkbox"
|
||||||
|
type="checkbox"
|
||||||
|
value="push_conversation_creation"
|
||||||
|
@input="handlePushInput"
|
||||||
|
/>
|
||||||
|
<label for="conversation_creation">
|
||||||
|
{{
|
||||||
|
$t(
|
||||||
|
'PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.CONVERSATION_CREATION'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
v-model="selectedPushFlags"
|
||||||
|
class="notification--checkbox"
|
||||||
|
type="checkbox"
|
||||||
|
value="push_conversation_assignment"
|
||||||
|
@input="handlePushInput"
|
||||||
|
/>
|
||||||
|
<label for="conversation_assignment">
|
||||||
|
{{
|
||||||
|
$t(
|
||||||
|
'PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.CONVERSATION_ASSIGNMENT'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
import configMixin from 'shared/mixins/configMixin';
|
||||||
|
import {
|
||||||
|
hasPushPermissions,
|
||||||
|
requestPushPermissions,
|
||||||
|
verifyServiceWorkerExistence,
|
||||||
|
} from '../../../../helper/pushHelper';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [alertMixin, configMixin],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selectedEmailFlags: [],
|
||||||
|
selectedPushFlags: [],
|
||||||
|
hasEnabledPushPermissions: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
emailFlags: 'userNotificationSettings/getSelectedEmailFlags',
|
||||||
|
pushFlags: 'userNotificationSettings/getSelectedPushFlags',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
emailFlags(value) {
|
||||||
|
this.selectedEmailFlags = value;
|
||||||
|
},
|
||||||
|
pushFlags(value) {
|
||||||
|
this.selectedPushFlags = value;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (hasPushPermissions()) {
|
||||||
|
this.getPushSubscription();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$store.dispatch('userNotificationSettings/get');
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onRegistrationSuccess() {
|
||||||
|
this.hasEnabledPushPermissions = true;
|
||||||
|
},
|
||||||
|
onRequestPermissions() {
|
||||||
|
requestPushPermissions({
|
||||||
|
onSuccess: this.onRegistrationSuccess,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getPushSubscription() {
|
||||||
|
verifyServiceWorkerExistence(registration =>
|
||||||
|
registration.pushManager
|
||||||
|
.getSubscription()
|
||||||
|
.then(subscription => {
|
||||||
|
console.log(subscription);
|
||||||
|
if (!subscription) {
|
||||||
|
this.hasEnabledPushPermissions = false;
|
||||||
|
} else {
|
||||||
|
this.hasEnabledPushPermissions = true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => console.log(error))
|
||||||
|
);
|
||||||
|
},
|
||||||
|
async updateNotificationSettings() {
|
||||||
|
try {
|
||||||
|
this.$store.dispatch('userNotificationSettings/update', {
|
||||||
|
selectedEmailFlags: this.selectedEmailFlags,
|
||||||
|
selectedPushFlags: this.selectedPushFlags,
|
||||||
|
});
|
||||||
|
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_SUCCESS'));
|
||||||
|
} catch (error) {
|
||||||
|
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_ERROR'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleEmailInput(e) {
|
||||||
|
this.selectedEmailFlags = this.toggleInput(
|
||||||
|
this.selectedEmailFlags,
|
||||||
|
e.target.value
|
||||||
|
);
|
||||||
|
|
||||||
|
this.updateNotificationSettings();
|
||||||
|
},
|
||||||
|
handlePushInput(e) {
|
||||||
|
this.selectedPushFlags = this.toggleInput(
|
||||||
|
this.selectedPushFlags,
|
||||||
|
e.target.value
|
||||||
|
);
|
||||||
|
|
||||||
|
this.updateNotificationSettings();
|
||||||
|
},
|
||||||
|
toggleInput(selected, current) {
|
||||||
|
if (selected.includes(current)) {
|
||||||
|
const newSelectedFlags = selected.filter(flag => flag !== current);
|
||||||
|
return newSelectedFlags;
|
||||||
|
}
|
||||||
|
return [...selected, current];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '~dashboard/assets/scss/variables.scss';
|
||||||
|
|
||||||
|
.notification--checkbox {
|
||||||
|
font-size: $font-size-large;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide on Safari
|
||||||
|
.push-row:not(:root:root) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -17,6 +17,9 @@ export const getters = {
|
||||||
getSelectedEmailFlags: $state => {
|
getSelectedEmailFlags: $state => {
|
||||||
return $state.record.selected_email_flags;
|
return $state.record.selected_email_flags;
|
||||||
},
|
},
|
||||||
|
getSelectedPushFlags: $state => {
|
||||||
|
return $state.record.selected_push_flags;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
|
@ -35,12 +38,13 @@ export const actions = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
update: async ({ commit }, params) => {
|
update: async ({ commit }, { selectedEmailFlags, selectedPushFlags }) => {
|
||||||
commit(types.default.SET_USER_NOTIFICATION_UI_FLAG, { isUpdating: true });
|
commit(types.default.SET_USER_NOTIFICATION_UI_FLAG, { isUpdating: true });
|
||||||
try {
|
try {
|
||||||
const response = await UserNotificationSettings.update({
|
const response = await UserNotificationSettings.update({
|
||||||
notification_settings: {
|
notification_settings: {
|
||||||
selected_email_flags: params,
|
selected_email_flags: selectedEmailFlags,
|
||||||
|
selected_push_flags: selectedPushFlags,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
commit(types.default.SET_USER_NOTIFICATION, response.data);
|
commit(types.default.SET_USER_NOTIFICATION, response.data);
|
||||||
|
|
|
@ -26,6 +26,10 @@ import router from '../dashboard/routes';
|
||||||
import store from '../dashboard/store';
|
import store from '../dashboard/store';
|
||||||
import vueActionCable from '../dashboard/helper/actionCable';
|
import vueActionCable from '../dashboard/helper/actionCable';
|
||||||
import constants from '../dashboard/constants';
|
import constants from '../dashboard/constants';
|
||||||
|
import {
|
||||||
|
verifyServiceWorkerExistence,
|
||||||
|
registerSubscription,
|
||||||
|
} from '../dashboard/helper/pushHelper';
|
||||||
|
|
||||||
Vue.config.env = process.env;
|
Vue.config.env = process.env;
|
||||||
|
|
||||||
|
@ -66,15 +70,12 @@ window.onload = () => {
|
||||||
vueActionCable.init();
|
vueActionCable.init();
|
||||||
};
|
};
|
||||||
|
|
||||||
if ('serviceWorker' in navigator) {
|
window.addEventListener('load', () => {
|
||||||
window.addEventListener('load', () => {
|
verifyServiceWorkerExistence(registration =>
|
||||||
navigator.serviceWorker
|
registration.pushManager.getSubscription().then(subscription => {
|
||||||
.register('/sw.js')
|
if (subscription) {
|
||||||
.then(registration => {
|
registerSubscription();
|
||||||
console.log('SW registered: ', registration);
|
}
|
||||||
})
|
})
|
||||||
.catch(registrationError => {
|
);
|
||||||
console.log('SW registration failed: ', registrationError);
|
});
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
|
@ -6,5 +6,8 @@ export default {
|
||||||
twilioCallbackURL() {
|
twilioCallbackURL() {
|
||||||
return `${this.hostURL}/twilio/callback`;
|
return `${this.hostURL}/twilio/callback`;
|
||||||
},
|
},
|
||||||
|
vapidPublicKey() {
|
||||||
|
return window.chatwootConfig.vapidPublicKey;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
7
app/jobs/notification/email_notification_job.rb
Normal file
7
app/jobs/notification/email_notification_job.rb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
class Notification::EmailNotificationJob < ApplicationJob
|
||||||
|
queue_as :default
|
||||||
|
|
||||||
|
def perform(notification)
|
||||||
|
Notification::EmailNotificationService.new(notification: notification).perform
|
||||||
|
end
|
||||||
|
end
|
7
app/jobs/notification/push_notification_job.rb
Normal file
7
app/jobs/notification/push_notification_job.rb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
class Notification::PushNotificationJob < ApplicationJob
|
||||||
|
queue_as :default
|
||||||
|
|
||||||
|
def perform(notification)
|
||||||
|
Notification::PushNotificationService.new(notification: notification).perform
|
||||||
|
end
|
||||||
|
end
|
|
@ -19,6 +19,7 @@ class AgentBot < ApplicationRecord
|
||||||
|
|
||||||
def push_event_data
|
def push_event_data
|
||||||
{
|
{
|
||||||
|
id: id,
|
||||||
name: name,
|
name: name,
|
||||||
avatar_url: avatar_url,
|
avatar_url: avatar_url,
|
||||||
type: 'agent_bot'
|
type: 'agent_bot'
|
||||||
|
|
|
@ -41,7 +41,9 @@ class Notification < ApplicationRecord
|
||||||
private
|
private
|
||||||
|
|
||||||
def process_notification_delivery
|
def process_notification_delivery
|
||||||
Notification::EmailNotificationService.new(notification: self).perform
|
Notification::PushNotificationJob.perform_later(self)
|
||||||
# Notification::PushNotificationService.new(notification: self).perform
|
|
||||||
|
# Queuing after 2 minutes so that we won't send emails for read notifications
|
||||||
|
Notification::EmailNotificationJob.set(wait: 2.minutes).perform_later(self)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
# Table name: notification_subscriptions
|
# Table name: notification_subscriptions
|
||||||
#
|
#
|
||||||
# id :bigint not null, primary key
|
# id :bigint not null, primary key
|
||||||
|
# identifier :string
|
||||||
# subscription_attributes :jsonb not null
|
# subscription_attributes :jsonb not null
|
||||||
# subscription_type :integer not null
|
# subscription_type :integer not null
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
|
@ -11,11 +12,13 @@
|
||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
# index_notification_subscriptions_on_user_id (user_id)
|
# index_notification_subscriptions_on_identifier (identifier) UNIQUE
|
||||||
|
# index_notification_subscriptions_on_user_id (user_id)
|
||||||
#
|
#
|
||||||
|
|
||||||
class NotificationSubscription < ApplicationRecord
|
class NotificationSubscription < ApplicationRecord
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
|
validates :identifier, presence: true
|
||||||
|
|
||||||
SUBSCRIPTION_TYPES = {
|
SUBSCRIPTION_TYPES = {
|
||||||
browser_push: 1,
|
browser_push: 1,
|
||||||
|
|
|
@ -128,6 +128,7 @@ class User < ApplicationRecord
|
||||||
|
|
||||||
def push_event_data
|
def push_event_data
|
||||||
{
|
{
|
||||||
|
id: id,
|
||||||
name: name,
|
name: name,
|
||||||
avatar_url: avatar_url,
|
avatar_url: avatar_url,
|
||||||
type: 'user'
|
type: 'user'
|
||||||
|
|
|
@ -2,11 +2,14 @@ class Notification::EmailNotificationService
|
||||||
pattr_initialize [:notification!]
|
pattr_initialize [:notification!]
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
|
# don't send emails if user read the push notification already
|
||||||
|
return if notification.read_at.present?
|
||||||
return unless user_subscribed_to_notification?
|
return unless user_subscribed_to_notification?
|
||||||
|
|
||||||
# TODO : Clean up whatever happening over here
|
# TODO : Clean up whatever happening over here
|
||||||
|
# Segregate the mailers properly
|
||||||
AgentNotifications::ConversationNotificationsMailer.public_send(notification
|
AgentNotifications::ConversationNotificationsMailer.public_send(notification
|
||||||
.notification_type.to_s, notification.primary_actor, notification.user).deliver_later
|
.notification_type.to_s, notification.primary_actor, notification.user).deliver_now
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -1,17 +1,73 @@
|
||||||
class Notification::PushNotificationService
|
class Notification::PushNotificationService
|
||||||
|
include Rails.application.routes.url_helpers
|
||||||
|
|
||||||
pattr_initialize [:notification!]
|
pattr_initialize [:notification!]
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
return unless user_subscribed_to_notification?
|
return unless user_subscribed_to_notification?
|
||||||
# TODO: implement the push delivery logic here
|
|
||||||
|
notification_subscriptions.each do |subscription|
|
||||||
|
send_browser_push(subscription) if subscription.browser_push?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
delegate :user, to: :notification
|
||||||
|
delegate :notification_subscriptions, to: :user
|
||||||
|
delegate :notification_settings, to: :user
|
||||||
|
|
||||||
def user_subscribed_to_notification?
|
def user_subscribed_to_notification?
|
||||||
notification_setting = notification.user.notification_settings.find_by(account_id: notification.account.id)
|
notification_setting = notification_settings.find_by(account_id: notification.account.id)
|
||||||
return true if notification_setting.public_send("push_#{notification.notification_type}?")
|
return true if notification_setting.public_send("push_#{notification.notification_type}?")
|
||||||
|
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def conversation
|
||||||
|
@conversation ||= notification.primary_actor
|
||||||
|
end
|
||||||
|
|
||||||
|
def push_message_title
|
||||||
|
if notification.notification_type == 'conversation_creation'
|
||||||
|
return "A new conversation [ID -#{conversation.display_id}] has been created in #{conversation.inbox.name}"
|
||||||
|
end
|
||||||
|
|
||||||
|
if notification.notification_type == 'conversation_assignment'
|
||||||
|
return "A new conversation [ID -#{conversation.display_id}] has been assigned to you."
|
||||||
|
end
|
||||||
|
|
||||||
|
''
|
||||||
|
end
|
||||||
|
|
||||||
|
def push_message
|
||||||
|
{
|
||||||
|
title: push_message_title,
|
||||||
|
tag: "#{notification.notification_type}_#{conversation.display_id}",
|
||||||
|
url: push_url
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def push_url
|
||||||
|
app_account_conversation_url(account_id: conversation.account_id, id: conversation.display_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def send_browser_push(subscription)
|
||||||
|
Webpush.payload_send(
|
||||||
|
message: JSON.generate(push_message),
|
||||||
|
endpoint: subscription.subscription_attributes['endpoint'],
|
||||||
|
p256dh: subscription.subscription_attributes['p256dh'],
|
||||||
|
auth: subscription.subscription_attributes['auth'],
|
||||||
|
vapid: {
|
||||||
|
subject: push_url,
|
||||||
|
public_key: ENV['VAPID_PUBLIC_KEY'],
|
||||||
|
private_key: ENV['VAPID_PRIVATE_KEY']
|
||||||
|
},
|
||||||
|
ssl_timeout: 5,
|
||||||
|
open_timeout: 5,
|
||||||
|
read_timeout: 5
|
||||||
|
)
|
||||||
|
rescue Webpush::ExpiredSubscription
|
||||||
|
subscription.destroy!
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,3 +3,5 @@ json.user_id @notification_setting.user_id
|
||||||
json.account_id @notification_setting.account_id
|
json.account_id @notification_setting.account_id
|
||||||
json.all_email_flags @notification_setting.all_email_flags
|
json.all_email_flags @notification_setting.all_email_flags
|
||||||
json.selected_email_flags @notification_setting.selected_email_flags
|
json.selected_email_flags @notification_setting.selected_email_flags
|
||||||
|
json.all_push_flags @notification_setting.all_push_flags
|
||||||
|
json.selected_push_flags @notification_setting.selected_push_flags
|
||||||
|
|
|
@ -34,7 +34,10 @@
|
||||||
hostURL: '<%= ENV.fetch('FRONTEND_URL', '') %>',
|
hostURL: '<%= ENV.fetch('FRONTEND_URL', '') %>',
|
||||||
fbAppId: '<%= ENV.fetch('FB_APP_ID', nil) %>',
|
fbAppId: '<%= ENV.fetch('FB_APP_ID', nil) %>',
|
||||||
billingEnabled: <%= ActiveModel::Type::Boolean.new.cast(ENV.fetch('BILLING_ENABLED', false)) %>,
|
billingEnabled: <%= ActiveModel::Type::Boolean.new.cast(ENV.fetch('BILLING_ENABLED', false)) %>,
|
||||||
signupEnabled: '<%= ENV.fetch('ENABLE_ACCOUNT_SIGNUP', true) %>'
|
signupEnabled: '<%= ENV.fetch('ENABLE_ACCOUNT_SIGNUP', true) %>',
|
||||||
|
<% if ENV['VAPID_PUBLIC_KEY'] %>
|
||||||
|
vapidPublicKey: new Uint8Array(<%= Base64.urlsafe_decode64(ENV['VAPID_PUBLIC_KEY']).bytes %>),
|
||||||
|
<% end %>
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
class AddIndexOnNotificationSubscriptions < ActiveRecord::Migration[6.0]
|
||||||
|
def change
|
||||||
|
add_column :notification_subscriptions, :identifier, :string
|
||||||
|
add_index :notification_subscriptions, :identifier, unique: true
|
||||||
|
end
|
||||||
|
end
|
18
db/schema.rb
18
db/schema.rb
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 2020_05_03_151130) do
|
ActiveRecord::Schema.define(version: 2020_05_04_144712) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "pgcrypto"
|
enable_extension "pgcrypto"
|
||||||
|
@ -284,6 +284,8 @@ ActiveRecord::Schema.define(version: 2020_05_03_151130) do
|
||||||
t.jsonb "subscription_attributes", default: "{}", null: false
|
t.jsonb "subscription_attributes", default: "{}", null: false
|
||||||
t.datetime "created_at", precision: 6, null: false
|
t.datetime "created_at", precision: 6, null: false
|
||||||
t.datetime "updated_at", precision: 6, null: false
|
t.datetime "updated_at", precision: 6, null: false
|
||||||
|
t.string "identifier"
|
||||||
|
t.index ["identifier"], name: "index_notification_subscriptions_on_identifier", unique: true
|
||||||
t.index ["user_id"], name: "index_notification_subscriptions_on_user_id"
|
t.index ["user_id"], name: "index_notification_subscriptions_on_user_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -316,6 +318,20 @@ ActiveRecord::Schema.define(version: 2020_05_03_151130) do
|
||||||
t.boolean "payment_source_added", default: false
|
t.boolean "payment_source_added", default: false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "super_admins", force: :cascade do |t|
|
||||||
|
t.string "email", default: "", null: false
|
||||||
|
t.string "encrypted_password", default: "", null: false
|
||||||
|
t.datetime "remember_created_at"
|
||||||
|
t.integer "sign_in_count", default: 0, null: false
|
||||||
|
t.datetime "current_sign_in_at"
|
||||||
|
t.datetime "last_sign_in_at"
|
||||||
|
t.inet "current_sign_in_ip"
|
||||||
|
t.inet "last_sign_in_ip"
|
||||||
|
t.datetime "created_at", precision: 6, null: false
|
||||||
|
t.datetime "updated_at", precision: 6, null: false
|
||||||
|
t.index ["email"], name: "index_super_admins_on_email", unique: true
|
||||||
|
end
|
||||||
|
|
||||||
create_table "taggings", id: :serial, force: :cascade do |t|
|
create_table "taggings", id: :serial, force: :cascade do |t|
|
||||||
t.integer "tag_id"
|
t.integer "tag_id"
|
||||||
t.string "taggable_type"
|
t.string "taggable_type"
|
||||||
|
|
168
public/sw.js
168
public/sw.js
|
@ -1,137 +1,37 @@
|
||||||
/* eslint-disable */
|
/* eslint-disable no-restricted-globals, no-console */
|
||||||
/** *
|
/* globals clients */
|
||||||
*
|
self.addEventListener('push', event => {
|
||||||
* The rest of the code is auto-generated. Please don't update this file
|
let notification = event.data && event.data.json();
|
||||||
* directly; instead, make changes to your Workbox build configuration
|
|
||||||
* and re-run your build process.
|
|
||||||
* See https://goo.gl/2aRDsh
|
|
||||||
*/
|
|
||||||
|
|
||||||
importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js");
|
event.waitUntil(
|
||||||
|
self.registration.showNotification(notification.title, {
|
||||||
self.addEventListener('message', (event) => {
|
tag: notification.tag,
|
||||||
if (event.data && event.data.type === 'SKIP_WAITING') {
|
data: {
|
||||||
self.skipWaiting();
|
url: notification.url,
|
||||||
}
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
self.addEventListener('notificationclick', event => {
|
||||||
* The workboxSW.precacheAndRoute() method efficiently caches and responds to
|
let notification = event.notification;
|
||||||
* requests for URLs in the manifest.
|
|
||||||
* See https://goo.gl/S9QRab
|
event.waitUntil(
|
||||||
*/
|
clients.matchAll({ type: 'window' }).then(windowClients => {
|
||||||
self.__precacheManifest = [
|
let matchingWindowClients = windowClients.filter(
|
||||||
{
|
client => client.url === notification.data.url
|
||||||
"url": "android-icon-144x144.png",
|
);
|
||||||
"revision": "d9e3ad004635d6d3154da20ef6e53077"
|
|
||||||
},
|
if (matchingWindowClients.length) {
|
||||||
{
|
let firstWindow = matchingWindowClients[0];
|
||||||
"url": "android-icon-192x192.png",
|
if (firstWindow && 'focus' in firstWindow) {
|
||||||
"revision": "8f2f76058ff81bb03e390ed941f68a70"
|
firstWindow.focus();
|
||||||
},
|
return;
|
||||||
{
|
}
|
||||||
"url": "android-icon-36x36.png",
|
}
|
||||||
"revision": "70b2fa97615a1ccf8fa373674928d0e3"
|
if (clients.openWindow) {
|
||||||
},
|
clients.openWindow(notification.data.url);
|
||||||
{
|
}
|
||||||
"url": "android-icon-48x48.png",
|
})
|
||||||
"revision": "c0e8a16e2ea4430deddac82979f97c60"
|
);
|
||||||
},
|
});
|
||||||
{
|
|
||||||
"url": "android-icon-72x72.png",
|
|
||||||
"revision": "98f4881cce0daf4b89f0b30825b16d80"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "android-icon-96x96.png",
|
|
||||||
"revision": "02cf787c7a88eb898976d79ad0b4e041"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "apple-icon-114x114.png",
|
|
||||||
"revision": "544c150aa39d3ecfd6071e3c54d1503e"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "apple-icon-120x120.png",
|
|
||||||
"revision": "3b10208d8f4b09c5c3631eb5e4e67d9a"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "apple-icon-144x144.png",
|
|
||||||
"revision": "d9e3ad004635d6d3154da20ef6e53077"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "apple-icon-152x152.png",
|
|
||||||
"revision": "a866770945a41e5bcf29706f37e5beba"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "apple-icon-180x180.png",
|
|
||||||
"revision": "327e9272f10374d2859d2a26c86698ec"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "apple-icon-57x57.png",
|
|
||||||
"revision": "ee6e09647e6a26e29655ed4091a6d577"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "apple-icon-60x60.png",
|
|
||||||
"revision": "136acdd5567a57f0b30c4704c93ce412"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "apple-icon-72x72.png",
|
|
||||||
"revision": "98f4881cce0daf4b89f0b30825b16d80"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "apple-icon-76x76.png",
|
|
||||||
"revision": "5de2acd8f66a8fa583830286231abe88"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "apple-icon-precomposed.png",
|
|
||||||
"revision": "03175edf677b78aae0c7ce1c90996bcc"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "apple-icon.png",
|
|
||||||
"revision": "03175edf677b78aae0c7ce1c90996bcc"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "apple-touch-icon-precomposed.png",
|
|
||||||
"revision": "d41d8cd98f00b204e9800998ecf8427e"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "apple-touch-icon.png",
|
|
||||||
"revision": "d41d8cd98f00b204e9800998ecf8427e"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "favicon-16x16.png",
|
|
||||||
"revision": "df49c81fbfd18e43ea9199153f1d5e1f"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "favicon-32x32.png",
|
|
||||||
"revision": "e781cbd8ca95543e247fa913eef30f9c"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "favicon-512x512.png",
|
|
||||||
"revision": "48e48806ef9cbe9edcbe81a08713dc7f"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "favicon-96x96.png",
|
|
||||||
"revision": "02cf787c7a88eb898976d79ad0b4e041"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "favicon.ico",
|
|
||||||
"revision": "788f4b1590d83444281e0c96792fd42b"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "ms-icon-144x144.png",
|
|
||||||
"revision": "d9e3ad004635d6d3154da20ef6e53077"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "ms-icon-150x150.png",
|
|
||||||
"revision": "0770f6909fd7676a02922cd34d23ff15"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "ms-icon-310x310.png",
|
|
||||||
"revision": "492181f5f2a4c199936f7f03c70e4914"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "ms-icon-70x70.png",
|
|
||||||
"revision": "c1b4c1be97c6768c0e5547c2b07bf2a2"
|
|
||||||
}
|
|
||||||
].concat(self.__precacheManifest || []);
|
|
||||||
workbox.precaching.precacheAndRoute(self.__precacheManifest, {});
|
|
||||||
|
|
|
@ -46,7 +46,7 @@ describe ::ContactMergeAction do
|
||||||
expect do
|
expect do
|
||||||
described_class.new(account: new_account, base_contact: base_contact,
|
described_class.new(account: new_account, base_contact: base_contact,
|
||||||
mergee_contact: mergee_contact).perform
|
mergee_contact: mergee_contact).perform
|
||||||
end .to raise_error('contact does not belong to the account')
|
end.to raise_error('contact does not belong to the account')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -70,7 +70,7 @@ RSpec.describe 'Contacts API', type: :request do
|
||||||
expect do
|
expect do
|
||||||
post "/api/v1/accounts/#{account.id}/contacts", headers: admin.create_new_auth_token,
|
post "/api/v1/accounts/#{account.id}/contacts", headers: admin.create_new_auth_token,
|
||||||
params: valid_params
|
params: valid_params
|
||||||
end .to change(Contact, :count).by(1)
|
end.to change(Contact, :count).by(1)
|
||||||
|
|
||||||
expect(response).to have_http_status(:success)
|
expect(response).to have_http_status(:success)
|
||||||
end
|
end
|
||||||
|
|
|
@ -17,14 +17,66 @@ RSpec.describe 'Notifications Subscriptions API', type: :request do
|
||||||
|
|
||||||
it 'creates a notification subscriptions' do
|
it 'creates a notification subscriptions' do
|
||||||
post '/api/v1/notification_subscriptions',
|
post '/api/v1/notification_subscriptions',
|
||||||
params: { notification_subscription: { subscription_type: 'browser_push', 'subscription_attributes': { test: 'test' } } },
|
params: {
|
||||||
|
notification_subscription: {
|
||||||
|
subscription_type: 'browser_push',
|
||||||
|
'subscription_attributes': {
|
||||||
|
endpoint: 'test',
|
||||||
|
p256dh: 'test',
|
||||||
|
auth: 'test'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
headers: agent.create_new_auth_token,
|
headers: agent.create_new_auth_token,
|
||||||
as: :json
|
as: :json
|
||||||
|
|
||||||
expect(response).to have_http_status(:success)
|
expect(response).to have_http_status(:success)
|
||||||
json_response = JSON.parse(response.body)
|
json_response = JSON.parse(response.body)
|
||||||
expect(json_response['subscription_type']).to eq('browser_push')
|
expect(json_response['subscription_type']).to eq('browser_push')
|
||||||
expect(json_response['subscription_attributes']).to eq({ 'test' => 'test' })
|
expect(json_response['subscription_attributes']['auth']).to eq('test')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns existing notification subscription if subscription exists' do
|
||||||
|
subscription = create(:notification_subscription, user: agent)
|
||||||
|
post '/api/v1/notification_subscriptions',
|
||||||
|
params: {
|
||||||
|
notification_subscription: {
|
||||||
|
subscription_type: 'browser_push',
|
||||||
|
'subscription_attributes': {
|
||||||
|
endpoint: 'test',
|
||||||
|
p256dh: 'test',
|
||||||
|
auth: 'test'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
headers: agent.create_new_auth_token,
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
json_response = JSON.parse(response.body)
|
||||||
|
expect(json_response['id']).to eq(subscription.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'move notification subscription to user if its of another user' do
|
||||||
|
subscription = create(:notification_subscription, user: create(:user))
|
||||||
|
post '/api/v1/notification_subscriptions',
|
||||||
|
params: {
|
||||||
|
notification_subscription: {
|
||||||
|
subscription_type: 'browser_push',
|
||||||
|
'subscription_attributes': {
|
||||||
|
endpoint: 'test',
|
||||||
|
p256dh: 'test',
|
||||||
|
auth: 'test'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
headers: agent.create_new_auth_token,
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
json_response = JSON.parse(response.body)
|
||||||
|
expect(json_response['id']).to eq(subscription.id)
|
||||||
|
expect(json_response['user_id']).to eq(agent.id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
10
spec/factories/notification_subscriptions.rb
Normal file
10
spec/factories/notification_subscriptions.rb
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
FactoryBot.define do
|
||||||
|
factory :notification_subscription do
|
||||||
|
user
|
||||||
|
identifier { 'test' }
|
||||||
|
subscription_type { 'browser_push' }
|
||||||
|
subscription_attributes { { endpoint: 'test', auth: 'test' } }
|
||||||
|
end
|
||||||
|
end
|
|
@ -11,16 +11,11 @@ describe NotificationListener do
|
||||||
describe 'conversation_created' do
|
describe 'conversation_created' do
|
||||||
let(:event_name) { :'conversation.created' }
|
let(:event_name) { :'conversation.created' }
|
||||||
|
|
||||||
before do
|
|
||||||
creation_mailer = double
|
|
||||||
allow(AgentNotifications::ConversationNotificationsMailer).to receive(:conversation_creation).and_return(creation_mailer)
|
|
||||||
allow(creation_mailer).to receive(:deliver_later).and_return(true)
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when conversation is created' do
|
context 'when conversation is created' do
|
||||||
it 'sends email to inbox members who have notifications turned on' do
|
it 'creates notifications for inbox members who have notifications turned on' do
|
||||||
notification_setting = agent_with_notification.notification_settings.first
|
notification_setting = agent_with_notification.notification_settings.first
|
||||||
notification_setting.selected_email_flags = [:email_conversation_creation]
|
notification_setting.selected_email_flags = [:email_conversation_creation]
|
||||||
|
notification_setting.selected_push_flags = []
|
||||||
notification_setting.save!
|
notification_setting.save!
|
||||||
|
|
||||||
create(:inbox_member, user: agent_with_notification, inbox: inbox)
|
create(:inbox_member, user: agent_with_notification, inbox: inbox)
|
||||||
|
@ -29,13 +24,13 @@ describe NotificationListener do
|
||||||
event = Events::Base.new(event_name, Time.zone.now, conversation: conversation)
|
event = Events::Base.new(event_name, Time.zone.now, conversation: conversation)
|
||||||
|
|
||||||
listener.conversation_created(event)
|
listener.conversation_created(event)
|
||||||
expect(AgentNotifications::ConversationNotificationsMailer).to have_received(:conversation_creation)
|
expect(notification_setting.user.notifications.count).to eq(1)
|
||||||
.with(conversation, agent_with_notification)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not send and email to inbox members who have notifications turned off' do
|
it 'does not create notification for inbox members who have notifications turned off' do
|
||||||
notification_setting = agent_with_notification.notification_settings.first
|
notification_setting = agent_with_out_notification.notification_settings.first
|
||||||
notification_setting.unselect_all_email_flags
|
notification_setting.unselect_all_email_flags
|
||||||
|
notification_setting.unselect_all_push_flags
|
||||||
notification_setting.save!
|
notification_setting.save!
|
||||||
|
|
||||||
create(:inbox_member, user: agent_with_out_notification, inbox: inbox)
|
create(:inbox_member, user: agent_with_out_notification, inbox: inbox)
|
||||||
|
@ -44,8 +39,7 @@ describe NotificationListener do
|
||||||
event = Events::Base.new(event_name, Time.zone.now, conversation: conversation)
|
event = Events::Base.new(event_name, Time.zone.now, conversation: conversation)
|
||||||
|
|
||||||
listener.conversation_created(event)
|
listener.conversation_created(event)
|
||||||
expect(AgentNotifications::ConversationNotificationsMailer).not_to have_received(:conversation_creation)
|
expect(notification_setting.user.notifications.count).to eq(0)
|
||||||
.with(conversation, agent_with_out_notification)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -122,28 +122,19 @@ RSpec.describe Conversation, type: :model do
|
||||||
expect(conversation.reload.assignee).to eq(agent)
|
expect(conversation.reload.assignee).to eq(agent)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'send assignment mailer' do
|
it 'creates a new notification for the agent' do
|
||||||
allow(AgentNotifications::ConversationNotificationsMailer).to receive(:conversation_assignment).and_return(assignment_mailer)
|
|
||||||
allow(assignment_mailer).to receive(:deliver_later)
|
|
||||||
|
|
||||||
Current.user = conversation.assignee
|
|
||||||
|
|
||||||
expect(update_assignee).to eq(true)
|
expect(update_assignee).to eq(true)
|
||||||
# send_email_notification_to_assignee
|
expect(agent.notifications.count).to eq(1)
|
||||||
expect(AgentNotifications::ConversationNotificationsMailer).to have_received(:conversation_assignment).with(conversation, agent)
|
|
||||||
|
|
||||||
expect(assignment_mailer).to have_received(:deliver_later) if ENV.fetch('SMTP_ADDRESS', nil).present?
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not send assignment mailer if notification setting is turned off' do
|
it 'does not create assignment notification if notification setting is turned off' do
|
||||||
allow(AgentNotifications::ConversationNotificationsMailer).to receive(:conversation_assignment).and_return(assignment_mailer)
|
|
||||||
|
|
||||||
notification_setting = agent.notification_settings.first
|
notification_setting = agent.notification_settings.first
|
||||||
notification_setting.unselect_all_email_flags
|
notification_setting.unselect_all_email_flags
|
||||||
|
notification_setting.unselect_all_push_flags
|
||||||
notification_setting.save!
|
notification_setting.save!
|
||||||
|
|
||||||
expect(update_assignee).to eq(true)
|
expect(update_assignee).to eq(true)
|
||||||
expect(AgentNotifications::ConversationNotificationsMailer).not_to have_received(:conversation_assignment).with(conversation, agent)
|
expect(agent.notifications.count).to eq(0)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue