fix: Render articles in widget if it is available (#7654)

Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Sojan <sojan@pepalo.com>
This commit is contained in:
Pranav Raj S
2023-09-14 07:26:17 -07:00
committed by GitHub
parent 616371adbb
commit 94a20af9db
19 changed files with 300 additions and 95 deletions

View File

@@ -2,6 +2,7 @@
"arrow-clockwise-outline": "M12 4.75a7.25 7.25 0 1 0 7.201 6.406c-.068-.588.358-1.156.95-1.156.515 0 .968.358 1.03.87a9.25 9.25 0 1 1-3.432-6.116V4.25a1 1 0 1 1 2.001 0v2.698l.034.052h-.034v.25a1 1 0 0 1-1 1h-3a1 1 0 1 1 0-2h.666A7.219 7.219 0 0 0 12 4.75Z",
"arrow-right-outline": "M13.267 4.209a.75.75 0 0 0-1.034 1.086l6.251 5.955H3.75a.75.75 0 0 0 0 1.5h14.734l-6.251 5.954a.75.75 0 0 0 1.034 1.087l7.42-7.067a.996.996 0 0 0 .3-.58.758.758 0 0 0-.001-.29.995.995 0 0 0-.3-.578l-7.419-7.067Z",
"attach-outline": "M11.772 3.743a6 6 0 0 1 8.66 8.302l-.19.197-8.8 8.798-.036.03a3.723 3.723 0 0 1-5.489-4.973.764.764 0 0 1 .085-.13l.054-.06.086-.088.142-.148.002.003 7.436-7.454a.75.75 0 0 1 .977-.074l.084.073a.75.75 0 0 1 .074.976l-.073.084-7.594 7.613a2.23 2.23 0 0 0 3.174 3.106l8.832-8.83A4.502 4.502 0 0 0 13 4.644l-.168.16-.013.014-9.536 9.536a.75.75 0 0 1-1.133-.977l.072-.084 9.549-9.55h.002Z",
"chat-outline": "M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10a9.96 9.96 0 0 1-4.587-1.112l-3.826 1.067a1.25 1.25 0 0 1-1.54-1.54l1.068-3.823A9.96 9.96 0 0 1 2 12C2 6.477 6.477 2 12 2Zm0 1.5A8.5 8.5 0 0 0 3.5 12c0 1.47.373 2.883 1.073 4.137l.15.27-1.112 3.984 3.987-1.112.27.15A8.5 8.5 0 1 0 12 3.5ZM8.75 13h4.498a.75.75 0 0 1 .102 1.493l-.102.007H8.75a.75.75 0 0 1-.102-1.493L8.75 13h4.498H8.75Zm0-3.5h6.505a.75.75 0 0 1 .101 1.493l-.101.007H8.75a.75.75 0 0 1-.102-1.493L8.75 9.5h6.505H8.75Z",
"checkmark-outline": "M4.53 12.97a.75.75 0 0 0-1.06 1.06l4.5 4.5a.75.75 0 0 0 1.06 0l11-11a.75.75 0 0 0-1.06-1.06L8.5 16.94l-3.97-3.97Z",
"chevron-down-outline": "M4.22 8.47a.75.75 0 0 1 1.06 0L12 15.19l6.72-6.72a.75.75 0 1 1 1.06 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L4.22 9.53a.75.75 0 0 1 0-1.06Z",
"chevron-left-outline": "M15.53 4.22a.75.75 0 0 1 0 1.06L8.81 12l6.72 6.72a.75.75 0 1 1-1.06 1.06l-7.25-7.25a.75.75 0 0 1 0-1.06l7.25-7.25a.75.75 0 0 1 1.06 0Z",

View File

@@ -1,6 +1,6 @@
<template>
<div>
<h3 class="text-base font-medium text-slate-900 dark:text-slate-50 mb-0">
<h3 class="text-sm font-medium text-slate-800 dark:text-slate-50 mb-0">
{{ title }}
</h3>
<article-list :articles="articles" @click="onArticleClick" />

View File

@@ -1,7 +1,7 @@
<template>
<category-card
:title="$t('PORTAL.POPULAR_ARTICLES')"
:articles="articles.slice(0, 4)"
:articles="articles.slice(0, 6)"
@view-all="$emit('view-all')"
@view="onArticleClick"
/>

View File

@@ -1,7 +1,6 @@
<template>
<li
class="py-1 flex items-center justify-between -mx-1 px-1 hover:bg-slate-75 dark:hover:bg-slate-600 rounded cursor-pointer text-slate-700 dark:text-slate-50 dark:hover:text-slate-25 hover:text-slate-900 "
@click="onClick"
class="py-1 flex items-center justify-between -mx-1 px-1 hover:bg-slate-25 dark:hover:bg-slate-600 rounded cursor-pointer text-slate-700 dark:text-slate-50 dark:hover:text-slate-25 hover:text-slate-900 "
>
<button
class="underline-offset-2 text-sm leading-6 text-left"

View File

@@ -16,7 +16,7 @@ export default {
},
computed: {
users() {
return this.agents.slice(0, 5).map(agent => ({
return this.agents.slice(0, 4).map(agent => ({
id: agent.id,
avatar: agent.avatar_url,
name: agent.name,

View File

@@ -4,7 +4,11 @@
:class="$dm('bg-white', 'dark:bg-slate-900')"
>
<div class="flex items-center">
<button v-if="showBackButton" @click="onBackButtonClick">
<button
v-if="showBackButton"
class="-ml-3 px-2"
@click="onBackButtonClick"
>
<fluent-icon
icon="chevron-left"
size="24"

View File

@@ -1,7 +1,6 @@
<template>
<header
class="header-expanded py-6 px-5 relative box-border w-full"
:class="$dm('bg-white', 'dark:bg-slate-900')"
class="header-expanded pt-6 pb-4 px-5 relative box-border w-full bg-transparent"
>
<div
class="flex items-start"
@@ -13,23 +12,25 @@
:src="avatarUrl"
alt="Avatar"
/>
<header-actions :show-popout-button="showPopoutButton" />
<header-actions
:show-popout-button="showPopoutButton"
:show-end-conversation-button="false"
/>
</div>
<h2
v-dompurify-html="introHeading"
class="mt-5 text-3xl mb-3 leading-8 font-normal"
class="mt-4 text-2xl mb-1.5 font-medium"
:class="$dm('text-slate-900', 'dark:text-slate-50')"
/>
<p
v-dompurify-html="introBody"
class="text-lg leading-normal"
class="text-base leading-normal"
:class="$dm('text-slate-700', 'dark:text-slate-200')"
/>
</header>
</template>
<script>
import { mapGetters } from 'vuex';
import HeaderActions from './HeaderActions';
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
@@ -57,10 +58,5 @@ export default {
default: false,
},
},
computed: {
...mapGetters({
widgetColor: 'appConfig/getWidgetColor',
}),
},
};
</script>

View File

@@ -0,0 +1,61 @@
<template>
<button
type="button"
class="flex w-full justify-between items-center rounded-md ring-1 ring-inset ring-slate-50 px-2 py-2 text-sm text-slate-700 bg-slate-25 hover:bg-slate-50 dark:text-white dark:bg-slate-800 dark:ring-slate-900 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-woot-600 group"
@click="$emit('continue')"
>
<div
class="w-10 h-10 rounded-md bg-slate-75 dark:bg-slate-700 text-lg flex items-center justify-center flex-shrink-0"
>
<fluent-icon
icon="chat"
size="16"
class="text-slate-600 dark:text-slate-400"
/>
</div>
<div
class="text-left flex flex-col justify-start flex-grow max-w-[calc(100%-80px)] mx-2 group-hover:opacity-75"
>
<h5 class="font-medium text-slate-900 dark:text-white">
{{ title }}
</h5>
<p class="h-4 leading-4 flex items-center gap-1">
<span
v-if="unreadCount > 0"
class="inline-flex items-center justify-center rounded-full bg-green-200 px-1 min-w-[16px] leading-4 text-xxs font-medium text-green-700 mr-0.5"
>
{{ unreadCount }}
</span>
<span
v-dompurify-html="content"
class="leading-4 h-4 text-ellipsis overflow-hidden whitespace-nowrap dark:text-slate-25"
/>
</p>
</div>
<div class="w-8 h-10 flex items-center justify-center">
<fluent-icon icon="chevron-right" />
</div>
</button>
</template>
<script>
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
export default {
components: { FluentIcon },
props: {
title: {
type: String,
default: 'Continue your chat',
},
content: {
type: String,
default: 'Chat with us',
},
unreadCount: {
type: Number,
default: 0,
},
},
};
</script>

View File

@@ -10,7 +10,7 @@
"
>
<thumbnail
size="40px"
size="36px"
:username="user.name"
:src="user.avatar"
has-border

View File

@@ -1,7 +1,11 @@
<template>
<div v-if="showHeaderActions" class="actions flex items-center">
<button
v-if="canLeaveConversation && hasEndConversationEnabled"
v-if="
canLeaveConversation &&
hasEndConversationEnabled &&
showEndConversationButton
"
class="button transparent compact"
:title="$t('END_CONVERSATION')"
@click="resolveConversation"
@@ -56,6 +60,10 @@ export default {
type: Boolean,
default: false,
},
showEndConversationButton: {
type: Boolean,
default: true,
},
},
computed: {
...mapGetters({

View File

@@ -1,34 +1,34 @@
<template>
<div class="px-5">
<div class="flex items-center justify-between mb-4">
<div
class="max-w-xs"
:class="$dm('text-black-700', 'dark:text-slate-50')"
>
<div class="text-base leading-5 font-medium mb-1">
<div class="p-4 shadow-sm rounded-md bg-white dark:bg-slate-700">
<div class="flex items-center justify-between ">
<div class=" ">
<div class="text-sm font-medium text-slate-700 dark:text-slate-50">
{{
isOnline
? $t('TEAM_AVAILABILITY.ONLINE')
: $t('TEAM_AVAILABILITY.OFFLINE')
}}
</div>
<div class="text-xs leading-3 mt-1">
<div class="text-sm mt-1 text-slate-500 dark:text-slate-100">
{{ replyWaitMessage }}
</div>
</div>
<available-agents v-if="isOnline" :agents="availableAgents" />
</div>
<custom-button
class="font-medium"
block
:bg-color="widgetColor"
:text-color="textColor"
<button
class="inline-flex text-sm font-medium rounded-md py-1 mt-2 px-2 -ml-2 leading-6 text-slate-800 dark:text-slate-50 justify-between items-center hover:bg-slate-25 dark:hover:bg-slate-800"
:style="{ color: widgetColor }"
@click="startConversation"
>
<span class="pr-2 text-sm">
{{
hasConversation ? $t('CONTINUE_CONVERSATION') : $t('START_CONVERSATION')
hasConversation
? $t('CONTINUE_CONVERSATION')
: $t('START_CONVERSATION')
}}
</custom-button>
</span>
<fluent-icon icon="arrow-right" size="14" />
</button>
</div>
</template>
@@ -37,18 +37,16 @@ import { mapGetters } from 'vuex';
import { getContrastingTextColor } from '@chatwoot/utils';
import nextAvailabilityTime from 'widget/mixins/nextAvailabilityTime';
import AvailableAgents from 'widget/components/AvailableAgents.vue';
import CustomButton from 'shared/components/Button';
import configMixin from 'widget/mixins/configMixin';
import availabilityMixin from 'widget/mixins/availability';
import darkMixin from 'widget/mixins/darkModeMixin.js';
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
export default {
name: 'TeamAvailability',
components: {
AvailableAgents,
CustomButton,
FluentIcon,
},
mixins: [configMixin, nextAvailabilityTime, availabilityMixin, darkMixin],
mixins: [configMixin, nextAvailabilityTime, availabilityMixin],
props: {
availableAgents: {
type: Array,
@@ -58,6 +56,10 @@ export default {
type: Boolean,
default: false,
},
unreadCount: {
type: Number,
default: 0,
},
},
computed: {

View File

@@ -1,23 +1,18 @@
<template>
<div
class="w-full h-full flex flex-col"
:class="$dm('bg-slate-50', 'dark:bg-slate-800')"
class="w-full h-full bg-slate-25 dark:bg-slate-800"
:class="{ 'overflow-auto': isOnHomeView }"
@keydown.esc="closeWindow"
>
<div class="flex flex-col h-full relative">
<div
class="header-wrap"
class="header-wrap sticky top-0 z-40 transition-all"
:class="{
expanded: !isHeaderCollapsed,
collapsed: isHeaderCollapsed,
'custom-header-shadow': isHeaderCollapsed,
...opacityClass,
}"
>
<transition
enter-active-class="transition-all delay-200 duration-300 ease-in"
leave-active-class="transition-all duration-200 ease-out"
enter-class="opacity-0"
enter-to-class="opacity-100"
leave-class="opacity-100"
leave-to-class="opacity-0"
>
<chat-header-expanded
v-if="!isHeaderCollapsed"
@@ -32,24 +27,14 @@
:avatar-url="channelConfig.avatarUrl"
:show-popout-button="appConfig.showPopoutButton"
:available-agents="availableAgents"
:show-back-button="showBackButton"
/>
</transition>
</div>
<banner />
<transition
enter-active-class="transition-all delay-300 duration-300 ease-in"
leave-active-class="transition-all duration-200 ease-out"
enter-class="opacity-0"
enter-to-class="opacity-100"
leave-class="opacity-100"
leave-to-class="opacity-0"
>
<router-view />
</transition>
<branding
class="bg-slate-25 dark:bg-slate-800"
:disable-branding="disableBranding"
/>
<branding v-if="!isOnArticleViewer" :disable-branding="disableBranding" />
</div>
</div>
</template>
<script>
@@ -58,7 +43,6 @@ import Branding from 'shared/components/Branding.vue';
import ChatHeader from '../ChatHeader.vue';
import ChatHeaderExpanded from '../ChatHeaderExpanded.vue';
import configMixin from '../../mixins/configMixin';
import darkModeMixin from 'widget/mixins/darkModeMixin';
import { mapGetters } from 'vuex';
import { IFrameHelper } from 'widget/helpers/utils';
@@ -69,34 +53,88 @@ export default {
ChatHeader,
ChatHeaderExpanded,
},
mixins: [configMixin, darkModeMixin],
mixins: [configMixin],
data() {
return {
showPopoutButton: false,
scrollPosition: 0,
ticking: true,
disableBranding: window.chatwootWebChannel.disableBranding || false,
};
},
computed: {
...mapGetters({
availableAgents: 'agent/availableAgents',
appConfig: 'appConfig/getAppConfig',
availableAgents: 'agent/availableAgents',
widgetColor: 'appConfig/getWidgetColor',
}),
portal() {
return window.chatwootWebChannel.portal;
},
isHeaderCollapsed() {
if (!this.hasIntroText) {
return true;
}
return this.$route.name !== 'home';
return !this.isOnHomeView;
},
hasIntroText() {
return (
this.channelConfig.welcomeTitle || this.channelConfig.welcomeTagline
);
},
showBackButton() {
return ['article-viewer', 'messages', 'prechat-form'].includes(
this.$route.name
);
},
isOnArticleViewer() {
return ['article-viewer'].includes(this.$route.name);
},
isOnHomeView() {
return ['home'].includes(this.$route.name);
},
opacityClass() {
if (this.isHeaderCollapsed) {
return {};
}
if (this.scrollPosition > 30) {
return { 'opacity-30': true };
}
if (this.scrollPosition > 25) {
return { 'opacity-40': true };
}
if (this.scrollPosition > 20) {
return { 'opacity-60': true };
}
if (this.scrollPosition > 15) {
return { 'opacity-80': true };
}
if (this.scrollPosition > 10) {
return { 'opacity-90': true };
}
return {};
},
},
mounted() {
this.$el.addEventListener('scroll', this.updateScrollPosition);
},
beforeDestroy() {
this.$el.removeEventListener('scroll', this.updateScrollPosition);
},
methods: {
closeWindow() {
IFrameHelper.sendMessage({ event: 'closeWindow' });
},
updateScrollPosition(event) {
this.scrollPosition = event.target.scrollTop;
if (!this.ticking) {
window.requestAnimationFrame(() => {
this.ticking = false;
});
this.ticking = true;
}
},
},
};
</script>
@@ -105,11 +143,13 @@ export default {
@import '~widget/assets/scss/variables';
@import '~widget/assets/scss/mixins';
.custom-header-shadow {
@include shadow-large;
}
.header-wrap {
flex-shrink: 0;
transition: max-height 300ms;
z-index: 99;
@include shadow-large;
transition: max-height 100ms;
&.expanded {
max-height: 16rem;

View File

@@ -34,7 +34,9 @@
"START_CONVERSATION": "Start Conversation",
"END_CONVERSATION": "End Conversation",
"CONTINUE_CONVERSATION": "Continue conversation",
"YOU": "You",
"START_NEW_CONVERSATION": "Start a new conversation",
"VIEW_UNREAD_MESSAGES": "You have unread messages",
"UNREAD_VIEW": {
"VIEW_MESSAGES_BUTTON": "See new messages",
"CLOSE_MESSAGES_BUTTON": "Close",
@@ -108,6 +110,27 @@
},
"PORTAL": {
"POPULAR_ARTICLES": "Popular Articles",
"VIEW_ALL_ARTICLES": "View all articles"
"VIEW_ALL_ARTICLES": "View all articles",
"IFRAME_LOAD_ERROR": "There was an error loading the article, please refresh the page and try again."
},
"ATTACHMENTS": {
"image": {
"CONTENT": "Picture message"
},
"audio": {
"CONTENT": "Audio message"
},
"video": {
"CONTENT": "Video message"
},
"file": {
"CONTENT": "File Attachment"
},
"location": {
"CONTENT": "Location"
},
"fallback": {
"CONTENT": "has shared a url"
}
}
}

View File

@@ -36,6 +36,12 @@ export default new Router({
name: 'messages',
component: () => import('./views/Messages.vue'),
},
{
path: '/article',
name: 'article-viewer',
props: true,
component: () => import('./views/ArticleViewer.vue'),
},
],
},
],

View File

@@ -1,25 +1,53 @@
<template>
<div class="flex flex-1 flex-col justify-end">
<div class="flex flex-1 overflow-auto">
<!-- Load Conversation List Components Here -->
</div>
<div
class="z-50 rounded-md w-full flex flex-1 flex-col"
:class="{ 'pb-2': showArticles, 'justify-end': !showArticles }"
>
<div class="px-4 pt-4 w-full">
<team-availability
:available-agents="availableAgents"
:has-conversation="!!conversationSize"
:unread-count="unreadMessageCount"
@start-conversation="startConversation"
/>
</div>
<div v-if="showArticles" class="px-4 py-2 w-full">
<div class="p-4 rounded-md bg-white dark:bg-slate-700 shadow-sm w-full">
<article-hero
v-if="
!articleUiFlags.isFetching &&
!articleUiFlags.isError &&
popularArticles.length
"
:articles="popularArticles"
@view="openArticleInArticleViewer"
@view-all="viewAllArticles"
/>
</div>
</div>
<div v-if="articleUiFlags.isFetching" class="px-4 py-2 w-full">
<div class="p-4 rounded-md bg-white dark:bg-slate-700 shadow-sm w-full">
<article-card-skeleton-loader />
</div>
</div>
</div>
</template>
<script>
import configMixin from '../mixins/configMixin';
import TeamAvailability from 'widget/components/TeamAvailability';
import ArticleHero from 'widget/components/ArticleHero';
import ArticleCardSkeletonLoader from 'widget/components/ArticleCardSkeletonLoader';
import { mapGetters } from 'vuex';
import routerMixin from 'widget/mixins/routerMixin';
import configMixin from 'widget/mixins/configMixin';
export default {
name: 'Home',
components: {
ArticleHero,
TeamAvailability,
ArticleCardSkeletonLoader,
},
mixins: [configMixin, routerMixin],
props: {
@@ -32,15 +60,33 @@ export default {
default: false,
},
},
data() {
return {};
},
computed: {
...mapGetters({
availableAgents: 'agent/availableAgents',
activeCampaign: 'campaign/getActiveCampaign',
conversationSize: 'conversation/getConversationSize',
unreadMessageCount: 'conversation/getUnreadMessageCount',
popularArticles: 'article/popularArticles',
articleUiFlags: 'article/uiFlags',
}),
portal() {
return window.chatwootWebChannel.portal;
},
showArticles() {
return (
this.portal &&
!this.articleUiFlags.isFetching &&
this.popularArticles.length
);
},
},
mounted() {
if (this.portal && this.popularArticles.length === 0) {
this.$store.dispatch('article/fetch', {
slug: this.portal.slug,
locale: 'en',
});
}
},
methods: {
startConversation() {
@@ -49,6 +95,19 @@ export default {
}
return this.replaceRoute('messages');
},
openArticleInArticleViewer(link) {
this.$router.push({
name: 'article-viewer',
params: { link: `${link}?show_plain_layout=true` },
});
},
viewAllArticles() {
const {
portal: { slug },
locale = 'en',
} = window.chatwootWebChannel;
this.openArticleInArticleViewer(`/hc/${slug}/${locale}`);
},
},
};
</script>

View File

@@ -1,5 +1,7 @@
<template>
<div class="flex flex-col flex-1 overflow-hidden">
<div
class="flex flex-col flex-1 overflow-hidden rounded-b-lg bg-slate-25 dark:bg-slate-800"
>
<div class="flex flex-1 overflow-auto">
<conversation-wrap :grouped-messages="groupedMessages" />
</div>
@@ -10,6 +12,7 @@
</template>
<script>
import { mapGetters } from 'vuex';
import ChatFooter from '../components/ChatFooter.vue';
import ConversationWrap from '../components/ConversationWrap.vue';

View File

@@ -38,3 +38,5 @@ json.associated_articles do
end
end
end
json.link "hc/#{article.portal.slug}/articles/#{article.slug}"

View File

@@ -19,7 +19,7 @@ window.chatwootSettings = {
locale: 'en',
useBrowserLanguage: true,
type: '<%= @widget_type %>',
showPopoutButton: true,
// showPopoutButton: true,
widgetStyle: '<%= @widget_style %>',
darkMode: '<%= @dark_mode %>',
};

View File

@@ -14,6 +14,7 @@
welcomeTagline: '<%= @web_widget.welcome_tagline %>',
welcomeTitle: '<%= @web_widget.welcome_title %>',
widgetColor: '<%= @web_widget.widget_color %>',
portal: <%= @web_widget.inbox.portal.to_json.html_safe %>,
enabledFeatures: <%= @web_widget.selected_feature_flags.to_json.html_safe %>,
enabledLanguages: <%= available_locales_with_name.to_json.html_safe %>,
replyTime: '<%= @web_widget.reply_time %>',