Feature: Availability Statuses (#874)
Co-authored-by: Pranav Raj S <pranav@thoughtwoot.com>
This commit is contained in:
@@ -1,10 +1,42 @@
|
|||||||
class RoomChannel < ApplicationCable::Channel
|
class RoomChannel < ApplicationCable::Channel
|
||||||
def subscribed
|
def subscribed
|
||||||
stream_from params[:pubsub_token]
|
ensure_stream
|
||||||
::OnlineStatusTracker.add_subscription(params[:pubsub_token])
|
current_user
|
||||||
|
current_account
|
||||||
|
update_subscription
|
||||||
end
|
end
|
||||||
|
|
||||||
def unsubscribed
|
def update_presence
|
||||||
::OnlineStatusTracker.remove_subscription(params[:pubsub_token])
|
update_subscription
|
||||||
|
data = { account_id: @current_account.id, users: ::OnlineStatusTracker.get_available_users(@current_account.id) }
|
||||||
|
data[:contacts] = ::OnlineStatusTracker.get_available_contacts(@current_account.id) if @current_user.is_a? User
|
||||||
|
ActionCable.server.broadcast(@pubsub_token, { event: 'presence.update', data: data })
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def ensure_stream
|
||||||
|
@pubsub_token = params[:pubsub_token]
|
||||||
|
stream_from @pubsub_token
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_subscription
|
||||||
|
::OnlineStatusTracker.update_presence(@current_account.id, @current_user.class.name, @current_user.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def current_user
|
||||||
|
@current_user ||= if params[:user_id].blank?
|
||||||
|
Contact.find_by!(pubsub_token: @pubsub_token)
|
||||||
|
else
|
||||||
|
User.find_by!(pubsub_token: @pubsub_token, id: params[:user_id])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def current_account
|
||||||
|
@current_account ||= if @current_user.is_a? Contact
|
||||||
|
@current_user.account
|
||||||
|
else
|
||||||
|
@current_user.accounts.find(params[:account_id])
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -16,6 +16,6 @@ class Api::V1::ProfilesController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def profile_params
|
def profile_params
|
||||||
params.require(:profile).permit(:email, :name, :password, :password_confirmation, :avatar)
|
params.require(:profile).permit(:email, :name, :password, :password_confirmation, :avatar, :availability)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -118,21 +118,18 @@ export default {
|
|||||||
return axios.post(urlData.url, { email });
|
return axios.post(urlData.url, { email });
|
||||||
},
|
},
|
||||||
|
|
||||||
profileUpdate({ name, email, password, password_confirmation, avatar }) {
|
profileUpdate({ password, password_confirmation, ...profileAttributes }) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
if (name) {
|
Object.keys(profileAttributes).forEach(key => {
|
||||||
formData.append('profile[name]', name);
|
const value = profileAttributes[key];
|
||||||
}
|
if (value) {
|
||||||
if (email) {
|
formData.append(`profile[${key}]`, value);
|
||||||
formData.append('profile[email]', email);
|
}
|
||||||
}
|
});
|
||||||
if (password && password_confirmation) {
|
if (password && password_confirmation) {
|
||||||
formData.append('profile[password]', password);
|
formData.append('profile[password]', password);
|
||||||
formData.append('profile[password_confirmation]', password_confirmation);
|
formData.append('profile[password_confirmation]', password_confirmation);
|
||||||
}
|
}
|
||||||
if (avatar) {
|
|
||||||
formData.append('profile[avatar]', avatar);
|
|
||||||
}
|
|
||||||
return axios.put(endPoints('profileUpdate').url, formData);
|
return axios.put(endPoints('profileUpdate').url, formData);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -56,7 +56,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
<div class="current-user" @click.prevent="showOptions()">
|
<div class="current-user" @click.prevent="showOptions()">
|
||||||
<thumbnail :src="currentUser.avatar_url" :username="currentUser.name" />
|
<thumbnail
|
||||||
|
:src="currentUser.avatar_url"
|
||||||
|
:username="currentUser.name"
|
||||||
|
:status="currentUser.availability_status"
|
||||||
|
/>
|
||||||
<div class="current-user--data">
|
<div class="current-user--data">
|
||||||
<h3 class="current-user--name">
|
<h3 class="current-user--name">
|
||||||
{{ currentUser.name }}
|
{{ currentUser.name }}
|
||||||
|
|||||||
@@ -21,11 +21,6 @@
|
|||||||
:style="badgeStyle"
|
:style="badgeStyle"
|
||||||
src="~dashboard/assets/images/fb-badge.png"
|
src="~dashboard/assets/images/fb-badge.png"
|
||||||
/>
|
/>
|
||||||
<div
|
|
||||||
v-else-if="status === 'online'"
|
|
||||||
class="source-badge user--online"
|
|
||||||
:style="statusStyle"
|
|
||||||
></div>
|
|
||||||
<img
|
<img
|
||||||
v-if="badge === 'Channel::TwitterProfile'"
|
v-if="badge === 'Channel::TwitterProfile'"
|
||||||
id="badge"
|
id="badge"
|
||||||
@@ -33,7 +28,6 @@
|
|||||||
:style="badgeStyle"
|
:style="badgeStyle"
|
||||||
src="~dashboard/assets/images/twitter-badge.png"
|
src="~dashboard/assets/images/twitter-badge.png"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<img
|
<img
|
||||||
v-if="badge === 'Channel::TwilioSms'"
|
v-if="badge === 'Channel::TwilioSms'"
|
||||||
id="badge"
|
id="badge"
|
||||||
@@ -41,6 +35,11 @@
|
|||||||
:style="badgeStyle"
|
:style="badgeStyle"
|
||||||
src="~dashboard/assets/images/channels/whatsapp.png"
|
src="~dashboard/assets/images/channels/whatsapp.png"
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
v-if="showStatusIndicator"
|
||||||
|
:class="`source-badge user-online-status user-online-status--${status}`"
|
||||||
|
:style="statusStyle"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
@@ -89,6 +88,9 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
showStatusIndicator() {
|
||||||
|
return this.status === 'online' || this.status === 'busy';
|
||||||
|
},
|
||||||
avatarSize() {
|
avatarSize() {
|
||||||
return Number(this.size.replace(/\D+/g, ''));
|
return Number(this.size.replace(/\D+/g, ''));
|
||||||
},
|
},
|
||||||
@@ -150,8 +152,7 @@ export default {
|
|||||||
width: $space-slab;
|
width: $space-slab;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user--online {
|
.user-online-status {
|
||||||
background: $success-color;
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
bottom: $space-micro;
|
bottom: $space-micro;
|
||||||
|
|
||||||
@@ -159,5 +160,13 @@ export default {
|
|||||||
content: ' ';
|
content: ' ';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-online-status--online {
|
||||||
|
background: $success-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-online-status--busy {
|
||||||
|
background: $warning-color;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
:badge="currentContact.channel"
|
:badge="currentContact.channel"
|
||||||
class="columns"
|
class="columns"
|
||||||
:username="currentContact.name"
|
:username="currentContact.name"
|
||||||
|
:status="currentContact.availability_status"
|
||||||
size="40px"
|
size="40px"
|
||||||
/>
|
/>
|
||||||
<div class="conversation--details columns">
|
<div class="conversation--details columns">
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
size="40px"
|
size="40px"
|
||||||
:badge="currentContact.channel"
|
:badge="currentContact.channel"
|
||||||
:username="currentContact.name"
|
:username="currentContact.name"
|
||||||
|
:status="currentContact.availability_status"
|
||||||
/>
|
/>
|
||||||
<div class="user--profile__meta">
|
<div class="user--profile__meta">
|
||||||
<h3 v-if="!isContactPanelOpen" class="user--name text-truncate">
|
<h3 v-if="!isContactPanelOpen" class="user--name text-truncate">
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class ActionCableConnector extends BaseActionCableConnector {
|
|||||||
'conversation.typing_on': this.onTypingOn,
|
'conversation.typing_on': this.onTypingOn,
|
||||||
'conversation.typing_off': this.onTypingOff,
|
'conversation.typing_off': this.onTypingOff,
|
||||||
'conversation.contact_changed': this.onConversationContactChange,
|
'conversation.contact_changed': this.onConversationContactChange,
|
||||||
|
'presence.update': this.onPresenceUpdate,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,6 +30,11 @@ class ActionCableConnector extends BaseActionCableConnector {
|
|||||||
this.app.$store.dispatch('updateMessage', data);
|
this.app.$store.dispatch('updateMessage', data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onPresenceUpdate = data => {
|
||||||
|
this.app.$store.dispatch('contacts/updatePresence', data.contacts);
|
||||||
|
this.app.$store.dispatch('agents/updatePresence', data.users);
|
||||||
|
};
|
||||||
|
|
||||||
onConversationContactChange = payload => {
|
onConversationContactChange = payload => {
|
||||||
const { meta = {}, id: conversationId } = payload;
|
const { meta = {}, id: conversationId } = payload;
|
||||||
const { sender } = meta || {};
|
const { sender } = meta || {};
|
||||||
|
|||||||
@@ -48,6 +48,23 @@
|
|||||||
"ERROR": "Please enter a valid name",
|
"ERROR": "Please enter a valid name",
|
||||||
"PLACEHOLDER": "Please enter your name, this would be displayed in conversations"
|
"PLACEHOLDER": "Please enter your name, this would be displayed in conversations"
|
||||||
},
|
},
|
||||||
|
"AVAILABILITY": {
|
||||||
|
"LABEL": "Availability",
|
||||||
|
"STATUSES_LIST": [
|
||||||
|
{
|
||||||
|
"value": "online",
|
||||||
|
"label": "Online"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "busy",
|
||||||
|
"label": "Busy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "offline",
|
||||||
|
"label": "Offline"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"EMAIL": {
|
"EMAIL": {
|
||||||
"LABEL": "Your email address",
|
"LABEL": "Your email address",
|
||||||
"ERROR": "Please enter a valid email address",
|
"ERROR": "Please enter a valid email address",
|
||||||
|
|||||||
@@ -38,6 +38,19 @@
|
|||||||
{{ $t('PROFILE_SETTINGS.FORM.EMAIL.ERROR') }}
|
{{ $t('PROFILE_SETTINGS.FORM.EMAIL.ERROR') }}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
<label>
|
||||||
|
{{ $t('PROFILE_SETTINGS.FORM.AVAILABILITY.LABEL') }}
|
||||||
|
<select v-model="availability">
|
||||||
|
<option
|
||||||
|
v-for="status in availabilityStatuses"
|
||||||
|
:key="status.key"
|
||||||
|
class="text-capitalize"
|
||||||
|
:value="status.value"
|
||||||
|
>
|
||||||
|
{{ status.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="profile--settings--row row">
|
<div class="profile--settings--row row">
|
||||||
@@ -99,16 +112,17 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
/* global bus */
|
|
||||||
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 NotificationSettings from './NotificationSettings';
|
import NotificationSettings from './NotificationSettings';
|
||||||
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
NotificationSettings,
|
NotificationSettings,
|
||||||
},
|
},
|
||||||
|
mixin: [alertMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
avatarFile: '',
|
avatarFile: '',
|
||||||
@@ -117,7 +131,11 @@ export default {
|
|||||||
email: '',
|
email: '',
|
||||||
password: '',
|
password: '',
|
||||||
passwordConfirmation: '',
|
passwordConfirmation: '',
|
||||||
|
availability: 'online',
|
||||||
isUpdating: false,
|
isUpdating: false,
|
||||||
|
availabilityStatuses: this.$t(
|
||||||
|
'PROFILE_SETTINGS.FORM.AVAILABILITY.STATUSES_LIST'
|
||||||
|
),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
validations: {
|
validations: {
|
||||||
@@ -164,11 +182,12 @@ export default {
|
|||||||
this.name = this.currentUser.name;
|
this.name = this.currentUser.name;
|
||||||
this.email = this.currentUser.email;
|
this.email = this.currentUser.email;
|
||||||
this.avatarUrl = this.currentUser.avatar_url;
|
this.avatarUrl = this.currentUser.avatar_url;
|
||||||
|
this.availability = this.currentUser.availability_status;
|
||||||
},
|
},
|
||||||
async updateUser() {
|
async updateUser() {
|
||||||
this.$v.$touch();
|
this.$v.$touch();
|
||||||
if (this.$v.$invalid) {
|
if (this.$v.$invalid) {
|
||||||
bus.$emit('newToastMessage', this.$t('PROFILE_SETTINGS.FORM.ERROR'));
|
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.ERROR'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.isUpdating = true;
|
this.isUpdating = true;
|
||||||
@@ -179,15 +198,13 @@ export default {
|
|||||||
email: this.email,
|
email: this.email,
|
||||||
avatar: this.avatarFile,
|
avatar: this.avatarFile,
|
||||||
password: this.password,
|
password: this.password,
|
||||||
|
availability: this.availability,
|
||||||
password_confirmation: this.passwordConfirmation,
|
password_confirmation: this.passwordConfirmation,
|
||||||
});
|
});
|
||||||
this.isUpdating = false;
|
this.isUpdating = false;
|
||||||
if (hasEmailChanged) {
|
if (hasEmailChanged) {
|
||||||
clearCookiesOnLogout();
|
clearCookiesOnLogout();
|
||||||
bus.$emit(
|
this.showAlert(this.$t('PROFILE_SETTINGS.AFTER_EMAIL_CHANGED'));
|
||||||
'newToastMessage',
|
|
||||||
this.$t('PROFILE_SETTINGS.AFTER_EMAIL_CHANGED')
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.isUpdating = false;
|
this.isUpdating = false;
|
||||||
|
|||||||
@@ -57,6 +57,13 @@ export const actions = {
|
|||||||
throw new Error(error);
|
throw new Error(error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updatePresence: async ({ commit }, data) => {
|
||||||
|
commit(types.default.SET_AGENT_UPDATING_STATUS, true);
|
||||||
|
commit(types.default.UPDATE_AGENTS_PRESENCE, data);
|
||||||
|
commit(types.default.SET_AGENT_UPDATING_STATUS, false);
|
||||||
|
},
|
||||||
|
|
||||||
delete: async ({ commit }, agentId) => {
|
delete: async ({ commit }, agentId) => {
|
||||||
commit(types.default.SET_AGENT_DELETING_STATUS, true);
|
commit(types.default.SET_AGENT_DELETING_STATUS, true);
|
||||||
try {
|
try {
|
||||||
@@ -88,6 +95,7 @@ export const mutations = {
|
|||||||
[types.default.ADD_AGENT]: MutationHelpers.create,
|
[types.default.ADD_AGENT]: MutationHelpers.create,
|
||||||
[types.default.EDIT_AGENT]: MutationHelpers.update,
|
[types.default.EDIT_AGENT]: MutationHelpers.update,
|
||||||
[types.default.DELETE_AGENT]: MutationHelpers.destroy,
|
[types.default.DELETE_AGENT]: MutationHelpers.destroy,
|
||||||
|
[types.default.UPDATE_AGENTS_PRESENCE]: MutationHelpers.updatePresence,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@@ -59,6 +59,10 @@ export const actions = {
|
|||||||
throw new Error(error);
|
throw new Error(error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updatePresence: ({ commit }, data) => {
|
||||||
|
commit(types.default.UPDATE_CONTACTS_PRESENCE, data);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mutations = {
|
export const mutations = {
|
||||||
@@ -88,6 +92,21 @@ export const mutations = {
|
|||||||
[types.default.EDIT_CONTACT]: ($state, data) => {
|
[types.default.EDIT_CONTACT]: ($state, data) => {
|
||||||
Vue.set($state.records, data.id, data);
|
Vue.set($state.records, data.id, data);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
[types.default.UPDATE_CONTACTS_PRESENCE]: ($state, data) => {
|
||||||
|
Object.values($state.records).forEach(element => {
|
||||||
|
const availabilityStatus = data[element.id];
|
||||||
|
if (availabilityStatus) {
|
||||||
|
Vue.set(
|
||||||
|
$state.records[element.id],
|
||||||
|
'availability_status',
|
||||||
|
availabilityStatus
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Vue.delete($state.records[element.id], 'availability_status');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@@ -93,4 +93,16 @@ describe('#actions', () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('#updatePresence', () => {
|
||||||
|
it('sends correct actions if API is success', async () => {
|
||||||
|
const data = { users: { 1: 'online' }, contacts: { 2: 'online' } };
|
||||||
|
actions.updatePresence({ commit }, data);
|
||||||
|
expect(commit.mock.calls).toEqual([
|
||||||
|
[types.default.SET_AGENT_UPDATING_STATUS, true],
|
||||||
|
[types.default.UPDATE_AGENTS_PRESENCE, data],
|
||||||
|
[types.default.SET_AGENT_UPDATING_STATUS, false],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -60,4 +60,40 @@ describe('#mutations', () => {
|
|||||||
expect(state.records).toEqual([]);
|
expect(state.records).toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('#UPDATE_AGENTS_PRESENCE', () => {
|
||||||
|
it('updates agent presence', () => {
|
||||||
|
const state = {
|
||||||
|
records: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Agent1',
|
||||||
|
email: 'agent1@chatwoot.com',
|
||||||
|
availability_status: 'offline',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'Agent1',
|
||||||
|
email: 'agent1@chatwoot.com',
|
||||||
|
availability_status: 'online',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
mutations[types.default.UPDATE_AGENTS_PRESENCE](state, { '1': 'busy' });
|
||||||
|
expect(state.records).toEqual([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Agent1',
|
||||||
|
email: 'agent1@chatwoot.com',
|
||||||
|
availability_status: 'busy',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'Agent1',
|
||||||
|
email: 'agent1@chatwoot.com',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ export default {
|
|||||||
ADD_AGENT: 'ADD_AGENT',
|
ADD_AGENT: 'ADD_AGENT',
|
||||||
EDIT_AGENT: 'EDIT_AGENT',
|
EDIT_AGENT: 'EDIT_AGENT',
|
||||||
DELETE_AGENT: 'DELETE_AGENT',
|
DELETE_AGENT: 'DELETE_AGENT',
|
||||||
|
UPDATE_AGENTS_PRESENCE: 'UPDATE_AGENTS_PRESENCE',
|
||||||
|
|
||||||
// Canned Response
|
// Canned Response
|
||||||
SET_CANNED_UI_FLAG: 'SET_CANNED_UI_FLAG',
|
SET_CANNED_UI_FLAG: 'SET_CANNED_UI_FLAG',
|
||||||
@@ -91,6 +92,7 @@ export default {
|
|||||||
SET_CONTACT_ITEM: 'SET_CONTACT_ITEM',
|
SET_CONTACT_ITEM: 'SET_CONTACT_ITEM',
|
||||||
SET_CONTACTS: 'SET_CONTACTS',
|
SET_CONTACTS: 'SET_CONTACTS',
|
||||||
EDIT_CONTACT: 'EDIT_CONTACT',
|
EDIT_CONTACT: 'EDIT_CONTACT',
|
||||||
|
UPDATE_CONTACTS_PRESENCE: 'UPDATE_CONTACTS_PRESENCE',
|
||||||
|
|
||||||
// Contact Conversation
|
// Contact Conversation
|
||||||
SET_CONTACT_CONVERSATIONS_UI_FLAG: 'SET_CONTACT_CONVERSATIONS_UI_FLAG',
|
SET_CONTACT_CONVERSATIONS_UI_FLAG: 'SET_CONTACT_CONVERSATIONS_UI_FLAG',
|
||||||
|
|||||||
@@ -1,20 +1,31 @@
|
|||||||
import { createConsumer } from '@rails/actioncable';
|
import { createConsumer } from '@rails/actioncable';
|
||||||
|
|
||||||
|
const PRESENCE_INTERVAL = 60000;
|
||||||
|
|
||||||
class BaseActionCableConnector {
|
class BaseActionCableConnector {
|
||||||
constructor(app, pubsubToken) {
|
constructor(app, pubsubToken) {
|
||||||
this.consumer = createConsumer();
|
this.consumer = createConsumer();
|
||||||
this.consumer.subscriptions.create(
|
this.subscription = this.consumer.subscriptions.create(
|
||||||
{
|
{
|
||||||
channel: 'RoomChannel',
|
channel: 'RoomChannel',
|
||||||
pubsub_token: pubsubToken,
|
pubsub_token: pubsubToken,
|
||||||
|
account_id: app.$store.getters.getCurrentAccountId,
|
||||||
|
user_id: app.$store.getters.getCurrentUserID,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
updatePresence() {
|
||||||
|
this.perform('update_presence');
|
||||||
|
},
|
||||||
received: this.onReceived,
|
received: this.onReceived,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
this.app = app;
|
this.app = app;
|
||||||
this.events = {};
|
this.events = {};
|
||||||
this.isAValidEvent = () => true;
|
this.isAValidEvent = () => true;
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
this.subscription.updatePresence();
|
||||||
|
}, PRESENCE_INTERVAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnect() {
|
disconnect() {
|
||||||
|
|||||||
@@ -34,6 +34,17 @@ export const updateAttributes = (state, data) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const updatePresence = (state, data) => {
|
||||||
|
state.records.forEach((element, index) => {
|
||||||
|
const availabilityStatus = data[element.id];
|
||||||
|
if (availabilityStatus) {
|
||||||
|
Vue.set(state.records[index], 'availability_status', availabilityStatus);
|
||||||
|
} else {
|
||||||
|
Vue.delete(state.records[index], 'availability_status');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const destroy = (state, id) => {
|
export const destroy = (state, id) => {
|
||||||
state.records = state.records.filter(record => record.id !== id);
|
state.records = state.records.filter(record => record.id !== id);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ class ActionCableConnector extends BaseActionCableConnector {
|
|||||||
'conversation.typing_off': this.onTypingOff,
|
'conversation.typing_off': this.onTypingOff,
|
||||||
'conversation.resolved': this.onStatusChange,
|
'conversation.resolved': this.onStatusChange,
|
||||||
'conversation.opened': this.onStatusChange,
|
'conversation.opened': this.onStatusChange,
|
||||||
|
'presence.update': this.onPresenceUpdate,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,6 +26,10 @@ class ActionCableConnector extends BaseActionCableConnector {
|
|||||||
this.app.$store.dispatch('conversation/updateMessage', data);
|
this.app.$store.dispatch('conversation/updateMessage', data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onPresenceUpdate = data => {
|
||||||
|
this.app.$store.dispatch('agent/updatePresence', data.users);
|
||||||
|
};
|
||||||
|
|
||||||
onTypingOn = () => {
|
onTypingOn = () => {
|
||||||
this.clearTimer();
|
this.clearTimer();
|
||||||
this.app.$store.dispatch('conversation/toggleAgentTyping', {
|
this.app.$store.dispatch('conversation/toggleAgentTyping', {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import { getAvailableAgents } from 'widget/api/agent';
|
import { getAvailableAgents } from 'widget/api/agent';
|
||||||
|
import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers';
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
records: [],
|
records: [],
|
||||||
@@ -27,12 +28,16 @@ export const actions = {
|
|||||||
commit('setHasFetched', true);
|
commit('setHasFetched', true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
updatePresence: async ({ commit }, data) => {
|
||||||
|
commit('updatePresence', data);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mutations = {
|
export const mutations = {
|
||||||
setAgents($state, data) {
|
setAgents($state, data) {
|
||||||
Vue.set($state, 'records', data);
|
Vue.set($state, 'records', data);
|
||||||
},
|
},
|
||||||
|
updatePresence: MutationHelpers.updatePresence,
|
||||||
setError($state, value) {
|
setError($state, value) {
|
||||||
Vue.set($state.uiFlags, 'isError', value);
|
Vue.set($state.uiFlags, 'isError', value);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -25,4 +25,9 @@ describe('#actions', () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('#updatePresence', () => {
|
||||||
|
actions.updatePresence({ commit }, { 1: 'online' });
|
||||||
|
expect(commit.mock.calls).toEqual([['updatePresence', { 1: 'online' }]]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { mutations } from '../../agent';
|
import { mutations } from '../../agent';
|
||||||
import agents from './data';
|
import { agents } from './data';
|
||||||
|
|
||||||
describe('#mutations', () => {
|
describe('#mutations', () => {
|
||||||
describe('#setAgents', () => {
|
describe('#setAgents', () => {
|
||||||
@@ -25,4 +25,35 @@ describe('#mutations', () => {
|
|||||||
expect(state.uiFlags.hasFetched).toEqual(true);
|
expect(state.uiFlags.hasFetched).toEqual(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('#updatePresence', () => {
|
||||||
|
it('updates agent presence', () => {
|
||||||
|
const state = { records: agents };
|
||||||
|
mutations.updatePresence(state, { 1: 'busy', 2: 'online' });
|
||||||
|
expect(state.records).toEqual([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'John',
|
||||||
|
avatar_url: '',
|
||||||
|
availability_status: 'busy',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'Xavier',
|
||||||
|
avatar_url: '',
|
||||||
|
availability_status: 'online',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'Pranav',
|
||||||
|
avatar_url: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: 'Nithin',
|
||||||
|
avatar_url: '',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
module AvailabilityStatusable
|
module AvailabilityStatusable
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
def online?
|
def online_presence?
|
||||||
::OnlineStatusTracker.subscription_count(pubsub_token) != 0
|
::OnlineStatusTracker.get_presence(availability_account_id, self.class.name, id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def availability_status
|
def availability_status
|
||||||
online? ? 'online' : 'offline'
|
return 'offline' unless online_presence?
|
||||||
|
return 'online' if is_a? Contact
|
||||||
|
|
||||||
|
::OnlineStatusTracker.get_status(availability_account_id, id) || 'online'
|
||||||
|
end
|
||||||
|
|
||||||
|
def availability_account_id
|
||||||
|
return account_id if is_a? Contact
|
||||||
|
|
||||||
|
Current.account.present? ? Current.account.id : accounts.first.id
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
# Table name: users
|
# Table name: users
|
||||||
#
|
#
|
||||||
# id :integer not null, primary key
|
# id :integer not null, primary key
|
||||||
|
# availability :integer default(0)
|
||||||
# confirmation_sent_at :datetime
|
# confirmation_sent_at :datetime
|
||||||
# confirmation_token :string
|
# confirmation_token :string
|
||||||
# confirmed_at :datetime
|
# confirmed_at :datetime
|
||||||
@@ -53,6 +54,8 @@ class User < ApplicationRecord
|
|||||||
:validatable,
|
:validatable,
|
||||||
:confirmable
|
:confirmable
|
||||||
|
|
||||||
|
enum availability: { online: 0, offline: 1, busy: 2 }
|
||||||
|
|
||||||
# The validation below has been commented out as it does not
|
# The validation below has been commented out as it does not
|
||||||
# work because :validatable in devise overrides this.
|
# work because :validatable in devise overrides this.
|
||||||
# validates_uniqueness_of :email, scope: :account_id
|
# validates_uniqueness_of :email, scope: :account_id
|
||||||
@@ -75,6 +78,7 @@ class User < ApplicationRecord
|
|||||||
before_validation :set_password_and_uid, on: :create
|
before_validation :set_password_and_uid, on: :create
|
||||||
|
|
||||||
after_create :create_access_token
|
after_create :create_access_token
|
||||||
|
after_save :update_presence_in_redis, if: :saved_change_to_availability?
|
||||||
|
|
||||||
def send_devise_notification(notification, *args)
|
def send_devise_notification(notification, *args)
|
||||||
devise_mailer.send(notification, self, *args).deliver_later
|
devise_mailer.send(notification, self, *args).deliver_later
|
||||||
@@ -126,7 +130,8 @@ class User < ApplicationRecord
|
|||||||
id: id,
|
id: id,
|
||||||
name: name,
|
name: name,
|
||||||
avatar_url: avatar_url,
|
avatar_url: avatar_url,
|
||||||
type: 'user'
|
type: 'user',
|
||||||
|
availability_status: availability_status
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -138,4 +143,12 @@ class User < ApplicationRecord
|
|||||||
type: 'user'
|
type: 'user'
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def update_presence_in_redis
|
||||||
|
accounts.each do |account|
|
||||||
|
OnlineStatusTracker.set_status(account.id, id, availability)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
json.payload do
|
json.payload do
|
||||||
json.array! @contacts do |contact|
|
json.array! @contacts do |contact|
|
||||||
json.id contact.id
|
json.partial! 'api/v1/models/contact.json.jbuilder', resource: contact
|
||||||
json.name contact.name
|
|
||||||
json.email contact.email
|
|
||||||
json.phone_number contact.phone_number
|
|
||||||
json.thumbnail contact.avatar_url
|
|
||||||
json.additional_attributes contact.additional_attributes
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,9 +1,3 @@
|
|||||||
json.payload do
|
json.payload do
|
||||||
json.availability_status @contact.availability_status
|
json.partial! 'api/v1/models/contact.json.jbuilder', resource: @contact
|
||||||
json.email @contact.email
|
|
||||||
json.id @contact.id
|
|
||||||
json.name @contact.name
|
|
||||||
json.phone_number @contact.phone_number
|
|
||||||
json.thumbnail @contact.avatar_url
|
|
||||||
json.additional_attributes @contact.additional_attributes
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,8 +1,3 @@
|
|||||||
json.payload do
|
json.payload do
|
||||||
json.id @contact.id
|
json.partial! 'api/v1/models/contact.json.jbuilder', resource: @contact
|
||||||
json.name @contact.name
|
|
||||||
json.email @contact.email
|
|
||||||
json.phone_number @contact.phone_number
|
|
||||||
json.thumbnail @contact.avatar_url
|
|
||||||
json.additional_attributes @contact.additional_attributes
|
|
||||||
end
|
end
|
||||||
|
|||||||
7
app/views/api/v1/models/_contact.json.jbuilder
Normal file
7
app/views/api/v1/models/_contact.json.jbuilder
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
json.additional_attributes resource.additional_attributes
|
||||||
|
json.availability_status resource.availability_status
|
||||||
|
json.email resource.email
|
||||||
|
json.id resource.id
|
||||||
|
json.name resource.name
|
||||||
|
json.phone_number resource.phone_number
|
||||||
|
json.thumbnail resource.avatar_url
|
||||||
@@ -11,6 +11,7 @@ json.inviter_id resource.active_account_user&.inviter_id
|
|||||||
json.confirmed resource.confirmed?
|
json.confirmed resource.confirmed?
|
||||||
json.avatar_url resource.avatar_url
|
json.avatar_url resource.avatar_url
|
||||||
json.access_token resource.access_token.token
|
json.access_token resource.access_token.token
|
||||||
|
json.availability_status resource.availability_status
|
||||||
json.accounts do
|
json.accounts do
|
||||||
json.array! resource.account_users do |account_user|
|
json.array! resource.account_users do |account_user|
|
||||||
json.id account_user.account_id
|
json.id account_user.account_id
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
development:
|
default: &default
|
||||||
adapter: redis
|
adapter: redis
|
||||||
url: <%= ENV.fetch('REDIS_URL', 'redis://127.0.0.1:6379') %>
|
url: <%= ENV.fetch('REDIS_URL', 'redis://127.0.0.1:6379') %>
|
||||||
password: <%= ENV.fetch('REDIS_PASSWORD', nil).presence %>
|
password: <%= ENV.fetch('REDIS_PASSWORD', nil).presence %>
|
||||||
|
channel_prefix: <%= "chatwoot_#{Rails.env}_action_cable" %>
|
||||||
|
|
||||||
|
development:
|
||||||
|
<<: *default
|
||||||
|
|
||||||
test:
|
test:
|
||||||
adapter: test
|
adapter: test
|
||||||
|
channel_prefix: <%= "chatwoot_#{Rails.env}_action_cable" %>
|
||||||
|
|
||||||
staging:
|
staging:
|
||||||
adapter: redis
|
<<: *default
|
||||||
url: <%= ENV.fetch('REDIS_URL', 'redis://127.0.0.1:6379') %>
|
|
||||||
password: <%= ENV.fetch('REDIS_PASSWORD', nil).presence %>
|
|
||||||
|
|
||||||
production:
|
production:
|
||||||
adapter: redis
|
<<: *default
|
||||||
url: <%= ENV.fetch('REDIS_URL', 'redis://127.0.0.1:6379') %>
|
|
||||||
password: <%= ENV.fetch('REDIS_PASSWORD', nil).presence %>
|
|
||||||
|
|||||||
5
db/migrate/20200629122646_add_availability_to_user.rb
Normal file
5
db/migrate/20200629122646_add_availability_to_user.rb
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
class AddAvailabilityToUser < ActiveRecord::Migration[6.0]
|
||||||
|
def change
|
||||||
|
add_column :users, :availability, :integer, default: 0
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -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_06_25_154254) do
|
ActiveRecord::Schema.define(version: 2020_06_29_122646) 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 "pg_stat_statements"
|
enable_extension "pg_stat_statements"
|
||||||
@@ -413,6 +413,7 @@ ActiveRecord::Schema.define(version: 2020_06_25_154254) do
|
|||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.string "pubsub_token"
|
t.string "pubsub_token"
|
||||||
|
t.integer "availability", default: 0
|
||||||
t.index ["email"], name: "index_users_on_email"
|
t.index ["email"], name: "index_users_on_email"
|
||||||
t.index ["pubsub_token"], name: "index_users_on_pubsub_token", unique: true
|
t.index ["pubsub_token"], name: "index_users_on_pubsub_token", unique: true
|
||||||
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
|
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
|
||||||
|
|||||||
@@ -1,19 +1,50 @@
|
|||||||
module OnlineStatusTracker
|
module OnlineStatusTracker
|
||||||
def self.add_subscription(channel_id)
|
PRESENCE_DURATION = 60.seconds
|
||||||
count = subscription_count(channel_id)
|
|
||||||
::Redis::Alfred.setex(channel_id, count + 1)
|
# presence : sorted set with timestamp as the score & object id as value
|
||||||
|
|
||||||
|
# obj_type: Contact | User
|
||||||
|
def self.update_presence(account_id, obj_type, obj_id)
|
||||||
|
::Redis::Alfred.zadd(presence_key(account_id, obj_type), Time.now.to_i, obj_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.remove_subscription(channel_id)
|
def self.get_presence(account_id, obj_type, obj_id)
|
||||||
count = subscription_count(channel_id)
|
connected_time = ::Redis::Alfred.zscore(presence_key(account_id, obj_type), obj_id)
|
||||||
if count == 1
|
connected_time && connected_time > (Time.zone.now - PRESENCE_DURATION).to_i
|
||||||
::Redis::Alfred.delete(channel_id)
|
end
|
||||||
elsif count != 0
|
|
||||||
::Redis::Alfred.setex(channel_id, count - 1)
|
def self.presence_key(account_id, type)
|
||||||
|
case type
|
||||||
|
when 'Contact'
|
||||||
|
Redis::Alfred::ONLINE_PRESENCE_CONTACTS % account_id
|
||||||
|
else
|
||||||
|
Redis::Alfred::ONLINE_PRESENCE_USERS % account_id
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.subscription_count(channel_id)
|
# online status : online | busy | offline
|
||||||
::Redis::Alfred.get(channel_id).to_i
|
# redis hash with obj_id key && status as value
|
||||||
|
|
||||||
|
def self.set_status(account_id, user_id, status)
|
||||||
|
::Redis::Alfred.hset(status_key(account_id), user_id, status)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.get_status(account_id, user_id)
|
||||||
|
::Redis::Alfred.hget(status_key(account_id), user_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.status_key(account_id)
|
||||||
|
Redis::Alfred::ONLINE_STATUS % account_id
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.get_available_contacts(account_id)
|
||||||
|
contact_ids = ::Redis::Alfred.zrangebyscore(presence_key(account_id, 'Contact'), (Time.zone.now - PRESENCE_DURATION).to_i, Time.now.to_i)
|
||||||
|
contact_ids.index_with { |_id| 'online' }
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.get_available_users(account_id)
|
||||||
|
user_ids = ::Redis::Alfred.zrangebyscore(presence_key(account_id, 'User'), (Time.zone.now - PRESENCE_DURATION).to_i, Time.now.to_i)
|
||||||
|
user_availabilities = ::Redis::Alfred.hmget(status_key(account_id), user_ids)
|
||||||
|
user_ids.map.with_index { |id, index| [id, (user_availabilities[index] || 'online')] }.to_h
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
module Redis::Alfred
|
module Redis::Alfred
|
||||||
CONVERSATION_MAILER_KEY = 'CONVERSATION::%d'.freeze
|
CONVERSATION_MAILER_KEY = 'CONVERSATION::%d'.freeze
|
||||||
|
|
||||||
|
# hash containing user_id key and status as value ONLINE_STATUS::%accountid
|
||||||
|
ONLINE_STATUS = 'ONLINE_STATUS::%s'.freeze
|
||||||
|
# sorted set storing online presense of account contacts : ONLINE_PRESENCE::%accountid::CONTACTS
|
||||||
|
ONLINE_PRESENCE_CONTACTS = 'ONLINE_PRESENCE::%s::CONTACTS'.freeze
|
||||||
|
# sorted set storing online presense of account users : ONLINE_PRESENCE::%accountid::USERS
|
||||||
|
ONLINE_PRESENCE_USERS = 'ONLINE_PRESENCE::%s::USERS'.freeze
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def rpoplpush(source, destination)
|
def rpoplpush(source, destination)
|
||||||
$alfred.rpoplpush(source, destination)
|
$alfred.rpoplpush(source, destination)
|
||||||
@@ -22,8 +29,46 @@ module Redis::Alfred
|
|||||||
$alfred.setex(key, expiry, value)
|
$alfred.setex(key, expiry, value)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set(key, value)
|
||||||
|
$alfred.set(key, value)
|
||||||
|
end
|
||||||
|
|
||||||
def get(key)
|
def get(key)
|
||||||
$alfred.get(key)
|
$alfred.get(key)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# hash operation
|
||||||
|
|
||||||
|
# add a key value to redis hash
|
||||||
|
def hset(key, field, value)
|
||||||
|
$alfred.hset(key, field, value)
|
||||||
|
end
|
||||||
|
|
||||||
|
# get value from redis hash
|
||||||
|
def hget(key, field)
|
||||||
|
$alfred.hget(key, field)
|
||||||
|
end
|
||||||
|
|
||||||
|
# get values of multiple keys from redis hash
|
||||||
|
def hmget(key, fields)
|
||||||
|
$alfred.hmget(key, *fields)
|
||||||
|
end
|
||||||
|
|
||||||
|
# sorted set functions
|
||||||
|
|
||||||
|
# add score and value for a key
|
||||||
|
def zadd(key, score, value)
|
||||||
|
$alfred.zadd(key, score, value)
|
||||||
|
end
|
||||||
|
|
||||||
|
# get score of a value for key
|
||||||
|
def zscore(key, value)
|
||||||
|
$alfred.zscore(key, value)
|
||||||
|
end
|
||||||
|
|
||||||
|
# get values by score
|
||||||
|
def zrangebyscore(key, range_start, range_end)
|
||||||
|
$alfred.zrangebyscore(key, range_start, range_end)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe RoomChannel, type: :channel do
|
RSpec.describe RoomChannel, type: :channel do
|
||||||
let!(:user) { create(:user) }
|
let!(:contact) { create(:contact) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_connection
|
stub_connection
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'subscribes to a stream when pubsub_token is provided' do
|
it 'subscribes to a stream when pubsub_token is provided' do
|
||||||
subscribe(pubsub_token: user.uid)
|
subscribe(pubsub_token: contact.pubsub_token)
|
||||||
expect(subscription).to be_confirmed
|
expect(subscription).to be_confirmed
|
||||||
expect(subscription).to have_stream_for(user.uid)
|
expect(subscription).to have_stream_for(contact.pubsub_token)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -77,6 +77,16 @@ RSpec.describe 'Profile API', type: :request do
|
|||||||
agent.reload
|
agent.reload
|
||||||
expect(agent.avatar.attached?).to eq(true)
|
expect(agent.avatar.attached?).to eq(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'updates the availability status' do
|
||||||
|
put '/api/v1/profile',
|
||||||
|
params: { profile: { availability: 'offline' } },
|
||||||
|
headers: agent.create_new_auth_token,
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
expect(::OnlineStatusTracker.get_status(account.id, agent.id)).to eq('offline')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user