Merge branch 'develop' into feat/reload-banner-chat-list

This commit is contained in:
Sivin Varghese 2022-03-25 19:21:32 +05:30 committed by GitHub
commit 38488ae37d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
122 changed files with 1582 additions and 380 deletions

View file

@ -50,3 +50,6 @@ exclude_patterns:
- 'app/javascript/dashboard/routes/dashboard/settings/automation/constants.js'
- 'app/javascript/dashboard/components/widgets/FilterInput/FilterOperatorTypes.js'
- 'app/javascript/dashboard/routes/dashboard/settings/reports/constants.js'
- 'app/javascript/dashboard/i18n/index.js'
- 'app/javascript/widget/i18n/index.js'
- 'app/javascript/survey/i18n/index.js'

View file

@ -32,6 +32,11 @@ REDIS_SENTINELS=
# You can find list of master using "SENTINEL masters" command
REDIS_SENTINEL_MASTER_NAME=
# Redis premium breakage in heroku fix
# enable the following configuration
# ref: https://github.com/chatwoot/chatwoot/issues/2420
# REDIS_OPENSSL_VERIFY_MODE=none
# Postgres Database config variables
POSTGRES_HOST=postgres
POSTGRES_USERNAME=postgres

View file

@ -29,8 +29,8 @@ module.exports = {
'vue/html-self-closing': 'off',
"vue/no-v-html": 'off',
'vue/singleline-html-element-content-newline': 'off',
'import/extensions': ['off']
'import/extensions': ['off'],
'no-console': 'error'
},
settings: {
'import/resolver': {

View file

@ -42,7 +42,7 @@ gem 'down', '~> 5.0'
gem 'aws-sdk-s3', require: false
gem 'azure-storage-blob', require: false
gem 'google-cloud-storage', require: false
gem 'image_processing'
gem 'image_processing', '~> 1.12.2'
##-- gems for database --#
gem 'groupdate'

View file

@ -688,7 +688,7 @@ DEPENDENCIES
hairtrigger
hashie
html2text
image_processing
image_processing (~> 1.12.2)
jbuilder
json_refs
json_schemer

View file

@ -32,6 +32,10 @@
"INSTALLATION_ENV": {
"description": "Installation method used for Chatwoot.",
"value": "heroku"
},
"REDIS_OPENSSL_VERIFY_MODE":{
"description": "OpenSSL verification mode for Redis connections. ref https://help.heroku.com/HC0F8CUS/redis-connection-issues",
"value": "none"
}
},
"formation": {

View file

@ -19,7 +19,7 @@ class V2::ReportBuilder
# For backward compatible with old report
def build
timeseries.each_with_object([]) do |p, arr|
arr << { value: p[1], timestamp: p[0].to_time.to_i }
arr << { value: p[1], timestamp: p[0].in_time_zone(@timezone).to_i }
end
end

View file

@ -18,7 +18,7 @@ class Api::V1::Accounts::AgentBotsController < Api::V1::Accounts::BaseController
end
def destroy
@agent_bot.destroy
@agent_bot.destroy!
head :ok
end

View file

@ -18,7 +18,7 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
end
def destroy
@agent.current_account_user.destroy
@agent.current_account_user.destroy!
head :ok
end

View file

@ -34,7 +34,7 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
params.permit(
:name, :description, :event_name, :account_id, :active,
conditions: [:attribute_key, :filter_operator, :query_operator, { values: [] }],
actions: [:action_name, { action_params: [] }]
actions: [:action_name, { action_params: [{}] }]
)
end

View file

@ -11,7 +11,7 @@ class Api::V1::Accounts::CampaignsController < Api::V1::Accounts::BaseController
end
def destroy
@campaign.destroy
@campaign.destroy!
head :ok
end

View file

@ -17,7 +17,7 @@ class Api::V1::Accounts::CannedResponsesController < Api::V1::Accounts::BaseCont
end
def destroy
@canned_response.destroy
@canned_response.destroy!
head :ok
end

View file

@ -10,7 +10,7 @@ class Api::V1::Accounts::Contacts::NotesController < Api::V1::Accounts::Contacts
end
def destroy
@note.destroy
@note.destroy!
head :ok
end

View file

@ -18,7 +18,7 @@ class Api::V1::Accounts::CustomAttributeDefinitionsController < Api::V1::Account
end
def destroy
@custom_attribute_definition.destroy
@custom_attribute_definition.destroy!
head :no_content
end

View file

@ -18,7 +18,7 @@ class Api::V1::Accounts::CustomFiltersController < Api::V1::Accounts::BaseContro
end
def destroy
@custom_filter.destroy
@custom_filter.destroy!
head :no_content
end

View file

@ -48,7 +48,10 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
# Inbox update doesn't necessarily need channel attributes
return if permitted_params(channel_attributes)[:channel].blank?
validate_email_channel(channel_attributes) if @inbox.inbox_type == 'Email'
if @inbox.inbox_type == 'Email'
validate_email_channel(channel_attributes)
@inbox.channel.reauthorized!
end
@inbox.channel.update!(permitted_params(channel_attributes)[:channel])
update_channel_feature_flags
@ -70,7 +73,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
end
def destroy
@inbox.destroy
@inbox.destroy!
head :ok
end

View file

@ -11,7 +11,7 @@ class Api::V1::Accounts::Integrations::HooksController < Api::V1::Accounts::Base
end
def destroy
@hook.destroy
@hook.destroy!
head :ok
end

View file

@ -20,7 +20,7 @@ class Api::V1::Accounts::Integrations::SlackController < Api::V1::Accounts::Base
end
def destroy
@hook.destroy
@hook.destroy!
head :ok
end

View file

@ -14,7 +14,7 @@ class Api::V1::Accounts::Kbase::CategoriesController < Api::V1::Accounts::Kbase:
end
def destroy
@category.destroy
@category.destroy!
head :ok
end

View file

@ -14,7 +14,7 @@ class Api::V1::Accounts::Kbase::PortalsController < Api::V1::Accounts::Kbase::Ba
end
def destroy
@portal.destroy
@portal.destroy!
head :ok
end

View file

@ -18,7 +18,7 @@ class Api::V1::Accounts::LabelsController < Api::V1::Accounts::BaseController
end
def destroy
@label.destroy
@label.destroy!
head :ok
end

View file

@ -18,7 +18,7 @@ class Api::V1::Accounts::TeamsController < Api::V1::Accounts::BaseController
end
def destroy
@team.destroy
@team.destroy!
head :ok
end

View file

@ -16,7 +16,7 @@ class Api::V1::Accounts::WebhooksController < Api::V1::Accounts::BaseController
end
def destroy
@webhook.destroy
@webhook.destroy!
head :ok
end

View file

@ -9,7 +9,7 @@ class Api::V1::NotificationSubscriptionsController < Api::BaseController
def destroy
notification_subscription = NotificationSubscription.where(["subscription_attributes->>'push_token' = ?", params[:push_token]]).first
notification_subscription.destroy
notification_subscription.destroy!
head :ok
end

View file

@ -13,7 +13,7 @@ class Platform::Api::V1::AccountUsersController < PlatformController
end
def destroy
@resource.account_users.find_by(user_id: account_user_params[:user_id])&.destroy
@resource.account_users.find_by(user_id: account_user_params[:user_id])&.destroy!
head :ok
end

View file

@ -7,9 +7,9 @@ class Platform::Api::V1::UsersController < PlatformController
def create
@resource = (User.find_by(email: user_params[:email]) || User.new(user_params))
@resource.skip_confirmation!
@resource.save!
@resource.confirm
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
@platform_app.platform_app_permissibles.find_or_create_by!(permissible: @resource)
end
def login

View file

@ -26,7 +26,8 @@
code {
border: 0;
font-family: 'Monaco', Verdana;
font-family: 'ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas',
'"Liberation Mono"', '"Courier New"', 'monospace';
font-size: $font-size-mini;
&.hljs {
@ -55,7 +56,6 @@ code {
padding-right: var(--space-normal);
}
.badge {
border-radius: var(--border-radius-normal);
}

View file

@ -8,6 +8,7 @@
:active-menu-item="activePrimaryMenu.key"
@toggle-accounts="toggleAccountModal"
@key-shortcut-modal="toggleKeyShortcutModal"
@open-notification-panel="openNotificationPanel"
/>
<secondary-sidebar
:account-id="accountId"
@ -176,6 +177,9 @@ export default {
showAddLabelPopup() {
this.$emit('show-add-label-popup');
},
openNotificationPanel() {
this.$emit('open-notification-panel');
},
},
};
</script>

View file

@ -1,19 +1,21 @@
<template>
<div class="notifications-link">
<primary-nav-item
name="NOTIFICATIONS"
icon="alert"
:to="`/app/accounts/${accountId}/notifications`"
:count="unreadCount"
/>
<woot-button
class-names="notifications-link--button"
variant="clear"
color-scheme="secondary"
:class="{ 'is-active': isNotificationPanelActive }"
@click="openNotificationPanel"
>
<fluent-icon icon="alert" />
<span v-if="unreadCount" class="badge warning">{{ unreadCount }}</span>
</woot-button>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import PrimaryNavItem from './PrimaryNavItem';
export default {
components: { PrimaryNavItem },
computed: {
...mapGetters({
accountId: 'getCurrentAccountId',
@ -28,8 +30,17 @@ export default {
? `${this.notificationMetadata.unreadCount}`
: '99+';
},
isNotificationPanelActive() {
return this.$route.name === 'notifications_index';
},
},
methods: {
openNotificationPanel() {
if (this.$route.name !== 'notifications_index') {
this.$emit('open-notification-panel');
}
},
},
methods: {},
};
</script>
@ -37,4 +48,32 @@ export default {
.notifications-link {
margin-bottom: var(--space-small);
}
.badge {
position: absolute;
right: var(--space-minus-smaller);
top: var(--space-minus-smaller);
}
.notifications-link--button {
display: flex;
position: relative;
border-radius: var(--border-radius-large);
border: 1px solid transparent;
color: var(--s-600);
margin: var(--space-small) 0;
&:hover {
background: var(--w-50);
color: var(--s-600);
}
&:focus {
border-color: var(--w-500);
}
&.is-active {
background: var(--w-50);
color: var(--w-500);
}
}
</style>

View file

@ -16,7 +16,7 @@
/>
</nav>
<div class="menu vertical user-menu">
<notification-bell />
<notification-bell @open-notification-panel="openNotificationPanel" />
<agent-details @toggle-menu="toggleOptions" />
<options-menu
:show="showOptionsMenu"
@ -83,6 +83,9 @@ export default {
toggleSupportChatWindow() {
window.$chatwoot.toggle();
},
openNotificationPanel() {
this.$emit('open-notification-panel');
},
},
};
</script>

View file

@ -14,6 +14,10 @@ const i18nConfig = new VueI18n({
messages: i18n,
});
const $route = {
name: 'notifications_index',
};
describe('notificationBell', () => {
const accountId = 1;
const notificationMetadata = { unreadCount: 19 };
@ -45,24 +49,40 @@ describe('notificationBell', () => {
});
it('it should return unread count 19 ', () => {
const notificationBell = shallowMount(NotificationBell, {
store,
const wrapper = shallowMount(NotificationBell, {
localVue,
i18n: i18nConfig,
store,
mocks: {
$route,
},
});
const statusViewTitle = notificationBell.find('primary-nav-item-stub');
expect(statusViewTitle.vm.count).toBe('19');
expect(wrapper.vm.unreadCount).toBe('19');
});
it('it should return unread count 99+ ', async () => {
notificationMetadata.unreadCount = 101;
notificationMetadata.unreadCount = 100;
const wrapper = shallowMount(NotificationBell, {
localVue,
i18n: i18nConfig,
store,
mocks: {
$route,
},
});
expect(wrapper.vm.unreadCount).toBe('99+');
});
it('isNotificationPanelActive', async () => {
const notificationBell = shallowMount(NotificationBell, {
store,
localVue,
i18n: i18nConfig,
mocks: {
$route,
},
});
const statusViewTitle = notificationBell.find('primary-nav-item-stub');
expect(statusViewTitle.vm.count).toBe('99+');
expect(notificationBell.vm.isNotificationPanelActive).toBe(true);
});
});

View file

@ -1,5 +1,5 @@
<template>
<div>
<div class="preview-item__wrap">
<div
v-for="(attachment, index) in attachments"
:key="attachment.id"
@ -19,7 +19,7 @@
</span>
</div>
<div class="file-size-wrap">
<span class="item">
<span class="item text-truncate">
{{ formatFileSize(attachment.resource) }}
</span>
</div>
@ -70,15 +70,23 @@ export default {
};
</script>
<style lang="scss" scoped>
.preview-item__wrap {
display: flex;
flex-direction: column;
overflow: auto;
margin-top: var(--space-normal);
max-height: 20rem;
}
.preview-item {
display: flex;
padding: var(--space-slab) 0 0;
background: var(--color-background-light);
background: var(--b-50);
border-radius: var(--border-radius-normal);
width: fit-content;
width: 24rem;
padding: var(--space-smaller);
margin-top: var(--space-normal);
margin-bottom: var(--space-one);
}
.thumb-wrap {
@ -114,6 +122,7 @@ export default {
> .item {
margin: 0;
overflow: hidden;
font-size: var(--font-size-mini);
font-weight: var(--font-weight-medium);
}
@ -124,7 +133,8 @@ export default {
}
.file-name-wrap {
max-width: 50%;
max-width: 60%;
min-width: 50%;
overflow: hidden;
text-overflow: ellipsis;
margin-left: var(--space-small);

View file

@ -186,6 +186,12 @@ export default {
blur: () => {
this.onBlur();
},
paste: (view, event) => {
const data = event.clipboardData.files;
if (data.length > 0) {
event.preventDefault();
}
},
},
});
this.focusEditorInputField();

View file

@ -453,12 +453,16 @@ export default {
methods: {
onPaste(e) {
const data = e.clipboardData.files;
if (!this.showRichContentEditor && data.length !== 0) {
this.$refs.messageInput.$el.blur();
}
if (!data.length || !data[0]) {
return;
}
const file = data[0];
const { name, type, size } = file;
this.onFileUpload({ name, type, size, file });
data.forEach(file => {
const { name, type, size } = file;
this.onFileUpload({ name, type, size, file: file });
});
},
toggleUserMention(currentMentionState) {
this.hasUserMention = currentMentionState;

View file

@ -8,7 +8,7 @@
<div class="input-group-field">
<woot-input
v-model.trim="$v.ccEmailsVal.$model"
type="email"
type="text"
:class="{ error: $v.ccEmailsVal.$error }"
:placeholder="$t('CONVERSATION.REPLYBOX.EMAIL_HEAD.CC.PLACEHOLDER')"
@blur="onBlur"
@ -35,7 +35,7 @@
<div class="input-group-field">
<woot-input
v-model.trim="$v.bccEmailsVal.$model"
type="email"
type="text"
:class="{ error: $v.bccEmailsVal.$error }"
:placeholder="
$t('CONVERSATION.REPLYBOX.EMAIL_HEAD.BCC.PLACEHOLDER')

View file

@ -22,7 +22,7 @@
"is_not_present": "غير موجود",
"is_greater_than": "هو أكبر من",
"is_less_than": "هو أقل من",
"days_before": "Is x days before"
"days_before": "قبل x أيام"
},
"ATTRIBUTE_LABELS": {
"TRUE": "صحيح",
@ -44,7 +44,7 @@
"CUSTOM_ATTRIBUTE_NUMBER": "العدد",
"CUSTOM_ATTRIBUTE_LINK": "الرابط",
"CUSTOM_ATTRIBUTE_CHECKBOX": "مربع",
"CREATED_AT": "Created At",
"CREATED_AT": "تم إنشاؤها في",
"LAST_ACTIVITY": "آخر نشاط"
},
"GROUPS": {

View file

@ -23,7 +23,7 @@
"is_not_present": "غير موجود",
"is_greater_than": "هو أكبر من",
"is_lesser_than": "هو أقل من",
"days_before": "Is x days before"
"days_before": "قبل x أيام"
},
"ATTRIBUTES": {
"NAME": "الاسم",
@ -37,7 +37,7 @@
"CUSTOM_ATTRIBUTE_NUMBER": "العدد",
"CUSTOM_ATTRIBUTE_LINK": "الرابط",
"CUSTOM_ATTRIBUTE_CHECKBOX": "مربع",
"CREATED_AT": "Created At",
"CREATED_AT": "تم إنشاؤها في",
"LAST_ACTIVITY": "آخر نشاط"
},
"GROUPS": {

View file

@ -48,7 +48,7 @@
}
},
"UPDATE_CHATWOOT": "يتوفر تحديث %{latestChatwootVersion} لـ Chatwoot. الرجاء التحديث.",
"LEARN_MORE": "Learn more"
"LEARN_MORE": "اعرف المزيد"
},
"FORMS": {
"MULTISELECT": {

View file

@ -60,6 +60,13 @@
"NOTIFICATIONS_PAGE": {
"HEADER": "Notifications",
"MARK_ALL_DONE": "Mark All Done",
"DELETE_TITLE": "deleted",
"UNREAD_NOTIFICATION": {
"TITLE": "Unread Notifications",
"ALL_NOTIFICATIONS": "View all notifications",
"LOADING_UNREAD_MESSAGE": "Loading unread notifications...",
"EMPTY_MESSAGE": "You have no unread notifications"
},
"LIST": {
"LOADING_MESSAGE": "Loading notifications...",
"404": "No Notifications",

View file

@ -1,41 +1,41 @@
{
"AUTOMATION": {
"HEADER": "Automations",
"HEADER": "Automazioni",
"HEADER_BTN_TXT": "Aggiungi regola di automazione",
"LOADING": "Fetching automation rules",
"SIDEBAR_TXT": "<p><b>Automation Rules</b> <p>Automation can replace and automate existing processes that require manual effort. You can do many things with automation, including adding labels and assigning conversation to the best agent. So the team focuses on what they do best and spends more little time on manual tasks.</p>",
"LOADING": "Recupero delle regole di automazione",
"SIDEBAR_TXT": "<p><b>Regole di automazione</b> <p>L'automazione può sostituire e automatizzare i processi esistenti che richiedono uno sforzo manuale. È possibile fare molte cose con l'automazione, tra cui l'aggiunta di etichette e l'assegnazione di una conversazione al miglior agente. Così il team si concentra su quello che fanno meglio e spende meno tempo per le attività manuali.</p>",
"ADD": {
"TITLE": "Aggiungi regola di automazione",
"SUBMIT": "Crea",
"CANCEL_BUTTON_TEXT": "annulla",
"FORM": {
"NAME": {
"LABEL": "Rule Name",
"PLACEHOLDER": "Enter rule name",
"LABEL": "Nome regola",
"PLACEHOLDER": "Inserisci il nome della regola",
"ERROR": "Il nome è obbligatorio"
},
"DESC": {
"LABEL": "Descrizione",
"PLACEHOLDER": "Enter rule description",
"PLACEHOLDER": "Inserisci la descrizione della regola",
"ERROR": "La descrizione è obbligatoria"
},
"EVENT": {
"LABEL": "Event",
"LABEL": "Evento",
"PLACEHOLDER": "Si prega di selezionarne uno",
"ERROR": "Event is required"
"ERROR": "L'evento è obbligatorio"
},
"CONDITIONS": {
"LABEL": "Conditions"
"LABEL": "Condizioni"
},
"ACTIONS": {
"LABEL": "Azioni"
}
},
"CONDITION_BUTTON_LABEL": "Add Condition",
"ACTION_BUTTON_LABEL": "Add Action",
"CONDITION_BUTTON_LABEL": "Aggiungi condizione",
"ACTION_BUTTON_LABEL": "Aggiungi azione",
"API": {
"SUCCESS_MESSAGE": "Automation rule added successfully",
"ERROR_MESSAGE": "Could not able to create a automation rule, Please try again later"
"SUCCESS_MESSAGE": "Regola di automazione aggiunta con successo",
"ERROR_MESSAGE": "Impossibile creare una regola di automazione, riprova più tardi"
}
},
"LIST": {
@ -43,12 +43,12 @@
"Nome",
"Descrizione",
"Attivo",
"Created on"
"Creato il"
],
"404": "No automation rules found"
"404": "Nessuna regola di automazione trovata"
},
"DELETE": {
"TITLE": "Delete Automation Rule",
"TITLE": "Elimina regola di automazione",
"SUBMIT": "Elimina",
"CANCEL_BUTTON_TEXT": "annulla",
"CONFIRM": {
@ -58,24 +58,24 @@
"NO": "No, Conserva "
},
"API": {
"SUCCESS_MESSAGE": "Automation rule deleted successfully",
"ERROR_MESSAGE": "Could not able to delete a automation rule, Please try again later"
"SUCCESS_MESSAGE": "Regola di automazione eliminata con successo",
"ERROR_MESSAGE": "Impossibile eliminare una regola di automazione, riprova più tardi"
}
},
"EDIT": {
"TITLE": "Edit Automation Rule",
"TITLE": "Modifica regola di automazione",
"SUBMIT": "Aggiorna",
"CANCEL_BUTTON_TEXT": "annulla",
"API": {
"SUCCESS_MESSAGE": "Automation rule updated successfully",
"ERROR_MESSAGE": "Could not update automation rule, Please try again later"
"SUCCESS_MESSAGE": "Regola di automazione aggiornata con successo",
"ERROR_MESSAGE": "Impossibile aggiornare la regola di automazione, riprova più tardi"
}
},
"CLONE": {
"TOOLTIP": "Clone",
"TOOLTIP": "Clona",
"API": {
"SUCCESS_MESSAGE": "Automation cloned successfully",
"ERROR_MESSAGE": "Could not clone automation rule, Please try again later"
"SUCCESS_MESSAGE": "Automazione clonata con successo",
"ERROR_MESSAGE": "Impossibile clonare la regola di automazione, riprova più tardi"
}
},
"FORM": {
@ -83,24 +83,24 @@
"CREATE": "Crea",
"DELETE": "Elimina",
"CANCEL": "annulla",
"RESET_MESSAGE": "Changing event type will reset the conditions and events you have added below"
"RESET_MESSAGE": "Cambiare il tipo di evento resetterà le condizioni e gli eventi che hai aggiunto di seguito"
},
"CONDITION": {
"DELETE_MESSAGE": "You need to have atleast one condition to save"
"DELETE_MESSAGE": "È necessario avere almeno una condizione per salvare"
},
"ACTION": {
"DELETE_MESSAGE": "You need to have atleast one action to save"
"DELETE_MESSAGE": "È necessario avere almeno una azione da salvare"
},
"TOGGLE": {
"ACTIVATION_TITLE": "Activate Automation Rule",
"DEACTIVATION_TITLE": "Deactivate Automation Rule",
"ACTIVATION_DESCRIPTION": "This action will activate the automation rule '{automationName}'. Are you sure you want to proceed?",
"DEACTIVATION_DESCRIPTION": "This action will deactivate the automation rule '{automationName}'. Are you sure you want to proceed?",
"ACTIVATION_SUCCESFUL": "Automation Rule Activated Successfully",
"DEACTIVATION_SUCCESFUL": "Automation Rule Deactivated Successfully",
"ACTIVATION_ERROR": "Could not Activate Automation, Please try again later",
"DEACTIVATION_ERROR": "Could not Deactivate Automation, Please try again later",
"CONFIRMATION_LABEL": "Yes",
"ACTIVATION_TITLE": "Attiva regola di automazione",
"DEACTIVATION_TITLE": "Disattiva regola di automazione",
"ACTIVATION_DESCRIPTION": "Questa azione attiverà la regola di automazione '{automationName}'. Sei sicuro di voler procedere?",
"DEACTIVATION_DESCRIPTION": "Questa azione disattiverà la regola di automazione '{automationName}'. Sei sicuro di voler procedere?",
"ACTIVATION_SUCCESFUL": "Regola di automazione attivata con successo",
"DEACTIVATION_SUCCESFUL": "Regola di automazione disattivata con successo",
"ACTIVATION_ERROR": "Impossibile attivare l'automazione, riprova più tardi",
"DEACTIVATION_ERROR": "Impossibile disattivare l'automazione, riprova più tardi",
"CONFIRMATION_LABEL": "",
"CANCEL_LABEL": "No"
}
}

View file

@ -186,7 +186,7 @@
"NO_CONTACTS": "There are no available contacts",
"TABLE_HEADER": {
"NAME": "Nome",
"PHONE_NUMBER": "Phone Number",
"PHONE_NUMBER": "Numero di telefono",
"CONVERSATIONS": "Conversazioni",
"LAST_ACTIVITY": "Last Activity",
"COUNTRY": "Country",

View file

@ -2,7 +2,7 @@
"GENERAL_SETTINGS": {
"TITLE": "Impostazioni account",
"SUBMIT": "Aggiorna le impostazioni",
"BACK": "Back",
"BACK": "Indietro",
"UPDATE": {
"ERROR": "Impossibile aggiornare le impostazioni, riprova!",
"SUCCESS": "Impostazioni account aggiornate con successo"
@ -14,8 +14,8 @@
"NOTE": ""
},
"ACCOUNT_ID": {
"TITLE": "Account ID",
"NOTE": "This ID is required if you are building an API based integration"
"TITLE": "ID Account",
"NOTE": "Questo ID è richiesto se si sta costruendo un'integrazione basata su API"
},
"NAME": {
"LABEL": "Nome account",
@ -28,8 +28,8 @@
"ERROR": ""
},
"DOMAIN": {
"LABEL": "Incoming Email Domain",
"PLACEHOLDER": "The domain where you will receive the emails",
"LABEL": "Dominio email in entrata",
"PLACEHOLDER": "Il dominio in cui riceverai le email",
"ERROR": ""
},
"SUPPORT_EMAIL": {
@ -38,95 +38,95 @@
"ERROR": ""
},
"AUTO_RESOLVE_DURATION": {
"LABEL": "Number of days after a ticket should auto resolve if there is no activity",
"LABEL": "Numero di giorni dopo che un ticket dovrebbe risolvere automaticamente se non c'è attività",
"PLACEHOLDER": "30",
"ERROR": "Please enter a valid auto resolve duration (minimum 1 day and maximum 999 days)"
"ERROR": "Inserisci una durata di risoluzione automatica valida (minimo 1 giorno e massimo 999 giorni)"
},
"FEATURES": {
"INBOUND_EMAIL_ENABLED": "Conversation continuity with emails is enabled for your account.",
"CUSTOM_EMAIL_DOMAIN_ENABLED": "You can receive emails in your custom domain now."
"INBOUND_EMAIL_ENABLED": "La continuità della conversazione con le email è abilitata per il tuo account.",
"CUSTOM_EMAIL_DOMAIN_ENABLED": "Puoi ricevere email nel tuo dominio personalizzato ora."
}
},
"UPDATE_CHATWOOT": "An update %{latestChatwootVersion} for Chatwoot is available. Please update your instance.",
"LEARN_MORE": "Learn more"
"UPDATE_CHATWOOT": "È disponibile un aggiornamento %{latestChatwootVersion} per Chatwoot. Aggiorna la tua istanza.",
"LEARN_MORE": "Scopri di più"
},
"FORMS": {
"MULTISELECT": {
"ENTER_TO_SELECT": "Press enter to select",
"ENTER_TO_REMOVE": "Press enter to remove",
"SELECT_ONE": "Select one"
"ENTER_TO_SELECT": "Premi Invio per selezionare",
"ENTER_TO_REMOVE": "Premi Invio per rimuovere",
"SELECT_ONE": "Selezionane uno"
}
},
"NOTIFICATIONS_PAGE": {
"HEADER": "Notifiche",
"MARK_ALL_DONE": "Mark All Done",
"MARK_ALL_DONE": "Contrassegna tutto come fatto",
"LIST": {
"LOADING_MESSAGE": "Loading notifications...",
"404": "No Notifications",
"LOADING_MESSAGE": "Caricamento notifiche...",
"404": "Nessuna notifica",
"TABLE_HEADER": [
"Nome",
"Phone Number",
"Numero di telefono",
"Conversazioni",
"Last Contacted"
"Ultimo contattato"
]
},
"TYPE_LABEL": {
"conversation_creation": "New conversation",
"conversation_assignment": "Conversation Assigned",
"assigned_conversation_new_message": "New Message",
"conversation_mention": "Mention"
"conversation_creation": "Nuova conversazione",
"conversation_assignment": "Conversazione assegnata",
"assigned_conversation_new_message": "Nuovo messaggio",
"conversation_mention": "Menzione"
}
},
"NETWORK": {
"NOTIFICATION": {
"TEXT": "Disconnected from Chatwoot"
"TEXT": "Disconnesso da Chatwoot"
},
"BUTTON": {
"REFRESH": "Refresh"
"REFRESH": "Aggiorna"
}
},
"COMMAND_BAR": {
"SEARCH_PLACEHOLDER": "Search or jump to",
"SEARCH_PLACEHOLDER": "Cerca o salta a",
"SECTIONS": {
"GENERAL": "General",
"GENERAL": "Generale",
"REPORTS": "Segnalazioni",
"CONVERSATION": "Conversazioni",
"CHANGE_ASSIGNEE": "Change Assignee",
"CHANGE_TEAM": "Change Team",
"ADD_LABEL": "Add label to the conversation",
"REMOVE_LABEL": "Remove label from the conversation",
"CHANGE_ASSIGNEE": "Cambia assegnatario",
"CHANGE_TEAM": "Cambia Team",
"ADD_LABEL": "Aggiungi etichetta alla conversazione",
"REMOVE_LABEL": "Rimuovi etichetta dalla conversazione",
"SETTINGS": "Impostazioni"
},
"COMMANDS": {
"GO_TO_CONVERSATION_DASHBOARD": "Vai alla dashboard Conversazioni",
"GO_TO_CONTACTS_DASHBOARD": "Vai alla dashboard Contatti",
"GO_TO_REPORTS_OVERVIEW": "Go to Reports Overview",
"GO_TO_AGENT_REPORTS": "Go to Agent Reports",
"GO_TO_LABEL_REPORTS": "Go to Label Reports",
"GO_TO_INBOX_REPORTS": "Go to Inbox Reports",
"GO_TO_TEAM_REPORTS": "Go to Team Reports",
"GO_TO_SETTINGS_AGENTS": "Go to Agent Settings",
"GO_TO_SETTINGS_TEAMS": "Go to Team Settings",
"GO_TO_SETTINGS_INBOXES": "Go to Inbox Settings",
"GO_TO_SETTINGS_LABELS": "Go to Label Settings",
"GO_TO_SETTINGS_CANNED_RESPONSES": "Go to Canned Response Settings",
"GO_TO_SETTINGS_APPLICATIONS": "Go to Application Settings",
"GO_TO_SETTINGS_ACCOUNT": "Go to Account Settings",
"GO_TO_SETTINGS_PROFILE": "Go to Profile Settings",
"GO_TO_NOTIFICATIONS": "Go to Notifications",
"ADD_LABELS_TO_CONVERSATION": "Add label to the conversation",
"ASSIGN_AN_AGENT": "Assign an agent",
"ASSIGN_A_TEAM": "Assign a team",
"MUTE_CONVERSATION": "Mute conversation",
"UNMUTE_CONVERSATION": "Unmute conversation",
"REMOVE_LABEL_FROM_CONVERSATION": "Remove label from the conversation",
"GO_TO_REPORTS_OVERVIEW": "Vai alla panoramica dei report",
"GO_TO_AGENT_REPORTS": "Vai ai report degli agenti",
"GO_TO_LABEL_REPORTS": "Vai ai report delle etichette",
"GO_TO_INBOX_REPORTS": "Vai ai report delle caselle",
"GO_TO_TEAM_REPORTS": "Vai ai report dei team",
"GO_TO_SETTINGS_AGENTS": "Vai alle impostazioni dell'agente",
"GO_TO_SETTINGS_TEAMS": "Vai alle impostazioni del team",
"GO_TO_SETTINGS_INBOXES": "Vai alle impostazioni delle caselle",
"GO_TO_SETTINGS_LABELS": "Vai alle impostazioni delle etichette",
"GO_TO_SETTINGS_CANNED_RESPONSES": "Vai alle impostazioni delle risposte predefinite",
"GO_TO_SETTINGS_APPLICATIONS": "Vai alle impostazioni dell'applicazione",
"GO_TO_SETTINGS_ACCOUNT": "Vai alle impostazioni dell'account",
"GO_TO_SETTINGS_PROFILE": "Vai alle impostazioni del profilo",
"GO_TO_NOTIFICATIONS": "Vai alle notifiche",
"ADD_LABELS_TO_CONVERSATION": "Aggiungi etichetta alla conversazione",
"ASSIGN_AN_AGENT": "Assegna un agente",
"ASSIGN_A_TEAM": "Assegna un team",
"MUTE_CONVERSATION": "Silenzia conversazione",
"UNMUTE_CONVERSATION": "Riattiva conversazione",
"REMOVE_LABEL_FROM_CONVERSATION": "Rimuovi etichetta dalla conversazione",
"REOPEN_CONVERSATION": "Riapri la conversazione",
"RESOLVE_CONVERSATION": "Risolvi la conversazione",
"SEND_TRANSCRIPT": "Send an email transcript",
"SNOOZE_CONVERSATION": "Snooze Conversation",
"UNTIL_NEXT_REPLY": "Until next reply",
"UNTIL_NEXT_WEEK": "Until next week",
"UNTIL_TOMORROW": "Until tomorrow"
"SEND_TRANSCRIPT": "Invia una trascrizione email",
"SNOOZE_CONVERSATION": "Posticipa conversazione",
"UNTIL_NEXT_REPLY": "Fino alla prossima risposta",
"UNTIL_NEXT_WEEK": "Fino alla prossima settimana",
"UNTIL_TOMORROW": "Fino a domani"
}
}
}

View file

@ -148,7 +148,7 @@
},
"BANDWIDTH": {
"ACCOUNT_ID": {
"LABEL": "Account ID",
"LABEL": "ID Account",
"PLACEHOLDER": "Please enter your Bandwidth Account ID",
"ERROR": "Questo campo è obbligatorio"
},

View file

@ -22,11 +22,11 @@
"is_not_present": "Não está presente",
"is_greater_than": "É maior do que",
"is_less_than": "É menor do que",
"days_before": "Is x days before"
"days_before": "É x dias antes"
},
"ATTRIBUTE_LABELS": {
"TRUE": "True",
"FALSE": "False"
"TRUE": "Verdadeiro",
"FALSE": "Falso"
},
"ATTRIBUTES": {
"STATUS": "SItuação",
@ -44,7 +44,7 @@
"CUSTOM_ATTRIBUTE_NUMBER": "Número",
"CUSTOM_ATTRIBUTE_LINK": "Endereço",
"CUSTOM_ATTRIBUTE_CHECKBOX": "Caixa de Seleção",
"CREATED_AT": "Created At",
"CREATED_AT": "Criado Em",
"LAST_ACTIVITY": "Última atividade"
},
"GROUPS": {

View file

@ -29,7 +29,7 @@
"PLACEHOLDER": "Por favor, selecione um tipo",
"ERROR": "Tipo é obrigatório",
"LIST": {
"LABEL": "List Values",
"LABEL": "Listar Valores",
"PLACEHOLDER": "Por favor insira um valor e pressione Enter",
"ERROR": "Deve possuir pelo menos um valor"
}
@ -42,7 +42,7 @@
}
},
"API": {
"SUCCESS_MESSAGE": "Custom Attribute added successfully",
"SUCCESS_MESSAGE": "Atributo Personalizado adicionado com sucesso",
"ERROR_MESSAGE": "Could not able to create a custom attribute, Please try again later"
}
},

View file

@ -1,38 +1,38 @@
{
"AUTOMATION": {
"HEADER": "Automations",
"HEADER_BTN_TXT": "Add Automation Rule",
"HEADER": "Automatizações",
"HEADER_BTN_TXT": "Adicionar Regra de Automação",
"LOADING": "Fetching automation rules",
"SIDEBAR_TXT": "<p><b>Automation Rules</b> <p>Automation can replace and automate existing processes that require manual effort. You can do many things with automation, including adding labels and assigning conversation to the best agent. So the team focuses on what they do best and spends more little time on manual tasks.</p>",
"ADD": {
"TITLE": "Add Automation Rule",
"TITLE": "Adicionar Regra de Automação",
"SUBMIT": "Criar",
"CANCEL_BUTTON_TEXT": "cancelar",
"FORM": {
"NAME": {
"LABEL": "Rule Name",
"PLACEHOLDER": "Enter rule name",
"LABEL": "Nome da Regra",
"PLACEHOLDER": "Insira nome da regra",
"ERROR": "Nome é obrigatório"
},
"DESC": {
"LABEL": "Descrição",
"PLACEHOLDER": "Enter rule description",
"PLACEHOLDER": "Insera descrição da regra",
"ERROR": "Descrição é obrigatória"
},
"EVENT": {
"LABEL": "Event",
"LABEL": "Evento",
"PLACEHOLDER": "Por favor selecione um",
"ERROR": "Event is required"
"ERROR": "Evento é necessário"
},
"CONDITIONS": {
"LABEL": "Conditions"
"LABEL": "Condições"
},
"ACTIONS": {
"LABEL": "Ações."
}
},
"CONDITION_BUTTON_LABEL": "Add Condition",
"ACTION_BUTTON_LABEL": "Add Action",
"CONDITION_BUTTON_LABEL": "Adicionar Condição",
"ACTION_BUTTON_LABEL": "Adicionar Ação",
"API": {
"SUCCESS_MESSAGE": "Automation rule added successfully",
"ERROR_MESSAGE": "Could not able to create a automation rule, Please try again later"
@ -43,12 +43,12 @@
"Nome:",
"Descrição",
"Ativa",
"Created on"
"Criado em"
],
"404": "No automation rules found"
"404": "Nenhuma regra de automação encontrada"
},
"DELETE": {
"TITLE": "Delete Automation Rule",
"TITLE": "Apagar Regra de Automação",
"SUBMIT": "excluir",
"CANCEL_BUTTON_TEXT": "cancelar",
"CONFIRM": {
@ -63,7 +63,7 @@
}
},
"EDIT": {
"TITLE": "Edit Automation Rule",
"TITLE": "Editar Regra de Automação",
"SUBMIT": "Atualização",
"CANCEL_BUTTON_TEXT": "cancelar",
"API": {
@ -74,8 +74,8 @@
"CLONE": {
"TOOLTIP": "Clone",
"API": {
"SUCCESS_MESSAGE": "Automation cloned successfully",
"ERROR_MESSAGE": "Could not clone automation rule, Please try again later"
"SUCCESS_MESSAGE": "Automação clonada com sucesso",
"ERROR_MESSAGE": "Não foi possível clonar regra de automação, por favor, tente novamente mais tarde"
}
},
"FORM": {
@ -86,20 +86,20 @@
"RESET_MESSAGE": "Changing event type will reset the conditions and events you have added below"
},
"CONDITION": {
"DELETE_MESSAGE": "You need to have atleast one condition to save"
"DELETE_MESSAGE": "É necessário ter pelo menos uma condição para salvar"
},
"ACTION": {
"DELETE_MESSAGE": "You need to have atleast one action to save"
"DELETE_MESSAGE": "É necessário ter pelo menos uma ação para salvar"
},
"TOGGLE": {
"ACTIVATION_TITLE": "Activate Automation Rule",
"DEACTIVATION_TITLE": "Deactivate Automation Rule",
"ACTIVATION_DESCRIPTION": "This action will activate the automation rule '{automationName}'. Are you sure you want to proceed?",
"DEACTIVATION_DESCRIPTION": "This action will deactivate the automation rule '{automationName}'. Are you sure you want to proceed?",
"ACTIVATION_SUCCESFUL": "Automation Rule Activated Successfully",
"DEACTIVATION_SUCCESFUL": "Automation Rule Deactivated Successfully",
"ACTIVATION_ERROR": "Could not Activate Automation, Please try again later",
"DEACTIVATION_ERROR": "Could not Deactivate Automation, Please try again later",
"ACTIVATION_TITLE": "Ativar Regra de Automação",
"DEACTIVATION_TITLE": "Desativar Regra de Automação",
"ACTIVATION_DESCRIPTION": "Esta ação irá ativar a regra de automação '{automationName}'. Tem a certeza que deseja continuar?",
"DEACTIVATION_DESCRIPTION": "Esta ação irá desativar a regra de automação '{automationName}'. Tem a certeza que deseja continuar?",
"ACTIVATION_SUCCESFUL": "Regra de Automação Ativada com Sucesso",
"DEACTIVATION_SUCCESFUL": "Regra de Automação Desativada com Sucesso",
"ACTIVATION_ERROR": "Não foi possível Ativar a Automação, por favor, tente novamente mais tarde",
"DEACTIVATION_ERROR": "Não foi possível Desativar a Automação, por favor, tente novamente mais tarde",
"CONFIRMATION_LABEL": "Sim",
"CANCEL_LABEL": "Não"
}

View file

@ -54,7 +54,7 @@
"ERROR": "O tempo na página é obrigatório"
},
"ENABLED": "Ativar a campanha",
"TRIGGER_ONLY_BUSINESS_HOURS": "Trigger only during business hours",
"TRIGGER_ONLY_BUSINESS_HOURS": "Ativar apenas durante o horário de trabalho",
"SUBMIT": "Adicionar Campanha"
},
"API": {

View file

@ -1,15 +1,15 @@
{
"CONTACTS_FILTER": {
"TITLE": "Filter Contacts",
"SUBTITLE": "Add filters below and hit 'Submit' to filter contacts.",
"TITLE": "Filtrar Contactos",
"SUBTITLE": "Adicionar filtros abaixo e clicar 'Submeter' para filtrar contactos.",
"ADD_NEW_FILTER": "Adicionar Filtro",
"CLEAR_ALL_FILTERS": "Clear All Filters",
"CLEAR_ALL_FILTERS": "Limpar Todos os Filtros",
"FILTER_DELETE_ERROR": "Você deve ter pelo menos um filtro para guardar",
"SUBMIT_BUTTON_LABEL": "submeter",
"CANCEL_BUTTON_LABEL": "cancelar",
"CLEAR_BUTTON_LABEL": "Limpar Filtros",
"EMPTY_VALUE_ERROR": "Valor obrigatório",
"TOOLTIP_LABEL": "Filter contacts",
"TOOLTIP_LABEL": "Filtrar contactos",
"QUERY_DROPDOWN_LABELS": {
"AND": "E",
"OR": "OU"
@ -23,13 +23,13 @@
"is_not_present": "Não está presente",
"is_greater_than": "É maior do que",
"is_lesser_than": "É menor do que",
"days_before": "Is x days before"
"days_before": "É x dias antes"
},
"ATTRIBUTES": {
"NAME": "Nome:",
"EMAIL": "e-mail",
"PHONE_NUMBER": "Número de telefone",
"IDENTIFIER": "Identifier",
"IDENTIFIER": "Identificador",
"CITY": "Cidade",
"COUNTRY": "País",
"CUSTOM_ATTRIBUTE_LIST": "Lista",
@ -37,7 +37,7 @@
"CUSTOM_ATTRIBUTE_NUMBER": "Número",
"CUSTOM_ATTRIBUTE_LINK": "Endereço",
"CUSTOM_ATTRIBUTE_CHECKBOX": "Caixa de Seleção",
"CREATED_AT": "Created At",
"CREATED_AT": "Criado Em",
"LAST_ACTIVITY": "Última atividade"
},
"GROUPS": {

View file

@ -57,9 +57,9 @@
}
},
"FOOTER": {
"MESSAGE_SIGN_TOOLTIP": "Message signature",
"ENABLE_SIGN_TOOLTIP": "Enable signature",
"DISABLE_SIGN_TOOLTIP": "Disable signature",
"MESSAGE_SIGN_TOOLTIP": "Assinatura da mensagem",
"ENABLE_SIGN_TOOLTIP": "Habilitar assinatura",
"DISABLE_SIGN_TOOLTIP": "Desativar assinatura",
"MSG_INPUT": "Shift + enter para nova linha. Comece com '/' para selecionar uma Resposta Pronta.",
"PRIVATE_MSG_INPUT": "Shift + Enter para a nova linha. Isto será visível apenas para Agentes",
"MESSAGE_SIGNATURE_NOT_CONFIGURED": "Message signature is not configured, please configure it in profile settings.",
@ -166,7 +166,7 @@
},
"ACCORDION": {
"CONTACT_DETAILS": "Detalhes do Contacto",
"CONVERSATION_ACTIONS": "Conversation Actions",
"CONVERSATION_ACTIONS": "Ações de Conversa",
"CONVERSATION_LABELS": "Etiquetas da conversa",
"CONVERSATION_INFO": "Conversation Information",
"CONTACT_ATTRIBUTES": "Atributos do Contato",

View file

@ -48,7 +48,7 @@
}
},
"UPDATE_CHATWOOT": "Está disponível uma nova atualização %{latestChatwootVersion} para o ChatWoot. Por favor, atualize a sua versão.",
"LEARN_MORE": "Learn more"
"LEARN_MORE": "Saiba mais"
},
"FORMS": {
"MULTISELECT": {
@ -79,7 +79,7 @@
},
"NETWORK": {
"NOTIFICATION": {
"TEXT": "Disconnected from Chatwoot"
"TEXT": "Desconectado do Chatwoot"
},
"BUTTON": {
"REFRESH": "Atualizar"
@ -116,17 +116,17 @@
"GO_TO_NOTIFICATIONS": "Ir para Notificações",
"ADD_LABELS_TO_CONVERSATION": "Adicionar etiqueta à conversa",
"ASSIGN_AN_AGENT": "Atribuir um agente",
"ASSIGN_A_TEAM": "Assign a team",
"ASSIGN_A_TEAM": "Atribuir uma equipa",
"MUTE_CONVERSATION": "Silenciar Conversa",
"UNMUTE_CONVERSATION": "Unmute conversation",
"UNMUTE_CONVERSATION": "Reativar conversa",
"REMOVE_LABEL_FROM_CONVERSATION": "Remover etiqueta da conversa",
"REOPEN_CONVERSATION": "Reopen conversation",
"RESOLVE_CONVERSATION": "Resolve conversation",
"SEND_TRANSCRIPT": "Send an email transcript",
"REOPEN_CONVERSATION": "Reabrir conversa",
"RESOLVE_CONVERSATION": "Resolver conversa",
"SEND_TRANSCRIPT": "Enviar transcrição por e-mail",
"SNOOZE_CONVERSATION": "Snooze Conversation",
"UNTIL_NEXT_REPLY": "Until next reply",
"UNTIL_NEXT_WEEK": "Until next week",
"UNTIL_TOMORROW": "Until tomorrow"
"UNTIL_NEXT_REPLY": "Até à próxima resposta",
"UNTIL_NEXT_WEEK": "Até à próxima semana",
"UNTIL_TOMORROW": "Até amanhã"
}
}
}

View file

@ -61,7 +61,7 @@
},
"CHANNEL_WEBHOOK_URL": {
"LABEL": "URL do Webhook",
"PLACEHOLDER": "Enter your Webhook URL",
"PLACEHOLDER": "Introduza o seu URL Webhook",
"ERROR": "Por favor, insira uma URL válida"
},
"CHANNEL_DOMAIN": {
@ -118,7 +118,7 @@
},
"CHANNEL_NAME": {
"LABEL": "Nome Caixa de Entrada",
"PLACEHOLDER": "Please enter a inbox name",
"PLACEHOLDER": "Por favor, insira um nome para a caixa de entrada",
"ERROR": "Este campo é obrigatório"
},
"PHONE_NUMBER": {
@ -137,14 +137,14 @@
},
"SMS": {
"TITLE": "Canal SMS",
"DESC": "Start supporting your customers via SMS.",
"DESC": "Comece a apoiar os seus clientes via SMS.",
"PROVIDERS": {
"LABEL": "API Provider",
"TWILIO": "Twilio",
"BANDWIDTH": "Banda"
},
"API": {
"ERROR_MESSAGE": "We were not able to save the SMS channel"
"ERROR_MESSAGE": "Não conseguimos salvar o canal de SMS"
},
"BANDWIDTH": {
"ACCOUNT_ID": {
@ -469,7 +469,7 @@
"TITLE": "IMAP",
"SUBTITLE": "Defina os seus dados IMAP",
"UPDATE": "Atualizar configurações IMAP",
"TOGGLE_AVAILABILITY": "Enable IMAP configuration for this inbox",
"TOGGLE_AVAILABILITY": "Ativar a configuração IMAP para esta caixa de entrada",
"TOGGLE_HELP": "Enabling IMAP will help the user to recieve email",
"EDIT": {
"SUCCESS_MESSAGE": "IMAP settings updated successfully",

View file

@ -30,7 +30,7 @@
},
"LIST": {
"FETCHING": "Procurando Hooks de integração",
"INBOX": "Recebidas",
"INBOX": "Caixa de Entrada",
"DELETE": {
"BUTTON_TEXT": "excluir"
}

View file

@ -240,7 +240,7 @@
"HEADER": "Visão Geral da Caixa de Entrada",
"LOADING_CHART": "Carregando dados da carta...",
"NO_ENOUGH_DATA": "Não recebemos pontos de dados suficientes para gerar o relatório. Por favor, tente novamente mais tarde.",
"DOWNLOAD_INBOX_REPORTS": "Download inbox reports",
"DOWNLOAD_INBOX_REPORTS": "Descarregar relatórios de caixa de entrada",
"FILTER_DROPDOWN_LABEL": "Escolher caixa de entrada",
"METRICS": {
"CONVERSATIONS": {
@ -300,11 +300,11 @@
}
},
"TEAM_REPORTS": {
"HEADER": "Team Overview",
"HEADER": "Resumo de Equipa",
"LOADING_CHART": "Carregando dados da carta...",
"NO_ENOUGH_DATA": "Não recebemos pontos de dados suficientes para gerar o relatório. Por favor, tente novamente mais tarde.",
"DOWNLOAD_TEAM_REPORTS": "Download team reports",
"FILTER_DROPDOWN_LABEL": "Select Team",
"DOWNLOAD_TEAM_REPORTS": "Descarregar relatórios de equipa",
"FILTER_DROPDOWN_LABEL": "Escolher Equipa",
"METRICS": {
"CONVERSATIONS": {
"NAME": "Conversas",
@ -367,7 +367,7 @@
"NO_RECORDS": "Sem dados CSAT disponíveis para reposta.",
"FILTERS": {
"AGENTS": {
"PLACEHOLDER": "Choose Agents"
"PLACEHOLDER": "Escolher Agentes"
}
},
"TABLE": {

View file

@ -101,21 +101,21 @@
"PLACEHOLDER": "Por favor, digite a password atual"
},
"PASSWORD": {
"LABEL": "New password",
"LABEL": "Nova senha",
"ERROR": "Por favor, digite uma senha de comprimento 6 ou mais",
"PLACEHOLDER": "Por favor, digite uma nova senha"
},
"PASSWORD_CONFIRMATION": {
"LABEL": "Confirme a nova senha",
"ERROR": "Confirme a senha deve corresponder à senha",
"PLACEHOLDER": "Please re-enter your new password"
"PLACEHOLDER": "Por favor, digite novamente a sua senha nova"
}
}
},
"SIDEBAR_ITEMS": {
"CHANGE_AVAILABILITY_STATUS": "Trocar",
"CHANGE_ACCOUNTS": "Trocar de conta",
"CONTACT_SUPPORT": "Contact Support",
"CONTACT_SUPPORT": "Contactar Suporte",
"SELECTOR_SUBTITLE": "Escolha uma conta da lista a seguir",
"PROFILE_SETTINGS": "Configurações do perfil",
"KEYBOARD_SHORTCUTS": "Atalhos do teclado",
@ -124,7 +124,7 @@
"APP_GLOBAL": {
"TRIAL_MESSAGE": "dias de teste restantes.",
"TRAIL_BUTTON": "Comprar agora",
"DELETED_USER": "Deleted User"
"DELETED_USER": "Usuário Excluído"
},
"COMPONENTS": {
"CODE": {
@ -164,15 +164,15 @@
"APPLICATIONS": "Aplicações",
"LABELS": "Etiquetas",
"CUSTOM_ATTRIBUTES": "Atributos personalizados",
"AUTOMATION": "Automation",
"AUTOMATION": "Automatização",
"TEAMS": "Equipas",
"CUSTOM_VIEWS_FOLDER": "Folders",
"CUSTOM_VIEWS_SEGMENTS": "Segments",
"CUSTOM_VIEWS_FOLDER": "Pastas",
"CUSTOM_VIEWS_SEGMENTS": "Segmentos",
"ALL_CONTACTS": "Todos os contatos",
"TAGGED_WITH": "Etiquetada com",
"NEW_LABEL": "New label",
"NEW_TEAM": "New team",
"NEW_INBOX": "New inbox",
"NEW_LABEL": "Nova etiqueta",
"NEW_TEAM": "Nova equipa",
"NEW_INBOX": "Nova caixa de entrada",
"REPORTS_OVERVIEW": "Visão geral",
"CSAT": "CSAT",
"CAMPAIGNS": "Campanhas",
@ -181,8 +181,8 @@
"REPORTS_AGENT": "Agentes",
"REPORTS_LABEL": "Etiquetas",
"REPORTS_INBOX": "Caixa de Entrada",
"REPORTS_TEAM": "Team",
"SET_AVAILABILITY_TITLE": "Set yourself as",
"REPORTS_TEAM": "Equipa",
"SET_AVAILABILITY_TITLE": "Defina-se como",
"BETA": "Beta"
},
"CREATE_ACCOUNT": {
@ -215,7 +215,7 @@
"GO_TO_REPORTS_SIDEBAR": "Ir para barra lateral de Relatórios",
"MOVE_TO_NEXT_TAB": "Mover para próximo separador da lista de conversas",
"GO_TO_SETTINGS": "Ir para as configurações",
"SWITCH_CONVERSATION_STATUS": "Switch to the next conversation status",
"SWITCH_CONVERSATION_STATUS": "Mudar para o próximo estado de conversa",
"SWITCH_TO_PRIVATE_NOTE": "Alterar para nota privada",
"TOGGLE_RICH_CONTENT_EDITOR": "Ativar/desativar editor de conteúdo",
"SWITCH_TO_REPLY": "Mudar para resposta",

View file

@ -3,6 +3,7 @@
<sidebar
:route="currentRoute"
:class="sidebarClassName"
@open-notification-panel="openNotificationPanel"
@toggle-account-modal="toggleAccountModal"
@open-key-shortcut-modal="toggleKeyShortcutModal"
@close-key-shortcut-modal="closeKeyShortcutModal"
@ -25,6 +26,10 @@
@close="closeKeyShortcutModal"
@clickaway="closeKeyShortcutModal"
/>
<notification-panel
v-if="isNotificationPanel"
@close="closeNotificationPanel"
/>
<woot-modal :show.sync="showAddLabelModal" :on-close="hideAddLabelPopup">
<add-label-modal @close="hideAddLabelPopup" />
</woot-modal>
@ -40,6 +45,7 @@ import WootKeyShortcutModal from 'dashboard/components/widgets/modal/WootKeyShor
import AddAccountModal from 'dashboard/components/layout/sidebarComponents/AddAccountModal';
import AccountSelector from 'dashboard/components/layout/sidebarComponents/AccountSelector';
import AddLabelModal from 'dashboard/routes/dashboard/settings/labels/AddLabel.vue';
import NotificationPanel from 'dashboard/routes/dashboard/notifications/components/NotificationPanel.vue';
export default {
components: {
@ -49,6 +55,7 @@ export default {
AddAccountModal,
AccountSelector,
AddLabelModal,
NotificationPanel,
},
data() {
return {
@ -58,6 +65,7 @@ export default {
showCreateAccountModal: false,
showAddLabelModal: false,
showShortcutModal: false,
isNotificationPanel: false,
};
},
computed: {
@ -126,6 +134,12 @@ export default {
hideAddLabelPopup() {
this.showAddLabelModal = false;
},
openNotificationPanel() {
this.isNotificationPanel = true;
},
closeNotificationPanel() {
this.isNotificationPanel = false;
},
},
};
</script>

View file

@ -80,7 +80,11 @@ export default {
return false;
},
},
watch: {
conversationId() {
this.fetchConversationIfUnavailable();
},
},
mounted() {
this.$store.dispatch('agents/get');
this.initialize();

View file

@ -59,7 +59,29 @@
</div>
<div class="row">
<div class="columns">
<label :class="{ error: $v.message.$error }">
<div v-if="isAnEmailInbox || isAnWebWidgetInbox">
<label>
{{ $t('NEW_CONVERSATION.FORM.MESSAGE.LABEL') }}
<reply-email-head
v-if="isAnEmailInbox"
:cc-emails.sync="ccEmails"
:bcc-emails.sync="bccEmails"
/>
<label class="editor-wrap">
<woot-message-editor
v-model="message"
class="message-editor"
:class="{ editor_warning: $v.message.$error }"
:placeholder="$t('NEW_CONVERSATION.FORM.MESSAGE.PLACEHOLDER')"
@blur="$v.message.$touch"
/>
<span v-if="$v.message.$error" class="editor-warning__message">
{{ $t('NEW_CONVERSATION.FORM.MESSAGE.ERROR') }}
</span>
</label>
</label>
</div>
<label v-else :class="{ error: $v.message.$error }">
{{ $t('NEW_CONVERSATION.FORM.MESSAGE.LABEL') }}
<textarea
v-model="message"
@ -89,6 +111,8 @@
<script>
import { mapGetters } from 'vuex';
import Thumbnail from 'dashboard/components/widgets/Thumbnail';
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor';
import ReplyEmailHead from 'dashboard/components/widgets/conversation/ReplyEmailHead';
import alertMixin from 'shared/mixins/alertMixin';
import { INBOX_TYPES } from 'shared/mixins/inboxMixin';
@ -98,6 +122,8 @@ import { required, requiredIf } from 'vuelidate/lib/validators';
export default {
components: {
Thumbnail,
WootMessageEditor,
ReplyEmailHead,
},
mixins: [alertMixin],
props: {
@ -116,6 +142,8 @@ export default {
subject: '',
message: '',
selectedInbox: '',
bccEmails: '',
ccEmails: '',
};
},
validations: {
@ -136,7 +164,7 @@ export default {
currentUser: 'getCurrentUser',
}),
getNewConversation() {
return {
const payload = {
inboxId: this.targetInbox.inbox.id,
sourceId: this.targetInbox.source_id,
contactId: this.contact.id,
@ -144,6 +172,14 @@ export default {
mailSubject: this.subject,
assigneeId: this.currentUser.id,
};
if (this.ccEmails) {
payload.message.cc_emails = this.ccEmails;
}
if (this.bccEmails) {
payload.message.bcc_emails = this.bccEmails;
}
return payload;
},
targetInbox: {
get() {
@ -168,6 +204,12 @@ export default {
this.selectedInbox.inbox.channel_type === INBOX_TYPES.EMAIL
);
},
isAnWebWidgetInbox() {
return (
this.selectedInbox &&
this.selectedInbox.inbox.channel_type === INBOX_TYPES.WEB
);
},
},
methods: {
onCancel() {

View file

@ -0,0 +1,274 @@
<template>
<div class="modal-mask">
<div
v-on-clickaway="closeNotificationPanel"
class="notification-wrap flex-space-between"
>
<div class="header-wrap w-full flex-space-between">
<div class="header-title--wrap flex-view">
<span class="header-title">
{{ $t('NOTIFICATIONS_PAGE.UNREAD_NOTIFICATION.TITLE') }}
</span>
<span v-if="totalUnreadNotifications" class="total-count block-title">
{{ totalUnreadNotifications }}
</span>
</div>
<div class="flex-view">
<woot-button
v-if="!noUnreadNotificationAvailable"
color-scheme="primary"
variant="smooth"
size="tiny"
class-names="action-button"
:is-loading="uiFlags.isUpdating"
@click="onMarkAllDoneClick"
>
{{ $t('NOTIFICATIONS_PAGE.MARK_ALL_DONE') }}
</woot-button>
<woot-button
color-scheme="secondary"
variant="link"
size="tiny"
icon="dismiss"
@click="closeNotificationPanel"
/>
</div>
</div>
<notification-panel-list
:notifications="getUnreadNotifications"
:is-loading="uiFlags.isFetching"
:on-click-notification="openConversation"
:in-last-page="inLastPage"
/>
<div v-if="records.length !== 0" class="footer-wrap flex-space-between">
<div class="flex-view">
<woot-button
size="medium"
variant="clear"
color-scheme="secondary"
class-names="page-change--button"
:is-disabled="inFirstPage"
@click="onClickFirstPage"
>
<fluent-icon icon="chevron-left" size="16" />
<fluent-icon
icon="chevron-left"
size="16"
class="margin-left-minus-slab"
/>
</woot-button>
<woot-button
color-scheme="secondary"
variant="clear"
size="medium"
icon="chevron-left"
:disabled="inFirstPage"
@click="onClickPreviousPage"
>
</woot-button>
</div>
<span class="page-count"> {{ currentPage }} - {{ lastPage }} </span>
<div class="flex-view">
<woot-button
color-scheme="secondary"
variant="clear"
size="medium"
icon="chevron-right"
:disabled="inLastPage"
@click="onClickNextPage"
>
</woot-button>
<woot-button
size="medium"
variant="clear"
color-scheme="secondary"
class-names="page-change--button"
:disabled="inLastPage"
@click="onClickLastPage"
>
<fluent-icon icon="chevron-right" size="16" />
<fluent-icon
icon="chevron-right"
size="16"
class="margin-left-minus-slab"
/>
</woot-button>
</div>
</div>
<div v-else></div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import { mixin as clickaway } from 'vue-clickaway';
import NotificationPanelList from './NotificationPanelList';
export default {
components: {
NotificationPanelList,
},
mixins: [clickaway],
data() {
return {
pageSize: 15,
};
},
computed: {
...mapGetters({
accountId: 'getCurrentAccountId',
meta: 'notifications/getMeta',
records: 'notifications/getNotifications',
uiFlags: 'notifications/getUIFlags',
}),
totalUnreadNotifications() {
return this.meta.unreadCount;
},
noUnreadNotificationAvailable() {
return this.meta.unreadCount === 0;
},
getUnreadNotifications() {
return this.records.filter(notification => notification.read_at === null);
},
currentPage() {
return Number(this.meta.currentPage);
},
lastPage() {
if (this.totalUnreadNotifications > 15) {
return Math.ceil(this.totalUnreadNotifications / this.pageSize);
}
return 1;
},
inFirstPage() {
const page = Number(this.meta.currentPage);
return page === 1;
},
inLastPage() {
return this.currentPage === this.lastPage;
},
},
mounted() {
this.$store.dispatch('notifications/get', { page: 1 });
},
methods: {
onPageChange(page) {
this.$store.dispatch('notifications/get', { page });
},
openConversation(notification) {
const {
primary_actor_id: primaryActorId,
primary_actor_type: primaryActorType,
primary_actor: { id: conversationId },
} = notification;
this.$store.dispatch('notifications/read', {
primaryActorId,
primaryActorType,
unreadCount: this.meta.unreadCount,
});
this.$router.push({
name: 'inbox_conversation',
params: { conversation_id: conversationId },
});
this.$emit('close');
},
onClickNextPage() {
if (!this.inLastPage) {
const page = this.currentPage + 1;
this.onPageChange(page);
}
},
onClickPreviousPage() {
if (!this.inFirstPage) {
const page = this.currentPage - 1;
this.onPageChange(page);
}
},
onClickFirstPage() {
if (!this.inFirstPage) {
const page = 1;
this.onPageChange(page);
}
},
onClickLastPage() {
if (!this.inLastPage) {
const page = this.lastPage;
this.onPageChange(page);
}
},
onMarkAllDoneClick() {
this.$store.dispatch('notifications/readAll');
},
closeNotificationPanel() {
this.$emit('close');
},
},
};
</script>
<style lang="scss" scoped>
.flex-view {
display: flex;
}
.flex-space-between {
display: flex;
justify-content: space-between;
}
.notification-wrap {
flex-direction: column;
height: 90vh;
width: 52rem;
background-color: var(--white);
border-radius: var(--border-radius-medium);
position: absolute;
left: var(--space-jumbo);
margin: var(--space-small);
}
.header-wrap {
flex-direction: row;
align-items: center;
border-bottom: 1px solid var(--s-50);
padding: var(--space-two) var(--space-medium) var(--space-slab)
var(--space-medium);
.header-title--wrap {
align-items: center;
}
.header-title {
font-size: var(--font-size-two);
font-weight: var(--font-weight-black);
}
.total-count {
padding: var(--space-smaller) var(--space-small);
background: var(--b-50);
border-radius: var(--border-radius-rounded);
font-size: var(--font-size-micro);
font-weight: var(--font-weight-bold);
}
.action-button {
padding: var(--space-micro) var(--space-small);
margin-right: var(--space-small);
}
}
.page-count {
font-size: var(--font-size-micro);
font-weight: var(--font-weight-bold);
color: var(--s-500);
}
.footer-wrap {
align-items: center;
padding: var(--space-smaller) var(--space-two);
}
.page-change--button:hover {
background: var(--s-50);
}
</style>

View file

@ -0,0 +1,219 @@
<template>
<div class="notification-list-item--wrap h-full flex-view ">
<woot-button
v-for="notificationItem in notifications"
v-show="!isLoading"
:key="notificationItem.id"
size="expanded"
color-scheme="secondary"
variant="link"
@click="() => onClickNotification(notificationItem)"
>
<div class="notification-list--wrap flex-view w-full">
<div
v-if="!notificationItem.read_at"
class="notification-unread--indicator"
></div>
<div v-else class="empty flex-view"></div>
<div class="notification-content--wrap w-full flex-space-between">
<div class="flex-space-between">
<div class="title-wrap flex-view ">
<span class="notification-title">
{{
`#${
notificationItem.primary_actor
? notificationItem.primary_actor.id
: $t(`NOTIFICATIONS_PAGE.DELETE_TITLE`)
}`
}}
</span>
<span class="notification-type">
{{
$t(
`NOTIFICATIONS_PAGE.TYPE_LABEL.${notificationItem.notification_type}`
)
}}
</span>
</div>
<div>
<thumbnail
v-if="notificationItem.primary_actor.meta.assignee"
:src="notificationItem.primary_actor.meta.assignee.thumbnail"
size="16px"
:username="notificationItem.primary_actor.meta.assignee.name"
/>
</div>
</div>
<div class="w-full flex-view ">
<span class="notification-message text-truncate">
{{ notificationItem.push_message_title }}
</span>
</div>
<span class="timestamp flex-view">
{{ dynamicTime(notificationItem.created_at) }}
</span>
</div>
</div>
</woot-button>
<empty-state
v-if="showEmptyResult"
:title="$t('NOTIFICATIONS_PAGE.UNREAD_NOTIFICATION.EMPTY_MESSAGE')"
/>
<woot-button
v-if="!isLoading && inLastPage"
size="medium"
variant="clear"
color-scheme="primary"
class-names="action-button"
@click="openNotificationPage"
>
{{ $t('NOTIFICATIONS_PAGE.UNREAD_NOTIFICATION.ALL_NOTIFICATIONS') }}
</woot-button>
<div v-if="isLoading" class="notifications-loader flex-view">
<spinner />
<span>{{
$t('NOTIFICATIONS_PAGE.UNREAD_NOTIFICATION.LOADING_UNREAD_MESSAGE')
}}</span>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import Spinner from 'shared/components/Spinner.vue';
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
import timeMixin from '../../../../mixins/time';
export default {
components: {
Thumbnail,
Spinner,
EmptyState,
},
mixins: [timeMixin],
props: {
notifications: {
type: Array,
default: () => [],
},
isLoading: {
type: Boolean,
default: true,
},
onClickNotification: {
type: Function,
default: () => {},
},
inLastPage: {
type: Boolean,
default: false,
},
},
computed: {
...mapGetters({
notificationMetadata: 'notifications/getMeta',
}),
showEmptyResult() {
return !this.isLoading && this.notifications.length === 0;
},
},
methods: {
openNotificationPage() {
if (this.$route.name !== 'notifications_index') {
this.$router.push({
name: 'notifications_index',
});
}
},
},
};
</script>
<style lang="scss" scoped>
.flex-view {
display: flex;
}
.flex-space-between {
display: flex;
justify-content: space-between;
}
.notification-list-item--wrap {
flex-direction: column;
padding: var(--space-small) var(--space-slab);
overflow: scroll;
}
.empty {
width: var(--space-small);
}
.notification-list--wrap {
flex-direction: row;
align-items: center;
padding: var(--space-slab);
line-height: 1.4;
border-bottom: 1px solid var(--b-50);
}
.notification-list--wrap:hover {
background: var(--b-100);
border-radius: var(--border-radius-normal);
}
.notification-content--wrap {
flex-direction: column;
margin-left: var(--space-slab);
overflow: hidden;
}
.title-wrap {
align-items: center;
}
.notification-title {
font-weight: var(--font-weight-black);
}
.notification-type {
font-size: var(--font-size-micro);
padding: var(--space-micro) var(--space-smaller);
margin-left: var(--space-small);
background: var(--s-50);
border-radius: var(--border-radius-normal);
}
.notification-message {
color: var(--color-body);
font-weight: var(--font-weight-normal);
}
.timestamp {
margin-top: var(--space-smaller);
color: var(--b-500);
font-size: var(--font-size-micro);
font-weight: var(--font-weight-bold);
}
.notification-unread--indicator {
width: var(--space-small);
height: var(--space-small);
border-radius: var(--border-radius-rounded);
background: var(--color-woot);
}
.action-button {
margin-top: var(--space-slab);
}
.notifications-loader {
align-items: center;
justify-content: center;
margin: var(--space-larger) var(--space-small);
font-size: var(--font-size-small);
font-weight: var(--font-weight-medium);
}
</style>

View file

@ -17,17 +17,17 @@
@click="() => onClickNotification(notificationItem)"
>
<td>
<div class="">
<div class="flex-view notification-contant--wrap">
<h5 class="notification--title">
{{
`#${
notificationItem.primary_actor
? notificationItem.primary_actor.id
: 'deleted'
: $t(`NOTIFICATIONS_PAGE.DELETE_TITLE`)
}`
}}
</h5>
<span class="notification--message-title">
<span class="notification--message-title text-truncate">
{{ notificationItem.push_message_title }}
</span>
</div>
@ -197,6 +197,11 @@ export default {
text-align: right;
}
.notification-contant--wrap {
flex-direction: column;
max-width: 50rem;
}
.notification--message-title {
color: var(--s-700);
}

View file

@ -79,7 +79,7 @@ export const AUTOMATIONS = {
// {
// key: 'send_email_to_team',
// name: 'Send an email to team',
// attributeI18nKey: 'SEND_MESSAGE',
// attributeI18nKey: 'SEND_EMAIL_TO_TEAM',
// },
],
},
@ -186,7 +186,7 @@ export const AUTOMATIONS = {
// {
// key: 'send_email_to_team',
// name: 'Send an email to team',
// attributeI18nKey: 'SEND_MESSAGE',
// attributeI18nKey: 'SEND_EMAIL_TO_TEAM',
// },
{
key: 'assign_agent',

View file

@ -66,9 +66,6 @@ export const actions = {
id: contactId,
data: response.data.payload,
});
commit(types.default.SET_ALL_CONVERSATION, response.data.payload, {
root: true,
});
commit(types.default.SET_CONTACT_CONVERSATIONS_UI_FLAG, {
isFetching: false,
});

View file

@ -4,6 +4,7 @@ import getters, { getSelectedChatConversation } from './getters';
import actions from './actions';
import { findPendingMessageIndex } from './helpers';
import wootConstants from '../../../constants';
import { BUS_EVENTS } from '../../../../shared/constants/busEvents';
const state = {
allConversations: [],
@ -109,7 +110,7 @@ export const mutations = {
chat.messages.push(message);
chat.timestamp = message.created_at;
if (selectedChatId === conversationId) {
window.bus.$emit('scrollToMessage');
window.bus.$emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
}
}
},
@ -131,7 +132,7 @@ export const mutations = {
};
Vue.set(allConversations, currentConversationIndex, currentConversation);
if (_state.selectedChatId === conversation.id) {
window.bus.$emit('scrollToMessage');
window.bus.$emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
}
} else {
_state.allConversations.push(conversation);

View file

@ -19,7 +19,6 @@ describe('#actions', () => {
types.default.SET_CONTACT_CONVERSATIONS,
{ id: 1, data: conversationList },
],
[types.default.SET_ALL_CONVERSATION, conversationList, { root: true }],
[
types.default.SET_CONTACT_CONVERSATIONS_UI_FLAG,
{ isFetching: false },

View file

@ -120,7 +120,7 @@ describe('#mutations', () => {
timestamp: 1602256198,
},
]);
expect(global.bus.$emit).toHaveBeenCalledWith('scrollToMessage');
expect(global.bus.$emit).toHaveBeenCalledWith('SCROLL_TO_MESSAGE');
});
it('update message if it exist in the store', () => {

View file

@ -3,6 +3,7 @@ import Vuelidate from 'vuelidate';
import VueI18n from 'vue-i18n';
import App from '../survey/App.vue';
import i18n from '../survey/i18n';
import store from '../survey/store';
Vue.use(VueI18n);
Vue.use(Vuelidate);
@ -20,6 +21,7 @@ Vue.config.productionTip = false;
window.onload = () => {
window.WOOT_SURVEY = new Vue({
i18n: i18nConfig,
store,
render: h => h(App),
}).$mount('#app');
};

View file

@ -21,7 +21,6 @@
<script>
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
import { mapGetters } from 'vuex';
const {
LOGO_THUMBNAIL: logoThumbnail,
@ -41,13 +40,20 @@ export default {
};
},
computed: {
...mapGetters({ referrerHost: 'appConfig/getReferrerHost' }),
brandRedirectURL() {
const baseURL = `${this.globalConfig.widgetBrandURL}?utm_source=widget_branding`;
if (this.referrerHost) {
return `${baseURL}&utm_referrer=${this.referrerHost}`;
try {
const referrerHost = this.$store.getters['appConfig/getReferrerHost'];
const baseURL = `${this.globalConfig.widgetBrandURL}?utm_source=${
referrerHost ? 'widget_branding' : 'survey_branding'
}`;
if (referrerHost) {
return `${baseURL}&utm_referrer=${referrerHost}`;
}
return baseURL;
} catch (e) {
// Suppressing the error as getter is not defined in some cases
}
return baseURL;
return '';
},
},
};

View file

@ -1,5 +1,69 @@
import { default as ar } from './locale/ar.json';
import { default as ca } from './locale/ca.json';
import { default as cs } from './locale/cs.json';
import { default as da } from './locale/da.json';
import { default as de } from './locale/de.json';
import { default as el } from './locale/el.json';
import { default as en } from './locale/en.json';
import { default as es } from './locale/es.json';
import { default as fa } from './locale/fa.json';
import { default as fi } from './locale/fi.json';
import { default as fr } from './locale/fr.json';
import { default as hi } from './locale/hi.json';
import { default as hu } from './locale/hu.json';
import { default as id } from './locale/id.json';
import { default as it } from './locale/it.json';
import { default as ja } from './locale/ja.json';
import { default as ko } from './locale/ko.json';
import { default as ml } from './locale/ml.json';
import { default as nl } from './locale/nl.json';
import { default as no } from './locale/no.json';
import { default as pl } from './locale/pl.json';
import { default as pt } from './locale/pt.json';
import { default as pt_BR } from './locale/pt_BR.json';
import { default as ro } from './locale/ro.json';
import { default as ru } from './locale/ru.json';
import { default as sk } from './locale/sk.json';
import { default as sv } from './locale/sv.json';
import { default as ta } from './locale/ta.json';
import { default as tr } from './locale/tr.json';
import { default as uk } from './locale/uk.json';
import { default as vi } from './locale/vi.json';
import { default as zh_CN } from './locale/zh_CN.json';
import { default as zh_TW } from './locale/zh_TW.json';
export default {
ar,
ca,
cs,
da,
de,
el,
en,
es,
fa,
fi,
fr,
hi,
hu,
id,
it,
ja,
ko,
ml,
nl,
no,
pl,
pt,
pt_BR,
ro,
ru,
sk,
sv,
ta,
tr,
uk,
vi,
zh_CN,
zh_TW,
};

View file

@ -0,0 +1,11 @@
import Vue from 'vue';
import Vuex from 'vuex';
import globalConfig from 'shared/store/globalConfig';
Vue.use(Vuex);
export default new Vuex.Store({
modules: {
globalConfig,
},
});

View file

@ -144,6 +144,7 @@ export default {
this.surveyDetails = result?.data?.csat_survey_response;
this.selectedRating = this.surveyDetails?.rating;
this.feedbackMessage = this.surveyDetails?.feedback_message || '';
this.setLocale(result.data.locale);
} catch (error) {
const errorMessage = error?.response?.data?.message;
this.errorMessage = errorMessage || this.$t('SURVEY.API.ERROR_MESSAGE');
@ -179,6 +180,9 @@ export default {
this.isUpdating = false;
}
},
setLocale(locale) {
this.$root.$i18n.locale = locale || 'en';
},
},
};
</script>

View file

@ -51,7 +51,10 @@
.has-attachment {
overflow: hidden;
padding: 0;
:not([audio]) {
padding: 0;
}
&.has-text {
margin-top: $space-smaller;
@ -213,11 +216,14 @@
display: inline-block;
font-size: $font-size-default;
line-height: 1.5;
max-width: 100%;
padding: $space-slab $space-normal;
text-align: left;
word-break: break-word;
:not([audio]) {
max-width: 100%;
}
>a {
color: $color-primary;
word-break: break-all;

View file

@ -34,6 +34,9 @@
:readable-time="readableTime"
@error="onImageLoadError"
/>
<audio v-else-if="attachment.file_type === 'audio'" controls>
<source :src="attachment.data_url" />
</audio>
<file-bubble v-else :url="attachment.data_url" />
</div>
</div>

View file

@ -1,5 +1,5 @@
<template>
<div class="flex overflow-hidden">
<div class="flex">
<span
v-for="(user, index) in users"
:key="user.id"

View file

@ -1,7 +1,7 @@
<template>
<div class="px-5">
<div class="flex items-center justify-between mb-4">
<div class="text-black-700">
<div class="text-black-700 max-w-xs">
<div class="text-base leading-5 font-medium mb-1">
{{
isOnline

View file

@ -22,7 +22,7 @@
"IN_A_DAY": "عادة نقوم بالرد خلال يوم واحد"
},
"START_CONVERSATION": "ابدأ المحادثة",
"END_CONVERSATION": "End Conversation",
"END_CONVERSATION": "إنهاء المحادثة",
"CONTINUE_CONVERSATION": "متابعة المحادثة",
"START_NEW_CONVERSATION": "ابدأ محادثة جديدة",
"UNREAD_VIEW": {

View file

@ -22,7 +22,7 @@
"IN_A_DAY": "Normalmente respondemos num dia"
},
"START_CONVERSATION": "Iniciar Conversa",
"END_CONVERSATION": "End Conversation",
"END_CONVERSATION": "Terminar Conversa",
"CONTINUE_CONVERSATION": "Continuar conversa",
"START_NEW_CONVERSATION": "Iniciar uma nova conversa",
"UNREAD_VIEW": {

View file

@ -40,12 +40,16 @@ export default {
availableAgents: 'agent/availableAgents',
activeCampaign: 'campaign/getActiveCampaign',
conversationSize: 'conversation/getConversationSize',
currentUser: 'contacts/getCurrentUser',
}),
},
methods: {
startConversation() {
const isUserEmailAvailable = !!this.currentUser.email;
if (this.preChatFormEnabled && !this.conversationSize) {
return this.replaceRoute('prechat-form');
return this.replaceRoute('prechat-form', {
disableContactFields: isUserEmailAvailable,
});
}
return this.replaceRoute('messages');
},

View file

@ -1,9 +1,28 @@
require 'net/imap'
class Inboxes::FetchImapEmailsJob < ApplicationJob
queue_as :low
def perform(channel)
return unless channel.imap_enabled?
return unless should_fetch_email?(channel)
process_mail_for_channel(channel)
rescue Errno::ECONNREFUSED, Net::OpenTimeout, Net::IMAP::NoResponseError
channel.authorization_error!
rescue StandardError => e
channel.authorization_error!
Sentry.capture_exception(e)
end
private
def should_fetch_email?(channel)
channel.imap_enabled? && !channel.reauthorization_required?
end
def process_mail_for_channel(channel)
# TODO: rather than setting this as default method for all mail objects, lets if can do new mail object
# using Mail.retriever_method.new(params)
Mail.defaults do
retriever_method :imap, address: channel.imap_address,
port: channel.imap_port,
@ -21,6 +40,6 @@ class Inboxes::FetchImapEmailsJob < ApplicationJob
end
end
Channel::Email.update(channel.id, imap_inbox_synced_at: Time.now.utc) if new_mails
channel.update(imap_inbox_synced_at: Time.now.utc) if new_mails
end
end

View file

@ -1,51 +1,58 @@
class AutomationRuleListener < BaseListener
def conversation_updated(event_obj)
conversation = event_obj.data[:conversation]
return unless rule_present?('conversation_updated', conversation)
account = conversation.account
return unless rule_present?('conversation_updated', account)
@rules.each do |rule|
conditions_match = ::AutomationRules::ConditionsFilterService.new(rule, conversation).perform
AutomationRules::ActionService.new(rule, conversation).perform if conditions_match.present?
AutomationRules::ActionService.new(rule, account, conversation).perform if conditions_match.present?
end
end
def conversation_status_changed(event_obj)
conversation = event_obj.data[:conversation]
return unless rule_present?('conversation_status_changed', conversation)
account = conversation.account
return unless rule_present?('conversation_status_changed', account)
@rules.each do |rule|
conditions_match = ::AutomationRules::ConditionsFilterService.new(rule, conversation).perform
AutomationRules::ActionService.new(rule, conversation).perform if conditions_match.present?
AutomationRules::ActionService.new(rule, account, conversation).perform if conditions_match.present?
end
end
def conversation_created(event_obj)
conversation = event_obj.data[:conversation]
return unless rule_present?('conversation_created', conversation)
account = conversation.account
return unless rule_present?('conversation_created', account)
@rules.each do |rule|
conditions_match = ::AutomationRules::ConditionsFilterService.new(rule, conversation).perform
::AutomationRules::ActionService.new(rule, conversation).perform if conditions_match.present?
::AutomationRules::ActionService.new(rule, account, conversation).perform if conditions_match.present?
end
end
def message_created(event_obj)
message = event_obj.data[:message]
conversation = message.conversation
return unless rule_present?('message_created', conversation)
account = message.try(:account)
return unless rule_present?('message_created', account)
@rules.each do |rule|
conditions_match = ::AutomationRules::ConditionsFilterService.new(rule, conversation).message_conditions(message)
::AutomationRules::ActionService.new(rule, conversation).perform if conditions_match.present?
conditions_match = ::AutomationRules::ConditionsFilterService.new(rule, message.conversation).message_conditions
::AutomationRules::ActionService.new(rule, account, message.conversation).perform if conditions_match.present?
end
end
def rule_present?(event_name, conversation)
return if conversation.blank?
def rule_present?(event_name, account)
return if account.blank?
@rules = AutomationRule.where(
event_name: event_name,
account_id: conversation.account_id,
account_id: account.id,
active: true
)
@rules.any?

View file

@ -5,7 +5,7 @@ module MailboxHelper
@message = @conversation.messages.create(
account_id: @conversation.account_id,
sender: @conversation.contact,
content: mail_content,
content: mail_content&.truncate(150_000),
inbox_id: @conversation.inbox_id,
message_type: 'incoming',
content_type: 'incoming_email',

View file

@ -15,6 +15,14 @@ class AdministratorNotifications::ChannelNotificationsMailer < ApplicationMailer
send_mail_with_liquid(to: admin_emails, subject: subject) and return
end
def email_disconnect(inbox)
return unless smtp_config_set_or_development?
subject = 'Your email inbox has been disconnected. Please update the credentials for SMTP/IMAP'
@action_url = "#{ENV['FRONTEND_URL']}/app/accounts/#{Current.account.id}/settings/inboxes/#{inbox.id}"
send_mail_with_liquid(to: admin_emails, subject: subject) and return
end
private
def admin_emails

View file

@ -40,6 +40,10 @@ module ConversationReplyMailerHelper
end
def email_smtp_enabled
@inbox.inbox_type == 'Email' && @channel.smtp_enabled
end
def email_imap_enabled
@inbox.inbox_type == 'Email' && @channel.imap_enabled
end
@ -48,6 +52,6 @@ module ConversationReplyMailerHelper
end
def email_reply_to
email_smtp_enabled ? @channel.smtp_email : reply_email
email_imap_enabled ? @channel.imap_email : reply_email
end
end

View file

@ -4,50 +4,57 @@ class TeamNotifications::AutomationNotificationMailer < ApplicationMailer
@agents = team.team_members
@conversation = conversation
@message = message
@custom_message = message
@action_url = app_account_conversation_url(account_id: @conversation.account_id, id: @conversation.display_id)
send_an_email_to_team
send_an_email_to_team and return
end
def conversation_updated(conversation, team)
def conversation_updated(conversation, team, message)
return unless smtp_config_set_or_development?
@agents = team.team_members
@conversation = conversation
@message = message
@custom_message = message
@action_url = app_account_conversation_url(account_id: @conversation.account_id, id: @conversation.display_id)
send_an_email_to_team
send_an_email_to_team and return
end
def message_created(message, agent)
def message_created(conversation, team, message)
return unless smtp_config_set_or_development?
@agent = agent
@conversation = message.conversation
@message = message
subject = "#{@agent.available_name}, You have been mentioned in conversation [ID - #{@conversation.display_id}]"
@agents = team.team_members
@conversation = conversation
@custom_message = message
@action_url = app_account_conversation_url(account_id: @conversation.account_id, id: @conversation.display_id)
send_mail_with_liquid(to: @agent.email, subject: subject)
send_an_email_to_team and return
end
private
def send_an_email_to_team
@agents.each do |agent|
subject = "#{@agent.available_name}, A new conversation [ID - #{@conversation.display_id}] has been created in #{@conversation.inbox&.name}."
subject = "#{agent.user.available_name}, This email has been sent via automation rule actions."
@action_url = app_account_conversation_url(account_id: @conversation.account_id, id: @conversation.display_id)
send_mail_with_liquid(to: agent.email, subject: subject)
@agent = agent
send_mail_with_liquid(to: @agent.user.email, subject: subject)
end
end
def liquid_droppables
super.merge({
user: @agent,
conversation: @conversation,
inbox: @conversation.inbox,
message: @message
})
super.merge!({
user: @agent.user,
conversation: @conversation,
inbox: @conversation.inbox
})
end
def liquid_locals
super.merge!({
custom_message: @custom_message
})
end
end

View file

@ -79,6 +79,7 @@ class Account < ApplicationRecord
before_validation :validate_limit_keys
after_create_commit :notify_creation
after_destroy :remove_account_sequences
def agents
users.where(account_users: { role: :agent })
@ -110,7 +111,7 @@ class Account < ApplicationRecord
end
def support_email
super || GlobalConfig.get('MAILER_SUPPORT_EMAIL')['MAILER_SUPPORT_EMAIL'] || ENV.fetch('MAILER_SENDER_EMAIL', 'Chatwoot <accounts@chatwoot.com>')
super || ENV['MAILER_SENDER_EMAIL'] || GlobalConfig.get('MAILER_SUPPORT_EMAIL')['MAILER_SUPPORT_EMAIL']
end
def usage_limits
@ -137,4 +138,9 @@ class Account < ApplicationRecord
def validate_limit_keys
# method overridden in enterprise module
end
def remove_account_sequences
ActiveRecord::Base.connection.exec_query("drop sequence IF EXISTS camp_dpid_seq_#{id}")
ActiveRecord::Base.connection.exec_query("drop sequence IF EXISTS conv_dpid_seq_#{id}")
end
end

View file

@ -20,9 +20,9 @@
class AutomationRule < ApplicationRecord
belongs_to :account
validates :account, presence: true
validate :json_conditions_format
validate :json_actions_format
validates :account_id, presence: true
scope :active, -> { where(active: true) }

View file

@ -34,6 +34,7 @@
class Channel::Email < ApplicationRecord
include Channelable
include Reauthorizable
self.table_name = 'channel_email'
EDITABLE_ATTRS = [:email, :imap_enabled, :imap_email, :imap_password, :imap_address, :imap_port, :imap_enable_ssl, :imap_inbox_synced_at,

View file

@ -20,7 +20,7 @@ module ActivityMessageHandler
def create_status_change_message(user_name)
content = if user_name
I18n.t("conversations.activity.status.#{status}", user_name: user_name)
elsif Current.contact.present?
elsif Current.contact.present? && resolved?
I18n.t('conversations.activity.status.contact_resolved', contact_name: Current.contact.name.capitalize)
elsif resolved?
I18n.t('conversations.activity.status.auto_resolved', duration: auto_resolve_duration)

View file

@ -39,10 +39,11 @@ module Reauthorizable
if (is_a? Integrations::Hook) && slack?
AdministratorNotifications::ChannelNotificationsMailer.with(account: account).slack_disconnect.deliver_later
elsif is_a? Channel::FacebookPage
AdministratorNotifications::ChannelNotificationsMailer.with(account: account).facebook_disconnect(inbox).deliver_later
elsif is_a? Channel::Email
AdministratorNotifications::ChannelNotificationsMailer.with(account: account).email_disconnect(inbox).deliver_later
end
return unless is_a? Channel::FacebookPage
AdministratorNotifications::ChannelNotificationsMailer.with(account: account).facebook_disconnect(inbox).deliver_later
end
# call this after you successfully Reauthorized the object in UI

View file

@ -50,6 +50,8 @@ class Conversation < ApplicationRecord
validates :account_id, presence: true
validates :inbox_id, presence: true
before_validation :validate_additional_attributes
validates :additional_attributes, jsonb_attributes_length: true
validates :custom_attributes, jsonb_attributes_length: true
enum status: { open: 0, resolved: 1, pending: 2, snoozed: 3 }
@ -210,6 +212,8 @@ class Conversation < ApplicationRecord
end
def dispatcher_dispatch(event_name)
return if Current.executed_by.present? && Current.executed_by.instance_of?(AutomationRule)
Rails.configuration.dispatcher.dispatch(event_name, Time.zone.now, conversation: self, notifiable_assignee_change: notifiable_assignee_change?)
end

View file

@ -0,0 +1,21 @@
class JsonbAttributesLengthValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return if value.empty?
@attribute = attribute
@record = record
value.each do |key, attribute_value|
validate_keys(key, attribute_value)
end
end
def validate_keys(key, attribute_value)
case attribute_value.class.name
when 'String'
@record.errors.add @attribute, "#{key} length should be < 1500" if attribute_value.length > 1500
when 'Integer'
@record.errors.add @attribute, "#{key} value should be < 9999999999" if attribute_value > 9_999_999_999
end
end
end

View file

@ -166,6 +166,8 @@ class Message < ApplicationRecord
end
def dispatch_create_events
return if Current.executed_by.present? && Current.executed_by.instance_of?(AutomationRule)
Rails.configuration.dispatcher.dispatch(MESSAGE_CREATED, Time.zone.now, message: self)
if outgoing? && conversation.messages.outgoing.count == 1
@ -174,6 +176,8 @@ class Message < ApplicationRecord
end
def dispatch_update_event
return if Current.executed_by.present? && Current.executed_by.instance_of?(AutomationRule)
Rails.configuration.dispatcher.dispatch(MESSAGE_UPDATED, Time.zone.now, message: self)
end

View file

@ -38,7 +38,7 @@ class Team < ApplicationRecord
end
def remove_member(user_id)
team_members.find_by(user_id: user_id)&.destroy
team_members.find_by(user_id: user_id)&.destroy!
end
def messages

View file

@ -1,40 +1,68 @@
class AutomationRules::ActionService
def initialize(rule, conversation)
def initialize(rule, account, conversation)
@rule = rule
@account = account
@conversation = conversation
@account = @conversation.account
Current.executed_by = rule
end
def perform
@rule.actions.each do |action, _current_index|
@rule.actions.each do |action|
action = action.with_indifferent_access
send(action[:action_name], action[:action_params])
begin
send(action[:action_name], action[:action_params])
rescue StandardError => e
Sentry.capture_exception(e)
end
end
ensure
Current.reset
end
private
def send_email_transcript(email)
ConversationReplyMailer.with(account: conversation.account).conversation_transcript(@conversation, email)&.deliver_later
end
def mute_conversation(_params)
@conversation.mute!
end
def change_status(status)
@conversation.update!(status: status[0])
end
def send_webhook_events(webhook_url)
payload = @conversation.webhook_data.merge(event: "automation_event: #{@rule.event_name}")
WebhookJob.perform_later(webhook_url, payload)
end
def send_message(message)
# params = { content: message, private: false }
# mb = Messages::MessageBuilder.new(@administrator, @conversation, params)
# mb.perform
return if @rule.event_name == 'message_created'
params = { content: message[0], private: false }
mb = Messages::MessageBuilder.new(@administrator, @conversation, params)
mb.perform
end
def assign_team(team_ids = [])
return unless team_belongs_to_account?(team_ids)
@account.teams.find_by(id: team_ids)
@conversation.update!(team_id: team_ids[0])
end
def assign_best_agents(agent_ids = [])
def assign_best_agent(agent_ids = [])
return unless agent_belongs_to_account?(agent_ids)
@agent = @account.users.find_by(id: agent_ids)
@conversation.update_assignee(@agent)
@conversation.update!(assignee_id: @agent.id) if @agent.present?
end
def add_label(labels = [])
def add_label(labels)
return if labels.empty?
@conversation.add_labels(labels)
end
@ -43,11 +71,11 @@ class AutomationRules::ActionService
case @rule.event_name
when 'conversation_created', 'conversation_status_changed'
TeamNotifications::AutomationNotificationMailer.conversation_creation(@conversation, team, params[:message])
TeamNotifications::AutomationNotificationMailer.conversation_creation(@conversation, team, params[:message])&.deliver_now
when 'conversation_updated'
TeamNotifications::AutomationNotificationMailer.conversation_updated(@conversation, team, params[:message])
TeamNotifications::AutomationNotificationMailer.conversation_updated(@conversation, team, params[:message])&.deliver_now
when 'message_created'
TeamNotifications::AutomationNotificationMailer.message_created(@conversation, team, params[:message])
TeamNotifications::AutomationNotificationMailer.message_created(@conversation, team, params[:message])&.deliver_now
end
end

View file

@ -1,7 +1,7 @@
require 'json'
class AutomationRules::ConditionsFilterService < FilterService
def initialize(rule, conversation)
def initialize(rule, conversation = nil)
super([], nil)
@rule = rule
@conversation = conversation
@ -21,14 +21,14 @@ class AutomationRules::ConditionsFilterService < FilterService
records.any?
end
def message_conditions(message)
def message_conditions
message_filters = @filters['messages']
@rule.conditions.each_with_index do |query_hash, current_index|
current_filter = message_filters[query_hash['attribute_key']]
@query_string += message_query_string(current_filter, query_hash.with_indifferent_access, current_index)
end
records = Message.where(id: message.id).where(@query_string, @filter_values.with_indifferent_access)
records = Message.where(conversation: @conversation).where(@query_string, @filter_values.with_indifferent_access)
records.any?
end
@ -44,6 +44,19 @@ class AutomationRules::ConditionsFilterService < FilterService
end
end
# This will be used in future for contact automation rule
def contact_conditions(_contact)
conversation_filters = @filters['conversations']
@rule.conditions.each_with_index do |query_hash, current_index|
current_filter = conversation_filters[query_hash['attribute_key']]
@query_string += conversation_query_string(current_filter, query_hash.with_indifferent_access, current_index)
end
records = base_relation.where(@query_string, @filter_values.with_indifferent_access)
records.any?
end
def conversation_query_string(current_filter, query_hash, current_index)
attribute_key = query_hash['attribute_key']
query_operator = query_hash['query_operator']
@ -63,6 +76,6 @@ class AutomationRules::ConditionsFilterService < FilterService
end
def base_relation
Conversation.where(id: @conversation)
Conversation.where(id: @conversation.id)
end
end

View file

@ -20,6 +20,8 @@ class Instagram::MessageText < Instagram::WebhooksBaseService
# person can connect the channel and then delete the inbox
return if @inbox.blank?
return unsend_message if message_is_deleted?
ensure_contact(contact_id)
create_message
@ -46,6 +48,19 @@ class Instagram::MessageText < Instagram::WebhooksBaseService
@messaging[:message][:is_echo].present?
end
def message_is_deleted?
@messaging[:message][:is_deleted].present?
end
def unsend_message
message_to_delete = @inbox.messages.find_by(
source_id: @messaging[:message][:mid]
)
return if message_to_delete.blank?
message_to_delete.update!(content: I18n.t('conversations.messages.deleted'), deleted: true)
end
def create_message
Messages::Instagram::MessageBuilder.new(@messaging, @inbox, outgoing_echo: agent_message_via_echo?).perform
end

View file

@ -0,0 +1,8 @@
<p>Hello,</p>
<p>Your email inbox has been disconnected due to configuration errors. </p>
<p>Please update it to continue receiving messages.</p>
<p>
Click <a href="{{action_url}}">here</a> to re-connect.
</p>

View file

@ -1,7 +1,7 @@
<p>Hello,</p>
<p>Your Facebook Inbox Access has expired. </p>
<p>Please reconnect Facebook Page to continue receiving messages in Chatwoot</p>
<p>Please reconnect Facebook Page to continue receiving messages.</p>
<p>
Click <a href="{{action_url}}">here</a> to re-connect.

View file

@ -1,7 +1,7 @@
<p>Hi {{user.available_name}}</p>
<p>Time to save the world. A new conversation has been created in {{ inbox.name }}</p>
<p>{{ custom_message }}</p>
<p>
Click <a href="{{ action_url }}">here</a> to get cracking.

View file

@ -0,0 +1,8 @@
<p>Hi {{user.available_name}}</p>
<p>{{ custom_message }}</p>
<p>
Click <a href="{{ action_url }}">here</a> to get cracking.
</p>

View file

@ -0,0 +1,8 @@
<p>Hi {{user.available_name}}</p>
<p>{{ custom_message }}</p>
<p>
Click <a href="{{ action_url }}">here</a> to get cracking.
</p>

Some files were not shown because too many files have changed in this diff Show more