@@ -153,14 +155,21 @@ export default {
diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalListItem.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalListItem.vue
index 956827c18..2282ebd37 100644
--- a/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalListItem.vue
+++ b/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalListItem.vue
@@ -236,8 +236,10 @@ export default {
});
},
articleCount() {
- const { all_articles_count: count } = this.portal.meta;
- return count;
+ const { allowed_locales: allowedLocales } = this.portal.config;
+ return allowedLocales.reduce((acc, locale) => {
+ return acc + locale.articles_count;
+ }, 0);
},
},
methods: {
diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalSwitch.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalSwitch.vue
index 4442150c5..30f3fb4ff 100644
--- a/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalSwitch.vue
+++ b/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalSwitch.vue
@@ -105,7 +105,10 @@ export default {
return this.portal?.config?.allowed_locales;
},
articlesCount() {
- return this.portal?.meta?.all_articles_count;
+ const { allowed_locales: allowedLocales } = this.portal.config;
+ return allowedLocales.reduce((acc, locale) => {
+ return acc + locale.articles_count;
+ }, 0);
},
},
mounted() {
diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/articles/EditArticle.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/articles/EditArticle.vue
index 1dba3a1d4..ff737913f 100644
--- a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/articles/EditArticle.vue
+++ b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/articles/EditArticle.vue
@@ -151,6 +151,7 @@ export default {
params: {
portalSlug: this.selectedPortalSlug,
locale: this.locale,
+ recentlyDeleted: true,
},
});
} catch (error) {
diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/articles/NewArticle.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/articles/NewArticle.vue
index 865c87c3a..a7c8d9c21 100644
--- a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/articles/NewArticle.vue
+++ b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/articles/NewArticle.vue
@@ -87,6 +87,7 @@ export default {
articleSlug: articleId,
portalSlug: this.selectedPortalSlug,
locale: this.locale,
+ recentlyCreated: true,
},
});
} catch (error) {
diff --git a/app/javascript/dashboard/routes/dashboard/settings/profile/NotificationSettings.vue b/app/javascript/dashboard/routes/dashboard/settings/profile/NotificationSettings.vue
index 60584d726..f6b216b96 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/profile/NotificationSettings.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/profile/NotificationSettings.vue
@@ -10,50 +10,132 @@
-
-
-
-
-
-
-
-
-
-
-
@@ -257,6 +339,19 @@ export default {
selectedPushFlags: [],
enableAudioAlerts: false,
hasEnabledPushPermissions: false,
+ playAudioWhenTabIsInactive: false,
+ alertIfUnreadConversationExist: false,
+ notificationTone: 'ding',
+ notificationAlertTones: [
+ {
+ value: 'ding',
+ label: 'Ding',
+ },
+ {
+ value: 'bell',
+ label: 'Bell',
+ },
+ ],
};
},
computed: {
@@ -280,20 +375,29 @@ export default {
this.selectedPushFlags = value;
},
uiSettings(value) {
- const { enable_audio_alerts: enableAudio = false } = value;
- this.enableAudioAlerts = enableAudio;
+ this.notificationUISettings(value);
},
},
mounted() {
if (hasPushPermissions()) {
this.getPushSubscription();
}
-
+ this.notificationUISettings(this.uiSettings);
this.$store.dispatch('userNotificationSettings/get');
- const { enable_audio_alerts: enableAudio = false } = this.uiSettings;
- this.enableAudioAlerts = enableAudio;
},
methods: {
+ notificationUISettings(uiSettings) {
+ const {
+ enable_audio_alerts: enableAudio = false,
+ always_play_audio_alert: alwaysPlayAudioAlert,
+ alert_if_unread_assigned_conversation_exist: alertIfUnreadConversationExist,
+ notification_tone: notificationTone,
+ } = uiSettings;
+ this.enableAudioAlerts = enableAudio;
+ this.playAudioWhenTabIsInactive = !alwaysPlayAudioAlert;
+ this.alertIfUnreadConversationExist = alertIfUnreadConversationExist;
+ this.notificationTone = notificationTone || 'ding';
+ },
onRegistrationSuccess() {
this.hasEnabledPushPermissions = true;
},
@@ -351,6 +455,23 @@ export default {
});
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_SUCCESS'));
},
+ handleAudioAlertConditions(e) {
+ let condition = e.target.value;
+ if (condition === 'tab_is_inactive') {
+ this.updateUISettings({
+ always_play_audio_alert: !e.target.checked,
+ });
+ } else if (condition === 'conversations_are_read') {
+ this.updateUISettings({
+ alert_if_unread_assigned_conversation_exist: e.target.checked,
+ });
+ }
+ this.showAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_SUCCESS'));
+ },
+ handleAudioToneChange(e) {
+ this.updateUISettings({ notification_tone: e.target.value });
+ this.showAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_SUCCESS'));
+ },
toggleInput(selected, current) {
if (selected.includes(current)) {
const newSelectedFlags = selected.filter(flag => flag !== current);
@@ -372,4 +493,21 @@ export default {
.push-notification--button {
margin-bottom: var(--space-one);
}
+
+.notification-items--wrapper {
+ margin-bottom: var(--space-smaller);
+}
+
+.notification-label {
+ display: flex;
+ font-weight: var(--font-weight-bold);
+ margin-bottom: var(--space-small);
+}
+
+.tone-selector {
+ height: var(--space-large);
+ padding-bottom: var(--space-micro);
+ padding-top: var(--space-micro);
+ width: var(--space-mega);
+}
diff --git a/app/javascript/dashboard/store/modules/conversations/index.js b/app/javascript/dashboard/store/modules/conversations/index.js
index b3c9ab7e4..a8a2fdabe 100644
--- a/app/javascript/dashboard/store/modules/conversations/index.js
+++ b/app/javascript/dashboard/store/modules/conversations/index.js
@@ -108,6 +108,8 @@ export const mutations = {
} else {
chat.messages.push(message);
chat.timestamp = message.created_at;
+ const { conversation: { unread_count: unreadCount = 0 } = {} } = message;
+ chat.unread_count = unreadCount;
if (selectedChatId === conversationId) {
window.bus.$emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
}
diff --git a/app/javascript/dashboard/store/modules/specs/conversations/mutations.spec.js b/app/javascript/dashboard/store/modules/specs/conversations/mutations.spec.js
index caa9df090..738de5c55 100644
--- a/app/javascript/dashboard/store/modules/specs/conversations/mutations.spec.js
+++ b/app/javascript/dashboard/store/modules/specs/conversations/mutations.spec.js
@@ -103,6 +103,7 @@ describe('#mutations', () => {
created_at: 1602256198,
},
],
+ unread_count: 0,
timestamp: 1602256198,
},
]);
@@ -130,6 +131,7 @@ describe('#mutations', () => {
created_at: 1602256198,
},
],
+ unread_count: 0,
timestamp: 1602256198,
},
]);
diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js
index 9f6c7ac30..e3cb16063 100644
--- a/app/javascript/packs/application.js
+++ b/app/javascript/packs/application.js
@@ -16,11 +16,6 @@ import App from '../dashboard/App';
import i18n from '../dashboard/i18n';
import createAxios from '../dashboard/helper/APIHelper';
import commonHelpers, { isJSONValid } from '../dashboard/helper/commons';
-import {
- getAlertAudio,
- initOnEvents,
-} from '../shared/helpers/AudioNotificationHelper';
-import { initFaviconSwitcher } from '../shared/helpers/faviconHelper';
import router, { initalizeRouter } from '../dashboard/routes';
import store from '../dashboard/store';
import constants from '../dashboard/constants';
@@ -93,17 +88,6 @@ window.onload = () => {
}).$mount('#app');
};
-const setupAudioListeners = () => {
- getAlertAudio().then(() => {
- initOnEvents.forEach(event => {
- document.removeEventListener(event, setupAudioListeners, false);
- });
- });
-};
window.addEventListener('load', () => {
window.playAudioAlert = () => {};
- initOnEvents.forEach(e => {
- document.addEventListener(e, setupAudioListeners, false);
- });
- initFaviconSwitcher();
});
diff --git a/app/javascript/sdk/IFrameHelper.js b/app/javascript/sdk/IFrameHelper.js
index 2556241dd..7a8efa04f 100644
--- a/app/javascript/sdk/IFrameHelper.js
+++ b/app/javascript/sdk/IFrameHelper.js
@@ -127,7 +127,7 @@ export const IFrameHelper = {
setupAudioListeners: () => {
const { baseUrl = '' } = window.$chatwoot;
- getAlertAudio(baseUrl, 'widget').then(() =>
+ getAlertAudio(baseUrl, { type: 'widget', alertTone: 'ding' }).then(() =>
initOnEvents.forEach(event => {
document.removeEventListener(
event,
diff --git a/app/javascript/shared/assets/audio/bell.mp3 b/app/javascript/shared/assets/audio/bell.mp3
new file mode 100644
index 000000000..77087072f
Binary files /dev/null and b/app/javascript/shared/assets/audio/bell.mp3 differ
diff --git a/app/javascript/shared/components/ResizableTextArea.vue b/app/javascript/shared/components/ResizableTextArea.vue
index 6c7f8a78a..ce2a42488 100644
--- a/app/javascript/shared/components/ResizableTextArea.vue
+++ b/app/javascript/shared/components/ResizableTextArea.vue
@@ -37,6 +37,13 @@ export default {
this.resizeTextarea();
},
},
+ mounted() {
+ this.$nextTick(() => {
+ if (this.value) {
+ this.resizeTextarea();
+ }
+ });
+ },
methods: {
resizeTextarea() {
if (!this.value) {
diff --git a/app/javascript/shared/helpers/AudioNotificationHelper.js b/app/javascript/shared/helpers/AudioNotificationHelper.js
index a80a12e14..cf1cd8087 100644
--- a/app/javascript/shared/helpers/AudioNotificationHelper.js
+++ b/app/javascript/shared/helpers/AudioNotificationHelper.js
@@ -1,8 +1,3 @@
-import { MESSAGE_TYPE } from 'shared/constants/messages';
-import { IFrameHelper } from 'widget/helpers/utils';
-
-import { showBadgeOnFavicon } from './faviconHelper';
-
export const initOnEvents = ['click', 'touchstart', 'keypress', 'keydown'];
export const getAudioContext = () => {
@@ -15,10 +10,9 @@ export const getAudioContext = () => {
return audioCtx;
};
-export const getAlertAudio = async (baseUrl = '', type = 'dashboard') => {
+export const getAlertAudio = async (baseUrl = '', requestContext) => {
const audioCtx = getAudioContext();
-
- const playsound = audioBuffer => {
+ const playSound = audioBuffer => {
window.playAudioAlert = () => {
if (audioCtx) {
const source = audioCtx.createBufferSource();
@@ -31,13 +25,14 @@ export const getAlertAudio = async (baseUrl = '', type = 'dashboard') => {
};
if (audioCtx) {
- const resourceUrl = `${baseUrl}/audio/${type}/ding.mp3`;
+ const { type = 'dashboard', alertTone = 'ding' } = requestContext || {};
+ const resourceUrl = `${baseUrl}/audio/${type}/${alertTone}.mp3`;
const audioRequest = new Request(resourceUrl);
fetch(audioRequest)
.then(response => response.arrayBuffer())
.then(buffer => {
- audioCtx.decodeAudioData(buffer).then(playsound);
+ audioCtx.decodeAudioData(buffer).then(playSound);
return new Promise(res => res());
})
.catch(() => {
@@ -45,83 +40,3 @@ export const getAlertAudio = async (baseUrl = '', type = 'dashboard') => {
});
}
};
-
-export const notificationEnabled = (enableAudioAlerts, id, userId) => {
- if (enableAudioAlerts === 'mine') {
- return userId === id;
- }
- if (enableAudioAlerts === 'all') {
- return true;
- }
- return false;
-};
-
-export const shouldPlayAudio = (
- message,
- conversationId,
- userId,
- isDocHidden
-) => {
- const {
- conversation_id: incomingConvId,
- sender_id: senderId,
- message_type: messageType,
- private: isPrivate,
- } = message;
- if (!isDocHidden && messageType === MESSAGE_TYPE.INCOMING) {
- showBadgeOnFavicon();
- return false;
- }
- const isFromCurrentUser = userId === senderId;
-
- const playAudio =
- !isFromCurrentUser && (messageType === MESSAGE_TYPE.INCOMING || isPrivate);
- if (isDocHidden) return playAudio;
- if (conversationId !== incomingConvId) return playAudio;
- return false;
-};
-
-export const getAssigneeFromNotification = currentConv => {
- let id;
- if (currentConv.meta) {
- const assignee = currentConv.meta.assignee;
- if (assignee) {
- id = assignee.id;
- }
- }
- return id;
-};
-export const newMessageNotification = data => {
- const { conversation_id: currentConvId } = window.WOOT.$route.params;
- const currentUserId = window.WOOT.$store.getters.getCurrentUserID;
- const { conversation_id: incomingConvId } = data;
- const currentConv =
- window.WOOT.$store.getters.getConversationById(incomingConvId) || {};
- const assigneeId = getAssigneeFromNotification(currentConv);
- const isDocHidden = document.hidden;
- const {
- enable_audio_alerts: enableAudioAlerts = false,
- } = window.WOOT.$store.getters.getUISettings;
- const playAudio = shouldPlayAudio(
- data,
- currentConvId,
- currentUserId,
- isDocHidden
- );
- const isNotificationEnabled = notificationEnabled(
- enableAudioAlerts,
- currentUserId,
- assigneeId
- );
-
- if (playAudio && isNotificationEnabled) {
- window.playAudioAlert();
- showBadgeOnFavicon();
- }
-};
-
-export const playNewMessageNotificationInWidget = () => {
- IFrameHelper.sendMessage({
- event: 'playAudio',
- });
-};
diff --git a/app/javascript/shared/helpers/specs/AudioNotificationHelper.spec.js b/app/javascript/shared/helpers/specs/AudioNotificationHelper.spec.js
deleted file mode 100644
index 7dc935400..000000000
--- a/app/javascript/shared/helpers/specs/AudioNotificationHelper.spec.js
+++ /dev/null
@@ -1,151 +0,0 @@
-/**
- * @jest-environment jsdom
- */
-
-import {
- shouldPlayAudio,
- notificationEnabled,
- getAssigneeFromNotification,
-} from '../AudioNotificationHelper';
-
-describe('shouldPlayAudio', () => {
- describe('Document active', () => {
- it('Retuns true if incoming message', () => {
- const message = {
- conversation_id: 10,
- sender_id: 5,
- message_type: 0,
- private: false,
- };
- const [conversationId, userId, isDocHiddden] = [1, 2, true];
- const result = shouldPlayAudio(
- message,
- conversationId,
- userId,
- isDocHiddden
- );
- expect(result).toBe(true);
- });
- it('Retuns false if outgoing message', () => {
- const message = {
- conversation_id: 10,
- sender_id: 5,
- message_type: 1,
- private: false,
- };
- const [conversationId, userId, isDocHiddden] = [1, 2, false];
- const result = shouldPlayAudio(
- message,
- conversationId,
- userId,
- isDocHiddden
- );
- expect(result).toBe(false);
- });
-
- it('Retuns false if from Same sender', () => {
- const message = {
- conversation_id: 1,
- sender_id: 2,
- message_type: 0,
- private: false,
- };
- const [conversationId, userId, isDocHiddden] = [1, 2, true];
- const result = shouldPlayAudio(
- message,
- conversationId,
- userId,
- isDocHiddden
- );
- expect(result).toBe(false);
- });
- it('Retuns true if private message from another agent', () => {
- const message = {
- conversation_id: 1,
- sender_id: 5,
- message_type: 1,
- private: true,
- };
- const [conversationId, userId, isDocHiddden] = [1, 2, true];
- const result = shouldPlayAudio(
- message,
- conversationId,
- userId,
- isDocHiddden
- );
- expect(result).toBe(true);
- });
- });
- describe('Document inactive', () => {
- it('Retuns true if incoming message', () => {
- const message = {
- conversation_id: 1,
- sender_id: 5,
- message_type: 0,
- private: false,
- };
- const [conversationId, userId, isDocHiddden] = [1, 2, true];
- const result = shouldPlayAudio(
- message,
- conversationId,
- userId,
- isDocHiddden
- );
- expect(result).toBe(true);
- });
- it('Retuns false if outgoing message', () => {
- const message = {
- conversation_id: 1,
- sender_id: 5,
- message_type: 1,
- private: false,
- };
- const [conversationId, userId, isDocHiddden] = [1, 2, true];
- const result = shouldPlayAudio(
- message,
- conversationId,
- userId,
- isDocHiddden
- );
- expect(result).toBe(false);
- });
- });
-});
-describe('notificationEnabled', () => {
- it('returns true if mine', () => {
- const [enableAudioAlerts, userId, id] = ['mine', 1, 1];
- const result = notificationEnabled(enableAudioAlerts, userId, id);
- expect(result).toBe(true);
- });
- it('returns true if all', () => {
- const [enableAudioAlerts, userId, id] = ['all', 1, 2];
- const result = notificationEnabled(enableAudioAlerts, userId, id);
- expect(result).toBe(true);
- });
- it('returns false if none', () => {
- const [enableAudioAlerts, userId, id] = ['none', 1, 2];
- const result = notificationEnabled(enableAudioAlerts, userId, id);
- expect(result).toBe(false);
- });
-});
-describe('getAssigneeFromNotification', () => {
- it('Retuns true if gets notification from assignee', () => {
- const currentConv = {
- id: 1,
- accountId: 1,
- meta: {
- assignee: {
- id: 1,
- name: 'John',
- },
- },
- };
- const result = getAssigneeFromNotification(currentConv);
- expect(result).toBe(1);
- });
- it('Retuns true if gets notification from assignee is udefined', () => {
- const currentConv = {};
- const result = getAssigneeFromNotification(currentConv);
- expect(result).toBe(undefined);
- });
-});
diff --git a/app/javascript/survey/views/Response.vue b/app/javascript/survey/views/Response.vue
index 828a9cbee..a8dfd8a1a 100644
--- a/app/javascript/survey/views/Response.vue
+++ b/app/javascript/survey/views/Response.vue
@@ -174,7 +174,7 @@ export default {
feedback_message: this.feedbackMessage,
};
} catch (error) {
- const errorMessage = error?.response?.data?.message;
+ const errorMessage = error?.response?.data?.error;
this.errorMessage = errorMessage || this.$t('SURVEY.API.ERROR_MESSAGE');
} finally {
this.isUpdating = false;
diff --git a/app/javascript/widget/helpers/WidgetAudioNotificationHelper.js b/app/javascript/widget/helpers/WidgetAudioNotificationHelper.js
new file mode 100644
index 000000000..3f1997f7b
--- /dev/null
+++ b/app/javascript/widget/helpers/WidgetAudioNotificationHelper.js
@@ -0,0 +1,5 @@
+import { IFrameHelper } from 'widget/helpers/utils';
+
+export const playNewMessageNotificationInWidget = () => {
+ IFrameHelper.sendMessage({ event: 'playAudio' });
+};
diff --git a/app/javascript/widget/helpers/actionCable.js b/app/javascript/widget/helpers/actionCable.js
index 84edb35ba..4b7140bcf 100644
--- a/app/javascript/widget/helpers/actionCable.js
+++ b/app/javascript/widget/helpers/actionCable.js
@@ -1,5 +1,5 @@
import BaseActionCableConnector from '../../shared/helpers/BaseActionCableConnector';
-import { playNewMessageNotificationInWidget } from 'shared/helpers/AudioNotificationHelper';
+import { playNewMessageNotificationInWidget } from 'widget/helpers/WidgetAudioNotificationHelper';
import { ON_AGENT_MESSAGE_RECEIVED } from '../constants/widgetBusEvents';
class ActionCableConnector extends BaseActionCableConnector {
diff --git a/app/javascript/widget/i18n/locale/da.json b/app/javascript/widget/i18n/locale/da.json
index 95512ccce..74e118d39 100644
--- a/app/javascript/widget/i18n/locale/da.json
+++ b/app/javascript/widget/i18n/locale/da.json
@@ -74,8 +74,8 @@
}
},
"EMOJI": {
- "PLACEHOLDER": "Search emojis",
- "NOT_FOUND": "No emoji match your search"
+ "PLACEHOLDER": "Søg efter emojis",
+ "NOT_FOUND": "Ingen emoji matcher din søgning"
},
"CSAT": {
"TITLE": "Bedøm din samtale",
diff --git a/app/javascript/widget/i18n/locale/es.json b/app/javascript/widget/i18n/locale/es.json
index 3c0a8ce8f..07ed0c92d 100644
--- a/app/javascript/widget/i18n/locale/es.json
+++ b/app/javascript/widget/i18n/locale/es.json
@@ -74,8 +74,8 @@
}
},
"EMOJI": {
- "PLACEHOLDER": "Search emojis",
- "NOT_FOUND": "No emoji match your search"
+ "PLACEHOLDER": "Buscar emojis",
+ "NOT_FOUND": "Ningún emoji coincide con tu búsqueda"
},
"CSAT": {
"TITLE": "Califica tu conversación",
diff --git a/app/javascript/widget/i18n/locale/is.json b/app/javascript/widget/i18n/locale/is.json
index 6c97b1fb8..bd993874f 100644
--- a/app/javascript/widget/i18n/locale/is.json
+++ b/app/javascript/widget/i18n/locale/is.json
@@ -74,8 +74,8 @@
}
},
"EMOJI": {
- "PLACEHOLDER": "Search emojis",
- "NOT_FOUND": "No emoji match your search"
+ "PLACEHOLDER": "Leita að emoji",
+ "NOT_FOUND": "Enginn emoji fannst"
},
"CSAT": {
"TITLE": "Gefðu samtalinu einkunn",
diff --git a/app/models/message.rb b/app/models/message.rb
index 2c8827b12..5ff749f6b 100644
--- a/app/models/message.rb
+++ b/app/models/message.rb
@@ -104,7 +104,10 @@ class Message < ApplicationRecord
created_at: created_at.to_i,
message_type: message_type_before_type_cast,
conversation_id: conversation.display_id,
- conversation: { assignee_id: conversation.assignee_id }
+ conversation: {
+ assignee_id: conversation.assignee_id,
+ unread_count: conversation.unread_incoming_messages.count
+ }
)
data.merge!(echo_id: echo_id) if echo_id.present?
validate_instagram_story if instagram_story_mention?
diff --git a/config/app.yml b/config/app.yml
index af9505556..d3a714af3 100644
--- a/config/app.yml
+++ b/config/app.yml
@@ -1,5 +1,5 @@
shared: &shared
- version: '2.12.0'
+ version: '2.12.1'
development:
<<: *shared
diff --git a/config/initializers/devise_token_auth.rb b/config/initializers/devise_token_auth.rb
index f222bd7ba..e8500019b 100644
--- a/config/initializers/devise_token_auth.rb
+++ b/config/initializers/devise_token_auth.rb
@@ -15,7 +15,7 @@ DeviseTokenAuth.setup do |config|
# Sets the max number of concurrent devices per user, which is 10 by default.
# After this limit is reached, the oldest tokens will be removed.
- # config.max_number_of_devices = 10
+ config.max_number_of_devices = 25
# Sometimes it's necessary to make several requests to the API at the same
# time. In this case, each request in the batch will need to share the same
diff --git a/config/locales/da.yml b/config/locales/da.yml
index 5e62be8ef..021bbea46 100644
--- a/config/locales/da.yml
+++ b/config/locales/da.yml
@@ -153,7 +153,7 @@ da:
search:
search_placeholder: Search for article by title or body...
empty_placeholder: Ingen resultater fundet.
- loading_placeholder: Searching...
- results_title: Search results
+ loading_placeholder: Søger...
+ results_title: Søgeresultater
hero:
- sub_title: Search for the articles here or browse the categories below.
+ sub_title: Søg efter artiklerne her eller gennemse kategorierne nedenfor.
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index fa365fa5c..4f6357232 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -48,11 +48,11 @@ fr:
imap:
socket_error: Veuillez vérifier la connexion, l'adresse IMAP et réessayez.
no_response_error: Veuillez vérifier les identifiants IMAP et réessayez.
- host_unreachable_error: Host unreachable, Please check the IMAP address, IMAP port and try again.
- connection_timed_out_error: Connection timed out for %{address}:%{port}
- connection_closed_error: Connection closed.
+ host_unreachable_error: Hôte injoignable, veuillez vérifier l'adresse IMAP, le port IMAP et réessayer.
+ connection_timed_out_error: La connexion a expiré pour %{address}:%{port}
+ connection_closed_error: Connexion fermée.
validations:
- name: should not start or end with symbols, and it should not have < > / \ @ characters.
+ name: 'ne doit pas commencer ou se terminer par des symboles, et ne doit pas comporter les caractères suivants : "< > / \ @".'
reports:
period: Période de rapport %{since} à %{until}
agent_csv:
@@ -61,14 +61,14 @@ fr:
avg_first_response_time: Temps de réponse moyen (Minutes)
avg_resolution_time: Temps moyen de résolution (Minutes)
inbox_csv:
- inbox_name: Inbox name
- inbox_type: Inbox type
- conversations_count: No. of conversations
+ inbox_name: Nom de la boîte de réception
+ inbox_type: Type de boîte de réception
+ conversations_count: Nbre de conversations
avg_first_response_time: Temps de réponse moyen (Minutes)
avg_resolution_time: Temps moyen de résolution (Minutes)
label_csv:
- label_title: Label
- conversations_count: No. of conversations
+ label_title: Libellé
+ conversations_count: Nbre de conversations
avg_first_response_time: Temps de réponse moyen (Minutes)
avg_resolution_time: Temps moyen de résolution (Minutes)
team_csv:
@@ -79,13 +79,13 @@ fr:
default_group_by: jour
csat:
headers:
- contact_name: Contact Name
- contact_email_address: Contact Email Address
- contact_phone_number: Contact Phone Number
- link_to_the_conversation: Link to the conversation
+ contact_name: Nom du contact
+ contact_email_address: Adresse e-mail du contact
+ contact_phone_number: Numéro de téléphone du contact
+ link_to_the_conversation: Lier à la conversation
agent_name: Nom de l'agent
rating: Note
- feedback: Feedback Comment
+ feedback: Commentaire
recorded_at: Recorded date
notifications:
notification_title:
@@ -96,7 +96,7 @@ fr:
conversations:
messages:
instagram_story_content: "%{story_sender} vous a mentionné dans l'histoire: "
- instagram_deleted_story_content: This story is no longer available.
+ instagram_deleted_story_content: Cette Story n'est plus disponible.
deleted: Ce message a été supprimé
activity:
status:
@@ -151,9 +151,9 @@ fr:
description: "L'intégration FullContact permet d'enrichir les profils de visiteurs. Identifiez les utilisateurs dès qu'ils partagent leur adresse de courriel et offrez-leur un service client sur mesure. Connectez FullContact à votre compte en partageant la clé API FullContact."
public_portal:
search:
- search_placeholder: Search for article by title or body...
+ search_placeholder: Rechercher un article par titre ou contenu...
empty_placeholder: Aucun résultat trouvé.
- loading_placeholder: Searching...
- results_title: Search results
+ loading_placeholder: Recherche en cours...
+ results_title: Résultats de recherche
hero:
- sub_title: Search for the articles here or browse the categories below.
+ sub_title: Recherchez les articles ici ou parcourez les catégories ci-dessous.
diff --git a/config/webpack/environment.js b/config/webpack/environment.js
index ee3f385f8..1301fc36e 100644
--- a/config/webpack/environment.js
+++ b/config/webpack/environment.js
@@ -6,7 +6,7 @@ const vue = require('./loaders/vue');
environment.plugins.prepend('VueLoaderPlugin', new VueLoaderPlugin());
environment.loaders.prepend('vue', vue);
-environment.loaders.append('opus', {
+environment.loaders.append('opus-ogg', {
test: /encoderWorker\.min\.js$/,
loader: 'file-loader',
options: {
@@ -14,6 +14,14 @@ environment.loaders.append('opus', {
},
});
+environment.loaders.append('opus-wav', {
+ test: /waveWorker\.min\.js$/,
+ loader: 'file-loader',
+ options: {
+ name: '[name].[ext]',
+ },
+});
+
environment.loaders.append('audio', {
test: /\.(mp3)(\?.*)?$/,
loader: 'url-loader',
diff --git a/enterprise/app/models/enterprise/account.rb b/enterprise/app/models/enterprise/account.rb
index dac6bb7a1..2089247a6 100644
--- a/enterprise/app/models/enterprise/account.rb
+++ b/enterprise/app/models/enterprise/account.rb
@@ -15,7 +15,11 @@ module Enterprise::Account
def get_limits(limit_name)
config_name = "ACCOUNT_#{limit_name.to_s.upcase}_LIMIT"
- self[:limits][limit_name.to_s] || GlobalConfig.get(config_name)[config_name] || ChatwootApp.max_limit
+ return self[:limits][limit_name.to_s] if self[:limits][limit_name.to_s].present?
+
+ return GlobalConfig.get(config_name)[config_name] if GlobalConfig.get(config_name)[config_name].present?
+
+ ChatwootApp.max_limit
end
def validate_limit_keys
diff --git a/package.json b/package.json
index 6d9bfeb46..ee830f12a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@chatwoot/chatwoot",
- "version": "2.12.0",
+ "version": "2.12.1",
"license": "MIT",
"scripts": {
"eslint": "eslint app/**/*.{js,vue}",
diff --git a/public/audio/dashboard/bell.mp3 b/public/audio/dashboard/bell.mp3
new file mode 100644
index 000000000..77087072f
Binary files /dev/null and b/public/audio/dashboard/bell.mp3 differ
diff --git a/spec/enterprise/models/account_spec.rb b/spec/enterprise/models/account_spec.rb
index 16a664ef5..feb87951b 100644
--- a/spec/enterprise/models/account_spec.rb
+++ b/spec/enterprise/models/account_spec.rb
@@ -38,5 +38,27 @@ RSpec.describe Account do
}
)
end
+
+ it 'returns max limits from global config if account limit is absent' do
+ account.update(limits: { agents: '' })
+ expect(account.usage_limits).to eq(
+ {
+ agents: 20,
+ inboxes: ChatwootApp.max_limit
+ }
+ )
+ end
+
+ it 'returns max limits from app limit if account limit and installation config is absent' do
+ account.update(limits: { agents: '' })
+ InstallationConfig.where(name: 'ACCOUNT_AGENTS_LIMIT').update(value: '')
+
+ expect(account.usage_limits).to eq(
+ {
+ agents: ChatwootApp.max_limit,
+ inboxes: ChatwootApp.max_limit
+ }
+ )
+ end
end
end