chore: Replace darkmode mixin with useDarkMode composable [CW-3474] (#9949)

# Pull Request Template

## Description

Replaces darkModeMixin with the new useDarkMode composable and replaces
wll usages of mixin the the composable in components and pages

Fixes
https://linear.app/chatwoot/issue/CW-3474/rewrite-darkmodemixin-mixin-to-a-composable

## Type of change

Please delete options that are not relevant.

- [x] New feature (non-breaking change which adds functionality)

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
This commit is contained in:
Fayaz Ahmed
2024-09-12 00:29:41 +05:30
committed by GitHub
parent 2c17c95eab
commit a76cd7684a
27 changed files with 357 additions and 207 deletions

View File

@@ -1,12 +1,11 @@
<script>
import CardButton from 'shared/components/CardButton.vue';
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
import { useDarkMode } from 'widget/composables/useDarkMode';
export default {
components: {
CardButton,
},
mixins: [darkModeMixin],
props: {
title: {
type: String,
@@ -25,21 +24,30 @@ export default {
default: () => [],
},
},
computed: {},
setup() {
const { getThemeClass } = useDarkMode();
return { getThemeClass };
},
};
</script>
<template>
<div
class="card-message chat-bubble agent"
:class="$dm('bg-white', 'dark:bg-slate-700')"
:class="getThemeClass('bg-white', 'dark:bg-slate-700')"
>
<img class="media" :src="mediaUrl" />
<div class="card-body">
<h4 class="title" :class="$dm('text-black-900', 'dark:text-slate-50')">
<h4
class="title"
:class="getThemeClass('text-black-900', 'dark:text-slate-50')"
>
{{ title }}
</h4>
<p class="body" :class="$dm('text-black-700', 'dark:text-slate-100')">
<p
class="body"
:class="getThemeClass('text-black-700', 'dark:text-slate-100')"
>
{{ description }}
</p>
<CardButton v-for="action in actions" :key="action.id" :action="action" />

View File

@@ -1,10 +1,9 @@
<script>
import { mapGetters } from 'vuex';
import { getContrastingTextColor } from '@chatwoot/utils';
import darkModeMixin from 'widget/mixins/darkModeMixin';
import { useDarkMode } from 'widget/composables/useDarkMode';
export default {
mixins: [darkModeMixin],
props: {
buttonLabel: {
type: String,
@@ -19,6 +18,10 @@ export default {
default: () => [],
},
},
setup() {
const { getThemeClass } = useDarkMode();
return { getThemeClass };
},
data() {
return {
formValues: {},
@@ -33,8 +36,8 @@ export default {
return getContrastingTextColor(this.widgetColor);
},
inputColor() {
return `${this.$dm('bg-white', 'dark:bg-slate-600')}
${this.$dm('text-black-900', 'dark:text-slate-50')}`;
return `${this.getThemeClass('bg-white', 'dark:bg-slate-600')}
${this.getThemeClass('text-black-900', 'dark:text-slate-50')}`;
},
isFormValid() {
return this.items.reduce((acc, { name }) => {
@@ -80,7 +83,7 @@ export default {
<template>
<div
class="form chat-bubble agent"
:class="$dm('bg-white', 'dark:bg-slate-700')"
:class="getThemeClass('bg-white', 'dark:bg-slate-700')"
>
<form @submit.prevent="onSubmit">
<div
@@ -91,7 +94,7 @@ export default {
'has-submitted': hasSubmitted,
}"
>
<label :class="$dm('text-black-900', 'dark:text-slate-50')">{{
<label :class="getThemeClass('text-black-900', 'dark:text-slate-50')">{{
item.label
}}</label>
<input

View File

@@ -3,7 +3,7 @@ import { mapGetters } from 'vuex';
import Spinner from 'shared/components/Spinner.vue';
import { CSAT_RATINGS } from 'shared/constants/messages';
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
import darkModeMixin from 'widget/mixins/darkModeMixin';
import { useDarkMode } from 'widget/composables/useDarkMode';
import { getContrastingTextColor } from '@chatwoot/utils';
export default {
@@ -11,7 +11,6 @@ export default {
Spinner,
FluentIcon,
},
mixins: [darkModeMixin],
props: {
messageContentAttributes: {
type: Object,
@@ -22,6 +21,10 @@ export default {
required: true,
},
},
setup() {
const { getThemeClass } = useDarkMode();
return { getThemeClass };
},
data() {
return {
email: '',
@@ -44,8 +47,8 @@ export default {
return !(this.selectedRating && this.feedback);
},
inputColor() {
return `${this.$dm('bg-white', 'dark:bg-slate-600')}
${this.$dm('text-black-900', 'dark:text-slate-50')}`;
return `${this.getThemeClass('bg-white', 'dark:bg-slate-600')}
${this.getThemeClass('text-black-900', 'dark:text-slate-50')}`;
},
textColor() {
return getContrastingTextColor(this.widgetColor);
@@ -105,10 +108,13 @@ export default {
<template>
<div
class="customer-satisfaction"
:class="$dm('bg-white', 'dark:bg-slate-700')"
:class="getThemeClass('bg-white', 'dark:bg-slate-700')"
:style="{ borderColor: widgetColor }"
>
<h6 class="title" :class="$dm('text-slate-900', 'dark:text-slate-50')">
<h6
class="title"
:class="getThemeClass('text-slate-900', 'dark:text-slate-50')"
>
{{ title }}
</h6>
<div class="ratings">

View File

@@ -1,15 +1,18 @@
<script>
import { formatDate } from 'shared/helpers/DateHelper';
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
import { useDarkMode } from 'widget/composables/useDarkMode';
export default {
mixins: [darkModeMixin],
props: {
date: {
type: String,
required: true,
},
},
setup() {
const { getThemeClass } = useDarkMode();
return { getThemeClass };
},
computed: {
formattedDate() {
return formatDate({
@@ -25,7 +28,7 @@ export default {
<template>
<div
class="date--separator"
:class="$dm('text-slate-700', 'dark:text-slate-200')"
:class="getThemeClass('text-slate-700', 'dark:text-slate-200')"
>
{{ formattedDate }}
</div>

View File

@@ -2,7 +2,6 @@ import DateSeparator from '../DateSeparator.vue';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import VueI18n from 'vue-i18n';
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
const localVue = createLocalVue();
import i18n from 'dashboard/i18n';
localVue.use(Vuex);
@@ -40,7 +39,6 @@ describe('dateSeparator', () => {
propsData: { date: 'Nov 18, 2019' },
mocks: { $t: msg => msg },
i18n: i18nConfig,
mixins: [darkModeMixin],
});
});

View File

@@ -18,7 +18,7 @@ import {
ON_CAMPAIGN_MESSAGE_CLICK,
ON_UNREAD_MESSAGE_CLICK,
} from './constants/widgetBusEvents';
import darkModeMixin from 'widget/mixins/darkModeMixin';
import { useDarkMode } from 'widget/composables/useDarkMode';
import { SDK_SET_BUBBLE_VISIBILITY } from '../shared/constants/sharedFrameEvents';
import { emitter } from 'shared/helpers/mitt';
@@ -27,7 +27,11 @@ export default {
components: {
Spinner,
},
mixins: [availabilityMixin, configMixin, routerMixin, darkModeMixin],
mixins: [availabilityMixin, configMixin, routerMixin],
setup() {
const { prefersDarkMode } = useDarkMode();
return { prefersDarkMode };
},
data() {
return {
isMobile: false,

View File

@@ -11,7 +11,7 @@ import { MESSAGE_TYPE } from 'widget/helpers/constants';
import configMixin from '../mixins/configMixin';
import messageMixin from '../mixins/messageMixin';
import { isASubmittedFormMessage } from 'shared/helpers/MessageTypeHelper';
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
import { useDarkMode } from 'widget/composables/useDarkMode';
import ReplyToChip from 'widget/components/ReplyToChip.vue';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import { emitter } from 'shared/helpers/mitt';
@@ -28,7 +28,7 @@ export default {
MessageReplyButton,
ReplyToChip,
},
mixins: [configMixin, messageMixin, darkModeMixin],
mixins: [configMixin, messageMixin],
props: {
message: {
type: Object,
@@ -39,6 +39,12 @@ export default {
default: () => {},
},
},
setup() {
const { getThemeClass } = useDarkMode();
return {
getThemeClass,
};
},
data() {
return {
hasImageError: false,
@@ -192,7 +198,9 @@ export default {
<div
v-if="hasAttachments"
class="space-y-2 chat-bubble has-attachment agent"
:class="(wrapClass, $dm('bg-white', 'dark:bg-slate-700'))"
:class="
(wrapClass, getThemeClass('bg-white', 'dark:bg-slate-700'))
"
>
<div
v-for="attachment in message.attachments"
@@ -231,7 +239,7 @@ export default {
v-if="message.showAvatar || hasRecordedResponse"
v-dompurify-html="agentName"
class="agent-name"
:class="$dm('text-slate-700', 'dark:text-slate-200')"
:class="getThemeClass('text-slate-700', 'dark:text-slate-200')"
/>
</div>
</div>

View File

@@ -6,7 +6,7 @@ import ChatOptions from 'shared/components/ChatOptions.vue';
import ChatArticle from './template/Article.vue';
import EmailInput from './template/EmailInput.vue';
import CustomerSatisfaction from 'shared/components/CustomerSatisfaction.vue';
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
import { useDarkMode } from 'widget/composables/useDarkMode';
import IntegrationCard from './template/IntegrationCard.vue';
export default {
@@ -20,7 +20,6 @@ export default {
CustomerSatisfaction,
IntegrationCard,
},
mixins: [darkModeMixin],
props: {
message: { type: String, default: null },
contentType: { type: String, default: null },
@@ -34,11 +33,13 @@ export default {
setup() {
const { formatMessage, getPlainText, truncateMessage, highlightContent } =
useMessageFormatter();
const { getThemeClass } = useDarkMode();
return {
formatMessage,
getPlainText,
truncateMessage,
highlightContent,
getThemeClass,
};
},
computed: {
@@ -98,7 +99,7 @@ export default {
!isCards && !isOptions && !isForm && !isArticle && !isCards && !isCSAT
"
class="chat-bubble agent"
:class="$dm('bg-white', 'dark:bg-slate-700 has-dark-mode')"
:class="getThemeClass('bg-white', 'dark:bg-slate-700 has-dark-mode')"
>
<div
v-dompurify-html="formatMessage(message, false)"

View File

@@ -1,8 +1,11 @@
<script>
import darkModeMixing from 'widget/mixins/darkModeMixin.js';
import { useDarkMode } from 'widget/composables/useDarkMode';
export default {
name: 'AgentTypingBubble',
mixins: [darkModeMixing],
setup() {
const { getThemeClass } = useDarkMode();
return { getThemeClass };
},
};
</script>
@@ -13,7 +16,7 @@ export default {
<div class="message-wrap mt-2">
<div
class="typing-bubble chat-bubble agent"
:class="$dm('bg-white', 'dark:bg-slate-700')"
:class="getThemeClass('bg-white', 'dark:bg-slate-700')"
>
<img
src="~widget/assets/images/typing.gif"

View File

@@ -4,7 +4,7 @@ import nextAvailabilityTime from 'widget/mixins/nextAvailabilityTime';
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
import HeaderActions from './HeaderActions.vue';
import routerMixin from 'widget/mixins/routerMixin';
import darkMixin from 'widget/mixins/darkModeMixin.js';
import { useDarkMode } from 'widget/composables/useDarkMode';
export default {
name: 'ChatHeader',
@@ -12,7 +12,7 @@ export default {
FluentIcon,
HeaderActions,
},
mixins: [nextAvailabilityTime, availabilityMixin, routerMixin, darkMixin],
mixins: [nextAvailabilityTime, availabilityMixin, routerMixin],
props: {
avatarUrl: {
type: String,
@@ -35,6 +35,10 @@ export default {
default: () => {},
},
},
setup() {
const { getThemeClass } = useDarkMode();
return { getThemeClass };
},
computed: {
isOnline() {
const { workingHoursEnabled } = this.channelConfig;
@@ -57,7 +61,7 @@ export default {
<template>
<header
class="flex justify-between w-full p-5"
:class="$dm('bg-white', 'dark:bg-slate-900')"
:class="getThemeClass('bg-white', 'dark:bg-slate-900')"
>
<div class="flex items-center">
<button
@@ -68,7 +72,7 @@ export default {
<FluentIcon
icon="chevron-left"
size="24"
:class="$dm('text-black-900', 'dark:text-slate-50')"
:class="getThemeClass('text-black-900', 'dark:text-slate-50')"
/>
</button>
<img
@@ -80,7 +84,7 @@ export default {
<div>
<div
class="flex items-center text-base font-medium leading-4"
:class="$dm('text-black-900', 'dark:text-slate-50')"
:class="getThemeClass('text-black-900', 'dark:text-slate-50')"
>
<span v-dompurify-html="title" class="mr-1" />
<div
@@ -90,7 +94,7 @@ export default {
</div>
<div
class="mt-1 text-xs leading-3"
:class="$dm('text-black-700', 'dark:text-slate-400')"
:class="getThemeClass('text-black-700', 'dark:text-slate-400')"
>
{{ replyWaitMessage }}
</div>

View File

@@ -1,13 +1,12 @@
<script>
import HeaderActions from './HeaderActions.vue';
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
import { useDarkMode } from 'widget/composables/useDarkMode';
export default {
name: 'ChatHeaderExpanded',
components: {
HeaderActions,
},
mixins: [darkModeMixin],
props: {
avatarUrl: {
type: String,
@@ -26,6 +25,10 @@ export default {
default: false,
},
},
setup() {
const { getThemeClass } = useDarkMode();
return { getThemeClass };
},
};
</script>
@@ -51,12 +54,12 @@ export default {
<h2
v-dompurify-html="introHeading"
class="mt-4 text-2xl mb-1.5 font-medium"
:class="$dm('text-slate-900', 'dark:text-slate-50')"
:class="getThemeClass('text-slate-900', 'dark:text-slate-50')"
/>
<p
v-dompurify-html="introBody"
class="text-base leading-normal"
:class="$dm('text-slate-700', 'dark:text-slate-200')"
:class="getThemeClass('text-slate-700', 'dark:text-slate-200')"
/>
</header>
</template>

View File

@@ -6,7 +6,7 @@ import ChatSendButton from 'widget/components/ChatSendButton.vue';
import configMixin from '../mixins/configMixin';
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
import ResizableTextArea from 'shared/components/ResizableTextArea.vue';
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
import { useDarkMode } from 'widget/composables/useDarkMode';
const EmojiInput = () => import('shared/components/emoji/EmojiInput.vue');
@@ -19,7 +19,7 @@ export default {
FluentIcon,
ResizableTextArea,
},
mixins: [configMixin, darkModeMixin],
mixins: [configMixin],
props: {
onSendMessage: {
type: Function,
@@ -30,7 +30,10 @@ export default {
default: () => {},
},
},
setup() {
const { getThemeClass } = useDarkMode();
return { getThemeClass };
},
data() {
return {
userInput: '',
@@ -51,13 +54,16 @@ export default {
return this.userInput.length > 0;
},
inputColor() {
return `${this.$dm('bg-white', 'dark:bg-slate-600')}
${this.$dm('text-black-900', 'dark:text-slate-50')}`;
return `${this.getThemeClass('bg-white', 'dark:bg-slate-600')}
${this.getThemeClass('text-black-900', 'dark:text-slate-50')}`;
},
emojiIconColor() {
return this.showEmojiPicker
? `text-woot-500 ${this.$dm('text-black-900', 'dark:text-slate-100')}`
: `${this.$dm('text-black-900', 'dark:text-slate-100')}`;
? `text-woot-500 ${this.getThemeClass(
'text-black-900',
'dark:text-slate-100'
)}`
: `${this.getThemeClass('text-black-900', 'dark:text-slate-100')}`;
},
},
watch: {
@@ -128,7 +134,7 @@ export default {
<template>
<div
class="chat-message--input is-focused"
:class="$dm('bg-white ', 'dark:bg-slate-600')"
:class="getThemeClass('bg-white ', 'dark:bg-slate-600')"
@keydown.esc="hideEmojiPicker"
>
<ResizableTextArea
@@ -148,7 +154,7 @@ export default {
<div class="button-wrap">
<ChatAttachmentButton
v-if="showAttachment"
:class="$dm('text-black-900', 'dark:text-slate-100')"
:class="getThemeClass('text-black-900', 'dark:text-slate-100')"
:on-attach="onSendAttachment"
/>
<button

View File

@@ -3,7 +3,7 @@ import ChatMessage from 'widget/components/ChatMessage.vue';
import AgentTypingBubble from 'widget/components/AgentTypingBubble.vue';
import DateSeparator from 'shared/components/DateSeparator.vue';
import Spinner from 'shared/components/Spinner.vue';
import darkModeMixin from 'widget/mixins/darkModeMixin';
import { useDarkMode } from 'widget/composables/useDarkMode';
import { MESSAGE_TYPE } from 'shared/constants/messages';
import { mapActions, mapGetters } from 'vuex';
@@ -15,13 +15,16 @@ export default {
DateSeparator,
Spinner,
},
mixins: [darkModeMixin],
props: {
groupedMessages: {
type: Array,
default: () => [],
},
},
setup() {
const { darkMode } = useDarkMode();
return { darkMode };
},
data() {
return {
previousScrollHeight: 0,

View File

@@ -1,13 +1,12 @@
<script>
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
import { useDarkMode } from 'widget/composables/useDarkMode';
import { getContrastingTextColor } from '@chatwoot/utils';
export default {
components: {
FluentIcon,
},
mixins: [darkModeMixin],
props: {
url: {
type: String,
@@ -26,6 +25,10 @@ export default {
default: false,
},
},
setup() {
const { getThemeClass } = useDarkMode();
return { getThemeClass };
},
computed: {
title() {
return this.isInProgress
@@ -45,7 +48,7 @@ export default {
},
titleColor() {
return !this.isUserBubble
? this.$dm('text-black-900', 'dark:text-slate-50')
? this.getThemeClass('text-black-900', 'dark:text-slate-50')
: '';
},
},

View File

@@ -1,7 +1,6 @@
<script>
import darkModeMixin from 'widget/mixins/darkModeMixin';
import { useDarkMode } from 'widget/composables/useDarkMode';
export default {
mixins: [darkModeMixin],
props: {
label: {
type: String,
@@ -28,20 +27,33 @@ export default {
default: '',
},
},
setup() {
const { getThemeClass } = useDarkMode();
return { getThemeClass };
},
computed: {
labelClass() {
return this.error
? `text-red-400 ${this.$dm('text-black-800', 'dark:text-slate-50')}`
: `text-black-800 ${this.$dm('text-black-800', 'dark:text-slate-50')}`;
? `text-red-400 ${this.getThemeClass(
'text-black-800',
'dark:text-slate-50'
)}`
: `text-black-800 ${this.getThemeClass(
'text-black-800',
'dark:text-slate-50'
)}`;
},
isInputDarkOrLightMode() {
return `${this.$dm('bg-white', 'dark:bg-slate-600')} ${this.$dm(
'text-slate-700',
'dark:text-slate-50'
)}`;
return `${this.getThemeClass(
'bg-white',
'dark:bg-slate-600'
)} ${this.getThemeClass('text-slate-700', 'dark:text-slate-50')}`;
},
inputBorderColor() {
return `${this.$dm('border-black-200', 'dark:border-black-500')}`;
return `${this.getThemeClass(
'border-black-200',
'dark:border-black-500'
)}`;
},
inputHasError() {
return this.error

View File

@@ -2,13 +2,13 @@
import countries from 'shared/constants/countries.js';
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
import FormulateInputMixin from '@braid/vue-formulate/src/FormulateInputMixin';
import darkModeMixin from 'widget/mixins/darkModeMixin';
import { useDarkMode } from 'widget/composables/useDarkMode';
export default {
components: {
FluentIcon,
},
mixins: [FormulateInputMixin, darkModeMixin],
mixins: [FormulateInputMixin],
props: {
placeholder: {
type: String,
@@ -19,6 +19,10 @@ export default {
default: false,
},
},
setup() {
const { getThemeClass } = useDarkMode();
return { getThemeClass };
},
data() {
return {
selectedIndex: -1,
@@ -45,28 +49,31 @@ export default {
return this.activeCountryCode ? 'Clear selection' : 'Select Country';
},
dropdownClass() {
return `${this.$dm('bg-slate-100', 'dark:bg-slate-700')} ${this.$dm(
'text-slate-700',
'dark:text-slate-50'
)}`;
return `${this.getThemeClass(
'bg-slate-100',
'dark:bg-slate-700'
)} ${this.getThemeClass('text-slate-700', 'dark:text-slate-50')}`;
},
dropdownBackgroundClass() {
return `${this.$dm('bg-white', 'dark:bg-slate-700')} ${this.$dm(
'text-slate-700',
'dark:text-slate-50'
)}`;
return `${this.getThemeClass(
'bg-white',
'dark:bg-slate-700'
)} ${this.getThemeClass('text-slate-700', 'dark:text-slate-50')}`;
},
dropdownItemClass() {
return `${this.$dm('text-slate-700', 'dark:text-slate-50')} ${this.$dm(
'hover:bg-slate-50',
'dark:hover:bg-slate-600'
)}`;
return `${this.getThemeClass(
'text-slate-700',
'dark:text-slate-50'
)} ${this.getThemeClass('hover:bg-slate-50', 'dark:hover:bg-slate-600')}`;
},
activeDropdownItemClass() {
return `active ${this.$dm('bg-slate-100', 'dark:bg-slate-800')}`;
return `active ${this.getThemeClass(
'bg-slate-100',
'dark:bg-slate-800'
)}`;
},
focusedDropdownItemClass() {
return `focus ${this.$dm('bg-slate-50', 'dark:bg-slate-600')}`;
return `focus ${this.getThemeClass('bg-slate-50', 'dark:bg-slate-600')}`;
},
inputHasError() {
return this.hasErrorInPhoneInput
@@ -74,13 +81,16 @@ export default {
: `hover:border-black-300 focus:border-black-300 ${this.inputLightAndDarkModeColor} ${this.inputBorderColor}`;
},
inputBorderColor() {
return `${this.$dm('border-black-200', 'dark:border-black-500')}`;
return `${this.getThemeClass(
'border-black-200',
'dark:border-black-500'
)}`;
},
inputLightAndDarkModeColor() {
return `${this.$dm('bg-white', 'dark:bg-slate-600')} ${this.$dm(
'text-slate-700',
'dark:text-slate-50'
)}`;
return `${this.getThemeClass(
'bg-white',
'dark:bg-slate-600'
)} ${this.getThemeClass('text-slate-700', 'dark:text-slate-50')}`;
},
items() {
return this.countries.filter(country => {
@@ -241,7 +251,7 @@ export default {
<span
v-if="activeDialCode"
class="py-2 pl-2 pr-0 text-base"
:class="$dm('text-slate-700', 'dark:text-slate-50')"
:class="getThemeClass('text-slate-700', 'dark:text-slate-50')"
>
{{ activeDialCode }}
</span>
@@ -273,7 +283,10 @@ export default {
type="text"
:placeholder="$t('PRE_CHAT_FORM.FIELDS.PHONE_NUMBER.DROPDOWN_SEARCH')"
class="w-full h-8 px-3 py-2 mt-1 mb-1 text-sm border border-solid rounded outline-none dropdown-search"
:class="[$dm('bg-slate-50', 'dark:bg-slate-600'), inputBorderColor]"
:class="[
getThemeClass('bg-slate-50', 'dark:bg-slate-600'),
inputBorderColor,
]"
/>
</div>
<div
@@ -298,7 +311,7 @@ export default {
<div v-if="items.length === 0">
<span
class="flex justify-center mt-4 text-sm text-center"
:class="$dm('text-slate-700', 'dark:text-slate-50')"
:class="getThemeClass('text-slate-700', 'dark:text-slate-50')"
>
{{ $t('PRE_CHAT_FORM.FIELDS.PHONE_NUMBER.DROPDOWN_EMPTY') }}
</span>

View File

@@ -1,7 +1,6 @@
<script>
import darkModeMixin from 'widget/mixins/darkModeMixin';
import { useDarkMode } from 'widget/composables/useDarkMode';
export default {
mixins: [darkModeMixin],
props: {
label: {
type: String,
@@ -20,20 +19,33 @@ export default {
default: '',
},
},
setup() {
const { getThemeClass } = useDarkMode();
return { getThemeClass };
},
computed: {
labelClass() {
return this.error
? `text-red-400 ${this.$dm('text-black-800', 'dark:text-slate-50')}`
: `text-black-800 ${this.$dm('text-black-800', 'dark:text-slate-50')}`;
? `text-red-400 ${this.getThemeClass(
'text-black-800',
'dark:text-slate-50'
)}`
: `text-black-800 ${this.getThemeClass(
'text-black-800',
'dark:text-slate-50'
)}`;
},
isTextAreaDarkOrLightMode() {
return `${this.$dm('bg-white', 'dark:bg-slate-600')} ${this.$dm(
'text-slate-700',
'dark:text-slate-50'
)}`;
return `${this.getThemeClass(
'bg-white',
'dark:bg-slate-600'
)} ${this.getThemeClass('text-slate-700', 'dark:text-slate-50')}`;
},
textAreaBorderColor() {
return `${this.$dm('border-black-200', 'dark:border-black-500')}`;
return `${this.getThemeClass(
'border-black-200',
'dark:border-black-500'
)}`;
},
isTextAreaHasError() {
return this.error

View File

@@ -3,14 +3,14 @@ import { mapGetters } from 'vuex';
import { IFrameHelper, RNHelper } from 'widget/helpers/utils';
import { popoutChatWindow } from '../helpers/popoutHelper';
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
import darkModeMixin from 'widget/mixins/darkModeMixin';
import { useDarkMode } from 'widget/composables/useDarkMode';
import configMixin from 'widget/mixins/configMixin';
import { CONVERSATION_STATUS } from 'shared/constants/messages';
export default {
name: 'HeaderActions',
components: { FluentIcon },
mixins: [configMixin, darkModeMixin],
mixins: [configMixin],
props: {
showPopoutButton: {
type: Boolean,
@@ -21,6 +21,10 @@ export default {
default: true,
},
},
setup() {
const { getThemeClass } = useDarkMode();
return { getThemeClass };
},
computed: {
...mapGetters({
conversationAttributes: 'conversationAttributes/getConversationParams',
@@ -92,7 +96,7 @@ export default {
<FluentIcon
icon="sign-out"
size="22"
:class="$dm('text-black-900', 'dark:text-slate-50')"
:class="getThemeClass('text-black-900', 'dark:text-slate-50')"
/>
</button>
<button
@@ -103,7 +107,7 @@ export default {
<FluentIcon
icon="open"
size="22"
:class="$dm('text-black-900', 'dark:text-slate-50')"
:class="getThemeClass('text-black-900', 'dark:text-slate-50')"
/>
</button>
<button
@@ -116,7 +120,7 @@ export default {
<FluentIcon
icon="dismiss"
size="24"
:class="$dm('text-black-900', 'dark:text-slate-50')"
:class="getThemeClass('text-black-900', 'dark:text-slate-50')"
/>
</button>
</div>

View File

@@ -7,7 +7,7 @@ import { isEmptyObject } from 'widget/helpers/utils';
import { getRegexp } from 'shared/helpers/Validators';
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
import routerMixin from 'widget/mixins/routerMixin';
import darkModeMixin from 'widget/mixins/darkModeMixin';
import { useDarkMode } from 'widget/composables/useDarkMode';
import configMixin from 'widget/mixins/configMixin';
export default {
@@ -15,7 +15,7 @@ export default {
CustomButton,
Spinner,
},
mixins: [routerMixin, darkModeMixin, configMixin],
mixins: [routerMixin, configMixin],
props: {
options: {
type: Object,
@@ -24,9 +24,8 @@ export default {
},
setup() {
const { formatMessage } = useMessageFormatter();
return {
formatMessage,
};
const { getThemeClass } = useDarkMode();
return { formatMessage, getThemeClass };
},
data() {
return {
@@ -131,25 +130,28 @@ export default {
return `mt-1 border rounded w-full py-2 px-3 text-slate-700 outline-none`;
},
isInputDarkOrLightMode() {
return `${this.$dm('bg-white', 'dark:bg-slate-600')} ${this.$dm(
'text-slate-700',
'dark:text-slate-50'
)}`;
return `${this.getThemeClass(
'bg-white',
'dark:bg-slate-600'
)} ${this.getThemeClass('text-slate-700', 'dark:text-slate-50')}`;
},
inputBorderColor() {
return `${this.$dm('border-black-200', 'dark:border-black-500')}`;
return `${this.getThemeClass(
'border-black-200',
'dark:border-black-500'
)}`;
},
},
methods: {
labelClass(context) {
const { hasErrors } = context;
if (!hasErrors) {
return `text-xs font-medium ${this.$dm(
return `text-xs font-medium ${this.getThemeClass(
'text-black-800',
'dark:text-slate-50'
)}`;
}
return `text-xs font-medium ${this.$dm(
return `text-xs font-medium ${this.getThemeClass(
'text-red-400',
'dark:text-red-400'
)}`;
@@ -264,7 +266,7 @@ export default {
v-if="shouldShowHeaderMessage"
v-dompurify-html="formatMessage(headerMessage, false)"
class="mb-4 text-sm leading-5 pre-chat-header-message"
:class="$dm('text-black-800', 'dark:text-slate-50')"
:class="getThemeClass('text-black-800', 'dark:text-slate-50')"
/>
<FormulateInput
v-for="item in enabledPreChatFields"

View File

@@ -7,11 +7,11 @@ import {
ON_CAMPAIGN_MESSAGE_CLICK,
ON_UNREAD_MESSAGE_CLICK,
} from '../constants/widgetBusEvents';
import darkModeMixin from 'widget/mixins/darkModeMixin';
import { useDarkMode } from 'widget/composables/useDarkMode';
export default {
name: 'UnreadMessage',
components: { Thumbnail },
mixins: [configMixin, darkModeMixin],
mixins: [configMixin],
props: {
message: {
type: String,
@@ -31,9 +31,15 @@ export default {
},
},
setup() {
const { formatMessage } = useMessageFormatter();
const { formatMessage, getPlainText, truncateMessage, highlightContent } =
useMessageFormatter();
const { getThemeClass } = useDarkMode();
return {
formatMessage,
getPlainText,
truncateMessage,
highlightContent,
getThemeClass,
};
},
computed: {
@@ -91,7 +97,7 @@ export default {
<div class="chat-bubble-wrap">
<button
class="chat-bubble agent"
:class="$dm('bg-white', 'dark:bg-slate-50')"
:class="getThemeClass('bg-white', 'dark:bg-slate-50')"
@click="onClickMessage"
>
<div v-if="showSender" class="row--agent-block">

View File

@@ -1,13 +1,12 @@
<script>
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
import { useDarkMode } from 'widget/composables/useDarkMode';
export default {
components: {
FluentIcon,
},
mixins: [darkModeMixin],
props: {
items: {
type: Array,
@@ -16,9 +15,8 @@ export default {
},
setup() {
const { truncateMessage } = useMessageFormatter();
return {
truncateMessage,
};
const { getThemeClass } = useDarkMode();
return { getThemeClass, truncateMessage };
},
};
</script>
@@ -27,7 +25,7 @@ export default {
<div
v-if="!!items.length"
class="chat-bubble agent"
:class="$dm('bg-white', 'dark:bg-slate-700')"
:class="getThemeClass('bg-white', 'dark:bg-slate-700')"
>
<div v-for="item in items" :key="item.link" class="article-item">
<a :href="item.link" target="_blank" rel="noopener noreferrer nofollow">
@@ -35,15 +33,16 @@ export default {
<FluentIcon
icon="link"
class="mr-1"
:class="$dm('text-black-900', 'dark:text-slate-50')"
:class="getThemeClass('text-black-900', 'dark:text-slate-50')"
/>
<span :class="$dm('text-slate-900', 'dark:text-slate-50')">{{
item.title
}}</span>
<span
:class="getThemeClass('text-slate-900', 'dark:text-slate-50')"
>{{ item.title }}</span
>
</span>
<span
class="description"
:class="$dm('text-slate-700', 'dark:text-slate-200')"
:class="getThemeClass('text-slate-700', 'dark:text-slate-200')"
>
{{ truncateMessage(item.description) }}
</span>

View File

@@ -6,14 +6,13 @@ import { getContrastingTextColor } from '@chatwoot/utils';
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
import Spinner from 'shared/components/Spinner.vue';
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
import { useDarkMode } from 'widget/composables/useDarkMode';
export default {
components: {
FluentIcon,
Spinner,
},
mixins: [darkModeMixin],
props: {
messageId: {
type: Number,
@@ -25,7 +24,8 @@ export default {
},
},
setup() {
return { v$: useVuelidate() };
const { getThemeClass } = useDarkMode();
return { v$: useVuelidate(), getThemeClass };
},
data() {
return {
@@ -47,9 +47,9 @@ export default {
);
},
inputColor() {
return `${this.$dm('bg-white', 'dark:bg-slate-600')}
${this.$dm('text-black-900', 'dark:text-slate-50')}
${this.$dm('border-black-200', 'dark:border-black-500')}`;
return `${this.getThemeClass('bg-white', 'dark:bg-slate-600')}
${this.getThemeClass('text-black-900', 'dark:text-slate-50')}
${this.getThemeClass('border-black-200', 'dark:border-black-500')}`;
},
inputHasError() {
return this.v$.email.$error

View File

@@ -0,0 +1,71 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { useDarkMode } from '../useDarkMode';
import { useMapGetter } from 'dashboard/composables/store';
vi.mock('dashboard/composables/store', () => ({
useMapGetter: vi.fn(),
}));
describe('useDarkMode', () => {
let mockDarkMode;
beforeEach(() => {
mockDarkMode = { value: 'light' };
vi.mocked(useMapGetter).mockReturnValue(mockDarkMode);
});
it('returns darkMode, prefersDarkMode, and getThemeClass', () => {
const result = useDarkMode();
expect(result).toHaveProperty('darkMode');
expect(result).toHaveProperty('prefersDarkMode');
expect(result).toHaveProperty('getThemeClass');
});
describe('prefersDarkMode', () => {
it('returns false when darkMode is light', () => {
const { prefersDarkMode } = useDarkMode();
expect(prefersDarkMode.value).toBe(false);
});
it('returns true when darkMode is dark', () => {
mockDarkMode.value = 'dark';
const { prefersDarkMode } = useDarkMode();
expect(prefersDarkMode.value).toBe(true);
});
it('returns true when darkMode is auto and OS prefers dark mode', () => {
mockDarkMode.value = 'auto';
vi.spyOn(window, 'matchMedia').mockReturnValue({ matches: true });
const { prefersDarkMode } = useDarkMode();
expect(prefersDarkMode.value).toBe(true);
});
it('returns false when darkMode is auto and OS prefers light mode', () => {
mockDarkMode.value = 'auto';
vi.spyOn(window, 'matchMedia').mockReturnValue({ matches: false });
const { prefersDarkMode } = useDarkMode();
expect(prefersDarkMode.value).toBe(false);
});
});
describe('getThemeClass', () => {
it('returns light class when darkMode is light', () => {
const { getThemeClass } = useDarkMode();
expect(getThemeClass('light-class', 'dark-class')).toBe('light-class');
});
it('returns dark class when darkMode is dark', () => {
mockDarkMode.value = 'dark';
const { getThemeClass } = useDarkMode();
expect(getThemeClass('light-class', 'dark-class')).toBe('dark-class');
});
it('returns both classes when darkMode is auto', () => {
mockDarkMode.value = 'auto';
const { getThemeClass } = useDarkMode();
expect(getThemeClass('light-class', 'dark-class')).toBe(
'light-class dark-class'
);
});
});
});

View File

@@ -0,0 +1,39 @@
import { computed } from 'vue';
import { useMapGetter } from 'dashboard/composables/store';
const isDarkModeAuto = mode => mode === 'auto';
const isDarkMode = mode => mode === 'dark';
const getSystemPreference = () =>
window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false;
const calculatePrefersDarkMode = (mode, systemPreference) =>
isDarkModeAuto(mode) ? systemPreference : isDarkMode(mode);
const calculateThemeClass = (mode, light, dark) => {
if (isDarkModeAuto(mode)) return `${light} ${dark}`;
return isDarkMode(mode) ? dark : light;
};
/**
* Composable for handling dark mode.
* @returns {Object} An object containing computed properties and methods for dark mode.
*/
export function useDarkMode() {
const darkMode = useMapGetter('appConfig/darkMode');
const systemPreference = computed(getSystemPreference);
const prefersDarkMode = computed(() =>
calculatePrefersDarkMode(darkMode.value, systemPreference.value)
);
const getThemeClass = (light, dark) =>
calculateThemeClass(darkMode.value, light, dark);
return {
darkMode,
prefersDarkMode,
getThemeClass,
};
}

View File

@@ -1,24 +0,0 @@
import { mapGetters } from 'vuex';
export default {
computed: {
...mapGetters({ darkMode: 'appConfig/darkMode' }),
prefersDarkMode() {
const isOSOnDarkMode =
this.darkMode === 'auto' &&
window.matchMedia('(prefers-color-scheme: dark)').matches;
return isOSOnDarkMode || this.darkMode === 'dark';
},
},
methods: {
$dm(light, dark) {
if (this.darkMode === 'light') {
return light;
}
if (this.darkMode === 'dark') {
return dark;
}
return light + ' ' + dark;
},
},
};

View File

@@ -1,41 +0,0 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import darkModeMixin from '../darkModeMixin';
import Vuex from 'vuex';
const localVue = createLocalVue();
localVue.use(Vuex);
const darkModeValues = ['light', 'auto'];
describe('darkModeMixin', () => {
let getters;
let store;
beforeEach(() => {
getters = {
'appConfig/darkMode': () => darkModeValues[0],
};
store = new Vuex.Store({ getters });
});
it('if light theme', () => {
const Component = {
render() {},
mixins: [darkModeMixin],
};
const wrapper = shallowMount(Component, { store, localVue });
expect(wrapper.vm.$dm('bg-100', 'bg-600')).toBe('bg-100');
});
it('if auto theme', () => {
getters = {
'appConfig/darkMode': () => darkModeValues[2],
};
store = new Vuex.Store({ getters });
const Component = {
render() {},
mixins: [darkModeMixin],
};
const wrapper = shallowMount(Component, { store, localVue });
expect(wrapper.vm.$dm('bg-100', 'bg-600')).toBe('bg-100 bg-600');
});
});

View File

@@ -4,7 +4,7 @@ import ArticleHero from 'widget/components/ArticleHero.vue';
import ArticleCardSkeletonLoader from 'widget/components/ArticleCardSkeletonLoader.vue';
import { mapGetters } from 'vuex';
import darkModeMixin from 'widget/mixins/darkModeMixin';
import { useDarkMode } from 'widget/composables/useDarkMode';
import routerMixin from 'widget/mixins/routerMixin';
import configMixin from 'widget/mixins/configMixin';
@@ -15,7 +15,11 @@ export default {
TeamAvailability,
ArticleCardSkeletonLoader,
},
mixins: [configMixin, routerMixin, darkModeMixin],
mixins: [configMixin, routerMixin],
setup() {
const { prefersDarkMode } = useDarkMode();
return { prefersDarkMode };
},
computed: {
...mapGetters({
availableAgents: 'agent/availableAgents',