feat: conversation participants (#4145)

Fixes #241
Fixes: chatwoot/product#648

Co-authored-by: Aswin Dev P.S <aswindevps@gmail.com>
Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
Sojan Jose
2023-02-16 13:35:06 +05:30
committed by GitHub
co-authored by Aswin Dev P.S Nithin David Thomas Muhsin Keloth Pranav Raj S
parent 7be2ef3292
commit ca1adb9960
26 changed files with 486 additions and 122 deletions
@@ -39,6 +39,24 @@
/>
</accordion-item>
</div>
<div
v-else-if="element.name === 'conversation_participants'"
class="conversation--actions"
>
<accordion-item
:title="$t('CONVERSATION_PARTICIPANTS.SIDEBAR_TITLE')"
:is-open="isContactSidebarItemOpen('is_conv_participants_open')"
@click="
value =>
toggleSidebarUIState('is_conv_participants_open', value)
"
>
<conversation-participant
:conversation-id="conversationId"
:inbox-id="inboxId"
/>
</accordion-item>
</div>
<div v-else-if="element.name === 'conversation_info'">
<accordion-item
:title="$t('CONVERSATION_SIDEBAR.ACCORDION.CONVERSATION_INFO')"
@@ -118,6 +136,7 @@ import alertMixin from 'shared/mixins/alertMixin';
import AccordionItem from 'dashboard/components/Accordion/AccordionItem';
import ContactConversations from './ContactConversations.vue';
import ConversationAction from './ConversationAction.vue';
import ConversationParticipant from './ConversationParticipant.vue';
import ContactInfo from './contact/ContactInfo';
import ConversationInfo from './ConversationInfo';
@@ -136,6 +155,7 @@ export default {
CustomAttributes,
CustomAttributeSelector,
ConversationAction,
ConversationParticipant,
draggable,
MacrosList,
},
@@ -0,0 +1,270 @@
<template>
<div class="watchers-wrap">
<div class="watchers--collapsed">
<div class="content-wrap">
<div>
<p v-if="watchersList.length" class="total-watchers message-text">
<spinner v-if="watchersUiFlas.isFetching" size="tiny" />
{{ totalWatchersText }}
</p>
<p v-else class="text-muted message-text">
{{ $t('CONVERSATION_PARTICIPANTS.NO_PARTICIPANTS_TEXT') }}
</p>
</div>
<woot-button
v-tooltip.left="$t('CONVERSATION_PARTICIPANTS.ADD_PARTICIPANTS')"
:title="$t('CONVERSATION_PARTICIPANTS.ADD_PARTICIPANTS')"
icon="settings"
size="tiny"
variant="smooth"
color-scheme="secondary"
@click="onOpenDropdown"
/>
</div>
</div>
<div class="actions">
<thumbnail-group
:more-thumbnails-text="moreThumbnailsText"
:show-more-thumbnails-count="showMoreThumbs"
:users-list="thumbnailList"
/>
<p v-if="isUserWatching" class="text-muted message-text">
{{ $t('CONVERSATION_PARTICIPANTS.YOU_ARE_WATCHING') }}
</p>
<woot-button
v-else
icon="arrow-right"
variant="link"
size="small"
@click="onSelfAssign"
>
{{ $t('CONVERSATION_PARTICIPANTS.WATCH_CONVERSATION') }}
</woot-button>
</div>
<div
v-on-clickaway="
() => {
onCloseDropdown();
}
"
:class="{ 'dropdown-pane--open': showDropDown }"
class="dropdown-pane"
>
<div class="dropdown__header">
<h4 class="text-block-title text-truncate">
{{ $t('CONVERSATION_PARTICIPANTS.ADD_PARTICIPANTS') }}
</h4>
<woot-button
icon="dismiss"
size="tiny"
color-scheme="secondary"
variant="clear"
@click="onCloseDropdown"
/>
</div>
<multiselect-dropdown-items
:options="agentsList"
:selected-items="selectedWatchers"
:has-thumbnail="true"
@click="onClickItem"
/>
</div>
</div>
</template>
<script>
import { mixin as clickaway } from 'vue-clickaway';
import Spinner from 'shared/components/Spinner';
import alertMixin from 'shared/mixins/alertMixin';
import { mapGetters } from 'vuex';
import agentMixin from 'dashboard/mixins/agentMixin';
import ThumbnailGroup from 'dashboard/components/widgets/ThumbnailGroup';
import MultiselectDropdownItems from 'shared/components/ui/MultiselectDropdownItems';
export default {
components: {
Spinner,
ThumbnailGroup,
MultiselectDropdownItems,
},
mixins: [alertMixin, agentMixin, clickaway],
props: {
conversationId: {
type: [Number, String],
required: true,
},
inboxId: {
type: Number,
default: undefined,
},
},
data() {
return {
selectedWatchers: [],
showDropDown: false,
};
},
computed: {
...mapGetters({
watchersUiFlas: 'conversationWatchers/getUIFlags',
currentUser: 'getCurrentUser',
}),
watchersFromStore() {
return this.$store.getters['conversationWatchers/getByConversationId'](
this.conversationId
);
},
watchersList: {
get() {
return this.selectedWatchers;
},
set(participants) {
this.selectedWatchers = [...participants];
const userIds = participants.map(el => el.id);
this.updateParticipant(userIds);
},
},
isUserWatching() {
return this.selectedWatchers.some(
watcher => watcher.id === this.currentUser.id
);
},
thumbnailList() {
return this.selectedWatchers.slice(0, 4);
},
moreAgentCount() {
const maxThumbnailCount = 4;
return this.watchersList.length - maxThumbnailCount;
},
moreThumbnailsText() {
if (this.moreAgentCount > 1) {
return this.$t('CONVERSATION_PARTICIPANTS.REMANING_PARTICIPANTS_TEXT', {
count: this.moreAgentCount,
});
}
return this.$t('CONVERSATION_PARTICIPANTS.REMANING_PARTICIPANT_TEXT', {
count: 1,
});
},
showMoreThumbs() {
return this.moreAgentCount > 0;
},
totalWatchersText() {
if (this.selectedWatchers.length > 1) {
return this.$t('CONVERSATION_PARTICIPANTS.TOTAL_PARTICIPANTS_TEXT', {
count: this.selectedWatchers.length,
});
}
return this.$t('CONVERSATION_PARTICIPANTS.TOTAL_PARTICIPANT_TEXT', {
count: 1,
});
},
},
watch: {
conversationId() {
this.fetchParticipants();
},
watchersFromStore(participants) {
this.selectedWatchers = [...participants];
},
},
mounted() {
this.fetchParticipants();
this.$store.dispatch('agents/get');
},
methods: {
fetchParticipants() {
const conversationId = this.conversationId;
this.$store.dispatch('conversationWatchers/show', { conversationId });
},
async updateParticipant(userIds) {
const conversationId = this.conversationId;
let alertMessage = this.$t(
'CONVERSATION_PARTICIPANTS.API.SUCCESS_MESSAGE'
);
try {
await this.$store.dispatch('conversationWatchers/update', {
conversationId,
userIds,
});
} catch (error) {
alertMessage =
error?.message ||
this.$t('CONVERSATION_PARTICIPANTS.API.ERROR_MESSAGE');
} finally {
this.showAlert(alertMessage);
}
this.fetchParticipants();
},
onOpenDropdown() {
this.showDropDown = true;
},
onCloseDropdown() {
this.showDropDown = false;
},
onClickItem(agent) {
const isAgentSelected = this.watchersList.some(
participant => participant.id === agent.id
);
if (isAgentSelected) {
const updatedList = this.watchersList.filter(
participant => participant.id !== agent.id
);
this.watchersList = [...updatedList];
} else {
this.watchersList = [...this.watchersList, agent];
}
},
onSelfAssign() {
this.watchersList = [...this.selectedWatchers, this.currentUser];
},
},
};
</script>
<style lang="scss">
.watchers-wrap {
position: relative;
}
.content-wrap {
display: flex;
justify-content: space-between;
width: 100%;
margin-bottom: var(--space-smaller);
}
.watchers--collapsed {
display: flex;
justify-content: space-between;
}
.dropdown-pane {
box-sizing: border-box;
top: var(--space-large);
width: 100%;
}
.dropdown__header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-smaller);
.text-block-title {
margin: 0;
}
}
.actions {
display: flex;
justify-content: space-between;
align-items: center;
}
.message-text {
margin: 0;
font-size: var(--font-size-small);
}
</style>
@@ -121,7 +121,6 @@ export default {
conversationType: 'mention',
}),
},
{
path: frontendURL('accounts/:accountId/unattended/conversations'),
name: 'conversation_unattended',
@@ -141,5 +140,24 @@ export default {
conversationType: 'unattended',
}),
},
{
path: frontendURL('accounts/:accountId/participating/conversations'),
name: 'conversation_participating',
roles: ['administrator', 'agent'],
component: ConversationView,
props: () => ({ conversationType: 'participating' }),
},
{
path: frontendURL(
'accounts/:accountId/participating/conversations/:conversationId'
),
name: 'conversation_through_participating',
roles: ['administrator', 'agent'],
component: ConversationView,
props: route => ({
conversationId: route.params.conversationId,
conversationType: 'participating',
}),
},
],
};
@@ -216,6 +216,22 @@
}}
</label>
</div>
<div>
<input
v-model="selectedEmailFlags"
class="notification--checkbox"
type="checkbox"
value="email_participating_conversation_new_message"
@input="handleEmailInput"
/>
<label for="assigned_conversation_new_message">
{{
$t(
'PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.PARTICIPATING_CONVERSATION_NEW_MESSAGE'
)
}}
</label>
</div>
</div>
</div>
<div
@@ -315,6 +331,23 @@
}}
</label>
</div>
<div>
<input
v-model="selectedPushFlags"
class="notification--checkbox"
type="checkbox"
value="push_participating_conversation_new_message"
@input="handlePushInput"
/>
<label for="assigned_conversation_new_message">
{{
$t(
'PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.PARTICIPATING_CONVERSATION_NEW_MESSAGE'
)
}}
</label>
</div>
</div>
</div>
</div>