diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Voice.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Voice.vue
index e19ad23bb..7559f7c3e 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Voice.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Voice.vue
@@ -22,7 +22,6 @@ const state = reactive({
authToken: '',
apiKeySid: '',
apiKeySecret: '',
- twimlAppSid: '',
});
const uiFlags = useMapGetter('inboxes/getUIFlags');
@@ -33,7 +32,6 @@ const validationRules = {
authToken: { required },
apiKeySid: { required },
apiKeySecret: { required },
- twimlAppSid: { required },
};
const v$ = useVuelidate(validationRules, state);
@@ -55,9 +53,6 @@ const formErrors = computed(() => ({
apiKeySecret: v$.value.apiKeySecret?.$error
? t('INBOX_MGMT.ADD.VOICE.TWILIO.API_KEY_SECRET.REQUIRED')
: '',
- twimlAppSid: v$.value.twimlAppSid?.$error
- ? t('INBOX_MGMT.ADD.VOICE.TWILIO.TWIML_APP_SID.REQUIRED')
- : '',
}));
function getProviderConfig() {
@@ -67,7 +62,6 @@ function getProviderConfig() {
api_key_sid: state.apiKeySid,
api_key_secret: state.apiKeySecret,
};
- if (state.twimlAppSid) config.outgoing_application_sid = state.twimlAppSid;
return config;
}
@@ -160,17 +154,6 @@ async function createChannel() {
@blur="v$.apiKeySecret?.$touch"
/>
-
-
+
+
+
+
+
+
+
+
+
{
+ Object.keys(src).forEach(key => {
+ if (
+ acc[key] &&
+ typeof acc[key] === 'object' &&
+ typeof src[key] === 'object'
+ ) {
+ acc[key] = mergeMessages(acc[key], src[key]);
+ } else {
+ acc[key] = src[key];
+ }
+ });
+ return acc;
+ }, {});
+}
+
const i18n = createI18n({
legacy: false, // https://github.com/intlify/vue-i18n/issues/1902
locale: 'en',
- messages: i18nMessages,
+ messages: mergeMessages(
+ structuredClone(dashboardI18n),
+ structuredClone(widgetI18n)
+ ),
});
export const setupVue3 = defineSetupVue3(({ app }) => {
diff --git a/app/javascript/sdk/IFrameHelper.js b/app/javascript/sdk/IFrameHelper.js
index 25f90912d..3ab1ba1f5 100644
--- a/app/javascript/sdk/IFrameHelper.js
+++ b/app/javascript/sdk/IFrameHelper.js
@@ -50,35 +50,11 @@ const updateCampaignReadStatus = baseDomain => {
});
};
-const sanitizeURL = url => {
- if (url === '') return '';
-
- try {
- // any invalid url will not be accepted
- // example - JaVaScRiP%0at:alert(document.domain)"
- // this has an obfuscated javascript protocol
- const parsedURL = new URL(url);
-
- // filter out dangerous protocols like `javascript`, `data`, `vbscript`
- if (!['https', 'http'].includes(parsedURL.protocol)) {
- throw new Error('Invalid Protocol');
- }
- } catch (e) {
- // eslint-disable-next-line no-console
- console.error('Invalid URL', e);
- }
-
- return 'about:blank'; // blank page URL
-};
-
export const IFrameHelper = {
getUrl({ baseUrl, websiteToken }) {
- baseUrl = sanitizeURL(baseUrl);
return `${baseUrl}/widget?website_token=${websiteToken}`;
},
createFrame: ({ baseUrl, websiteToken }) => {
- baseUrl = sanitizeURL(baseUrl);
-
if (IFrameHelper.getAppFrame()) {
return;
}
@@ -126,12 +102,10 @@ export const IFrameHelper = {
window.onmessage = e => {
if (
typeof e.data !== 'string' ||
- e.data.indexOf('chatwoot-widget:') !== 0 ||
- e.origin !== window.location.origin
+ e.data.indexOf('chatwoot-widget:') !== 0
) {
return;
}
-
const message = JSON.parse(e.data.replace('chatwoot-widget:', ''));
if (typeof IFrameHelper.events[message.event] === 'function') {
IFrameHelper.events[message.event](message);
@@ -166,9 +140,7 @@ export const IFrameHelper = {
},
setupAudioListeners: () => {
- let { baseUrl = '' } = window.$chatwoot;
- baseUrl = sanitizeURL(baseUrl);
-
+ const { baseUrl = '' } = window.$chatwoot;
getAlertAudio(baseUrl, { type: 'widget', alertTone: 'ding' }).then(() =>
initOnEvents.forEach(event => {
document.removeEventListener(
@@ -262,7 +234,6 @@ export const IFrameHelper = {
},
popoutChatWindow: ({ baseUrl, websiteToken, locale }) => {
- baseUrl = sanitizeURL(baseUrl);
const cwCookie = Cookies.get('cw_conversation');
window.$chatwoot.toggle('close');
popoutChatWindow(baseUrl, websiteToken, locale, cwCookie);
diff --git a/app/javascript/shared/constants/messages.js b/app/javascript/shared/constants/messages.js
index 7b7b4f331..d87db470d 100644
--- a/app/javascript/shared/constants/messages.js
+++ b/app/javascript/shared/constants/messages.js
@@ -36,7 +36,6 @@ export const CONVERSATION_PRIORITY_ORDER = {
// Size in mega bytes
export const MAXIMUM_FILE_UPLOAD_SIZE = 40;
-export const MAXIMUM_FILE_UPLOAD_SIZE_TWILIO_SMS_CHANNEL = 5;
export const ALLOWED_FILE_TYPES =
'image/*,' +
@@ -50,18 +49,6 @@ export const ALLOWED_FILE_TYPES =
'application/vnd.openxmlformats-officedocument.presentationml.presentation, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,' +
'application/vnd.openxmlformats-officedocument.wordprocessingml.document,';
-export const ALLOWED_FILE_TYPES_FOR_TWILIO_WHATSAPP =
- 'image/png, image/jpeg,' +
- 'audio/mpeg, audio/opus, audio/ogg, audio/amr,' +
- 'video/mp4,' +
- 'application/pdf,';
-// https://developers.line.biz/en/reference/messaging-api/#image-message, https://developers.line.biz/en/reference/messaging-api/#video-message
-export const ALLOWED_FILE_TYPES_FOR_LINE = 'image/png, image/jpeg,video/mp4';
-
-// https://developers.facebook.com/docs/instagram-platform/instagram-api-with-instagram-login/messaging-api#requirements
-export const ALLOWED_FILE_TYPES_FOR_INSTAGRAM =
- 'image/png, image/jpeg, video/mp4, video/mov, video/webm';
-
export const CSAT_RATINGS = [
{
key: 'disappointed',
diff --git a/app/javascript/shared/mixins/inboxMixin.js b/app/javascript/shared/mixins/inboxMixin.js
index bf8ec492b..c1f48f6f4 100644
--- a/app/javascript/shared/mixins/inboxMixin.js
+++ b/app/javascript/shared/mixins/inboxMixin.js
@@ -57,15 +57,15 @@ export default {
isALineChannel() {
return this.channelType === INBOX_TYPES.LINE;
},
+ isAVoiceChannel() {
+ return this.channelType === INBOX_TYPES.VOICE;
+ },
isAnEmailChannel() {
return this.channelType === INBOX_TYPES.EMAIL;
},
isATelegramChannel() {
return this.channelType === INBOX_TYPES.TELEGRAM;
},
- isAVoiceChannel() {
- return this.channelType === INBOX_TYPES.VOICE;
- },
isATwilioSMSChannel() {
const { medium: medium = '' } = this.inbox;
return this.isATwilioChannel && medium === 'sms';
diff --git a/app/javascript/widget/App.vue b/app/javascript/widget/App.vue
index 48409cef2..379b2bc53 100755
--- a/app/javascript/widget/App.vue
+++ b/app/javascript/widget/App.vue
@@ -4,12 +4,10 @@ import { setHeader } from 'widget/helpers/axios';
import addHours from 'date-fns/addHours';
import { IFrameHelper, RNHelper } from 'widget/helpers/utils';
import configMixin from './mixins/configMixin';
-import availabilityMixin from 'widget/mixins/availability';
import { getLocale } from './helpers/urlParamsHelper';
import { getLanguageDirection } from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
import { isEmptyObject } from 'widget/helpers/utils';
import Spinner from 'shared/components/Spinner.vue';
-import routerMixin from './mixins/routerMixin';
import {
getExtraSpaceToScroll,
loadedEventConfig,
@@ -20,6 +18,8 @@ import {
ON_UNREAD_MESSAGE_CLICK,
} from './constants/widgetBusEvents';
import { useDarkMode } from 'widget/composables/useDarkMode';
+import { useRouter } from 'vue-router';
+import { useAvailability } from 'widget/composables/useAvailability';
import { SDK_SET_BUBBLE_VISIBILITY } from '../shared/constants/sharedFrameEvents';
import { emitter } from 'shared/helpers/mitt';
@@ -28,10 +28,13 @@ export default {
components: {
Spinner,
},
- mixins: [availabilityMixin, configMixin, routerMixin],
+ mixins: [configMixin],
setup() {
const { prefersDarkMode } = useDarkMode();
- return { prefersDarkMode };
+ const router = useRouter();
+ const { isInWorkingHours } = useAvailability();
+
+ return { prefersDarkMode, router, isInWorkingHours };
},
data() {
return {
@@ -157,15 +160,17 @@ export default {
this.setUnreadView();
});
emitter.on(ON_UNREAD_MESSAGE_CLICK, () => {
- this.replaceRoute('messages').then(() => this.unsetUnreadView());
+ this.router
+ .replace({ name: 'messages' })
+ .then(() => this.unsetUnreadView());
});
},
registerCampaignEvents() {
emitter.on(ON_CAMPAIGN_MESSAGE_CLICK, () => {
if (this.shouldShowPreChatForm) {
- this.replaceRoute('prechat-form');
+ this.router.replace({ name: 'prechat-form' });
} else {
- this.replaceRoute('messages');
+ this.router.replace({ name: 'messages' });
emitter.emit('execute-campaign', {
campaignId: this.activeCampaign.id,
});
@@ -176,7 +181,7 @@ export default {
const { customAttributes, campaignId } = campaignDetails;
const { websiteToken } = window.chatwootWebChannel;
this.executeCampaign({ campaignId, websiteToken, customAttributes });
- this.replaceRoute('messages');
+ this.router.replace({ name: 'messages' });
});
emitter.on('snooze-campaigns', () => {
const expireBy = addHours(new Date(), 1);
@@ -192,7 +197,7 @@ export default {
!messageCount &&
!shouldSnoozeCampaign;
if (this.isIFrame && isCampaignReadyToExecute) {
- this.replaceRoute('campaigns').then(() => {
+ this.router.replace({ name: 'campaigns' }).then(() => {
this.setIframeHeight(true);
IFrameHelper.sendMessage({ event: 'setUnreadMode' });
});
@@ -207,7 +212,7 @@ export default {
unreadMessageCount > 0 &&
!this.isWidgetOpen
) {
- this.replaceRoute('unread-messages').then(() => {
+ this.router.replace({ name: 'unread-messages' }).then(() => {
this.setIframeHeight(true);
IFrameHelper.sendMessage({ event: 'setUnreadMode' });
});
@@ -263,7 +268,7 @@ export default {
this.initCampaigns({
currentURL: referrerURL,
websiteToken,
- isInBusinessHours: this.isInBusinessHours,
+ isInBusinessHours: this.isInWorkingHours,
});
window.referrerURL = referrerURL;
this.setReferrerHost(referrerHost);
@@ -314,12 +319,12 @@ export default {
['unread-messages', 'campaigns'].includes(this.$route.name);
if (shouldShowMessageView) {
- this.replaceRoute('messages');
+ this.router.replace({ name: 'messages' });
}
if (shouldShowHomeView) {
this.$store.dispatch('conversation/setUserLastSeen');
this.unsetUnreadView();
- this.replaceRoute('home');
+ this.router.replace({ name: 'home' });
}
if (!message.isOpen) {
this.resetCampaign();
diff --git a/app/javascript/widget/components/Availability/AvailabilityContainer.vue b/app/javascript/widget/components/Availability/AvailabilityContainer.vue
new file mode 100644
index 000000000..367564751
--- /dev/null
+++ b/app/javascript/widget/components/Availability/AvailabilityContainer.vue
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+ {{ headerText }}
+
+
+
+
+
+
+
+
diff --git a/app/javascript/widget/components/Availability/AvailabilityText.story.vue b/app/javascript/widget/components/Availability/AvailabilityText.story.vue
new file mode 100644
index 000000000..aafcb3556
--- /dev/null
+++ b/app/javascript/widget/components/Availability/AvailabilityText.story.vue
@@ -0,0 +1,217 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/widget/components/Availability/AvailabilityText.vue b/app/javascript/widget/components/Availability/AvailabilityText.vue
new file mode 100644
index 000000000..649224d20
--- /dev/null
+++ b/app/javascript/widget/components/Availability/AvailabilityText.vue
@@ -0,0 +1,178 @@
+
+
+
+
+
+
+ {{ replyTimeMessage }}
+
+
+
+
+ {{
+ isOnline
+ ? replyTimeMessage
+ : t('TEAM_AVAILABILITY.BACK_AS_SOON_AS_POSSIBLE')
+ }}
+
+
+
+
+
+ {{ t('TEAM_AVAILABILITY.BACK_AS_SOON_AS_POSSIBLE') }}
+
+
+
+
+ {{ t('REPLY_TIME.BACK_IN_SOME_TIME') }}
+
+
+
+
+ {{ t('REPLY_TIME.BACK_TOMORROW') }}
+
+
+
+
+ {{
+ t('REPLY_TIME.BACK_ON_DAY', {
+ day: dayNames[nextSlot.config.dayOfWeek],
+ })
+ }}
+
+
+
+
+ {{
+ t('REPLY_TIME.BACK_IN_MINUTES', {
+ time: `${roundedMinutesUntilOpen}`,
+ })
+ }}
+
+
+
+
+ {{ t('REPLY_TIME.BACK_IN_HOURS', adjustedHoursUntilOpen) }}
+
+
+
+
+ {{
+ t('REPLY_TIME.BACK_AT_TIME', {
+ time: formattedOpeningTime,
+ })
+ }}
+
+
+
diff --git a/app/javascript/widget/components/ChatFooter.vue b/app/javascript/widget/components/ChatFooter.vue
index 2bc6ba7ec..c85727a2b 100755
--- a/app/javascript/widget/components/ChatFooter.vue
+++ b/app/javascript/widget/components/ChatFooter.vue
@@ -6,7 +6,7 @@ import FooterReplyTo from 'widget/components/FooterReplyTo.vue';
import ChatInputWrap from 'widget/components/ChatInputWrap.vue';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import { sendEmailTranscript } from 'widget/api/conversation';
-import routerMixin from 'widget/mixins/routerMixin';
+import { useRouter } from 'vue-router';
import { IFrameHelper } from '../helpers/utils';
import { CHATWOOT_ON_START_CONVERSATION } from '../constants/sdkEvents';
import { emitter } from 'shared/helpers/mitt';
@@ -17,7 +17,10 @@ export default {
CustomButton,
FooterReplyTo,
},
- mixins: [routerMixin],
+ setup() {
+ const router = useRouter();
+ return { router };
+ },
data() {
return {
inReplyTo: null,
@@ -77,7 +80,7 @@ export default {
this.inReplyTo = null;
},
startNewConversation() {
- this.replaceRoute('prechat-form');
+ this.router.replace({ name: 'prechat-form' });
IFrameHelper.sendMessage({
event: 'onEvent',
eventIdentifier: CHATWOOT_ON_START_CONVERSATION,
diff --git a/app/javascript/widget/components/ChatHeader.vue b/app/javascript/widget/components/ChatHeader.vue
index 7c497b70f..578fb984e 100644
--- a/app/javascript/widget/components/ChatHeader.vue
+++ b/app/javascript/widget/components/ChatHeader.vue
@@ -1,55 +1,26 @@
-
@@ -79,9 +50,12 @@ export default {
${isOnline ? 'bg-n-teal-10' : 'hidden'}`"
/>
-
- {{ replyWaitMessage }}
-
+